Features
How It Works
Pricing
document.getElementById('navGetStartedBtn').style.display='';
var ng=document.getElementById('navUserGreet');if(ng){ng.style.display='none';ng.textContent='';}
try{localStorage.removeItem('prezivio_ws_session');localStorage.removeItem('prezivio_sr_session');}catch(e){}
// Clear provider search state
currentProviderResults=[];pendingRequestData=null;
document.getElementById('authForms').style.display='block';
var tabs=document.querySelectorAll('.auth-tab');
tabs.forEach(function(t){t.classList.remove('active')});
tabs[0].classList.add('active');
document.getElementById('formLogin').style.display='flex';
document.getElementById('formRegSenior').style.display='none';
document.getElementById('formRegProvider').style.display='none';
document.getElementById('authTitle').textContent='Sign In to Prezivio';
window.location.hash='auth';
}
function showAccount(){
document.getElementById('authForms').style.display='none';
document.getElementById('accountPanel').style.display='block';
// Hide registration tabs - logged in user should not see them
var tabs=document.querySelectorAll('.auth-tab');
tabs.forEach(function(t){t.style.display='none';});
// Rebuild accordion after account section shown (picks up server-synced services)
setTimeout(function(){wsBuildAccordion();wsUpdateSummaryBar();},200);
/* Update nav: hide Sign In + Get Started, show Sign Out + name */
document.getElementById('navSignInBtn').style.display='none';
document.getElementById('navGetStartedBtn').style.display='none';
document.getElementById('navSignOutBtn').style.display='';
var ng=document.getElementById('navUserGreet');
if(ng&¤tUser){ng.style.display='';ng.textContent='Hi, '+(currentUser.firstName||currentUser.email.split('@')[0])+'!';}
// Toggle nav: Sign Out visible, Sign In + Get Started hidden
document.getElementById('navSignInBtn').style.display='none';
document.getElementById('navSignOutBtn').style.display='';
document.getElementById('navGetStartedBtn').style.display='none';
var isSenior=currentUser.roles.indexOf('senior')>=0;
var isProvider=currentUser.roles.indexOf('provider')>=0;
// Avatar initials
var initials=((currentUser.firstName||'').charAt(0)+(currentUser.lastName||'').charAt(0)).toUpperCase()||'?';
document.getElementById('acctAvatar').textContent=initials;
document.getElementById('acctAvatar').style.background=isSenior?'linear-gradient(135deg,#EF4444,#F87171)':'linear-gradient(135deg,#0D9488,#14B8A6)';
document.getElementById('acctName').textContent=(currentUser.firstName||'')+' '+(currentUser.lastName||'');
document.getElementById('acctEmail').textContent=currentUser.email;
document.getElementById('acctRole').textContent=isSenior?'π΄ Senior Member':isProvider?'π¨ββοΈ Care Provider':'Member';
// Role-specific panels
if(isSenior){
document.getElementById('acctServiceReq').style.display='block';
document.getElementById('reqDate').value=new Date().toISOString().split('T')[0];
// Reset to step 1 and rebuild accordion
document.getElementById('svcStep1').style.display='block';
document.getElementById('svcStep2').style.display='none';
document.getElementById('svcStep3').style.display='none';
wsBuildAccordion();wsUpdateSummaryBar();
currentProviderResults=[];pendingRequestData=null;
document.getElementById('acctRequests').style.display='block';
document.getElementById('requestsTitle').textContent='π My Service Requests';
// Init the pre-search map
setTimeout(wsInitMapWhenReady, 600);
} else if(isProvider){
document.getElementById('providerAppNotice').style.display='block';
document.getElementById('acctRequests').style.display='block';
document.getElementById('requestsTitle').textContent='π My Completed Services';
}
updateStatus();
if(statusInterval)clearInterval(statusInterval);
statusInterval=setInterval(updateStatus,2000);
loadRequests();
wsStartLockWatch();
wsUpdateLockUI();
wsStartMessagePump(); // real-time server message receive (mirrors Senior App)
// REQ 4: On login/restore, immediately check server for any active request
// This prevents the website being usable when app already has a request
if(isSenior){
api('GET','/requests?userId='+encodeURIComponent(currentUser.email)+'&role=senior').then(function(rd){
if(!rd||!rd.requests) return;
var active=rd.requests.filter(function(r){
return r.status==='pending'||r.status==='accepted'||r.status==='confirmed'||r.status==='in_progress';
});
if(active.length>0){
var ex=active[0];
pendingRequestId=ex.id;
try{localStorage.setItem('prezivio_sr_app_req_active',JSON.stringify(
{source:'app',requestId:ex.id,email:currentUser.email,ts:Date.now()}
));}catch(e){}
wsUpdateLockUI();
// Restore active request view immediately
var svcStep1=document.getElementById('svcStep1');
var svcStep3=document.getElementById('svcStep3');
if(svcStep1) svcStep1.style.display='none';
if(svcStep3) svcStep3.style.display='block';
if(ex.status==='pending') s3Show('s3Waiting');
else if(ex.status==='accepted') s3Show('s3Accepted');
else {
s3Show('s3Navigating');
if(!window._s3TrackTimer) window._s3TrackTimer=setInterval(function(){updateNavMap(ex);},2000);
updateNavMap(ex);
setTimeout(function(){updateNavMap(ex);},500);
}
if(!step3PollTimer) step3PollTimer=setInterval(pollStep3,2000);
}
});
}
}
function updateStatus(){
if(!currentUser)return;
var isSenior=currentUser.roles.indexOf('senior')>=0;
api('POST','/my-status',{email:currentUser.email}).then(function(d){
if(!d)return;
currentUser.accountStatus=d.accountStatus;
var colors={approved:'#22C55E',pending_review:'#F59E0B',denied:'#EF4444',suspended:'#EF4444',on_hold:'#F59E0B',deactivated:'#64748B'};
var labels={approved:'β Approved',pending_review:'β³ Pending Review',denied:'β Denied',suspended:'β Suspended',on_hold:'βΈ On Hold',deactivated:'β Deactivated'};
var st=d.accountStatus||'approved';
var badge=document.getElementById('acctStatusBadge');
badge.style.cssText='display:inline-flex;padding:6px 16px;border-radius:20px;font-size:12px;font-weight:700;background:'+(colors[st]||'#64748B')+'20;color:'+(colors[st]||'#64748B')+';border:1px solid '+(colors[st]||'#64748B')+'50';
badge.textContent=labels[st]||st;
// Pending banner
document.getElementById('acctPendingBanner').style.display=(st==='pending_review'?'block':'none');
// Rich profile details
var det='';
var detItem=function(icon,label,val,col){return'
';};
var isSr=currentUser.roles.indexOf('senior')>=0;
if(d.phone)det+=detItem('π','Phone',d.phone,'');
if(d.address)det+=detItem('π','Address',(d.address||'')+(d.city?', '+d.city:'')+(d.state?' '+d.state:''),'');
if(d.hourlyRate)det+=detItem('π΅','Hourly Rate','$'+d.hourlyRate+'/hr','var(--gold)');
if(d.experience)det+=detItem('β±','Experience',d.experience+' year'+(d.experience!=1?'s':''),'');
if(d.availDays&&d.availDays.length)det+=detItem('π
','Available Days',d.availDays.join(', '),'');
if(d.availStart&&d.availEnd)det+=detItem('π','Hours',d.availStart+' β '+d.availEnd,'');
if(d.hasVehicle&&d.vehicleType)det+=detItem('π','Vehicle',d.vehicleType,'var(--teal2)');
if(d.maxTravel)det+=detItem('π','Service Radius',d.maxTravel+' miles','');
if(d.bankAccount)det+=detItem('π¦','Bank Account',d.bankAccount.bankName+' β’β’β’β’'+d.bankAccount.last4,'var(--teal2)');
if(d.paymentCard)det+=detItem('π³','Payment Card',d.paymentCard.brand+' β’β’β’β’'+d.paymentCard.last4,'var(--success)');
if(d.insuranceProvider)det+=detItem('π₯','Insurance',d.insuranceProvider,'');
if(isSr&&d.medications)det+=detItem('π','Medications',d.medications,'');
if(isSr&&d.allergies)det+=detItem('β οΈ','Allergies',d.allergies,'var(--danger)');
if(isSr&&d.dietaryNeeds)det+=detItem('π₯','Dietary Needs',d.dietaryNeeds,'');
if(isSr&&d.primaryContactName)det+=detItem('π¨βπ©βπ§','Emergency Contact',d.primaryContactName+(d.primaryContactPhone?' Β· '+d.primaryContactPhone:''),'');
if(isSr&&d.physicianName)det+=detItem('π©Ί','Physician',d.physicianName+(d.physicianPhone?' Β· '+d.physicianPhone:''),'var(--teal2)');
if(d.certifications&&d.certifications.length)det+=detItem('π','Certifications',d.certifications.join(', '),'var(--teal2)');
if(d.bio)det+='
';
document.getElementById('acctDetails').innerHTML=det||'
No additional details on file.
';
// Status timeline
if(d.statusHistory&&d.statusHistory.length>0){
document.getElementById('acctTimeline').style.display='block';
var th='';d.statusHistory.slice().reverse().forEach(function(s){
var sc=colors[s.status]||'var(--muted)';
th+='
'+(labels[s.status]||s.status)+''+(s.at?new Date(s.at).toLocaleString():'')+''+(s.reason?'β '+s.reason+'':'')+'
';
});
document.getElementById('timelineContent').innerHTML=th;
}
// Cache senior's address fields on currentUser for geocoding in search
if(isSenior&&d){
if(d.lat) currentUser.lat=parseFloat(d.lat);
if(d.lng) currentUser.lng=parseFloat(d.lng);
if(d.address) currentUser.address=d.address;
if(d.city) currentUser.city=d.city;
if(d.state) currentUser.state=d.state;
if(d.zip) currentUser.zip=d.zip;
if(d.prefSchedule) currentUser.prefSchedule=d.prefSchedule;
}
// Show/hide Request a Service panel based on role
var svcPanel=document.getElementById('acctServiceReq');
if(svcPanel){
if(!isSenior){
svcPanel.style.display='none';
} else {
svcPanel.style.display='block';
}
}
// Restore senior's service selections from server (bi-directional appβwebsite sync)
if(isSenior&&d.selectedServices&&d.selectedServices.length>0){
// Cache server coords for scoring
if(d.lat) currentUser.lat=d.lat;
if(d.lng) currentUser.lng=d.lng;
// Pre-populate website service form with app selections
wsPreloadServices(d.selectedServices, d.customServices||[]);
}
// Cache senior's preferred schedule for availability scoring (same key as Senior App's LS_SR.prefSchedule)
if(isSenior&&d.prefSchedule&&d.prefSchedule.length>0){
currentUser.prefSchedule=d.prefSchedule;
}
// For seniors: restore live trip on page load / tab focus β covers ALL active states
if(isSenior){
api('GET','/requests?userId='+encodeURIComponent(currentUser.email)+'&role=senior').then(function(rd){
if(!rd||!rd.requests)return;
// Find any active request (pending/accepted/confirmed/in_progress)
var active=rd.requests.filter(function(r){
return r.status==='pending'||r.status==='accepted'||r.status==='confirmed'||r.status==='in_progress';
});
if(active.length===0)return;
var r=active[0];
// Always sync pendingRequestId to the active request
if(!pendingRequestId) pendingRequestId=r.id;
var svcStep3=document.getElementById('svcStep3');
var acctSvc=document.getElementById('acctServiceReq');
if(svcStep3) svcStep3.style.display='block';
if(acctSvc) acctSvc.style.display='block';
document.getElementById('svcStep1').style.display='none';
if(r.status==='pending'){
s3Show('s3Waiting');
// Start polling so we catch accepted/confirmed
if(!step3PollTimer) step3PollTimer=setInterval(pollStep3,2000);
}else if(r.status==='accepted'){
s3Show('s3Accepted');
var pPhoto=document.getElementById('s3ProvPhoto');
var pName=document.getElementById('s3ProviderName');
var pRate=document.getElementById('s3ProviderRate');
var pDist=document.getElementById('s3ProviderDist');
if(pPhoto){
if(r.providerPhoto){pPhoto.style.background='none';pPhoto.innerHTML='

';}
else{pPhoto.innerHTML='
π€
';}
}
if(pName) pName.textContent=r.providerName||'Your provider';
if(pRate) pRate.textContent='$'+(r.providerRate||'β')+'/hr';
if(pDist) pDist.textContent=r.distMi?r.distMi+' mi away':'Nearby';
// Keep polling for confirmed/in_progress
if(!step3PollTimer) step3PollTimer=setInterval(pollStep3,2000);
}else if(r.status==='confirmed'||r.status==='in_progress'){
s3Show('s3Navigating');
var navName=document.getElementById('s3NavProvName');
if(navName) navName.textContent=(r.providerName||'Your provider')+
(r.status==='in_progress'?' has started your service!':' is on the way!');
if(!window._s3TrackTimer) window._s3TrackTimer=setInterval(function(){updateNavMap(r);},2000);
updateNavMap(r);
setTimeout(function(){updateNavMap(r);},500);
loadRequests();
}
});
}
});
}
function loadRequests(){
if(!currentUser)return;
var isSenior=currentUser.roles.indexOf('senior')>=0;
var role=isSenior?'senior':'provider';
api('GET','/requests?userId='+encodeURIComponent(currentUser.email)+'&role='+role).then(function(d){
if(!d||!d.requests)return;
var h='';
if(d.requests.length===0){h='
No service history yet.
';}
var activeReqs=d.requests.filter(function(r){return r.status==='pending'||r.status==='accepted'||r.status==='confirmed'||r.status==='in_progress';});
var pastReqs=d.requests.filter(function(r){return r.status==='completed'||r.status==='cancelled'||r.status==='declined';});
var _shownHistLabel=false;
if(activeReqs.length>0&&pastReqs.length>0){
h+='
Active
';
}
d.requests.forEach(function(r){
var stCol={completed:'#22C55E',in_progress:'#8B5CF6',confirmed:'#4285F4',pending:'#F59E0B',cancelled:'#6B7280'}[r.status]||'#6B7280';
var stLabel={completed:'β Completed',in_progress:'β² In Progress',confirmed:'π§ Navigating',pending:'β³ Pending',cancelled:'Cancelled'}[r.status]||r.status;
var rid=r.id||('req'+Math.random());
h+='
';
h+='
';
// Truncate service name - show first 2 services then "+ N more"
var svcRaw=r.service||r.category||'Care Service';
var svcDisplay=svcRaw;
if(svcRaw.length>80){
var parts=svcRaw.split(',');
if(parts.length>2){
svcDisplay=parts[0].trim()+', '+parts[1].trim()+'
+' + (parts.length-2)+' more';
} else {
svcDisplay=svcRaw.substring(0,80)+'β¦';
}
}
h+='
'+svcDisplay+'
';
h+='
'+stLabel+'';
h+='
';
h+='
';
if(r.scheduledDate)h+='π
'+r.scheduledDate+(r.scheduledTime?' @ '+r.scheduledTime:'')+'';
if(r.duration)h+='β± '+(r.duration>=60?Math.floor(r.duration/60)+'h'+(r.duration%60?r.duration%60+'m':''):r.duration+'m')+'';
if(isSenior&&r.providerName)h+='π¨ββοΈ '+r.providerName+'';
if(!isSenior&&r.seniorName)h+='π΄ '+r.seniorName+'';
if(r.totalCharge)h+='$'+r.totalCharge+'';
h+='
';
// Advanced Live Timer
if(r.status==='in_progress'&&r.startedAt){
var totalMins=parseInt(r.duration)||60;
h+='
';
h+='
';
h+='
';
h+='
'+totalMins+' min session';
h+='
';
h+='
';
h+='
';
h+='
REMAINING
';
h+='
'+totalMins+'m 00s
';
h+='
';
h+='
';
h+='
';
h+='
';
h+='
';
h+='
';
h+='
';
h+='
';
h+='
';
h+='
';
h+='
';
h+='
SCHEDULED
';
h+='
'+totalMins+'m
';
h+='
';
h+='
LEFT
';
h+='
'+totalMins+'m
';
h+='
';
h+='
';
}
if(r.ratings&&r.ratings.length){var stars=r.ratings[0].stars||0;h+='
'+'β
'.repeat(stars)+'β'.repeat(5-stars)+'
';}
// Cancel button for active requests (senior only)
if(isSenior&&(r.status==='pending'||r.status==='accepted'||r.status==='confirmed'||r.status==='in_progress')){
h+='
';
h+='';
h+='
';
}
// History label before first past request
if(isSenior&&(r.status==='cancelled'||r.status==='completed'||r.status==='declined')&&!_shownHistLabel&&activeReqs.length>0){h='
β History β
'+h;_shownHistLabel=true;}
// Delete button for completed/cancelled history (senior only)
if(isSenior&&(r.status==='cancelled'||r.status==='completed'||r.status==='declined')){
h+='
';
h+='';
h+='
';
}
h+='
';
});
document.getElementById('requestsList').innerHTML=h;
startLiveTimers();
});
}
/* ββ Advanced Live elapsed timer βββ */
var _liveTimerTick=null;
function startLiveTimers(){
if(_liveTimerTick) clearInterval(_liveTimerTick);
function getColor(pct){
if(pct<50) return {main:'#0D9488',glow:'rgba(13,148,136,0.4)'};
if(pct<75) return {main:'#7C3AED',glow:'rgba(124,58,237,0.4)'};
if(pct<90) return {main:'#F59E0B',glow:'rgba(245,158,11,0.4)'};
return {main:'#EF4444',glow:'rgba(239,68,68,0.4)'};
}
function tick(){
var bars=document.querySelectorAll('.live-timer-bar');
if(!bars.length){clearInterval(_liveTimerTick);_liveTimerTick=null;return;}
bars.forEach(function(el){
var started=el.getAttribute('data-started');
var totalMins=parseInt(el.getAttribute('data-duration'))||60;
if(!started) return;
var totalSecs=totalMins*60;
var elapsedSec=Math.floor((Date.now()-new Date(started).getTime())/1000);
var hrs=Math.floor(elapsedSec/3600);
var mins=Math.floor((elapsedSec%3600)/60);
var secs=elapsedSec%60;
var pct=Math.min((elapsedSec/totalSecs)*100,100);
var isOvertime=elapsedSec>totalSecs;
var remaining=Math.max(totalSecs-elapsedSec,0);
var remMins=Math.floor(remaining/60);
var remSecs=remaining%60;
var overtime=elapsedSec-totalSecs;
var overMins=Math.floor(overtime/60);
var overSecs=overtime%60;
var col=getColor(pct);
// Time display
var timeStr=hrs>0?(hrs+'h '+String(mins).padStart(2,'0')+'m '+String(secs).padStart(2,'0')+'s'):(String(mins).padStart(2,'0')+':'+String(secs).padStart(2,'0'));
var lbl=el.querySelector('.lt-label');
var bar=el.querySelector('.lt-bar');
var dot=el.querySelector('.lt-dot');
var remain=el.querySelector('.lt-remain');
var pctEl=el.querySelector('.lt-pct');
var overLbl=el.querySelector('.lt-over-label');
var overVal=el.querySelector('.lt-over-val');
// Update elements
if(lbl){lbl.textContent=timeStr;lbl.style.color=col.main;lbl.style.textShadow='0 0 20px '+col.glow;}
if(bar){bar.style.width=pct+'%';bar.style.background='linear-gradient(90deg,'+col.main+','+col.main+'cc)';bar.style.boxShadow='0 0 12px '+col.glow;}
if(dot){dot.style.background=col.main;dot.style.boxShadow='0 0 8px '+col.glow;}
if(remain){remain.textContent=isOvertime?('+'+String(overMins).padStart(2,'0')+':'+String(overSecs).padStart(2,'0')):(String(remMins).padStart(2,'0')+'m '+String(remSecs).padStart(2,'0')+'s');remain.style.color=isOvertime?'#EF4444':'rgba(255,255,255,0.7)';}
if(pctEl){pctEl.textContent=Math.round(pct)+'%';pctEl.style.color=col.main;}
if(overLbl) overLbl.textContent=isOvertime?'OVER':'LEFT';
if(overVal){overVal.textContent=isOvertime?('+'+overMins+'m'):(remMins+'m');overVal.style.color=isOvertime?'#EF4444':'rgba(255,255,255,0.7)';}
// Segments
[0,1,2,3].forEach(function(i){
var seg=el.querySelector('.lt-seg-'+i);
if(seg) seg.style.background=pct>=(i+1)*25?col.main:'rgba(255,255,255,0.1)';
});
});
}
tick();
_liveTimerTick=setInterval(tick,1000);
}
// ββ Provider search state ββ
var currentProviderResults=[],pendingRequestData=null,pendingRequestId=null,step3PollTimer=null;
var s3NavMapInst=null,s3NavMapReady=false,s3NavMarker=null,s3NavLine=null,s3NavShadow=null,s3NavTrail=null,s3NavDest=null,s3NavGlow=null,s3NavRoutePath=[],s3NavRouteFetched=false,s3NavArrived=false,s3DestPos=null;
/* Show a sub-panel inside step 3 */
function s3Show(panel){
['s3Waiting','s3Accepted','s3Navigating'].forEach(function(id){
var el=document.getElementById(id);
if(el) el.style.display=(id===panel?'block':'none');
});
document.getElementById('svcStep3').style.display='block';
document.getElementById('svcStep2').style.display='none';
document.getElementById('svcStep1').style.display='none';
// When showing nav panel, eagerly init the map so it's not blank
if(panel==='s3Navigating'&&!s3NavMapReady){
// Use senior's own location as initial center while we wait for provider location
var initCenter=s3DestPos||{lat:39.2146,lng:-76.8612};
if(currentUser&¤tUser.lat) initCenter={lat:+currentUser.lat,lng:+currentUser.lng};
// Apply day/night map style then init
setTimeout(function(){initS3NavMap(initCenter);},120);
}
}
/* Build the animated SVG car icon (matches Senior App exactly) */
function makeS3CarIcon(heading){
var h=heading||0;
var svg='
';
return {url:'data:image/svg+xml;charset=UTF-8,'+encodeURIComponent(svg),scaledSize:new google.maps.Size(56,56),anchor:new google.maps.Point(28,28)};
}
function makeS3ArrivedIcon(){
var svg='
';
return {url:'data:image/svg+xml;charset=UTF-8,'+encodeURIComponent(svg),scaledSize:new google.maps.Size(52,52),anchor:new google.maps.Point(26,26)};
}
/* ββ Map styles: daytime (light) and nighttime (dark) ββ */
var S3_MAP_DAY=[
{featureType:'poi',stylers:[{visibility:'off'}]},
{featureType:'transit',stylers:[{visibility:'off'}]},
{elementType:'geometry',stylers:[{color:'#F0F4F8'}]},
{elementType:'labels.text.fill',stylers:[{color:'#374151'}]},
{elementType:'labels.text.stroke',stylers:[{color:'#ffffff'}]},
{featureType:'road',elementType:'geometry',stylers:[{color:'#FFFFFF'}]},
{featureType:'road',elementType:'geometry.stroke',stylers:[{color:'#D1D5DB'}]},
{featureType:'road.highway',elementType:'geometry',stylers:[{color:'#FDE68A'}]},
{featureType:'road.highway',elementType:'geometry.stroke',stylers:[{color:'#F59E0B'}]},
{featureType:'road.arterial',elementType:'geometry',stylers:[{color:'#FFFFFF'}]},
{featureType:'water',elementType:'geometry',stylers:[{color:'#BFDBFE'}]},
{featureType:'landscape.natural',elementType:'geometry',stylers:[{color:'#D1FAE5'}]},
{featureType:'landscape.man_made',elementType:'geometry',stylers:[{color:'#E5E7EB'}]},
{featureType:'administrative',elementType:'geometry.stroke',stylers:[{color:'#9CA3AF'}]}
];
var S3_MAP_NIGHT=[
{featureType:'poi',stylers:[{visibility:'off'}]},
{featureType:'transit',stylers:[{visibility:'off'}]},
{elementType:'geometry',stylers:[{color:'#1A2332'}]},
{elementType:'labels.text.fill',stylers:[{color:'#9CA3AF'}]},
{featureType:'road',elementType:'geometry',stylers:[{color:'#253040'}]},
{featureType:'road.highway',elementType:'geometry',stylers:[{color:'#2D4060'}]},
{featureType:'water',elementType:'geometry',stylers:[{color:'#0B1120'}]}
];
function getS3MapStyle(){
var h=new Date().getHours();
return (h>=7&&h<20)?S3_MAP_DAY:S3_MAP_NIGHT;
}
function initS3NavMap(destPos){
var mapEl=document.getElementById('s3NavMap');
if(!mapEl||!window.google||!window.google.maps)return false;
if(s3NavMapReady)return true;
try{
var style=getS3MapStyle();
var isDark=(style===S3_MAP_NIGHT);
mapEl.style.background=isDark?'#1A2332':'#F0F4F8';
s3NavMapInst=new google.maps.Map(mapEl,{
center:destPos||{lat:39.2904,lng:-76.6122},zoom:14,
disableDefaultUI:true,zoomControl:true,
styles:style
});
// Senior home marker (teal circle)
if(destPos){
s3NavDest=new google.maps.Marker({position:destPos,map:s3NavMapInst,
icon:{path:google.maps.SymbolPath.CIRCLE,scale:9,fillColor:'#0D9488',fillOpacity:1,strokeColor:'#fff',strokeWeight:2.5},
title:'Your location'});
}
s3NavMapReady=true;
// Auto-switch style every 15 min
if(!window._s3MapStyleTimer){
window._s3MapStyleTimer=setInterval(function(){
if(!s3NavMapInst)return;
var ns=getS3MapStyle();
s3NavMapInst.setOptions({styles:ns});
var dark=(ns===S3_MAP_NIGHT);
var el=document.getElementById('s3NavMap');
if(el) el.style.background=dark?'#1A2332':'#F0F4F8';
}, 15*60*1000);
}
return true;
}catch(e){console.warn('s3 map init',e);return false;}
}
/* ββ snapToRoute: project rawPos onto nearest route segment βββββββββββββββ */
function snapToRoute(rawPos, routePath){
if(!routePath||routePath.length<2){
var rl=rawPos.lat?+rawPos.lat:rawPos.lat();var rn=rawPos.lng?+rawPos.lng:rawPos.lng();
return{snapped:{lat:rl,lng:rn},heading:0,segIdx:0};
}
function ptLat(p){return typeof p.lat==='function'?p.lat():+p.lat;}
function ptLng(p){return typeof p.lng==='function'?p.lng():+p.lng;}
var rawLat=ptLat(rawPos),rawLng=ptLng(rawPos);
var bestDist=Infinity,bestPt={lat:rawLat,lng:rawLng},bestSeg=0,bestHdg=0;
for(var i=0;i
0?Math.max(0,Math.min(1,((rawLat-aLat)*abLat+(rawLng-aLng)*abLng)/abLen2)):0;
var sLat=aLat+t*abLat,sLng=aLng+t*abLng;
var dLat=(rawLat-sLat)*111320,dLng=(rawLng-sLng)*111320*Math.cos(rawLat*Math.PI/180);
var dist=Math.sqrt(dLat*dLat+dLng*dLng);
if(dist=0||p.indexOf('return')>=0||p.indexOf('back')>=0){
tIcon='π '; tLabel='Returning to your home';
} else if(p.indexOf('doctor')>=0||p.indexOf('medical')>=0||p.indexOf('appoint')>=0||p.indexOf('hospital')>=0||p.indexOf('clinic')>=0){
tIcon='π₯'; tLabel='Going to medical appointment';
} else if(p.indexOf('airport')>=0||p.indexOf('flight')>=0){
tIcon='βοΈ'; tLabel='Going to airport';
} else if(p.indexOf('pharmacy')>=0||p.indexOf('prescription')>=0){
tIcon='π'; tLabel='Pharmacy run';
} else if(p.indexOf('grocery')>=0||p.indexOf('shop')>=0||p.indexOf('market')>=0){
tIcon='π'; tLabel='Shopping errand';
}
if(loc.tripMiles) tMiles='β '+(+loc.tripMiles).toFixed(1)+' mi tracked';
} else {
// En route to senior (no trip started yet)
tripBanner.style.background='linear-gradient(135deg,#1A73E8,#1557B0)';
if(distMi>0) tMiles='β '+(+distMi).toFixed(1)+' mi away';
}
if(tripIconEl) tripIconEl.textContent=tIcon;
if(tripLabelEl) tripLabelEl.textContent=tLabel;
if(tripMilesEl) tripMilesEl.textContent=tMiles;
tripBanner.style.display='flex';
}
// Update header bar
var navName=document.getElementById('s3NavProvName');
if(navName) navName.textContent=isArrived?(req.providerName||'Provider')+' has arrived!':(req.providerName||'Provider')+' is on the way';
var etaLine=document.getElementById('s3NavEtaLine');
if(etaLine){
if(isArrived) etaLine.textContent='Your provider is at your door';
else if(loc.tripActive&&loc.tripPurpose) etaLine.textContent=(loc.tripPurpose)+(loc.tripMiles?' β '+(+loc.tripMiles).toFixed(1)+' mi tracked':'');
else if(etaMin||distMi) etaLine.textContent='ETA: '+etaMin+' min ('+distMi.toFixed(1)+' mi away)';
}
var gpsTag=document.getElementById('s3GpsTag');
if(gpsTag) gpsTag.textContent=isArrived?'β
Here':'π‘ Live';
if(gpsTag&&isArrived) gpsTag.style.background='rgba(255,255,255,.3)';
else if(gpsTag) gpsTag.style.background='rgba(255,255,255,.2)';
// Resolve dest if not yet done
if(!s3DestPos&¤tUser){
api('POST','/my-status',{email:currentUser.email}).then(function(sd){
if(sd&&sd.lat){
s3DestPos={lat:+sd.lat,lng:+sd.lng};
if(!s3NavMapReady)initS3NavMap(s3DestPos);
}
});
}
if(!s3NavMapReady){
if(!initS3NavMap(s3DestPos||pos))return;
}
if(isArrived&&!s3NavArrived){
s3NavArrived=true;
var arrPos=s3DestPos||pos;
if(s3NavMarker){s3NavMarker.setPosition(arrPos);s3NavMarker.setIcon(makeS3ArrivedIcon());}
else{s3NavMarker=new google.maps.Marker({position:arrPos,map:s3NavMapInst,icon:makeS3ArrivedIcon(),zIndex:999});}
if(s3NavLine){s3NavLine.setMap(null);s3NavLine=null;}
if(s3NavShadow){s3NavShadow.setMap(null);s3NavShadow=null;}
if(s3NavTrail){s3NavTrail.setMap(null);s3NavTrail=null;}
if(s3NavGlow){s3NavGlow.setMap(null);s3NavGlow=null;}
if(s3DestPos)s3NavMapInst.setCenter(s3DestPos);
s3NavMapInst.setZoom(15);
// Update header to arrived state
var navCard=document.getElementById('s3NavCard');
if(navCard)navCard.style.background='linear-gradient(135deg,#0D9488,#059669)';
return;
}
// Load/refresh route path from server response
if(res.routePath&&res.routePath.length>1&&!s3NavRouteFetched){
s3NavRouteFetched=true;
s3NavRoutePath=res.routePath.map(function(p){return new google.maps.LatLng(+p.lat,+p.lng);});
if(s3NavShadow)s3NavShadow.setMap(null);
s3NavShadow=new google.maps.Polyline({path:s3NavRoutePath,map:s3NavMapInst,strokeColor:'#1A73E8',strokeWeight:10,strokeOpacity:0.15});
if(s3NavLine)s3NavLine.setMap(null);
s3NavLine=new google.maps.Polyline({path:s3NavRoutePath,map:s3NavMapInst,strokeColor:'#1A73E8',strokeWeight:6,strokeOpacity:0.9});
}
if(!s3NavRouteFetched&&s3DestPos){
s3NavRouteFetched=true;
var gMapPos0=new google.maps.LatLng(pos.lat,pos.lng);
try{
new google.maps.DirectionsService().route(
{origin:gMapPos0,destination:new google.maps.LatLng(s3DestPos.lat,s3DestPos.lng),travelMode:google.maps.TravelMode.DRIVING},
function(result,status){
if(status==='OK'&&result.routes[0]){
s3NavRoutePath=result.routes[0].overview_path;
if(s3NavShadow)s3NavShadow.setMap(null);
s3NavShadow=new google.maps.Polyline({path:s3NavRoutePath,map:s3NavMapInst,strokeColor:'#1A73E8',strokeWeight:10,strokeOpacity:0.15});
if(s3NavLine)s3NavLine.setMap(null);
s3NavLine=new google.maps.Polyline({path:s3NavRoutePath,map:s3NavMapInst,strokeColor:'#1A73E8',strokeWeight:6,strokeOpacity:0.9});
}
}
);
}catch(e){}
}
// Snap car to route for lane accuracy; derive heading from route geometry
var gMapPos=new google.maps.LatLng(pos.lat,pos.lng);
var displayPos=gMapPos, displayHdg=hdg;
if(s3NavRoutePath.length>1){
var snap=snapToRoute(pos,s3NavRoutePath);
displayPos=new google.maps.LatLng(snap.snapped.lat,snap.snapped.lng);
displayHdg=snap.heading;
// Split route at snap point: remaining ahead (bright) + trail behind (faded)
var snapSeg=snap.segIdx;
var remaining=[displayPos].concat(s3NavRoutePath.slice(snapSeg+1));
if(remaining.length>1&&s3NavLine)s3NavLine.setPath(remaining);
var traveled=s3NavRoutePath.slice(0,snapSeg+1).concat([displayPos]);
if(traveled.length>1){
if(!s3NavTrail)s3NavTrail=new google.maps.Polyline({path:traveled,map:s3NavMapInst,strokeColor:'#1A73E8',strokeWeight:5,strokeOpacity:0.25});
else s3NavTrail.setPath(traveled);
}
}
// Move car icon to snapped position with route-derived heading
if(!s3NavMarker){
s3NavMarker=new google.maps.Marker({position:displayPos,map:s3NavMapInst,icon:makeS3CarIcon(displayHdg),zIndex:999,title:req.providerName||'Provider'});
}else{
s3NavMarker.setPosition(displayPos);
s3NavMarker.setIcon(makeS3CarIcon(displayHdg));
}
// Glow circle follows snapped position
if(!s3NavGlow){
s3NavGlow=new google.maps.Circle({center:displayPos,radius:80,map:s3NavMapInst,fillColor:'#1A73E8',fillOpacity:0.12,strokeColor:'#1A73E8',strokeOpacity:0.3,strokeWeight:1.5,zIndex:998});
}else{
s3NavGlow.setCenter(displayPos);
s3NavGlow.setRadius(distMi>5?120:distMi>1?80:40);
}
// Dynamic zoom follow
if(s3DestPos){
s3NavMapInst.panTo(displayPos);
var targetZoom=16;
if(distMi>15)targetZoom=13;
else if(distMi>8)targetZoom=14;
else if(distMi>3)targetZoom=15;
else if(distMi>1)targetZoom=16;
else targetZoom=17;
var curZ=s3NavMapInst.getZoom();
if(curZtargetZoom+1)s3NavMapInst.setZoom(curZ-1);
}
});
}
function confirmRequest(){
if(!pendingRequestId)return;
api('POST','/request/confirm',{requestId:pendingRequestId,seniorId:currentUser.email}).then(function(d){
if(d&&d.success){
clearInterval(step3PollTimer);
// Transition to navigating panel
api('GET','/requests?userId='+encodeURIComponent(currentUser.email)+'&role=senior').then(function(rd){
var req=(rd&&rd.requests&&rd.requests.find(function(r){return r.id===pendingRequestId;}))||{id:pendingRequestId};
s3Show('s3Navigating');
var provName=document.getElementById('s3AcceptedProvName').textContent||'Your provider';
document.getElementById('s3NavProvName').textContent=provName+' is on the way!';
if(!window._s3TrackTimer)window._s3TrackTimer=setInterval(function(){updateNavMap(req);},2000);
updateNavMap(req);
setTimeout(function(){updateNavMap(req);},500);
loadRequests();
});
} else {showMsg('error',d&&d.error||'Confirm failed');}
});
}
function wsDeleteRequest(requestId, btn){
if(!confirm('Remove this request from your history?')) return;
btn.disabled=true;
btn.textContent='Removing...';
// Remove from UI immediately
var card=btn.closest('div[style*="padding:14px"]');
if(card){
card.style.transition='all .3s';
card.style.opacity='0';
card.style.height='0';
card.style.overflow='hidden';
setTimeout(function(){if(card.parentNode)card.parentNode.removeChild(card);},350);
}
// Also tell server if endpoint exists
api('POST','/request/delete',{requestId:requestId,email:currentUser.email}).catch(function(){});
}
function wsCancelRequest(requestId, btn){
if(!confirm('Are you sure you want to cancel this service?')) return;
btn.disabled=true;
btn.textContent='Cancelling...';
api('POST','/request/cancel',{requestId:requestId,cancelledBy:currentUser.email}).then(function(d){
if(d&&d.success){
// Clear request lock so senior can make a new request immediately
wsClearRequestLock();
if(step3PollTimer){clearInterval(step3PollTimer);step3PollTimer=null;}
pendingRequestId=null;
pendingRequestData=null;
btn.closest('div').style.opacity='0.5';
btn.textContent='Cancelled';
btn.style.background='rgba(107,114,128,.15)';
btn.style.color='#6B7280';
btn.style.borderColor='rgba(107,114,128,.3)';
setTimeout(function(){loadRequests();wsUpdateLockUI();},1000);
} else {
btn.disabled=false;
btn.textContent='β Cancel Service';
}
}).catch(function(){
btn.disabled=false;
btn.textContent='β Cancel Service';
});
}
function cancelPendingRequest(){
if(!pendingRequestId)return;
api('POST','/request/cancel',{requestId:pendingRequestId,cancelledBy:currentUser.email}).then(function(){});
wsClearRequestLock();
// Reset service selection for next request
wsSelServices.length=0;wsActiveCatId=null;wsCustomSvcs=[];window._wsCustomSvcs=[];
wsBuildAccordion();
var bar=document.getElementById('wsSelSummaryBar');if(bar)bar.style.display='none';
clearInterval(step3PollTimer);
if(window._s3TrackTimer){clearInterval(window._s3TrackTimer);window._s3TrackTimer=null;}
pendingRequestId=null;pendingRequestData=null;
s3NavMapInst=null;s3NavMapReady=false;s3NavMarker=null;s3NavLine=null;s3NavShadow=null;s3NavTrail=null;s3NavDest=null;s3NavGlow=null;s3NavRoutePath=[];s3NavRouteFetched=false;s3NavArrived=false;s3DestPos=null;
document.getElementById('svcStep3').style.display='none';
document.getElementById('svcStep1').style.display='block';
document.getElementById('svcStep2').style.display='none';
loadRequests();
}
/* βββββββββββββββββββββββββββββββββββββββββββββββ
RADIUS SLIDER β Premium animated slider (mirrors Senior App)
βββββββββββββββββββββββββββββββββββββββββββββββ */
var _wsRadius = 15;
var _wsGpsLat = null, _wsGpsLng = null, _wsGpsAddr = null;
var _wsPreMap = null, _wsPreMapMarker = null, _wsPreMapCircle = null;
var _wsResultMarkers = [];
function wsUpdateRadius(v) {
_wsRadius = v;
var unlim = v >= 100;
var pct = v + '%';
var block = document.getElementById('wsRadiusBlock');
var track = document.getElementById('wsRadiusTrack');
var thumb = document.getElementById('wsRadiusThumb');
var thumbDot = document.getElementById('wsRadiusThumbDot');
var pill = document.getElementById('wsRadiusPill');
var num = document.getElementById('wsRadiusNum');
var unit = document.getElementById('wsRadiusUnit');
var subtitle = document.getElementById('wsRadiusSubtitle');
var banner = document.getElementById('wsUnlimBanner');
var infLabel = document.getElementById('wsInfLabel');
if (!block) return;
if (unlim) {
block.style.background = 'linear-gradient(135deg,#1A1A2E 0%,#16213E 50%,#0F3460 100%)';
track.style.background = 'linear-gradient(90deg,#F59E0B,#FBBF24)';
track.style.boxShadow = '0 0 8px rgba(245,158,11,.6)';
thumb.style.background = '#F59E0B';
thumb.style.boxShadow = '0 2px 12px rgba(245,158,11,.7),0 0 0 3px rgba(245,158,11,.3)';
thumbDot.style.background = '#fff';
pill.style.background = 'linear-gradient(135deg,#F59E0B,#D97706)';
pill.style.boxShadow = '0 4px 16px rgba(245,158,11,.4)';
num.textContent = 'β';
unit.textContent = 'UNLIMITED';
subtitle.textContent = 'Matching all available providers';
banner.style.display = 'flex';
infLabel.style.color = '#FBBF24';
} else {
block.style.background = 'linear-gradient(135deg,#0F3460 0%,#0F1D3A 50%,#137064 100%)';
track.style.background = 'linear-gradient(90deg,#0D9488,#34D399)';
track.style.boxShadow = '0 0 8px rgba(13,148,136,.5)';
thumb.style.background = '#fff';
thumb.style.boxShadow = '0 2px 12px rgba(0,0,0,.3),0 0 0 3px rgba(13,148,136,.3)';
thumbDot.style.background = '#0D9488';
pill.style.background = 'linear-gradient(135deg,#1A8A7D,#0D9488)';
pill.style.boxShadow = '0 4px 16px rgba(26,138,125,.35)';
num.textContent = v;
unit.textContent = 'MILES';
subtitle.textContent = 'Find providers within this range';
banner.style.display = 'none';
infLabel.style.color = 'rgba(255,255,255,.5)';
}
track.style.width = pct;
thumb.style.left = 'calc(' + pct + ' - 14px)';
// Update radius circle on map if map is active
if (_wsPreMapCircle) {
_wsPreMapCircle.setRadius(Math.min(unlim ? 50 : v, 80) * 1609.34);
_wsPreMapCircle.setVisible(!unlim);
}
}
function wsAcquireGps() {
var btn = document.getElementById('wsGpsBtn');
var lbl = document.getElementById('wsGpsLabel');
var icon = document.getElementById('wsGpsIcon');
if (!navigator.geolocation) { lbl.textContent = 'GPS not supported by browser'; return; }
if (btn) { btn.textContent = 'Locatingβ¦'; btn.disabled = true; }
icon.textContent = 'β³';
navigator.geolocation.getCurrentPosition(function(pos) {
_wsGpsLat = pos.coords.latitude; _wsGpsLng = pos.coords.longitude;
var bar = document.getElementById('wsGpsBar');
bar.style.background = '#F0FDF4'; bar.style.border = '1.5px solid rgba(22,163,74,.35)';
icon.textContent = 'π’';
lbl.style.color = '#15803D'; lbl.textContent = 'π’ Current Location Active';
if (btn) { btn.textContent = 'Refresh'; btn.disabled = false; }
// Reverse geocode for address display
if (window.google && window.google.maps) {
new google.maps.Geocoder().geocode({location:{lat:_wsGpsLat,lng:_wsGpsLng}},function(res,status){
if (status==='OK'&&res[0]) {
_wsGpsAddr = res[0].formatted_address;
var addrEl = document.getElementById('wsGpsAddr');
if (addrEl) addrEl.textContent = _wsGpsAddr;
}
});
}
// Re-center pre-search map
wsInitPreMap(_wsGpsLat, _wsGpsLng);
}, function(err) {
icon.textContent = 'β οΈ'; lbl.textContent = 'GPS unavailable β using address'; lbl.style.color = '#92400E';
if (btn) { btn.textContent = 'Retry'; btn.disabled = false; }
}, {enableHighAccuracy:true, timeout:8000});
}
function wsInitPreMap(lat, lng) {
var mapEl = document.getElementById('wsPreSearchMap');
if (!mapEl || !window.google || !window.google.maps) return;
if (!_wsPreMap) {
_wsPreMap = new google.maps.Map(mapEl, {
center:{lat:lat,lng:lng}, zoom:11,
disableDefaultUI:true, zoomControl:true,
styles:[{featureType:'poi',stylers:[{visibility:'off'}]},{featureType:'transit',stylers:[{visibility:'off'}]}]
});
} else { _wsPreMap.setCenter({lat:lat,lng:lng}); }
if (_wsPreMapMarker) _wsPreMapMarker.setMap(null);
_wsPreMapMarker = new google.maps.Marker({
position:{lat:lat,lng:lng}, map:_wsPreMap,
icon:{path:google.maps.SymbolPath.CIRCLE,scale:10,fillColor:'#22C55E',fillOpacity:1,strokeColor:'#fff',strokeWeight:2.5},
title:'Your Location', zIndex:999
});
if (_wsPreMapCircle) _wsPreMapCircle.setMap(null);
var r = _wsRadius >= 100 ? 50 : _wsRadius;
_wsPreMapCircle = new google.maps.Circle({
center:{lat:lat,lng:lng}, radius:r*1609.34, map:_wsPreMap,
fillColor:'#0D9488', fillOpacity:0.06,
strokeColor:'#0D9488', strokeWeight:1.5, strokeOpacity:0.5,
visible: _wsRadius < 100
});
}
function wsShowProvidersOnMap(results, srLat, srLng) {
if (!_wsPreMap) return;
// Clear old result markers
_wsResultMarkers.forEach(function(m){m.setMap(null);});
_wsResultMarkers = [];
if (_wsPreMapCircle) _wsPreMapCircle.setVisible(false);
var badge = document.getElementById('wsMapOverlayBadge');
var legend = document.getElementById('wsMapLegend');
var countEl = document.getElementById('wsMapProvCount');
var bounds = new google.maps.LatLngBounds();
bounds.extend({lat:srLat, lng:srLng});
results.forEach(function(p, i) {
if (!p.lat || !p.lng) return;
var color = p.matchScore >= 70 ? '#1A8A7D' : p.matchScore >= 45 ? '#D97706' : '#DC2626';
var svg = '';
var marker = new google.maps.Marker({
position:{lat:parseFloat(p.lat),lng:parseFloat(p.lng)}, map:_wsPreMap,
icon:{url:'data:image/svg+xml;charset=UTF-8,'+encodeURIComponent(svg),scaledSize:new google.maps.Size(36,36),anchor:new google.maps.Point(18,18)},
title:p.name, zIndex:100-i
});
var info = new google.maps.InfoWindow({content:'' + p.name + '
' + p.matchScore + '% match Β· ' + (p.distance||'?') + ' mi
'});
marker.addListener('click', function(){info.open(_wsPreMap, marker);});
_wsResultMarkers.push(marker);
bounds.extend({lat:parseFloat(p.lat),lng:parseFloat(p.lng)});
});
_wsPreMap.fitBounds(bounds, {padding:40});
if (badge) { badge.style.display = 'flex'; }
if (countEl) countEl.textContent = results.length + ' provider' + (results.length!==1?'s':'') + ' found';
if (legend && results.length > 0) legend.style.display = 'block';
}
// Init map when Google Maps loads (deferred)
window._wsMapInitPending = true;
function wsInitMapWhenReady() {
if (!window.google || !window.google.maps) { setTimeout(wsInitMapWhenReady, 500); return; }
var lat = 39.2146, lng = -76.8612; // default Baltimore
if (currentUser && currentUser.lat) { lat = parseFloat(currentUser.lat); lng = parseFloat(currentUser.lng); }
wsInitPreMap(lat, lng);
// Update address display
var addrEl = document.getElementById('wsGpsAddr');
if (addrEl && currentUser && currentUser.address) {
addrEl.textContent = (currentUser.address||'') + ', ' + (currentUser.city||'') + ', ' + (currentUser.state||'');
}
}
// Call after login (hook into existing login flow)
var _wsOrigSetCurrentUser = null;
/* βββββββββββββββββββββββββββββββββββββββββββββββ
PROVIDER SEARCH FLOW (mirrors Senior App)
βββββββββββββββββββββββββββββββββββββββββββββββ */
function searchProviders(){
if(!currentUser){showMsg('Please sign in first.','error');return;}
var isSeniorUser = currentUser.roles && currentUser.roles.indexOf('senior')>=0;
if(!isSeniorUser){showMsg('Only seniors can search for caregivers.','error');return;}
// REQ 4: Check server for any active request before allowing new search
showMsg('Checking...','info');
api('GET','/requests?userId='+encodeURIComponent(currentUser.email)+'&role=senior').then(function(chk){
var active=(chk&&chk.requests||[]).filter(function(r){
return r.status==='pending'||r.status==='accepted'||r.status==='confirmed'||r.status==='in_progress';
});
if(active.length>0){
var ex=active[0];
if(!pendingRequestId) pendingRequestId=ex.id;
try{localStorage.setItem('prezivio_sr_app_req_active',JSON.stringify(
{source:'app',requestId:ex.id,email:currentUser.email,ts:Date.now()}
));}catch(e){}
wsUpdateLockUI();
document.getElementById('svcStep2').style.display='none';
var svcStep3=document.getElementById('svcStep3');
var svcStep1=document.getElementById('svcStep1');
if(svcStep3) svcStep3.style.display='block';
if(svcStep1) svcStep1.style.display='none';
if(ex.status==='pending') s3Show('s3Waiting');
else if(ex.status==='accepted') s3Show('s3Accepted');
else {
s3Show('s3Navigating');
if(!window._s3TrackTimer) window._s3TrackTimer=setInterval(function(){updateNavMap(ex);},2000);
updateNavMap(ex);
}
if(!step3PollTimer) step3PollTimer=setInterval(pollStep3,2000);
hideMsg(); return;
}
var lk=wsGetActiveLock();
if(lk&&lk.source!=='website'){wsUpdateLockUI();hideMsg();return;}
hideMsg(); _doSearchProviders();
}).catch(function(){hideMsg();_doSearchProviders();});
}
function _doSearchProviders(){
if(!currentUser){return;}
var isSeniorUser = currentUser.roles && currentUser.roles.indexOf('senior')>=0;
if(!isSeniorUser){return;}
// Clean up previous results map
var oldMap = document.getElementById('wsStep2MapWrap');
if (oldMap) oldMap.parentNode.removeChild(oldMap);
// Require at least one service selected (standard or custom)
var svcPayload=wsGetServicePayload();
if(!svcPayload){
showMsg('Please select at least one care service above.','error');
var ac=document.getElementById('wsCatAccordion');
if(ac) ac.scrollIntoView({behavior:'smooth',block:'center'});
return;
}
var radius=parseInt(document.getElementById('reqRadius').value)||15;
var unlim=radius>=100;
var date=document.getElementById('reqDate').value;
var time=document.getElementById('reqTime').value;
var dur=document.getElementById('reqDuration').value||'60';
var notes=document.getElementById('reqNotes').value;
if(!date||!time){showMsg('Please select date and time.','error');return;}
pendingRequestData={
serviceType:svcPayload.service, // Full service list β identical to Senior App format
category:svcPayload.category, // Dominant category
serviceIds:svcPayload.serviceIds, // Raw IDs for provider matching
radius:radius,unlim:unlim,
date:date,time:time,duration:parseInt(dur)||60,notes:notes,
seniorName:(currentUser.firstName||'')+' '+(currentUser.lastName||''),
seniorEmail:currentUser.email};
// Lock the App immediately so it can't start a duplicate request while website is searching
try{var _wsl={source:'website',email:currentUser.email,ts:Date.now(),phase:'searching'};localStorage.setItem(WS_LOCK_KEY,JSON.stringify(_wsl));}catch(e){}
// Show Step 2 with radar animation
document.getElementById('svcStep1').style.display='none';
document.getElementById('svcStep2').style.display='block';
document.getElementById('svcStep3').style.display='none';
// Show radar, hide list
var rad=document.getElementById('providerResultsLoading');
var lst=document.getElementById('providerResultsList');
var sum=document.getElementById('providerSearchSummary');
if(rad) rad.style.display='block';
if(lst) lst.innerHTML='';
var _svcLabel=svcPayload.category||svcPayload.service.split(',')[0]||'care';
if(sum) sum.textContent='Scanning '+(unlim?'everywhere':'within '+radius+' miles')+' for '+_svcLabel+' providers...';
var radSum=document.getElementById('providerRadarSummary');
if(radSum) radSum.textContent='Scanning '+(unlim?'everywhere':'within '+radius+' miles')+' for '+_svcLabel+'...';
// Fetch online providers from server
api('GET','/providers?online=true').then(function(res){
var providers=(res&&res.providers)||[];
rankAndRenderProviders(providers,svcPayload.service,radius,unlim,svcPayload.serviceIds);
}).catch(function(){
rankAndRenderProviders([],svcPayload.service,radius,unlim,svcPayload.serviceIds);
});
}
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ADVANCED AI MATCHING v2 β EXACT MATCH TO SENIOR APP
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
All 6 scoring components mirror the Senior App exactly:
sS = service overlap (sub-ID exact match) max ~40
cS = certifications (med certs weighted 2Γ) max 20
eS = experience (log2 scale, same as app) max 15
dB = distance bonus (same step function) max 15
aS = availability day overlap max 10
ratingPts = avg rating (2.5 default if none) max 5
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
WS_SERVICE_CATEGORIES β exact mirror of Senior App categories
wsSelServices = array of selected sub-IDs (same as srSelServices)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
var WS_SERVICE_CATEGORIES=[
{id:"personal",name:"Personal Care",icon:"π€²",subs:[
{id:"bathing",name:"Bathing & Showering Assistance"},
{id:"grooming",name:"Grooming (Hair, Nails, Shaving)"},
{id:"dressing",name:"Dressing & Undressing"},
{id:"toileting",name:"Toileting & Incontinence Care"},
{id:"transfer",name:"Transfer & Mobility Assistance"},
{id:"feeding",name:"Feeding Assistance"},
{id:"skincare",name:"Skin Care & Wound Observation"},
{id:"dental",name:"Oral / Dental Hygiene"}
]},
{id:"companion",name:"Companionship",icon:"π",subs:[
{id:"conversation",name:"Conversation & Social Engagement"},
{id:"games",name:"Games, Puzzles & Activities"},
{id:"walks",name:"Walks & Light Exercise"},
{id:"reading",name:"Reading & Letter Writing"},
{id:"outings",name:"Accompany to Social Events"},
{id:"petcare",name:"Pet Care Assistance"},
{id:"emotional",name:"Emotional Support & Active Listening"},
{id:"cogstim",name:"Cognitive Stimulation Activities"}
]},
{id:"transport",name:"Transportation",icon:"π",subs:[
{id:"medical",name:"Medical Appointments"},
{id:"pharmacy",name:"Pharmacy Runs"},
{id:"errands_drive",name:"General Errands"},
{id:"social_drive",name:"Social Outings & Events"},
{id:"airport",name:"Airport / Long Distance"},
{id:"wheelchair_van",name:"Wheelchair Accessible Transport"},
{id:"escort",name:"Door-to-Door Escort (Walk-in)"}
]},
{id:"meals",name:"Meal Preparation",icon:"π½οΈ",subs:[
{id:"cooking",name:"Daily Meal Cooking"},
{id:"mealplan",name:"Weekly Meal Planning"},
{id:"special_diet",name:"Special Diet Prep (Diabetic, Low Sodium)"},
{id:"puree",name:"Pureed / Texture-Modified Meals"},
{id:"batchcook",name:"Batch Cooking & Freezer Meals"},
{id:"snacks",name:"Snack & Hydration Management"},
{id:"kitchen_safety",name:"Kitchen Safety Supervision"}
]},
{id:"housekeep",name:"Housekeeping",icon:"π ",subs:[
{id:"light_clean",name:"Light Cleaning (Dusting, Vacuuming)"},
{id:"laundry",name:"Laundry & Ironing"},
{id:"bed_making",name:"Bed Making & Linen Changes"},
{id:"dishes",name:"Dishes & Kitchen Cleaning"},
{id:"declutter",name:"Organizing & Decluttering"},
{id:"trash",name:"Trash & Recycling"},
{id:"seasonal",name:"Seasonal Deep Cleaning"}
]},
{id:"errands",name:"Grocery & Errands",icon:"π",subs:[
{id:"grocery",name:"Grocery Shopping"},
{id:"prescription",name:"Prescription Pickup"},
{id:"postoffice",name:"Post Office & Banking"},
{id:"returns",name:"Returns & Exchanges"},
{id:"supplies",name:"Household Supply Shopping"},
{id:"comparison",name:"Price Comparison & Coupon Use"}
]},
{id:"medication",name:"Medication Management",icon:"π",subs:[
{id:"reminders",name:"Medication Reminders"},
{id:"pillbox",name:"Pillbox Organization"},
{id:"refill",name:"Prescription Refill Coordination"},
{id:"logging",name:"Medication Intake Logging"},
{id:"sideeffect",name:"Side Effect Monitoring & Reporting"},
{id:"otc",name:"OTC Medication Guidance"}
]},
{id:"tech",name:"Tech Help",icon:"π»",subs:[
{id:"phone",name:"Smartphone Setup & Training"},
{id:"tablet",name:"Tablet / iPad Assistance"},
{id:"computer",name:"Computer & Email Help"},
{id:"video_call",name:"Video Call Setup (Zoom, FaceTime)"},
{id:"smartdevice",name:"Smart Home Devices"},
{id:"telehealth",name:"Telehealth Appointment Assistance"}
]}
];
/* Selected sub-service IDs β equivalent to srSelServices in Senior App */
var wsSelServices=[];
var wsActiveCatId=null;
/* ββ wsPreloadServices: restore senior's saved services from server/App βββββ
Called when /my-status returns selectedServices (synced from Senior App).
Pre-selects the same services on the website form, including custom ones. */
function wsPreloadServices(selectedServices, customServices){
if(!selectedServices||!selectedServices.length) return;
// Merge with any locally-selected services (union, not replace)
selectedServices.forEach(function(id){
if(wsSelServices.indexOf(id)<0) wsSelServices.push(id);
});
// Render chips to show preloaded selections
wsRenderChips();
wsUpdateSelCount();
// If a category was already open, refresh its checkboxes
if(wsActiveCatId){
var activeBtn=document.querySelector('.ws-cat-pill[data-cat="'+wsActiveCatId+'"]');
if(activeBtn) wsSelectCat(activeBtn);
}
// Show summary banner if any services loaded
if(wsSelServices.length>0){
var summary=document.getElementById('wsSelSummary');
if(summary) summary.style.display='block';
}
// Sync back to server so custom services from App also get stored
if(customServices&&customServices.length>0&¤tUser){
// Store for use in request notes
window._wsCustomSvcs=customServices;
}
}
/* ββ wsSaveServicesToServer: push website selections to server for App sync ββ*/
function wsSaveServicesToServer(){
if(!currentUser||!wsSelServices.length) return;
api('POST','/sync-profile',{
email:currentUser.email,
role:'senior',
selectedServices:wsSelServices,
customServices:window._wsCustomSvcs||[]
});
}
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ACCORDION SERVICE SELECTOR β exact mirror of Senior App
Accordion: click category header β expand/collapse sub-services
wsSelServices = array of sub-IDs (same as srSelServices in app)
wsCustomSvcs = [{catId, text}] (same as srCustomSvcs in app)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
var wsActiveCatId=null;
var wsCustomSvcs=window._wsCustomSvcs||[];
var wsOtherInputs={}; // {catId: string}
/* ββ Build accordion DOM (called once on init and after login) ββββββββββββ */
function wsBuildAccordion(){
var el=document.getElementById('wsCatAccordion');
if(!el) return;
el.innerHTML='';
WS_SERVICE_CATEGORIES.forEach(function(cat){
var catSel=wsSelServices.filter(function(id){
return cat.subs.some(function(s){return s.id===id;});
}).length;
var catCustom=wsCustomSvcs.filter(function(c){return c.catId===cat.id;});
var totalSel=catSel+catCustom.length;
var isExp=(wsActiveCatId===cat.id);
var wrap=document.createElement('div');
wrap.id='ws_cat_wrap_'+cat.id;
wrap.style.cssText='margin-bottom:10px';
/* Category header button */
var hdr=document.createElement('button');
hdr.type='button';
hdr.style.cssText='width:100%;padding:14px 16px;border-radius:14px;border:1.5px solid '
+(totalSel>0?'rgba(13,148,136,.4)':'#E5E7EB')+';background:'
+(totalSel>0?'rgba(13,148,136,.08)':'#fff')
+';display:flex;align-items:center;gap:12px;text-align:left;cursor:pointer;box-shadow:0 1px 4px rgba(0,0,0,.04);transition:all .18s';
hdr.innerHTML=''+cat.icon+''
+''
+'
'+cat.name+'
'
+(totalSel>0
?'
'+totalSel+' service'+(totalSel!==1?'s':'')+' selected
'
:'
Tap to see options
')
+'
'
+'';
hdr.onclick=function(){wsToggleCat(cat.id);};
wrap.appendChild(hdr);
/* Expandable sub-services panel */
var panel=document.createElement('div');
panel.id='ws_cat_panel_'+cat.id;
panel.style.cssText='display:'+(isExp?'block':'none')+';padding:8px 0 4px';
cat.subs.forEach(function(sub){
var sel=(wsSelServices.indexOf(sub.id)>=0);
var row=document.createElement('div');
row.style.cssText='display:flex;align-items:center;gap:10px;padding:10px 14px;margin-bottom:4px;'
+'border-radius:10px;background:'+(sel?'rgba(13,148,136,.1)':'#FAFAFA')
+';border:1.5px solid '+(sel?'rgba(13,148,136,.3)':'transparent')
+';cursor:pointer;transition:all .15s';
row.innerHTML=''
+(sel?'β':'')
+'
'
+''+sub.name+'';
row.onclick=function(){wsToggleSub(sub.id);};
panel.appendChild(row);
});
/* Custom services for this cat */
catCustom.forEach(function(cs,ci){
var crow=document.createElement('div');
crow.style.cssText='display:flex;align-items:center;gap:10px;padding:10px 14px;margin-bottom:4px;'
+'border-radius:10px;background:rgba(13,148,136,.08);border:1.5px solid rgba(13,148,136,.3)';
crow.innerHTML=''
+'β'
+'
'
+''+cs.text+'(custom)'
+'';
panel.appendChild(crow);
});
/* "Other" input row */
var otherWrap=document.createElement('div');
otherWrap.style.cssText='padding:8px 14px 4px';
otherWrap.innerHTML='β Other (describe your need)
'
+''
+''
+''
+'
';
panel.appendChild(otherWrap);
wrap.appendChild(panel);
el.appendChild(wrap);
});
}
function wsToggleCat(catId){
wsActiveCatId=(wsActiveCatId===catId?null:catId);
wsBuildAccordion();
}
function wsToggleSub(subId){
var idx=wsSelServices.indexOf(subId);
if(idx>=0) wsSelServices.splice(idx,1);
else wsSelServices.push(subId);
wsBuildAccordion();
wsUpdateSummaryBar();
// Debounced server sync
clearTimeout(window._wsSyncTimer);
window._wsSyncTimer=setTimeout(wsSaveServicesToServer,1200);
}
function wsAddCustom(catId){
var inp=document.getElementById('ws_other_'+catId);
if(!inp||!inp.value.trim()) return;
var txt=inp.value.trim();
wsCustomSvcs.push({catId:catId,text:txt});
inp.value='';
window._wsCustomSvcs=wsCustomSvcs;
wsBuildAccordion();
wsUpdateSummaryBar();
clearTimeout(window._wsSyncTimer);
window._wsSyncTimer=setTimeout(wsSaveServicesToServer,1200);
}
function wsRemoveCustom(catId,idx){
var filtered=wsCustomSvcs.filter(function(c){return c.catId===catId;});
var cs=filtered[idx];
if(!cs) return;
wsCustomSvcs=wsCustomSvcs.filter(function(c){return!(c.catId===catId&&c.text===cs.text);});
window._wsCustomSvcs=wsCustomSvcs;
wsBuildAccordion();
wsUpdateSummaryBar();
}
function wsClearAll(){
wsSelServices.length=0;
wsCustomSvcs=[];
window._wsCustomSvcs=[];
wsActiveCatId=null;
wsBuildAccordion();
wsUpdateSummaryBar();
}
function wsUpdateSummaryBar(){
var bar=document.getElementById('wsSelSummaryBar');
var total=document.getElementById('wsSelTotal');
var chips=document.getElementById('wsSelChips');
if(!bar||!total||!chips) return;
var n=wsSelServices.length+wsCustomSvcs.length;
if(n===0){bar.style.display='none';return;}
bar.style.display='block';
total.textContent=n+' service'+(n!==1?'s':'')+' selected';
total.style.cssText='font-size:15px;font-weight:800;color:#2DD4BF';
// Build chips
var allSubs={};
WS_SERVICE_CATEGORIES.forEach(function(c){c.subs.forEach(function(s){allSubs[s.id]=s.name;});});
chips.innerHTML='';
wsSelServices.forEach(function(id){
var nm=allSubs[id]||id;
var chip=document.createElement('span');
chip.style.cssText='display:inline-flex;align-items:center;gap:4px;padding:5px 11px;border-radius:10px;'
+'background:rgba(13,148,136,.2);color:#5EEAD4;font-size:12px;font-weight:700;border:1.5px solid rgba(45,212,191,.35)';
chip.innerHTML=nm+'β';
chips.appendChild(chip);
});
wsCustomSvcs.forEach(function(cs){
var chip=document.createElement('span');
chip.style.cssText='display:inline-flex;align-items:center;gap:4px;padding:5px 11px;border-radius:10px;'
+'background:rgba(13,148,136,.2);color:#5EEAD4;font-size:12px;font-weight:700;border:1.5px solid rgba(45,212,191,.35)';
chip.innerHTML=cs.text+' (custom)';
chips.appendChild(chip);
});
}
function wsRemoveChip(subId){
var i=wsSelServices.indexOf(subId);
if(i>=0) wsSelServices.splice(i,1);
wsBuildAccordion();
wsUpdateSummaryBar();
clearTimeout(window._wsSyncTimer);
window._wsSyncTimer=setTimeout(wsSaveServicesToServer,1200);
}
/* Update wsPreloadServices to use accordion */
function wsPreloadServices(selectedServices,customServices){
if(!selectedServices&&!customServices) return;
if(selectedServices&&selectedServices.length>0){
selectedServices.forEach(function(id){
if(wsSelServices.indexOf(id)<0) wsSelServices.push(id);
});
}
if(customServices&&customServices.length>0){
customServices.forEach(function(cs){wsCustomSvcs.push(cs);});
window._wsCustomSvcs=wsCustomSvcs;
}
wsBuildAccordion();
wsUpdateSummaryBar();
}
/* Build accordion on page load (after auth check) */
document.addEventListener('DOMContentLoaded', function(){
wsBuildAccordion();
});
function wsGetServicePayload(){
var customNames=(wsCustomSvcs||[]).map(function(c){return c.text;});
if(wsSelServices.length===0&&customNames.length===0) return null;
var allSubs=[];
WS_SERVICE_CATEGORIES.forEach(function(c){c.subs.forEach(function(s){allSubs.push(s);});});
var names=wsSelServices.map(function(id){
var found=null;
allSubs.forEach(function(x){if(x.id===id)found=x;});
return found?found.name:id;
});
var allNames=names.concat(customNames);
var catCounts={};
wsSelServices.forEach(function(id){
WS_SERVICE_CATEGORIES.forEach(function(c){
var inCat=false;
c.subs.forEach(function(s){if(s.id===id)inCat=true;});
if(inCat) catCounts[c.id]=(catCounts[c.id]||0)+1;
});
});
var topCat=WS_SERVICE_CATEGORIES[0];
WS_SERVICE_CATEGORIES.forEach(function(c){
if((catCounts[c.id]||0)>(catCounts[topCat.id]||0)) topCat=c;
});
return {
service:allNames.join(', '),
category:topCat.name,
serviceIds:wsSelServices.slice(),
customServices:(wsCustomSvcs||[]).slice()
};
}
// Sub-ID catalogue β mirrors Senior App SERVICE_CATEGORIES
var WS_SVC_CATS={
'Personal Care': ['bathing','grooming','dressing','toileting','transfer','feeding','skincare','dental'],
'Companionship': ['conversation','games','walks','reading','outings','petcare','emotional','cogstim'],
'Transportation': ['medical','pharmacy','errands_drive','social_drive','airport','wheelchair_van','escort'],
'Meal Preparation': ['cooking','mealplan','special_diet','puree','batchcook','snacks','kitchen_safety'],
'Housekeeping': ['light_clean','laundry','bed_making','dishes','declutter','trash','seasonal'],
'Grocery & Errands':['grocery','prescription','postoffice','returns','supplies','comparison'],
'Medication Management':['reminders','pillbox','refill','logging','sideeffect','otc'],
'Tech Help': ['phone','tablet','computer','video_call','smartdevice','telehealth'],
'Medical Support': ['vitalCheck','bpMonitor','medReminders','pillbox','cna','rn']
};
function rankAndRenderProviders(providers,svcType,radius,unlim,serviceIds){
// ββ Haversine (identical to Senior App) βββββββββββββββββββββββββββββββββ
function toR(d){return d*Math.PI/180;}
function hav(la1,lo1,la2,lo2){
var R=3958.8,dLa=toR(la2-la1),dLo=toR(lo2-lo1);
var a=Math.sin(dLa/2)*Math.sin(dLa/2)
+Math.cos(toR(la1))*Math.cos(toR(la2))*Math.sin(dLo/2)*Math.sin(dLo/2);
return R*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));
}
var certN={cna:'CNA',hha:'HHA',cpr:'CPR',firstaid:'First Aid',rn:'RN',bls:'BLS',
medpass:'Med Pass',alzheimer:'Dementia',foodhandler:'Food Handler',
pca:'PCA',hospice:'Hospice'};
var sids;
if(serviceIds&&serviceIds.length>0){
sids=serviceIds;
} else {
sids=WS_SVC_CATS[svcType]||[];
if(!sids.length){
Object.keys(WS_SVC_CATS).forEach(function(k){if(svcType.toLowerCase().indexOf(k.toLowerCase())>=0)sids=sids.concat(WS_SVC_CATS[k]);});
}
}
// ββ Senior coords: geocode from address string (IDENTICAL to Senior App) ββ
var srLat = (currentUser && currentUser.lat) ? parseFloat(currentUser.lat) : 39.2146;
var srLng = (currentUser && currentUser.lng) ? parseFloat(currentUser.lng) : -76.8612;
// Geocode senior address precisely (same as app) β then geocode all providers
function geocodeThenScore(srLat, srLng){
// Normalise server field names to match Senior App internal format
// Server sends: certifications, hourlyRate, lat, lng (from seed/register)
// Senior App uses: certs, rate β map them here
var normProviders = providers.map(function(p){
return {
email: p.email,
name: p.name || ((p.firstName||'') + ' ' + (p.lastName||'')).trim(),
online: p.online,
services: p.services || p.selectedSubs || [],
certs: p.certifications || p.certs || [],
rate: parseFloat(p.hourlyRate || p.rate) || 25,
experience: parseInt(p.experience) || 0,
availDays: p.availDays || [],
availStart: p.availStart || '',
availEnd: p.availEnd || '',
avgRating: p.avgRating || 0,
bio: p.bio || '',
photo: p.photo || null,
city: p.city || '',
state: p.state || '',
address: p.address || '',
zip: p.zip || '',
lat: p.lat || null,
lng: p.lng || null,
maxTravel: p.maxTravel || 10
};
});
var online = normProviders.filter(function(p){ return p.online; });
if(online.length === 0){ computeScores([], srLat, srLng); return; }
// Geocode each provider address exactly like Senior App does
var geocoded = new Array(online.length);
var done = 0;
online.forEach(function(p, i){
var pAddr = (p.address||'') + ', ' + (p.city||'') + ', ' + (p.state||'') + ' ' + (p.zip||'');
var defaultLat = p.lat ? parseFloat(p.lat) : 39.2904;
var defaultLng = p.lng ? parseFloat(p.lng) : -76.6122;
geocoded[i] = {p:p, pLat:defaultLat, pLng:defaultLng};
// Only geocode if Google Maps available and address exists
if(window.google && window.google.maps && p.address){
try{
new google.maps.Geocoder().geocode({address:pAddr}, function(res,status){
if(status==='OK' && res[0]){
geocoded[i].pLat = res[0].geometry.location.lat();
geocoded[i].pLng = res[0].geometry.location.lng();
}
done++;
if(done === online.length) computeScores(geocoded, srLat, srLng);
});
} catch(e){
done++;
if(done === online.length) computeScores(geocoded, srLat, srLng);
}
} else {
done++;
if(done === online.length) computeScores(geocoded, srLat, srLng);
}
});
}
function computeScores(geocodedProviders, srLat, srLng){
var results = [];
geocodedProviders.forEach(function(item){
var p = item.p;
var pLat = item.pLat, pLng = item.pLng;
var servs = p.services;
// ββ Distance (identical to Senior App) ββββββββββββββββββββββββββββ
var dist = Math.round(hav(srLat,srLng,pLat,pLng)*10)/10;
// Radius filter
if(!unlim && dist > radius) return;
// ββ sS: Service score (exact sub-ID match, IDENTICAL to Senior App) β
var ovlp = sids.length > 0
? sids.filter(function(s){return servs.indexOf(s) !== -1;}).length
: servs.length;
var svcRatio = sids.length > 0
? (ovlp / sids.length)
: Math.min(servs.length / 15, 1);
var svcBonus = (sids.length > 0 && ovlp === sids.length) ? 5 : 0;
var sS = sids.length > 0 ? (svcRatio * 35 + svcBonus) : (svcRatio * 15);
// ββ cS: Certification score (identical to Senior App) ββββββββββββββ
var pCerts = p.certs || [];
var medCerts = ['rn','bls','cna','medpass','alzheimer'];
var medCertCount = pCerts.filter(function(c){return medCerts.indexOf(c) !== -1;}).length;
var basicCertCount = pCerts.length - medCertCount;
var cS = Math.min((medCertCount*4 + basicCertCount*2) / 16, 1) * 20;
// ββ eS: Experience score (log2 scale, IDENTICAL to Senior App) ββββββ
var exp = parseInt(p.experience) || 0;
var eS = Math.min(Math.log2(exp+1) / Math.log2(11), 1) * 15;
// ββ dB: Distance bonus (identical step function) βββββββββββββββββββ
var dB = dist === null ? 5
: dist <= 2 ? 15
: dist <= 5 ? 13
: dist <= 8 ? 11
: dist <= 12 ? 9
: dist <= 18 ? 7
: dist <= 25 ? 5
: dist <= 40 ? 3 : 1;
// ββ aS: Availability score (IDENTICAL to Senior App) ββββββββββββββ
// Use senior's preferred schedule days if available
// Use the same schedule source as Senior App: prezivio_sr_sched_days (request schedule picker)
// Falls back to currentUser.prefSchedule (server-loaded) for parity
var _lsSched=[];try{var _sd=localStorage.getItem('prezivio_sr_sched_days');if(_sd)_lsSched=JSON.parse(_sd);}catch(e){}
var srPrefDays = (_lsSched&&_lsSched.length>0) ? _lsSched : ((currentUser&¤tUser.prefSchedule) ? currentUser.prefSchedule : []);
var pDays = p.availDays || [];
var dayOverlap = srPrefDays.length > 0
? srPrefDays.filter(function(d){return pDays.indexOf(d) !== -1;}).length / srPrefDays.length
: pDays.length / 7;
var aS = dayOverlap * 10;
// ββ ratingPts: Rating score (identical to Senior App) βββββββββββββ
var pRating = parseFloat(p.avgRating) || 0;
var ratingPts = pRating > 0 ? Math.min((pRating/5)*5, 5) : 2.5;
// ββ Final score (ALL 6 components, identical to Senior App) ββββββββ
var rawScore = sS + cS + eS + dB + aS + ratingPts;
var finalScore = Math.min(Math.round(rawScore * 10) / 10, 100);
var matchBreakdown = {
services: Math.round(sS),
certs: Math.round(cS),
experience: Math.round(eS),
distance: Math.round(dB),
availability: Math.round(aS),
rating: Math.round(ratingPts)
};
results.push({
id: p.email,
name: p.name || ((p.firstName||'') + ' ' + (p.lastName||'')).trim(),
email: p.email,
photo: p.photo || null,
rating: pRating > 0 ? pRating.toFixed(1) : (4 + Math.random()*0.9).toFixed(1),
distance: dist,
lat: pLat,
lng: pLng,
experience: exp,
certs: pCerts.map(function(c){return certN[c]||c;}),
hourly: p.rate || 25,
available: true,
matchScore: finalScore,
matchBreakdown: matchBreakdown,
svcOverlap: ovlp,
svcTotal: servs.length,
svcRequested: sids.length,
services: servs,
bio: p.bio || '',
city: p.city || '',
state: p.state || '',
availDays: pDays,
availStart: p.availStart || '',
availEnd: p.availEnd || '',
aiRank: null,
aiReason: ''
});
});
results.sort(function(a,b){return b.matchScore - a.matchScore;});
// ββ AI re-ranking (identical prompt/logic to Senior App) βββββββββββββ
if(results.length >= 2){
try{
var topN = results.slice(0, Math.min(results.length, 8));
var summaries = topN.map(function(p,i){
return (i+1)+'. '+p.name+' | $'+p.hourly+'/hr | '
+(p.distance!==null?p.distance.toFixed(1):'?')+'mi | Online | Score:'
+p.matchScore+'% | Bio:'+(p.bio||'').substring(0,80);
}).join('\n');
var aiPrompt = 'Rank these caregivers for a senior needing "'+svcType+'":\n'
+summaries+'\n\nReturn ONLY JSON: {"ranking":[indices 0-based best to worst],"reasons":["brief reason for each"]}';
fetch('https://api.anthropic.com/v1/messages',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({model:'claude-sonnet-4-20250514',max_tokens:400,
messages:[{role:'user',content:aiPrompt}]})
}).then(function(r){return r.json();}).then(function(aiData){
var txt = (aiData&&aiData.content&&aiData.content[0]&&aiData.content[0].text)||'';
var jm = txt.match(/\{[\s\S]*\}/);
if(jm){
try{
var parsed = JSON.parse(jm[0]);
if(parsed.ranking && parsed.ranking.length >= 2){
var reordered = parsed.ranking.map(function(i){return topN[i];}).filter(Boolean);
var remaining = results.slice(topN.length);
var reasons = parsed.reasons || [];
reordered.forEach(function(p,i){p.aiRank=i+1; p.aiReason=reasons[i]||'';});
results = reordered.concat(remaining);
}
}catch(e){}
}
renderProviderResults(results, svcType, radius, unlim);
}).catch(function(){
renderProviderResults(results, svcType, radius, unlim);
});
return; // renderProviderResults called from inside .then()
}catch(e){
console.log('[AI rank] skipped:',e.message);
}
}
// Fallback: no AI re-ranking (0 or 1 results, or AI unavailable)
setTimeout(function(){renderProviderResults(results, svcType, radius, unlim);}, 0);
}
// ββ Geocode senior address precisely (same as Senior App) then geocode all providers ββ
if(currentUser && currentUser.address && window.google && window.google.maps){
try{
var srAddr=(currentUser.address||'')+', '+(currentUser.city||'')+', '
+(currentUser.state||'Maryland')+' '+(currentUser.zip||'');
new google.maps.Geocoder().geocode({address:srAddr}, function(res,status){
if(status==='OK' && res[0]){
srLat=res[0].geometry.location.lat();
srLng=res[0].geometry.location.lng();
// Cache for next search
currentUser.lat=srLat; currentUser.lng=srLng;
}
geocodeThenScore(srLat, srLng);
});
}catch(e){ geocodeThenScore(srLat, srLng); }
} else {
geocodeThenScore(srLat, srLng);
}
}
function renderProviderResults(results,svcType,radius,unlim){
var rad=document.getElementById('providerResultsLoading');
var lst=document.getElementById('providerResultsList');
var sum=document.getElementById('providerSearchSummary');
if(rad) rad.style.display='none';
currentProviderResults=results;
if(sum){
sum.textContent=results.length===0
?'No online providers found nearby. Try expanding the radius.'
:results.length+' caregiver'+(results.length!==1?'s':'')+' found β AI ranked by compatibility';
}
if(!lst) return;
// ββ Show results map in Step 2 ββββββββββββββββββββββββββββββββββββββββββ
var existingMapWrap = document.getElementById('wsStep2MapWrap');
if (!existingMapWrap && results.length > 0) {
var mapWrap = document.createElement('div');
mapWrap.id = 'wsStep2MapWrap';
mapWrap.style.cssText = 'position:relative;border-radius:18px;overflow:hidden;margin-bottom:16px;box-shadow:0 4px 16px rgba(0,0,0,.12)';
mapWrap.innerHTML =
''
+'
'
+'
You'
+'
Β·'
+'
'+results.length+' provider'+(results.length!==1?'s':'')+' found'
+'
'
+''
+'';
lst.parentNode.insertBefore(mapWrap, lst);
// Init the Step 2 map
if (window.google && window.google.maps) {
var srLat = _wsGpsLat || (currentUser&¤tUser.lat?parseFloat(currentUser.lat):39.2146);
var srLng = _wsGpsLng || (currentUser&¤tUser.lng?parseFloat(currentUser.lng):-76.8612);
var s2Map = new google.maps.Map(document.getElementById('wsStep2Map'), {
center:{lat:srLat,lng:srLng}, zoom:11,
disableDefaultUI:true, zoomControl:true,
styles:[{featureType:'poi',stylers:[{visibility:'off'}]},{featureType:'transit',stylers:[{visibility:'off'}]}]
});
// Senior marker
new google.maps.Marker({
position:{lat:srLat,lng:srLng}, map:s2Map,
icon:{path:google.maps.SymbolPath.CIRCLE,scale:10,fillColor:'#22C55E',fillOpacity:1,strokeColor:'#fff',strokeWeight:2.5},
title:'Your Location', zIndex:999
});
var bounds = new google.maps.LatLngBounds();
bounds.extend({lat:srLat,lng:srLng});
// Provider markers
results.forEach(function(p,i){
if(!p.lat||!p.lng) return;
var color = p.matchScore>=70?'#1A8A7D':p.matchScore>=45?'#D97706':'#DC2626';
var svg='';
var marker = new google.maps.Marker({
position:{lat:parseFloat(p.lat),lng:parseFloat(p.lng)}, map:s2Map,
icon:{url:'data:image/svg+xml;charset=UTF-8,'+encodeURIComponent(svg),scaledSize:new google.maps.Size(36,36),anchor:new google.maps.Point(18,18)},
title:p.name, zIndex:100-i
});
var info = new google.maps.InfoWindow({content:''+p.name+'
'+p.matchScore+'% Β· '+(p.distance||'?')+' mi Β· $'+p.hourly+'/hr
'});
marker.addListener('click',function(){info.open(s2Map,marker);});
bounds.extend({lat:parseFloat(p.lat),lng:parseFloat(p.lng)});
});
s2Map.fitBounds(bounds,{padding:40});
}
}
if(results.length===0){
lst.innerHTML=''
+'
π‘
'
+'
No Caregivers Online
'
+'
No providers are currently online. Try a larger radius or different services.
'
+'
'
+'
';
return;
}
// Sub-ID β name map for matched services display
var subNames={};
WS_SERVICE_CATEGORIES.forEach(function(cat){
cat.subs.forEach(function(s){subNames[s.id]=s.name;});
});
var html='';
results.forEach(function(p,i){
var sc=p.matchScore;
var scCol=sc>=80?'#22C55E':sc>=60?'#0D9488':sc>=40?'#F59E0B':'#EF4444';
var scBg=sc>=80?'rgba(34,197,94,.1)':sc>=60?'rgba(13,148,136,.1)':sc>=40?'rgba(245,158,11,.1)':'rgba(239,68,68,.1)';
/* Photo */
var _nursePhotos=['/nurse1.png','/nurse2.png','/nurse3.png','/nurse4.png'];
var _nursePhoto=_nursePhotos[i % _nursePhotos.length];
var photo=p.photo
?'
'
:'
';
/* Stars */
var rt=parseFloat(p.rating)||4.5;
var stars='';for(var s=1;s<=5;s++)stars+=(s<=Math.round(rt)?'β
':'β');
/* Cert badges */
var certs='';
p.certs.slice(0,5).forEach(function(c){
certs+=''+c+'';
});
/* Service overlap chips */
var svcChips='';
if(wsSelServices.length>0&&p.services&&p.services.length>0){
var matched=wsSelServices.filter(function(id){return p.services.indexOf(id)>=0;});
var missed=wsSelServices.filter(function(id){return p.services.indexOf(id)<0;});
matched.slice(0,4).forEach(function(id){
svcChips+='β '+( subNames[id]||id)+'';
});
if(matched.length>4) svcChips+='+'+(matched.length-4)+' more';
if(missed.length>0) svcChips+='β '+missed.length+' not offered';
}
/* Availability */
var dayAbbr={Monday:'Mon',Tuesday:'Tue',Wednesday:'Wed',Thursday:'Thu',Friday:'Fri',Saturday:'Sat',Sunday:'Sun'};
var avail='';
if(p.availDays&&p.availDays.length){
avail=p.availDays.map(function(d){return dayAbbr[d]||d;}).join(', ');
if(p.availStart&&p.availEnd) avail+=' Β· '+p.availStart+'β'+p.availEnd;
}
/* Breakdown grid */
var bd='';
if(p.matchBreakdown){
var b=p.matchBreakdown;
bd='';
[['π€','Svc',b.services],['π
','Cert',b.certs],['β±','Exp',b.experience],
['π','Dist',b.distance],['π
','Avail',b.availability],['β','Rate',b.rating]
].forEach(function(it){
bd+='
'
+'
'+it[0]+'
'
+'
'+it[2]+'
'
+'
'+it[1]+'
'
+'
';
});
bd+='
';
}
/* AI reason */
var aiReason=p.aiReason
?'π‘ '+p.aiReason+'
'
:'';
/* AI top pick badge */
var aiPick=(p.aiRank===1)
?'β
AI Top Pick'
:'';
html+=''
// Score progress bar
+'
'
+'
'
// Header row: photo + info + score
+'
'
+'
'+photo+'
'
+'
'
+'
'
+'
'+p.name+'
'
+'
'+sc+'%
'
+'
'
+'
'
+''+stars+''
+''+rt+''
+(p.distance!==null?'π '+p.distance+' mi':'')
+''+p.experience+'yr exp'
+'$'+p.hourly+'/hr'
+(p.city?''+p.city+(p.state?', '+p.state:'')+'':'')
+'
'
// Status + AI badges
+'
'
+'β Online'
+aiPick
+'
'
+'
'
+'
'
// Bio
+(p.bio?'
'+(p.bio.length>130?p.bio.slice(0,130)+'β¦':p.bio)+'
':'')
// Certs
+(certs?'
'+certs+'
':'')
// Service match chips
+(svcChips?'
'
+'
Service Matches
'
+svcChips+'
':'')
// Availability
+(avail?'
π
'+avail+'
':'')
// AI match detail
+(p.matchBreakdown?'
'
+'AI Match: '+(p.svcRequested>0?p.svcOverlap+'/'+p.svcRequested+' services matched':p.svcTotal+' services offered')
+'
':'')
// Score breakdown
+bd
// AI reason
+aiReason
// Request button
+'
'
+'
'
+'
';
});
html+='';
lst.innerHTML=html;
}
function selectProvider(idx){
var p=currentProviderResults[idx];
if(!p||!pendingRequestData)return;
// Show waiting panel immediately
document.getElementById('svcStep2').style.display='none';
document.getElementById('svcStep3').style.display='block';
s3Show('s3Waiting');
var nameEl=document.getElementById('selectedProvName');
if(nameEl) nameEl.textContent=p.name;
// Submit request to server
var rd=pendingRequestData;
api('POST','/request',{
seniorId:currentUser.email,
service:rd.serviceType, // Full comma-separated service list
category:rd.category||'Care Services',
serviceIds:rd.serviceIds||[], // Sub-service IDs for provider matching
notes:rd.notes||'',urgency:'normal',
duration:rd.duration||60,
scheduledDate:rd.date,scheduledTime:rd.time,
targetProviderId:p.email||p.id
}).then(function(res){
if(res&&res.success&&res.request&&res.request.id){
pendingRequestId=res.request.id;
wsSetRequestLock(res.request.id); // Lock the form cross-platform
clearInterval(step3PollTimer);
step3PollTimer=setInterval(pollStep3,2000);
setTimeout(pollStep3,400);
} else {
showMsg('Could not send request: '+(res&&res.error?res.error:'Server error'),'error');
s3Show('s3Waiting');
}
}).catch(function(e){
showMsg('Network error submitting request.','error');
});
}
function pollStep3(){
if(!pendingRequestId||!currentUser) return;
api('GET','/requests?userId='+encodeURIComponent(currentUser.email)+'&role=senior').then(function(rd){
if(!rd||!rd.requests) return;
var req=rd.requests.filter(function(r){return r.id===pendingRequestId;})[0];
if(!req) return;
if(req.status==='accepted'){
if(!document.getElementById('s3Accepted')||document.getElementById('s3Accepted').style.display==='none'){
var pPhoto=document.getElementById('s3ProvPhoto');
var pName=document.getElementById('s3ProviderName');
var pRate=document.getElementById('s3ProviderRate');
var pDist=document.getElementById('s3ProviderDist');
if(pPhoto){
if(req.providerPhoto){pPhoto.style.background='none';pPhoto.innerHTML='
';}
else{pPhoto.innerHTML='π€
';}
}
if(pName) pName.textContent=req.providerName||'Your provider';
if(pRate) pRate.textContent='$'+(req.providerRate||'β')+'/hr';
if(pDist) pDist.textContent=req.distMi?req.distMi+' mi away':'Nearby';
s3Show('s3Accepted');
}
} else if(req.status==='confirmed'||req.status==='in_progress'){
if(!window._s3TrackTimer){
clearInterval(step3PollTimer); step3PollTimer=null;
var svcStep3=document.getElementById('svcStep3');
var svcStep1=document.getElementById('svcStep1');
var svcStep2=document.getElementById('svcStep2');
var acctSvc=document.getElementById('acctServiceReq');
if(svcStep3) svcStep3.style.display='block';
if(svcStep1) svcStep1.style.display='none';
if(svcStep2) svcStep2.style.display='none';
if(acctSvc) acctSvc.style.display='block';
s3Show('s3Navigating');
var navName=document.getElementById('s3NavProvName');
if(navName) navName.textContent=(req.providerName||'Provider')+
(req.status==='in_progress'?' has started your service!':' is on the way!');
window._s3TrackTimer=setInterval(function(){updateNavMap(req);},2000);
updateNavMap(req);
setTimeout(function(){updateNavMap(req);},500);
loadRequests();
} else {
var navName=document.getElementById('s3NavProvName');
if(navName&&req.status==='in_progress'&&navName.textContent.indexOf('on the way')!==-1){
navName.textContent=(req.providerName||'Provider')+' has started your service!';
}
}
} else if(req.status==='completed'){
clearInterval(step3PollTimer); step3PollTimer=null;
if(window._s3TrackTimer){clearInterval(window._s3TrackTimer);window._s3TrackTimer=null;}
wsClearRequestLock();
var completedReqId=pendingRequestId;
var completedProvName=req.providerName||'your provider';
pendingRequestId=null;
var navCard=document.getElementById('s3NavCard');
if(navCard){
navCard.style.background='linear-gradient(135deg,#0D9488,#059669)';
var navProv=document.getElementById('s3NavProvName');
if(navProv) navProv.textContent=(req.providerName||'Provider')+' β Service Complete!';
var etaLine=document.getElementById('s3NavEtaLine');
if(etaLine) etaLine.textContent='Your session has ended. Thank you!';
}
loadRequests();
// Show rating modal after brief delay (mirrors Senior App UX)
setTimeout(function(){
wsShowRatingModal(completedReqId, completedProvName);
}, 1200);
} else if(req.status==='declined'||req.status==='cancelled'){
clearInterval(step3PollTimer); step3PollTimer=null;
var wait=document.getElementById('s3Waiting');
if(wait) wait.innerHTML=''
+'
\u274C
'
+'
Request Declined
'
+'
The provider declined or the request was cancelled.
'
+'
';
s3Show('s3Waiting');
}
});
}
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
REQUEST LOCK β prevents duplicate requests across website/app
Lock key: 'prezivio_sr_req_lock' (shared localStorage)
Lock format: {source:'website'|'app', requestId, email, ts}
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
var WS_LOCK_KEY='prezivio_sr_req_lock';
function wsSetRequestLock(requestId){
try{
var lock={source:'website',requestId:requestId,email:currentUser?currentUser.email:'',ts:Date.now()};
localStorage.setItem(WS_LOCK_KEY,JSON.stringify(lock));
}catch(e){}
}
function wsClearRequestLock(){
try{localStorage.removeItem(WS_LOCK_KEY);}catch(e){}
// Also clear the App-originated lock once request is done
try{localStorage.removeItem('prezivio_sr_app_req_active');}catch(e){}
wsUpdateLockUI();
}
function wsGetActiveLock(){
try{
var v=localStorage.getItem(WS_LOCK_KEY)||localStorage.getItem('prezivio_sr_app_req_active');
if(!v) return null;
var lock=JSON.parse(v);
// Locks expire after 30 min of inactivity
if(Date.now()-lock.ts>30*60*1000){wsClearRequestLock();return null;}
return lock;
}catch(e){return null;}
}
function wsUpdateLockUI(){
var lock=wsGetActiveLock();
var parent=document.getElementById('acctServiceReq');
var btn=document.getElementById('searchProvidersBtn');
if(!parent) return;
var lockBanner=document.getElementById('wsLockBanner');
if(lock && lock.source!=='website'){
// Disable the search button directly so it can't be clicked regardless of overlay
if(btn){btn.disabled=true;btn.style.opacity='0.4';btn.style.cursor='not-allowed';}
if(!lockBanner){
lockBanner=document.createElement('div');
lockBanner.id='wsLockBanner';
parent.style.position='relative';
parent.appendChild(lockBanner);
}
lockBanner.style.display='flex';
lockBanner.style.cssText='position:absolute;inset:0;background:rgba(255,255,255,.97);'
+'border-radius:16px;display:flex;flex-direction:column;align-items:center;'
+'justify-content:center;z-index:100;padding:28px;text-align:center;'
+'border:2px solid #F59E0B40';
lockBanner.innerHTML='π±
'
+'Mobile App Request Active
'
+''
+'A service request is in progress on your Prezivio mobile app.
'
+'Complete or cancel it there before starting a new one here.'
+'
'
+''
+''
+''
+'
';
} else {
// Re-enable the search button
if(btn){btn.disabled=false;btn.style.opacity='';btn.style.cursor='';}
if(lockBanner) lockBanner.style.display='none';
}
}
function wsForceUnlock(){
wsClearRequestLock();
wsUpdateLockUI();
}
/* ββββββββββββββββββββββββββββββββββββββββ
WEBSITE RATING MODAL
ββββββββββββββββββββββββββββββββββββββββ */
var _wsRatingRequestId=null, _wsRatingStars=0, _wsRatingProviderName='';
function wsShowRatingModal(requestId, providerName){
_wsRatingRequestId=requestId;
_wsRatingStars=0;
_wsRatingProviderName=providerName||'your provider';
var modal=document.getElementById('wsRatingModal');
var subtitle=document.getElementById('wsRatingSubtitle');
var comment=document.getElementById('wsRatingComment');
var submitBtn=document.getElementById('wsRatingSubmitBtn');
if(!modal) return;
if(subtitle) subtitle.textContent='How was your experience with '+_wsRatingProviderName+'?';
if(comment) comment.value='';
if(submitBtn){submitBtn.style.background='#CBD5E1';submitBtn.style.cursor='not-allowed';}
wsSetRatingStar(0);
modal.style.display='flex';
}
function wsSetRatingStar(n){
_wsRatingStars=n;
var stars=document.querySelectorAll('#wsRatingStars button');
stars.forEach(function(btn,i){
var filled=i0?'#0D9488':'#CBD5E1';
submitBtn.style.cursor=n>0?'pointer':'not-allowed';
}
}
function wsSubmitRating(){
if(!_wsRatingStars||!_wsRatingRequestId) return;
var comment=document.getElementById('wsRatingComment');
var commentVal=comment?comment.value.trim():'';
api('POST','/rate',{requestId:_wsRatingRequestId,stars:_wsRatingStars,comment:commentVal,role:'senior'}).then(function(){
document.getElementById('wsRatingModal').style.display='none';
wsShowToast('Rating submitted β thank you! β','#0D9488');
loadRequests();
}).catch(function(){
document.getElementById('wsRatingModal').style.display='none';
wsShowToast('Rating sent!','#0D9488');
});
}
function wsShowToast(msg,color){
var t=document.getElementById('wsToast');
if(!t){
t=document.createElement('div');
t.id='wsToast';
t.style.cssText='position:fixed;bottom:90px;left:50%;transform:translateX(-50%);'
+'padding:12px 22px;border-radius:14px;font-size:13px;font-weight:700;color:#fff;'
+'z-index:10000;transition:opacity .3s;pointer-events:none;white-space:nowrap';
document.body.appendChild(t);
}
t.style.background=color||'#0D9488';
t.textContent=msg;
t.style.opacity='1';
clearTimeout(window._wsToastTimer);
window._wsToastTimer=setTimeout(function(){t.style.opacity='0';},3500);
}
function wsSyncLock(){
// Re-check server for any active request from this senior
if(!currentUser) return;
api('GET','/requests?userId='+encodeURIComponent(currentUser.email)+'&role=senior').then(function(rd){
if(!rd||!rd.requests) return;
var active=rd.requests.filter(function(r){
return r.status==='pending'||r.status==='accepted'||r.status==='confirmed'||r.status==='in_progress';
});
if(active.length===0){
// No active request β clear lock
wsClearRequestLock();
wsUpdateLockUI();
} else {
// Active request exists β restore it
var r=active[0];
pendingRequestId=r.id;
wsSetRequestLock(r.id);
wsUpdateLockUI();
// Restore the step3 panel
var svcStep3=document.getElementById('svcStep3');
var svcStep1=document.getElementById('svcStep1');
var acctSvc=document.getElementById('acctServiceReq');
if(svcStep3) svcStep3.style.display='block';
if(svcStep1) svcStep1.style.display='none';
if(acctSvc) acctSvc.style.display='block';
if(r.status==='pending') s3Show('s3Waiting');
else if(r.status==='accepted') s3Show('s3Accepted');
else {
s3Show('s3Navigating');
if(!window._s3TrackTimer) window._s3TrackTimer=setInterval(function(){updateNavMap(r);},2000);
updateNavMap(r);
}
if(!step3PollTimer) step3PollTimer=setInterval(pollStep3,2000);
}
});
}
/* Poll for App-originated lock every 5s when form is visible */
function wsStartLockWatch(){
if(window._wsLockWatcher) return;
// Instant response when App sets/clears the lock key (storage event fires across tabs)
if(!window._wsLockStorageListener){
window._wsLockStorageListener=function(e){
if(e.key==='prezivio_sr_req_lock'||e.key==='prezivio_sr_app_req_active'){
wsUpdateLockUI();
}
};
window.addEventListener('storage',window._wsLockStorageListener);
}
window._wsLockWatcher=setInterval(function(){
if(!currentUser) return;
wsUpdateLockUI();
// Also check server for active requests not initiated here
if(!pendingRequestId){
api('GET','/requests?userId='+encodeURIComponent(currentUser.email)+'&role=senior').then(function(rd){
if(!rd||!rd.requests) return;
var active=rd.requests.filter(function(r){
return r.status==='pending'||r.status==='accepted'||r.status==='confirmed'||r.status==='in_progress';
});
// Detect App-originated completion: lock held for App request, now nothing active
var lock=wsGetActiveLock();
if(lock&&lock.source==='app'&&lock.requestId&&active.length===0){
var completedReq=rd.requests.filter(function(r){return r.id===lock.requestId&&r.status==='completed';})[0];
if(completedReq&&window._wsAppCompletionShown!==completedReq.id){
window._wsAppCompletionShown=completedReq.id;
wsClearRequestLock();
wsUpdateLockUI();
loadRequests();
wsShowToast('Service complete! Total: $'+(completedReq.totalCharge||'β'),'#16A34A');
setTimeout(function(){wsShowRatingModal(completedReq.id,completedReq.providerName||'your provider');},1200);
return;
}
}
if(active.length>0&&!pendingRequestId){
// App-originated request detected β set lock so form is blocked
try{
localStorage.setItem('prezivio_sr_app_req_active',JSON.stringify({
source:'app',requestId:active[0].id,email:currentUser.email,ts:Date.now()
}));
}catch(e){}
wsUpdateLockUI();
} else if(active.length===0){
wsClearRequestLock();
wsUpdateLockUI();
}
});
}
},5000);
}
function doRequestService(){ searchProviders(); }
function doLogout(){doSignOut();}
/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
WEBSITE MESSAGE PUMP β polls /receive every 2s after login
Mirrors Senior App's serverSeqRef + /receive mechanism.
Handles: service_completed, service_started, request_accepted,
request_confirmed, senior_confirmed, earning_received,
rating_received β and syncs request list on every tick.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
var _wsMsgSeq=0, _wsMsgPumpTimer=null;
function wsStartMessagePump(){
if(_wsMsgPumpTimer) return; // already running
_wsMsgSeq=0;
_wsMsgPumpTimer=setInterval(wsMessagePoll,2000);
wsMessagePoll(); // immediate first poll
}
function wsStopMessagePump(){
if(_wsMsgPumpTimer){clearInterval(_wsMsgPumpTimer);_wsMsgPumpTimer=null;}
}
function wsMessagePoll(){
if(!currentUser||!currentUser.email) return;
api('GET','/receive?userId='+encodeURIComponent(currentUser.email)+'&seq='+_wsMsgSeq).then(function(res){
if(!res) return;
// Process messages
if(res.messages&&res.messages.length>0){
res.messages.forEach(function(msg){
try{
if(msg.seq>_wsMsgSeq) _wsMsgSeq=msg.seq;
var p=msg.payload||{};
wsHandleServerMessage(msg.type,p);
}catch(me){ console.log('[WS-PUMP] msg error:',me); }
});
// ACK
api('POST','/ack',{userId:currentUser.email,seq:_wsMsgSeq});
}
// Always sync request list for live status in My Requests tab
wsRefreshRequests();
}).catch(function(e){ /* silent β server may be unreachable briefly */ });
}
function wsHandleServerMessage(type,p){
console.log('[WS-PUMP] Received:',type,p);
if(type==='service_completed'){
wsShowToast('β
Service complete! Total: $'+(p.totalCharge||'β'),'#16A34A');
// Clear tracking and lock
if(window._s3TrackTimer){clearInterval(window._s3TrackTimer);window._s3TrackTimer=null;}
wsClearRequestLock();
// Update step3 nav card to show completion
var navCard=document.getElementById('s3NavCard');
if(navCard){
navCard.style.background='linear-gradient(135deg,#0D9488,#059669)';
var navProv=document.getElementById('s3NavProvName');
if(navProv) navProv.textContent=(p.providerName||'Provider')+' β Service Complete!';
var etaLine=document.getElementById('s3NavEtaLine');
if(etaLine) etaLine.textContent='Your session has ended. Thank you!';
}
// Stop pollStep3 if still running
if(step3PollTimer){clearInterval(step3PollTimer);step3PollTimer=null;}
var completedId=p.requestId||pendingRequestId;
var completedProv=p.providerName||'your provider';
pendingRequestId=null;
loadRequests();
// Show rating modal after brief delay
if(!window._wsRatingShownFor||window._wsRatingShownFor!==completedId){
window._wsRatingShownFor=completedId;
setTimeout(function(){wsShowRatingModal(completedId,completedProv);},1400);
}
}
if(type==='service_started'){
wsShowToast('π '+(p.providerName||'Provider')+' has arrived and started your service!','#7C3AED');
var navName=document.getElementById('s3NavProvName');
if(navName&&navName.textContent.indexOf('on the way')!==-1){
navName.textContent=(p.providerName||'Provider')+' has started your service!';
}
}
if(type==='request_accepted'){
if(p.providerName&&pendingRequestId){
wsShowToast('π '+(p.providerName||'Provider')+' accepted your request!','#0D9488');
}
}
if(type==='request_confirmed'||type==='senior_confirmed'){
wsShowToast('β Confirmed β provider is on the way!','#0D9488');
}
if(type==='rating_received'){
wsShowToast('β '+(p.raterName||'Provider')+' rated you '+p.stars+' stars!','#D4A93C');
}
}
function wsRefreshRequests(){
if(!currentUser) return;
var isSenior=currentUser.roles&¤tUser.roles.indexOf('senior')>=0;
if(!isSenior) return;
api('GET','/requests?userId='+encodeURIComponent(currentUser.email)+'&role=senior').then(function(rd){
if(!rd||!rd.requests) return;
// If step3 is showing, let pollStep3 handle step3 transitions
// But always keep My Requests / history tab in sync
var reqListEl=document.getElementById('requestsList');
if(reqListEl&&document.getElementById('acctRequests')&&document.getElementById('acctRequests').style.display!=='none'){
loadRequests();
}
// Check if an in-progress/confirmed request we're tracking just completed
if(pendingRequestId){
var tracked=rd.requests.filter(function(r){return r.id===pendingRequestId;})[0];
if(tracked&&tracked.status==='completed'&&!window._wsRatingShownFor!==tracked.id){
wsHandleServerMessage('service_completed',{
requestId:tracked.id,totalCharge:tracked.totalCharge,providerName:tracked.providerName
});
}
}
// Detect App-originated completion (no pendingRequestId on website)
var lock=wsGetActiveLock();
if(lock&&lock.source==='app'&&lock.requestId){
var appReq=rd.requests.filter(function(r){return r.id===lock.requestId;})[0];
if(appReq&&appReq.status==='completed'&&window._wsAppCompletionShown!==appReq.id){
window._wsAppCompletionShown=appReq.id;
wsHandleServerMessage('service_completed',{
requestId:appReq.id,totalCharge:appReq.totalCharge,providerName:appReq.providerName
});
}
}
}).catch(function(){});
}
/* ββ SESSION RESTORE: check for saved session on page load ββ */
/* Shares session with Senior App via 'prezivio_sr_session' key */
(function(){
var saved = null;
try{saved = localStorage.getItem('prezivio_ws_session') || localStorage.getItem('prezivio_sr_session');}catch(e){}
if(!saved) return;
api('POST','/login-token',{token:saved}).then(function(d){
if(d&&d.success){
currentUser={email:d.email,roles:d.roles,firstName:d.firstName,lastName:d.lastName,accountStatus:d.accountStatus||'approved'};
try{localStorage.setItem('prezivio_ws_session',d.email);localStorage.setItem('prezivio_sr_session',d.email);}catch(e){}
showAccount();
setTimeout(function(){wsBuildAccordion();wsUpdateSummaryBar();},300);
}
}).catch(function(){});
})();