// Hotel Walks — Send a walk request.
// Multi-step flow:
// 1. Guest + nights → 2. Routing strategy + partners → 3. Review + send
// 4. Live broadcast view with timer and (faked) partner responses.
(function() {
const { useState, useEffect, useMemo } = React;
const { Caps, Btn, Field, Input, Stepper, Pill, Toggle, Hr, money } = window.UI;
const { HOTELS, HOTEL_BY_ID, PARTNERS, ME } = window.HW_DATA;
const STEPS = ["Guest", "Routing", "Review", "Broadcast"];
function Stepper7({ step }) {
return (
{STEPS.map((s, i) => {
const done = i < step;
const active = i === step;
return (
{String(i+1).padStart(2,"0")}
{s}
{done &&
✓ }
);
})}
);
}
function PageShell({ step, title, sub, children, footer }) {
return (
New walk request
{title}
{sub &&
{sub}
}
{children}
{footer && (
{footer}
)}
);
}
/* ────────────────── STEP 1 ────────────────── */
function StepGuest({ form, setForm, onNext }) {
return (
← Cancel
Routing →
>
}>
setForm({...form, arrival: e.target.value})} type="date" />
Reason
{["Oversold","Mechanical","VIP bump","Guest request"].map(r => (
setForm({...form, reason: r})} style={{
background: form.reason === r ? "var(--ink)" : "transparent",
color: form.reason === r ? "var(--bone)" : "var(--ink)",
border: "1px solid var(--ink)",
padding: "9px 16px", fontSize: 12,
fontFamily: "'JetBrains Mono', monospace", letterSpacing: "0.10em",
textTransform: "uppercase", cursor: "pointer",
}}>{r}
))}
setForm({...form, note: e.target.value})} placeholder="Late arrival, ETA 10pm" />
);
}
/* ────────────────── STEP 2 ────────────────── */
function StepRouting({ form, setForm, onNext, onBack }) {
const selectedPartners = form.partners;
const togglePartner = (id) => {
const cur = new Set(selectedPartners);
cur.has(id) ? cur.delete(id) : cur.add(id);
setForm({...form, partners: Array.from(cur)});
};
return (
← Back
Review →
>
}>
{/* Controls */}
setForm({...form, targetRate: e.target.value.replace(/[^0-9]/g,"")})} placeholder="425" />
{[["preference","By preference"], ["rate","By best rate"]].map(([v, l]) => (
setForm({...form, strategy: v})} style={{
flex: 1,
background: form.strategy === v ? "var(--ink)" : "transparent",
color: form.strategy === v ? "var(--bone)" : "var(--ink)",
border: "1px solid var(--ink)",
padding: "10px 12px", fontSize: 11,
fontFamily: "'JetBrains Mono', monospace", letterSpacing: "0.10em", textTransform: "uppercase",
cursor: "pointer",
}}>{l}
))}
setForm({...form, timer: v})} min={0} max={180} step={5} prefix="" width="100%" />
Urgent mode
Bypass timers. Broadcast to all partners at once.
setForm({...form, urgent: v})} />
{/* Partners list */}
Partners · {selectedPartners.length} selected
setForm({...form, partners: PARTNERS.map(p => p.id)})} style={{ background: "none", border: "none", padding: 0 }}>
Select all
{PARTNERS.map(p => {
const hotel = HOTEL_BY_ID[p.id];
const checked = selectedPartners.includes(p.id);
return (
togglePartner(p.id)} style={{
width: "100%", textAlign: "left",
background: "transparent", border: "none",
borderBottom: "1px solid var(--rule)",
padding: "20px 0", cursor: "pointer",
display: "grid",
gridTemplateColumns: "30px 60px 1fr 130px 130px 90px",
gap: 16, alignItems: "center",
}} className="partner-row">
{checked ? "✓" : ""}
#{p.pref}
{hotel.name}
{hotel.area}
Last rate
{money(p.lastRate)}
Accepts
{Math.round(p.acceptance*100)}% · ~{p.avgMin}m
{checked ? "Selected" : ""}
);
})}
);
}
/* ────────────────── STEP 3 ────────────────── */
function StepReview({ form, onBack, onSend }) {
const lines = [
["Guest", form.guest || "—"],
["Confirmation", form.confirmation || "—"],
["Party · nights", `${form.party} · ${form.nights}`],
["Arrival", form.arrival || "Today"],
["Reason", form.reason],
["Max days covered", `${form.maxDays} of ${form.nights}`],
["Target rate", form.targetRate ? money(form.targetRate) : "Open"],
["Strategy", form.strategy === "preference" ? "By partner preference" : "By best rate"],
["Escalation", form.urgent ? "URGENT — broadcast simultaneously" : `${form.timer} min per partner`],
["Partners", form.partners.length + " selected"],
];
return (
← Back
Send walk request
>
}>
Walk envelope
{lines.map(([k, v], i) => (
))}
Partners · in order
{form.partners.map((id, i) => {
const h = HOTEL_BY_ID[id];
return (
{String(i+1).padStart(2,"0")}
{h.name}
{form.urgent ? "Now" : `+${i * form.timer}m`}
);
})}
Estimated cost · to {ME.property.name}
$4.00 + 10%
per walked guest
);
}
/* ────────────────── STEP 4 — Live broadcast ────────────────── */
function StepBroadcast({ form, onDone }) {
// Simulate partner responses over time.
const [responses, setResponses] = useState(
form.partners.map(id => ({ id, state: "pending" }))
);
const [elapsed, setElapsed] = useState(0);
const acceptedIdx = responses.findIndex(r => r.state === "accepted");
useEffect(() => {
const tick = setInterval(() => setElapsed(e => e + 1), 1000);
return () => clearInterval(tick);
}, []);
useEffect(() => {
// Fake response timeline based on partner stats
const timers = form.partners.map((id, i) => {
const p = PARTNERS.find(pp => pp.id === id);
const accepts = Math.random() < (p.acceptance);
const wait = (form.urgent ? 0 : i * 1.2) + (p.avgMin * 0.4); // seconds
return setTimeout(() => {
setResponses(prev => prev.map(r => r.id === id ? {...r, state: accepts ? (prev.some(x => x.state === "accepted") ? "passed" : "accepted") : "passed"} : r));
}, wait * 1000);
});
return () => timers.forEach(clearTimeout);
}, []);
const accepted = responses.find(r => r.state === "accepted");
const acceptedHotel = accepted ? HOTEL_BY_ID[accepted.id] : null;
return (
← Back to today
Generate walk letter →
>
) : (
<>
{ if(confirm("Cancel this walk?")) onDone(); }}>Cancel broadcast
Elapsed · {elapsed}s
>
)
}>
Partner responses
{responses.map((r, i) => {
const h = HOTEL_BY_ID[r.id];
const isAccepted = r.state === "accepted";
const isPassed = r.state === "passed";
const isPending = r.state === "pending";
return (
{String(i+1).padStart(2,"0")}
{isPending &&
Waiting… }
{isAccepted &&
Accepted }
{isPassed &&
Passed }
{isAccepted ? money(form.targetRate || h.rate) : isPending ? "—" : "—"}
);
})}
{accepted ? (
Accepted
{acceptedHotel.name}
{acceptedHotel.area}
= 3 ? "1" : "1"} dark />
) : (
Live status
Waiting for the first accept.
{form.urgent
? "All partners notified simultaneously. The first to accept wins the rooms."
: `If the top partner doesn't respond in ${form.timer} minutes, the next one is offered.`}
r.state==="pending").length)} />
r.state==="passed").length)} />
)}
);
}
function Stat({ label, value }) {
return (
);
}
function Row({ k, v, dark, mono }) {
return (
);
}
/* ────────────────── Root ────────────────── */
function SendWalk() {
const [step, setStep] = useState(0);
const [form, setForm] = useState({
guest: "Maya Klein",
confirmation: "WY-882103",
party: 2,
nights: 1,
maxDays: 1,
arrival: "2026-05-21",
reason: "Oversold",
note: "Late arrival, ETA 10pm",
targetRate: "425",
strategy: "preference",
timer: 30,
urgent: false,
partners: ["1HBB","HOXBK","PNHST","WMBG"],
});
if (step === 0) return setStep(1)} />;
if (step === 1) return setStep(2)} onBack={() => setStep(0)} />;
if (step === 2) return setStep(1)} onSend={() => setStep(3)} />;
if (step === 3) return window.HWgo("/app")} />;
return null;
}
const responsiveCSS = `
@media (max-width: 880px) {
.form-cols, .route-controls, .review-cols { grid-template-columns: 1fr !important; gap: 32px !important; }
.partner-row { grid-template-columns: 24px 30px 1fr !important; gap: 12px !important; }
.partner-row > *:nth-child(n+4) { display: none; }
}
@media (max-width: 720px) {
.page-title { font-size: 36px !important; }
.send-footer { padding: 14px 18px !important; }
.send-footer > * { flex: 1; }
.send-footer button, .send-footer a > * { width: 100%; }
}
`;
function WrappedSendWalk() {
return (
<>
>
);
}
window.Screens = window.Screens || {};
window.Screens.SendWalk = WrappedSendWalk;
})();