LA PEÑA DE LA CAPITANA- LA VALENTI - Teatro Margarita Xirgu | 7 de noviembre | Buenos Aires, Argentina.
Después de agotar las ocho funciones de "La Peña de La Capitana" en el Santa Evita, La Valenti se anima a lo más grande: por primera vez, lleva este ciclo a un teatro, con una puesta en escena nunca antes vista.
Una noche única e irrepetible, con invitados especiales, para vivir su repertorio y los grandes clásicos del folclore latinoamericano como nunca antes los escuchaste.
"Hay lugares que te esperan, aunque no sepas dónde. El mío resultó ser un escenario."
No te la pierdas: el 7 de noviembre, La Gran Peña de La Capitana llega, por primera vez, al Teatro Margarita Xirgu.
"La Peña de La Capitana" en el Xirgu
7 de noviembre a las 20:00
CABA
"La Peña de La Capitana" en el Xirgu
7 de noviembre a las 20:00
CABA
Evento por
La Valenti
Organizadora verificada
SUCEDE EN 80 DÍAS
Entradas disponibles
Conseguí la tuya
En venta desde $ 35.000,00
7 de noviembre a las 20:00
en 3 meses
$ 35.000,00
precio base
Locación
Teatro Xirgu
Chacabuco 875, C1069AAR Cdad. Autónoma de Buenos Aires, Argentina
Evento por
La Valenti
Organizadora verificada
LA PEÑA DE LA CAPITANA- LA VALENTI - Teatro Margarita Xirgu | 7 de noviembre | Buenos Aires, Argentina.
Después de agotar las ocho funciones de "La Peña de La Capitana" en el Santa Evita, La Valenti se anima a lo más grande: por primera vez, lleva este ciclo a un teatro, con una puesta en escena nunca antes vista.
Una noche única e irrepetible, con invitados especiales, para vivir su repertorio y los grandes clásicos del folclore latinoamericano como nunca antes los escuchaste.
"Hay lugares que te esperan, aunque no sepas dónde. El mío resultó ser un escenario."
No te la pierdas: el 7 de noviembre, La Gran Peña de La Capitana llega, por primera vez, al Teatro Margarita Xirgu.
SUCEDE EN 80 DÍAS
Entradas disponibles
Conseguí la tuya
Sin vueltas ni demoras
Venta de entradas recaudada por el Organizador.
Digital e independiente
Para vos, entrada en formato QR via mail; para el Organizador, liquidación según su preferencia
Elegí tu sector
Tu tiempo se agotó. Los asientos fueron liberados.
Podés elegir hasta 5 asientos por compra.
Platea Preferencial
$ 45.000,00
Incluye sorteo Meet&Greet previo al show
Platea
$ 40.000,00
Palcos Bajos
$ 40.000,00
Palcos Altos
$ 35.000,00
Frontales
$ 42.000,00
Platea 1
Numerado
$ 45.000,00
Platea 2
Numerado
$ 45.000,00
Platea 1
Numerado
$ 40.000,00
Platea 2
Numerado
$ 40.000,00
Palco 1
Numerado
$ 40.000,00
Palco 2
Numerado
$ 40.000,00
Palco 3
Numerado
$ 40.000,00
Palco 4
Numerado
$ 40.000,00
Palco 5
Numerado
$ 40.000,00
Palco 6
Numerado
$ 40.000,00
Palco 7
Numerado
$ 40.000,00
Palco 8
Numerado
$ 40.000,00
Palco 9
Numerado
$ 40.000,00
Palco 10
Numerado
$ 40.000,00
Palco 11
Numerado
$ 40.000,00
Palco 12
Numerado
$ 40.000,00
Palco 13
Numerado
$ 40.000,00
Palco 18
Numerado
$ 40.000,00
Palco 15
Numerado
$ 40.000,00
Palco 16
Numerado
$ 40.000,00
Palco 17
Numerado
$ 40.000,00
Palco 14
Numerado
$ 40.000,00
Palco 2
Numerado
$ 35.000,00
Palco 1
Numerado
$ 35.000,00
Palco 4
Numerado
$ 35.000,00
Palco 3
Numerado
$ 35.000,00
Palco 6
Numerado
$ 35.000,00
Palco 5
Numerado
$ 35.000,00
Palco 8
Numerado
$ 35.000,00
Palco 7
Numerado
$ 35.000,00
Palco 9
Numerado
$ 35.000,00
Palco 16
Numerado
$ 35.000,00
Palco 15
Numerado
$ 35.000,00
Palco 18
Numerado
$ 35.000,00
Palco 17
Numerado
$ 35.000,00
Palco 10
Numerado
GRATIS
Palco 12
Numerado
GRATIS
Palco 11
Numerado
GRATIS
Palco 14
Numerado
GRATIS
Palco 13
Numerado
GRATIS
Palco 19
Numerado
$ 42.000,00
Palco 21
Numerado
$ 42.000,00
Palco 22
Numerado
$ 42.000,00
Palco 20
Numerado
$ 42.000,00
Presidencial
Numerado
$ 42.000,00
Este asiento ya fue tomado por otro usuario.
0 && activeSvgIds.indexOf(el.id) === -1);
});
}
function clearInactive() {
var svgEl = container.querySelector('svg');
if (!svgEl) return;
svgEl.querySelectorAll('.venue-sector--inactive').forEach(function(el) {
el.classList.remove('venue-sector--inactive');
});
}
// ---- State machine ----
function transitionTo(newState) {
state = newState;
groupListEl.style.display = 'none';
sectorListEl.style.display = 'none';
backBtn.style.display = 'none';
footerEl.style.display = newState === 'SEAT_PICK' ? '' : 'none';
// El layout flex + space-between (referencia arriba, mapa distribuido, mini-mapa
// abajo) es específico del paso de elección de asiento; en los otros pasos el
// contenido va apilado normalmente de arriba hacia abajo.
contentWrapperEl.classList.toggle('d-flex', newState === 'SEAT_PICK');
contentWrapperEl.classList.toggle('flex-column', newState === 'SEAT_PICK');
contentWrapperEl.classList.toggle('justify-content-between', newState === 'SEAT_PICK');
clearInactive();
var activeGroup = null;
if (newState === 'GROUP_PICK') {
groupListEl.style.display = '';
subtitleEl.textContent = 'Elegí tu sector';
activeGroupId = null;
} else if (newState === 'SECTOR_PICK') {
sectorListEl.style.display = '';
// activeGroupId siempre está seteado en SECTOR_PICK (click de grupo o skip de 1 grupo).
// Mostramos solo los sectores del grupo activo.
sectorListEl.querySelectorAll('[data-sector-id]').forEach(function(card) {
var cardGroupId = card.getAttribute('data-sector-group-id');
if (cardGroupId == activeGroupId) { card.style.removeProperty('display'); } else { card.style.setProperty('display', 'none', 'important'); }
});
activeGroup = VENUE_MAP_DATA.groups.find(function(g) { return g.id === activeGroupId; });
subtitleEl.textContent = activeGroup ? humanize(activeGroup.name) : 'Elegí tu sector';
// Con un solo grupo no hay GROUP_PICK al que volver → sin botón atrás acá.
if (!GROUP_PICK_SKIPPED) backBtn.style.display = '';
setGroupActive(activeGroupId);
} else if (newState === 'SEAT_PICK') {
subtitleEl.textContent = 'Elegí tu lugar';
if (previousState) backBtn.style.display = '';
}
// Al (re)entrar a la vista general del mapa (elección de grupo/sector) refrescamos
// la disponibilidad por sector: pudo cambiar mientras el usuario estaba dentro de un
// sector. En SEAT_PICK no aplica (ahí se ve la grilla de butacas, no el mapa). En el
// arranque esto es no-op (map aún no existe); el fetch inicial lo hace la llamada
// explícita tras crear el mapa. En GROUP_PICK se consulta TODO el plano (es la vista
// completa); en SECTOR_PICK solo los sectores del grupo activo (los demás están
// grisados/inactivos y no hace falta su disponibilidad en este paso).
if (newState === 'GROUP_PICK') {
refreshSoldOutSectors();
} else if (newState === 'SECTOR_PICK') {
refreshSoldOutSectors(activeGroup ? activeGroup.sector_svg_ids : undefined);
}
}
// ---- Init: run initial state ----
// Con un solo grupo visible activamos ese grupo antes de entrar directo a SECTOR_PICK.
if (GROUP_PICK_SKIPPED) activeGroupId = VISIBLE_GROUPS[0].id;
transitionTo(INITIAL_STATE);
// ---- Group list click (GROUP_PICK → SECTOR_PICK or SEAT_PICK) ----
groupListEl.addEventListener('click', function(e) {
var groupCard = e.target.closest('[data-group-id]');
if (groupCard) {
expiredWarn.style.display = 'none';
var groupId = parseInt(groupCard.getAttribute('data-group-id'), 10);
var group = VENUE_MAP_DATA.groups.find(function(g) { return g.id === groupId; });
if (!group) return;
activeGroupId = groupId;
previousState = 'GROUP_PICK';
if (group.sector_svg_ids.length === 1) {
// Single-sector group — skip SECTOR_PICK
var sectorId = group.sector_svg_ids[0];
var sector = VENUE_MAP_DATA.sectors.find(function(s) { return s.svg_id === sectorId; });
if (!sector) return;
if (sector.sector_type === 'general') {
map && map.renderer && map.renderer.selectSector(sectorId);
} else {
map && map.renderer && map.renderer.enterSector(sectorId);
}
} else {
transitionTo('SECTOR_PICK');
}
return;
}
});
// ---- Sector list click (SECTOR_PICK → SEAT_PICK) ----
sectorListEl.addEventListener('click', function(e) {
var card = e.target.closest('[data-sector-id]');
if (!card) return;
var sectorId = card.getAttribute('data-sector-id');
var sector = VENUE_MAP_DATA.sectors.find(function(s) { return s.svg_id === sectorId; });
if (!sector) return;
previousState = 'SECTOR_PICK';
if (sector.sector_type === 'general') {
map && map.renderer && map.renderer.selectSector(sectorId);
} else {
map && map.renderer && map.renderer.enterSector(sectorId);
}
});
// ---- Back button ----
backBtn.addEventListener('click', function() {
if (state === 'SEAT_PICK') {
isNavigatingBack = true;
stopPolling();
stopTimer();
// releaseAllHolds() snapshotea selectedSectorId/selectedSeatIds de forma síncrona
// antes de arrancar los fetch DELETE, así que liberar "de fondo" (sin esperar su
// promesa) es seguro: el reset visual de abajo no depende de que el request termine.
releaseAllHolds();
selectedSectorId = null;
selectedSeatIds = [];
if (map) map.reset();
updateSummary();
updateNextButton();
var dest = previousState || INITIAL_STATE;
previousState = null;
if (dest !== 'SECTOR_PICK') activeGroupId = null;
isNavigatingBack = false;
transitionTo(dest);
} else if (state === 'SECTOR_PICK') {
if (GROUP_PICK_SKIPPED) return; // 1 grupo → no hay GROUP_PICK al que volver
activeGroupId = null;
transitionTo('GROUP_PICK');
}
});
// ---- VenueMap setup ----
var sectorConfigs = VENUE_MAP_DATA.sectors.map(function(s) {
var cfg = { id: s.svg_id };
if (s.numbering) cfg.numbering = s.numbering;
if (s.seatsPerRow) cfg.seatsPerRow = s.seatsPerRow;
if (s.resetRow) cfg.resetRow = s.resetRow;
if (s.rowDir) cfg.rowDir = s.rowDir;
if (s.seatDir) cfg.seatDir = s.seatDir;
if (s.align) cfg.align = s.align;
if (s.img_url) cfg.imgUrl = s.img_url;
return cfg;
});
var sectorAccessMap = {};
VENUE_MAP_DATA.sectors.forEach(function(s) {
sectorAccessMap[s.svg_id] = { access_id: s.access_id, access_name: s.access_name, name: s.name, group_id: s.group_id, price: s.price, currency: s.currency };
});
var svgScriptEl = document.getElementById('venue-svg-content');
var map;
if (svgScriptEl) {
// Revertir el escape de " 0) {
var applyHiddenSectors = function() {
var sectorsGroup = container.querySelector('svg #sectors');
if (!sectorsGroup) return false;
HIDDEN_SECTOR_IDS.forEach(function(svgId) {
var el = sectorsGroup.querySelector('[id="' + svgId + '"]');
if (el) el.classList.add('pe-none');
});
return true;
};
if (!applyHiddenSectors()) {
var hiddenSectorsObserver = new MutationObserver(function() {
if (applyHiddenSectors()) hiddenSectorsObserver.disconnect();
});
hiddenSectorsObserver.observe(container, { childList: true, subtree: true });
}
}
// ---- Sectores agotados (grisado a nivel sector) ----
// Consulta la disponibilidad agregada por sector (mode=counts) y grisa/deshabilita
// los agotados. Se ejecuta al montar el mapa Y cada vez que se (re)entra a la vista
// general del mapa (ver transitionTo): la disponibilidad pudo cambiar mientras el
// usuario estaba dentro de un sector. Aplica SIEMPRE el resultado (aunque el set de
// agotados quede vacío) para des-grisar sectores que se liberaron. El SVG es un blob
// local (el renderer queda listo casi de inmediato) mientras que este fetch va a la
// red y resuelve después, así map.setUnavailable ya encuentra el mapa renderizado.
// Best-effort: si falla, no toca el mapa.
// sectorCodes opcional: sin él trae TODOS los sectores del plano (paso de elección de
// grupo, donde el mapa completo es visible); pasándolo limita la consulta a esos
// sectores puntuales (paso intermedio de elección de sector dentro de un grupo).
function refreshSoldOutSectors(sectorCodes) {
if (!map) return;
var url = '/api/events/' + EVENT_ID + '/sectors/unavailable-seats?event_date_id='
+ VENUE_MAP_DATA.event_date_id + '&mode=counts';
if (sectorCodes && sectorCodes.length > 0) {
url += '§or_codes=' + encodeURIComponent(sectorCodes.join(','));
}
fetch(url, { credentials: 'same-origin' })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!map || !data || !data.sectors) return;
var soldOut = data.sectors
.filter(function(s) { return s.available === false; })
.map(function(s) { return s.sector_code; });
map.setUnavailable(soldOut);
})
.catch(function() { /* best-effort */ });
}
// Fetch inicial: transitionTo(INITIAL_STATE) corrió antes de que `map` existiera (era
// no-op), así que lo disparamos acá una vez creado, con el mismo alcance que le
// correspondería al estado inicial (todo el plano, o solo el grupo si se saltó GROUP_PICK).
refreshSoldOutSectors(GROUP_PICK_SKIPPED ? VISIBLE_GROUPS[0].sector_svg_ids : undefined);
// ---- Session token ----
function getSessionToken() {
var match = document.cookie.match(/(?:^|;\s*)venue_session=([^;]+)/);
return match ? match[1] : null;
}
// ---- API helpers ----
function apiHold(seatId) {
return fetch('/api/events/' + EVENT_ID + '/seats/hold', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event_date_id: VENUE_MAP_DATA.event_date_id, sector_code: selectedSectorId, seat_id: seatId }),
}).then(function(r) { return r.json(); });
}
// Libera uno o más holds de un sector en un ÚNICO request. Acepta un array de
// seat ids (o un id suelto). keepalive:true para que el request sobreviva al
// unload: así el mismo helper sirve desde 'pagehide' (un fetch normal se cancela
// al descargar el documento) y un solo llamado evita que el browser dropee los
// DELETE en paralelo que hacía la versión anterior.
function apiRelease(sectorId, seatIds) {
var ids = Array.isArray(seatIds) ? seatIds : [seatIds];
if (ids.length === 0) return Promise.resolve({ ok: true });
return fetch('/api/events/' + EVENT_ID + '/seats/hold', {
method: 'DELETE', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
keepalive: true,
body: JSON.stringify({ event_date_id: VENUE_MAP_DATA.event_date_id, sector_code: sectorId, seat_ids: ids }),
}).then(function(r) { return r.json(); });
}
function fetchUnavailable(sectorId) {
// Dedup de llamados solapados: cada nuevo fetch cancela el anterior en vuelo
// (poll de 15s + sector-enter + post-hold pueden dispararse casi juntos), así
// una respuesta vieja no pisa el estado de butacas más reciente.
if (unavailableAbort) unavailableAbort.abort();
unavailableAbort = new AbortController();
var signal = unavailableAbort.signal;
fetch('/api/events/' + EVENT_ID + '/sectors/unavailable-seats?event_date_id=' + VENUE_MAP_DATA.event_date_id + '§or_code=' + encodeURIComponent(sectorId), { credentials: 'same-origin', signal: signal })
.then(function(r) { return r.json(); })
.then(function(data) {
if (signal.aborted) return;
if (map && data.unavailable) {
var ownSeats = new Set(selectedSeatIds);
var unavailable = data.unavailable.filter(function(id) { return !ownSeats.has(id); });
map.setUnavailableSeats(sectorId, unavailable);
setTimeout(function() {
container.querySelectorAll('.venue-seat--selected').forEach(function(btn) {
var sid = btn.getAttribute('data-seat-id');
if (sid && !ownSeats.has(sid)) btn.classList.remove('venue-seat--selected');
});
}, 0);
}
}).catch(function() {});
}
// ---- Timer ----
function startTimer() {
if (timerInterval) return;
timerSeconds = 300;
timerWrap.style.display = '';
timerInterval = setInterval(function() {
timerSeconds--;
if (timerSeconds <= 0) { clearInterval(timerInterval); timerInterval = null; onTimerExpired(); return; }
var m = Math.floor(timerSeconds / 60);
var s = timerSeconds % 60;
timerDisplay.textContent = (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
}, 1000);
}
function stopTimer() {
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
timerSeconds = 0;
// setProperty(..., 'important'): hay una regla CSS con !important que le gana a un
// display:none inline seteado sin prioridad.
timerWrap.style.setProperty('display', 'none', 'important');
timerDisplay.textContent = '05:00';
}
function onTimerExpired() {
// Liberar los holds en Redis al expirar el timer. releaseAllHolds() snapshotea
// selectedSectorId/selectedSeatIds de forma SÍNCRONA, por eso se invoca ANTES de
// limpiar la selección; si no, las butacas quedan holdeadas hasta que vence su TTL
// en Redis (hasta 5 min), bloqueándolas para otros compradores.
releaseAllHolds();
timerDisplay.textContent = '00:00';
expiredWarn.style.display = '';
nextBtn.disabled = true;
summaryEl.style.display = 'none';
selectedSectorId = null;
selectedSeatIds = [];
if (map) map.reset();
stopPolling();
previousState = null;
activeGroupId = null;
transitionTo(INITIAL_STATE);
}
function releaseAllHolds(cb) {
if (!selectedSectorId || selectedSeatIds.length === 0) { if (cb) cb(); return; }
apiRelease(selectedSectorId, selectedSeatIds.slice())
.then(function() { if (cb) cb(); }).catch(function() { if (cb) cb(); });
}
// ---- Actividad del usuario ----
// Reutilizamos UserActivityTracker (ver /js/UserActivityTracker.js) para no
// golpear /unavailable "a ciegas": sólo refrescamos si el usuario está presente
// (pestaña visible + interacción reciente). onResume hace el catch-up inmediato
// al volver de inactivo->activo o al reactivar la pestaña.
var activityTracker = new UserActivityTracker({
idleThresholdMs: IDLE_THRESHOLD_MS,
onResume: function () { fetchUnavailable(currentPollSectorId); }
});
function startPolling(sectorId) {
stopPolling();
currentPollSectorId = sectorId;
activityTracker.start();
pollingInterval = setInterval(function() {
// Sólo golpeamos el endpoint si el usuario está activo; si no, el tracker
// recuerda el estado idle y dispara un catch-up cuando la actividad vuelve.
if (activityTracker.isActive()) fetchUnavailable(currentPollSectorId);
}, POLL_INTERVAL_MS);
}
function stopPolling() {
if (pollingInterval) { clearInterval(pollingInterval); pollingInterval = null; }
if (unavailableAbort) { unavailableAbort.abort(); unavailableAbort = null; }
currentPollSectorId = null;
activityTracker.stop();
}
// ---- UI helpers ----
function sectorDisplayName(info) {
if (!info) return selectedSectorId;
var sectorName = info.name || selectedSectorId;
var group = VENUE_MAP_DATA.groups.find(function(g) { return g.id === info.group_id; });
return group ? humanize(group.name) + '/' + sectorName : sectorName;
}
function updateSummary() {
if (!selectedSectorId) { summaryEl.style.display = 'none'; stopTimer(); return; }
var info = sectorAccessMap[selectedSectorId];
var label = sectorDisplayName(info);
var text = selectedSeatIds.length > 0
? selectedSeatIds.length + ' asiento' + (selectedSeatIds.length > 1 ? 's' : '') + ' en ' + label
: 'Sector: ' + label;
summaryEl.textContent = text;
summaryEl.style.display = '';
// Fuente única de verdad para la visibilidad del countdown: se llama desde todos los
// puntos donde cambia la selección, así timerWrap nunca queda visible sin butacas.
if (selectedSeatIds.length === 0) stopTimer();
}
function updateNextButton() {
var sector = VENUE_MAP_DATA.sectors.find(function(s) { return s.svg_id === selectedSectorId; });
var hasSelection = selectedSectorId && (sector?.sector_type === 'general' ? true : selectedSeatIds.length > 0);
nextBtn.disabled = !hasSelection;
}
// ---- Aviso de tope de butacas: entra con fadeIn+slideUp, se auto-oculta a los 10s
// con la animación inversa (fadeOut+slideDown). ----
function showMaxSeatsWarning() {
if (maxWarnFadeOutTimeout) { clearTimeout(maxWarnFadeOutTimeout); maxWarnFadeOutTimeout = null; }
if (maxWarnAutoHideTimeout) clearTimeout(maxWarnAutoHideTimeout);
maxWarn.classList.remove('venue-max-seats-warning--out');
maxWarn.style.display = '';
// Forzar reflow: si ya estaba visible y se vuelve a disparar, re-arranca la animación in.
void maxWarn.offsetWidth;
maxWarn.classList.add('venue-max-seats-warning--in');
maxWarnAutoHideTimeout = setTimeout(hideMaxSeatsWarning, 10000);
}
function hideMaxSeatsWarning() {
if (maxWarnAutoHideTimeout) { clearTimeout(maxWarnAutoHideTimeout); maxWarnAutoHideTimeout = null; }
if (maxWarn.style.display === 'none') return;
maxWarn.classList.remove('venue-max-seats-warning--in');
maxWarn.classList.add('venue-max-seats-warning--out');
maxWarnFadeOutTimeout = setTimeout(function() {
maxWarn.style.display = 'none';
maxWarn.classList.remove('venue-max-seats-warning--out');
maxWarnFadeOutTimeout = null;
}, 300);
}
// ---- VenueMap events ----
container.addEventListener('venue:sector-select', function(e) {
expiredWarn.style.display = 'none';
selectedSectorId = e.detail.sectorId;
selectedSeatIds = [];
stopPolling();
hideMaxSeatsWarning();
proceedToForm();
});
container.addEventListener('venue:sector-enter', function(e) {
expiredWarn.style.display = 'none';
selectedSectorId = e.detail.sectorId;
selectedSeatIds = [];
inPurchaseFlow = false;
// startPolling() va ANTES del fetch inmediato: internamente llama a stopPolling(),
// que aborta el request de unavailable-seats en vuelo. Si el fetch inmediato fuera
// primero, ese mismo stopPolling() lo cancelaría siempre (el primer request "se
// cancela"). Con este orden, stopPolling() limpia cualquier request stale del sector
// anterior y recién después disparamos el fetch inmediato, que ya no se aborta.
startPolling(e.detail.sectorId);
fetchUnavailable(e.detail.sectorId);
updateSummary();
updateNextButton();
hideMaxSeatsWarning();
previousState = state;
transitionTo('SEAT_PICK');
});
container.addEventListener('venue:sector-exit', function() {
if (isNavigatingBack) return;
stopPolling();
stopTimer();
releaseAllHolds(function() {
selectedSectorId = null;
selectedSeatIds = [];
updateSummary();
updateNextButton();
transitionTo(previousState || INITIAL_STATE);
previousState = null;
});
});
// El renderer hace cumplir MAX_SEATS: al alcanzar el tope, un click sobre una butaca
// no seleccionada NO se agrega a la selección (no se "encola") y dispara este evento.
// Sólo mostramos la alerta; no tocamos selectedSeatIds ni /hold.
container.addEventListener('venue:seat-limit', function() {
showMaxSeatsWarning();
});
container.addEventListener('venue:seat-select', function(e) {
var newSeatIds = e.detail.seatIds;
hideMaxSeatsWarning();
var prevSet = new Set(selectedSeatIds);
var newSet = new Set(newSeatIds);
var added = newSeatIds.filter(function(id) { return !prevSet.has(id); });
var removed = selectedSeatIds.filter(function(id) { return !newSet.has(id); });
added.forEach(function(seatId) {
apiHold(seatId).then(function(res) {
if (res.error === 'SEAT_TAKEN') {
var btn = container.querySelector('.venue-seat[data-seat-id="' + seatId + '"]');
if (btn) { btn.classList.remove('venue-seat--selected'); btn.classList.add('venue-seat--unavailable'); btn.disabled = true; }
selectedSeatIds = selectedSeatIds.filter(function(id) { return id !== seatId; });
updateSummary(); updateNextButton(); showSeatTakenToast();
} else if (res.ok) {
// La respuesta del hold puede llegar después de que el usuario ya deseleccionó
// este asiento (y liberó el hold). Si ya no está en la selección actual, arrancar
// el timer acá lo dejaría corriendo para una selección vacía.
if (selectedSeatIds.indexOf(seatId) !== -1) startTimer();
}
fetchUnavailable(selectedSectorId);
}).catch(function() {});
});
// Deseleccionar también refresca el mapa: al liberar el hold, esas butacas vuelven a
// estar disponibles, así que re-pedimos /unavailable-seats para que el estado del
// renderer refleje la liberación (simétrico al refresh post-hold de la rama `added`).
if (removed.length) apiRelease(selectedSectorId, removed).then(function() { fetchUnavailable(selectedSectorId); }).catch(function() {});
selectedSeatIds = newSeatIds.slice();
updateSummary();
updateNextButton();
});
// ---- Ir al formulario de compra ----
function proceedToForm() {
var info = sectorAccessMap[selectedSectorId];
if (!info) return;
// inPurchaseFlow se marca en el submit real del formulario (ver listener más abajo),
// no acá: llegar al formulario todavía no es un pago en curso, y si el usuario recarga
// o cierra mientras completa sus datos, el hold debe liberarse igual.
window.__venueSeatSelection = {
access_id: info.access_id,
access_name: info.access_name,
sector_name: sectorDisplayName(info),
sector_id: selectedSectorId,
seat_ids: selectedSeatIds.slice(),
event_date_id: VENUE_MAP_DATA.event_date_id,
};
if (typeof populatePurchaseForm === 'function') populatePurchaseForm(info.access_id);
if (typeof applyVenueSeatMode === 'function') applyVenueSeatMode(window.__venueSeatSelection);
if (typeof venuePurchaseOffcanvas !== 'undefined') {
proceedingToPurchase = true;
//venuePurchaseOffcanvas.close();
}
if (typeof purchaseOffcanvas !== 'undefined') purchaseOffcanvas.open();
}
// ---- Botón "Siguiente" ----
nextBtn.addEventListener('click', function() {
if (!selectedSectorId) return;
proceedToForm();
});
// ---- Refrescar disponibilidad al abrir el offcanvas ----
// El body de este offcanvas se renderiza server-side una sola vez (no vía fetch), así
// que el script corre al cargar la página, no necesariamente cuando el usuario lo abre.
// Si pasó tiempo entre carga y apertura, el fetch inicial de refreshSoldOutSectors()
// puede haber quedado desactualizado. offcanvas.ejs dispara 'offcanvas:open' (evento de
// jQuery, no nativo) en cada open(), incluida la primera vez.
var venueOffcanvasEl = document.getElementById('venuePurchase-offcanvas');
if (venueOffcanvasEl && window.jQuery) {
$(venueOffcanvasEl).on('offcanvas:open', function() {
if (state === 'SEAT_PICK') return;
var activeGroup = state === 'SECTOR_PICK'
? VENUE_MAP_DATA.groups.find(function(g) { return g.id === activeGroupId; })
: null;
refreshSoldOutSectors(activeGroup ? activeGroup.sector_svg_ids : undefined);
});
}
// ---- Release holds on offcanvas dismiss ----
if (venueOffcanvasEl) {
venueOffcanvasEl.addEventListener('hide.bs.offcanvas', function() {
if (proceedingToPurchase) { proceedingToPurchase = false; return; }
isNavigatingBack = true;
stopPolling();
stopTimer();
releaseAllHolds();
selectedSectorId = null;
selectedSeatIds = [];
previousState = null;
activeGroupId = null;
if (map) map.reset();
updateSummary();
updateNextButton();
isNavigatingBack = false;
transitionTo(INITIAL_STATE);
});
}
// Marca el submit real del formulario de compra: recién ahí protegemos el hold de
// liberarse (ver pagehide y el listener de purchase-offcanvas más abajo).
var purchaseFormEl = document.getElementById('purchase_form');
if (purchaseFormEl) {
purchaseFormEl.addEventListener('submit', function() { inPurchaseFlow = true; });
}
// ---- Release holds si se cierra el formulario de datos personales sin pagar ----
// Cerrar ese offcanvas (X, backdrop, ESC) en cualquier paso del venue debe liberar los
// asientos y volver la sesión al primer paso de selección. No aplica si es un "volver al
// mapa" (window.__venueReturningToMap, misma sección) ni si ya se envió el pago.
var purchaseFormOffcanvasEl = document.getElementById('purchase-offcanvas');
if (purchaseFormOffcanvasEl) {
purchaseFormOffcanvasEl.addEventListener('hide.bs.offcanvas', function() {
if (window.__venueReturningToMap) { window.__venueReturningToMap = false; return; }
if (inPurchaseFlow) return;
if (!selectedSectorId || selectedSeatIds.length === 0) return;
isNavigatingBack = true;
stopPolling();
stopTimer();
releaseAllHolds();
selectedSectorId = null;
selectedSeatIds = [];
previousState = null;
activeGroupId = null;
window.__venueSeatSelection = null;
if (map) map.reset();
updateSummary();
updateNextButton();
isNavigatingBack = false;
transitionTo(INITIAL_STATE);
});
}
// ---- Liberar holds al abandonar la página (cerrar pestaña / navegar / recargar) ----
// apiRelease() manda un ÚNICO DELETE con { keepalive: true }, que sobrevive al unload
// (un fetch normal se cancela al descargar el documento) y libera TODAS las butacas de
// una. La versión anterior disparaba un keepalive por asiento y el browser dropeaba la
// mayoría, dejando holds colgados hasta que vencía su TTL en Redis. No liberamos si ya
// entró al flujo de compra (inPurchaseFlow / proceedingToPurchase): en ese caso los holds
// se consumen al pagar y liberarlos podría revender la butaca durante el pago. Best-effort.
window.addEventListener('pagehide', function() {
if (inPurchaseFlow || proceedingToPurchase) return;
if (!selectedSectorId || selectedSeatIds.length === 0) return;
try {
apiRelease(selectedSectorId, selectedSeatIds.slice());
} catch (e) { /* best-effort: si falla, el TTL de Redis igual libera */ }
});
})();
Ya tenés una compra en curso
Tenés una compra en proceso para este evento.
Si iniciás una compra nueva, la anterior se descartará.
¿Qué tipo de entrada querías?
Palcos Altos Laterales
$ 35.000,00
Platea
$ 40.000,00
Palcos Bajos
$ 40.000,00
Palcos Altos Frontales
$ 42.000,00
Platea Preferencial
Incluye sorteo Meet&Greet previo al show
$ 45.000,00
Tipo de entrada:
Palcos Altos Laterales
cambiar
"La Peña de La Capitana" en el Xirgu
7 de noviembre de 2026 a las 20:00
Tu compra de $ 35.000,00
Estarías llevándote 1 entradas
Precio por entrada: $ 35.000,00