Improve sold-out and past-day cell display in rate matrix

- Sold out + last price: show SOLD label above strikethrough price; remove price-index badge (rate no longer bookable, delta misleading)
- Sold out, no history: unchanged — shows 'Sold'
- Past days: price-index badge now grey/faded (#94a3b8 on #e2e8f0) instead of coloured — historical context only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-09 23:56:40 +00:00
parent d97bfdab90
commit e472ba65c4

View file

@ -1678,16 +1678,28 @@ const RateMatrixTab: React.FC = () => {
? formatCurrency(rate.last_available_rate) ? formatCurrency(rate.last_available_rate)
: null : null
// For sold-out / past: show last known available rate with strikethrough // Rate text + strikethrough logic:
// Sold out + last price → "SOLD" label above + strikethrough price (no badge)
// Sold out, no history → "Sold" text only
// Past + last price → strikethrough price (grey/faded badge)
// Past, no history → —
// Available → live rate + coloured badge
let rateText = '' let rateText = ''
let strikethrough = false let strikethrough = false
let showSoldLabel = false
if (!rate) { if (!rate) {
rateText = '' rateText = ''
} else if (isSoldOrPast && lastAvailRate) { } else if (isSoldOut && lastAvailRate) {
rateText = lastAvailRate + staleMark rateText = lastAvailRate + staleMark
strikethrough = true strikethrough = true
} else if (isSoldOrPast) { showSoldLabel = true
rateText = isSoldOut ? 'Sold' : '—' } else if (isSoldOut) {
rateText = 'Sold'
} else if (isPast && lastAvailRate) {
rateText = lastAvailRate + staleMark
strikethrough = true
} else if (isPast) {
rateText = '—'
} else if (isAvailable && rate.rate_gross) { } else if (isAvailable && rate.rate_gross) {
rateText = formatCurrency(rate.rate_gross) + staleMark rateText = formatCurrency(rate.rate_gross) + staleMark
} else { } else {
@ -1699,7 +1711,7 @@ const RateMatrixTab: React.FC = () => {
rate.breakfast_included ? 'Breakfast incl.' : null, rate.breakfast_included ? 'Breakfast incl.' : null,
rate.free_cancellation ? 'Free cancel' : null, rate.free_cancellation ? 'Free cancel' : null,
rate.rooms_left ? `${rate.rooms_left} left` : null, rate.rooms_left ? `${rate.rooms_left} left` : null,
lastAvailRate && isSoldOrPast ? `Last available: ${lastAvailRate}` : null, lastAvailRate && (isSoldOut || isPast) ? `Last available: ${lastAvailRate}` : null,
rate.scraped_at ? `Scraped: ${new Date(rate.scraped_at).toLocaleString('en-GB', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' })}` : null, rate.scraped_at ? `Scraped: ${new Date(rate.scraped_at).toLocaleString('en-GB', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' })}` : null,
isStale ? 'Not updated in latest scrape' : null, isStale ? 'Not updated in latest scrape' : null,
].filter(Boolean).join(' | ') : '' ].filter(Boolean).join(' | ') : ''
@ -1724,11 +1736,22 @@ const RateMatrixTab: React.FC = () => {
} }
} }
// Price index badge for competitor rows (use last_available_rate for sold/past) // Price index badge:
const rateForIndex = isSoldOrPast ? rate?.last_available_rate : rate?.rate_gross // Sold out → no badge (price no longer bookable, delta misleading)
// Past → grey/faded badge (historical context only)
// Future available → coloured badge
const rateForIndex = isPast ? rate?.last_available_rate : (!isSoldOut ? rate?.rate_gross : null)
let priceIndexBadge: React.ReactNode = null let priceIndexBadge: React.ReactNode = null
if (hotel.tier === 'competitor' && rateForIndex && ownRateByDate[d]) { if (hotel.tier === 'competitor' && rateForIndex && ownRateByDate[d]) {
const delta = Math.round((rateForIndex / ownRateByDate[d]! - 1) * 100) const delta = Math.round((rateForIndex / ownRateByDate[d]! - 1) * 100)
if (isPast) {
priceIndexBadge = (
<span style={{ fontSize: 9, fontWeight: 600, color: '#94a3b8', background: '#e2e8f0',
borderRadius: 4, padding: '0 3px', lineHeight: '14px' }}>
{delta > 0 ? '+' : ''}{delta}%
</span>
)
} else {
const bg = delta > 5 ? '#dcfce7' : delta < -15 ? '#fee2e2' : delta < -5 ? '#fef3c7' : '#f1f5f9' const bg = delta > 5 ? '#dcfce7' : delta < -15 ? '#fee2e2' : delta < -5 ? '#fef3c7' : '#f1f5f9'
const fg = delta > 5 ? '#16a34a' : delta < -15 ? '#dc2626' : delta < -5 ? '#d97706' : '#64748b' const fg = delta > 5 ? '#16a34a' : delta < -15 ? '#dc2626' : delta < -5 ? '#d97706' : '#64748b'
priceIndexBadge = ( priceIndexBadge = (
@ -1738,6 +1761,7 @@ const RateMatrixTab: React.FC = () => {
</span> </span>
) )
} }
}
const showEye = !isPast && !!bookingUrl && !!rate const showEye = !isPast && !!bookingUrl && !!rate
const showHistory = !!rate const showHistory = !!rate
@ -1762,6 +1786,9 @@ const RateMatrixTab: React.FC = () => {
onMouseLeave={onCellLeave} onMouseLeave={onCellLeave}
> >
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}>
{showSoldLabel && (
<span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.05em', color: '#d97706' }}>SOLD</span>
)}
<span style={{ textDecoration: strikethrough ? 'line-through' : 'none' }}> <span style={{ textDecoration: strikethrough ? 'line-through' : 'none' }}>
{rateText} {rateText}
</span> </span>
@ -1849,7 +1876,7 @@ const RateMatrixTab: React.FC = () => {
<div style={styles.legend}> <div style={styles.legend}>
<span style={styles.legendTitle}>Legend:</span> <span style={styles.legendTitle}>Legend:</span>
<span style={mergeStyles(styles.legendItem, styles.matrixCellAvailable)}>Available</span> <span style={mergeStyles(styles.legendItem, styles.matrixCellAvailable)}>Available</span>
<span style={mergeStyles(styles.legendItem, styles.matrixCellSoldOut)}>Sold Out (strikethrough = last rate)</span> <span style={mergeStyles(styles.legendItem, styles.matrixCellSoldOut)}>Sold Out (SOLD + strikethrough last rate if known)</span>
<span style={mergeStyles(styles.legendItem, styles.matrixCellNoRate)}>No Rate</span> <span style={mergeStyles(styles.legendItem, styles.matrixCellNoRate)}>No Rate</span>
<span style={mergeStyles(styles.legendItem, styles.matrixCellPast)}>Past</span> <span style={mergeStyles(styles.legendItem, styles.matrixCellPast)}>Past</span>
<span style={mergeStyles(styles.legendItem, styles.matrixCellEmpty)}>No Data</span> <span style={mergeStyles(styles.legendItem, styles.matrixCellEmpty)}>No Data</span>