PageSourceSearch

https://dimayor.com.co/wp-content/plugins/dimayor-competition-management/assets/js/ui-modals.js?ver=6.10.2

js dimayor.com.co collected 2026-09-24 23:42:44 UTC 24,688 bytes, 565 lines download raw bytes

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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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">&times;</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.