1(function ($) { 2 // ====== CONFIG (v6.0.1) ====== 3 // Si tienes esto por wp_localize_script, se respeta. Si no, se crean defaults. 4 window.DIMAYOR_UI = window.DIMAYOR_UI || {}; 5 DIMAYOR_UI.restBase = DIMAYOR_UI.restBase || '/wp-json/'; 6 DIMAYOR_UI.endpoints = DIMAYOR_UI.endpoints || { 7 eventos: 'dimayor/v1/matches/%d/events', 8 tiempos: 'dimayor/v1/matches/%d/timing', 9 nominas: 'dimayor/v1/matches/%d/lineups', 10 arbitros: 'dimayor/v1/matches/%d/officials', 11 player_details: 'dimayor/v1/player/%s/details', 12 sync_pj: 'dimayor/v1/player/%s/sync-pj' 13 }; 14 DIMAYOR_UI.i18n = DIMAYOR_UI.i18n || { 15 empty: 'Sin datos' 16 }; 17 18 // ====== HELPERS ====== 19 function buildUrl(kind, matchId) { 20 if (!window.DIMAYOR_UI || !DIMAYOR_UI.endpoints[kind]) return null; 21 const tpl = DIMAYOR_UI.endpoints[kind]; 22 const path = tpl.replace('%d', String(matchId)).replace('%s', String(matchId)); 23 return (DIMAYOR_UI.restBase || '/wp-json/') + path; 24 } 25 26 function renderFallback($panel, msg) { 27 const message = msg || (DIMAYOR_UI && DIMAYOR_UI.i18n && DIMAYOR_UI.i18n.empty) || 'Sin datos'; 28 const html = `<div class="dimayor-card" style="padding: 20px; text-align: center;"><div>${message}</div></div>`; 29 if ($panel && $panel.length) { 30 $panel.html(html); 31 } 32 return html; 33 } 34 35 function prettyError(kind) { 36 if (kind === 'arbitros') return 'No fue posible cargar la información de árbitros.'; 37 if (kind === 'eventos') return 'No fue posible cargar los eventos.'; 38 if (kind === 'tiempos') return 'No fue posible cargar los tiempos del partido.'; 39 if (kind === 'nominas') return 'No fue posible cargar la alineación.'; 40 return 'No fue posible cargar la información.'; 41 } 42 43 function renderTiempos(data) { 44 const periods = data && Array.isArray(data.periods) ? data.periods : []; 45 if (!periods.length) return renderFallback(null, 'COMET todavÃa no reporta tiempos de las fases.'); 46 let html = '<div class="dimayor-card"><table class="dimayor-table" style="width:100%"><thead><tr><th>Fase</th><th>Inicio</th><th>Fin</th><th>Tiempo añadido</th></tr></thead><tbody>'; 47 periods.forEach(period => { 48 html += `<tr><td><strong>${escapeHtml(period.label)}</strong></td><td>${escapeHtml(period.start || 'â')}</td><td>${escapeHtml(period.end || 'â')}</td><td>${period.added !== null && period.added !== '' ? `+${escapeHtml(period.added)} min` : 'â'}</td></tr>`; 49 }); 50 html += '</tbody></table></div>'; 51 return html; 52 } 53 54 function escapeHtml(str) { 55 return String(str || '').replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])); 56 } 57 58 // ====== RENDERERS (devuelven HTML) ====== 59 function renderEventos(data, $modal) { 60 if (!Array.isArray(data) || data.length === 0) return renderFallback(null, 'No hay eventos disponibles.'); 61 62 const iconMap = { 63 'GOAL': '<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/><path d="M12 6l-2.5 3.5h5L12 6zM7 11l1 3h3l-1.5-3L7 11zM17 11l-2.5 0-1.5 3h3l1-3zM10.5 15h3l1.5 2.5h-6l1.5-2.5z"/></svg>', 64 'YELLOW': '<svg viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="22" x="4" y="1" rx="2" fill="#FFC107" stroke="#B8860B" stroke-width="0.5"/></svg>', 65 'RED': '<svg viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="22" x="4" y="1" rx="2" fill="#F44336" stroke="#8B0000" stroke-width="0.5"/></svg>', 66 'SUBSTITUTION': '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 4V16M8 4L5 7M8 4L11 7" stroke="#28a745" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M16 20V8M16 20L13 17M16 20L19 17" stroke="#dc3545" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/></svg>', 67 'PENALTY': '<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2.5"/><circle cx="12" cy="12" r="3.5"/></svg>', 68 'DEFAULT': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>' 69 }; 70 const labelEs = { 'GOAL': 'Gol', 'YELLOW': 'Amarilla', 'SECOND_YELLOW': 'Doble Amarilla', 'RED': 'Roja', 'SUBSTITUTION': 'Camb
70io', 'PENALTY': 'Penal' }; 71 const hiddenTech = ['START', 'END', 'FULL_TIME']; 72 73 let html = '<div class="dim-timeline-grid">'; 74 let eventCount = 0; 75 76 data.forEach(event => { 77 const code = (event.eventType?.fcdName || '').toUpperCase(); 78 if (hiddenTech.includes(code) || !event.displayMinute) return; 79 80 eventCount++; 81 const side = event.homeTeam ? 'home' : 'away'; 82 const title = labelEs[code] || code; 83 const minute = escapeHtml(event.displayMinute); 84 const protagonist = event.player?.name || event.teamOfficial?.name || ''; 85 const player2 = event.player2?.name || ''; 86 let description = escapeHtml(protagonist); 87 if (code === 'SUBSTITUTION' && player2) { 88 description = `Sale: ${escapeHtml(player2)}<br>Entra: ${escapeHtml(protagonist)}`; 89 } 90 const eventBodyHtml = ` 91 <div class="event-body event-type-${code.toLowerCase()}"> 92 <div class="event-title">${title}</div> 93 <div class="event-desc">${description}</div> 94 </div>`; 95 96 html += `<div class="timeline-grid-event">`; 97 if (side === 'home') { 98 html += `<div class="event-content event-content--home">${eventBodyHtml}</div>`; 99 } else { 100 html += `<div></div>`; 101 } 102 html += ` 103 <div class="event-timeline-marker"> 104 <div class="event-icon">${iconMap[code] || iconMap['DEFAULT']}</div> 105 <div class="event-minute">${minute}</div> 106 </div>`; 107 if (side === 'away') { 108 html += `<div class="event-content event-content--away">${eventBodyHtml}</div>`; 109 } else { 110 html += `<div></div>`; 111 } 112 html += `</div>`; 113 }); 114 115 html += '</div>'; 116 return eventCount > 0 ? html : renderFallback(null, 'No hay eventos relevantes para mostrar.'); 117 } 118 119 function renderNominas(data) { 120 if (!data || (!data.home && !data.away)) return renderFallback(null, 'No hay datos de alineación.'); 121 122 const competitionId = data.competition?.id || data.match?.competition?.id || ''; 123 124 const getPlayerEventIcons = (player) => { 125 if (!player.events || player.events.length === 0) return ''; 126 let icons = ''; 127 128 const subOffEvent = player.events.find(e => e.eventType.fcdName === 'SUBSTITUTION' && e.player2?.personId === player.personId); 129 if (subOffEvent) { 130 icons += '<span class="lineup-event-icon sub-off" title="Sustituido"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 20V4M12 20L8 16M12 20L16 16" stroke="#dc3545" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg></span>'; 131 } 132 const subOnEvent = player.events.find(e => e.eventType.fcdName === 'SUBSTITUTION' && e.player?.personId === player.personId); 133 if (subOnEvent) { 134 icons += '<span class="lineup-event-icon sub-on" title="Ingresó"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 4V20M12 4L8 8M12 4L16 8" stroke="#28a745" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg></span>'; 135 } 136 player.events.forEach(event => { 137 switch (event.eventType.fcdName) { 138 case 'YELLOW': 139 case 'SECOND_YELLOW': 140 icons += '<span class="lineup-event-icon yellow-card" title="Tarjeta Amarilla"><svg viewBox="0 0 24 24" width="12" height="12" xmlns="http://www.w3.org/2000/svg"><rect width="18" height="24" x="3" y="0" rx="2" fill="#FFC107"/></svg></span>'; 141 break; 142 case 'RED': 143 icons += '<span class="lineup-event-icon red-card" title="Tarjeta Roja"><svg viewBox="0 0 24 24" width="12" height="12" xmlns="http://www.w3.org/2000/svg"><rect width="18" height="24" x="3" y="0" rx="2" fill="#F44336"/></svg></span>'; 144 break; 145 } 146 }); 147 return icons ? `<div class="lineup-player-events">${icons}</div>` : ''; 148 }; 149 150 const renderSide = (sideData, title) => { 151 const teamName = sideData?.team?.name || sideData?.name || title; 152 const players = sideData?.players || []; 153 const officials = sideData?.officials || []; 154 const coach = officials.find(o => o.role === 'Head coach'); 155 156 if (players.length === 0) { 157 return `<div class="lineup-column"><h4>${escapeHtml(teamName)}</h4><p>Sin datos de jugadores.</p></div>`; 158 } 159 160 const starters = players.filter(p => p.starting).sort((a, b) => a.orderNumber - b.orderNumber); 161 const subs = players.filter(p => !p.starting).sort((a, b) => a.orderNumber - b.orderNumber); 162 163 let html = `<div class="lineup-column"><h4>${escapeHtml(teamName)}</h4>`; 164 165 const renderPlayerRow = (p) => { 166 const picId = p.picture || p.pictureId; 167 const avatarUrl = picId ? `${DIMAYOR_UI.restBase}dimayor/v1/image/${picId}` : ''; 168 const avatarHtml = `<img src="${avatarUrl}" class="lineup-player-avatar" onerror="this.outerHTML='<div class=\\'lineup-player-avatar\\'>ð¤</div>'" alt="${escapeHtml(p.name)}">`; 169 170 const positionIcons = { 171 'G': '<span class="lineup-pos pos-gk" title="Portero">PORTERO</span>', 172 'D': '<span class="lineup-pos pos-def" title="Defensa">DEF</span>', 173 'M': '<span class="lineup-pos pos-mid" title="Centrocampista">MED</span>', 174 'F': '<span class="lineup-pos pos-fwd" title="Delantero">DEL</span>' 175 }; 176 const posChar = (p.position || '').substring(0, 1).toUpperCase(); 177 const posHtml = positionIcons[posChar] || ''; 178 179 return `
180 <div class="lineup-row dimayor-player-clickable" data-player-id="${p.personId || p.id || ''}" data-competition-id="${competitionId}"> 181 <div class="lineup-shirt-number">${escapeHtml(p.shirtNumber || '?')}</div> 182 <div class="lineup-player-info" style="display:flex;align-items:center;gap:8px;flex:1;min-width:0;"> 183 ${avatarHtml} 184 ${posHtml} 185 <div class="lineup-player-name">${escapeHtml(p.name)} ${p.captain ? '<strong>(Capitán)</strong>' : ''}</div> 186 </div> 187 ${getPlayerEventIcons(p)} 188 </div>`; 189 }; 190 191 html += '<div class="lineup-section">'; 192 html += '<h5>Titulares</h5>'; 193 html += '<div class="lineup-table">'; 194 starters.forEach(p => { 195 html += renderPlayerRow(p); 196 }); 197 html += '</div></div>'; 198 199 if (subs.length > 0) { 200 html += '<div class="lineup-section">'; 201 html += '<h5>Suplentes</h5>'; 202 html += '<div class="lineup-table">'; 203 subs.forEach(p => { 204 html += renderPlayerRow(p); 205 }); 206 html += '</div></div>'; 207 } 208 209 if (coach) { 210 html += '<div class="lineup-section coach-section">'; 211 html += `<h6>Director Técnico</h6>`; 212 html += `<p>${escapeHtml(coach.name)}</p>`; 213 html += '</div>'; 214 } 215 216 html += '</div>'; 217 return html; 218 }; 219 220 return `<div class="lineups-container"> 221 <style> 222 .lineup-player-avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; background: #eee; border: 1px solid #ccc; flex-shrink: 0; display:flex; align-items:center; justify-content:center; font-size: 14px; color: #888; } 223 .lineup-pos { font-size: 10px; font-weight: 700; color: #fff; padding: 2px 5px; border-radius: 4px; flex-shrink: 0; } 224 .pos-gk { background: #f1c40f; } .pos-def { background: #3498db; } .pos-mid { background: #2ecc71; } .pos-fwd { background: #e74c3c; } 225 </style> 226 ${renderSide(data.home, 'Local')} 227 ${renderSide(data.away, 'Visitante')} 228 </div>`; 229 } 230 231 function renderArbitros(data) { 232 if (!data) return renderFallback(null, 'No hay datos de árbitros.'); 233 234 // Fallback dictionary for known roles 235 const roleMap = { 236 'referee': 'Ãrbitro Central', 237 'assistantrefereeone': 'Asistente 1', 238 'assistantrefereetwo': 'Asistente 2', 239 'fourthofficial': 'Cuarto Ãrbitro', 240 'videoassistantreferee': 'VAR', 241 'assistantvideoassistantreferee': 'AVAR' 242 }; 243 244 let html = '<table class="dimayor-table" style="width: 100%;"><thead><tr><th>Rol</th><th>Apellido - Nombre</th></tr></thead><tbody>'; 245 let found = false; 246 247 // Prioritize all_officials list if available (Live data) 248 if (data.all_officials && Array.isArray(data.all_officials) && data.all_officials.length > 0) { 249 // Sort by order number if available 250 data.all_officials.sort((a, b) => (a.order || 99) - (b.order || 99)); 251 252 data.all_officials.forEach(official => { 253 const roleKey = (official.role || '').toLowerCase(); 254 // Use role_es from API if available, otherwise fallback to roleMap 255 let roleName = official.role_es || roleMap[roleKey] || official.role; 256 257 html += `<tr><td><strong>${escapeHtml(roleName)}</strong></td><td>${escapeHtml(official.name)}</td></tr>`; 258 found = true; 259 }); 260 } 261 // Fallback to legacy structure (Manual data or old structure) 262 else { 263 const legacyMap = { 264 'referee': 'Ãrbitro Central', 265 'assistantRefereeOne': 'Asistente 1', 266 'assistantRefereeTwo': 'Asistente 2', 267 'fourthOfficial': 'Cuarto Ãrbitro', 268 'videoAssistantReferee': 'VAR', 269 'assistantVideoAssistantReferee': 'AVAR' 270 }; 271 272 for (const key in legacyMap) { 273 if (data[key] && data[key].name) { 274 html += `<tr><td><strong>${escapeHtml(legacyMap[key])}</strong></td><td>${escapeHtml(data[key].name)}</td></tr>`; 275 found = true; 276 } 277 } 278 } 279 280 html += '</tbody></table>'; 281 return found ? html : renderFallback(null, 'No hay datos de árbitros.'); 282 } 283
284 // ====== MAPEO DE RENDERERS ====== 285 const RENDERERS = { 286 eventos: typeof renderEventos === 'function' ? renderEventos : null, 287 tiempos: typeof renderTiempos === 'function' ? renderTiempos : null, 288 nominas: typeof renderNominas === 'function' ? renderNominas : null, 289 arbitros: typeof renderArbitros === 'function' ? renderArbitros : null 290 }; 291 292 // ====== FETCH + RENDER (seguro y sin throws) ====== 293 async function fetchAndRender(kind, matchId, $panel, $modal) { 294 const url = buildUrl(kind, matchId); 295 if (!url) { 296 $panel.html(renderFallback(null, prettyError(kind))); 297 return; 298 } 299 300 const renderFn = RENDERERS[kind]; 301 if (typeof renderFn !== 'function') { 302 console.error('[ui-modals] No existe renderer para', kind); 303 $panel.html(renderFallback(null, prettyError(kind))); 304 return; 305 } 306 307 try { 308 const res = await fetch(url, { credentials: 'same-origin' }); 309 if (!res.ok) { 310 console.warn('[ui-modals] HTTP', res.status, 'en', url); 311 $panel.html(renderFallback(null, prettyError(kind))); 312 return; 313 } 314 315 const data = await res.json(); 316 317 // Renderers devuelven HTML (string). Si alguno escribe directo, esto no le afecta. 318 let html = ''; 319 try { 320 html = renderFn.length >= 2 ? renderFn(data, $modal) : renderFn(data); 321 } catch (e) { 322 console.error('[ui-modals] Error en renderer', kind, e); 323 $panel.html(renderFallback(null, prettyError(kind))); 324 return; 325 } 326 327 if (!html || typeof html !== 'string') { 328 $panel.html(renderFallback(null, prettyError(kind))); 329 return; 330 } 331 332 $panel.html(html); 333 334 } catch (err) { 335 console.error('[ui-modals] Error fetchAndRender', kind, err); 336 $panel.html(renderFallback(null, prettyError(kind))); 337 } 338 } 339 340 // ====== HANDLERS ====== 341 $(document).on('click', '.match-action-btn', function () { 342 const matchId = $(this).data('match-api-id'); 343 const homeName = $(this).data('home-name'); 344 const awayName = $(this).data('away-name'); 345 if (!matchId) return; 346 347 const $modalContainer = $('.dimayor-modal-container').first().clone(true, true); 348 $('body').append($modalContainer); 349 $modalContainer.show(); 350 351 const $modal = $modalContainer.find('.dimayor-modal'); 352 $modal.data('match-id', matchId) 353 .data('homeName', homeName) 354 .data('awayName', awayName) 355 .attr('aria-hidden', 'false'); 356 357 const $panel = $modal.find('.dimayor-tabpanel.is-active'); 358 $panel.data('loaded', true); 359 360 // Carga inicial: eventos 361 fetchAndRender('eventos', matchId, $panel, $modal); 362 }); 363 364 $(document).on('click', '.dimayor-modal-backdrop, .modal-close', function () { 365 $(this).closest('.dimayor-modal-container').remove(); 366 }); 367 368 $(document).on('keydown', function (e) { 369 if (e.key === 'Escape') { 370 $('.dimayor-modal-container').last().remove(); 371 } 372 }); 373 374 $(document).on('click', '.dimayor-tab', function () { 375 const $btn = $(this); 376 if ($btn.hasClass('is-active')) return; 377 378 const tab = $btn.data('tab'); // 'eventos' | 'nominas' | 'arbitros' 379 const $modal = $btn.closest('.dimayor-modal'); 380 const $panel = $modal.find(`.dimayor-tabpanel[data-remote="${tab}"]`); 381 const matchId = $modal.data('match-id'); 382 383 $btn.siblings().removeClass('is-active'); 384 $panel.siblings().removeClass('is-active'); 385 386 $btn.addClass('is-active'); 387 $panel.addClass('is-active'); 388 389 if (!$panel.data('loaded')) { 390 $panel.data('loaded', true); 391 fetchAndRender(tab, matchId, $panel, $modal); 392 } 393 }); 394 395 $(document).on('click', '.dimayor-btn-retry', function () { 396 const $btn = $(this); 397 const $panel = $btn.closest('.dimayor-tabpanel'); 398 const kind = $btn.data('kind'); // 'eventos' | 'nominas' | 'arbitros' 399 const matchId = $btn.data('match-id'); 400 const $modal = $btn.closest('.dimayor-modal'); 401 fetchAndRender(kind, matchId, $panel, $modal); 402 }); 403 404 // ====== PLAYER PROFILE MODAL ====== 405 $(document).on('click', '.dimayor-player-clickable', async function (e) { 406 e.preventDefault(); 407 e.stopPropagation(); 408 const playerId = String($(this).data('player-id') || ''); 409 const competitionId = String($(this).data('competition-id') || ''); 410 if (!playerId || playerId === '0' || playerId === 'null') return; 411 412 // Create Player Modal 413 const modalHtml = ` 414 <div class="dimayor-modal-container player-profile-container" style="display:flex;"> 415 <div class="dimayor-modal-backdrop"></div> 416 <div class="dimayor-modal player-profile-modal" aria-hidden="false"> 417 <button class="modal-close" aria-label="Cerrar">×</button> 418 <div class="player-profile-content"> 419 <div class="player-profile-loading"> 420 <div class="dimayor-spinner"></div> 421 <p>Cargando perfil...</p> 422 </div> 423 </div> 424 </div> 425 </div>`; 426 427 const $modalContainer = $(modalHtml); 428 $('body').append($modalContainer); 429 430 try { 431 let url = buildUrl('player_details', playerId);
432 if (competitionId) { 433 url += (url.includes('?') ? '&' : '?') + 'competition_id=' + competitionId; 434 } 435 const res = await fetch(url, { credentials: 'same-origin' }); 436 if (!res.ok) throw new Error('Error de red'); 437 const data = await res.json(); 438 439 renderPlayerProfile(data, $modalContainer.find('.player-profile-content')); 440 } catch (err) { 441 console.error('[ui-modals] Error player profile', err); 442 $modalContainer.find('.player-profile-content').html('<p style="padding:40px;text-align:center;">No se pudo cargar la información del jugador.</p>'); 443 } 444 }); 445 446 function renderPlayerProfile(data, $container) { 447 const photoId = data.photo || ''; 448 const photoUrl = photoId ? `${DIMAYOR_UI.restBase}dimayor/v1/image/${photoId}` : ''; 449 450 // Calcular totales históricos 451 let totalMatches = 0; 452 let totalGoals = 0; 453 let totalYellows = 0; 454 let totalReds = 0; 455 const history = data.history || []; 456 457 history.forEach(h => { 458 totalMatches += h.matchesPlayed || 0; 459 totalGoals += h.goals || 0; 460 totalYellows += h.yellowCards || 0; 461 totalReds += h.redCards || 0; 462 }); 463 464 let bodyHtml = ''; 465 466 // 1. Resumen de Totales Históricos 467 bodyHtml += ` 468 <div class="section-title">Trayectoria histórica en el <strong>FPC</strong></div> 469 <div class="player-stats-grid"> 470 <div class="stat-item"> 471 <span class="stat-value">${totalMatches}</span> 472 <span class="stat-label">Partidos</span> 473 </div> 474 <div class="stat-item highlight"> 475 <span class="stat-value">${totalGoals}</span> 476 <span class="stat-label">Goles</span> 477 </div> 478 <div class="stat-item"> 479 <span class="stat-value">${totalYellows}</span> 480 <span class="stat-label">Amarillas</span> 481 </div> 482 <div class="stat-item"> 483 <span class="stat-value">${totalReds}</span> 484 <span class="stat-label">Rojas</span> 485 </div> 486 </div> 487 `; 488 489 const html = ` 490 <div class="player-card-header"> 491 <div class="player-main-info"> 492 <div class="player-photo-wrapper"> 493 ${photoUrl ? `<img src="${photoUrl}" alt="${escapeHtml(data.name)}" class="player-profile-img" onerror="this.src='data:image/svg+xml;charset=utf-8,%3Csvg xmlns%3D\\'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg\\' width%3D\\'100\\' height%3D\\'100\\' viewBox%3D\\'0 0 100 100\\' %3E%3Crect width%3D\\'100%25\\' height%3D\\'100%25\\' fill%3D\\'%23eee\\' %2F%3E%3Ctext x%3D\\'50%25\\' y%3D\\'55%25\\' fill%3D\\'%23aaa\\' font-family%3D\\'Arial\\' font-size%3D\\'40\\' text-anchor%3D\\'middle\\' %3Eð¤%3C%2Ftext%3E%3C%2Fsvg%3E'">` : '<div class="player-photo-placeholder">ð¤</div>'} 494 </div> 495 <div class="player-name-details"> 496 <h2 class="player-profile-name">${escapeHtml(data.name)}</h2> 497 <div class="player-meta"> 498 <span class="meta-item"><i class="dimayor-icon-pos"></i> ${escapeHtml(data.position)}</span> 499 ${data.age ? `<span class="meta-item"><strong>${data.age}</strong> años</span>` : ''} 500 ${data.nickname ? `<span class="meta-item">"${escapeHtml(data.nickname)}"</span>` : ''} 501 </div> 502 </div> 503 </div> 504 </div> 505 <div class="player-card-body" style="max-height: 400px; overflow-y: auto;"> 506 ${bodyHtml} 507 </div> 508 `; 509 510 $container.html(html); 511 } 512 513 // ====== AUTO-SYNC SCORERS PJ ====== 514 async function syncScorerPJ() { 515 const $cells = $('.scorer-pj-cell.needs-sync'); 516 if (!$cells.length) return; 517 518 console.log(`[dimayor] Sincronizando ${$cells.length} jugadores con PJ 0...`); 519 520 $cells.each(async function() { 521 const $cell = $(this); 522 const pid = $cell.data('player-id'); 523 const cid = $cell.data('comp-id'); 524 const goals = $cell.data('goals'); 525 526 if (!pid || !cid) return; 527 528 try { 529 const endpointTpl = DIMAYOR_UI.endpoints['sync_pj']; 530 if (!endpointTpl) { 531 $cell.html('0').removeClass('needs-sync'); 532 return; 533 } 534 535 let url = buildUrl('sync_pj', pid); 536 url += (url.includes('?') ? '&' : '?') + 'competition_id=' + cid + '&goals=' + goals; 537 538 // Timeout de 15 segundos para no bloquear el UI 539 const controller = new AbortController(); 540 const timeoutId = setTimeout(() => controller.abort(), 15000); 541 542 const res = await fetch(url, { credentials: 'same-origin', signal: controller.signal }); 543 clearTimeout(timeoutId); 544 545 if (!res.ok) throw new Error('Error sync'); 546 547 const data = await res.json(); 548 if (data.success && data.matchesPlayed !== undefined) { 549 $cell.html(data.matchesPlayed).removeClass('needs-sync'); 550 } else { 551 $cell.html('0').removeClass('needs-sync'); 552 } 553 } catch (e) { 554 console.error(`[dimayor] Falló sync para ${pid}`, e); 555 $cell.html('0').removeClass('needs-sync'); 556 } 557 }); 558 } 559 560 // Ejecutar al cargar la página 561 $(document).ready(function() { 562 syncScorerPJ(); 563 }); 564 565})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.