const { useEffect, useMemo, useState } = React;
const API_BASE = (window.__APP_CONFIG__ && window.__APP_CONFIG__.apiBase) || '../api';
async function apiRequest(endpoint, options = {}) {
const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData;
const response = await fetch(`${API_BASE}/${endpoint}`, {
credentials: 'include',
headers: isFormData ? { ...(options.headers || {}) } : { 'Content-Type': 'application/json', ...(options.headers || {}) },
...options,
});
const payload = await response.json().catch(() => ({ message: 'Unexpected server response.' }));
if (!response.ok) {
const error = new Error(payload.message || 'Request failed.');
error.payload = payload;
throw error;
}
return payload;
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
return Uint8Array.from([...rawData].map((character) => character.charCodeAt(0)));
}
function initialPushState() {
if (!('serviceWorker' in navigator) || !('PushManager' in window) || !('Notification' in window)) return 'unsupported';
if (Notification.permission === 'denied') return 'denied';
return 'default';
}
async function enableBrowserPush() {
if (initialPushState() === 'unsupported') throw new Error('This browser does not support desktop notifications.');
const permission = await Notification.requestPermission();
if (permission !== 'granted') throw new Error('Notification permission was not granted.');
const keyResult = await apiRequest('push_public_key.php');
const registration = await navigator.serviceWorker.ready;
let subscription = await registration.pushManager.getSubscription();
if (!subscription) {
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(keyResult.publicKey),
});
}
await apiRequest('push_subscribe.php', {
method: 'POST',
body: JSON.stringify({
subscription: subscription.toJSON(),
content_encoding: subscription.options?.contentEncoding || 'aes128gcm',
}),
});
}
function Icon({ name, size = 20 }) {
const paths = {
arrow: <> >,
check: <> >,
chevron: ,
calendar: <> >,
handshake: <> >,
chat: <> >,
bell: <> >,
clipboard: <> >,
heart: ,
lock: <> >,
logout: <> >,
menu: <> >,
phone: ,
pin: <> >,
shield: <> >,
user: <> >,
users: <> >,
sparkle: <> >,
play: ,
};
return {paths[name] || paths.sparkle} ;
}
function Brand({ light = false }) {
return
R Renovar
;
}
function PrimaryButton({ children, onClick, type = 'button', variant = 'primary', className = '', disabled = false }) {
return {children} ;
}
function LandingPage({ onLogin, onRegister }) {
return
Evidence-led mental wellness
Relief that works when medications don't.
TMS therapy targets depression at its source, giving you a fresh start with a care team that sees the whole you.
Start your journey
document.getElementById('approach')?.scrollIntoView({ behavior: 'smooth' })}>› Learn how it works
JM AS TK
Trusted by our patients Real care. Real progress.
Care that meets you Wherever you are in your journey
Personalized support Every step of the way
Personalized care Clinically guided Patient first Here when you need us
A better way forward
Care designed around your life. We believe mental healthcare should feel clear, supportive and personal. From your first question to your next breakthrough, our team stays by your side.
Explore care options
Support for the whole you. Every person responds differently. Our team will help you explore the service that best fits your needs.
Your care, in one place
Stay connected to your care team. Register for your secure patient portal to share what you need, view your next steps and keep every conversation together.
Create patient account
;
}
function FeatureCard({ icon, number, title, text }) {
return {number}
{title} {text}
;
}
function ServiceRow({ number, title, text }) {
return {number} {title} {text}
;
}
function LandingPageV2({ onLogin, onRegister, onAdminLogin, onStaffLogin }) {
return
A different approach to mental wellness
Relief that works when medications don't. TMS Therapy targets depression at its source, giving you a fresh start with personalized care and support that stays with you.
Register now if TMS is right for you document.getElementById('approach')?.scrollIntoView({ behavior: 'smooth' })}> Learn about TMS ★★★★★ Trusted by patients who are ready to feel better
TMS therapy, made clear
More than treatment.A path forward. When medication has not been enough, TMS can offer a new option. Our team makes every part of the process feel simple, informed and personal.
Start your care journey
Non-invasive. Personalized. Hopeful.A treatment worth understanding
What is TMS? TMS is a non-invasive treatment that uses magnetic pulses to stimulate the areas of the brain involved in mood. There is no anesthesia, no downtime and no need to change who you are.
No surgery or medication changes
Sessions designed around your schedule
A calm team beside you at every visit
Learn if you qualify
Explore your options
There is more thanone way forward. Every person is different. We will help you understand the care option that makes sense for your goals.
We accept most major insurance plans
How to Prepare for TMS Therapy
The people behind your care
Meet Our Provider Dr. Majid Malik, MD Dr. Majid Malik is a graduate of Texas Tech University and a lifelong member of the Michiana community - a place he proudly calls home. As the founder of Serenity Home Medical and Renovar Wellness, his work is driven by a deep commitment to caring for the people who live here.
After seeing firsthand how few advanced mental health treatment options were available locally, Dr. Malik made the decision to bring FDA-approved BrainsWay neuromodulation therapy to the community. This treatment is designed for individuals living with depression, anxiety, and OCD, especially those who have not found relief through medication alone.
Raising his family in Indiana, this mission is deeply personal. Dr. Malik's goal is to ensure that individuals and families in the community have access to compassionate, effective care - without having to leave home to find it.
Your care, in one place
Stay connected to your care team. Register for your secure patient portal to share what you need, view your next steps and keep every conversation together.
Create patient account
;
}
function StatCard({ value, label }) {
const target = parseInt(value, 10);
const suffix = value.replace(/[0-9]/g, '');
const [count, setCount] = useState(0);
const ref = React.useRef(null);
useEffect(() => {
let frame;
const observer = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) return;
const startedAt = performance.now();
const duration = 1400;
const animate = (now) => {
const progress = Math.min((now - startedAt) / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
setCount(Math.round(target * eased));
if (progress < 1) frame = requestAnimationFrame(animate);
};
frame = requestAnimationFrame(animate);
observer.disconnect();
}, { threshold: 0.45 });
if (ref.current) observer.observe(ref.current);
return () => { observer.disconnect(); if (frame) cancelAnimationFrame(frame); };
}, [target]);
return {count}{suffix} {label}
;
}
function TreatmentFeature({ icon, title, text }) { return {title} {text}
; }
function InsuranceLogo({ src, alt }) { return ; }
function InsuranceLogos() {
const [visible, setVisible] = useState(false);
const ref = React.useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) { setVisible(true); observer.disconnect(); }
}, { threshold: 0.35 });
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
const logos = [
['insurance-united.webp', 'United Healthcare'],
['insurance-bcbs.webp', 'Blue Cross Blue Shield'],
['insurance-aetna.webp', 'Aetna'],
['insurance-cigna.webp', 'Cigna'],
['insurance-medicare.webp', 'Medicare'],
];
return {logos.map(([src, alt]) => )}{logos.map(([src, alt]) => )}
;
}
function PrepareCard({ icon, title, time, text }) { return {title} {time} {text}
; }
function AuthScreen({ initialMode = 'register', onSuccess, onHome }) {
const [mode, setMode] = useState(initialMode);
return
A fresh start begins here
Your next chapter can feel different. Connect with the Renovar Wellness team and take the first step towards care built around you.
“ Supportive, calm and genuinely focused on helping me move forward.
— A Renovar patient
← Back to website Patient portal
{mode === 'register' ? 'Create your account' : 'Welcome back'} {mode === 'register' ? 'Share a few details and our care team will be in touch.' : 'Log in to view your care journey and messages.'}
{mode === 'register' ?
setMode('login')} /> : setMode('register')} />}
;
}
const initialRegistration = { first_name: '', last_name: '', mobile_no: '', email: '', password: '', confirm_password: '', service_interested: '', preferred_contact_method: 'SMS', note: '' };
function RegistrationForm({ onSuccess, onLogin }) {
const [form, setForm] = useState(initialRegistration);
const [error, setError] = useState('');
const [fieldErrors, setFieldErrors] = useState({});
const [loading, setLoading] = useState(false);
const update = (event) => setForm({ ...form, [event.target.name]: event.target.value });
const submit = async (event) => {
event.preventDefault(); setError(''); setFieldErrors({}); setLoading(true);
try { const result = await apiRequest('register.php', { method: 'POST', body: JSON.stringify(form) }); onSuccess(result.patient); }
catch (err) { setError(err.message); setFieldErrors(err.payload?.errors || {}); }
finally { setLoading(false); }
};
return ;
}
function LoginForm({ onSuccess, onRegister, onForgot }) {
const [form, setForm] = useState({ email: '', password: '' }); const [error, setError] = useState(''); const [loading, setLoading] = useState(false);
const submit = async (event) => { event.preventDefault(); setError(''); setLoading(true); try { const result = await apiRequest('login.php', { method: 'POST', body: JSON.stringify(form) }); onSuccess(result.patient); } catch (err) { setError(err.message); } finally { setLoading(false); } };
return setForm({ ...form, email: e.target.value })} placeholder="you@example.com" required /> setForm({ ...form, password: e.target.value })} placeholder="Your password" required /> Remember meForgot password?
{loading ? 'Logging in...' : 'Log in to patient portal'} Need an account? Create one here
;
}
function AdminLoginScreen({ onSuccess, onHome }) {
return
Secure team access
Welcome to the admin portal. Sign in to manage patient care, conversations and clinic operations.
← Back to website Admin portal
Admin sign in Use your authorized admin credentials to continue.
;
}
function AdminLoginForm({ onSuccess }) {
const [form, setForm] = useState({ username: '', password: '' });
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const submit = async (event) => {
event.preventDefault(); setError(''); setLoading(true);
try { const result = await apiRequest('admin_login.php', { method: 'POST', body: JSON.stringify(form) }); onSuccess(result.admin); }
catch (err) { setError(err.message); }
finally { setLoading(false); }
};
return setForm({ ...form, username: e.target.value })} placeholder="Admin username" required /> setForm({ ...form, password: e.target.value })} placeholder="Admin password" required />{loading ? 'Signing in...' : 'Sign in to admin portal'} Patient access? window.location.href = './'}>Return to patient portal
;
}
function StaffLoginScreen({ onSuccess, onHome }) {
return
Care team access
Make every patient interaction count. Sign in to review patient conversations and stay connected with the people in your care.
← Back to website Staff portal
Staff sign in Use the username and password provided by your administrator.
;
}
function StaffLoginForm({ onSuccess }) {
const [form, setForm] = useState({ username: '', password: '' });
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const submit = async (event) => {
event.preventDefault(); setError(''); setLoading(true);
try { const result = await apiRequest('staff_login.php', { method: 'POST', body: JSON.stringify(form) }); onSuccess(result.staff); }
catch (err) { setError(err.message); }
finally { setLoading(false); }
};
return setForm({ ...form, username: e.target.value })} placeholder="Staff username" required /> setForm({ ...form, password: e.target.value })} placeholder="Your password" required />{loading ? 'Signing in...' : 'Sign in to staff portal'} Patient access? window.location.href = './'}>Return to patient portal
;
}
function AuthScreenWithReset({ initialMode = 'register', resetToken = '', onSuccess, onHome }) {
const [mode, setMode] = useState(initialMode);
const [resetMessage, setResetMessage] = useState('');
const title = mode === 'register' ? 'Create your account' : mode === 'forgot' ? 'Reset your password' : mode === 'reset' ? 'Create a new password' : 'Welcome back';
const description = mode === 'register' ? 'Share a few details and our care team will be in touch.' : mode === 'forgot' ? 'Enter your registered email to receive a secure reset link.' : mode === 'reset' ? 'Choose a new password for your secure patient portal account.' : 'Log in to view your care journey and messages.';
return
A fresh start begins here
Your next chapter can feel different. Connect with the Renovar Wellness team and take the first step towards care built around you.
“ Supportive, calm and genuinely focused on helping me move forward.
— A Renovar patient
↠Back to website Patient portal
{title} {description}
{resetMessage &&
{resetMessage}
}{mode === 'register' ?
setMode('login')} /> : mode === 'login' ? setMode('register')} onForgot={() => { setResetMessage(''); setMode('forgot'); }} /> : mode === 'forgot' ? setMode('login')} /> : { setResetMessage('Password reset successful. Please log in with your new password.'); setMode('login'); window.history.replaceState({}, document.title, window.location.pathname); }} />}
;
}
function ForgotPasswordForm({ onBack }) {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [sent, setSent] = useState(false);
const submit = async (event) => {
event.preventDefault(); setError(''); setLoading(true);
try { await apiRequest('forgot_password.php', { method: 'POST', body: JSON.stringify({ email }) }); setSent(true); }
catch (err) { setError(err.message); }
finally { setLoading(false); }
};
if (sent) return Check your email If this email belongs to a patient account, a secure password reset link has been sent to it.
Return to login ;
return setEmail(e.target.value)} placeholder="you@example.com" required />{loading ? 'Sending reset link...' : 'Send reset link'} Remembered your password? Return to login
;
}
function ResetPasswordForm({ token, onLogin }) {
const [form, setForm] = useState({ password: '', confirm_password: '' });
const [error, setError] = useState('');
const [fieldErrors, setFieldErrors] = useState({});
const [loading, setLoading] = useState(false);
const submit = async (event) => {
event.preventDefault(); setError(''); setFieldErrors({}); setLoading(true);
try { await apiRequest('reset_password.php', { method: 'POST', body: JSON.stringify({ token, ...form }) }); onLogin(); }
catch (err) { setError(err.message); setFieldErrors(err.payload?.errors || {}); }
finally { setLoading(false); }
};
return {!token && This reset link is missing. Please request a new one.
} setForm({ ...form, password: e.target.value })} placeholder="At least 8 characters" error={fieldErrors.password} required /> setForm({ ...form, confirm_password: e.target.value })} placeholder="Re-enter your new password" error={fieldErrors.confirm_password} required />{loading ? 'Saving password...' : 'Save new password'} Already have access? Return to login
;
}
function AdminDashboard({ admin, onLogout, onAdminUpdate }) {
const [section, setSection] = useState('overview');
const [mobileNav, setMobileNav] = useState(false);
const [stats, setStats] = useState({ total_patients: 0, total_staff: 0, new_patients: 0, unread_notifications: 0 });
const [recentPatients, setRecentPatients] = useState([]);
const [staff, setStaff] = useState([]);
const [loading, setLoading] = useState(true);
const [notice, setNotice] = useState('');
const [error, setError] = useState('');
const [staffErrors, setStaffErrors] = useState({});
const [staffForm, setStaffForm] = useState({ first_name: '', last_name: '', username: '', email: '', mobile_no: '', role: 'agent', password: '', confirm_password: '' });
const [settings, setSettings] = useState({ first_name: admin.first_name || '', last_name: admin.last_name || '', mobile_no: admin.mobile_no || '', email: admin.email || '' });
const [settingsSaving, setSettingsSaving] = useState(false);
const [passwordForm, setPasswordForm] = useState({ current_password: '', new_password: '', confirm_password: '' });
const [passwordSaving, setPasswordSaving] = useState(false);
const [passwordError, setPasswordError] = useState('');
const [passwordNotice, setPasswordNotice] = useState('');
const [passwordFieldErrors, setPasswordFieldErrors] = useState({});
const loadDashboard = async () => {
setLoading(true);
try {
const [overview, staffResult] = await Promise.all([apiRequest('admin_overview.php'), apiRequest('admin_staff.php')]);
setStats(overview.stats || {});
setRecentPatients(overview.recent_patients || []);
setStaff(staffResult.staff || []);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => { loadDashboard(); }, []);
const updateStaffForm = (event) => setStaffForm({ ...staffForm, [event.target.name]: event.target.value });
const addStaff = async (event) => {
event.preventDefault(); setError(''); setNotice(''); setStaffErrors({});
try {
const result = await apiRequest('admin_staff.php', { method: 'POST', body: JSON.stringify(staffForm) });
setStaff(result.staff || []);
setStats((current) => ({ ...current, total_staff: (result.staff || []).length }));
setStaffForm({ first_name: '', last_name: '', username: '', email: '', mobile_no: '', role: 'agent', password: '', confirm_password: '' });
setNotice(result.message);
} catch (err) {
setError(err.message); setStaffErrors(err.payload?.errors || {});
}
};
const saveSettings = async (event) => {
event.preventDefault(); setError(''); setNotice(''); setSettingsSaving(true);
try {
const result = await apiRequest('admin_settings.php', { method: 'POST', body: JSON.stringify(settings) });
setSettings({ first_name: result.admin.first_name, last_name: result.admin.last_name, mobile_no: result.admin.mobile_no, email: result.admin.email });
if (onAdminUpdate) onAdminUpdate(result.admin);
setNotice(result.message);
} catch (err) { setError(err.message); }
finally { setSettingsSaving(false); }
};
const changePassword = async (event) => {
event.preventDefault(); setPasswordError(''); setPasswordNotice(''); setPasswordFieldErrors({}); setPasswordSaving(true);
try {
const result = await apiRequest('change_password.php', { method: 'POST', body: JSON.stringify(passwordForm) });
setPasswordForm({ current_password: '', new_password: '', confirm_password: '' }); setPasswordNotice(result.message);
} catch (err) { setPasswordError(err.message); setPasswordFieldErrors(err.payload?.errors || {}); }
finally { setPasswordSaving(false); }
};
const sectionTitle = section === 'staff' ? 'Staff management' : section === 'settings' ? 'Admin settings' : 'Dashboard overview';
const initials = `${admin.first_name?.[0] || ''}${admin.last_name?.[0] || ''}`;
const patientAvatar = (patient) => patient.profile_image_url ? `${API_BASE}/${patient.profile_image_url}` : '';
return
{notice && {notice}
}
{error && {error}
}
{section === 'overview' && <>
Patient activity
Recent patients setSection('staff')}>Manage staff {loading ? Loading dashboard...
: recentPatients.length === 0 ? No patient registrations yet.
: {recentPatients.map((patient) => { const image = patientAvatar(patient); const patientInitials = `${patient.first_name?.[0] || ''}${patient.last_name?.[0] || ''}`; return
{image ? : patientInitials} {patient.first_name} {patient.last_name} {patient.email}
{patient.service_interested} {patient.status} ; })}
}Administration
Manage your team Add authorized staff members, assign their access role and give each person secure login credentials.
setSection('staff')}>Add staff member
>}
{section === 'staff' && Team access
Add staff member Admin only
setStaffForm({ ...staffForm, role: event.target.value.toLowerCase() })} options={['Agent', 'Admin']} error={staffErrors.role} required />
Add staff member Authorized users
Staff accounts {staff.length} accounts {staff.map((member) => { const image = member.profile_image_url ? `${API_BASE}/${member.profile_image_url}` : ''; const memberInitials = `${member.first_name?.[0] || ''}${member.last_name?.[0] || ''}`; return
{image ? : memberInitials} {member.first_name} {member.last_name} @{member.username} · {member.email}
{member.role} {member.status} ; })}
}
{section === 'settings' && Account details
Admin profile setSettings({ ...settings, first_name: event.target.value })} required /> setSettings({ ...settings, last_name: event.target.value })} required />
setSettings({ ...settings, email: event.target.value })} required /> setSettings({ ...settings, mobile_no: event.target.value })} />
Username @{admin.username}
{settingsSaving ? 'Saving settings...' : 'Save admin profile'} {passwordNotice && {passwordNotice}
}{passwordError && } setPasswordForm({ ...passwordForm, current_password: event.target.value })} error={passwordFieldErrors.current_password} required /> setPasswordForm({ ...passwordForm, new_password: event.target.value })} error={passwordFieldErrors.new_password} required /> setPasswordForm({ ...passwordForm, confirm_password: event.target.value })} error={passwordFieldErrors.confirm_password} required />{passwordSaving ? 'Changing password...' : 'Change password'} }
;
}
function AdminStat({ label, value }) { return {label} {value ?? 0}
; }
function AgentPatientProcessPage({ staff, onLogout, onOpenOverview, onOpenPatients, onOpenConversations }) {
const [patients, setPatients] = useState([]);
const [services, setServices] = useState([]);
const [status, setStatus] = useState('all');
const [service, setService] = useState('all');
const [searchInput, setSearchInput] = useState('');
const [search, setSearch] = useState('');
const [sort, setSort] = useState('recent');
const [viewMode, setViewMode] = useState('board');
const [filtersOpen, setFiltersOpen] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [notice, setNotice] = useState('');
const [draggedPatient, setDraggedPatient] = useState(null);
const [unreadConversations, setUnreadConversations] = useState(0);
const [mobileNav, setMobileNav] = useState(false);
const initials = `${staff.first_name?.[0] || ''}${staff.last_name?.[0] || ''}`;
const columns = [['new', 'New Lead'], ['scheduled', 'Consult Set'], ['vob', 'VOB'], ['contacted', 'Contacted'], ['first_follow_up', 'First Follow Up'], ['second_follow_up', 'Second Follow Up'], ['active', 'Active'], ['no_response', 'No Response'], ['closed', 'Closed']];
const loadPatients = async () => {
setLoading(true); setError('');
const params = new URLSearchParams({ list: 'all', status, service, sort, q: search, page: '1', per_page: '50' });
try {
const result = await apiRequest(`staff_patients.php?${params.toString()}`);
setPatients(result.patients || []); setServices(result.services || []); setUnreadConversations(Number(result.unread_conversations || 0));
} catch (err) { setError(err.message); }
finally { setLoading(false); }
};
useEffect(() => { loadPatients(); }, [status, service, sort, search]);
const applySearch = (event) => { event.preventDefault(); setSearch(searchInput.trim()); };
const updateStage = async (patientId, stage, nextFollowUpAt = '') => {
setError(''); setNotice('');
try {
const result = await apiRequest('staff_update_stage.php', { method: 'POST', body: JSON.stringify({ patient_id: patientId, stage, next_follow_up_at: nextFollowUpAt }) });
setNotice(result.message); await loadPatients();
} catch (err) { setError(err.message); }
};
const handleDrop = async (stage) => { if (!draggedPatient || draggedPatient.status === stage) return; await updateStage(draggedPatient.id, stage, draggedPatient.next_follow_up_at || ''); setDraggedPatient(null); };
const imageFor = (patient) => patient.profile_image_url ? `${API_BASE}/${patient.profile_image_url}` : '';
const initialsFor = (patient) => `${patient.first_name?.[0] || ''}${patient.last_name?.[0] || ''}`;
const formatDateShort = (value) => value ? new Date(value.replace(' ', 'T')).toLocaleDateString([], { month: 'short', day: 'numeric' }) : '';
const filteredColumns = columns.map(([value, label]) => [value, label, patients.filter((patient) => patient.status === value)]);
return
setMobileNav(false)}>×
⌕ Search Ctrl K ⌂ Home ▣ Patient Process ♙ Patient List ◌ Conversations {unreadConversations > 0 && {unreadConversations} }□ Appointments
◉ Dashboard ⌁ Reporting ☷ Sign out
setMobileNav(true)}>
✣ Slack ⌕ ✦ ♟ {initials}
Patient Process Tracker Forecast ⇩ Import + Add opportunity ⋮
{notice && {notice}
}{error && {error}
} {}}>Patient Pipeline {patients.length} opportunities ▦ setViewMode(viewMode === 'board' ? 'list' : 'board')} title="Toggle list view">☷ setFiltersOpen((open) => !open)}>⚱ Advanced filters setSort(event.target.value)}>↕ Sort Name Activity ↻ Refresh
⌕ setSearchInput(event.target.value)} placeholder="Search opportunities" /> {filtersOpen && Stage setStatus(event.target.value)}>All stages {columns.map(([value, label]) => {label} )} Service setService(event.target.value)}>All services {services.map((item) => {item} )} { setStatus('all'); setService('all'); setSearchInput(''); setSearch(''); }}>Clear filters
}{viewMode === 'list' ? Patient Service Stage Phone Last activity {loading ? Loading opportunities... : patients.map((patient) => {patient.first_name} {patient.last_name} {patient.service_interested} updateStage(patient.id, event.target.value)}>{columns.map(([value, label]) => {label} )} {patient.mobile_no} {formatDateShort(patient.last_activity)} )}
: {filteredColumns.map(([stage, label, stagePatients]) =>
event.preventDefault()} onDrop={() => handleDrop(stage)}>
{label} {stagePatients.length} opportunities ‹ {loading ? Loading...
: stagePatients.length === 0 ? No opportunities
: stagePatients.map((patient) => { const image = imageFor(patient); return setDraggedPatient(patient)}> onOpenConversations()}>{patient.first_name} {patient.last_name} - {patient.service_interested} ⌕ onOpenConversations()} title="Open conversation">◌ ◇ {patient.conversation_count || 0} ▣ {image ? : initialsFor(patient)} ; })} )}
}
;
}
function AgentPatientPipelinePage({ staff, onLogout, onOpenOverview, onOpenConversations, onOpenProcess }) {
const [smartList, setSmartList] = useState('all');
const [mobileNav, setMobileNav] = useState(false);
const [status, setStatus] = useState('all');
const [service, setService] = useState('all');
const [sort, setSort] = useState('recent');
const [searchInput, setSearchInput] = useState('');
const [search, setSearch] = useState('');
const [filtersOpen, setFiltersOpen] = useState(false);
const [patients, setPatients] = useState([]);
const [smartCounts, setSmartCounts] = useState({ all: 0 });
const [unreadConversations, setUnreadConversations] = useState(0);
const [services, setServices] = useState([]);
const [stages, setStages] = useState(PIPELINE_STAGE_LABELS);
const [pagination, setPagination] = useState({ page: 1, pages: 1, total: 0, per_page: 20 });
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [notice, setNotice] = useState('');
const initials = `${staff.first_name?.[0] || ''}${staff.last_name?.[0] || ''}`;
const stageOptions = Object.entries(stages).length ? Object.entries(stages) : Object.entries(PIPELINE_STAGE_LABELS);
const smartLists = [['all', 'All'], ['new_lead', 'New Lead'], ['contacted', 'Contacted'], ['scheduled', 'Consult Set'], ['vob', 'VOB'], ['first_follow_up', 'First Follow Up'], ['second_follow_up', 'Second Follow Up'], ['active', 'Active'], ['no_response', 'No Response']];
const loadPatients = async () => {
setLoading(true); setError('');
const params = new URLSearchParams({ list: smartList, status, service, sort, q: search, page: String(page), per_page: '20' });
try {
const result = await apiRequest(`staff_patients.php?${params.toString()}`);
setPatients(result.patients || []); setSmartCounts(result.smart_counts || {}); setUnreadConversations(Number(result.unread_conversations || 0)); setServices(result.services || []); setStages(result.stages || PIPELINE_STAGE_LABELS); setPagination(result.pagination || {});
} catch (err) { setError(err.message); }
finally { setLoading(false); }
};
useEffect(() => { loadPatients(); }, [smartList, status, service, sort, search, page]);
const chooseList = (value) => { setSmartList(value); setStatus('all'); setPage(1); };
const applySearch = (event) => { event.preventDefault(); setPage(1); setSearch(searchInput.trim()); };
const updateStage = async (patient, nextStage, nextFollowUpAt = patient.next_follow_up_at || '') => {
setError(''); setNotice('');
try {
const result = await apiRequest('staff_update_stage.php', { method: 'POST', body: JSON.stringify({ patient_id: patient.id, stage: nextStage, next_follow_up_at: nextFollowUpAt }) });
setNotice(result.message); await loadPatients();
} catch (err) { setError(err.message); }
};
const formatListDate = (value) => value ? new Date(value.replace(' ', 'T')).toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }) : '—';
const formatFollowUp = (value) => value ? value.replace(' ', 'T').slice(0, 16) : '';
const initialsFor = (patient) => `${patient.first_name?.[0] || ''}${patient.last_name?.[0] || ''}`;
const imageFor = (patient) => patient.profile_image_url ? `${API_BASE}/${patient.profile_image_url}` : '';
return
setMobileNav(false)}>×
⌕ Search Ctrl K ⌂ Home ▣ Patient Process ♙ Patient List ◌ Conversations {unreadConversations > 0 && {unreadConversations} }□ Appointments
◉ Dashboard ⌁ Reporting ☷ Sign out
setMobileNav(true)}>
✣ Slack ⌕ ✦ ♟ {initials}
Patient List Pipeline ⇩ Import + Add Contact ⋮
{notice && {notice}
}{error && {error}
}
Contacts {pagination.total} Contacts {smartLists.map(([value, label]) => chooseList(value)} key={value}>☷ {label}{smartCounts[value] ?? 0} )}
{filtersOpen && Stage { setStatus(event.target.value); setSmartList('all'); setPage(1); }}>All stages {stageOptions.map(([value, label]) => {label} )} Service { setService(event.target.value); setPage(1); }}>All services {services.map((item) => {item} )} { setStatus('all'); setService('all'); setSmartList('all'); setSearchInput(''); setSearch(''); setPage(1); }}>Clear filters
}Page {pagination.page || 1} of {pagination.pages || 1} 20⌄ setPage((current) => Math.max(1, current - 1))}>Prev {pagination.page || 1} = (pagination.pages || 1)} onClick={() => setPage((current) => current + 1)}>Next
;
}
function AgentPatientListPage({ staff, onLogout, onOpenOverview, onOpenConversations }) {
const [smartList, setSmartList] = useState('all');
const [mobileNav, setMobileNav] = useState(false);
const [status, setStatus] = useState('all');
const [service, setService] = useState('all');
const [sort, setSort] = useState('recent');
const [searchInput, setSearchInput] = useState('');
const [search, setSearch] = useState('');
const [filtersOpen, setFiltersOpen] = useState(false);
const [patients, setPatients] = useState([]);
const [smartCounts, setSmartCounts] = useState({ all: 0, new_lead: 0, contacted: 0, first_follow_up: 0, second_follow_up: 0 });
const [unreadConversations, setUnreadConversations] = useState(0);
const [services, setServices] = useState([]);
const [pagination, setPagination] = useState({ page: 1, pages: 1, total: 0, per_page: 20 });
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const initials = `${staff.first_name?.[0] || ''}${staff.last_name?.[0] || ''}`;
const smartLists = [['all', 'All'], ['new_lead', 'New Lead'], ['contacted', 'Contacted'], ['first_follow_up', 'First Follow Up'], ['second_follow_up', 'Second Follow Up']];
const loadPatients = async () => {
setLoading(true); setError('');
const params = new URLSearchParams({ list: smartList, status, service, sort, q: search, page: String(page), per_page: '20' });
try {
const result = await apiRequest(`staff_patients.php?${params.toString()}`);
setPatients(result.patients || []); setSmartCounts(result.smart_counts || {}); setUnreadConversations(Number(result.unread_conversations || 0)); setServices(result.services || []); setPagination(result.pagination || {});
} catch (err) { setError(err.message); }
finally { setLoading(false); }
};
useEffect(() => { loadPatients(); }, [smartList, status, service, sort, search, page]);
const chooseList = (value) => { setSmartList(value); setStatus('all'); setPage(1); };
const applySearch = (event) => { event.preventDefault(); setPage(1); setSearch(searchInput.trim()); };
const initialsFor = (patient) => `${patient.first_name?.[0] || ''}${patient.last_name?.[0] || ''}`;
const imageFor = (patient) => patient.profile_image_url ? `${API_BASE}/${patient.profile_image_url}` : '';
const listDate = (value) => value ? new Date(value.replace(' ', 'T')).toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }) : '—';
return
setMobileNav(false)}>×
⌕ Search Ctrl K ⌂ Home ▣ Patient Process ♙ Patient List ◌ Conversations {unreadConversations > 0 && {unreadConversations} }□ Appointments
◉ Dashboard ⌁ Reporting ☷ Sign out
setMobileNav(true)}>
✣ Slack ⌕ ✦ ♟ {initials}
Patient List Smart Lists ⇩ Import + Add Contact ⋮
Contacts {pagination.total} Contacts {smartLists.map(([value, label]) => chooseList(value)} key={value}>☷ {label}{smartCounts[value] ?? 0} )}10 More⌄ + Add smart list
{filtersOpen && Status { setStatus(event.target.value); setSmartList('all'); setPage(1); }}>All statuses New Contacted Scheduled Active Closed Service { setService(event.target.value); setPage(1); }}>All services {services.map((item) => {item} )} { setStatus('all'); setService('all'); setSmartList('all'); setSearchInput(''); setSearch(''); setPage(1); }}>Clear filters
}Page {pagination.page || 1} of {pagination.pages || 1} 20⌄ setPage((current) => Math.max(1, current - 1))}>Prev {pagination.page || 1} = (pagination.pages || 1)} onClick={() => setPage((current) => current + 1)}>Next
;
}
function LegacyAgentConversationsPage({ staff, onLogout, onOverview, onPatientList, stats, threadGroups, selectedThread, selectedPatientId, onSelectPatient, message, setMessage, sendReply, notice, error, sending }) {
const initials = `${staff.first_name?.[0] || ''}${staff.last_name?.[0] || ''}`;
return
×
⌕ Search Ctrl K ⌂ Home ▣ Patient Process ♙ Patient List ◌ Conversations {stats.open_conversations > 0 && {stats.open_conversations} }□ Appointments
◉ Dashboard ⌁ Reporting ☷ Sign out
Patient communication
Patient conversations {threadGroups.length} patients {notice &&
{notice}
}{error &&
{error}
}
{threadGroups.length === 0 ?
No conversations yet.
: threadGroups.map((thread) => { const image = thread.imageUrl ? `${API_BASE}/${thread.imageUrl}` : ''; const lastMessage = thread.messages[thread.messages.length - 1]; const threadInitials = `${thread.firstName?.[0] || ''}${thread.lastName?.[0] || ''}`; return
onSelectPatient(thread.patientId)}>{image ? : threadInitials} {thread.firstName} {thread.lastName} {lastMessage.message} {formatDate(lastMessage.created_at)} ; })}
{selectedThread ? <>
{selectedThread.imageUrl ? : `${selectedThread.firstName?.[0] || ''}${selectedThread.lastName?.[0] || ''}`} {selectedThread.firstName} {selectedThread.lastName} {selectedThread.email}
{selectedThread.messages.map((item) =>
{item.sender_name} {formatDate(item.created_at)}
{item.message}
)}
setMessage(event.target.value)} placeholder="Write a reply to the patient..." rows="2" />{sending ? 'Sending...' : 'Send reply'} > :
Select a patient conversation.
}
;
}
function LegacyAgentConversationsPageWithDnd({ staff, onLogout, onOverview, onPatientList, stats, threadGroups, selectedThread, selectedPatientId, onSelectPatient, message, setMessage, sendReply, notice, error, sending }) {
const [inboxTab, setInboxTab] = useState('recent');
const [search, setSearch] = useState('');
const [starredPatients, setStarredPatients] = useState([]);
const initials = `${staff.first_name?.[0] || ''}${staff.last_name?.[0] || ''}`;
const unreadThreads = threadGroups.filter((thread) => thread.messages.some((item) => item.sender_type === 'patient' && !Number(item.is_read)));
const filteredThreads = threadGroups.filter((thread) => {
const matchesTab = inboxTab === 'all' || inboxTab === 'recent' || (inboxTab === 'unread' && unreadThreads.some((item) => item.patientId === thread.patientId)) || (inboxTab === 'starred' && starredPatients.includes(thread.patientId));
const haystack = `${thread.firstName} ${thread.lastName} ${thread.email} ${thread.messages[thread.messages.length - 1]?.message || ''}`.toLowerCase();
return matchesTab && haystack.includes(search.toLowerCase());
});
const activeThread = filteredThreads.find((thread) => thread.patientId === selectedPatientId) || filteredThreads[0] || null;
const activeImage = activeThread?.imageUrl ? `${API_BASE}/${activeThread.imageUrl}` : '';
const activeInitials = activeThread ? `${activeThread.firstName?.[0] || ''}${activeThread.lastName?.[0] || ''}` : '';
const activeUnread = activeThread ? activeThread.messages.filter((item) => item.sender_type === 'patient' && !Number(item.is_read)).length : 0;
const toggleStar = (event, patientId) => { event.stopPropagation(); setStarredPatients((current) => current.includes(patientId) ? current.filter((id) => id !== patientId) : [...current, patientId]); };
useEffect(() => {
if (activeThread && activeThread.patientId !== selectedPatientId) onSelectPatient(activeThread.patientId);
}, [activeThread?.patientId, selectedPatientId]);
const tabs = [['unread', 'Unread', unreadThreads.length], ['all', 'All', threadGroups.length], ['recent', 'Recent', threadGroups.length], ['starred', 'Starred', starredPatients.length]];
return
×
⌕ Search Ctrl K ⌂ Home ▣ Patient Process ♙ Patient List ◌ Conversations {unreadThreads.length > 0 && {unreadThreads.length} }□ Appointments
◉ Dashboard ⌁ Reporting ☷ Sign out
Conversations Conversations Templates Analytics Settings Trigger Links⌄ ⚙ Automation On {notice &&
{notice}
}{error &&
{error}
}
Team inbox {threadGroups.length} patient conversations
☷ ↕
{tabs.map(([value, label, count]) => setInboxTab(value)}>{label} {count} )}
Select all
event.preventDefault()}>⌕ setSearch(event.target.value)} placeholder="Search patients" /> {activeThread ? <>{activeImage ? : activeInitials} {activeThread.firstName} {activeThread.lastName} {activeThread.email}
◌ ⌕ ▱ toggleStar(event, activeThread.patientId)}>{starredPatients.includes(activeThread.patientId) ? '★' : '☆'} ✉
{activeThread.messages.map((item) =>
{item.sender_name} {formatDate(item.created_at)}
{item.message}
)}
☻⌄ setMessage(event.target.value)} placeholder="Type a message" rows="2" />{sending ? 'Sending...' : 'Send'} > : Select a patient conversation.
}
;
}
function useProcessNavOverride(onProcess) {
useEffect(() => {
if (!onProcess) return undefined;
const button = Array.from(document.querySelectorAll('.rise-agent-nav .nav-item')).find((item) => item.textContent.includes('Patient Process'));
if (!button) return undefined;
const handleProcessClick = (event) => {
event.preventDefault();
event.stopPropagation();
onProcess();
};
button.addEventListener('click', handleProcessClick);
return () => button.removeEventListener('click', handleProcessClick);
}, [onProcess]);
return null;
}
function AgentConversationsPage({ staff, onLogout, onOverview, onProcess, onPatientList, stats, threadGroups, selectedPatientId, onSelectPatient, message, setMessage, sendReply, notice, error, sending }) {
const [inboxTab, setInboxTab] = useState('recent');
const [search, setSearch] = useState('');
const [starredPatients, setStarredPatients] = useState([]);
const [mobileNav, setMobileNav] = useState(false);
const initials = `${staff.first_name?.[0] || ''}${staff.last_name?.[0] || ''}`;
useProcessNavOverride(onProcess);
const unreadThreads = threadGroups.filter((thread) => thread.messages.some((item) => item.sender_type === 'patient' && !Number(item.is_read)));
const filteredThreads = threadGroups.filter((thread) => {
const matchesTab = inboxTab === 'all' || inboxTab === 'recent' || (inboxTab === 'unread' && unreadThreads.some((item) => item.patientId === thread.patientId)) || (inboxTab === 'starred' && starredPatients.includes(thread.patientId));
const haystack = `${thread.firstName} ${thread.lastName} ${thread.email} ${thread.messages[thread.messages.length - 1]?.message || ''}`.toLowerCase();
return matchesTab && haystack.includes(search.toLowerCase());
});
const activeThread = filteredThreads.find((thread) => thread.patientId === selectedPatientId) || filteredThreads[0] || null;
const activeImage = activeThread?.imageUrl ? `${API_BASE}/${activeThread.imageUrl}` : '';
const activeInitials = activeThread ? `${activeThread.firstName?.[0] || ''}${activeThread.lastName?.[0] || ''}` : '';
const toggleStar = (event, patientId) => { event.stopPropagation(); setStarredPatients((current) => current.includes(patientId) ? current.filter((id) => id !== patientId) : [...current, patientId]); };
const displayDate = (value) => { if (!value) return 'Not provided'; const date = new Date(`${value}T00:00:00`); return Number.isNaN(date.getTime()) ? 'Not provided' : date.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }); };
const displayValue = (value) => value || 'Not provided';
useEffect(() => {
if (activeThread && activeThread.patientId !== selectedPatientId) onSelectPatient(activeThread.patientId);
}, [activeThread?.patientId, selectedPatientId]);
const tabs = [['unread', 'Unread', unreadThreads.length], ['all', 'All', threadGroups.length], ['recent', 'Recent', threadGroups.length], ['starred', 'Starred', starredPatients.length]];
const contactDetails = activeThread ? [
['Phone', activeThread.mobileNo],
['Email', activeThread.email],
['Date of birth', displayDate(activeThread.dateOfBirth)],
['Gender', activeThread.gender],
['City', activeThread.city],
['State', activeThread.state],
['ZIP code', activeThread.zipCode],
['Country', activeThread.country],
['Address', [activeThread.addressLine1, activeThread.addressLine2].filter(Boolean).join(', ')],
['Preferred contact', activeThread.preferredContact],
['Interested in', activeThread.serviceInterested],
['Status', activeThread.status],
['Emergency contact', [activeThread.emergencyName, activeThread.emergencyPhone, activeThread.emergencyRelationship].filter(Boolean).join(' · ')],
['Patient note', activeThread.note],
] : [];
return
setMobileNav(false)}>×
⌕ Search Ctrl K ⌂ Home ▣ Patient Process ♙ Patient List ◌ Conversations {unreadThreads.length > 0 && {unreadThreads.length} }□ Appointments
◉ Dashboard ⌁ Reporting ☷ Sign out
setMobileNav(true)}>
✣ Slack ⌕ ✦ ♟ {initials}
Conversations Conversations Templates Analytics Settings Trigger Links⌄ ⚙ Automation On {notice &&
{notice}
}{error &&
{error}
}
Team inbox {threadGroups.length} patient conversations
☷ ↕
{tabs.map(([value, label, count]) => setInboxTab(value)}>{label} {count} )}
Select all
event.preventDefault()}>⌕ setSearch(event.target.value)} placeholder="Search patients" /> {activeThread ? <>{activeImage ? : activeInitials} {activeThread.firstName} {activeThread.lastName} {activeThread.email}
◌ ⌕ ▱ toggleStar(event, activeThread.patientId)}>{starredPatients.includes(activeThread.patientId) ? '★' : '☆'} ✉
{activeThread.messages.map((item) =>
{item.sender_name} {formatDate(item.created_at)}
{item.message}
)}
☻⌄ setMessage(event.target.value)} placeholder="Type a message" rows="2" />{sending ? 'Sending...' : 'Send'} > : Select a patient conversation.
} {activeThread ? <>Patient Details ×
{activeImage ? : activeInitials}
{activeThread.firstName} {activeThread.lastName} {activeThread.email} {contactDetails.map(([label, value]) =>
{label} {displayValue(value)}
)}
> : Select a patient to view details.
}
;
}
function AgentPatientPipelinePageV2({ staff, onLogout, onOpenOverview, onOpenProcess, onOpenConversations, onOpenPatientConversation }) {
const [smartList, setSmartList] = useState('all');
const [status, setStatus] = useState('all');
const [service, setService] = useState('all');
const [sort, setSort] = useState('recent');
const [searchInput, setSearchInput] = useState('');
const [search, setSearch] = useState('');
const [filtersOpen, setFiltersOpen] = useState(false);
const [mobileNav, setMobileNav] = useState(false);
const [patients, setPatients] = useState([]);
const [smartCounts, setSmartCounts] = useState({});
const [services, setServices] = useState([]);
const [stages, setStages] = useState(PIPELINE_STAGE_LABELS);
const [unreadConversations, setUnreadConversations] = useState(0);
const [pagination, setPagination] = useState({ page: 1, pages: 1, total: 0 });
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [notice, setNotice] = useState('');
const initials = `${staff.first_name?.[0] || ''}${staff.last_name?.[0] || ''}`;
const stageOptions = Object.entries(stages).length ? Object.entries(stages) : Object.entries(PIPELINE_STAGE_LABELS);
const smartLists = [['all', 'All'], ['new_lead', 'New Lead'], ['scheduled', 'Consult Set'], ['vob', 'VOB'], ['contacted', 'Contacted'], ['first_follow_up', 'First Follow Up'], ['second_follow_up', 'Second Follow Up'], ['active', 'Active'], ['no_response', 'No Response']];
const loadPatients = async () => { setLoading(true); setError(''); const params = new URLSearchParams({ list: smartList, status, service, sort, q: search, page: String(page), per_page: '20' }); try { const result = await apiRequest(`staff_patients.php?${params.toString()}`); setPatients(result.patients || []); setSmartCounts(result.smart_counts || {}); setServices(result.services || []); setStages(result.stages || PIPELINE_STAGE_LABELS); setUnreadConversations(Number(result.unread_conversations || 0)); setPagination(result.pagination || {}); } catch (err) { setError(err.message); } finally { setLoading(false); } };
useEffect(() => { loadPatients(); }, [smartList, status, service, sort, search, page]);
const chooseList = (value) => { setSmartList(value); setStatus('all'); setPage(1); };
const applySearch = (event) => { event.preventDefault(); setPage(1); setSearch(searchInput.trim()); };
const updateStage = async (patient, stage, nextFollowUpAt = patient.next_follow_up_at || '') => { setError(''); setNotice(''); try { const result = await apiRequest('staff_update_stage.php', { method: 'POST', body: JSON.stringify({ patient_id: patient.id, stage, next_follow_up_at: nextFollowUpAt }) }); setNotice(result.message); await loadPatients(); } catch (err) { setError(err.message); } };
const imageFor = (patient) => patient.profile_image_url ? `${API_BASE}/${patient.profile_image_url}` : '';
const initialsFor = (patient) => `${patient.first_name?.[0] || ''}${patient.last_name?.[0] || ''}`;
const dateValue = (value) => value ? value.replace(' ', 'T').slice(0, 16) : '';
const dateLabel = (value) => value ? new Date(value.replace(' ', 'T')).toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }) : '—';
return
setMobileNav(false)}>×
⌕ Search Ctrl K ⌂ Home ▣ Patient Process ♙ Patient List ◌ Conversations {unreadConversations > 0 && {unreadConversations} }□ Appointments
◉ Dashboard ⌁ Reporting ☷ Sign out
setMobileNav(true)}>
✣ Slack ⌕ ✦ ♟ {initials}
Patient List Pipeline ⇩ Import + Add Contact ⋮
{notice && {notice}
}{error && {error}
}
Contacts {pagination.total || 0} Contacts {smartLists.map(([value, label]) => chooseList(value)} key={value}>☷ {label}{smartCounts[value] ?? 0} )}
{filtersOpen && Stage { setStatus(event.target.value); setSmartList('all'); setPage(1); }}>All stages {stageOptions.map(([value, label]) => {label} )} Service { setService(event.target.value); setPage(1); }}>All services {services.map((item) => {item} )} { setStatus('all'); setService('all'); setSmartList('all'); setSearchInput(''); setSearch(''); setPage(1); }}>Clear filters
}Page {pagination.page || 1} of {pagination.pages || 1} 20⌄ setPage((current) => Math.max(1, current - 1))}>Prev {pagination.page || 1} = (pagination.pages || 1)} onClick={() => setPage((current) => current + 1)}>Next
;
}
function AgentOverviewPage({ staff, onLogout, onProcess, onPatientList, onConversations, stats, recentPatients, loading, notice, error }) {
const [mobileNav, setMobileNav] = useState(false);
const initials = `${staff.first_name?.[0] || ''}${staff.last_name?.[0] || ''}`;
useProcessNavOverride(onProcess);
return
setMobileNav(false)}>×
⌕ Search Ctrl K setMobileNav(false)}>⌂ Home ▣ Patient Process ♙ Patient List ◌ Conversations {stats.open_conversations > 0 && {stats.open_conversations} }□ Appointments
setMobileNav(false)}>◉ Dashboard ⌁ Reporting ☷ Sign out
setMobileNav(true)}>
✣ Slack ⌕ ✦ ♟ {initials}
Dashboard Overview Patient List Conversations
{notice && {notice}
}{error && {error}
}Patient activity
Recent patients View all patients {loading ? Loading patients...
: recentPatients.length === 0 ? No patient registrations yet.
: {recentPatients.map((patient) => { const image = patient.profile_image_url ? `${API_BASE}/${patient.profile_image_url}` : ''; const patientInitials = `${patient.first_name?.[0] || ''}${patient.last_name?.[0] || ''}`; return
{image ? : patientInitials} {patient.first_name} {patient.last_name} {patient.email} {patient.service_interested} {patient.status} ; })}
}Care team workspace
Stay close to every patient Review your patient list, follow up on new leads and respond to conversations from one workspace.
Open conversations
;
}
function StaffDashboard({ staff, onLogout }) {
const [section, setSection] = useState('patients');
const [mobileNav, setMobileNav] = useState(false);
const [stats, setStats] = useState({ total_patients: 0, new_patients: 0, open_conversations: 0, unread_notifications: 0 });
const [recentPatients, setRecentPatients] = useState([]);
const [conversations, setConversations] = useState([]);
const [selectedPatientId, setSelectedPatientId] = useState(null);
const [message, setMessage] = useState('');
const [notice, setNotice] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
const [sending, setSending] = useState(false);
const loadData = async () => {
try {
const [overview, conversationResult] = await Promise.all([apiRequest('staff_overview.php'), apiRequest('staff_conversations.php')]);
setStats(overview.stats || {});
setRecentPatients(overview.recent_patients || []);
setConversations(conversationResult.conversations || []);
const notificationResult = await apiRequest('notifications.php');
setStats((current) => ({ ...current, unread_notifications: Number(notificationResult.unread_count || 0) }));
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
const timer = window.setInterval(loadData, 15000);
return () => window.clearInterval(timer);
}, []);
const threadGroups = useMemo(() => {
const grouped = {};
conversations.forEach((item) => {
if (!grouped[item.patient_id]) grouped[item.patient_id] = { patientId: item.patient_id, firstName: item.patient_first_name, lastName: item.patient_last_name, mobileNo: item.patient_mobile_no, email: item.patient_email, serviceInterested: item.patient_service_interested, preferredContact: item.patient_preferred_contact_method, note: item.patient_note, status: item.patient_status, imageUrl: item.patient_profile_image_url, dateOfBirth: item.patient_date_of_birth, gender: item.patient_gender, addressLine1: item.patient_address_line1, addressLine2: item.patient_address_line2, city: item.patient_city, state: item.patient_state, zipCode: item.patient_zip_code, country: item.patient_country, emergencyName: item.patient_emergency_contact_name, emergencyPhone: item.patient_emergency_contact_phone, emergencyRelationship: item.patient_emergency_contact_relationship, messages: [] };
grouped[item.patient_id].messages.push(item);
});
return Object.values(grouped).sort((a, b) => new Date(b.messages[b.messages.length - 1].created_at) - new Date(a.messages[a.messages.length - 1].created_at));
}, [conversations]);
useEffect(() => {
if (!selectedPatientId && threadGroups[0]) setSelectedPatientId(threadGroups[0].patientId);
if (selectedPatientId && !threadGroups.some((thread) => thread.patientId === selectedPatientId)) setSelectedPatientId(threadGroups[0]?.patientId || null);
}, [threadGroups, selectedPatientId]);
const selectedThread = threadGroups.find((thread) => thread.patientId === selectedPatientId) || null;
const markThreadRead = async (patientId) => {
const hasUnread = conversations.some((item) => Number(item.patient_id) === Number(patientId) && item.sender_type === 'patient' && !Number(item.is_read));
if (!hasUnread) return;
setConversations((current) => current.map((item) => Number(item.patient_id) === Number(patientId) && item.sender_type === 'patient' ? { ...item, is_read: 1 } : item));
setStats((current) => ({ ...current, open_conversations: Math.max(0, Number(current.open_conversations || 0) - 1) }));
await apiRequest('staff_mark_read.php', { method: 'POST', body: JSON.stringify({ patient_id: patientId }) }).catch(() => {});
};
useEffect(() => {
if (selectedPatientId) markThreadRead(selectedPatientId);
}, [selectedPatientId]);
const sendReply = async (event) => {
event.preventDefault();
if (!selectedThread || !message.trim()) return;
setSending(true); setError(''); setNotice('');
try {
const result = await apiRequest('admin_message.php', { method: 'POST', body: JSON.stringify({ patient_id: selectedThread.patientId, message }) });
setMessage(''); setNotice(result.message); await loadData();
} catch (err) { setError(err.message); }
finally { setSending(false); }
};
const markAllNotifications = async () => {
await apiRequest('notifications.php', { method: 'POST', body: JSON.stringify({ all: true }) }).catch(() => {});
setStats((current) => ({ ...current, unread_notifications: 0 }));
};
const initials = `${staff.first_name?.[0] || ''}${staff.last_name?.[0] || ''}`;
const sectionTitle = section === 'conversations' ? 'Patient conversations' : 'Staff dashboard';
if (section === 'patients') return setSection('overview')} onOpenProcess={() => setSection('process')} onOpenConversations={() => setSection('conversations')} onOpenPatientConversation={(patientId) => { setSelectedPatientId(patientId); setSection('conversations'); }} />;
if (section === 'process') return setSection('overview')} onOpenPatients={() => setSection('patients')} onOpenConversations={() => setSection('conversations')} />;
if (section === 'conversations') return setSection('overview')} onProcess={() => setSection('process')} onPatientList={() => setSection('patients')} stats={stats} threadGroups={threadGroups} selectedThread={selectedThread} selectedPatientId={selectedPatientId} onSelectPatient={setSelectedPatientId} message={message} setMessage={setMessage} sendReply={sendReply} notice={notice} error={error} sending={sending} />;
if (section === 'overview') return setSection('process')} onPatientList={() => setSection('patients')} onConversations={() => setSection('conversations')} stats={stats} recentPatients={recentPatients} loading={loading} notice={notice} error={error} />;
return
setMobileNav(true)}> Renovar care team
{sectionTitle} {stats.unread_notifications > 0 && {stats.unread_notifications} }{initials} {staff.first_name} {staff.last_name} Agent
{notice && {notice}
}{error && {error}
}{section === 'overview' && <>Patient activity
Recent patients setSection('conversations')}>Open conversations {loading ? Loading patients...
: recentPatients.length === 0 ? No patient registrations yet.
: {recentPatients.map((patient) => { const image = patient.profile_image_url ? `${API_BASE}/${patient.profile_image_url}` : ''; const patientInitials = `${patient.first_name?.[0] || ''}${patient.last_name?.[0] || ''}`; return
{ setSelectedPatientId(patient.id); setSection('conversations'); }}>{image ? : patientInitials} {patient.first_name} {patient.last_name} {patient.email} {patient.service_interested} {patient.status} ; })}
}Care team inbox
Stay close to every patient Review portal conversations and send a secure reply directly to the patient.
setSection('conversations')}>View conversations >}{section === 'conversations' && Team inbox
Patient conversations {threadGroups.length} patients {threadGroups.length === 0 ?
No conversations yet.
: threadGroups.map((thread) => { const image = thread.imageUrl ? `${API_BASE}/${thread.imageUrl}` : ''; const lastMessage = thread.messages[thread.messages.length - 1]; const threadInitials = `${thread.firstName?.[0] || ''}${thread.lastName?.[0] || ''}`; return
setSelectedPatientId(thread.patientId)}>{image ? : threadInitials} {thread.firstName} {thread.lastName} {lastMessage.message} {formatDate(lastMessage.created_at)} ; })}
{selectedThread ? <>
{selectedThread.imageUrl ? : `${selectedThread.firstName?.[0] || ''}${selectedThread.lastName?.[0] || ''}`} {selectedThread.firstName} {selectedThread.lastName} {selectedThread.email}
{selectedThread.messages.map((item) =>
{item.sender_name} {formatDate(item.created_at)}
{item.message}
)}
setMessage(event.target.value)} placeholder="Write a reply to the patient..." rows="2" />{sending ? 'Sending...' : 'Send reply'} > :
Select a patient conversation.
}
}
;
}
function Field({ label, name, type = 'text', value, onChange, placeholder, error, required = false }) {
return {label}{required && * } {error && {error} } ;
}
function SelectField({ label, name, value, onChange, error, options, required = false }) {
return {label}{required && * }Select an option {options.map((option) => {option} )} {error && {error} } ;
}
function Alert({ message }) { return message ? {message}
: null; }
const emptyProfile = { date_of_birth: '', gender: '', address_line1: '', address_line2: '', city: '', state: '', zip_code: '', country: 'United States', emergency_contact_name: '', emergency_contact_phone: '', emergency_contact_relationship: '', profile_image: '', profile_image_url: '' };
function PatientProfilePage({ patient, onImageChange }) {
const [profile, setProfile] = useState(emptyProfile);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [uploading, setUploading] = useState(false);
const [notice, setNotice] = useState('');
const [error, setError] = useState('');
const [passwordForm, setPasswordForm] = useState({ current_password: '', new_password: '', confirm_password: '' });
const [passwordSaving, setPasswordSaving] = useState(false);
const [passwordNotice, setPasswordNotice] = useState('');
const [passwordError, setPasswordError] = useState('');
const [passwordFieldErrors, setPasswordFieldErrors] = useState({});
useEffect(() => {
apiRequest('profile.php').then((result) => {
setProfile({ ...emptyProfile, ...(result.profile || {}) });
}).catch((err) => setError(err.message)).finally(() => setLoading(false));
}, []);
const update = (event) => setProfile({ ...profile, [event.target.name]: event.target.value });
const saveProfile = async (event) => {
event.preventDefault(); setError(''); setNotice(''); setSaving(true);
try {
const result = await apiRequest('profile.php', { method: 'POST', body: JSON.stringify(profile) });
setProfile({ ...emptyProfile, ...(result.profile || {}) });
setNotice(result.message);
} catch (err) {
setError(err.message);
} finally {
setSaving(false);
}
};
const uploadPhoto = async (event) => {
const file = event.target.files?.[0];
event.target.value = '';
if (!file) return;
if (file.size > 5 * 1024 * 1024) { setError('Profile picture must be 5 MB or smaller.'); return; }
setError(''); setNotice(''); setUploading(true);
const formData = new FormData();
formData.append('profile_image', file);
try {
const result = await apiRequest('profile_image.php', { method: 'POST', body: formData });
setProfile((current) => ({ ...current, profile_image: result.profile_image, profile_image_url: result.profile_image_url }));
if (onImageChange) onImageChange(result.profile_image_url || '');
setNotice(result.message);
} catch (err) {
setError(err.message);
} finally {
setUploading(false);
}
};
const changePassword = async (event) => {
event.preventDefault(); setPasswordError(''); setPasswordNotice(''); setPasswordFieldErrors({}); setPasswordSaving(true);
try {
const result = await apiRequest('change_password.php', { method: 'POST', body: JSON.stringify(passwordForm) });
setPasswordForm({ current_password: '', new_password: '', confirm_password: '' });
setPasswordNotice(result.message);
} catch (err) {
setPasswordError(err.message);
setPasswordFieldErrors(err.payload?.errors || {});
} finally {
setPasswordSaving(false);
}
};
const imageSource = profile.profile_image_url ? `${API_BASE}/${profile.profile_image_url}` : '';
const initials = `${patient.first_name?.[0] || ''}${patient.last_name?.[0] || ''}`;
if (loading) return ;
return
Your information
My profile Keep your contact and care information up to date so our team can support you properly.
Private & secure
{notice && {notice}
}
{error && {error}
}
{imageSource ?
:
{initials} }
{patient.first_name} {patient.last_name} Patient account
{uploading ? 'Uploading...' : 'Upload picture'} JPG, PNG or WEBP up to 5 MB
Personal details
Tell us a little more {saving ? 'Saving...' : 'Optional details'}
Registered email {patient.email}
Emergency contact
Optional, but helpful in case our team needs to reach someone for you.
{saving ? 'Saving changes...' : 'Save profile'}
Account security
Change password Secure {passwordNotice && {passwordNotice}
}{passwordError && {passwordError}
} setPasswordForm({ ...passwordForm, current_password: e.target.value })} error={passwordFieldErrors.current_password} required /> setPasswordForm({ ...passwordForm, new_password: e.target.value })} placeholder="At least 8 characters" error={passwordFieldErrors.new_password} required /> setPasswordForm({ ...passwordForm, confirm_password: e.target.value })} error={passwordFieldErrors.confirm_password} required />
{passwordSaving ? 'Changing password...' : 'Change password'}
;
}
function PatientDashboard({ patient, onLogout }) {
const [conversations, setConversations] = useState([]); const [message, setMessage] = useState(''); const [notice, setNotice] = useState(''); const [loading, setLoading] = useState(true); const [sending, setSending] = useState(false);
const [mobileNav, setMobileNav] = useState(false);
const loadConversations = async () => { try { const result = await apiRequest('conversations.php'); setConversations(result.conversations || []); } catch { setNotice('Conversation history is temporarily unavailable.'); } finally { setLoading(false); } };
useEffect(() => { loadConversations(); }, []);
const sendMessage = async (event) => { event.preventDefault(); if (!message.trim()) return; setSending(true); setNotice(''); try { await apiRequest('conversations.php', { method: 'POST', body: JSON.stringify({ message }) }); setMessage(''); setNotice('Your message has been added to the conversation.'); await loadConversations(); } catch (err) { setNotice(err.message); } finally { setSending(false); } };
const statusLabel = patient.status === 'new' ? 'Registration received' : patient.status;
return
Welcome to your care space
You have taken an important first step. Your care team has received your registration. We will review your information and contact you using your preferred method.
Your care journey
Next steps, made simple {statusLabel}
Your request
Care preferences
We will reach you through your selected contact method.
Stay connected
Your conversations {conversations.length} {conversations.length === 1 ? 'message' : 'messages'} {notice && {notice}
}{loading ?
Loading your messages...
: conversations.length === 0 ?
Your care team is here for you. Send a message below if you have a question or would like to share more about your journey.
:
{conversations.map((item) =>
{item.sender_name} {formatDate(item.created_at)}
{item.message}
)}
}
setMessage(e.target.value)} placeholder="Write a message to your care team..." rows="2" />{sending ? 'Sending...' : 'Send message'} ;
}
function PatientDashboardWithNotifications({ patient, onLogout }) {
const [conversations, setConversations] = useState([]);
const [message, setMessage] = useState('');
const [notice, setNotice] = useState('');
const [loading, setLoading] = useState(true);
const [sending, setSending] = useState(false);
const [mobileNav, setMobileNav] = useState(false);
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
const [unreadConversationCount, setUnreadConversationCount] = useState(0);
const [notificationOpen, setNotificationOpen] = useState(false);
const [pushState, setPushState] = useState(initialPushState);
const [pushBusy, setPushBusy] = useState(false);
const [section, setSection] = useState('overview');
const [profileImageUrl, setProfileImageUrl] = useState(patient.profile_image_url || '');
const loadConversations = async () => {
try {
const result = await apiRequest('conversations.php');
const nextConversations = result.conversations || [];
setConversations(nextConversations);
setUnreadConversationCount(Number(result.unread_count ?? nextConversations.filter((item) => item.sender_type !== 'patient' && !Number(item.is_read)).length));
} catch {
setNotice('Conversation history is temporarily unavailable.');
} finally {
setLoading(false);
}
};
const loadNotifications = async () => {
try {
const result = await apiRequest('notifications.php');
setNotifications(result.notifications || []);
setUnreadCount(Number(result.unread_count || 0));
} catch {
// Keep the dashboard usable if notifications are temporarily unavailable.
}
};
useEffect(() => {
loadConversations();
loadNotifications();
const timer = window.setInterval(() => {
loadConversations();
loadNotifications();
}, 15000);
return () => window.clearInterval(timer);
}, []);
useEffect(() => {
if (!('serviceWorker' in navigator)) return undefined;
navigator.serviceWorker.ready.then(async (registration) => {
const subscription = await registration.pushManager.getSubscription();
if (subscription) setPushState('granted');
}).catch(() => {});
return undefined;
}, []);
useEffect(() => {
apiRequest('profile.php')
.then((result) => setProfileImageUrl(result.profile?.profile_image_url || ''))
.catch(() => {});
}, [section]);
const sendMessage = async (event) => {
event.preventDefault();
if (!message.trim()) return;
setSending(true);
setNotice('');
try {
await apiRequest('conversations.php', { method: 'POST', body: JSON.stringify({ message }) });
setMessage('');
setNotice('Your message has been added to the conversation.');
await loadConversations();
} catch (err) {
setNotice(err.message);
} finally {
setSending(false);
}
};
const handleEnablePush = async () => {
setPushBusy(true);
setNotice('');
try {
await enableBrowserPush();
setPushState('granted');
setNotice('Browser notifications are now enabled on this device.');
} catch (err) {
setPushState(typeof Notification !== 'undefined' && Notification.permission === 'denied' ? 'denied' : pushState);
setNotice(err.message);
} finally {
setPushBusy(false);
}
};
const markNotification = async (item) => {
if (!Number(item.is_read)) {
await apiRequest('notifications.php', { method: 'POST', body: JSON.stringify({ id: item.id }) }).catch(() => {});
setNotifications((items) => items.map((entry) => entry.id === item.id ? { ...entry, is_read: 1 } : entry));
setUnreadCount((count) => Math.max(0, count - 1));
if (item.conversation_id) {
setConversations((items) => items.map((entry) => entry.id === item.conversation_id ? { ...entry, is_read: 1 } : entry));
setUnreadConversationCount((count) => Math.max(0, count - 1));
}
}
if (item.link && item.link !== './') window.location.href = item.link;
};
const markAllNotifications = async () => {
await apiRequest('notifications.php', { method: 'POST', body: JSON.stringify({ all: true }) }).catch(() => {});
setNotifications((items) => items.map((item) => ({ ...item, is_read: 1 })));
setConversations((items) => items.map((item) => item.sender_type !== 'patient' ? { ...item, is_read: 1 } : item));
setUnreadCount(0);
setUnreadConversationCount(0);
};
const statusLabel = patient.status === 'new' ? 'Registration received' : patient.status;
const showPushButton = pushState === 'default' || pushState === 'denied';
const profileImageSource = profileImageUrl ? `${API_BASE}/${profileImageUrl}` : '';
const patientInitials = `${patient.first_name?.[0] || ''}${patient.last_name?.[0] || ''}`;
return
{section === 'profile' && }
{pushState === 'denied' && Notifications are blocked in this browser. Allow them in your browser settings to receive new message alerts.
}
Welcome to your care space
You have taken an important first step. Your care team has received your registration. We will review your information and contact you using your preferred method.
Your care journey
Next steps, made simple {statusLabel}
Your request
Care preferences
We will reach you through your selected contact method.
Stay connected
Your conversations {unreadConversationCount} unread {unreadConversationCount === 1 ? 'message' : 'messages'} {notice && {notice}
}{loading ?
Loading your messages...
: conversations.length === 0 ?
Your care team is here for you. Send a message below if you have a question or would like to share more about your journey.
:
{conversations.map((item) => { const unreadMessage = item.sender_type !== 'patient' && !Number(item.is_read); return
{item.sender_type === 'patient' &&
{profileImageSource ? : patientInitials} }
{item.sender_name} {unreadMessage && Unread }{formatDate(item.created_at)}
{item.message}
; })}
}
setMessage(e.target.value)} placeholder="Write a message to your care team..." rows="2" />{sending ? 'Sending...' : 'Send message'}
;
}
function JourneyStep({ done, title, text }) { return ; }
function Detail({ label, value }) { return {label} {value}
; }
function formatDate(date) { return new Date(date.replace(' ', 'T')).toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); }
const PIPELINE_STAGE_LABELS = { new: 'New Lead', contacted: 'Contacted', scheduled: 'Consult Set', vob: 'VOB', first_follow_up: 'First Follow Up', second_follow_up: 'Second Follow Up', active: 'Active', no_response: 'No Response', closed: 'Closed' };
function pipelineStageLabel(stage) { return PIPELINE_STAGE_LABELS[stage] || stage || 'Not set'; }
function App() {
const query = new URLSearchParams(window.location.search);
const resetTokenFromUrl = query.get('reset') || '';
const adminRequested = query.get('admin') === '1';
const staffRequested = query.get('staff') === '1';
const [view, setView] = useState(resetTokenFromUrl ? 'auth' : adminRequested ? 'admin-login' : staffRequested ? 'staff-login' : 'home'); const [authMode, setAuthMode] = useState(resetTokenFromUrl ? 'reset' : 'register'); const [resetToken, setResetToken] = useState(resetTokenFromUrl); const [patient, setPatient] = useState(null); const [checkingSession, setCheckingSession] = useState(true);
useEffect(() => { if ('serviceWorker' in navigator) navigator.serviceWorker.register('./sw.js').catch(() => {}); }, []);
useEffect(() => { apiRequest('me.php').then((result) => { if (resetTokenFromUrl) return; if (result.patient.role === 'admin') { setPatient(result.patient); setView('admin-placeholder'); } else if (result.patient.role === 'agent') { setPatient(result.patient); setView('staff-dashboard'); } else if (!adminRequested && !staffRequested) { setPatient(result.patient); setView('dashboard'); } }).catch(() => {}).finally(() => setCheckingSession(false)); }, []);
const openAuth = (mode) => { setResetToken(''); setAuthMode(mode); setView('auth'); window.scrollTo(0, 0); };
const openAdminLogin = () => { setPatient(null); setView('admin-login'); window.history.replaceState({}, document.title, `${window.location.pathname}?admin=1`); window.scrollTo(0, 0); };
const openStaffLogin = () => { setPatient(null); setView('staff-login'); window.history.replaceState({}, document.title, `${window.location.pathname}?staff=1`); window.scrollTo(0, 0); };
const logout = async () => { try { await apiRequest('logout.php', { method: 'POST' }); } finally { setPatient(null); setView('home'); window.history.replaceState({}, document.title, window.location.pathname); } };
const leaveAdminLogin = () => { setView('home'); window.history.replaceState({}, document.title, window.location.pathname); };
const leaveStaffLogin = () => { setView('home'); window.history.replaceState({}, document.title, window.location.pathname); };
if (checkingSession) return R
Loading your care space... ;
if (view === 'admin-login') return { setPatient(account); setView('admin-placeholder'); }} onHome={leaveAdminLogin} />;
if (view === 'admin-placeholder' && patient) return ;
if (view === 'staff-login') return { setPatient(account); setView('staff-dashboard'); }} onHome={leaveStaffLogin} />;
if (view === 'staff-dashboard' && patient) return ;
if (view === 'dashboard' && patient) return ;
if (view === 'auth') return { setPatient(account); setView('dashboard'); }} onHome={() => setView('home')} />;
return openAuth('login')} onRegister={() => openAuth('register')} onAdminLogin={openAdminLogin} onStaffLogin={openStaffLogin} />;
}
ReactDOM.createRoot(document.getElementById('root')).render( );