feat: expand price automation contract

This commit is contained in:
TopherMayor
2026-04-30 11:53:18 -07:00
parent 4ad51ed2c6
commit a3b8e9a4b0
5 changed files with 316 additions and 58 deletions

View File

@@ -329,6 +329,7 @@
--green: #34d399;
--red: #f87171;
--hotel: #3b82f6;
--flight: #38bdf8;
--golf: #22c55e;
--nightlife: #a855f7;
--excursion: #06b6d4;
@@ -835,6 +836,7 @@
width: 0%;
}
.vote-bar-fill.hotel { background: var(--hotel); }
.vote-bar-fill.flight { background: var(--flight); }
.vote-bar-fill.golf { background: var(--golf); }
.vote-bar-fill.nightlife { background: var(--nightlife); }
.vote-bar-fill.excursion { background: var(--excursion); }
@@ -1430,6 +1432,7 @@
<div class="btn-row">
<select id="addCategory">
<option value="hotel">🏨 Hotel</option>
<option value="flight">✈️ Flight</option>
<option value="golf">⛳ Golf</option>
<option value="nightlife">🎧 Nightlife</option>
<option value="excursion">🚤 Excursion</option>
@@ -1677,6 +1680,26 @@
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
}
function formatBookingType(bookingType, priceBasis) {
const typeLabels = {
package: 'Package',
standalone: 'Standalone',
calculated: 'Calculated',
};
const basisLabels = {
perTraveler: 'per traveler',
perNight: 'per night',
perPerson: 'per person',
perGroup: 'per group',
totalPackage: 'total package',
perRound: 'per round',
perTable: 'per table',
};
const typeLabel = typeLabels[bookingType] || '';
const basisLabel = basisLabels[priceBasis] || priceBasis || '';
return [typeLabel, basisLabel].filter(Boolean).join(' · ');
}
function normalizeSourceKey(value) {
return String(value || '')
.trim()
@@ -1699,6 +1722,8 @@
sourceKey: defaultKey,
sourceLabel: defaultLabel,
sourceUrl: opt.automationInsights?.sourceUrl || null,
bookingType: opt.automationInsights?.bookingType || null,
priceBasis: opt.automationInsights?.priceBasis || null,
pointCount: Array.isArray(opt.priceHistory) ? opt.priceHistory.length : 0,
latestCheckedAt: opt.latestPricePoint?.checkedAt || null,
latestPrice: opt.latestPricePoint?.price ?? null,
@@ -1838,6 +1863,7 @@
const source = sources[0] || null;
const sourceLabel = source?.sourceLabel || opt.automationInsights?.source || 'Unknown source';
const meta = source ? [
formatBookingType(source.bookingType, source.priceBasis),
source.latestDisplayPrice || (typeof source.latestPrice === 'number' ? formatCurrency(source.latestPrice, source.currency || 'USD') : ''),
source.pointCount ? `${source.pointCount} point${source.pointCount === 1 ? '' : 's'}` : '',
].filter(Boolean).join(' · ') : '';
@@ -1852,6 +1878,7 @@
${sources.map(source => {
const labelParts = [
source.sourceLabel || 'Unknown source',
formatBookingType(source.bookingType, source.priceBasis),
source.latestDisplayPrice || (typeof source.latestPrice === 'number' ? formatCurrency(source.latestPrice, source.currency || 'USD') : ''),
source.pointCount ? `${source.pointCount} pt${source.pointCount === 1 ? '' : 's'}` : '',
].filter(Boolean);
@@ -1870,6 +1897,8 @@
const insights = selectedPoint ? {
source: selectedMeta?.sourceLabel || selectedPoint.source || 'Automation feed',
sourceUrl: selectedMeta?.sourceUrl || selectedPoint.sourceUrl || null,
bookingType: selectedMeta?.bookingType || selectedPoint.bookingType || null,
priceBasis: selectedMeta?.priceBasis || selectedPoint.priceBasis || null,
availability: selectedPoint.availability || null,
decisionNote: selectedPoint.decisionNote || null,
displayPrice: selectedPoint.displayPrice || null,
@@ -1878,6 +1907,10 @@
const currentPrice = typeof selectedPoint?.price === 'number' ? formatCurrency(selectedPoint.price, insights.currency || 'USD') : '';
const priceLabel = selectedPoint?.displayPrice || insights.displayPrice || currentPrice || 'Not yet tracked';
const sourceLabel = selectedMeta?.sourceLabel || insights.source || 'Automation feed';
const bookingLabel = formatBookingType(
selectedMeta?.bookingType || insights.bookingType,
selectedMeta?.priceBasis || insights.priceBasis,
);
const statusLabel = selectedPoint?.availability || selectedPoint?.decisionNote || insights.availability || insights.decisionNote || 'Matched from live search';
const overviewItems = normalizeTextList(opt.details);
const autoHighlights = normalizeTextList(selectedPoint?.highlights || opt.automationInsights?.highlights);
@@ -1904,6 +1937,10 @@
${renderSourceSelect(opt)}
${availableSources.length > 1 && sourceMetaLine ? `<div class="option-source-sub">${escapeHtml(sourceLabel)}${sourceMetaLine ? ` · ${escapeHtml(sourceMetaLine)}` : ''}</div>` : ''}
</div>
<div class="option-fact">
<span class="option-fact-label">Booking type</span>
<div class="option-fact-value">${escapeHtml(bookingLabel || 'Not classified')}</div>
</div>
<div class="option-fact">
<span class="option-fact-label">Status</span>
<div class="option-fact-value">${escapeHtml(statusLabel)}</div>
@@ -2232,15 +2269,17 @@
const winner = rank === 1 ? 'winner' : '';
const barColor = cat.id === 'hotel'
? 'var(--hotel)'
: cat.id === 'golf'
? 'var(--golf)'
: cat.id === 'nightlife'
? 'var(--nightlife)'
: cat.id === 'excursion'
? 'var(--excursion)'
: cat.id === 'budget'
? 'var(--budget)'
: 'var(--itinerary)';
: cat.id === 'flight'
? 'var(--flight)'
: cat.id === 'golf'
? 'var(--golf)'
: cat.id === 'nightlife'
? 'var(--nightlife)'
: cat.id === 'excursion'
? 'var(--excursion)'
: cat.id === 'budget'
? 'var(--budget)'
: 'var(--itinerary)';
return `
<div class="results-row">
<div class="results-rank ${medalClass}">${medal}</div>