import React, { useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { Inbox, BookOpen, Settings, Activity as ActivityIcon, Search, ArrowUpRight, ChevronDown, Check, CheckCheck, Clock3, FileText, LogOut, RefreshCw, ArrowLeft, Plus, X, Mail, ShieldCheck, Save, CircleHelp, Building2, ChevronRight } from 'lucide-react'; import { api, session, type Session, type Hotel, type Conversation, type Knowledge, type Activity, type Mailboxes } from './api'; import './style.css'; import { ReplyActions, ReplyControls } from './ReplyActions'; const labels: Record = { NeedsAttention: 'Needs attention', DraftReady: 'Draft ready', Completed: 'Completed' }; const initials = (name: string) => name.replace(/<.*>/, '').trim().split(' ').filter(Boolean).slice(0,2).map(x => x[0]).join('').toUpperCase(); const sender = (name: string) => name.replace(/<.*>/, '').replaceAll('"', '').trim(); const date = (value: string) => new Date(value).toLocaleString(undefined, { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }); function App() { const [auth,setAuth] = useState(null), [error,setError] = useState(''), [notice,setNotice] = useState(''); const [hotel,setHotel] = useState(null), [page,setPage] = useState(location.pathname === '/' ? '/inbox' : location.pathname), [busy,setBusy] = useState(false); const [conversations,setConversations] = useState([]), [knowledge,setKnowledge] = useState([]), [activity,setActivity] = useState([]), [mailboxes,setMailboxes] = useState({ configured:false,items:[] }); const [loaded,setLoaded] = useState(false); async function refresh() { const [h,c,k,a,m] = await Promise.all([api('/hotel'),api('/conversations'),api('/knowledge'),api('/activity'),api('/mailboxes')]); setHotel(h);setConversations(c);setKnowledge(k);setActivity(a);setMailboxes(m);setLoaded(true); } useEffect(() => { session().then(setAuth).catch(e=>setError(e.message)); const expired=()=>{setAuth(null);session().then(setAuth).catch(()=>{});setError('Your session has ended. Sign in again.');}; window.addEventListener('session-expired',expired); const pop=()=>setPage(location.pathname);window.addEventListener('popstate',pop);return()=>{window.removeEventListener('session-expired',expired);window.removeEventListener('popstate',pop);}; },[]); useEffect(()=>{if(auth?.user) refresh().catch(e=>setError(e.message));},[auth?.user?.id]); useEffect(()=>{if(!notice)return;const timer=setTimeout(()=>setNotice(''),4500);return()=>clearTimeout(timer);},[notice]); async function run(action:()=>Promise) { if(busy)return; setBusy(true);setError('');try{await action();}catch(e){setError(e instanceof Error?e.message:'Something went wrong.');}finally{setBusy(false);} } function go(path:string) { if (!window.dispatchEvent(new Event('workspace-navigate', { cancelable:true }))) return; setPage(path);history.pushState({},'',path);setError(''); } async function login(email:string,password:string) { await run(async()=>{await api('/auth/login','POST',{email,password});setAuth(await session());}); } async function preview() { await run(async()=>{await api('/preview/start','POST');setAuth(await session());}); } async function logout() { await run(async()=>{await api('/auth/logout','POST');setAuth(await session());setHotel(null);setLoaded(false);}); } const errorBox=error?
{error}
:null; if(!auth) return
g

Opening your workspace…

{errorBox}
; if(!auth.user) return ; const count=conversations.filter(c=>c.status!=='Completed').length; return
Workspace{page==='/inbox'?'Inbox':page==='/knowledge'?'Hotel knowledge':page==='/activity'?'Activity':'Settings'}
{auth.preview&&Preview · sample data}{hotel?.staffSendingEnabled?'Staff-approved sending':'Draft-only mode'}
{errorBox}{notice&&
{notice}
} {!loaded?

Loading your hotel…

:page==='/inbox'?setConversations(old=>old.map(x=>x.id===c.id?c:x))} notify={setNotice} go={go}/>:page==='/knowledge'?setKnowledge(old=>old.some(x=>x.id===item.id)?old.map(x=>x.id===item.id?item:x):[...old,item])} notify={setNotice}/>:page==='/activity'?
{activity.length?activity.map(a=>
{a.action}

{a.userName}

):}
:{setHotel(h);setNotice('Hotel settings saved.');}}/>}
; } function PageHeading({eyebrow,title,text}:{eyebrow:string;title:string;text:string}) { return
{eyebrow}

{title}

{text}

; } function Empty({title,text}:{title:string;text:string}) {return

{title}

{text}

;} function Login({preview,onLogin,onPreview,busy,error}:{preview:boolean;onLogin:(e:string,p:string)=>Promise;onPreview:()=>Promise;busy:boolean;error:React.ReactNode}){ const [email,setEmail]=useState(''),[password,setPassword]=useState(''); return
gguestops.
A little more time for your guests

Great hospitality.
A calmer inbox.

Your conversations, hotel knowledge and team.
Together in one thoughtful workspace.

Less time sorting emails.
More time making guests feel welcome.
Built around the way hotels work.
Your hotel workspace

Welcome back

Sign in to take care of your guests.

{error}
{e.preventDefault();onLogin(email,password);}}>

Need access or help signing in? Contact your hotel administrator.

{preview&&
Explore the interface with sample conversations.Preview changes are temporary. No real emails are sent.
}
; } type Run=(a:()=>Promise)=>Promise; function InboxPage({hotel,mailboxes,conversations,knowledge,busy,run,onUpdate,notify,go}:{hotel:Hotel;mailboxes:Mailboxes;conversations:Conversation[];knowledge:Knowledge[];busy:boolean;run:Run;onUpdate:(c:Conversation)=>void;notify:(s:string)=>void;go:(s:string)=>void}){ const [filter,setFilter]=useState('All'),[search,setSearch]=useState(''),[selected,setSelected]=useState(null),[draft,setDraft]=useState(''),[mobileDetail,setMobileDetail]=useState(false); const filtered=conversations.filter(c=>(filter==='All'||c.status===filter)&&(c.subject+' '+c.from+' '+c.body).toLowerCase().includes(search.toLowerCase())); const current=filtered.find(c=>c.id===selected)||filtered[0]; useEffect(()=>{setDraft(current?.draft||'');},[current?.id,current?.draft]); useEffect(()=>{if(!current || draft===current.draft)return;const leave=(e:BeforeUnloadEvent)=>{e.preventDefault();};const navigate=(e:Event)=>{if(!window.confirm('Discard your unsaved draft changes?'))e.preventDefault();};window.addEventListener('beforeunload',leave);window.addEventListener('workspace-navigate',navigate);return()=>{window.removeEventListener('beforeunload',leave);window.removeEventListener('workspace-navigate',navigate);};},[current?.id,current?.draft,draft]); const needs=conversations.filter(c=>c.status==='NeedsAttention').length,ready=conversations.filter(c=>c.status==='DraftReady').length; function select(c:Conversation) { if(current&&draft!==current.draft&&!window.confirm('Discard your unsaved draft changes?'))return;setSelected(c.id);setMobileDetail(true); } async function save() {if(!current)return;await run(async()=>{onUpdate(await api(`/conversations/${current.id}/draft`,'PUT',{draft,version:current.version}));notify('Draft saved. Nothing has been sent.');});} async function resolve() {if(!current)return;await run(async()=>{onUpdate(await api(`/conversations/${current.id}/status`,'PUT',{status:current.status==='Completed'?'NeedsAttention':'Completed',version:current.version}));notify(current.status==='Completed'?'Conversation reopened.':'Conversation marked completed.');});} return
{needs}Need attention
{ready}Drafts ready
{[['All','All messages'],['NeedsAttention','Needs attention'],['DraftReady','Drafts'],['Completed','Completed']].map(([key,label])=>)}
{!conversations.length?
:
{filtered.length} conversation{filtered.length===1?'':'s'}Newest first
{filtered.map((c,i)=>)}{!filtered.length&&}
{current?<>
{current.category}

{current.subject}

{initials(sender(current.from))}
{sender(current.from)}To your hotel · {date(current.receivedAt)}
{current.body}
{current.note&&
{current.note}
}
Your reply draftOnly visible to your team