489 lines
20 KiB
TypeScript
489 lines
20 KiB
TypeScript
/**
|
|
* <folk-vnb-view> — Main rVnb module view.
|
|
*
|
|
* Layout: Search/filter bar, vehicle grid with map toggle (Leaflet),
|
|
* owner dashboard (my vehicles + incoming requests), rental request sidebar.
|
|
*/
|
|
|
|
import './folk-vehicle-card';
|
|
import './folk-rental-request';
|
|
|
|
// ── Leaflet CDN Loader ──
|
|
|
|
let _leafletReady = false;
|
|
let _leafletPromise: Promise<void> | null = null;
|
|
|
|
function ensureLeaflet(): Promise<void> {
|
|
if (_leafletReady && typeof (window as any).L !== 'undefined') return Promise.resolve();
|
|
if (_leafletPromise) return _leafletPromise;
|
|
_leafletPromise = new Promise<void>((resolve, reject) => {
|
|
const s = document.createElement('script');
|
|
s.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
|
|
s.onload = () => { _leafletReady = true; resolve(); };
|
|
s.onerror = () => reject(new Error('Leaflet load failed'));
|
|
document.head.appendChild(s);
|
|
});
|
|
return _leafletPromise;
|
|
}
|
|
|
|
// ── Economy badges ──
|
|
|
|
const ECONOMY_BADGE: Record<string, { icon: string; label: string; cls: string }> = {
|
|
gift: { icon: '\u{1F49A}', label: 'Gift', cls: 'vnb-badge--gift' },
|
|
exchange: { icon: '\u{1F91D}', label: 'Exchange', cls: 'vnb-badge--exchange' },
|
|
sliding_scale: { icon: '\u{2696}', label: 'Sliding Scale', cls: 'vnb-badge--sliding_scale' },
|
|
suggested: { icon: '\u{1F4AD}', label: 'Suggested', cls: 'vnb-badge--suggested' },
|
|
fixed: { icon: '\u{1F3F7}', label: 'Fixed', cls: 'vnb-badge--fixed' },
|
|
};
|
|
|
|
const TYPE_ICONS: Record<string, string> = {
|
|
motorhome: '\u{1F690}', camper_van: '\u{1F68C}', travel_trailer: '\u{1F3D5}',
|
|
truck_camper: '\u{1F6FB}', skoolie: '\u{1F68E}', other: '\u{1F3E0}',
|
|
};
|
|
|
|
const MILEAGE_BADGE: Record<string, { icon: string; label: string }> = {
|
|
unlimited: { icon: '\u{267E}', label: 'Unlimited' },
|
|
per_mile: { icon: '\u{1F4CF}', label: 'Per Mile' },
|
|
included_miles: { icon: '\u{2705}', label: 'Included Miles' },
|
|
};
|
|
|
|
class FolkVnbView extends HTMLElement {
|
|
static observedAttributes = ['space'];
|
|
|
|
#space = 'demo';
|
|
#vehicles: any[] = [];
|
|
#rentals: any[] = [];
|
|
#stats: any = null;
|
|
#selectedRental: any = null;
|
|
#view: 'grid' | 'map' = 'grid';
|
|
#search = '';
|
|
#typeFilter = '';
|
|
#economyFilter = '';
|
|
#map: any = null;
|
|
#mapContainer: HTMLElement | null = null;
|
|
|
|
connectedCallback() {
|
|
this.#space = this.getAttribute('space') || 'demo';
|
|
this.#render();
|
|
this.#loadData();
|
|
}
|
|
|
|
attributeChangedCallback(name: string, _old: string, val: string) {
|
|
if (name === 'space') {
|
|
this.#space = val || 'demo';
|
|
this.#loadData();
|
|
}
|
|
}
|
|
|
|
async #loadData() {
|
|
try {
|
|
const [vehiclesRes, rentalsRes, statsRes] = await Promise.all([
|
|
fetch(`/${this.#space}/rvnb/api/vehicles`),
|
|
fetch(`/${this.#space}/rvnb/api/rentals`),
|
|
fetch(`/${this.#space}/rvnb/api/stats`),
|
|
]);
|
|
|
|
if (vehiclesRes.ok) {
|
|
const data = await vehiclesRes.json();
|
|
this.#vehicles = data.results || [];
|
|
}
|
|
if (rentalsRes.ok) {
|
|
const data = await rentalsRes.json();
|
|
this.#rentals = data.results || [];
|
|
}
|
|
if (statsRes.ok) {
|
|
this.#stats = await statsRes.json();
|
|
}
|
|
|
|
this.#renderContent();
|
|
} catch (err) {
|
|
console.warn('[rVnb] Failed to load data:', err);
|
|
}
|
|
}
|
|
|
|
get #filteredVehicles() {
|
|
let list = this.#vehicles;
|
|
|
|
if (this.#search) {
|
|
const term = this.#search.toLowerCase();
|
|
list = list.filter((v: any) =>
|
|
v.title.toLowerCase().includes(term) ||
|
|
v.description?.toLowerCase().includes(term) ||
|
|
v.pickup_location_name?.toLowerCase().includes(term) ||
|
|
v.owner_name?.toLowerCase().includes(term) ||
|
|
v.make?.toLowerCase().includes(term) ||
|
|
v.model?.toLowerCase().includes(term)
|
|
);
|
|
}
|
|
if (this.#typeFilter) list = list.filter((v: any) => v.type === this.#typeFilter);
|
|
if (this.#economyFilter) list = list.filter((v: any) => v.economy === this.#economyFilter);
|
|
|
|
return list;
|
|
}
|
|
|
|
#render() {
|
|
this.innerHTML = `
|
|
<div class="vnb-view">
|
|
<div class="vnb-view__header" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
|
|
<h2 style="margin:0;font-size:1.25rem;color:var(--rs-text,#e2e8f0);display:flex;align-items:center;gap:0.5rem">
|
|
\u{1F690} Community RV Sharing
|
|
${this.#stats ? `<span style="font-size:0.75rem;color:var(--rs-text-muted,#94a3b8);font-weight:400">${this.#stats.active_vehicles || 0} vehicles</span>` : ''}
|
|
</h2>
|
|
<div style="display:flex;gap:0.5rem">
|
|
<button class="vnb-view__toggle" data-view="grid" style="padding:0.4rem 0.75rem;border-radius:0.375rem;border:1px solid var(--rs-border,#334155);background:${this.#view === 'grid' ? 'rgba(16,185,129,0.15)' : 'transparent'};color:var(--rs-text,#e2e8f0);cursor:pointer;font-size:0.82rem">
|
|
\u{25A6} Grid
|
|
</button>
|
|
<button class="vnb-view__toggle" data-view="map" style="padding:0.4rem 0.75rem;border-radius:0.375rem;border:1px solid var(--rs-border,#334155);background:${this.#view === 'map' ? 'rgba(16,185,129,0.15)' : 'transparent'};color:var(--rs-text,#e2e8f0);cursor:pointer;font-size:0.82rem">
|
|
\u{1F5FA} Map
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="vnb-search">
|
|
<input class="vnb-search__input" type="text" placeholder="Search vehicles..." value="${this.#esc(this.#search)}">
|
|
<select class="vnb-search__select" id="type-filter">
|
|
<option value="">All Types</option>
|
|
<option value="motorhome" ${this.#typeFilter === 'motorhome' ? 'selected' : ''}>Motorhome</option>
|
|
<option value="camper_van" ${this.#typeFilter === 'camper_van' ? 'selected' : ''}>Camper Van</option>
|
|
<option value="travel_trailer" ${this.#typeFilter === 'travel_trailer' ? 'selected' : ''}>Travel Trailer</option>
|
|
<option value="truck_camper" ${this.#typeFilter === 'truck_camper' ? 'selected' : ''}>Truck Camper</option>
|
|
<option value="skoolie" ${this.#typeFilter === 'skoolie' ? 'selected' : ''}>Skoolie</option>
|
|
<option value="other" ${this.#typeFilter === 'other' ? 'selected' : ''}>Other</option>
|
|
</select>
|
|
<select class="vnb-search__select" id="economy-filter">
|
|
<option value="">All Economies</option>
|
|
<option value="gift" ${this.#economyFilter === 'gift' ? 'selected' : ''}>\u{1F49A} Gift</option>
|
|
<option value="exchange" ${this.#economyFilter === 'exchange' ? 'selected' : ''}>\u{1F91D} Exchange</option>
|
|
<option value="sliding_scale" ${this.#economyFilter === 'sliding_scale' ? 'selected' : ''}>\u{2696} Sliding Scale</option>
|
|
<option value="suggested" ${this.#economyFilter === 'suggested' ? 'selected' : ''}>\u{1F4AD} Suggested</option>
|
|
<option value="fixed" ${this.#economyFilter === 'fixed' ? 'selected' : ''}>\u{1F3F7} Fixed</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="vnb-view__content" id="vnb-content"></div>
|
|
|
|
<div class="vnb-view__sidebar" id="vnb-sidebar" style="display:none;position:fixed;top:0;right:0;width:420px;height:100vh;background:var(--rs-bg,#0f172a);border-left:1px solid var(--rs-border,#334155);z-index:100;overflow-y:auto;padding:1rem">
|
|
<button id="close-sidebar" style="position:absolute;top:0.75rem;right:0.75rem;background:none;border:none;color:var(--rs-text-muted,#94a3b8);font-size:1.25rem;cursor:pointer">\u{2715}</button>
|
|
<div id="sidebar-content"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
this.#wireEvents();
|
|
}
|
|
|
|
#wireEvents() {
|
|
// Search input
|
|
const searchInput = this.querySelector('.vnb-search__input') as HTMLInputElement;
|
|
searchInput?.addEventListener('input', (e) => {
|
|
this.#search = (e.target as HTMLInputElement).value;
|
|
this.#renderContent();
|
|
});
|
|
|
|
// Type filter
|
|
this.querySelector('#type-filter')?.addEventListener('change', (e) => {
|
|
this.#typeFilter = (e.target as HTMLSelectElement).value;
|
|
this.#renderContent();
|
|
});
|
|
|
|
// Economy filter
|
|
this.querySelector('#economy-filter')?.addEventListener('change', (e) => {
|
|
this.#economyFilter = (e.target as HTMLSelectElement).value;
|
|
this.#renderContent();
|
|
});
|
|
|
|
// View toggles
|
|
for (const btn of this.querySelectorAll('.vnb-view__toggle')) {
|
|
btn.addEventListener('click', () => {
|
|
this.#view = (btn as HTMLElement).dataset.view as 'grid' | 'map';
|
|
this.#render();
|
|
this.#renderContent();
|
|
});
|
|
}
|
|
|
|
// Close sidebar
|
|
this.querySelector('#close-sidebar')?.addEventListener('click', () => {
|
|
this.#closeSidebar();
|
|
});
|
|
|
|
// Rental action events (bubbled from folk-rental-request)
|
|
this.addEventListener('rental-action', ((e: CustomEvent) => {
|
|
this.#handleRentalAction(e.detail.rentalId, e.detail.action);
|
|
}) as EventListener);
|
|
|
|
this.addEventListener('rental-message', ((e: CustomEvent) => {
|
|
this.#handleRentalMessage(e.detail.rentalId, e.detail.body);
|
|
}) as EventListener);
|
|
}
|
|
|
|
#renderContent() {
|
|
const container = this.querySelector('#vnb-content');
|
|
if (!container) return;
|
|
|
|
const vehicles = this.#filteredVehicles;
|
|
|
|
if (this.#view === 'map') {
|
|
container.innerHTML = `<div id="vnb-map" style="width:100%;height:500px;border-radius:0.75rem;border:1px solid var(--rs-border,#334155)"></div>`;
|
|
this.#initMap(vehicles);
|
|
return;
|
|
}
|
|
|
|
// Grid view
|
|
if (vehicles.length === 0) {
|
|
container.innerHTML = `
|
|
<div style="text-align:center;padding:3rem;color:var(--rs-text-muted,#94a3b8)">
|
|
<div style="font-size:2.5rem;margin-bottom:1rem">\u{1F690}</div>
|
|
<p>No vehicles found. Try adjusting your search or filters.</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `<div class="vnb-grid">${vehicles.map((v: any) => this.#renderVehicleCard(v)).join('')}</div>`;
|
|
|
|
// Wire card clicks
|
|
for (const card of container.querySelectorAll('.vnb-card')) {
|
|
card.addEventListener('click', () => {
|
|
const vehicleId = (card as HTMLElement).dataset.vehicleId;
|
|
if (vehicleId) this.#showVehicleRentals(vehicleId);
|
|
});
|
|
}
|
|
}
|
|
|
|
#renderVehicleCard(v: any): string {
|
|
const eco = ECONOMY_BADGE[v.economy] || ECONOMY_BADGE.suggested;
|
|
const typeIcon = TYPE_ICONS[v.type] || '\u{1F690}';
|
|
const typeLabel = (v.type || 'camper_van').replace(/_/g, ' ');
|
|
const mileage = MILEAGE_BADGE[v.mileage_policy] || MILEAGE_BADGE.unlimited;
|
|
const specsLine = [
|
|
v.year,
|
|
v.make,
|
|
v.model,
|
|
].filter(Boolean).join(' ');
|
|
|
|
return `
|
|
<div class="vnb-card" data-vehicle-id="${v.id}">
|
|
<div class="vnb-card__cover">${typeIcon}</div>
|
|
<div class="vnb-card__body">
|
|
<div class="vnb-card__title">${this.#esc(v.title)}</div>
|
|
${specsLine ? `<div class="vnb-card__specs">${this.#esc(specsLine)}</div>` : ''}
|
|
<div class="vnb-card__owner">
|
|
<span>${this.#esc(v.owner_name)}</span>
|
|
${v.instant_accept ? '<span style="font-size:0.7rem;padding:0.1rem 0.3rem;border-radius:9999px;background:rgba(52,211,153,0.12);color:#34d399">\u{26A1} Auto</span>' : ''}
|
|
</div>
|
|
<div class="vnb-card__meta">
|
|
<span class="vnb-badge ${eco.cls}">${eco.icon} ${eco.label}</span>
|
|
<span class="vnb-badge vnb-badge--type">${typeIcon} ${typeLabel}</span>
|
|
<span class="vnb-badge" style="background:rgba(148,163,184,0.08);color:#94a3b8;border:1px solid rgba(148,163,184,0.15)">\u{1F6CF} Sleeps ${v.sleeps}</span>
|
|
<span class="vnb-badge vnb-badge--mileage">${mileage.icon} ${mileage.label}</span>
|
|
</div>
|
|
<div class="vnb-card__location">\u{1F4CD} ${this.#esc(v.pickup_location_name)}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async #initMap(vehicles: any[]) {
|
|
try {
|
|
await ensureLeaflet();
|
|
} catch { return; }
|
|
|
|
const L = (window as any).L;
|
|
const mapEl = this.querySelector('#vnb-map');
|
|
if (!mapEl) return;
|
|
|
|
// Destroy previous map if any
|
|
if (this.#map) { this.#map.remove(); this.#map = null; }
|
|
|
|
this.#map = L.map(mapEl).setView([50, 10], 4);
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap',
|
|
maxZoom: 18,
|
|
}).addTo(this.#map);
|
|
|
|
const bounds: [number, number][] = [];
|
|
|
|
// Green pickup icon, teal dropoff icon
|
|
const pickupIcon = L.divIcon({
|
|
html: '<div style="background:#10b981;width:12px;height:12px;border-radius:50%;border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,0.3)"></div>',
|
|
iconSize: [16, 16],
|
|
iconAnchor: [8, 8],
|
|
className: '',
|
|
});
|
|
const dropoffIcon = L.divIcon({
|
|
html: '<div style="background:#06b6d4;width:12px;height:12px;border-radius:50%;border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,0.3)"></div>',
|
|
iconSize: [16, 16],
|
|
iconAnchor: [8, 8],
|
|
className: '',
|
|
});
|
|
|
|
for (const v of vehicles) {
|
|
const typeIcon = TYPE_ICONS[v.type] || '\u{1F690}';
|
|
const eco = ECONOMY_BADGE[v.economy] || ECONOMY_BADGE.suggested;
|
|
|
|
// Pickup marker
|
|
if (v.pickup_location_lat && v.pickup_location_lng) {
|
|
const latlng: [number, number] = [v.pickup_location_lat, v.pickup_location_lng];
|
|
bounds.push(latlng);
|
|
const marker = L.marker(latlng, { icon: pickupIcon }).addTo(this.#map);
|
|
marker.bindPopup(`
|
|
<div style="min-width:180px">
|
|
<strong>${typeIcon} ${this.#esc(v.title)}</strong><br>
|
|
<span style="font-size:0.85em;color:#666">${this.#esc(v.owner_name)}</span><br>
|
|
<span style="font-size:0.82em">${eco.icon} ${eco.label} · Sleeps ${v.sleeps}</span><br>
|
|
<span style="font-size:0.8em;color:#10b981">\u{1F4CD} Pickup: ${this.#esc(v.pickup_location_name)}</span>
|
|
</div>
|
|
`);
|
|
marker.on('click', () => this.#showVehicleRentals(v.id));
|
|
}
|
|
|
|
// Dropoff marker (only if different from pickup)
|
|
if (!v.dropoff_same_as_pickup && v.dropoff_location_lat && v.dropoff_location_lng) {
|
|
const dropLatlng: [number, number] = [v.dropoff_location_lat, v.dropoff_location_lng];
|
|
bounds.push(dropLatlng);
|
|
const dropMarker = L.marker(dropLatlng, { icon: dropoffIcon }).addTo(this.#map);
|
|
dropMarker.bindPopup(`
|
|
<div style="min-width:160px">
|
|
<strong>${typeIcon} ${this.#esc(v.title)}</strong><br>
|
|
<span style="font-size:0.8em;color:#06b6d4">\u{1F3C1} Dropoff: ${this.#esc(v.dropoff_location_name || '')}</span>
|
|
</div>
|
|
`);
|
|
}
|
|
}
|
|
|
|
if (bounds.length > 0) {
|
|
this.#map.fitBounds(bounds, { padding: [30, 30], maxZoom: 10 });
|
|
}
|
|
}
|
|
|
|
#showVehicleRentals(vehicleId: string) {
|
|
const vehicle = this.#vehicles.find((v: any) => v.id === vehicleId);
|
|
const vehicleRentals = this.#rentals.filter((r: any) => r.vehicle_id === vehicleId);
|
|
|
|
const sidebar = this.querySelector('#vnb-sidebar') as HTMLElement;
|
|
const content = this.querySelector('#sidebar-content') as HTMLElement;
|
|
if (!sidebar || !content) return;
|
|
|
|
sidebar.style.display = 'block';
|
|
|
|
const eco = ECONOMY_BADGE[vehicle?.economy] || ECONOMY_BADGE.suggested;
|
|
const specsLine = [vehicle?.year, vehicle?.make, vehicle?.model].filter(Boolean).join(' ');
|
|
|
|
content.innerHTML = `
|
|
<h3 style="margin:0 0 0.25rem;font-size:1rem;color:var(--rs-text,#e2e8f0)">${this.#esc(vehicle?.title || '')}</h3>
|
|
${specsLine ? `<p style="font-size:0.8rem;color:var(--rs-text-muted,#94a3b8);margin:0 0 0.5rem">${this.#esc(specsLine)}</p>` : ''}
|
|
<p style="font-size:0.82rem;color:var(--rs-text-muted,#94a3b8);margin:0 0 0.5rem">${this.#esc(vehicle?.description || '')}</p>
|
|
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:0.75rem">
|
|
<span class="vnb-badge ${eco.cls}">${eco.icon} ${eco.label}</span>
|
|
<span class="vnb-badge" style="background:rgba(148,163,184,0.08);color:#94a3b8;border:1px solid rgba(148,163,184,0.15)">\u{1F6CF} Sleeps ${vehicle?.sleeps || 0}</span>
|
|
${vehicle?.length_feet ? `<span class="vnb-badge" style="background:rgba(148,163,184,0.08);color:#94a3b8;border:1px solid rgba(148,163,184,0.15)">\u{1F4CF} ${vehicle.length_feet}'</span>` : ''}
|
|
</div>
|
|
|
|
<div style="font-size:0.78rem;color:var(--rs-text-muted,#64748b);margin-bottom:1rem;display:flex;flex-wrap:wrap;gap:0.4rem">
|
|
${vehicle?.has_generator ? '<span style="padding:0.1rem 0.35rem;background:rgba(16,185,129,0.08);border:1px solid rgba(16,185,129,0.15);border-radius:0.25rem;color:#34d399">Generator</span>' : ''}
|
|
${vehicle?.has_solar ? '<span style="padding:0.1rem 0.35rem;background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.15);border-radius:0.25rem;color:#f59e0b">Solar</span>' : ''}
|
|
${vehicle?.has_ac ? '<span style="padding:0.1rem 0.35rem;background:rgba(96,165,250,0.08);border:1px solid rgba(96,165,250,0.15);border-radius:0.25rem;color:#60a5fa">A/C</span>' : ''}
|
|
${vehicle?.has_heating ? '<span style="padding:0.1rem 0.35rem;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.15);border-radius:0.25rem;color:#ef4444">Heating</span>' : ''}
|
|
${vehicle?.has_shower ? '<span style="padding:0.1rem 0.35rem;background:rgba(96,165,250,0.08);border:1px solid rgba(96,165,250,0.15);border-radius:0.25rem;color:#60a5fa">Shower</span>' : ''}
|
|
${vehicle?.has_toilet ? '<span style="padding:0.1rem 0.35rem;background:rgba(148,163,184,0.08);border:1px solid rgba(148,163,184,0.15);border-radius:0.25rem;color:#94a3b8">Toilet</span>' : ''}
|
|
${vehicle?.has_kitchen ? '<span style="padding:0.1rem 0.35rem;background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.15);border-radius:0.25rem;color:#f59e0b">Kitchen</span>' : ''}
|
|
${vehicle?.pet_friendly ? '<span style="padding:0.1rem 0.35rem;background:rgba(167,139,250,0.08);border:1px solid rgba(167,139,250,0.15);border-radius:0.25rem;color:#a78bfa">Pet Friendly</span>' : ''}
|
|
</div>
|
|
|
|
<h4 style="font-size:0.88rem;color:var(--rs-text,#e2e8f0);margin:1rem 0 0.75rem;border-top:1px solid var(--rs-border,#334155);padding-top:1rem">
|
|
Rental Requests (${vehicleRentals.length})
|
|
</h4>
|
|
${vehicleRentals.length === 0
|
|
? '<p style="font-size:0.82rem;color:var(--rs-text-muted,#64748b)">No rental requests yet.</p>'
|
|
: vehicleRentals.map((r: any) => `
|
|
<div class="rental-item" data-rental-id="${r.id}" style="padding:0.75rem;margin-bottom:0.5rem;border:1px solid var(--rs-border,#334155);border-radius:0.5rem;cursor:pointer;transition:background 0.15s">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.25rem">
|
|
<span style="font-size:0.85rem;font-weight:600;color:var(--rs-text,#e2e8f0)">${this.#esc(r.renter_name)}</span>
|
|
<span class="vnb-status vnb-status--${r.status}">${r.status}</span>
|
|
</div>
|
|
<div style="font-size:0.75rem;color:var(--rs-text-muted,#94a3b8)">
|
|
${r.pickup_date ? new Date(r.pickup_date).toLocaleDateString() : ''} \u{2192} ${r.dropoff_date ? new Date(r.dropoff_date).toLocaleDateString() : ''}
|
|
${r.estimated_miles ? `· ~${r.estimated_miles} mi` : ''}
|
|
· ${r.messages?.length || 0} msg${(r.messages?.length || 0) !== 1 ? 's' : ''}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
`;
|
|
|
|
// Wire rental item clicks to show detail
|
|
for (const item of content.querySelectorAll('.rental-item')) {
|
|
item.addEventListener('click', () => {
|
|
const rentalId = (item as HTMLElement).dataset.rentalId;
|
|
const rental = this.#rentals.find((r: any) => r.id === rentalId);
|
|
if (rental) this.#showRentalDetail(rental);
|
|
});
|
|
}
|
|
}
|
|
|
|
#showRentalDetail(rental: any) {
|
|
const content = this.querySelector('#sidebar-content') as HTMLElement;
|
|
if (!content) return;
|
|
|
|
content.innerHTML = `
|
|
<button id="back-to-vehicle" style="background:none;border:none;color:var(--rs-text-muted,#94a3b8);cursor:pointer;font-size:0.82rem;margin-bottom:0.75rem;padding:0">
|
|
\u{2190} Back to vehicle
|
|
</button>
|
|
<folk-rental-request rental-id="${rental.id}" space="${this.#space}"></folk-rental-request>
|
|
`;
|
|
|
|
content.querySelector('#back-to-vehicle')?.addEventListener('click', () => {
|
|
this.#showVehicleRentals(rental.vehicle_id);
|
|
});
|
|
}
|
|
|
|
#closeSidebar() {
|
|
const sidebar = this.querySelector('#vnb-sidebar') as HTMLElement;
|
|
if (sidebar) sidebar.style.display = 'none';
|
|
}
|
|
|
|
async #handleRentalAction(rentalId: string, action: string) {
|
|
try {
|
|
const res = await fetch(`/${this.#space}/rvnb/api/rentals/${rentalId}/${action}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
if (res.ok) {
|
|
await this.#loadData();
|
|
const rental = this.#rentals.find((r: any) => r.id === rentalId);
|
|
if (rental) this.#showRentalDetail(rental);
|
|
}
|
|
} catch (err) {
|
|
console.warn('[rVnb] Action failed:', err);
|
|
}
|
|
}
|
|
|
|
async #handleRentalMessage(rentalId: string, body: string) {
|
|
try {
|
|
const res = await fetch(`/${this.#space}/rvnb/api/rentals/${rentalId}/messages`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ body, sender_name: 'You' }),
|
|
});
|
|
if (res.ok) {
|
|
await this.#loadData();
|
|
const rental = this.#rentals.find((r: any) => r.id === rentalId);
|
|
if (rental) this.#showRentalDetail(rental);
|
|
}
|
|
} catch (err) {
|
|
console.warn('[rVnb] Message failed:', err);
|
|
}
|
|
}
|
|
|
|
#esc(s: string): string {
|
|
const el = document.createElement('span');
|
|
el.textContent = s || '';
|
|
return el.innerHTML;
|
|
}
|
|
}
|
|
|
|
if (!customElements.get('folk-vnb-view')) {
|
|
customElements.define('folk-vnb-view', FolkVnbView);
|
|
}
|
|
|
|
export { FolkVnbView };
|