// Hotel Walks — Invoice dashboard.
// Listing (sending) vs Accepts (receiving) rows. No-show toggle ONLY appears
// on receiving lines. Period switcher, status filter, totals.
(function() {
const { useState, useMemo } = React;
const { Caps, Btn, Pill, Toggle, Hr, money } = window.UI;
const { INVOICE_ROWS, INVOICE_PERIOD, HOTEL_BY_ID, ME } = window.HW_DATA;
function Invoices() {
const [rows, setRows] = useState(INVOICE_ROWS);
const [filter, setFilter] = useState("all"); // all | listing | accepts | open
const filtered = useMemo(() => {
if (filter === "all") return rows;
if (filter === "listing") return rows.filter(r => r.side === "Listing");
if (filter === "accepts") return rows.filter(r => r.side === "Accepts");
if (filter === "open") return rows.filter(r => r.status === "open");
return rows;
}, [rows, filter]);
const totals = useMemo(() => {
let listed = 0, accepted = 0, fees = 0, commission = 0;
rows.forEach(r => {
const due = r.fee + (r.noShow ? 0 : r.commission); // no-show waives commission, not the $4 fee
if (r.side === "Listing") listed += due;
else accepted += due;
fees += r.fee;
if (!r.noShow) commission += r.commission;
});
return { listed, accepted, fees, commission, gross: listed + accepted };
}, [rows]);
const toggleNoShow = (id) => {
setRows(prev => prev.map(r => r.id === id ? { ...r, noShow: !r.noShow } : r));
};
return (
Invoices · period {INVOICE_PERIOD.id}
{INVOICE_PERIOD.label}
$4 + 10% per side. No-shows can only be marked on the receiving line — sending hotels cannot dispute attendance.
Export CSV
Settle period
{/* Stat strip — content inset to page padding so cells align with the head */}
{/* Filters */}
{[
["all", "All"],
["listing", "Listing (sent)"],
["accepts", "Accepts (received)"],
["open", "Open only"],
].map(([k, l]) => (
))}
{filtered.length} rows
{/* Table */}
{["Date","Invoice","Side","Guest","Partner","Rate","Fee","Commission","Status / No-show"].map((h, i) => (
{h}
))}
{filtered.map(r => {
const partner = HOTEL_BY_ID[r.to || r.from];
const isAccepts = r.side === "Accepts";
const commission = r.noShow ? 0 : r.commission;
return (
{r.date}
{r.id.replace("INV-","")}
{partner.name}
{isAccepts ? "from" : "to"} · {partner.area}
{money(r.rate)}
{money(r.fee)}
{money(commission)}
{r.status === "paid"
?
Paid
:
Open}
{isAccepts && (
toggleNoShow(r.id)}
label={{r.noShow ? "No-show" : "Mark no-show"}}
/>
)}
);
})}
{/* Footer totals row */}
Period gross
{money(totals.fees)}
{money(totals.commission)}
{money(totals.gross)}
);
}
function StatCell({ label, value, positive, idx = 0, count = 1 }) {
const isFirst = idx === 0;
const isLast = idx === count - 1;
return (
);
}
const responsiveCSS = `
@media (max-width: 1100px) {
.invoice-stats { grid-template-columns: 1fr 1fr !important; }
.invoice-stats > div:nth-child(2) { border-right: none !important; }
.invoice-stats > div:nth-child(n+3) { border-top: 1px solid var(--rule); }
}
@media (max-width: 900px) {
.inv-grid { grid-template-columns: 1fr !important; gap: 8px !important; padding: 18px 0 !important; }
}
@media (max-width: 720px) {
.inv-head { grid-template-columns: 1fr !important; gap: 16px !important; }
.inv-head .page-title { font-size: 36px !important; }
.inv-head-ctas { width: 100%; }
.inv-head-ctas > * { flex: 1; }
.invoice-stats { padding: 0 20px !important; grid-template-columns: 1fr 1fr !important; }
.invoice-stats > div { padding: 18px 0 !important; border-right: none !important; }
.invoice-stats > div:nth-child(odd) { padding-right: 12px !important; border-right: 1px solid var(--rule) !important; }
.invoice-stats > div:nth-child(even) { padding-left: 12px !important; }
.invoice-stats > div .serif-i { font-size: 24px !important; }
.inv-filters { padding: 16px 20px !important; }
}
`;
function Wrapped() {
return (<>>);
}
window.Screens = window.Screens || {};
window.Screens.Invoices = Wrapped;
})();