30 lines
8.4 KiB
TypeScript
30 lines
8.4 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { api, type Hotel } from './api';
|
||
import { hotelTime } from './time';
|
||
type Snapshot={id:string;reservationId:string;confirmation:string;guestName:string;arrival:string;departure:string;status:string;roomType:string;total:string;fetchedAt:string};
|
||
type Change={id:string;reservationId:string;kind:string;arrival:string;departure:string;note:string;state:string;detail:string;version:number;updatedAt:string;expiresAt:string;before:Snapshot;after:Snapshot|null};
|
||
type Connection={configured:boolean;writesConfigured:boolean;hotelCode:string;preview:boolean};
|
||
type Props={hotel:Hotel;owner:boolean;canPropose:boolean;busy:boolean;run:(f:()=>Promise<void>)=>Promise<void>;onHotel:(h:Hotel)=>void};
|
||
export function PmsPage({hotel,owner,canPropose,busy,run,onHotel}:Props){
|
||
const [connection,setConnection]=useState<Connection|null>(null),[changes,setChanges]=useState<Change[]>([]),[confirmation,setConfirmation]=useState(''),[snapshot,setSnapshot]=useState<Snapshot|null>(null);
|
||
const [kind,setKind]=useState('AddNote'),[arrival,setArrival]=useState(''),[departure,setDeparture]=useState(''),[note,setNote]=useState(''),[selected,setSelected]=useState<string|null>(null),[checked,setChecked]=useState(false);
|
||
async function refresh(){const [status,history]=await Promise.all([api<Connection>('/pms/status'),api<Change[]>('/pms/changes')]);setConnection(status);setChanges(history);}
|
||
useEffect(()=>{run(refresh);},[hotel.id]);
|
||
const current=changes.find(c=>c.id===selected);
|
||
function update(change:Change){setChanges(old=>[change,...old.filter(c=>c.id!==change.id)]);setSelected(change.id);setChecked(false);}
|
||
async function lookup(e:React.FormEvent){e.preventDefault();await run(async()=>{const found=await api<Snapshot>('/pms/lookup','POST',{confirmation:confirmation.trim()});setSnapshot(found);setArrival(found.arrival);setDeparture(found.departure);setSelected(null);setNote('');setChecked(false);});}
|
||
async function propose(e:React.FormEvent){e.preventDefault();if(!snapshot)return;await run(async()=>update(await api<Change>('/pms/changes','POST',{snapshotId:snapshot.id,kind,arrival,departure,note})));}
|
||
async function action(name:string){if(!current)return;await run(async()=>{
|
||
if(name==='apply'&&!window.confirm(`Apply this change to OHIP reservation ${current.before.confirmation} for ${current.before.guestName||'the displayed guest'}?\n\n${current.kind==='StayDates'?`${current.before.arrival} – ${current.before.departure} → ${current.arrival} – ${current.departure}`:current.note}\n\nThis changes the live PMS selected by your administrator.`))return;
|
||
update(await api<Change>(`/pms/changes/${current.id}/${name}`,'POST',{version:current.version,availabilityAndPriceChecked:checked}));
|
||
});}
|
||
return <div className="page pms-page"><div className="heading-row"><div className="page-heading"><span className="eyebrow">Booking details, close at hand</span><h1>Reservations</h1><p>Look up a booking and review changes before applying them to your PMS.</p></div><button className="button secondary" disabled={busy} onClick={()=>run(refresh)}>Refresh status</button></div>
|
||
<section className="settings-card"><h2>OHIP connection</h2><p>{connection?.configured?`Connected configuration for property ${connection.hotelCode}. Lookup will verify access.`:connection?.preview?'Sample workspace: real PMS lookup and updates are disabled.':'Your administrator needs to configure this hotel’s OHIP connection.'}</p><p className="small muted">GuestOps hotel ID: {hotel.id}</p><label className="checkbox-label"><input type="checkbox" checked={hotel.pmsUpdatesEnabled||false} disabled={busy||!owner||(!connection?.writesConfigured&&!hotel.pmsUpdatesEnabled)} onChange={e=>{const enabled=e.target.checked;run(async()=>{if(enabled&&!window.confirm('Enable staff-approved PMS updates for this hotel? Only enable this after the configured OHIP sandbox has passed acceptance checks.'))return;onHotel(await api<Hotel>('/pms/controls','PUT',{version:hotel.version,enabled}));});}}/>Allow owner-approved PMS updates</label><p className="small muted">Lookup is available separately. Every change needs review; automatic PMS updates are off.</p></section>
|
||
<div className="pms-columns"><section className="settings-card"><h2>Find a reservation</h2><form onSubmit={lookup}><label>Exact confirmation number<input value={confirmation} maxLength={80} pattern="[a-zA-Z0-9-]+" required onChange={e=>setConfirmation(e.target.value)} placeholder="For example, 12345678"/></label><button className="button primary" disabled={busy||!connection?.configured}>Look up reservation</button></form>
|
||
{snapshot&&<><Reservation value={snapshot}/><form onSubmit={propose}><fieldset disabled={busy||!canPropose}><label>Proposed action<select value={kind} onChange={e=>setKind(e.target.value)}><option value="AddNote">Add an internal reservation note</option><option value="StayDates">Change stay dates</option></select></label>{kind==='AddNote'?<label>Note<textarea rows={4} maxLength={2000} value={note} required onChange={e=>setNote(e.target.value)}/></label>:<div className="form-grid"><label>New arrival<input type="date" value={arrival} required onChange={e=>setArrival(e.target.value)}/></label><label>New departure<input type="date" value={departure} required onChange={e=>setDeparture(e.target.value)}/></label></div>}<button className="button secondary">Prepare for review</button></fieldset></form><p className="small muted">Preparing a proposal does not update the PMS. Lookup and proposals expire after ten minutes.</p></>}
|
||
</section><section className="settings-card"><h2>Change history</h2>{!changes.length&&<p>No PMS changes have been prepared yet.</p>}<div className="pms-history">{changes.map(c=><button className={'pms-history-item '+(selected===c.id?'selected':'')} key={c.id} onClick={()=>{setSelected(c.id);setChecked(false);}}><strong>{c.before.confirmation} · {c.kind==='AddNote'?'Reservation note':'Stay dates'}</strong><span>{c.state==='NeedsReview'?'Needs verification':c.state} · {hotelTime(c.updatedAt,hotel.timezone)}</span></button>)}</div></section></div>
|
||
{current&&<section className="settings-card pms-review" aria-label="PMS change review"><h2>{current.state==='Review'?'Review this PMS change':'PMS change status'}</h2><Reservation value={current.before}/><div className="staff-note">{current.kind==='StayDates'?`Requested stay: ${current.arrival} to ${current.departure}`:`Add internal note: ${current.note}`}</div><p role="status"><strong>{current.state==='NeedsReview'?'Needs verification':current.state}</strong> — {current.detail}</p>{current.after&&<><h3>Latest observed PMS state</h3><Reservation value={current.after}/></>}{current.state==='Review'&&<>{current.kind==='StayDates'&&<label className="checkbox-label"><input type="checkbox" checked={checked} onChange={e=>setChecked(e.target.checked)}/>I checked availability, rate consequences and guest agreement in the PMS. GuestOps does not quote or guarantee a new price here.</label>}<div className="form-actions"><button className="button secondary" disabled={busy||!owner||connection?.preview} onClick={()=>action('cancel')}>Cancel proposal</button><button className="button primary" disabled={busy||!owner||!hotel.pmsUpdatesEnabled||!connection?.writesConfigured||(current.kind==='StayDates'&&!checked)||new Date(current.expiresAt)<new Date()} onClick={()=>action('apply')}>Approve and apply to PMS</button></div></>}{(current.state==='NeedsReview'||current.state==='Applying')&&<><button className="button secondary" disabled={busy||!owner||(current.state==='Applying'&&Date.now()-new Date(current.updatedAt).getTime()<300000)} onClick={()=>action('verify')}>Verify current PMS state</button><p className="small muted">An interrupted update can be checked after five minutes. Verification only reads the PMS; it never repeats the update.</p></>}<p className="small muted">Operation reference: {current.id}</p></section>}
|
||
</div>;
|
||
}
|
||
function Reservation({value}:{value:Snapshot}){return <dl className="pms-reservation"><div><dt>Confirmation</dt><dd>{value.confirmation}</dd></div><div><dt>Guest</dt><dd>{value.guestName||'Not returned by PMS — verify guest identity there'}</dd></div><div><dt>Stay</dt><dd>{value.arrival} → {value.departure}</dd></div><div><dt>Room type</dt><dd>{value.roomType||'Not returned'}</dd></div><div><dt>Status</dt><dd>{value.status||'Not returned'}</dd></div><div><dt>Recorded total</dt><dd>{value.total||'Not returned — check in PMS'}</dd></div></dl>;}
|