1<!DOCTYPE html> 2<html> 3<head> 4<meta charset="utf-8"> 5<meta name="rosgan-release" content="rosganapp-build-2026-10-09-019"> 6<link rel="stylesheet" href="/assets/app-update.css">
7<script src="/assets/app-update.js" defer></script>
7 8<meta name="viewport" content="width=device-width, initial-scale=1"> 9<meta name="apple-mobile-web-app-capable" content="yes"> 10<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> 11<title>Rosgan</title> 12<base href="/">
13<script src="/assets/rosgan-brand-bug.js" defer></script>
vendor: 1 bytes, line 13
13
14<script>if (/^\/remates\/[^/]+\/tv\/?$/.test(location.pathname)) document.documentElement.setAttribute('data-rosgan-tv-output','true');</script>
14 15<link rel="icon" type="image/png" href="/favicon.png"> 16<link rel="apple-touch-icon" href="/favicon.png"> 17<meta property="og:site_name" content="Rosgan" data-rosgan-social> 18<meta property="og:type" content="website" data-rosgan-social> 19<meta property="og:title" content="Rosgan" data-rosgan-social> 20<meta property="og:description" content="Catálogo Rosgan" data-rosgan-social> 21<meta property="og:image" content="/favicon.png" data-rosgan-social> 22<meta property="og:image:secure_url" content="/favicon.png" data-rosgan-social> 23<meta name="twitter:card" content="summary_large_image" data-rosgan-social> 24<meta name="twitter:title" content="Rosgan" data-rosgan-social> 25<meta name="twitter:description" content="Catálogo Rosgan" data-rosgan-social> 26<meta name="twitter:image" content="/favicon.png" data-rosgan-social>
27<script> 28(function () { 29 var enabled = false; 30 if (!enabled) return; 31 32 document.documentElement.setAttribute('data-safari-debug', 'true'); 33 var entries = []; 34 var startedAt = Date.now(); 35 var panel = null; 36 var output = null; 37 var collapsed = false; 38 39 function safePath(value) { 40 var text = String(value || ''); 41 try { 42 var anchor = document.createElement('a'); 43 anchor.href = text; 44 return (anchor.pathname || '/') + (anchor.hash ? '#â¦' : ''); 45 } catch (error) { 46 return text.split('?')[0].split('#')[0]; 47 } 48 } 49 50 function redact(value) { 51 var text = String(value == null ? '' : value); 52 text = text.replace(/Bearer\s+[^\s]+/gi, 'Bearer [oculto]'); 53 text = text.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[email oculto]'); 54 text = text.replace(/https?:\/\/[^\s)]+/gi, function (url) { return safePath(url); }); 55 text = text.replace(/eyJ[A-Za-z0-9._-]{20,}/g, '[token oculto]'); 56 return text.slice(0, 320); 57 } 58 59 function add(kind, message) { 60 entries.push({ 61 at: ((Date.now() - startedAt) / 1000).toFixed(1) + 's', 62 kind: kind, 63 message: redact(message) 64 }); 65 if (entries.length > 24) entries.shift(); 66 render(); 67 } 68 69 function isVisible(element) { 70 if (!element || !window.getComputedStyle) return false; 71 var style = window.getComputedStyle(element); 72 var rect = element.getBoundingClientRect(); 73 return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || 1) > 0.01 && rect.width > 0 && rect.height > 0; 74 } 75 76 function resourceSummary() { 77 var wanted = ['react.production.min.js', 'react-dom.production.min.js', 'support.js', 'firebase-app-compat.js', 'firebase-auth-compat.js']; 78 var resources = []; 79 try { 80 resources = window.performance && window.performance.getEntriesByType ? window.performance.getEntriesByType('resource') : []; 81 } catch (error) {} 82 return wanted.map(function (name) { 83 var tag = Array.prototype.slice.call(document.scripts || []).filter(function (script) { 84 return String(script.src || '').indexOf(name) !== -1; 85 })[0]; 86 var timing = resources.filter(function (item) { 87 return String(item.name || '').indexOf(name) !== -1; 88 })[0]; 89 var duration = timing ? Math.round(Number(timing.duration || 0)) + 'ms' : 'sin timing'; 90 return ' ' + name.replace('.production.min.js', '').replace('.js', '') + ': ' + (tag ? 'tag OK' : 'SIN TAG') + ' · ' + duration; 91 }).join('\n'); 92 } 93 94 function diagnosticText() { 95 var root = document.documentElement; 96 var fallback = document.getElementById('rosgan-static-boot'); 97 var authCards = document.querySelectorAll ? document.querySelectorAll('.rosgan-auth-card') : []; 98 var visibleAuthCard = false; 99 var authCardDetails = 'no encontrada'; 100 for (var i = 0; i < authCards.length; i += 1) { 101 var authStyle = window.getComputedStyle ? window.getComputedStyle(authCards[i]) : null; 102 var authRect = authCards[i].getBoundingClientRect(); 103 if (authRect.width > 0 && authRect.height > 0 && authStyle) { 104 authCardDetails = 'opacity ' + authStyle.opacity + ' · z ' + authStyle.zIndex + ' · visibility ' + authStyle.visibility + ' · transform ' + authStyle.transform; 105 } 106 if (isVisible(authCards[i])) visibleAuthCard = true; 107 } 108 var fallbackDisplay = 'no creado'; 109 if (fallback && window.getComputedStyle) fallbackDisplay = window.getComputedStyle(fallback).display; 110 var cssBackdrop = false; 111 try { 112 cssBackdrop = !!(window.CSS && CSS.supports && (CSS.supports('backdrop-filter', 'blur(2px)') || CSS.supports('-webkit-backdrop-filter', 'blur(2px)'))); 113 } catch (error) {} 114 115 var lines = [ 116 'DIAGNÃSTICO SAFARI · ' + new Date().toLocaleTimeString(), 117 'Ruta: ' + (window.location.pathname || '/'), 118 'Documento: ' + document.readyState, 119 'Viewport: ' + window.innerWidth + 'x' + window.innerHeight + ' · DPR ' + (window.devicePixelRatio || 1), 120 'Montaje: ' + (root.getAttribute('data-rosgan-mounted') || 'pendiente'), 121 'Ruta app: ' + (root.getAttribute('data-public-route') || 'sin definir'), 122 'React global: ' + (window.React ? 'sÃ' : 'no'), 123 'ReactDOM global: ' + (window.ReactDOM ? 'sÃ' : 'no'), 124 'Firebase global: ' + (window.firebase ? 'sÃ' : 'no'), 125 'Tarjeta de acceso visible: ' + (visibleAuthCard ? 'sÃ' : 'no'),
126 'Detalle tarjeta: ' + authCardDetails, 127 'Respaldo estático: ' + fallbackDisplay, 128 'Backdrop filter: ' + (cssBackdrop ? 'sÃ' : 'no'), 129 '', 130 'RECURSOS', 131 resourceSummary(), 132 '', 133 'EVENTOS' 134 ]; 135 if (!entries.length) lines.push(' Sin errores capturados.'); 136 entries.forEach(function (entry) { 137 lines.push(' [' + entry.at + '] ' + entry.kind + ': ' + entry.message); 138 }); 139 lines.push('', 'USER AGENT', redact(navigator.userAgent || 'no disponible')); 140 return lines.join('\n'); 141 } 142 143 function render() { 144 if (output) output.textContent = diagnosticText(); 145 } 146 147 function makeButton(label, action) { 148 var button = document.createElement('button'); 149 button.type = 'button'; 150 button.textContent = label; 151 button.style.cssText = 'min-height:34px;padding:0 11px;border:1px solid rgba(255,255,255,.3);border-radius:8px;background:rgba(255,255,255,.12);color:#fff;font:700 12px -apple-system,BlinkMacSystemFont,Arial,sans-serif;'; 152 button.addEventListener('click', action); 153 return button; 154 } 155 156 function mountPanel() { 157 if (panel || !document.body) return; 158 var style = document.createElement('style'); 159 style.textContent = '#rosgan-safari-debug{position:fixed;left:0;right:0;bottom:0;z-index:2147483647;max-height:58vh;box-sizing:border-box;display:flex;flex-direction:column;background:rgba(3,15,31,.97);color:#e8f4ff;border-top:2px solid #3dd5c6;box-shadow:0 -16px 42px rgba(0,0,0,.42);font-family:-apple-system,BlinkMacSystemFont,Arial,sans-serif;-webkit-text-size-adjust:100%}#rosgan-safari-debug.is-collapsed{max-height:none}#rosgan-safari-debug.is-collapsed pre{display:none}#rosgan-safari-debug header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:9px 10px;background:#073e6f}#rosgan-safari-debug header strong{font-size:13px;letter-spacing:.02em}#rosgan-safari-debug header div{display:flex;gap:6px}#rosgan-safari-debug pre{margin:0;padding:12px 12px max(16px,env(safe-area-inset-bottom));overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;color:#dbeafe;-webkit-overflow-scrolling:touch}'; 160 document.head.appendChild(style); 161 162 panel = document.createElement('aside'); 163 panel.id = 'rosgan-safari-debug'; 164 panel.setAttribute('aria-label', 'Diagnóstico técnico de Safari'); 165 var header = document.createElement('header'); 166 var title = document.createElement('strong'); 167 title.textContent = 'Diagnóstico Safari'; 168 var actions = document.createElement('div'); 169 actions.appendChild(makeButton('Actualizar', render)); 170 actions.appendChild(makeButton('Copiar', function () { 171 var text = diagnosticText(); 172 if (navigator.clipboard && navigator.clipboard.writeText) { 173 navigator.clipboard.writeText(text).then(function () { add('INFO', 'Diagnóstico copiado.'); }).catch(function () { add('AVISO', 'Safari no permitió copiar. Usá una captura.'); }); 174 } else { 175 add('AVISO', 'Portapapeles no disponible. Usá una captura.'); 176 } 177 })); 178 actions.appendChild(makeButton('Contraer', function (event) { 179 collapsed = !collapsed; 180 panel.className = collapsed ? 'is-collapsed' : ''; 181 event.currentTarget.textContent = collapsed ? 'Abrir' : 'Contraer'; 182 })); 183 header.appendChild(title); 184 header.appendChild(actions); 185 output = document.createElement('pre'); 186 panel.appendChild(header); 187 panel.appendChild(output); 188 document.body.appendChild(panel); 189 add('INFO', 'Panel de diagnóstico iniciado.'); 190 } 191 192 window.addEventListener('error', function (event) { 193 var target = event && event.target; 194 if (target && target !== window && (target.src || target.href)) { 195 add('RECURSO', (target.tagName || 'elemento') + ' no cargó: ' + safePath(target.src || target.href)); 196 return; 197 } 198 add('ERROR', (event && event.message ? event.message : 'Error JavaScript') + (event && event.filename ? ' · ' + safePath(event.filename) + ':' + (event.lineno || 0) : '')); 199 }, true); 200 201 window.addEventListener('unhandledrejection', function (event) { 202 var reason = event && event.reason; 203 add('PROMESA', reason && reason.message ? reason.message : reason || 'Rechazo sin detalle'); 204 }); 205 window.addEventListener('rosgan:app-mounted', function () { add('INFO', 'Interfaz principal montada.'); }); 206 document.addEventListener('DOMContentLoaded', function () { mountPanel(); add('INFO', 'DOM listo.'); }); 207 window.addEventListener('load', function () { add('INFO', 'Ventana cargada.'); }); 208 window.setInterval(render, 1500); 209 window.setTimeout(function () { add('CONTROL', 'Revisión a los 12 segundos.'); }, 12000); 210 window.__ROSGAN_SAFARI_DEBUG__ = { add: add, render: render, text: diagnosticText }; 211})(); 212</script>
vendor: 1 bytes, line 212
212
213<script> 214(() => { 215 const path = window.location.pathname.replace(/\/+$/, '') || '/'; 216 const hashPath = (window.location.hash || '').replace(/^#/, '').split('?')[0].replace(/\/+$/, ''); 217 const publicRoutes = new Set(['/lacassina', '/lacassina/estadisticas', '/lacassina/comercial', '/lacassina/precios', '/lacassina/tv', '/lacassina/tv/config']); 218 const publicPathCandidate = publicRoutes.has(hashPath) ? hashPath : (publicRoutes.has(path) ? path : ''); 219 const isPublicCabanas = /^\/cabanas(?:\/(?!crear(?:\/|$))[^/]+(?:\/(?:lotes\/[^/]+|estadisticas|precios|tv))?)?$/i.test(path); 220 const isCabanaTv = /^\/cabanas\/[^/]+\/tv$/i.test(path); 221 const isCabanaTvConfig = /^\/cabanas\/[^/]+\/tv\/config$/i.test(path); 222 const lacassinaLotMatch = publicPathCandidate ? null : path.match(/^\/lacassina\/([^/]+)\/([^/]+)$/i); 223 const publicPath = publicPathCandidate || (lacassinaLotMatch ? '/lacassina' : path); 224 const isLacassinaStats = publicPath === '/lacassina/estadisticas'; 225 const isLacassinaCommercial = publicPath === '/lacassina/comercial'; 226 const isLacassinaPrices = publicPath === '/lacassina/precios'; 227 const isLacassinaTv = publicPath === '/lacassina/tv'; 228 const isLacassinaTvConfig = publicPath === '/lacassina/tv/config'; 229 const isLacassinaLot = !!lacassinaLotMatch; 230 const isLacassinaPublic = publicPath === '/lacassina' || isLacassinaStats || isLacassinaCommercial || isLacassinaPrices || isLacassinaTv || isLacassinaTvConfig || isLacassinaLot; 231 const isLocalHost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; 232 const isLocalMailingEntry = isLocalHost && (path === '/mailing-admin' || hashPath === '/mailing-admin'); 233 window.__ROSGAN_PUBLIC_PATH__ = publicPath; 234 window.__ROSGAN_LOCAL_MAILING_ENTRY__ = isLocalMailingEntry; 235 window.__ROSGAN_PUBLIC_ADVANCE__ = isLacassinaPublic ? 'lacassina' : ''; 236 window.__ROSGAN_PUBLIC_CABANAS__ = isPublicCabanas; 237 window.__ROSGAN_PUBLIC_ROUTE__ = isLacassinaStats ? 'lacassina-stats' : (isLacassinaCommercial ? 'lacassina-commercial' : (isLacassinaPrices ? 'lacassina-prices' : ((isLacassinaTv || isCabanaTv) ? 'cabana-tv' : ((isLacassinaTvConfig || isCabanaTvConfig) ? 'cabana-tv-config' : (isLacassinaPublic ? 'lacassina' : (isPublicCabanas ? 'cabanas-public' : 'app')))))); 238 window.__ROSGAN_PUBLIC_LOT_NUMBER__ = lacassinaLotMatch ? decodeURIComponent(lacassinaLotMatch[1]) : ''; 239 window.__ROSGAN_PUBLIC_RP__ = lacassinaLotMatch ? decodeURIComponent(lacassinaLotMatch[2]) : ''; 240 document.documentElement.dataset.publicAdvance = window.__ROSGAN_PUBLIC_ADVANCE__; 241 document.documentElement.dataset.publicRoute = window.__ROSGAN_PUBLIC_ROUTE__; 242 if (!isLacassinaPublic) { 243 let hasPrivilegedSession = false; 244 try { 245 const cached = JSON.parse(sessionStorage.getItem('rosgan:authenticatedApiUser') || 'null'); 246 const apiUser = cached?.apiUser || {}; 247 const roles = [apiUser.roleId, apiUser.roles, apiUser.role] 248 .flat(Infinity) 249 .map((role) => typeof role === 'object' ?(role?.id || role?.roleId || role?.name || role?.value || '') : role) 250 .map((role) => String(role || '').trim().toLowerCase()); 251 hasPrivilegedSession = cached?.expiresAt > Date.now() 252 && roles.some((role) => ['admin', 'sysadmin', 'dev', 'administrador', 'administrador del sistema'].includes(role)); 253 } catch {} 254 document.documentElement.dataset.rosganSessionHint = hasPrivilegedSession ?'authenticated' : 'guest'; 255 } 256})(); 257</script>
vendor: 1 bytes, line 257
257
258<script> 259(function () { 260 if (typeof window.globalThis === 'undefined') window.globalThis = window; 261 if (!Object.hasOwn) { 262 Object.hasOwn = function (object, property) { 263 return Object.prototype.hasOwnProperty.call(Object(object), property); 264 }; 265 } 266 if (!window.queueMicrotask && window.Promise) { 267 window.queueMicrotask = function (callback) { 268 Promise.resolve().then(callback).catch(function (error) { 269 window.setTimeout(function () { throw error; }, 0); 270 }); 271 }; 272 } 273 if (!window.requestIdleCallback) { 274 window.requestIdleCallback = function (callback) { 275 var startedAt = Date.now(); 276 return window.setTimeout(function () { 277 callback({ 278 didTimeout: false, 279 timeRemaining: function () { return Math.max(0, 50 - (Date.now() - startedAt)); } 280 }); 281 }, 1); 282 }; 283 } 284 if (!window.cancelIdleCallback) window.cancelIdleCallback = window.clearTimeout; 285})(); 286</script>
286 287<style id="rosgan-static-boot-style"> 288#rosgan-static-boot{display:none} 289html:not([data-public-route]) #rosgan-static-boot, 290html[data-public-route="app"] #rosgan-static-boot{ 291 position:fixed; 292 top:0; 293 right:0; 294 bottom:0; 295 left:0; 296 inset:0; 297 z-index:2147483646; 298 display:none; 299 place-items:center; 300 box-sizing:border-box; 301 min-height:100vh; 302 min-height:100svh; 303 padding:22px 18px; 304 padding:max(22px,env(safe-area-inset-top)) max(18px,env(safe-area-inset-right)) max(22px,env(safe-area-inset-bottom)) max(18px,env(safe-area-inset-left)); 305 overflow:hidden; 306 background:#06264a url('/assets/auth/rosgan-angus-field.png') center/cover no-repeat; 307 font-family:Arial,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; 308} 309html:not([data-public-route]) #rosgan-static-boot.is-visible, 310html[data-public-route="app"] #rosgan-static-boot.is-visible{display:grid} 311html[data-rosgan-mounted="true"] #rosgan-static-boot{display:none!important} 312html[data-rosgan-session-hint="authenticated"] #rosgan-static-boot{ 313 place-items:start center; 314 background:#f4f7fb; 315} 316html[data-rosgan-session-hint="authenticated"] #rosgan-static-boot:before{display:none} 317#rosgan-static-boot:before{ 318 content:""; 319 position:absolute; 320 top:0; 321 right:0; 322 bottom:0; 323 left:0; 324 inset:0; 325 background:linear-gradient(145deg,rgba(0,30,66,.88),rgba(0,79,130,.72) 48%,rgba(0,26,58,.9)); 326} 327.rosgan-static-boot-card{ 328 position:relative; 329 z-index:2; 330 width:360px; 331 max-width:100%; 332 width:min(360px,100%); 333 box-sizing:border-box; 334 display:grid; 335 justify-items:center; 336 gap:17px; 337 padding:30px 24px; 338 border:1px solid rgba(255,255,255,.24); 339 border-radius:24px; 340 background:rgba(0,28,61,.52); 341 -webkit-backdrop-filter:blur(18px); 342 backdrop-filter:blur(18px); 343 box-shadow:0 28px 72px rgba(0,10,30,.36); 344 color:#fff; 345 text-align:center; 346} 347.rosgan-static-boot-logo{ 348 display:grid; 349 place-items:center; 350 width:112px; 351 height:112px; 352 border-radius:50%; 353 background:#fff; 354 box-shadow:0 15px 38px rgba(0,15,42,.28); 355} 356.rosgan-static-boot-logo img{display:block;width:82%;height:62%;object-fit:contain} 357.rosgan-static-boot-spinner{ 358 width:30px; 359 height:30px; 360 box-sizing:border-box; 361 border:3px solid rgba(255,255,255,.28); 362 border-top-color:#58d4dc; 363 border-radius:50%; 364 animation:rosganStaticSpin .78s linear infinite; 365} 366#rosgan-static-boot.has-error .rosgan-static-boot-spinner{display:none} 367.rosgan-static-boot-card strong{font-size:20px;line-height:1.15} 368.rosgan-static-boot-card p{margin:-7px 0 0;color:#d7e9f6;font-size:13px;line-height:1.5} 369.rosgan-static-boot-retry{ 370 min-height:46px; 371 padding:0 22px; 372 border:0; 373 border-radius:999px; 374 background:#fff; 375 color:#064b86; 376 font:800 14px Arial,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; 377 cursor:pointer; 378 -webkit-appearance:none; 379 appearance:none; 380} 381.rosgan-static-guest{display:grid} 382.rosgan-static-session{display:none} 383html[data-rosgan-session-hint="authenticated"] .rosgan-static-guest{display:none} 384html[data-rosgan-session-hint="authenticated"] .rosgan-static-session{display:grid} 385.rosgan-static-login-fields{display:grid;width:100%;gap:12px} 386.rosgan-static-login-field, 387.rosgan-static-login-action{ 388 min-height:48px; 389 box-sizing:border-box; 390 display:flex; 391 align-items:center; 392 padding:0 15px; 393 border:1px solid rgba(255,255,255,.24); 394 border-radius:12px; 395 color:#c8d8ea; 396 background:rgba(255,255,255,.1); 397 font-size:14px; 398 text-align:left; 399} 400.rosgan-static-login-action{justify-content:center;border-color:#fff;background:#fff;color:#063766;font-weight:800} 401.rosgan-static-login-google{justify-content:center;color:#fff;font-weight:700} 402.rosgan-static-session{ 403 position:relative; 404 z-index:2; 405 width:min(1120px,100%); 406 gap:18px; 407 color:#09284a; 408} 409.rosgan-static-session-bar, 410.rosgan-static-session-hero, 411.rosgan-static-session-card{ 412 box-sizing:border-box; 413 border:1px solid #dce7f2; 414 border-radius:20px; 415 background:#fff; 416 box-shadow:0 18px 44px rgba(27,63,101,.08); 417} 418.rosgan-static-session-bar{min-height:66px;display:flex;align-items:center;justify-content:space-between;padding:12px 20px} 419.rosgan-static-session-bar img{width:88px;height:38px;object-fit:contain} 420.rosgan-static-session-nav{display:flex;gap:9px} 421.rosgan-static-session-nav span{width:66px;height:28px;border-radius:999px;background:#edf3f8} 422.rosgan-static-session-hero{min-height:178px;padding:28px;background:linear-gradient(135deg,#052b54,#1266a9);border:0} 423.rosgan-static-session-hero:before{content:"";display:block;width:180px;height:18px;border-radius:999px;background:rgba(255,255,255,.92);box-shadow:0 34px 0 rgba(255,255,255,.28),0 68px 0 rgba(255,255,255,.18)} 424.rosgan-static-session-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px} 425.rosgan-static-session-card{min-height:145px;padding:20px} 426.rosgan-static-session-card:before,.rosgan-static-session-card:after{content:"";display:block;height:13px;border-radius:999px;background:#dbe7f1} 427.rosgan-static-session-card:before{width:44%;margin-bottom:34px}.rosgan-static-session-card:after{width:72%;height:28px;background:#edf3f8} 428@keyframes rosganStaticSpin{to{transform:rotate(360deg)}} 429@media(max-width:600px){ 430 html:not([data-public-route]) #rosgan-static-boot, 431 html[data-public-route="app"] #rosgan-static-boot{background-image:url('/assets/auth/rosgan-angus-field-vertical.png')} 432 .rosgan-static-boot-card{padding:28px 20px;border-radius:22px}
433 .rosgan-static-session-nav span:nth-child(n+4){display:none} 434 .rosgan-static-session-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px} 435 .rosgan-static-session-card{min-height:120px;padding:16px} 436 .rosgan-static-session-hero{min-height:150px;padding:22px} 437} 438@media(prefers-reduced-motion:reduce){.rosgan-static-boot-spinner{animation-duration:1.5s}} 439.roslot-op-row{grid-template-columns:minmax(0,1fr) auto auto;min-height:58px;padding:8px 10px} 440.roslot-op-topline{min-height:22px}.roslot-op-meta-line{display:flex;align-items:center;gap:10px;min-width:0;margin-top:3px}.roslot-op-meta-line p{margin:0;min-width:0}.roslot-op-meta-line .roslot-op-status{flex:none;margin:0} 441.roslot-op-alert{flex:none;width:auto;margin:0 0 0 auto;padding:4px 7px;border-radius:7px;box-shadow:0 4px 10px rgba(191,28,44,.2)}.roslot-op-alert i{font-size:12px}.roslot-op-alert strong{font-size:9px}.roslot-op-alert span{font-size:8px} 442.roslot-op-price{align-self:stretch;display:grid;grid-template-columns:auto;align-content:center;justify-items:end;gap:2px;min-width:128px;width:auto;margin:0;padding:6px 11px;border-radius:10px}.roslot-op-price span{font-size:8px}.roslot-op-price strong{font-size:27px;line-height:.95}.roslot-op-price em{font-size:8px} 443.roslot-op-row.is-winning .roslot-op-price,.roslot-op-row.is-seller .roslot-op-price{border-color:#bcded3;background:#f0faf6}.roslot-op-row.is-winning .roslot-op-price strong,.roslot-op-row.is-seller .roslot-op-price strong{color:#14745f} 444.roslot-op-actions button.is-hammer{display:inline-flex;align-items:center;gap:6px;border-color:#b86d00;background:linear-gradient(135deg,#c77a08,#a95b00);color:#fff;box-shadow:0 6px 14px rgba(169,91,0,.2)} 445.roslot-op-actions button.is-hammer:hover{border-color:#945000;background:linear-gradient(135deg,#b86d00,#944d00);color:#fff} 446.roslot-op-actions button.is-hammer:disabled{cursor:wait;color:#fff} 447@media(max-width:760px){.roslot-op-row{grid-template-columns:minmax(0,1fr) minmax(118px,auto);gap:7px;min-height:64px}.roslot-op-topline{flex-wrap:wrap}.roslot-op-alert{margin-left:0}.roslot-op-alert span{display:none}.roslot-op-price{min-width:108px}.roslot-op-price strong{font-size:24px}.roslot-op-actions{grid-column:1/-1}.roslot-op-meta-line{align-items:flex-start;flex-direction:column;gap:3px}} 448@supports (-webkit-touch-callout:none){ 449 .rosgan-static-boot-card{ 450 background:rgba(0,28,61,.9); 451 -webkit-backdrop-filter:none; 452 backdrop-filter:none; 453 } 454} 455.rosgan-tv-workspace{display:grid;grid-template-columns:minmax(290px,.8fr) minmax(0,1.2fr);gap:16px;margin-top:18px;align-items:start}.ros-tv-search{width:100%;box-sizing:border-box;padding:12px;border:1px solid #cad8e6;border-radius:10px;margin-bottom:12px}.ros-tv-lot-list{max-height:72vh;overflow:auto;display:grid;gap:6px;padding-right:5px}.ros-tv-lot-row{content-visibility:auto;contain-intrinsic-size:auto 76px;display:flex;align-items:center;gap:12px;text-align:left;border:1px solid #e0e8f0;background:#fff;padding:12px;border-radius:12px;cursor:pointer;color:#102f4b}.ros-tv-lot-row:hover{background:#f0f6fc}.ros-tv-lot-row.is-selected{border-color:#0a5ca8;background:#eaf4ff;box-shadow:inset 3px 0 #0a5ca8}.ros-tv-order{font-weight:900;font-size:21px;min-width:38px}.ros-tv-lot-copy{display:grid;gap:4px;flex:1;min-width:0}.ros-tv-lot-copy small{font-size:11px;color:#607890}.ros-tv-lot-copy strong{font-size:14px}.ros-tv-lot-copy>span{font-size:12px;color:#567086}.ros-tv-cons-mark{width:42px;height:42px;flex-shrink:0;display:grid;place-items:center;border-radius:9px;padding:6px;box-sizing:border-box}.ros-tv-cons-mark img{width:100%;height:100%;object-fit:contain}.ros-tv-editor{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.ros-tv-edit-field{display:grid;gap:7px;align-content:start;font-size:12px;font-weight:700;color:#526b82}.ros-tv-edit-field input,.ros-tv-edit-field textarea,.ros-tv-edit-field select{width:100%;box-sizing:border-box;min-height:40px;padding:10px;border:1px solid #cad8e6;border-radius:9px;font:inherit;color:#12334e;background:#fff}.ros-tv-breeds{display:flex;flex-wrap:wrap;gap:5px;max-height:160px;overflow:auto}.ros-tv-breeds button{border:1px solid #cad8e6;background:#fff;border-radius:7px;padding:6px;color:#34546d;cursor:pointer}.ros-tv-breeds .is-selected{background:#0a5ca8;color:#fff;border-color:#0a5ca8}.ros-tv-save-status{background:#edf5fb;color:#275576;padding:12px;border-radius:9px;font-size:12px}.ros-tv-retry{margin-top:16px;border:1px solid #cad8e6;padding:10px;border-radius:9px;background:#fff;color:#244d70;cursor:pointer}@media(max-width:800px){.rosgan-tv-workspace{grid-template-columns:1fr}.ros-tv-lot-list{max-height:360px}}@media(max-width:440px){.ros-tv-editor{grid-template-columns:1fr}} 456.ros-tv-catalog-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:15px}.rosgan-tv-config-card .ros-tv-catalog-heading h2{margin:0}.ros-tv-catalog-arrows{display:flex;gap:6px;flex-shrink:0}.ros-tv-catalog-arrows button{display:grid;place-items:center;width:36px;height:36px;padding:0;border:1px solid #cad8e6;border-radius:9px;background:#fff;color:#0a5ca8;font-size:18px;
456cursor:pointer}.ros-tv-catalog-arrows button:hover{background:#eaf4ff;border-color:#0a5ca8}.ros-tv-catalog-arrows button:focus-visible{outline:2px solid #0a5ca8;outline-offset:2px} 457 458.rd-company-dialog{width:min(590px,calc(100% - 32px));padding:0;border:0;border-radius:22px;box-shadow:0 28px 90px #001d3d40;font-family:inherit;max-height:calc(100dvh - 32px)} 459.rd-company-dialog::backdrop{background:#001d3d88;backdrop-filter:blur(4px)} 460.rd-company-dialog-header{display:flex;gap:14px;align-items:center;padding:24px;background:color-mix(in srgb,var(--company-color) 7%,white);border-bottom:1px solid #e5edf3} 461.rd-company-dialog-brand{display:grid;place-items:center;background:var(--company-color);width:56px;height:56px;flex-shrink:0;border-radius:15px;color:white;font-size:24px;font-weight:800}.rd-company-dialog-brand img{width:44px;height:44px;object-fit:contain} 462.rd-company-dialog-identity{display:grid;gap:6px;flex:1}.rd-company-dialog-eyebrow{font-size:10px;letter-spacing:.09em;color:#62748a;font-weight:700}.rd-company-dialog-identity strong{font-size:19px} 463.rd-company-dialog-dismiss{align-self:flex-start;border:1px solid #dbe5ed;background:white;border-radius:50%;width:32px;height:32px;font-size:23px;line-height:1;color:#52687b;cursor:pointer} 464.rd-company-dialog-body{padding:26px}.rd-company-dialog-body h2{font-size:22px;letter-spacing:-.5px;margin:0 0 8px}.rd-company-dialog-intro{margin:0 0 22px} 465.rd-company-dialog-actions{display:grid;grid-template-columns:1fr 1fr;gap:12px}.rd-company-dialog-actions .rd-company-dialog-option{display:flex;flex-direction:column;align-items:flex-start;text-align:left;gap:16px;background:white;color:#002e5f;border:1px solid #dce5ee;border-radius:14px;padding:18px;font:inherit;transition:background .15s,border-color .15s,transform .15s}.rd-company-dialog-option:hover{border-color:var(--company-color);background:color-mix(in srgb,var(--company-color) 5%,white);transform:translateY(-2px)}.rd-company-dialog-option:focus-visible,.rd-company-dialog-dismiss:focus-visible{outline:3px solid var(--company-color);outline-offset:3px} 466.rd-company-dialog-symbol{display:grid;place-items:center;width:40px;height:40px;border-radius:11px;background:color-mix(in srgb,var(--company-color) 10%,white)}.rd-company-dialog-symbol img{width:22px;height:22px}.rd-company-dialog-option-copy{display:flex;flex-direction:column;gap:8px;height:100%}.rd-company-dialog-option-copy strong{font-size:16px}.rd-company-dialog-option-copy>span{font-size:13px;line-height:1.5;color:#62748a}.rd-company-dialog-option-copy .rd-company-dialog-download{padding-top:12px;margin-top:auto;color:#002e5f;font-weight:750}.rd-company-dialog-format{margin-top:22px;padding-top:18px;border-top:1px solid #edf1f5;text-align:center;color:#62748a;font-size:12px} 467@media(max-width:480px){.rd-company-dialog-actions{grid-template-columns:1fr}.rd-company-dialog-actions .rd-company-dialog-option{flex-direction:row}.rd-company-dialog-symbol{flex-shrink:0}.rd-company-dialog-body{padding:20px}.rd-company-dialog-header{padding:20px}} 468</style> 469<style> 470.ros-tv-editor-heading{position:sticky;top:64px;z-index:8;background:#fff;padding:10px 0;border-bottom:1px solid #e5edf5;display:flex;align-items:center;justify-content:space-between;gap:12px}.ros-tv-editor-heading h2{margin:0}.ros-tv-save{display:grid;place-items:center;flex-shrink:0;width:38px;height:38px;border:1px solid #0a5ca8;border-radius:9px;background:#0a5ca8;color:#fff;cursor:pointer}.ros-tv-save:hover{background:#084a87}.ros-tv-save:focus-visible{outline:3px solid #93c5fd;outline-offset:3px}.ros-tv-save[aria-disabled="true"]{opacity:.4;cursor:default} 471</style> 472<style id="rosgan-clean-boot-style"> 473 .ros-tv-lot-main{display:flex;align-items:center;gap:12px;flex:1;min-width:0;border:0;background:transparent;color:inherit;text-align:left;padding:0;cursor:pointer}.ros-tv-slot-buttons{display:flex;gap:3px}.ros-tv-slot-buttons button{width:27px;height:30px;border:1px solid #cad8e6;border-radius:6px;background:#fff;color:#0a5ca8;font-weight:800;cursor:pointer}.ros-tv-slot-buttons button.is-active{background:#0a5ca8;color:#fff}.ros-tv-slot-buttons button:disabled{opacity:.3;cursor:default}.ros-tv-multi-controls{display:flex;flex-wrap:wrap;align-items:center;gap:7px;margin:10px 0;font-size:12px;color:#526b82}.ros-tv-multi-controls button{border:1px solid #cad8e6;background:#edf5fb;color:#0a5ca8;padding:6px 9px;border-radius:7px;cursor:pointer} 474html[data-public-route="app"] x-dc{visibility:hidden} 475html[data-public-route="app"][data-rosgan-mounted="true"] x-dc{visibility:visible} 476html[data-public-route="app"] #rosgan-static-boot{ 477 position:fixed; 478 inset:0; 479 z-index:2147483646; 480 display:grid!important; 481 place-items:center; 482 min-height:100vh; 483 min-height:100svh; 484 padding:0; 485 background:#f5f8fc; 486 opacity:1; 487 visibility:visible; 488 transition:opacity .14s ease,visibility 0s linear .14s; 489} 490html[data-public-route="app"][data-rosgan-mounted="true"] #rosgan-static-boot{ 491 display:grid!important; 492 opacity:0; 493 visibility:hidden; 494 pointer-events:none; 495} 496#rosgan-static-boot .rosgan-static-boot-card{ 497 width:auto; 498 padding:0; 499 border:0; 500 background:transparent; 501 box-shadow:none; 502 -webkit-backdrop-filter:none; 503 backdrop-filter:none; 504} 505html[data-rosgan-session-hint="authenticated"] #rosgan-static-boot .rosgan-static-guest{display:grid} 506#rosgan-static-boot .rosgan-static-boot-spinner, 507.rosgan-fast-loader-spinner{ 508 display:block; 509 width:28px; 510 height:28px; 511 box-sizing:border-box; 512 border:3px solid #d9e5f0; 513 border-top-color:#075da8; 514 border-radius:50%; 515 animation:rosganStaticSpin .62s linear infinite; 516} 517#rosgan-static-boot [data-rosgan-boot-status], 518#rosgan-static-boot [data-rosgan-boot-detail], 519#rosgan-static-boot [data-rosgan-boot-retry]{display:none} 520#rosgan-static-boot.has-error .rosgan-static-boot-card{gap:14px;padding:26px;text-align:center;color:#082d54} 521#rosgan-static-boot.has-error [data-rosgan-boot-status], 522#rosgan-static-boot.has-error [data-rosgan-boot-detail], 523#rosgan-static-boot.has-error [data-rosgan-boot-retry]{display:block} 524.rosgan-fast-loader{ 525 position:fixed; 526 inset:0; 527 z-index:2147483000; 528 display:grid; 529 place-items:center; 530 min-height:100vh; 531 min-height:100svh; 532 background:#f5f8fc; 533} 534@media(prefers-reduced-motion:reduce){ 535 html[data-public-route="app"] #rosgan-static-boot{transition:none} 536 #rosgan-static-boot .rosgan-static-boot-spinner, 537 .rosgan-fast-loader-spinner{animation-duration:1.1s} 538} 539</style>
540<script defer src="/vendor/react.production.min.js?v=18.2.0"></script>
vendor: 1 bytes, line 540
540
541<script defer src="/vendor/react-dom.production.min.js?v=18.2.0"></script>
vendor: 1 bytes, line 541
541
542<script defer src="/vendor/firebase-app-compat.js?v=10.12.5"></script>
vendor: 1 bytes, line 542
542
543<script defer src="/vendor/firebase-auth-compat.js?v=10.12.5"></script>
vendor: 1 bytes, line 543
543
544<script defer src="/support.js?v=20260827-stable-inputs-8"></script>
544 545<link rel="preload" href="/assets/auth/options/success-1.json" as="fetch" type="application/json" crossorigin> 546<link rel="modulepreload" href="/vendor/dotlottie-wc.js?v=0.9.27">
547<script type="module" src="/vendor/dotlottie-bootstrap.js?v=0.9.27"></script>
547 548<style id="lacassina-public-stats-style"> 549html[data-public-route="lacassina-stats"] body{ 550 margin:0; 551 overflow-x:hidden; 552 background:#071f3d !important; 553} 554html[data-public-route="lacassina-stats"] x-dc, 555html[data-public-route="lacassina-stats"] #dc-root{ 556 display:none !important; 557} 558#lacassina-stats-standalone{ 559 display:none; 560 min-height:100vh; 561 color:#102033; 562 font-family:'Archivo',system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; 563 background: 564 radial-gradient(circle at 16% 8%, rgba(26,112,185,.42), transparent 34rem), 565 radial-gradient(circle at 82% 12%, rgba(44,186,181,.20), transparent 30rem), 566 linear-gradient(180deg,#062340 0%,#0b4f88 36%,#f3f7fb 36%,#f3f7fb 100%); 567} 568html[data-public-route="lacassina-stats"] #lacassina-stats-standalone{ 569 display:block; 570} 571.lc-stats-shell{ 572 width:min(1180px, calc(100% - 32px)); 573 margin:0 auto; 574 padding:28px 0 52px; 575} 576.lc-stats-hero{ 577 position:relative; 578 overflow:hidden; 579 border-radius:28px; 580 padding:28px; 581 min-height:280px; 582 color:#fff; 583 background:linear-gradient(145deg,#061e3b 0%,#084673 52%,#062c52 100%); 584 box-shadow:0 28px 70px rgba(0,25,60,.28); 585} 586.lc-stats-hero:before{ 587 content:""; 588 position:absolute; 589 inset:-28%; 590 pointer-events:none; 591 background: 592 linear-gradient(120deg,transparent 10%,rgba(26,169,210,.34) 28%,rgba(255,255,255,.11) 42%,rgba(26,184,173,.30) 56%,transparent 76%), 593 linear-gradient(40deg,rgba(5,30,72,.18),rgba(31,116,184,.28),rgba(5,30,72,.16)); 594 background-size:180% 180%; 595 filter:blur(18px); 596 opacity:.86; 597 animation:lcStatsGlowShift 10s ease-in-out infinite alternate; 598} 599.lc-stats-hero:after{ 600 content:""; 601 position:absolute; 602 inset:0; 603 pointer-events:none; 604 background: 605 linear-gradient(180deg,rgba(255,255,255,.10),transparent 34%), 606 linear-gradient(0deg,rgba(0,18,42,.30),transparent 48%); 607} 608@keyframes lcStatsGlowShift{ 609 0%{transform:translate3d(-7%,-3%,0) scale(1);background-position:0% 44%} 610 100%{transform:translate3d(7%,4%,0) scale(1.04);background-position:100% 56%} 611} 612.lc-stats-hero-inner{ 613 position:relative; 614 z-index:1; 615 display:grid; 616 grid-template-columns:1fr minmax(260px,360px); 617 gap:28px; 618 align-items:center; 619} 620.lc-stats-back{ 621 position:relative; 622 z-index:1; 623 display:inline-flex; 624 align-items:center; 625 gap:8px; 626 width:max-content; 627 margin-bottom:22px; 628 padding:10px 14px; 629 border-radius:999px; 630 color:#dff1ff; 631 background:rgba(255,255,255,.10); 632 border:1px solid rgba(255,255,255,.16); 633 font-weight:800; 634 font:800 16px inherit; 635 cursor:pointer; 636 text-decoration:none; 637} 638.lc-stats-kicker{ 639 margin:0 0 8px; 640 color:#a9d9ff; 641 font-size:12px; 642 font-weight:900; 643 letter-spacing:.14em; 644 text-transform:uppercase; 645} 646.lc-stats-hero h1{ 647 margin:0; 648 max-width:780px; 649 font-size:clamp(34px,5vw,68px); 650 line-height:.94; 651 letter-spacing:0; 652} 653.lc-stats-subtitle{ 654 margin:18px 0 0; 655 max-width:680px; 656 color:#dbeefe; 657 font-size:17px; 658 line-height:1.45; 659 font-weight:600; 660} 661.lc-stats-logo-card{ 662 display:flex; 663 flex-direction:column; 664 align-items:center; 665 justify-content:center; 666 gap:18px; 667 min-height:178px; 668 padding:22px; 669 border-radius:22px; 670 background:rgba(255,255,255,.94); 671 border:1px solid rgba(255,255,255,.28); 672 box-shadow:0 20px 48px rgba(0,20,55,.22); 673} 674.lc-stats-main-logo{ 675 max-width:100%; 676 max-height:118px; 677 object-fit:contain; 678} 679.lc-stats-logo-label{ 680 color:#64748b; 681 font-size:11px; 682 font-weight:900; 683 letter-spacing:.10em; 684 text-transform:uppercase; 685} 686.lc-stats-guest-logos{ 687 display:flex; 688 flex-wrap:wrap; 689 justify-content:center; 690 gap:10px; 691 width:100%; 692 padding-top:14px; 693 border-top:1px solid #e3edf7; 694} 695.lc-stats-guest-logo{ 696 display:grid; 697 place-items:center; 698 width:86px; 699 height:44px; 700 padding:7px; 701 border-radius:12px; 702 background:#fff; 703 border:1px solid #e4edf6; 704} 705.lc-stats-guest-logo img{ 706 max-width:100%; 707 max-height:30px; 708 object-fit:contain; 709} 710.lc-stats-guest-logo span{ 711 overflow:hidden; 712 max-width:100%; 713 color:#5d7089; 714 font-size:10px; 715 font-weight:900; 716 text-align:center; 717 text-overflow:ellipsis; 718 white-space:nowrap; 719} 720.lc-stats-main{ 721 margin-top:24px; 722 display:grid; 723 gap:18px; 724} 725.lc-stats-card{ 726 border-radius:20px; 727 background:rgba(255,255,255,.96); 728 border:1px solid #dbe5f0; 729 box-shadow:0 18px 44px rgba(16,43,78,.08); 730} 731.lc-stats-section{ 732 padding:22px; 733} 734.lc-stats-section-head{ 735 display:flex; 736 justify-content:space-between; 737 align-items:flex-start; 738 gap:16px; 739 margin-bottom:18px; 740} 741.lc-stats-section h2{ 742 margin:0; 743 color:#102033; 744 font-size:22px; 745 line-height:1.08; 746} 747.lc-stats-section small{ 748 color:#71839c; 749 font-weight:800; 750} 751.lc-stats-status{ 752 display:inline-flex; 753 align-items:center; 754 gap:8px; 755 padding:9px 12px; 756 border-radius:999px; 757 background:#e9f7f2;
758 color:#0c785b; 759 font-size:12px; 760 font-weight:900; 761 white-space:nowrap; 762} 763.lc-stats-status:before{ 764 content:""; 765 width:8px; 766 height:8px; 767 border-radius:50%; 768 background:currentColor; 769} 770.lc-stats-grid{ 771 display:grid; 772 grid-template-columns:repeat(4,minmax(0,1fr)); 773 gap:14px; 774} 775.lc-stats-kpi{ 776 min-height:122px; 777 padding:18px; 778 border-radius:18px; 779 background:linear-gradient(180deg,#fff,#eef5fc); 780 border:1px solid #dce7f2; 781} 782.lc-stats-kpi span{ 783 display:block; 784 margin-bottom:12px; 785 color:#71839c; 786 font-size:12px; 787 font-weight:900; 788 letter-spacing:.08em; 789 text-transform:uppercase; 790} 791.lc-stats-kpi strong{ 792 display:block; 793 color:#0b315d; 794 font-size:34px; 795 line-height:1; 796} 797.lc-stats-kpi em{ 798 display:block; 799 margin-top:10px; 800 color:#60758f; 801 font-size:13px; 802 font-style:normal; 803 font-weight:700; 804} 805.lc-stats-two-col{ 806 display:grid; 807 grid-template-columns:minmax(0,1.15fr) minmax(320px,.85fr); 808 gap:18px; 809} 810.lc-stats-bars{ 811 display:grid; 812 gap:12px; 813} 814.lc-stat-row{ 815 display:grid; 816 grid-template-columns:minmax(130px,220px) 1fr auto; 817 gap:12px; 818 align-items:center; 819} 820.lc-stat-row b{ 821 color:#17314f; 822 font-size:14px; 823} 824.lc-stat-track{ 825 overflow:hidden; 826 height:12px; 827 border-radius:999px; 828 background:#edf3fa; 829} 830.lc-stat-fill{ 831 height:100%; 832 min-width:3px; 833 border-radius:999px; 834 background:linear-gradient(90deg,#0069b4,#20b8b0); 835} 836.lc-stat-row strong{ 837 color:#0b315d; 838 font-size:14px; 839} 840.lc-stats-panel-list{ 841 display:grid; 842 gap:10px; 843} 844.lc-stats-mini{ 845 display:flex; 846 align-items:center; 847 justify-content:space-between; 848 gap:12px; 849 padding:13px 14px; 850 border-radius:14px; 851 background:#f3f7fb; 852 border:1px solid #dfebf5; 853} 854.lc-stats-mini b{ 855 color:#132a46; 856} 857.lc-stats-mini span{ 858 color:#5d7089; 859 font-size:13px; 860 font-weight:800; 861} 862.lc-stats-insight-grid{ 863 display:grid; 864 grid-template-columns:repeat(3,minmax(0,1fr)); 865 gap:14px; 866} 867.lc-stats-insight{ 868 min-height:108px; 869 padding:18px 20px; 870 border-radius:18px; 871 background:#fff; 872 color:#102033; 873 border:1px solid #dbe5f0; 874 box-shadow:0 18px 38px rgba(16,43,78,.08); 875} 876.lc-stats-insight span{ 877 display:block; 878 margin-bottom:10px; 879 color:#71839c; 880 font-size:11px; 881 font-weight:900; 882 letter-spacing:.10em; 883 text-transform:uppercase; 884} 885.lc-stats-insight strong{ 886 display:block; 887 color:#0b315d; 888 font-size:22px; 889 line-height:1.08; 890} 891.lc-stats-insight em{ 892 display:block; 893 margin-top:10px; 894 color:#65758a; 895 font-size:13px; 896 line-height:1.35; 897 font-style:normal; 898 font-weight:700; 899} 900.lc-stats-funnel{ 901 display:grid; 902 grid-template-columns:repeat(4,minmax(0,1fr)); 903 gap:12px; 904} 905.lc-stats-count-card{padding:16px;border-radius:16px;background:#f7fbff;border:1px solid #dce8f5} 906.lc-stats-count-card span{display:block;color:#71839c;font-size:11px;font-weight:900;letter-spacing:.09em;text-transform:uppercase} 907.lc-stats-count-card strong{display:block;margin-top:8px;color:#0b315d;font-size:30px;line-height:1} 908.lc-stats-count-card small{display:block;margin-top:8px;color:#65758a;font-size:12px;font-weight:800} 909.lc-stats-funnel-step{ 910 display:grid; 911 grid-template-columns:150px 1fr 76px; 912 gap:12px; 913 align-items:center; 914} 915.lc-stats-funnel-step span{ 916 color:#637994; 917 font-size:12px; 918 font-weight:900; 919 text-transform:uppercase; 920} 921.lc-stats-funnel-step strong{ 922 color:#0b315d; 923 text-align:right; 924} 925.lc-stats-funnel-track{ 926 height:28px; 927 overflow:hidden; 928 border-radius:999px; 929 background:#edf3fa; 930} 931.lc-stats-funnel-fill{ 932 height:100%; 933 min-width:7px; 934 border-radius:999px; 935 background:linear-gradient(90deg,#0a5ca8,#18b8ad); 936} 937.lc-stats-alert-list{ 938 display:grid; 939 gap:10px; 940} 941.lc-stats-alert{ 942 display:flex; 943 gap:12px; 944 align-items:flex-start; 945 padding:14px; 946 border-radius:16px; 947 background:#fff8ed; 948 border:1px solid #fde7bf; 949 color:#6f4312; 950 font-size:13px; 951 font-weight:800; 952} 953.lc-stats-alert.is-good{ 954 background:#edfdf7; 955 border-color:#c7f3e4; 956 color:#0f765d; 957} 958.lc-stats-alert i{ 959 display:grid; 960 place-items:center; 961 width:24px; 962 height:24px; 963 flex:0 0 24px; 964 border-radius:50%; 965 background:currentColor; 966 color:#fff; 967 font-style:normal; 968 font-size:12px; 969} 970.lc-stats-media-grid{ 971 display:grid; 972 grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); 973 gap:12px; 974} 975.lc-stats-media-card{ 976 display:grid; 977 grid-template-columns:116px minmax(0,1fr) auto; 978 gap:12px; 979 align-items:center; 980 padding:10px; 981 border-radius:16px; 982 background:#f7fbff; 983 border:1px solid #dce8f5; 984 color:inherit; 985 text-decoration:none; 986 cursor:pointer; 987 transition:border-color .18s ease, box-shadow .18s ease, transform .18s ease; 988} 989.lc-stats-media-card:hover{ 990 border-color:#9fc8e9; 991 box-shadow:0 12px 26px rgba(15,42,70,.1); 992 transform:translateY(-1px); 993} 994.lc-stats-media-thumb{ 995 position:relative; 996 overflow:hidden; 997 width:116px; 998 aspect-ratio:16/9; 999 border-radius:12px; 1000 background:#0b315d; 1001} 1002.lc-stats-media-thumb img, 1003.lc-stats-media-thumb video{ 1004 width:100%; 1005 height:100%; 1006 object-fit:cover; 1007 display:block; 1008} 1009.lc-stats-media-play{ 1010 position:absolute; 1011 inset:auto 8px 8px auto; 1012 display:grid; 1013 place-items:center; 1014 width:28px; 1015 height:28px; 1016 border-radius:50%; 1017 background:rgba(255,255,255,.92); 1018 color:#0b315d; 1019 font-size:12px; 1020 font-weight:900; 1021} 1022.lc-stats-media-copy{ 1023 min-width:0; 1024} 1025.lc-stats-media-copy span{ 1026 display:block; 1027 color:#71839c; 1028 font-size:11px; 1029 font-weight:900; 1030 letter-spacing:.09em; 1031 text-transform:uppercase; 1032} 1033.lc-stats-media-copy strong{ 1034 display:block; 1035 overflow:hidden; 1036 margin-top:4px; 1037 color:#102033; 1038 font-size:14px; 1039 line-height:1.2; 1040 text-overflow:ellipsis; 1041 white-space:nowrap; 1042} 1043.lc-stats-media-copy small{ 1044 display:block; 1045 overflow:hidden; 1046 margin-top:4px; 1047 color:#65758a; 1048 font-size:12px; 1049 font-weight:800; 1050 text-overflow:ellipsis; 1051 white-space:nowrap; 1052} 1053.lc-stats-media-card > strong{ 1054 color:#0b67b2; 1055 font-size:22px; 1056} 1057.lc-stats-range-tabs{ 1058 display:flex; 1059 flex-wrap:wrap; 1060 gap:8px; 1061} 1062.lc-stats-range-tabs button{ 1063 min-height:34px; 1064 padding:8px 12px; 1065 border:1px solid #cbd8e7; 1066 border-radius:999px; 1067 background:#fff;
1068 color:#31465f; 1069 font:900 12px inherit; 1070 cursor:pointer; 1071} 1072.lc-stats-range-tabs button.is-active{ 1073 border-color:#0b67b2; 1074 background:#0b67b2; 1075 color:#fff; 1076} 1077.lc-stats-timeline{ 1078 display:grid; 1079 gap:14px; 1080} 1081.lc-stats-chart{ 1082 position:relative; 1083 overflow:hidden; 1084 min-height:220px; 1085 padding:0; 1086 border:1px solid #dce8f5; 1087 border-radius:18px; 1088 background:linear-gradient(180deg,#f7fbff,#fff); 1089} 1090.lc-stats-line-chart{display:block;width:100%;height:auto;min-height:220px} 1091.lc-stats-line-grid{stroke:#dce8f5;stroke-width:1} 1092.lc-stats-line-area{fill:url(#lcStatsLineArea)} 1093.lc-stats-line-path{fill:none;stroke:#0b67b2;stroke-width:4;stroke-linecap:round;stroke-linejoin:round} 1094.lc-stats-line-dot{fill:#fff;stroke:#0b67b2;stroke-width:3} 1095.lc-stats-line-label{fill:#60758f;font-size:11px;font-weight:900;text-anchor:middle} 1096.lc-stats-line-value{fill:#0b315d;font-size:11px;font-weight:900;text-anchor:middle} 1097.lc-stats-chart-bar{ 1098 display:grid; 1099 grid-template-rows:1fr auto; 1100 gap:8px; 1101 min-width:0; 1102 height:100%; 1103} 1104.lc-stats-chart-bar i{ 1105 align-self:end; 1106 display:block; 1107 min-height:4px; 1108 border-radius:999px 999px 4px 4px; 1109 background:linear-gradient(180deg,#18b8ad,#0a5ca8); 1110} 1111.lc-stats-chart-bar span{ 1112 overflow:hidden; 1113 color:#60758f; 1114 font-size:10px; 1115 font-weight:900; 1116 text-align:center; 1117 text-overflow:ellipsis; 1118 white-space:nowrap; 1119} 1120.lc-stats-chart-summary{ 1121 display:grid; 1122 grid-template-columns:repeat(3,minmax(0,1fr)); 1123 gap:10px; 1124} 1125.lc-stats-chart-summary div{ 1126 padding:12px 14px; 1127 border-radius:14px; 1128 background:#f3f7fb; 1129 border:1px solid #dfebf5; 1130} 1131.lc-stats-chart-summary span{display:block;color:#71839c;font-size:10px;font-weight:900;letter-spacing:.08em;text-transform:uppercase} 1132.lc-stats-chart-summary strong{display:block;margin-top:4px;color:#0b315d;font-size:20px} 1133.lc-stats-attribute-grid{ 1134 display:grid; 1135 grid-template-columns:repeat(5,minmax(0,1fr)); 1136 gap:12px; 1137} 1138.lc-stats-attribute-card{ 1139 padding:15px; 1140 border-radius:16px; 1141 background:#f7fbff; 1142 border:1px solid #dce8f5; 1143} 1144.lc-stats-attribute-card span{display:block;color:#71839c;font-size:11px;font-weight:900;letter-spacing:.08em;text-transform:uppercase} 1145.lc-stats-attribute-card strong{display:block;margin-top:8px;color:#0b315d;font-size:22px;line-height:1.1} 1146.lc-stats-attribute-card small{display:block;margin-top:7px;color:#65758a;font-size:12px;font-weight:800} 1147.lc-stats-table-wrap{ 1148 overflow:hidden; 1149 border-radius:16px; 1150 border:1px solid #dce7f2; 1151} 1152.lc-stats-table{ 1153 width:100%; 1154 border-collapse:collapse; 1155} 1156.lc-stats-table th, 1157.lc-stats-table td{ 1158 padding:14px 16px; 1159 border-bottom:1px solid #e5edf5; 1160 text-align:left; 1161} 1162.lc-stats-table th{ 1163 background:#f1f6fb; 1164 color:#697d96; 1165 font-size:12px; 1166 font-weight:900; 1167 letter-spacing:.07em; 1168 text-transform:uppercase; 1169} 1170.lc-stats-table td{ 1171 color:#142840; 1172 font-size:14px; 1173 font-weight:700; 1174} 1175.lc-stats-table tr:last-child td{ 1176 border-bottom:0; 1177} 1178.lc-stats-signal-grid{ 1179 display:grid; 1180 grid-template-columns:repeat(3,minmax(0,1fr)); 1181 gap:12px; 1182} 1183.lc-stats-signal{ 1184 display:flex; 1185 align-items:flex-start; 1186 gap:10px; 1187 padding:14px; 1188 border-radius:16px; 1189 border:1px solid #dce8f5; 1190 background:#f7fbff; 1191} 1192.lc-stats-signal i{ 1193 display:inline-flex; 1194 align-items:center; 1195 justify-content:center; 1196 width:28px; 1197 height:28px; 1198 flex:0 0 28px; 1199 border-radius:999px; 1200 background:#dbeafe; 1201 color:#0b5ca8; 1202 font-style:normal; 1203 font-size:11px; 1204 font-weight:900; 1205} 1206.lc-stats-signal b{ 1207 display:block; 1208 margin-bottom:3px; 1209 color:#102033; 1210 font-size:13px; 1211} 1212.lc-stats-signal span{ 1213 display:block; 1214 color:#62708a; 1215 font-size:12px; 1216 line-height:1.35; 1217} 1218.lc-stats-signal.is-live{ 1219 border-color:#b9ebd3; 1220 background:#ecfbf5; 1221} 1222.lc-stats-signal.is-live i{ 1223 background:#c9f5df; 1224 color:#0f7a4e; 1225} 1226.lc-stats-signal.is-pending{ 1227 border-color:#ffe0ad; 1228 background:#fff8ed; 1229} 1230.lc-stats-signal.is-pending i{ 1231 background:#ffe8bd; 1232 color:#9a5a00; 1233} 1234.lc-stats-market-list{ 1235 display:grid; 1236 gap:10px; 1237} 1238.lc-stats-market-item{ 1239 display:flex; 1240 align-items:center; 1241 justify-content:space-between; 1242 gap:12px; 1243 padding:12px 14px; 1244 border-radius:14px; 1245 border:1px solid #dce8f5; 1246 background:#f7fbff; 1247} 1248.lc-stats-market-item b{ 1249 color:#102033; 1250 font-size:13px; 1251} 1252.lc-stats-market-item span{ 1253 color:#62708a; 1254 font-size:12px; 1255 text-align:right; 1256} 1257.lc-stats-score{ 1258 display:inline-flex; 1259 align-items:center; 1260 justify-content:center; 1261 min-width:46px; 1262 padding:7px 10px; 1263 border-radius:999px; 1264 background:#e9f5ff; 1265 color:#005ea8; 1266 font-weight:900; 1267} 1268.lc-stats-action-pill{ 1269 display:inline-flex; 1270 align-items:center; 1271 justify-content:center; 1272 padding:7px 10px; 1273 border-radius:999px; 1274 background:#edf3f8; 1275 color:#52637a; 1276 font-size:11px; 1277 font-weight:800; 1278 white-space:nowrap; 1279} 1280.lc-stats-loader{ 1281 display:grid; 1282 gap:14px; 1283} 1284.lc-stats-skeleton{ 1285 height:104px; 1286 border-radius:18px; 1287 background:linear-gradient(90deg,#e9f0f8,#f8fbff,#e9f0f8); 1288 background-size:220% 100%; 1289 animation:lcStatsPulse 1.2s ease-in-out infinite; 1290} 1291@keyframes lcStatsPulse{ 1292 0%{background-position:0 0} 1293 100%{background-position:220% 0} 1294} 1295.lc-stats-error{ 1296 padding:22px; 1297 border-radius:18px; 1298 background:#fff1f2; 1299 color:#9f1239; 1300 border:1px solid #fecdd3; 1301 font-weight:800; 1302}
1303@media (max-width:900px){ 1304 .lc-stats-shell{width:min(100% - 20px, 680px); padding:14px 0 30px} 1305 .lc-stats-shell,.lc-stats-main,.lc-stats-main>*,.lc-stats-card,.lc-stats-section,.lc-stats-insight-grid,.lc-stats-two-col{min-width:0;max-width:100%;box-sizing:border-box} 1306 .lc-stats-main{grid-template-columns:minmax(0,1fr);overflow:hidden} 1307 .lc-stats-hero{padding:20px; border-radius:22px; min-height:auto} 1308 .lc-stats-hero-inner{grid-template-columns:1fr} 1309 .lc-stats-logo-card{min-height:126px} 1310 .lc-stats-main-logo{max-height:96px} 1311 .lc-stats-guest-logo{width:78px;height:40px} 1312 .lc-stats-grid{grid-template-columns:repeat(2,minmax(0,1fr))} 1313 .lc-stats-two-col{grid-template-columns:1fr} 1314 .lc-stats-insight-grid{grid-template-columns:1fr} 1315 .lc-stats-signal-grid{grid-template-columns:1fr} 1316 .lc-stats-funnel{grid-template-columns:repeat(2,minmax(0,1fr))} 1317 .lc-stats-chart-summary{grid-template-columns:repeat(2,minmax(0,1fr))} 1318 .lc-stats-attribute-grid{grid-template-columns:repeat(2,minmax(0,1fr))} 1319 .lc-stat-row{grid-template-columns:1fr auto; gap:8px} 1320 .lc-stat-track{grid-column:1 / -1; order:3} 1321 .lc-stats-funnel-step{grid-template-columns:1fr 64px} 1322 .lc-stats-funnel-track{grid-column:1 / -1; order:3} 1323} 1324@media (max-width:560px){ 1325 .lc-stats-shell{width:100%; padding:0 0 24px} 1326 .lc-stats-hero{border-radius:0; padding:18px 16px 24px} 1327 .lc-stats-back{margin-bottom:18px} 1328 .lc-stats-subtitle{font-size:15px} 1329 .lc-stats-main{padding:0 10px} 1330 .lc-stats-grid{grid-template-columns:1fr} 1331 .lc-stats-media-card{grid-template-columns:94px minmax(0,1fr) auto} 1332 .lc-stats-media-thumb{width:94px} 1333 .lc-stats-funnel,.lc-stats-chart-summary,.lc-stats-attribute-grid{grid-template-columns:1fr} 1334 .lc-stats-section{padding:16px} 1335 .lc-stats-section-head{display:grid} 1336 .lc-stats-kpi{min-height:auto} 1337 .lc-stats-table th, 1338 .lc-stats-table td{padding:12px 10px; font-size:12px} 1339} 1340 1341/* La Cassina public statistics panel */ 1342.lc-stats-empty{padding:16px;border:1px dashed rgba(13,72,124,.22);border-radius:18px;background:rgba(255,255,255,.72);display:grid;gap:4px;color:#65758a} 1343.lc-stats-empty strong{color:#10233f;font-size:14px} 1344.lc-stats-empty span{font-size:12px} 1345.lc-stat-card{border:1px solid rgba(13,72,124,.12);background:#fff;border-radius:20px;padding:18px;box-shadow:0 18px 38px rgba(20,50,92,.08);display:grid;gap:7px} 1346.lc-stat-card span,.lc-stats-mini-row span{font-size:11px;text-transform:uppercase;letter-spacing:.12em;color:#8090a6;font-weight:800} 1347.lc-stat-card strong{font-size:28px;color:#082b4f} 1348.lc-stat-card small,.lc-stats-mini-row small{font-size:12px;color:#63758d} 1349.lc-stats-bar-row{display:grid;grid-template-columns:minmax(120px,1.1fr) 2fr auto;gap:12px;align-items:center;padding:9px 0;border-bottom:1px solid rgba(13,72,124,.08)} 1350.lc-stats-bar-row:last-child{border-bottom:0} 1351.lc-stats-bar-row span{font-size:13px;font-weight:800;color:#173755} 1352.lc-stats-bar-row div{height:10px;border-radius:999px;background:#e8f0f8;overflow:hidden} 1353.lc-stats-bar-row i{display:block;height:100%;border-radius:999px;background:linear-gradient(90deg,#0b67b2,#20b6c8)} 1354.lc-stats-bar-row strong{font-size:13px;color:#0b3155} 1355.lc-stats-alert-list{display:grid;gap:10px} 1356.lc-stats-alert{display:flex;gap:10px;align-items:flex-start;border:1px solid rgba(239,68,68,.18);background:#fff7f7;border-radius:16px;padding:12px} 1357.lc-stats-alert.is-good{border-color:rgba(24,128,97,.18);background:#f2fbf7} 1358.lc-stats-alert i{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;background:#ef4444;color:#fff;font-style:normal;font-weight:900;font-size:12px;flex:0 0 auto} 1359.lc-stats-alert.is-good i{background:#188061} 1360.lc-stats-alert strong{display:block;color:#10233f;font-size:13px} 1361.lc-stats-alert span{display:block;color:#65758a;font-size:12px;margin-top:2px} 1362.lc-stats-mini-row{display:grid;grid-template-columns:1fr auto;gap:4px 12px;padding:12px 0;border-bottom:1px solid rgba(13,72,124,.08)} 1363.lc-stats-mini-row:last-child{border-bottom:0} 1364.lc-stats-mini-row strong{font-size:20px;color:#0b67b2;text-align:right} 1365.lc-stats-mini-row small{grid-column:1 / -1}
1366.lc-stats-loader span{display:block;height:12px;border-radius:999px;background:linear-gradient(90deg,#eef4fb,#fff,#eef4fb);background-size:200% 100%;animation:lcStatsPulse 1.15s linear infinite;margin:10px 0} 1367.lc-stats-loader span:nth-child(2){width:74%} 1368.lc-stats-loader span:nth-child(3){width:52%} 1369@keyframes lcStatsPulse{to{background-position:-200% 0}} 1370@media (max-width:760px){.lc-stats-bar-row{grid-template-columns:1fr;gap:6px}.lc-stat-card strong{font-size:24px}} 1371@media (max-width:560px){ 1372 html[data-public-route="lacassina-stats"], 1373 html[data-public-route="lacassina-stats"] body{overflow-x:hidden} 1374 html[data-public-route="lacassina-stats"] #lacassina-stats-standalone{overflow-x:hidden} 1375 .lc-stats-shell,.lc-stats-main,.lc-stats-card,.lc-stats-section{max-width:100%;box-sizing:border-box} 1376 .lc-stats-hero{overflow:hidden} 1377 .lc-stats-hero h1{font-size:clamp(30px,10vw,40px);line-height:1;overflow-wrap:anywhere} 1378 .lc-stats-kicker{font-size:10px;letter-spacing:.10em} 1379 .lc-stats-subtitle{font-size:14px;line-height:1.38} 1380 .lc-stats-logo-card{gap:12px;padding:16px;min-height:112px;border-radius:18px} 1381 .lc-stats-main-logo{max-height:76px} 1382 .lc-stats-guest-logos{gap:7px;padding-top:10px} 1383 .lc-stats-guest-logo{width:70px;height:36px;border-radius:10px} 1384 .lc-stats-section h2{font-size:20px} 1385 .lc-stats-section-head{gap:10px;margin-bottom:14px} 1386 .lc-stats-status{width:100%;display:grid;grid-template-columns:8px minmax(0,1fr);justify-content:stretch;align-items:center;white-space:normal;text-align:left;border-radius:16px;box-sizing:border-box;line-height:1.2} 1387 .lc-stats-status:before{grid-row:1 / span 3} 1388 .lc-stats-status span,.lc-stats-status strong,.lc-stats-status small{display:block;min-width:0} 1389 .lc-stats-status small{font-size:10px;opacity:.76} 1390 .lc-stats-grid{gap:10px} 1391 .lc-stats-kpi{padding:15px;border-radius:16px} 1392 .lc-stats-kpi span{margin-bottom:9px;font-size:10px} 1393 .lc-stats-kpi strong{font-size:30px} 1394 .lc-stats-kpi em{font-size:12px} 1395 .lc-stats-range-tabs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));width:100%} 1396 .lc-stats-range-tabs button{padding:8px 6px;font-size:11px} 1397 .lc-stats-chart{min-height:176px} 1398 .lc-stats-line-chart{min-height:176px} 1399 .lc-stats-line-path{stroke-width:3.4} 1400 .lc-stats-line-dot{stroke-width:2.6} 1401 .lc-stats-line-label,.lc-stats-line-value{font-size:10px} 1402 .lc-stats-chart-summary{gap:8px} 1403 .lc-stats-count-card,.lc-stats-attribute-card{padding:14px} 1404 .lc-stats-count-card strong{font-size:28px} 1405 .lc-stats-media-grid{grid-template-columns:1fr} 1406 .lc-stats-media-card{grid-template-columns:84px minmax(0,1fr) 30px;gap:10px;padding:9px;border-radius:14px} 1407 .lc-stats-media-thumb{width:84px;border-radius:10px} 1408 .lc-stats-media-copy span{font-size:10px} 1409 .lc-stats-media-copy strong{font-size:13px} 1410 .lc-stats-media-copy small{font-size:11px} 1411 .lc-stats-media-card > strong{font-size:20px;text-align:right} 1412 .lc-stats-bar-row span,.lc-stat-row b{overflow-wrap:anywhere;line-height:1.2} 1413 .lc-stats-table-wrap{overflow:visible;border:0;border-radius:0} 1414 .lc-stats-table,.lc-stats-table tbody,.lc-stats-table tr,.lc-stats-table td{width:100%;box-sizing:border-box} 1415 .lc-stats-table thead{display:none} 1416 .lc-stats-table tbody{display:grid;gap:10px} 1417 .lc-stats-table tr{display:grid;width:100%;grid-template-columns:repeat(5,minmax(0,1fr));gap:6px;align-items:stretch;padding:9px;border:1px solid #dce7f2;border-radius:14px;background:#fff;box-shadow:0 10px 22px rgba(16,43,78,.06)} 1418 .lc-stats-table td{display:grid;min-width:0;padding:0;border:0;text-align:left;font-size:12px} 1419 .lc-stats-table td:before{content:"";display:block;color:#71839c;font-size:9px;font-weight:900;letter-spacing:.05em;text-transform:uppercase} 1420 .lc-stats-table td:nth-child(1){grid-column:1 / 4;align-content:center} 1421 .lc-stats-table td:nth-child(1):before{content:""} 1422 .lc-stats-table td:nth-child(1) strong{display:block;overflow:hidden;color:#0b315d;font-size:14px;line-height:1.1;text-overflow:ellipsis;white-space:nowrap} 1423 .lc-stats-table td:nth-child(1) small{display:block;overflow:hidden;margin-top:2px;color:#65758a;font-size:11px;text-overflow:ellipsis;white-space:nowrap} 1424 .lc-stats-table td:nth-child(2){grid-column:4 / 6;align-content:center;overflow:hidden;color:#213b58;font-size:9px;font-weight:900;line-height:1.16;text-align:right;text-overflow:ellipsis;text-transform:uppercase} 1425 .lc-stats-table td:nth-child(2):before{content:"";display:none} 1426 .lc-stats-table td:nth-child(n+3){align-content:center;gap:1px;min-height:39px;padding:5px 3px;border:1px solid #e1ebf4;border-radius:10px;background:#f7fbff;text-align:center} 1427 .lc-stats-table td:nth-child(n+3):before{text-align:center} 1428 .lc-stats-table td:nth-child(3):before{content:"V. lote"} 1429 .lc-stats-table td:nth-child(4):before{content:"RP"} 1430 .lc-stats-table td:nth-child(5):before{content:"Vid"} 1431 .lc-stats-table td:nth-child(6):before{content:"Cont"} 1432 .lc-stats-table td:nth-child(7):before{content:"Comp"} 1433 .lc-stats-table td:nth-child(8):before{content:"Tpo"} 1434 .lc-stats-table td:nth-child(8){font-size:11px;line-height:1.1} 1435 .lc-stats-table td:nth-child(8) small{display:none} 1436 .lc-stats-table td strong,.lc-stats-table td small{overflow-wrap:anywhere} 1437} 1438</style> 1439<style> 1440.rg-lot-interest{display:flex;align-items:center;gap:5px;color:#075fac;font-size:11px;font-weight:700}.rg-lot-interest img{width:13px;height:13px}.rg-time-button{border:1px solid #ccdaea;border-radius:6px;background:#fff;color:#075fac;padding:4px 7px;cursor:pointer;margin-left:5px;font-size:12px}.rg-time-editor{display:flex;gap:4px;flex-wrap:wrap;margin-top:6px}.rg-time-editor input{width:100px;padding:6px;border:1px solid #b7cce2;border-radius:6px;font-variant-numeric:tabular-nums}.rg-airtime-panel{background:#fff;border:1px solid #dce7f2;border-radius:16px;padding:20px;margin:16px 0;color:#153858}.rg-airtime-panel header{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}.rg-airtime-panel header span{font-size:12px;color:#54718c}.rg-airtime-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin:20px 0}.rg-airtime-metrics small{display:block;color:#64809b;font-size:11px}.rg-airtime-metrics strong{display:block;font-size:25px;margin-top:5px;font-variant-numeric:tabular-nums}.rg-airtime-panel summary{cursor:pointer;font-weight:600;font-size:13px}.rg-airtime-panel p{font-size:11px;color:#6a8198;margin:14px 0 0}.rg-airtime-groups{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:14px}.rg-airtime-groups article{display:grid;gap:6px;background:#f3f7fb;padding:12px;border-radius:10px;font-size:12px}.rg-airtime-group-heading{display:flex;align-items:center;gap:10px;min-width:0}.rg-airtime-group-heading strong{overflow-wrap:anywhere}.rg-airtime-group-logo{display:flex;align-items:center;justify-content:center;flex:0 0 36px;width:36px;height:36px;border-radius:50%;color:#fff;font-weight:800;overflow:hidden}.rg-airtime-group-logo img{display:block;width:28px;height:28px;object-fit:contain}.rg-airtime-groups article small{color:#6a8198}.rg-airtime-lot{color:#075fac!important;font-variant-numeric:tabular-nums}@media(max-width:650px){.rg-airtime-metrics{grid-template-columns:repeat(2,1fr)}.rg-airtime-groups{grid-template-columns:1fr}} 1441</style><link rel="stylesheet" href="/assets/rosgan-action-buttons.css"><link rel="stylesheet" href="/assets/filming-assistant.css">
1441<script defer src="/assets/filming-assistant.js"></script>
1441<script defer src="/assets/raw-video-batch.js?v=multipart-1"></script>
1441<script defer src="/assets/raw-video-compression.js"></script>
1441<script defer src="/assets/field-mode.js?v=multipart-1"></script>
1441<script defer src="/assets/multipart-video.js?v=1"></script>
1441<script defer src="/assets/video-upload-queue.js?v=multipart-1"></script>
1441 1442<style> 1443.rg-commercial-page .rg-commercial-row{content-visibility:auto;contain-intrinsic-size:auto 145px}.rg-commercial-progress{padding:18px 20px;margin:16px 0;background:#fff;border:1px solid #dce7f2;border-radius:16px;color:#153858}.rg-commercial-progress header{display:flex;justify-content:space-between;gap:12px;margin-bottom:14px}.rg-commercial-progress header span{font-size:12px;color:#65758a}.rg-progress-track{display:flex;height:16px;border-radius:999px;overflow:hidden;background:#edf2f7}.rg-progress-track sc-for{display:contents}.rg-progress-segment{display:block;height:100%;flex-shrink:0;transition:width .35s ease}.rg-progress-legend{display:flex;flex-wrap:wrap;gap:12px 24px;margin-top:13px;font-size:12px}.rg-progress-legend>span,.rg-progress-legend sc-for>span{display:inline-flex;align-items:center;gap:6px}.rg-progress-legend i{width:9px;height:9px;border-radius:50%;flex-shrink:0}.rg-progress-legend strong{font-variant-numeric:tabular-nums}.rg-progress-legend small{color:#6a8198}.rg-commercial-page .rosgan-ops-status.is-no-vendido{background:#fff0f0;color:#c03434}.rg-commercial-page .rosgan-ops-status.is-levantado{background:#edf1f5;color:#526579}@media(prefers-reduced-motion:reduce){.rg-progress-segment{transition:none}} 1444</style><style>.rtv-china-badge{position:absolute;z-index:4;bottom:calc(100% + .45vh);left:0;display:flex;align-items:center;gap:.7vw;padding:.8vh 1vw;background:#c9242c;color:#fff;border-radius:.6vh;font:800 2vh Arial,Helvetica,sans-serif;text-transform:uppercase;letter-spacing:.035em;box-shadow:0 2px 6px #00142830}.rtv-lower:has(.rtv-observations) .rtv-china-badge{bottom:calc(100% + 4.3vh)}.rtv-china-badge img{display:block;width:3.2vh;height:2.4vh;object-fit:contain}.rtv-china-badge .sc-interp{font:inherit;color:inherit}</style><link rel="stylesheet" href="/assets/lot-destination.css">
1445<script src="/assets/remate-filters.js?v=eligibility-1"></script>
1445 1446<style> 1447.rd-filter-panel{margin-top:14px;padding:20px;background:linear-gradient(180deg,#fff 0%,#f8fbff 100%);border:1px solid #d6e4f1;border-radius:20px;box-shadow:0 14px 34px rgba(0,55,103,.08)} 1448.rd-filter-head{display:flex;align-items:center;justify-content:space-between;gap:16px;padding-bottom:15px;border-bottom:1px solid #e5edf5}.rd-filter-title{display:flex;align-items:center;gap:10px;color:#073d70}.rd-filter-title>span:first-child{display:grid;place-items:center;width:34px;height:34px;border-radius:10px;background:#e9f4ff}.rd-filter-title img{width:17px;height:17px}.rd-filter-title strong{font-size:15px}.rd-filter-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;border-radius:999px;background:#0865ad;color:#fff;font-size:10px;font-weight:900}.rd-filter-clear{display:inline-flex;align-items:center;gap:7px;padding:8px 11px;border:1px solid #d6e4f1;border-radius:10px;background:#fff;color:#0965aa;cursor:pointer;font:750 11px inherit;transition:.16s}.rd-filter-clear:hover{border-color:#8bb8db;background:#f0f8ff}.rd-filter-clear img{width:14px;height:14px} 1449.rd-filter-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-top:16px}.rd-filter-group{min-width:0;padding:13px;border:1px solid #e0eaf3;border-radius:15px;background:rgba(255,255,255,.9)}.rd-filter-group-head{display:flex;align-items:center;gap:8px;margin-bottom:10px;color:#466984}.rd-filter-group-icon{display:grid;place-items:center;width:28px;height:28px;flex:none;border-radius:8px;background:#edf6ff}.rd-filter-group-icon img{width:15px;height:15px}.rd-filter-group-head strong{font-size:11px;letter-spacing:.025em}.rd-filter-options{display:flex;align-content:flex-start;flex-wrap:wrap;gap:7px;max-height:132px;overflow:auto;padding:1px 4px 3px 1px;scrollbar-width:thin;scrollbar-color:#b6c9da transparent}.rd-filter-chip{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:5px 9px;border:1px solid #d1dfec;border-radius:10px;background:#fff;color:#375875;font:600 11px inherit;text-align:left;cursor:pointer;transition:transform .15s,box-shadow .15s,border-color .15s,background .15s}.rd-filter-chip:hover{transform:translateY(-1px);border-color:#80afd2;box-shadow:0 5px 12px rgba(4,70,124,.09)}.rd-filter-chip[aria-pressed="true"]{border-color:#075c9e;background:#075c9e;color:#fff;box-shadow:0 5px 13px rgba(0,65,119,.18)}.rd-filter-chip-logo{display:grid;place-items:center;flex:none;width:23px;height:23px;padding:3px;box-sizing:border-box;border-radius:6px}.rd-filter-chip-logo img{width:100%;height:100%;object-fit:contain}.rd-filter-chip-count{display:inline-grid;place-items:center;min-width:18px;height:18px;padding:0 4px;border-radius:999px;background:#edf4fa;color:#51708c;font-size:9px;font-weight:900}.rd-filter-chip[aria-pressed="true"] .rd-filter-chip-count{background:#ffffff26;color:#fff} 1450@media(max-width:1050px){.rd-filter-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:620px){.rd-filter-panel{padding:14px}.rd-filter-grid{grid-template-columns:1fr}.rd-filter-head{align-items:flex-start}.rd-filter-group{padding:11px}} 1451</style> 1452<style> 1453.rosgan-tv-output .rtv-lower{transition:visibility 0s linear .45s}.rosgan-tv-output.has-name-strap .rtv-lower{visibility:hidden;transition-delay:0s} 1454.rtv-name-strap{position:absolute;left:25%;bottom:5vh;width:50%;height:16.6667vh;box-sizing:border-box;z-index:80;display:flex;align-items:center;padding:2vh 2.4vw;gap:2vw;background:linear-gradient(115deg,#001f43 0%,#003c74 62%,#0c6898 100%);border:1px solid #a8e8ff55;border-radius:1.5vh;box-shadow:0 1.2vh 3vh #001b3555;color:white;overflow:hidden;pointer-events:none;opacity:0;transform:translateY(3vh) scale(.97);visibility:hidden;transition:opacity .4s ease,transform .45s cubic-bezier(.22,1,.36,1),visibility 0s linear .45s} 1455.rtv-name-strap.is-on{opacity:1;transform:translateY(0) scale(1);visibility:visible;transition-delay:0s;animation:rtv-name-enter .5s cubic-bezier(.22,1,.36,1) both} 1456.rtv-name-strap:before{content:'';position:absolute;inset:0;background:linear-gradient(120deg,transparent 65%,#ffffff08 65%,#ffffff08 78%,transparent 78%);pointer-events:none}.rtv-name-strap:after{content:'';position:absolute;bottom:0;left:0;width:100%;height:.55vh;background:linear-gradient(90deg,#19a8c2,#96f0f5 50%,#147ab8)} 1457.rtv-name-emblem{flex:0 0 5.3vw;display:grid;place-items:center;padding-right:1.8vw;border-right:1px solid #bdd9ee55;height:9vh}.rtv-name-emblem img{width:100%;max-height:8vh;object-fit:contain;filter:brightness(0) invert(1)}.rtv-name-copy{flex:1;min-width:0;height:100%;position:relative;display:flex;flex-direction:column;justify-content:center;gap:.9vh}.rtv-name-strap .rtv-name-title{color:#fff;font:900 4.4vh/1.08 Archivo,Arial,sans-serif;letter-spacing:-.035em;white-space:nowrap;display:block;flex:none;overflow:hidden}.rtv-name-strap .rtv-name-subtitle{font:500 2.65vh/1.15 Archivo,Arial,sans-serif;color:#bde9f4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} 1458.rtv-name-strap .rtv-name-copy .sc-interp{font:inherit;color:inherit;letter-spacing:inherit;line-height:inherit;white-space:inherit} 1459@keyframes rtv-name-enter{from{opacity:0;transform:translateY(3vh) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}} 1460@media(prefers-reduced-motion:reduce){.rtv-name-strap,.rtv-name-strap.is-on{animation:none;transition:none}.rosgan-tv-output .rtv-lower{transition:none}} 1461.rtv-name-editor{display:flex;gap:10px;flex-wrap:wrap;margin:14px 0}.rtv-name-editor input{flex:1;min-width:170px;padding:11px;border:1px solid #cbddea;border-radius:9px;font:inherit}.rtv-name-item{display:flex;align-items:center;gap:10px;padding:12px 0;border-top:1px solid #e3edf5}.rtv-name-item div{flex:1}.rtv-name-item small{display:block;color:#68839a;margin-top:4px}.rtv-name-item button,.rtv-name-editor button{border:1px solid #cbddea;background:#f4f8fc;color:#004276;border-radius:9px;padding:10px;cursor:pointer;font:700 12px inherit} 1462</style>
1462<script src="/assets/rosgan-name-fit.js"></script>
vendor: 1 bytes, line 1462
1462
1463<script src="/assets/rosgan-countdown.js"></script>
1463<style>.rosgan-tv-output .rg-full-plate{z-index:100;background:linear-gradient(125deg,#002b54,#07538b,#003451);animation:none;backdrop-filter:none}.rg-full-plate:before,.rg-full-plate:after{animation:none;filter:none}.rg-full-plate .tv-waiting-frame{backdrop-filter:none}.rg-full-plate .tv-waiting-logos{display:flex;flex-wrap:wrap;justify-content:center;gap:2vw}.rg-full-plate .tv-waiting-logo{min-height:7vh;width:6vw;animation:none}.rg-full-plate .tv-waiting-logo img{max-height:7vh}.rg-full-plate .tv-waiting-logo.is-rosgan{width:17vw}.rg-full-plate .sc-interp{color:inherit;font:inherit}</style><style> 1464html[data-rosgan-video-hidden="true"],html[data-rosgan-video-hidden="true"] body,html[data-rosgan-video-hidden="true"] #dc-root,html[data-rosgan-video-hidden="true"] .sc-host,html[data-rosgan-video-hidden="true"] .sc-host>div{background:transparent!important} 1465.rosgan-tv-output.is-video-off{background:transparent!important} 1466.rosgan-tv-output.is-video-off .rtv-brand-cons,.rosgan-tv-output.is-video-off .rtv-brand-name,.rosgan-tv-output.is-video-off .ros-tv-air-pane .rtv-brand{display:none!important} 1467.rosgan-tv-output.is-video-off .rtv-brand-rosgan{border-radius:.55vw} 1468.rosgan-tv-output.is-video-off video,.rosgan-tv-output.is-video-off .rtv-lower,.rosgan-tv-output.is-video-off .rosgan-tv-fallback,.rosgan-tv-output.is-video-off .rosgan-tv-no-lot{visibility:hidden!important;transition:none!important} 1469</style><link rel="stylesheet" href="/assets/app-mobile.css"><link rel="stylesheet" href="/assets/cabana-video-fit.css?v=2"> 1470<link rel="stylesheet" href="/assets/cabana-auction-list.css">
1471<script src="/assets/feria-youtube-control.js" defer></script>
1471 1472</head> 1473<body> 1474<section id="lacassina-stats-standalone" aria-label="Estadisticas publicas La Cassina"> 1475 <div class="lc-stats-shell"> 1476 <header class="lc-stats-hero"> 1477 <a class="lc-stats-back" href="/lacassina"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catalogo</a> 1478 <div class="lc-stats-hero-inner"> 1479 <div> 1480 <p class="lc-stats-kicker">Rosgan Cabanas · Inteligencia comercial</p> 1481 <h1>Centro de estadisticas La Cassina</h1> 1482 <p class="lc-stats-subtitle" id="lc-stats-auction-title">Cargando informacion del remate...</p> 1483 </div> 1484 <div class="lc-stats-logo-card" id="lc-stats-logo-card"> 1485 <div class="lc-stats-skeleton" style="width:100%;height:132px"></div> 1486 </div> 1487 </div> 1488 </header> 1489 <main class="lc-stats-main"> 1490 <section class="lc-stats-card lc-stats-section"> 1491 <div class="lc-stats-section-head"> 1492 <div> 1493 <small>PANEL EJECUTIVO</small> 1494 <h2>Lectura rapida del remate</h2> 1495 </div> 1496 <span class="lc-stats-status" id="lc-stats-status">Cargando datos</span> 1497 </div> 1498 <div class="lc-stats-grid" id="lc-stats-kpis"> 1499 <div class="lc-stats-skeleton"></div> 1500 <div class="lc-stats-skeleton"></div> 1501 <div class="lc-stats-skeleton"></div> 1502 <div class="lc-stats-skeleton"></div> 1503 <div class="lc-stats-skeleton"></div> 1504 <div class="lc-stats-skeleton"></div> 1505 </div> 1506 </section> 1507 <section class="lc-stats-insight-grid" id="lc-stats-insights"> 1508 <article class="lc-stats-insight"><span>Insight</span><strong>Cargando analisis...</strong><em>Esperando datos del catalogo</em></article> 1509 <article class="lc-stats-insight"><span>Riesgo</span><strong>Cargando cobertura...</strong><em>Control de imagenes, videos y datos</em></article> 1510 <article class="lc-stats-insight"><span>Accion</span><strong>Preparando prioridades...</strong><em>Lotes para revisar primero</em></article> 1511 </section> 1512 <section class="lc-stats-card lc-stats-section"> 1513 <div class="lc-stats-section-head"> 1514 <div> 1515 <small>ACTIVIDAD EN EL TIEMPO</small> 1516 <h2>Evolucion de señales</h2> 1517 </div> 1518 <div class="lc-stats-range-tabs" id="lc-stats-range-tabs" aria-label="Filtro temporal de estadisticas"></div> 1519 </div> 1520 <div class="lc-stats-timeline" id="lc-stats-timeline"> 1521 <div class="lc-stats-skeleton"></div> 1522 <div class="lc-stats-skeleton"></div> 1523 <div class="lc-stats-skeleton"></div> 1524 </div> 1525 </section> 1526 <section class="lc-stats-card lc-stats-section"> 1527 <div class="lc-stats-section-head"> 1528 <div> 1529 <small>EMBUDO</small> 1530 <h2>Estado del catalogo</h2> 1531 </div> 1532 </div> 1533 <div class="lc-stats-funnel" id="lc-stats-funnel"> 1534 <div class="lc-stats-skeleton"></div> 1535 <div class="lc-stats-skeleton"></div> 1536 <div class="lc-stats-skeleton"></div> 1537 </div> 1538 </section> 1539 <section class="lc-stats-two-col"> 1540 <div class="lc-stats-card lc-stats-section"> 1541 <div class="lc-stats-section-head"> 1542 <div> 1543 <small>DISTRIBUCION DE INTERACCIONES</small> 1544 <h2>Interacciones por categoria</h2> 1545 <p>Suma vistas de lote, RPs vistos, videos e imagenes.</p> 1546 </div> 1547 </div> 1548 <div class="lc-stats-bars" id="lc-stats-category-bars"> 1549 <div class="lc-stats-skeleton"></div> 1550 <div class="lc-stats-skeleton"></div> 1551 </div> 1552 </div> 1553 <div class="lc-stats-card lc-stats-section"> 1554 <div class="lc-stats-section-head"> 1555 <div> 1556 <small>CABANAS</small> 1557 <h2>Participacion</h2> 1558 </div> 1559 </div> 1560 <div class="lc-stats-panel-list" id="lc-stats-cabana-list"> 1561 <div class="lc-stats-skeleton"></div> 1562 </div> 1563 </div> 1564 </section> 1565 <section class="lc-stats-card lc-stats-section"> 1566 <div class="lc-stats-section-head"> 1567 <div> 1568 <small>AUDIOVISUAL</small> 1569 <h2>Top 10 imagenes y videos</h2> 1570 </div> 1571 </div>
1572 <div class="lc-stats-panel-list" id="lc-stats-media-list"> 1573 <div class="lc-stats-skeleton"></div> 1574 </div> 1575 </section> 1576 <section class="lc-stats-card lc-stats-section"> 1577 <div class="lc-stats-section-head"> 1578 <div> 1579 <small>GENETICA Y MERCADO</small> 1580 <h2>Atributos del catalogo</h2> 1581 </div> 1582 </div> 1583 <div class="lc-stats-market-list" id="lc-stats-genetics-list"> 1584 <div class="lc-stats-skeleton"></div> 1585 </div> 1586 </section> 1587 <section class="lc-stats-card lc-stats-section"> 1588 <div class="lc-stats-section-head"> 1589 <div> 1590 <small>RANKING DE INTERES</small> 1591 <h2>Score operativo por lote</h2> 1592 <p>Ordenado por vistas, RPs vistos, reproducciones reales, imagenes, contactos y tiempo registrado.</p> 1593 </div> 1594 </div> 1595 <div class="lc-stats-table-wrap"> 1596 <table class="lc-stats-table"> 1597 <thead> 1598 <tr><th>Lote / RP</th><th>Categoria</th><th>Visitas lote</th><th>Vistas RP</th><th>Videos</th><th>Contactos</th><th>Compartidos</th><th>Tiempo prom. RP</th></tr> 1599 </thead> 1600 <tbody id="lc-stats-top-lots"> 1601 <tr><td colspan="8">Cargando lotes...</td></tr> 1602 </tbody> 1603 </table> 1604 </div> 1605 </section> 1606 </main> 1607 </div> 1608</section> 1609<section id="rosgan-static-boot" role="status" aria-live="polite"> 1610 <div class="rosgan-static-boot-card rosgan-static-guest"> 1611 <span class="rosgan-static-boot-spinner" aria-hidden="true"></span> 1612 <strong data-rosgan-boot-status>No pudimos completar el inicio.</strong> 1613 <p data-rosgan-boot-detail>Reintentá para renovar la sesión de forma segura.</p> 1614 <button class="rosgan-static-boot-retry" data-rosgan-boot-retry type="button" hidden onclick="window.location.reload()">Reintentar</button> 1615 </div> 1616</section>
1617<script> 1618(function () { 1619 window.__ROSGAN_STATIC_BOOT_TIMER__ = window.setTimeout(function () { 1620 if (document.documentElement.getAttribute('data-rosgan-mounted') === 'true') return; 1621 var fallback = document.getElementById('rosgan-static-boot'); 1622 if (!fallback) return; 1623 fallback.hidden = false; 1624 fallback.classList.add('is-visible'); 1625 if (fallback.className.indexOf('has-error') === -1) fallback.className += ' has-error'; 1626 var status = fallback.querySelector('[data-rosgan-boot-status]'); 1627 var detail = fallback.querySelector('[data-rosgan-boot-detail]'); 1628 var retry = fallback.querySelector('[data-rosgan-boot-retry]'); 1629 if (status) status.textContent = 'No pudimos completar el inicio en este navegador.'; 1630 if (detail) detail.textContent = 'Reintentá para renovar la sesión de forma segura.'; 1631 if (retry) retry.hidden = false; 1632 }, 15000); 1633})(); 1634</script>
1634 1635<x-dc> 1636<helmet> 1637<link rel="preconnect" href="https://fonts.googleapis.com"> 1638<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
1639<script src="/vendor/proj4.js?v=2.11.0"></script>
vendor: 1 bytes, line 1639
1639
1640<script src="./vendor/Sortable.min.js"></script>
1640 1641<style> 1642html, body { margin:0; width:100%; max-width:100%; min-height:100%; overflow-x:hidden; overflow-x:clip; background:#f4f7fb; font-family:'Archivo',sans-serif; -webkit-font-smoothing:antialiased; } 1643x-dc, #dc-root { display:block; width:100%; max-width:100%; min-width:0; overflow-x:hidden; overflow-x:clip; } 1644[style*="position:absolute; inset:0"], [style*="position:fixed; inset:0"] { top:0; right:0; bottom:0; left:0; } 1645.rosgan-auth-screen{position:relative;isolation:isolate;background:#001d3d;overflow:visible!important} 1646.rosgan-auth-background{position:fixed!important;inset:0!important;z-index:0!important;pointer-events:none} 1647.rosgan-auth-shade{position:fixed!important;inset:0!important;z-index:1!important;pointer-events:none} 1648.rosgan-auth-card{ 1649 z-index:5!important; 1650 max-width:calc(100vw - 32px)!important; 1651 box-sizing:border-box!important; 1652 opacity:1!important; 1653 visibility:visible!important; 1654 animation:none!important; 1655 -webkit-transform:translateZ(0)!important; 1656 transform:translateZ(0)!important; 1657} 1658.rosgan-auth-logo{ 1659 display:block!important; 1660 width:132px!important; 1661 height:auto!important; 1662 max-width:46vw!important; 1663 object-fit:contain!important; 1664 object-position:center!important; 1665 align-self:center!important; 1666 flex:0 0 auto!important; 1667 margin-left:auto!important; 1668 margin-right:auto!important; 1669} 1670.rosgan-account-help-launcher{ 1671 width:100%; border:1px solid rgba(154,219,255,.48); border-radius:13px; padding:12px 14px; 1672 background:rgba(18,139,207,.18); color:#fff; font:800 13px/1.35 inherit; cursor:pointer; 1673 display:flex; align-items:center; justify-content:center; gap:9px; text-align:center; 1674 transition:transform 160ms ease,background 160ms ease,border-color 160ms ease; 1675} 1676.rosgan-account-help-launcher:hover{transform:translateY(-1px);background:rgba(18,139,207,.3);border-color:rgba(189,231,255,.75)} 1677.rosgan-account-assistant{ 1678 display:grid; gap:12px; padding:15px; border-radius:16px; color:#07345e; 1679 background:linear-gradient(145deg,rgba(255,255,255,.98),rgba(235,247,255,.98)); 1680 border:2px solid #78c7f0; box-shadow:0 14px 38px rgba(0,30,66,.25); 1681} 1682.rosgan-account-assistant-head{display:flex;align-items:flex-start;gap:10px} 1683.rosgan-account-assistant-avatar{width:42px;height:42px;flex:0 0 42px;border-radius:50%;display:grid;place-items:center;background:#0673b8;color:#fff;font-size:21px;box-shadow:0 5px 14px rgba(6,115,184,.3)} 1684.rosgan-account-assistant-copy{min-width:0;flex:1} 1685.rosgan-account-assistant-copy strong{display:block;font-size:16px;line-height:1.15;color:#002e5f} 1686.rosgan-account-assistant-copy span{display:block;margin-top:3px;font-size:12px;line-height:1.45;color:#496a88} 1687.rosgan-account-assistant-close{width:32px;height:32px;flex:0 0 32px;border:0;border-radius:50%;background:#e4f1fa;color:#164c77;font:900 18px/1 inherit;cursor:pointer} 1688.rosgan-account-assistant-text{margin:0;font-size:14px;line-height:1.52;color:#163f62} 1689.rosgan-account-assistant-tip{padding:10px 11px;border-radius:11px;background:#e8f7ef;border:1px solid #a9dfc3;color:#12623d;font-size:12.5px;line-height:1.45;font-weight:700} 1690.rosgan-account-assistant-error{padding:9px 10px;border-radius:10px;background:#fff0f1;border:1px solid #f2b8bd;color:#a51f2a;font-size:12.5px;line-height:1.4;font-weight:750} 1691.rosgan-account-assistant-actions{display:grid;grid-template-columns:1fr 1fr;gap:9px} 1692.rosgan-account-assistant-actions button{min-height:44px;border-radius:11px;padding:10px 12px;font:800 13px/1.25 inherit;cursor:pointer} 1693.rosgan-account-assistant-primary{border:0;background:#006cb4;color:#fff} 1694.rosgan-account-assistant-secondary{border:1px solid #b7d4e9;background:#fff;color:#174d76} 1695.rosgan-account-assistant-progress{display:flex;align-items:center;gap:7px;color:#50708b;font-size:11px;font-weight:800} 1696.rosgan-account-assistant-progress span{height:7px;flex:1;border-radius:999px;background:#cfdeeb;overflow:hidden} 1697.rosgan-account-assistant-progress i{display:block;height:100%;border-radius:inherit;background:#0794d2;width:var(--account-help-progress,0%);transition:width 220ms ease} 1698.rosgan-auth-field-assisted{border-color:#65d6ff!important;box-shadow:0 0 0 4px rgba(55,193,246,.24),0 0 22px rgba(55,193,246,.2)!important} 1699.rosgan-account-verification-help{padding:13px 14px;border-radius:14px;background:#eaf7ff;border:2px solid #7dc8ee;color:#123f62;display:grid;gap:6px} 1700.rosgan-account-verification-help strong{font-size:15px;color:#002e5f}.rosgan-account-verification-help span{font-size:12.5px;line-height:1.48} 1701@media(max-width:520px){ 1702 .rosgan-account-assistant{padding:13px;gap:10px} 1703 .rosgan-account-assistant-actions{grid-template-columns:1fr} 1704 .rosgan-account-assistant-actions button{min-height:46px;font-size:13.5px} 1705} 1706@supports (-webkit-touch-callout:none){ 1707 .rosgan-auth-card{ 1708 background:rgba(0,29,61,.9)!important; 1709 -webkit-backdrop-filter:none!important; 1710 backdrop-filter:none!important; 1711 } 1712} 1713input, select, textarea, button { box-sizing:border-box; max-width:100%; } 1714a { color:#0a5ca8; text-decoration:none; } a:hover { color:#002e5f; } 1715@keyframes kenburns { from { transform:scale(1.03); } to { transform:scale(1.12); } } 1716@keyframes pulseDot { 0%,100% { opacity:1; } 50% { opacity:0.3; } } 1717@keyframes badgePop { from { opacity:0; transform:scale(0.6); } to { opacity:1; transform:scale(1); } } 1718@keyframes pageIn { from { opacity:0; transform:translateY(18px); } to { opacity:1; transform:translateY(0); } } 1719@keyframes prebidSuccessFlood { 1720 0% { opacity:.15; transform:scale(.015); } 1721 18% { opacity:1; } 1722 100% { opacity:1; transform:scale(1); } 1723} 1724@keyframes prebidSuccessCheck { 1725 0%,32% { opacity:0; transform:scale(.35) rotate(-18deg); } 1726 62% { opacity:1; transform:scale(1.16) rotate(4deg); } 1727 78% { transform:scale(.94) rotate(-1deg); } 1728 100% { opacity:1; transform:scale(1) rotate(0); } 1729} 1730@keyframes prebidSuccessCopy { 1731 0%,42% { opacity:0; transform:translateY(18px); } 1732 100% { opacity:1; transform:translateY(0); } 1733} 1734@keyframes prebidSuccessLayerOut { 1735 0%,82% { opacity:1; } 1736 100% { opacity:0; visibility:hidden; } 1737} 1738.prebid-success-layer { 1739 position:fixed; 1740 inset:0; 1741 z-index:110000; 1742 display:grid; 1743 place-items:center; 1744 overflow:hidden; 1745 padding:24px; 1746 color:#fff; 1747 text-align:center; 1748 isolation:isolate; 1749 pointer-events:auto; 1750 animation:prebidSuccessLayerOut 1850ms ease-in both; 1751} 1752.prebid-success-flood { 1753 position:absolute; 1754 z-index:-1; 1755 width:180vmax; 1756 height:180vmax; 1757 border-radius:50%; 1758 background:radial-gradient(circle at 50% 42%,rgba(71,221,165,.95) 0,rgba(24,173,117,.98) 30%,#087653 72%,#045b40 100%); 1759 box-shadow:0 0 0 100vmax #045b40; 1760 transform:scale(.015); 1761 animation:prebidSuccessFlood 660ms cubic-bezier(.18,.82,.24,1) both; 1762} 1763.prebid-success-content { display:grid; justify-items:center; width:min(520px,100%); } 1764.prebid-success-check { 1765 display:grid; 1766 place-items:center; 1767 width:96px; 1768 height:96px; 1769 border-radius:50%; 1770 background:#fff; 1771 box-shadow:0 24px 55px rgba(0,57,39,.28),0 0 0 12px rgba(255,255,255,.13); 1772 animation:prebidSuccessCheck 880ms cubic-bezier(.16,1,.3,1) both; 1773} 1774.prebid-success-check img { width:50px; height:50px; filter:invert(34%) sepia(72%) saturate(706%) hue-rotate(111deg) brightness(91%) contrast(92%); } 1775.prebid-success-copy { margin-top:26px; animation:prebidSuccessCopy 900ms cubic-bezier(.16,1,.3,1) both; } 1776.prebid-success-copy small { display:block; margin-bottom:8px; color:rgba(255,255,255,.78); font-size:11px; font-weight:900; letter-spacing:.16em; } 1777.prebid-success-copy h2 { margin:0; color:#fff; font-size:clamp(32px,6vw,52px); line-height:1; letter-spacing:-.035em; } 1778.prebid-success-amount { display:block; margin-top:14px; color:#fff; font-size:clamp(27px,5vw,42px); font-weight:950; font-variant-numeric:tabular-nums; } 1779.prebid-success-copy p { margin:10px 0 0; color:rgba(255,255,255,.86); font-size:15px; font-weight:750; } 1780@keyframes roslotCardBidSuccess { 1781 0% { opacity:0; transform:scale(.985); } 1782 12%,78% { opacity:1; transform:scale(1); } 1783 100% { opacity:0; transform:scale(1.01); } 1784} 1785.roslot-op-success { 1786 position:absolute; 1787 z-index:8; 1788 inset:0; 1789 display:flex; 1790 align-items:center; 1791 justify-content:center; 1792 gap:9px; 1793 overflow:hidden; 1794 border-radius:inherit; 1795 background:linear-gradient(105deg,#11966d,#087653); 1796 color:#fff; 1797 box-shadow:inset 0 0 0 1px rgba(255,255,255,.18); 1798 pointer-events:none; 1799 animation:roslotCardBidSuccess 1750ms ease-in-out both; 1800} 1801.roslot-op-success-lottie { display:block; width:48px; height:48px; flex:0 0 48px; } 1802.roslot-op-success-copy { display:grid; gap:1px; text-align:left; } 1803.roslot-op-success-copy small { color:rgba(255,255,255,.82); font-size:8px; font-weight:950; letter-spacing:.12em; } 1804.roslot-op-success-copy strong { color:#fff; font-size:17px; font-weight:950; line-height:1; font-variant-numeric:tabular-nums; } 1805.roslot-op-success-copy span { color:rgba(255,255,255,.88); font-size:9px; font-weight:750; } 1806.roslot-bid-card { position:relative; overflow:hidden; } 1807.roslot-op-success.is-detail { flex-direction:column; gap:0; } 1808.roslot-op-success.is-detail .roslot-op-success-lottie { width:96px; height:96px; flex-basis:96px; margin-bottom:-8px; } 1809.roslot-op-success.is-detail .roslot-op-success-copy { text-align:center; } 1810.roslot-op-success.is-detail .roslot-op-success-copy small { font-size:9px; } 1811.roslot-op-success.is-detail .roslot-op-success-copy strong { margin-top:5px; font-size:28px; } 1812.roslot-op-success.is-detail .roslot-op-success-copy span { margin-top:5px; font-size:11px; } 1813@media (prefers-reduced-motion:reduce) { 1814 .prebid-success-layer { animation-duration:1200ms; } 1815 .roslot-op-success { animation-duration:900ms; } 1816 .prebid-success-flood,.prebid-success-check,.prebid-success-copy { animation-duration:1ms; animation-delay:0ms; } 1817}
1818 @keyframes shimmer { 0% { background-position:-500px 0; } 100% { background-position:500px 0; } } 1819 @keyframes dc-spin { to { transform:rotate(360deg); } } 1820 .dc-skeleton { 1821 color:transparent !important; 1822 background:linear-gradient(100deg,#e7edf4 20%,#f8fafc 40%,#e7edf4 60%) !important; 1823 background-size:220% 100% !important; 1824 animation:shimmer 1.45s infinite !important; 1825 border-color:transparent !important; 1826 box-shadow:none !important; 1827 pointer-events:none; 1828 user-select:none; 1829 } 1830 .home-skeleton { 1831 position:relative; 1832 overflow:hidden; 1833 color:transparent !important; 1834 background:#e7edf4 !important; 1835 border-color:transparent !important; 1836 box-shadow:none !important; 1837 pointer-events:none; 1838 user-select:none; 1839 } 1840 .home-skeleton::after { 1841 content:""; 1842 position:absolute; 1843 inset:0; 1844 transform:translateX(-115%); 1845 background:linear-gradient(90deg, transparent 0%, rgba(255,255,255,.84) 48%, transparent 100%); 1846 animation:homeSkeletonSweep 1.35s ease-in-out infinite; 1847 } 1848 .home-skeleton-dark { background:rgba(255,255,255,.12) !important; } 1849 .home-skeleton-dark::after { background:linear-gradient(90deg, transparent 0%, rgba(255,255,255,.22) 48%, transparent 100%); } 1850 .home-loader-card { 1851 min-width:0; 1852 overflow:hidden; 1853 border:1px solid #e5ecf3; 1854 border-radius:16px; 1855 background:#fff; 1856 box-shadow:0 8px 26px rgba(0,30,60,.07); 1857 } 1858 @keyframes homeSkeletonSweep { to { transform:translateX(115%); } } 1859 @media (prefers-reduced-motion:reduce) { 1860 .home-skeleton::after { animation:none; display:none; } 1861 } 1862 .dc-media-loader { 1863 position:absolute; 1864 inset:0; 1865 z-index:3; 1866 display:grid; 1867 place-items:center; 1868 background:linear-gradient(100deg,#dfe7f0 20%,#f6f9fc 40%,#dfe7f0 60%); 1869 background-size:220% 100%; 1870 animation:shimmer 1.45s infinite; 1871 } 1872 .dc-spinner { 1873 width:34px; 1874 height:34px; 1875 border:3px solid rgba(10,92,168,.2); 1876 border-top-color:#0a5ca8; 1877 border-radius:50%; 1878 animation:dc-spin .8s linear infinite; 1879 } 1880 .dc-media-image { 1881 background:linear-gradient(100deg,#e7edf4 20%,#f8fafc 40%,#e7edf4 60%); 1882 background-size:220% 100%; 1883 animation:shimmer 1.45s infinite; 1884 color:transparent; 1885 } 1886 .dc-media-image[data-dc-loaded="true"] { 1887 background:none; 1888 animation:none; 1889 } 1890 .dc-cabana-card-media { 1891 transition:opacity .32s ease; 1892 will-change:opacity; 1893 } 1894 .cabana-auction-card { display:flex; flex-direction:column; min-width:0; overflow:hidden; border:1px solid #dce6f0; border-radius:20px; background:#fff; box-shadow:0 12px 34px rgba(0,29,61,.08); transition:transform 180ms ease,box-shadow 180ms ease,border-color 180ms ease; } 1895 .cabana-auction-card:hover { transform:translateY(-4px); border-color:#c5d6e7; box-shadow:0 22px 48px rgba(0,29,61,.14); } 1896 .cabana-card-hero { position:relative; min-height:176px; overflow:hidden; isolation:isolate; background:#eaf0f6; } 1897 .cabana-card-hero-media-stack { position:absolute; inset:0; z-index:0; overflow:hidden; background:transparent; } 1898 .cabana-card-hero-media-base { position:absolute; inset:0; z-index:0; width:100%; height:100%; object-fit:cover; } 1899 .cabana-card-hero-media { position:absolute; inset:0; z-index:0; width:100%; height:100%; object-fit:cover; opacity:0; transform:scale(1.015); animation:cabanaCardMediaCycle 40s linear infinite; } 1900 .cabana-card-hero-shade { position:absolute; inset:0; z-index:1; background:linear-gradient(180deg,rgba(0,24,49,.42) 0%,rgba(0,24,49,.04) 42%,rgba(0,24,49,.9) 100%); pointer-events:none; } 1901 .cabana-card-hero-fallback { position:absolute; inset:0; z-index:0; display:grid; place-items:center; padding:28px; box-sizing:border-box; background:#eef3f8; } 1902 .cabana-card-hero-fallback img { display:block; width:auto; max-width:66%; max-height:72px; object-fit:contain; } 1903 1904.home-channel{padding:28px clamp(16px,4vw,36px) 4px;color:#112033}.home-channel>header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:16px}.home-channel>header small{font-size:11px;letter-spacing:.14em;font-weight:800;color:#075da8}.home-channel h2{font-size:clamp(21px,2.5vw,28px);margin:4px 0}.home-channel>header button{min-height:44px;border:1px solid #cfdeeb;border-radius:12px;background:#fff;color:#075da8;padding:10px 14px;cursor:pointer}.home-channel-layout{display:grid;grid-template-columns:minmax(0,2.2fr) minmax(250px,1fr);border-radius:20px;overflow:hidden;background:#062541;box-shadow:0 12px 30px #002e5f12}.home-channel-screen{aspect-ratio:16/9;background:#00162a;align-self:start;scroll-margin-top:80px}.home-channel-screen video{display:block;width:100%;height:100%;object-fit:contain}.home-channel-screen iframe{display:block;width:100%;height:100%;border:0}.home-channel-empty{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:#fff;padding:24px;box-sizing:border-box}.home-channel-empty img{width:160px;filter:brightness(0) invert(1);margin-bottom:15px}.home-channel-empty h3{margin:8px 0;font-size:22px}.home-channel-empty p{color:#b7cbdd;font-size:14px}.home-channel-layout aside{color:#fff;min-width:0}.home-channel-current{padding:24px}.home-channel-current h3{font-size:clamp(18px,2vw,25px);line-height:1.2;margin:14px 0 10px}.home-channel-current p{font-size:13px;color:#b7cbdd}.home-channel-current>small{display:block;font-size:12px;line-height:1.5;color:#b7cbdd}.home-channel-current a{display:inline-block;color:#fff;font-size:13px;margin-top:16px}.home-channel-badge{font-size:10px;font-weight:850;letter-spacing:.08em;background:#ffffff1c;padding:6px 10px;border-radius:20px}.home-channel-badge.is-live{background:#e63946}.home-channel-today{border-top:1px solid #ffffff1c;padding:16px 24px;max-height:220px;overflow-y:auto}.home-channel-today h4{margin:0 0 12px;font-size:13px}.home-channel-today button{display:flex;text-align:left;gap:10px;width:100%;padding:9px;border:1px solid transparent;border-radius:10px;background:transparent;color:#fff;cursor:pointer}.home-channel-today button[aria-pressed="true"]{background:#ffffff12;border-color:#74b6ea}.home-channel-today img{width:76px;aspect-ratio:16/9;object-fit:cover;border-radius:6px;align-self:center}.home-channel-today strong{font-size:12px;line-height:1.4;display:block}.home-channel-today small{display:block;font-size:11px;color:#b7cbdd;margin-top:5px}.home-channel button:focus-visible,.home-channel a:focus-visible{outline:3px solid #74b6ea;outline-offset:2px}@media(max-width:800px){.home-channel-layout{grid-template-columns:1fr;border-radius:16px}.home-channel-current{padding:18px}.home-channel-current h3{font-size:21px}.home-channel-today{padding:16px 18px}.home-channel>header{align-items:flex-start}.home-channel>header button{font-size:11px;max-width:160px}.home-channel-empty h3{font-size:18px}.home-channel-empty img{width:110px}.home-channel-empty p{font-size:12px}} 1905 1906.home-channel-layout{align-items:start}.home-channel-layout aside{max-height:100%;overflow:hidden}.home-channel-links{display:flex;align-items:center;gap:16px;margin-top:14px}.home-channel-links a{margin:0}.home-channel-links button{border:0;padding:6px 0;background:transparent;
1906color:#bdddf6;font-size:12px;cursor:pointer}.home-channel-today{box-sizing:border-box;max-height:190px}.home-channel-today summary{font-size:13px;font-weight:750;cursor:pointer;padding:2px 0 10px}.home-channel-programs{display:grid;gap:3px} 1907@media(min-width:801px){.home-channel-layout{position:relative}.home-channel-layout aside{position:absolute;right:0;top:0;bottom:0;width:31.25%;display:flex;flex-direction:column}.home-channel-current{padding:18px 22px;flex:none}.home-channel-current h3{font-size:21px}.home-channel-today{min-height:0;flex:1;max-height:none}} 1908@media(max-width:800px){.home-channel{padding-top:20px}.home-channel>header{margin-bottom:10px}.home-channel h2{font-size:20px}.home-channel-current{padding:12px 14px}.home-channel-current h3{font-size:16px;line-height:1.25;margin:8px 0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.home-channel-current>p,.home-channel-current>small{display:none}.home-channel-links{margin-top:6px;font-size:12px}.home-channel-badge{font-size:9px;padding:4px 8px}.home-channel-today{padding:10px 14px;max-height:180px}.home-channel-today summary{padding:0;min-height:24px}.home-channel-today[open] summary{margin-bottom:8px}.home-channel-today strong{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.home-channel>header button{min-height:36px;padding:6px 10px}} 1909 1910.home-channel-picker{display:flex;flex-direction:column;min-height:0;overflow:hidden;padding:12px 14px!important}.home-channel-picker-heading{flex:none;margin-bottom:9px}.home-channel-picker-heading>strong{font-size:11px;color:#bcd3e7}.home-channel-programs{display:flex;flex-direction:column;gap:8px;min-height:0;overflow-x:hidden;overflow-y:auto;padding:2px;scrollbar-width:thin;scrollbar-color:#6689a3 transparent;overscroll-behavior-y:contain}.home-channel-programs .home-channel-chip{box-sizing:border-box;flex:none;width:100%;min-width:0;min-height:64px;padding:10px;border:1px solid #ffffff26;border-radius:11px;background:#ffffff09;align-items:center;gap:11px}.home-channel-programs .home-channel-chip[aria-pressed="true"]{background:#155b8a;border-color:#89caff;box-shadow:inset 3px 0 0 #89caff}.home-channel-chip img{width:64px;height:40px;border-radius:7px;object-fit:cover;flex:none}.home-channel-chip>span{min-width:0;flex:1}.home-channel-chip strong{font-size:12px;line-height:1.4;display:block;overflow-wrap:anywhere}.home-channel-chip small{font-size:11px;margin-top:4px}.home-channel-chip:focus-visible{outline-offset:-2px!important}@media(max-width:800px){.home-channel-picker{padding:12px 14px!important;max-height:260px}.home-channel-chip strong{display:block;-webkit-line-clamp:unset}.home-channel-chip img{width:64px;height:40px}} 1911.home-channel-links .home-channel-youtube{display:inline-flex;align-items:center;justify-content:center;gap:9px;min-height:42px;padding:0 12px;border:1px solid #ffffff32;border-radius:10px;background:#ffffff0d;color:#fff;font-size:12px;font-weight:700;text-decoration:none;transition:background .15s,border-color .15s}.home-channel-links .home-channel-youtube:hover{background:#ffffff1c;border-color:#ffffff65}.home-channel-youtube svg{flex:none}@media(max-width:800px){.home-channel-links .home-channel-youtube{min-height:40px;font-size:11px;gap:8px}} 1912 .cabana-card-hero-fallback.is-loading img { animation:cabanaCardLogoBreathe 1.5s ease-in-out infinite; } 1913 .cabana-card-hero-open { position:absolute; inset:0; z-index:1; width:100%; height:100%; border:0; padding:0; background:transparent; cursor:pointer; } 1914 .cabana-card-hero-content { position:relative; z-index:2; min-height:176px; box-sizing:border-box; display:flex; flex-direction:column; align-items:flex-start; padding:15px 16px 16px; color:#fff; pointer-events:none; } 1915 .cabana-card-topline { width:100%; display:flex; align-items:center; justify-content:flex-start; gap:9px; } 1916 .cabana-card-status { display:inline-flex; align-items:center; gap:6px; padding:5px 9px; border-radius:100px; color:#fff; font-size:9px; font-weight:900; letter-spacing:.08em; } 1917 .cabana-card-status::before { content:""; width:6px; height:6px; border-radius:50%; background:currentColor; opacity:.9; } 1918 .cabana-card-breed { overflow:hidden; max-width:52%; padding:5px 9px; border:1px solid rgba(255,255,255,.24); border-radius:100px; background:rgba(255,255,255,.12); color:#fff; font-size:9px; font-weight:800; text-overflow:ellipsis; white-space:nowrap; backdrop-filter:blur(8px); } 1919 .cabana-card-title { display:-webkit-box; overflow:hidden; max-width:100%; margin:auto 0 0; color:#fff; font-size:20px; line-height:1.13; letter-spacing:-.025em; text-shadow:0 2px 14px rgba(0,20,42,.4); -webkit-line-clamp:2; -webkit-box-orient:vertical; }
1920 @keyframes cabanaCardMediaCycle { 0%,10% { opacity:1; transform:scale(1.015); } 12.5%,97% { opacity:0; transform:scale(1.055); } 100% { opacity:1; transform:scale(1.015); } } 1921 @keyframes cabanaCardLogoBreathe { 0%,100% { opacity:.62; transform:scale(.985); } 50% { opacity:1; transform:scale(1); } } 1922 @media(prefers-reduced-motion:reduce){.cabana-card-hero-media{display:none;animation:none}.cabana-card-hero-media:first-child{display:block;opacity:1;transform:none}.cabana-card-hero-fallback.is-loading img{animation:none}} 1923 .cabana-card-body { display:grid; gap:11px; padding:15px 17px 17px; } 1924 .cabana-card-meta { display:flex; align-items:flex-start; gap:12px; min-width:0; padding-bottom:11px; border-bottom:1px solid #edf1f5; } 1925 .cabana-card-meta-item { min-width:0; flex:1 1 0; display:grid; grid-template-columns:17px minmax(0,1fr); gap:7px; align-items:start; padding:0; } 1926 .cabana-card-meta-item + .cabana-card-meta-item { padding-left:12px; border-left:1px solid #e4eaf0; } 1927 .cabana-card-meta-icon { width:17px; height:17px; display:grid; place-items:center; margin-top:1px; } 1928 .cabana-card-meta-icon img { width:13px; height:13px; opacity:.68; } 1929 .cabana-card-meta-item small { display:block; color:#8492a5; font-size:8px; font-weight:900; letter-spacing:.08em; text-transform:uppercase; } 1930 .cabana-card-meta-item strong { display:block; overflow:visible; margin-top:2px; color:#19334f; font-size:11px; line-height:1.3; text-overflow:clip; white-space:normal; overflow-wrap:anywhere; } 1931 .cabana-card-location strong { overflow:visible; text-overflow:clip; white-space:normal; } 1932 .cabana-card-brand-strip { min-width:0; display:flex; align-items:center; gap:12px; min-height:54px; padding:8px 11px; border:1px solid #e7edf4; border-radius:13px; background:#fff; } 1933 .cabana-card-brand-strip{align-items:flex-start;flex-direction:column;gap:8px} 1934 .cabana-card-participants{display:flex;flex-wrap:wrap;align-items:center;gap:8px;width:100%;min-width:0} 1935 .cabana-card-participant{display:flex;align-items:center;justify-content:center;min-width:0;width:52px;min-height:44px;box-sizing:border-box;padding:4px;border-radius:8px} 1936 1937 .cabana-participants-window{width:100%;min-width:0} 1938 .cabana-participants-track{min-width:0} 1939 .cabana-card-brand-strip.is-rolling .cabana-card-brand-label{display:flex;align-items:center;justify-content:space-between;gap:8px} 1940 .cabana-roll-toggle{border:0;background:transparent;color:#0a5ca8;font:600 10px inherit;cursor:pointer;padding:3px 5px} 1941 .cabana-card-brand-strip.is-rolling .cabana-participants-window{overflow-x:auto;overflow-y:hidden;scrollbar-width:thin;overscroll-behavior-x:contain} 1942 .cabana-card-brand-strip.is-rolling .cabana-participants-track{display:flex;width:max-content;animation:cabanaParticipantsRoll var(--participants-duration,40s) linear infinite} 1943 .cabana-card-brand-strip.is-rolling .cabana-card-participants{flex:none;flex-wrap:nowrap;width:auto;gap:8px;padding-right:8px} 1944 .cabana-card-brand-strip.is-rolling .cabana-card-participant{flex:0 0 100px;width:100px;height:56px} 1945 .cabana-card-brand-strip.is-rolling:hover .cabana-participants-track,.cabana-card-brand-strip.is-rolling:focus-within .cabana-p
1945articipants-track,.cabana-card-brand-strip.is-paused .cabana-participants-track{animation-play-state:paused} 1946 @keyframes cabanaParticipantsRoll{to{transform:translateX(-50%)}} 1947 @media(prefers-reduced-motion:reduce){.cabana-card-brand-strip.is-rolling .cabana-participants-track{animation:none}.cabana-participants-copy,.cabana-roll-toggle{display:none}} 1948 .cabana-card-participant.is-principal{width:100px} 1949 .cabana-card-participant img{display:block;max-width:100%;width:auto;height:36px;object-fit:contain} 1950 .cabana-card-participant>span{font-size:10px;line-height:1.25;color:#516982;overflow-wrap:anywhere;text-align:center} 1951 .cabana-card-brand-label { flex:0 0 auto; color:#8492a5; font-size:8px; font-weight:900; letter-spacing:.09em; text-transform:uppercase; } 1952 .cabana-card-main-logo { min-width:0; height:38px; flex:1 1 auto; display:flex; align-items:center; padding:3px 5px; border-radius:8px; box-sizing:border-box; transition:background-color .18s ease; } 1953 .cabana-card-main-logo img { max-width:100%; max-height:32px; object-fit:contain; object-position:left center; } 1954 .cabana-card-guest-logos { flex:0 1 auto; display:flex; align-items:center; gap:8px; overflow:visible; } 1955 .cabana-card-guest-logo { width:46px; height:38px; display:grid; place-items:center; padding:4px; border-radius:8px; box-sizing:border-box; overflow:visible; transition:background-color .18s ease; } 1956 .cabana-card-guest-logo img { display:block; width:auto; height:auto; max-width:100%; max-height:100%; object-fit:contain; } 1957 .cabana-card-main-logo.is-light-logo img,.cabana-card-guest-logo.is-light-logo img { filter:brightness(0) saturate(100%); } 1958 .cabana-card-consignor { min-width:0; display:flex; align-items:center; gap:9px; padding-top:1px; } 1959 .cabana-card-consignor-icon { flex:none; width:29px; height:29px; display:grid; place-items:center; border-radius:9px; } 1960 .cabana-card-consignor-icon img { width:17px; height:17px; object-fit:contain; } 1961 .cabana-card-consignor-copy { min-width:0; } 1962 .cabana-card-consignor-copy small { display:block; color:#8492a5; font-size:8px; font-weight:900; letter-spacing:.09em; text-transform:uppercase; } 1963 .cabana-card-consignor-copy strong { display:block; overflow:hidden; margin-top:2px; color:#19334f; font-size:11px; text-overflow:ellipsis; white-space:nowrap; } 1964 .cabana-card-admin-actions { display:flex; gap:7px; flex-wrap:wrap; } 1965 .cabana-card-admin-action { flex:1 1 90px; min-width:0; display:flex; align-items:center; justify-content:center; gap:6px; border:1px solid #d6e1ed; border-radius:10px; padding:8px 9px; background:#fff; color:#496077; font:800 10px inherit; cursor:pointer; } 1966 .cabana-card-admin-action img { width:13px; height:13px; opacity:.75; } 1967 @media (max-width:960px) { 1968 .cabana-auction-grid { grid-template-columns:minmax(0,1fr) !important; } 1969 } 1970 @media (max-width:520px) { 1971 .cabana-card-hero,.cabana-card-hero-content { min-height:164px; } 1972 .cabana-card-title { font-size:18px; } 1973 .cabana-card-meta { gap:9px; } 1974 .cabana-card-meta-item + .cabana-card-meta-item { padding-left:9px; } 1975 .cabana-card-brand-strip { align-items:flex-start; flex-wrap:wrap; } 1976 .cabana-card-brand-label { width:100%; } 1977 } 1978 @keyframes loadSlide { 0% { transform:translateX(-120%); } 100% { transform:translateX(320%); } } 1979@keyframes floatIn { from { opacity:0; transform:translateY(26px); } to { opacity:1; transform:translateY(0); } } 1980@keyframes bidPulse { 0% { box-shadow:0 0 0 0 rgba(230,57,70,0.45); } 100% { box-shadow:0 0 0 22px rgba(230,57,70,0); } } 1981@keyframes livePing { 0% { transform:scale(1); opacity:0.7; } 100% { transform:scale(2.4); opacity:0; } } 1982@keyframes drawLine { to { stroke-dashoffset:0; } } 1983@keyframes growBar { to { transform:scaleX(1); } } 1984@keyframes fadeScale { from { opacity:0; transform:scale(0.9); } to { opacity:1; transform:scale(1); } } 1985@keyframes spinIn { from { opacity:0; transform:rotate(-25deg) scale(0.85); } to { opacity:1; transform:rotate(0) scale(1); } } 1986@keyframes spin { to { transform:rotate(360deg); } } 1987@keyframes gradientShift { 0% { background-position:0% 50%; } 50% { background-position:100% 50%; } 100% { background-position:0% 50%; } } 1988@keyframes gradientBlob { 0% { transform:translate(-8%,-6%) scale(1); } 33% { transform:translate(10%,4%) scale(1.15); } 66% { transform:translate(-4%,8%) scale(1.05); } 100% { transform:translate(-8%,-6%) scale(1); } } 1989@keyframes logoPop { 0% { opacity:0; transform:scale(0.4) rotate(-8deg); } 55% { opacity:1; transform:scale(1.12) rotate(2deg); } 75% { transform:scale(0.97) rotate(0deg); } 100% { opacity:1; transform:scale(1) rotate(0deg); } } 1990@keyframes logoBreathe { 0%,100% { transform:scale(1); box-shadow:0 12px 34px rgba(0,0,0,0.22), 0 0 0 0 rgba(255,255,255,0.55); } 50% { transform:scale(1.06); box-shadow:0 20px 48px rgba(0,0,0,0.3), 0 0 0 14px rgba(255,255,255,0); } } 1991@keyframes logoWrap { 0% { opacity:0; transform:scale(0.55); } 60% { opacity:1; transform:scale(1.08); } 100% { opacity:1; transform:scale(1); } } 1992@keyframes initialPop { 0% { opacity:0; transform:scale(0.4); text-shadow:0 0 0 rgba(255,255,255,0); } 60% { opacity:1; transform:scale(1.18); } 100% { opacity:1; transform:scale(1); } } 1993@keyframes initialGlow { 0%,100% { text-shadow:0 0 12px rgba(255,255,255,0.25); transform:scale(1); } 50% { text-shadow:0 0 34px rgba(255,255,255,0.75), 0 0 60px rgba(255,255,255,0.35); transform:scale(1.08); } } 1994@keyframes heroTextFade { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } } 1995@keyframes heroImageReveal { from { opacity:.55; filter:saturate(.72); } to { opacity:1; filter:saturate(1); } } 1996@media(max-width:700px){ 1997 .home-hero{width:100%;aspect-ratio:16/9;height:auto!important;min-height:0!important;padding-bottom:0!important;overflow:hidden} 1998 .home-hero-content{height:100%;min-height:0;overflow:hidden} 1999 .home-hero-nav{padding:8px 18px!important;flex:0 0 auto} 2000 .home-hero-main{min-height:0;padding:7px 18px 10px!important;display:flex!important;flex-direction:column;align-items:stretch!important;justify-content:flex-end!important;gap:12px!important;flex-wrap:nowrap!important;overflow:hidden} 2001 .home-hero-copy{width:100%!important;max-width:none!important;flex:0 1 auto!important;min-height:0} 2002 .home-hero-brand{width:min(124px,37vw)!important;height:20px!important;margin-bottom:7px!important} 2003 .home-hero-badge{width:max-content;max-width:100%;gap:4px!important;padding:3px 8px!important;font-size:7.5px!important;line-height:1!important;white-space:nowrap!important;flex-wrap:nowrap!important} 2004 .home-hero-badge>.home-hero-status-dot{width:4px!important;height:4px!important} 2005 .home-hero-title{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden!important;margin:9px 0 7px!important;max-width:92%!important;font-size:clamp(16px,4.15vw,19px)!important;line-height:1.14!important;letter-spacing:-.018em!important} 2006 .home-hero-subtitle{min-height:0!important;max-width:92%;font-size:8px!important;line-height:1.3!important;white-space:nowrap;text-overflow:ellipsis;-webkit-line-clamp:1!important} 2007 .home-hero-actions{gap:7px!important;margin-top:10px!important;flex-wrap:nowrap!important} 2008 .home-hero-actions>a,.home-hero-actions>span{padding:5px 8px!important;border-radius:7px!important;font-size:8px!important;white-space:nowrap} 2009 .home-hero-dots{gap:5px!important;margin-top:7px!important} 2010 .home-hero-dots>span{height:3px!important} 2011 .home-hero-indices{width:100%;display:grid!important;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px!important;flex:0 0 auto} 2012 .home-hero-index-card{min-width:0!important;border-radius:8px!important;padding:5px 7px!important} 2013 .home-hero-index-card>div:first-child{gap:3px!important} 2014 .home-hero-index-card>div:first-child>
2014span:first-child{font-size:7px!important;letter-spacing:.04em!important} 2015 .home-hero-index-card>div:first-child>span:last-child{display:none!important} 2016 .home-hero-index-value{margin-top:3px!important;font-size:clamp(11px,3vw,14px)!important;line-height:1!important;white-space:nowrap} 2017 .home-hero-index-value>.home-hero-index-unit{font-size:6px!important} 2018 .home-hero-index-month{display:none!important} 2019 .home-month-card-media{height:180px!important} 2020 .home-month-card-title{font-size:13.5px!important;line-height:1.14!important} 2021 .home-month-card-meta{font-size:9.2px!important;line-height:1.2!important} 2022} 2023@keyframes drawLine { from { stroke-dashoffset:1; } to { stroke-dashoffset:0; } } 2024@keyframes areaRise { from { opacity:0; } to { opacity:1; } } 2025.sales-order-page{min-height:calc(100vh - 68px);box-sizing:border-box;overflow-x:hidden;background:#f2f6fb;padding:28px clamp(14px,4vw,40px) 110px;color:#112033} 2026.sales-order-shell{width:min(1180px,100%);margin:0 auto} 2027.sales-order-back{display:inline-flex;align-items:center;gap:7px;border:0;background:transparent;color:#0a5ca8;padding:0;cursor:pointer;font:800 13px inherit} 2028 2029.rg-order-save{flex-shrink:0}.rg-order-save .primary img{filter:brightness(0) invert(1)!important}.rg-order-save .sales-order-action.primary{background:#0a5ca8!important;color:#fff!important;border-color:#0a5ca8!important;min-height:44px;padding:11px 22px;border-radius:10px;box-shadow:0 4px 12px #0a5ca81f} 2030.rg-order-tools{display:grid;grid-template-columns:1.15fr 1fr 1fr;gap:12px;margin:0 0 20px;align-items:start} 2031.rg-order-tool-group{min-width:0;padding:18px;background:#fff;border:1px solid #dce6ef;border-radius:13px} 2032.rg-order-tool-group h2{font-size:13px;font-weight:800;color:#143550;margin:0 0 5px} 2033.rg-order-tool-group p{font-size:12px;line-height:1.5;color:#6a7c8f;margin:0 0 15px} 2034.rg-order-tool-buttons{display:flex;flex-wrap:wrap;gap:8px;align-items:center} 2035.rg-order-tool-buttons .sales-order-action{min-height:40px;border-radius:9px;font-weight:650} 2036.rg-order-tool-buttons img,.rg-order-save img{width:16px;height:16px;flex-shrink:0} 2037.rg-order-tools .sales-order-action:disabled{opacity:.5;cursor:not-allowed} 2038.rg-order-catalog-settings{margin-top:12px;border-top:1px solid #e8eef4;padding-top:10px} 2039.rg-order-catalog-settings summary{display:flex;justify-content:space-between;list-style:none;cursor:pointer;color:#426079;font-size:12px;font-weight:700;padding:4px 0} 2040.rg-order-catalog-settings summary::-webkit-details-marker{display:none}.rg-order-catalog-settings[open] summary{margin-bottom:12px}.rg-order-catalog-settings[open] summary span{transform:rotate(180deg)} 2041.rg-order-tools :focus-visible{outline:3px solid #65a5dc;outline-offset:3px} 2042@media(max-width:1000px){.rg-order-tools{grid-template-columns:1fr 1fr}.rg-order-tools>sc-if{grid-column:1/-1}} 2043@media(max-width:640px){.rg-order-tools{grid-template-columns:1fr}.rg-order-tools>sc-if{grid-column:auto}.rg-order-save.sales-order-head-actions{display:flex;width:auto}.rg-order-tool-group{padding:15px}.rg-order-tool-buttons .sales-order-action{width:auto;flex:1 1 auto}.rg-order-save .primary{width:auto}} 2044 2045.sales-order-head{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;margin:22px 0 16px} 2046.sales-order-head h1{margin:3px 0 5px;font-size:clamp(28px,4vw,42px);line-height:1.05} 2047.sales-order-head p{margin:0;color:#65758a;font-size:14px} 2048.sales-order-head-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end}.sales-order-import{position:relative;overflow:hidden}.sales-order-import input{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer}.sales-order-import.is-busy{opacity:.62;pointer-events:none} 2049.sales-order-count{flex:0 0 auto;padding:7px 11px;border-radius:100px;background:#e6eef7;color:#34506d;font-size:12px;font-weight:900} 2050.sales-order-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-bottom:14px;padding:13px 14px;border:1px solid #d8e2ed;border-radius:8px;background:#fff} 2051.sales-order-toolbar-copy{display:grid;gap:2px;min-width:220px;flex:1} 2052.sales-order-toolbar-copy strong{font-size:13px}.sales-order-toolbar-copy span{color:#65758a;font-size:11px} 2053.sales-order-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
2054.sales-order-action{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:38px;border:1px solid #cbd8e7;border-radius:7px;padding:8px 12px;background:#fff;color:#33475b;cursor:pointer;font:800 12px inherit} 2055.sales-order-action.primary{border-color:transparent;background:var(--sales-accent,#0a5ca8);color:#fff} 2056.sales-order-message{margin:0 0 12px;padding:10px 12px;border-radius:7px;font-size:12px;font-weight:700} 2057.sales-order-message.error,.sales-order-message.is-error{background:#fff0f1;color:#b42332}.sales-order-message.success,.sales-order-message.is-success{background:#eaf8f2;color:#177458} 2058.sales-order-import-summary{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 12px;padding:11px 13px;border:1px solid #bfd8ec;border-radius:10px;background:#f3f9ff;color:#174c78}.sales-order-import-summary strong{margin-right:auto;font-size:12px}.sales-order-import-summary span{display:inline-flex;align-items:center;gap:5px;padding:5px 8px;border-radius:100px;background:#fff;font-size:10.5px;font-weight:850}.sales-order-import-summary small{color:#607a93;font-size:10px} 2059.sales-order-filterbar{display:grid;grid-template-columns:minmax(220px,1fr) repeat(4,minmax(145px,.34fr));gap:9px;align-items:center;margin:0 0 14px;padding:11px;border:1px solid #d8e4f0;border-radius:12px;background:#fff}.sales-order-search{display:flex;align-items:center;gap:8px;min-width:0;height:40px;padding:0 11px;border:1px solid #cbd8e7;border-radius:9px;background:#f9fbfd}.sales-order-search img{width:17px;height:17px;opacity:.58}.sales-order-search input{width:100%;min-width:0;border:0;outline:0;background:transparent;color:#173a5d;font:700 12px inherit}.sales-order-filterbar select{min-width:0;width:100%;height:40px;padding:0 9px;border:1px solid #cbd8e7;border-radius:9px;background:#fff;color:#173a5d;font:750 11px inherit}.sales-order-filter-count{grid-column:1/-1;color:#60778e;font-size:10.5px}.sales-order-filter-count strong{color:#173a5d} 2060.sales-order-progress{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px;font-size:12px}.sales-order-pending{padding:5px 9px;border-radius:7px;background:#fff3d9;color:#885807}.sales-order-pending strong{color:inherit} 2061.sales-order-kicker{display:block;color:#0a5ca8;font-size:10px;font-weight:900;letter-spacing:1.2px} 2062.sales-order-summary{margin:0 0 12px;color:#65758a;font-size:12px}.sales-order-summary strong{color:#173a5d} 2063.sales-order-copy{min-width:0}.sales-order-copy strong,.sales-order-copy small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sales-order-copy strong{font-size:14px}.sales-order-copy small{margin-top:3px;color:#65758a;font-size:11px} 2064.sales-order-empty{padding:40px 16px;border:1px dashed #cbd8e7;border-radius:8px;background:#fff;text-align:center;color:#65758a;font-size:13px} 2065.sales-order-list,.sales-order-groups{display:grid;gap:10px} 2066.sales-order-row{display:grid;grid-template-columns:35px minmax(0,1fr) auto;align-items:start;gap:12px;min-height:118px;padding:14px;border:1px solid #dbe5ef;border-radius:12px;background:#fff;box-shadow:0 5px 16px rgba(15,42,70,.045)} 2067.sales-order-lot-main{min-width:0;display:grid;gap:10px}.sales-order-facts{display:grid;grid-template-columns:repeat(4,minmax(90px,1fr));gap:7px}.sales-order-facts>span{min-width:0;padding:7px 9px;border-radius:8px;background:#f3f7fb}.sales-order-facts small,.sales-order-consignataria small{display:block;color:#7890a8;font-size:8.5px;font-weight:900;letter-spacing:.06em}.sales-order-facts strong{display:block;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#193a5b;font-size:11.5px}.sales-order-lot-tools{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.sales-order-consignataria{min-width:210px;max-width:320px;display:flex;align-items:center;gap:9px}.sales-order-consignataria-logo{display:grid;place-items:center;width:36px;height:36px;box-sizing:border-box;padding:5px;border-radius:9px;flex:0 0 36px;overflow:hidden}.sales-order-consignataria-logo img{display:block;max-width:100%;max-height:100%;object-fit:contain}.sales-order-consignataria>span:last-child{min-width:0}.sales-order-consignataria strong{display:block;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#173a5d;font-size:11.5px}.sales-order-rating{display:flex;align-items:center;gap:7px;padding:5px 8px;border:1px solid #dce5ee;border-radius:8px;background:#fbfcfe}.sales-order-rating-label{min-width:67px;font-size:10px;font-weight:900}.sales-order-stars{display:flex;align-items:center;gap:1px}.sales-order-stars button{width:22px;height:22px;border:0;padding:0;background:transparent;cursor:pointer;font-size:19px;line-height:1;transition:transform .15s ease,color .15s ease}.sales-order-stars button:hover{transform:scale(1.2)}.sales-order-video-button{display:inline-flex;align-items:center;gap:7px;min-height:32px;padding:5px 9px;border:1px solid #bfd3e6;border-radius:8px;background:#f7fbff;color:#174c78;cursor:pointer;font:850 10.5px inherit}.sales-order-video-badge{display:inline-flex;align-items:center;border-radius:100px;padding:3px 6px;color:#fff;font-size:8px;font-weight:950;letter-spacing:.08em}.sales-order-video-badge.is-final{background:#16846e}.sales-order-video-badge.is-raw{background:#e07a19}.sales-order-no-video{color:#8a9aad;font-size:10.5px;font-weight:750}.sales-order-video-panel{grid-column:2/-1;display:grid;grid-template-columns:180px minmax(0,1fr);gap:14px;align-items:start;padding:12px;border-radius:10px;background:#071f38;color:#fff}.sales-order-video-panel>div{display:grid;gap:7px;justify-items:start}.sales-order-video-panel>div>strong{font-size:13px}.sales-order-video-panel>div>small{color:#d3deea;font-size:10.5px;line-height:1.4}.sales-order-video-direct{color:#fff;font-size:10.5px;font-weight:800;text-decoration:underline;text-underline-offset:3px}.sales-order-video-stage{position:relative;display:block;min-width:0;border-radius:8px;overflow:hidden;background:#001326}.sales-order-video-panel video{display:block;width:100%;max-height:390px;background:#001326}.sales-order-video-play{position:absolute;inset:50% auto auto 50%;transform:translate(-50%,-50%);display:inline-flex;align-items:center;gap:8px;min-height:44px;padding:0 17px;border:1px solid rgba(255,255,255,.7);border-radius:100px;background:rgba(0,46,95,.9);box-shadow:0 8px 24px rgba(0,0,0,.32);color:#fff;cursor:pointer;font:900 12px inherit;transition:opacity .18s ease,transform .18s ease}.sales-order-video-play span{font-size:18px}.sales-order-video-play:hover{transform:translate(-50%,-50%) scale(1.04)}.sales-order-video-stage.is-playing .sales-order-video-play{opacity:0;pointer-events:none}.sales-order-video-stage.is-error .sales-order-video-play{display:none} 2068.sales-order-excel-data{display:grid;grid-template-columns:minmax(140px,1.5fr) repeat(2,minmax(90px,.55fr));gap:7px}.sales-order-excel-data>span{min-width:0;padding:7px 9px;border:1px solid #cfe0ef;border-radius:8px;background:#f7fbff}.sales-order-excel-data small{display:block;color:#6f89a1;font-size:8px;font-weight:900;letter-spacing:.07em}.sales-order-excel-data strong{display:block;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#0a5ca8;font-size:11px} 2069.sales-order-status{display:inline-flex;align-items:center;gap:5px;width:max-content;margin-top:5px;padding:4px 7px;border-radius:100px;font-size:8.5px;font-weight:950;letter-spacing:.06em;text-transform:uppercase}.sales-order-status::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor}.sales-order-status.is-cargado{background:#fff2d8;color:#855b14}.sales-order-status.is-controlado{background:#e7effe;color:#325aa6}.sales-order-status.is-publicado{background:#e2f3eb;color:#287054}.sales-order-status.is-anulado{background:#fbe8eb;color:#a3404e}.sales-order-status.is-otro{background:#edf1f5;color:#576a7c} 2070.sales-order-row-copy{min-width:0}.sales-order-row-copy strong{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}
2070.sales-order-row-copy span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:3px;color:#65758a;font-size:11px} 2071.sales-order-handle{width:38px;height:42px;display:grid;place-items:center;border:0;border-radius:7px;background:#edf3f9;color:#486683;cursor:grab;touch-action:none;pointer-events:none}.sales-order-handle:active{cursor:grabbing}.sales-order-handle img{width:19px;height:19px;pointer-events:none} 2072.sales-order-position{min-width:35px;height:30px;display:grid;place-items:center;border-radius:7px;background:var(--sales-accent,#0a5ca8);color:#fff;font-size:11px;font-weight:900} 2073.sales-order-category{overflow:hidden;border:1px solid #cad8e6;border-radius:8px;background:#fff} 2074.sales-order-category-head{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:12px;padding:10px 12px;background:#e9f0f7} 2075.sales-order-category-head strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;color:#173a5d}.sales-order-category-head span{padding:5px 9px;border-radius:100px;background:#fff;color:#58718c;font-size:10px;font-weight:900} 2076.sales-order-category-list{display:grid;gap:7px;padding:9px} 2077.sales-order-controls{display:flex;align-items:center;justify-content:flex-end;gap:6px} 2078.sales-order-control{width:34px;height:34px;display:grid;place-items:center;border:1px solid #cbd8e7;border-radius:7px;background:#fff;cursor:pointer}.sales-order-control:disabled{opacity:.32;cursor:not-allowed}.sales-order-control img{width:17px;height:17px} 2079.sales-order-select{height:34px;min-width:58px;padding:0 7px;border:1px solid #cbd8e7;border-radius:7px;background:#fff;color:#173a5d;font:800 12px inherit} 2080.sales-order-drag-source{display:none!important} 2081.sales-order-drag-placeholder{box-sizing:border-box;border:2px dashed var(--sales-accent,#0a5ca8);border-radius:8px;background:rgba(10,92,168,.08)} 2082.sales-order-drag-proxy{position:fixed;z-index:10000;pointer-events:none;box-sizing:border-box;margin:0!important;opacity:.96;transform:rotate(.35deg);box-shadow:0 20px 50px rgba(15,42,70,.28)!important;will-change:left,top} 2083body.sales-order-is-dragging{cursor:grabbing!important;user-select:none!important;overscroll-behavior:none} 2084body.sales-order-is-dragging *{cursor:grabbing!important} 2085.sales-order-sortable-ghost{opacity:.22!important;background:#dceaf8!important} 2086.sales-order-sortable-chosen{box-shadow:0 14px 34px rgba(15,42,70,.2)!important} 2087.sales-order-sortable-drag{opacity:.98!important;cursor:grabbing!important} 2088.sales-order-native-drag{position:relative;z-index:5;opacity:.72;cursor:grabbing!important;box-shadow:0 16px 34px rgba(15,42,70,.2)!important;transform:scale(.995)} 2089.sales-order-native-target{outline:2px solid #1477c9;outline-offset:2px} 2090.sales-order-native-preview{position:fixed!important;inset:auto!important;z-index:9999!important;margin:0!important;pointer-events:none!important;opacity:.96!important;cursor:grabbing!important;box-shadow:0 20px 46px rgba(15,42,70,.28)!important;transform-origin:top left} 2091@media(max-width:1000px){.sales-order-filterbar{grid-template-columns:1fr 1fr}.sales-order-search{grid-column:1/-1}} 2092@media(max-width:680px){ 2093 .sales-order-page{padding:18px 12px 96px} 2094 .sales-order-head{align-items:flex-start;margin-top:18px}.sales-order-head h1{font-size:29px} 2095 .sales-order-head-actions{width:100%;display:grid;grid-template-columns:1fr 1fr}.sales-order-head{flex-wrap:wrap}.sales-order-head-actions .sales-order-action{justify-content:center} 2096 .sales-order-filterbar{grid-template-columns:1fr 1fr}.sales-order-search{grid-column:1/-1} 2097 .sales-order-toolbar{align-items:stretch}.sales-order-actions{width:100%;display:grid;grid-template-columns:1fr 1fr}.sales-order-action{width:100%} 2098 .sales-order-row{grid-template-columns:32px minmax(0,1fr);padding:10px;gap:9px}.sales-order-row .sales-order-controls{grid-column:1/-1}.sales-order-facts{grid-template-columns:repeat(2,minmax(0,1fr))}.sales-order-excel-data{grid-template-columns:1fr 1fr}.sales-order-excel-data>span:first-child{grid-column:1/-1}.sales-order-lot-tools{align-items:flex-start}.sales-order-rating{width:100%;box-sizing:border-box;justify-content:space-between}.sales-order-video-panel{grid-column:1/-1;grid-template-columns:1fr}.sales-order-video-panel video{max-height:280px} 2099 .sales-order-category-head{grid-template-columns:minmax(0,1fr);padding:9px;gap:9px}
2099.sales-order-category-head .sales-order-controls{justify-content:flex-start} 2100 .sales-order-row-copy strong{white-space:normal;line-height:1.25} 2101} 2102.commercial-page{min-height:100vh;background:#f3f7fb;color:#112033;padding:24px clamp(12px,3vw,32px) 96px} 2103.commercial-shell{width:min(1240px,100%);margin:0 auto} 2104.commercial-back{display:inline-flex;align-items:center;gap:8px;border:0;background:transparent;color:#0a5ca8;font:800 13px 'Archivo',sans-serif;padding:0;cursor:pointer} 2105.commercial-hero{margin-top:16px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:18px;align-items:end} 2106.commercial-kicker{display:block;color:#0a5ca8;font-size:11px;font-weight:950;letter-spacing:.12em;text-transform:uppercase} 2107.commercial-hero h1{margin:5px 0 6px;font-size:clamp(29px,3.6vw,42px);line-height:1;color:#10233b;letter-spacing:0} 2108.commercial-hero p{margin:0;color:#65758a;font-size:14px;line-height:1.45} 2109.commercial-hero-actions{display:flex;gap:10px;align-items:center;justify-content:flex-end;flex-wrap:wrap} 2110.commercial-save,.commercial-prices-link{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:0;border-radius:999px;background:#002e5f;color:#fff;padding:12px 16px;font:900 12px 'Archivo',sans-serif;box-shadow:0 12px 28px rgba(0,46,95,.18);cursor:pointer} 2111.commercial-autosave-note{display:inline-flex;align-items:center;gap:7px;border:1px solid rgba(23,112,77,.18);border-radius:999px;background:#e7f5ee;color:#17704d;padding:10px 13px;font:900 11px 'Archivo',sans-serif} 2112.commercial-autosave-note img{width:15px;height:15px} 2113.commercial-prices-link{background:#fff;color:#003b73;border:1px solid #cfe0f1;box-shadow:0 10px 24px rgba(15,42,70,.08)} 2114.commercial-save img,.commercial-prices-link img{width:16px;height:16px} 2115.commercial-save img{filter:brightness(0) invert(1)} 2116.price-board-story-export{background:linear-gradient(135deg,#833ab4 0%,#fd1d1d 50%,#fcb045 100%);color:#fff;border-color:transparent;box-shadow:0 10px 24px rgba(193,53,132,.22)} 2117.price-board-story-export img{filter:brightness(0) invert(1)} 2118.price-board-page{min-height:100vh;background:radial-gradient(circle at 12% 8%,rgba(55,162,196,.22),transparent 26%),linear-gradient(135deg,#edf5fb 0%,#f8fbff 46%,#fff 100%);color:#10233b;padding:26px clamp(12px,3vw,34px) 84px} 2119.price-board-shell{width:min(1180px,100%);margin:0 auto} 2120@media(max-width:760px){body:has(.price-board-page) .app-mobile-load-fab{display:none!important}.price-board-page{padding-left:14px!important;padding-right:14px!important}} 2121.price-board-hero{position:relative;overflow:hidden;border-radius:18px;background:linear-gradient(135deg,#002e5f 0%,#075995 58%,#16a4b4 100%);color:#fff;padding:30px;box-shadow:0 22px 54px rgba(0,46,95,.2)} 2122.price-board-hero:after{content:"";position:absolute;inset:-40% -12% auto auto;width:460px;height:460px;background:radial-gradient(circle,rgba(255,255,255,.2),transparent 63%);filter:blur(8px)} 2123.price-board-hero-content{position:relative;z-index:1;display:grid;grid-template-columns:minmax(0,1fr) 380px;gap:28px;align-items:center} 2124.price-board-kicker{display:block;font-size:11px;font-weight:950;letter-spacing:.14em;text-transform:uppercase;color:#9fe6ee} 2125.price-board-hero h1{margin:7px 0 8px;font-size:clamp(34px,5vw,58px);line-height:.96;letter-spacing:0;color:#fff} 2126.price-board-hero p{margin:0;color:rgba(255,255,255,.82);font-size:15px;line-height:1.45;max-width:620px} 2127.price-board-logos{display:grid;gap:10px;justify-items:stretch} 2128.price-board-brand-row{display:grid;grid-template-columns:.82fr 1.18fr;gap:10px} 2129.price-board-main-logo,.price-board-rosgan-logo{border-radius:14px;background:#fff;min-height:94px;display:grid;place-items:center;padding:16px;border:1px solid rgba(255,255,255,.72);box-shadow:0 16px 34px rgba(0,46,95,.16)} 2130.price-board-main-logo img,.price-board-rosgan-logo img{max-width:100%;max-height:70px;object-fit:contain} 2131.price-board-logo-fallback{font-size:24px;font-weight:950;color:#1f2d4d;letter-spacing:.04em;text-align:center} 2132.price-board-cons-card{display:flex;align-items:center;gap:10px;min-height:48px;border-radius:999px;background:rgba(255,255,255,.14);color:#fff;padding:8px 12px;border:1px solid rgba(255,255,255,.26);backdrop-filter:blur(10px);box-sizing:border-box} 2133rosgan-price-board{display:block;width:100%;margin:20px auto}rosgan-price-board svg{display:block;width:100%;height:auto;margin-bottom:20px}.price-board-view-switch{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}.price-board-view-switch button{padding:12px 18px;border:1px solid #cbd9e6;border-radius:24px;color:#102f5d;background:white;font-weight:700;cursor:pointer}.price-board-view-switch button[aria-pressed="true"]{background:#102f5d;color:white}.price-board-cons-avatar{flex:0 0 auto;width:34px;height:34px;display:grid;place-items:center;border-radius:50%;padding:8px;box-sizing:border-box;background:#d91549;line-height:0}.price-board-cons-avatar img{display:block;width:auto!important;height:auto!important;max-width:18px!important;max-height:18px!important;object-fit:contain;object-position:center}.price-board-cons-card div{min-width:0;display:flex;align-items:baseline;gap:8px}.price-board-cons-card small{display:block;color:rgba(255,255,255,.72);font-size:9px;font-weight:900;letter-spacing:.1em;text-transform:uppercase;white-space:nowrap}.price-board-cons-name{display:block;min-width:0;font-size:13px;font-weight:900;line-height:14px;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} 2134.price-board-toolbar{display:flex;justify-content:flex-end;
2134gap:12px;align-items:center;margin:20px 0 18px;flex-wrap:wrap} 2135.price-board-updated{color:#65758a;font-size:12px;font-weight:800} 2136.price-board-grid{display:grid;gap:10px} 2137.price-category-card{position:relative;overflow:hidden;border:1px solid #dce7f2;border-radius:12px;background:rgba(255,255,255,.98);box-shadow:0 12px 30px rgba(15,42,70,.055)} 2138.price-category-card:before{content:"";position:absolute;inset:0 auto 0 0;width:5px;background:linear-gradient(180deg,#0a5ca8,#22b4bd)} 2139.price-category-inner{display:grid;grid-template-columns:minmax(250px,.85fr) minmax(0,2.65fr);gap:12px;align-items:center;padding:13px 14px 13px 22px} 2140.price-category-name{display:flex;flex-direction:column;justify-content:center;min-width:0}.price-category-name small{color:#6f8198;font-size:9px;font-weight:950;letter-spacing:.1em;text-transform:uppercase}.price-category-name strong{margin-top:5px;color:#082f5c;font-size:17px;line-height:1.08;letter-spacing:0} 2141.price-category-spark{position:relative;height:72px;border:1px solid #dfeaf4;border-radius:10px;background:linear-gradient(180deg,#f9fcff,#fff);overflow:hidden}.price-category-spark:before{content:"";position:absolute;inset:8px 10px 24px;background:repeating-linear-gradient(180deg,rgba(0,46,95,.07) 0 1px,transparent 1px 16px)}.price-category-spark svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}.price-category-spark polyline{fill:none;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;filter:drop-shadow(0 5px 7px rgba(0,46,95,.12));stroke-dasharray:360;stroke-dashoffset:0;transform-origin:right center}.price-category-spark circle{filter:drop-shadow(0 4px 6px rgba(0,46,95,.18));transform-origin:center}.price-category-spark.is-a polyline{animation:priceTrendSlideA .78s cubic-bezier(.2,.72,.18,1) both}.price-category-spark.is-b polyline{animation:priceTrendSlideB .78s cubic-bezier(.2,.72,.18,1) both}.price-category-spark.is-a circle{animation:priceTrendDotA .62s cubic-bezier(.2,.72,.18,1) both}.price-category-spark.is-b circle{animation:priceTrendDotB .62s cubic-bezier(.2,.72,.18,1) both}.price-category-spark span{position:absolute;left:10px;right:10px;bottom:6px;height:14px;display:flex;align-items:center;background:linear-gradient(90deg,#f9fcff 0 58%,rgba(249,252,255,0));color:#75869a;font-size:8px;font-weight:950;text-transform:uppercase;letter-spacing:.08em;z-index:2} 2142@keyframes priceTrendSlideA{0%{stroke-dashoffset:150;opacity:.35;transform:translateX(14px) scaleX(1.08)}60%{opacity:1;transform:translateX(-2px) scaleX(.985)}100%{stroke-dashoffset:0;opacity:1;transform:translateX(0) scaleX(1)}}@keyframes priceTrendSlideB{0%{stroke-dashoffset:150;opacity:.35;transform:translateX(14px) scaleX(1.08)}60%{opacity:1;transform:translateX(-2px) scaleX(.985)}100%{stroke-dashoffset:0;opacity:1;transform:translateX(0) scaleX(1)}}@keyframes priceTrendDotA{0%{opacity:.25;transform:translateX(10px) scale(.45)}55%{transform:translateX(0) scale(1.35)}100%{opacity:1;transform:translateX(0) scale(1)}}@keyframes priceTrendDotB{0%{opacity:.25;transform:translateX(10px) scale(.45)}55%{transform:translateX(0) scale(1.35)}100%{opacity:1;transform:translateX(0) scale(1)}} 2143.price-stat-grid{display:grid;grid-template-columns:.58fr 1fr 1.12fr;gap:8px} 2144.price-stat-cell{min-height:56px;padding:9px 11px;border:1px solid #e1ebf4;border-radius:9px;background:linear-gradient(180deg,#fff,#fbfdff);display:grid;gap:5px;align-content:center}.price-stat-cell small{color:#75869a;font-size:8.5px;font-weight:950;letter-spacing:.08em;text-transform:uppercase}.price-roll{display:inline-flex;align-items:baseline;gap:4px;color:#002e5f;font-size:clamp(18px,2vw,25px);font-weight:950;line-height:1;font-variant-numeric:tabular-nums;white-space:nowrap}.price-stat-cell.is-count{background:#edf7fb;border-color:#cde6f1}.price-stat-cell.is-count .price-roll{font-size:clamp(22px,2.4vw,30px)}.price-stat-cell.is-average{background:linear-gradient(135deg,#002e5f,#0a5ca8);border-color:#0a5ca8}.price-stat-cell.is-average small,.price-stat-cell.is-average .price-roll{color:#fff}.price-roll em{font-style:normal;font-size:12px;color:#6f8198;font-weight:900} 2145.price-board-empty{border:1px solid #dbe7f2;border-radius:14px;background:#fff;padding:38px 18px;text-align:center;color:#65758a;font-weight:800;box-shadow:0 14px 34px rgba(15,42,70,.06)} 2146@media(max-width:760px){.price-board-page{padding:18px 6px 74px}.price-board-hero{border-radius:14px;padding:18px}.price-board-hero-content{grid-template-columns:1fr;gap:18px}.price-board-logos{grid-template-columns:1fr}.price-board-brand-row{grid-template-columns:1fr 1fr}.price-board-main-logo,.price-board-rosgan-logo{min-height:76px;padding:12px}.price-board-main-logo img,.price-board-rosgan-logo img{max-height:54px}.price-board-logo-fallback{font-size:18px}.price-board-cons-card{min-height:44px}.price-board-cons-card strong{font-size:12px}.price-board-grid{gap:7px}.price-category-card{border-radius:10px}.price-category-card:before{width:4px}.price-category-inner{grid-template-columns:1fr;gap:7px;padding:8px 6px 7px 10px}.price-category-name small{font-size:6px;letter-spacing:.04em}.price-category-name strong{margin-top:3px;font-size:10px;line-height:1.05;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.price-stat-grid{grid-template-columns:minmax(42px,48px) repeat(2,minmax(0,1fr));gap:3px}.price-stat-cell{min-height:43px;padding:5px 3px;border-radius:7px;gap:3px}.price-stat-cell.is-count{max-width:48px;padding-left:5px;padding-right:5px}.price-stat-cell small{font-size:5.7px;letter-spacing:0}.price-board-toolbar{justify-content:flex-start;margin:13px 0 10px}.price-roll{font-size:11px;gap:0}.price-stat-cell.is-count .price-roll{font-size:11px}} 2147@media(max-width:980px) and (min-width:761px){.price-board-hero-content{grid-template-columns:1fr 320px}.price-board-hero h1{font-size:48px}.price-board-main-logo{min-height:104px}} 2148.commercial-summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:20px 0 12px} 2149.commercial-metric{display:grid;grid-template-columns:42px minmax(0,1fr);gap:12px;align-items:center;min-height:78px;padding:14px 15px;border:1px solid #e0e9f2;border-radius:8px;background:#fff;box-shadow:0 10px 28px rgba(15,42,70,.06)} 2150.commercial-metric-icon{width:42px;height:42px;display:grid;place-items:center;border-radius:50%;background:#e8f2fc} 2151.commercial-metric-icon img{width:20px;height:20px} 2152.commercial-metric small{display:block;color:#6e7f94;font-size:11px;font-weight:850} 2153.commercial-metric strong{display:inline-block;margin-top:3px;color:#0b315d;font-size:24px;line-height:1} 2154.commercial-metric em{margin-left:7px;color:#8593a6;font-size:11px;font-style:normal;font-weight:800} 2155.commercial-filters{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0 14px} 2156.commercial-filter{display:inline-flex;align-items:center;gap:8px;min-height:38px;border:1px solid #d8e5f1;border-radius:999px;background:#fff;color:#31526f;padding:8px 13px;font:900 12px 'Archivo',sans-serif;box-shadow:0 7px 18px rgba(15,42,70,.05);cursor:pointer} 2157.commercial-filter.is-active{background:#002e5f;border-color:#002e5f;color:#fff} 2158.commercial-filter b{min-width:21px;height:21px;
2158display:grid;place-items:center;border-radius:999px;background:rgba(10,92,168,.1);font-size:11px} 2159.commercial-filter.is-active b{background:rgba(255,255,255,.2)} 2160.commercial-board{overflow:visible;border:1px solid #dce7f2;border-radius:8px;background:#fff;box-shadow:0 18px 42px rgba(15,42,70,.08)} 2161.commercial-table-head,.commercial-row-main{display:grid;grid-template-columns:48px 58px 72px 82px minmax(160px,1.3fr) 58px minmax(230px,1.55fr) minmax(170px,1.05fr) 40px;align-items:center;gap:8px} 2162 2163.commercial-table-head{position:sticky;top:58px;z-index:20;min-height:48px;padding:0 16px;border-radius:8px 8px 0 0;background:rgba(246,249,252,.97);color:#71839a;font-size:10px;font-weight:950;letter-spacing:.08em;text-transform:uppercase;box-shadow:0 1px 0 #dce7f2,0 8px 18px rgba(15,42,70,.08);backdrop-filter:blur(8px)} 2164html[data-public-route="lacassina-commercial"] .commercial-table-head{top:0} 2165.commercial-row{border-top:1px solid #e7eef5;background:#fff} 2166.commercial-row-main{padding:12px 16px} 2167.commercial-row-index,.commercial-row-strong,.commercial-category{color:#10233b;font-size:13px;font-weight:950;line-height:1.15} 2168.commercial-order-badge{width:30px;height:30px;display:grid;place-items:center;border-radius:50%;background:linear-gradient(145deg,#0a5ca8,#002e5f);color:#fff;font-size:12px;font-weight:950;line-height:1;box-shadow:0 7px 16px rgba(0,46,95,.22)} 2169.commercial-row-muted{display:none;color:#65758a;font-size:11px;font-weight:850;line-height:1.1} 2170.commercial-category{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} 2171.commercial-cons-avatar{width:34px;height:34px;display:grid;place-items:center;border-radius:50%;padding:7px;box-sizing:border-box;background:#d91549;box-shadow:0 8px 18px rgba(217,21,73,.2);line-height:0} 2172.commercial-cons-avatar img{display:block;width:auto!important;height:auto!important;max-width:20px!important;max-height:20px!important;object-fit:contain;object-position:center} 2173.commercial-cons-avatar strong{color:#fff;font-size:13px} 2174.commercial-status{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px} 2175.commercial-status button{min-height:28px;border:1px solid #dfe7ef;border-radius:6px;background:#f8fafc;color:#66778c;font:850 10px 'Archivo',sans-serif;cursor:pointer} 2176.commercial-status .is-vendido{background:#e7f5ee;border-color:#a9dcc4;color:#17704d} 2177.commercial-status .is-pendiente{background:#fff6df;border-color:#f3cf78;color:#9a6700} 2178.commercial-status .is-no-vendido{background:#fff0f0;border-color:#f1b4b4;color:#c03434} 2179.commercial-status .is-levantado{background:#eef2f6;border-color:#cbd8e5;color:#4e6279} 2180.commercial-status-pill{display:none;align-items:center;justify-content:center;min-height:28px;border:1px solid;border-radius:999px;padding:5px 8px;font:950 9px 'Archivo',sans-serif;text-align:center;line-height:1.05;white-space:nowrap} 2181.commercial-price{display:grid;gap:6px} 2182.commercial-mode{display:grid;grid-template-columns:1fr 1fr;border:1px solid #d7e3ef;border-radius:8px;overflow:hidden;background:#f7fafd} 2183.commercial-mode button{min-height:28px;border:0;background:transparent;color:#65758a;font:900 10px 'Archivo',sans-serif;cursor:pointer} 2184.commercial-mode button.is-active{background:#0f7b48;color:#fff} 2185.commercial-price input,.commercial-more-grid input{width:100%;min-height:34px;border:1px solid #d8e4f0;border-radius:8px;background:#fff;color:#10233b;padding:8px 10px;box-sizing:border-box;font:850 12px 'Archivo',sans-serif;outline:0} 2186.commercial-price input:focus,.commercial-more-grid input:focus{border-color:#0a5ca8;box-shadow:0 0 0 3px rgba(10,92,168,.12)} 2187.commercial-price input:disabled{background:#eef3f8;color:#8a98aa;cursor:not-allowed} 2188.commercial-mode button:disabled{opacity:.55;cursor:not-allowed} 2189.commercial-faena{display:grid;grid-template-columns:40px minmax(0,1fr);gap:8px;align-items:center} 2190.commercial-switch{width:40px;height:24px;border:0;border-radius:999px;background:#c7d1dc;padding:3px;cursor:pointer;transition:background-color .18s ease} 2191.commercial-switch span{display:block;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 2px 5px rgba(0,0,0,.2);transition:transform .18s ease} 2192.commercial-switch.is-on{background:#177b4d} 2193.commercial-switch.is-on span{transform:translateX(16px)} 2194.commercial-faena input{width:100%;min-height:30px;border:1px solid #d8e4f0;border-radius:999px;background:#f8fbfe;color:#177b4d;text-align:center;font:900 11px 'Archivo',sans-serif;outline:0} 2195.commercial-more-button{width:34px;height:34px;display:grid;place-items:center;border:1px solid #d7e3ef;border-radius:10px;background:#fff;cursor:pointer} 2196.commercial-more-button img{width:17px;height:17px;transition:transform .18s ease} 2197.commercial-more{display:grid;grid-template-columns:minmax(0,1fr);gap:12px;max-height:0;margin:0 16px;padding:0 15px;border:1px solid transparent;border-radius:8px;background:#fbfdff;overflow:hidden;opacity:0;transform:translateY(-6px) scaleY(.98);transform-origin:top;pointer-events:none;transition:max-height .24s cubic-bezier(.2,.72,.24,1),opacity .18s ease,transform .24s cubic-bezier(.2,.72,.24,1),padding .24s cubic-bezier(.2,.72,.24,1),margin .24s cubic-bezier(.2,.72,.24,1),border-color .18s ease} 2198.commercial-more.is-open{max-height:620px;margin:0 16px 14px;padding:15px;border-color:#dbe7f2;opacity:1;transform:translateY(0) scaleY(1);pointer-events:auto} 2199@media(prefers-reduced-motion:reduce){.commercial-more,.commercial-more-button img{transition:none}} 2200.commercial-more-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px} 2201.commercial-more-grid label{display:grid;gap:6px;color:#65758a;font-size:10px;font-weight:950;letter-spacing:.06em;text-transform:uppercase} 2202.commercial-more-status{display:none} 2203.commercial-category-divider{display:none} 2204.commercial-empty{padding:42px 18px;text-align:center;color:#65758a;font-weight:800} 2205.commercial-view-tabs{display:inline-flex;gap:4px;margin:0 0 14px;padding:4px;border:1px solid #d8e5f1;border-radius:10px;background:#eaf1f7} 2206.commercial-view-tab{display:inline-flex;align-items:center;gap:8px;min-height:36px;border:0;border-radius:7px;background:transparent;color:#4e6379;padding:8px 13px;font:900 11px 'Archivo',sans-serif;cursor:pointer} 2207.commercial-view-tab img{width:15px;height:15px}.commercial-view-tab.is-active{background:#fff;color:#003b73;box-shadow:0 5px 14px rgba(15,42,70,.1)} 2208.commercial-report{overflow:hidden;border:1px solid #dce7f2;border-radius:10px;background:#fff;box-shadow:0 18px 42px rgba(15,42,70,.08)} 2209.commercial-report-head{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:18px 20px;border-bottom:1px solid #e2ebf3;background:linear-gradient(135deg,#f7fbff,#eef6fb)} 2210.commercial-report-head h2{margin:0;color:#0b315d;font-size:20px}.commercial-report-head p{margin:4px 0 0;color:#65758a;font-size:12px;line-height:1.4} 2211.commercial-export-excel{display:inline-flex;align-items:center;justify-content:center;gap:8px;flex:0 0 auto;border:0;border-radius:9px;background:#17704d;color:#fff;padding:11px 15px;font:900 12px 'Archivo',sans-serif;box-shadow:0 10px 22px rgba(23,112,77,.2);cursor:pointer}.commercial-export-excel img{width:17px;height:17px;filter:brightness(0) invert(1)} 2212.commercial-report-scroll{overflow:auto;max-height:min(62vh,680px)} 2213.commercial-report-table{width:100%;min-width:1140px;border-collapse:separate;border-spacing:0;font-size:12px} 2214.commercial-report-table thead th{position:sticky;top:0;z-index:3;padding:12px 10px;background:#003b73;color:#fff;text-align:left;font-size:9px;font-weight:950;letter-spacing:.08em;text-transform:uppercase;white-space:nowrap} 2215.commercial-report-table tbody td{padding:11px 10px;border-bottom:1px solid #e7eef5;color:#1b3148;font-weight:750;vertical-align:middle}.commercial-report-table tbody tr:nth-child(even) td{background:#f8fbfd}.commercial-report-table tbody tr:hover td{background:#eef6fc} 2216.commercial-report-table .is-number{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}.commercial-report-table .is-strong{color:#082f5c;font-weight:950}.commercial-report-table .is-muted{color:#7a899a}.commercial-report-status{display:inline-flex;align-items:center;justify-content:center;border:1px solid;border-radius:999px;padding:5px 8px;font-size:9px;font-weight:950;white-space:nowrap} 2217.commercial-report-foot{display:flex;justify-content:space-between;gap:12px;padding:12px 20px;background:#f7fafc;color:#65758a;font-size:11px;font-weight:800} 2218@media(max-width:960px){ 2219 .commercial-hero{grid-template-columns:1fr}.commercial-save{justify-self:start} 2220 .commercial-summary{grid-template-columns:repeat(2,minmax(0,1fr))} 2221 .commercial-table-head{display:none} 2222 .commercial-board{display:grid;gap:10px;border:0;background:transparent;box-shadow:none} 2223 .commercial-report-head{align-items:flex-start;flex-direction:column}.commercial-export-excel{width:100%}.commercial-report{border-radius:8px}
2223.commercial-report-foot{flex-direction:column} 2224 .commercial-row{border:1px solid #dce7f2;border-radius:12px;overflow:hidden;box-shadow:0 10px 26px rgba(15,42,70,.06)} 2225 .commercial-row-main{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;padding:14px} 2226 .commercial-row-main>div{min-width:0} 2227 .commercial-row-muted{display:block} 2228 .commercial-mobile-span{grid-column:1/-1} 2229 .commercial-category{white-space:normal;line-height:1.28} 2230 .commercial-status{grid-template-columns:repeat(4,minmax(0,1fr))} 2231 .commercial-more-grid{grid-template-columns:repeat(2,minmax(0,1fr))} 2232} 2233@media(max-width:560px){ 2234 .commercial-page{padding:18px 10px 88px} 2235 .commercial-summary{grid-template-columns:1fr 1fr;gap:8px} 2236 .commercial-metric{grid-template-columns:34px minmax(0,1fr);min-height:68px;padding:10px}.commercial-metric-icon{width:34px;height:34px}.commercial-metric strong{font-size:20px} 2237 .commercial-filters{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));overflow:visible;gap:6px;margin:9px 0 11px;padding:0} 2238 .commercial-filter{min-width:0;width:100%;justify-content:space-between;min-height:34px;padding:6px 10px;gap:6px;font-size:10px;box-shadow:none} 2239 .commercial-filter:last-child:nth-child(odd){grid-column:1/-1} 2240 .commercial-filter b{min-width:17px;height:17px;font-size:9px} 2241 .commercial-board{gap:7px} 2242 .commercial-category-divider{display:flex;align-items:center;gap:8px;margin:10px 4px 2px;color:#0a5ca8;font:950 10px 'Archivo',sans-serif;letter-spacing:.08em;text-transform:uppercase} 2243 .commercial-category-divider:after{content:"";height:1px;flex:1;background:#cfe0f1} 2244 .commercial-row{border-radius:10px} 2245 .commercial-row-main{grid-template-columns:31px 31px minmax(54px,.75fr) 36px 28px minmax(110px,1.35fr) 28px;gap:5px;align-items:center;padding:9px 10px} 2246 .commercial-row-main>div{min-width:0} 2247 .commercial-row-main>div:nth-child(1),.commercial-row-main>div:nth-child(2),.commercial-row-main>div:nth-child(3),.commercial-row-main>div:nth-child(4){align-self:center} 2248 .commercial-row-main>div:nth-child(5){display:none} 2249 .commercial-row-main>div:nth-child(6){display:flex;align-items:center;justify-content:center} 2250 .commercial-row-main>div:nth-child(1),.commercial-row-main>div:nth-child(2),.commercial-row-main>div:nth-child(3),.commercial-row-main>div:nth-child(4),.commercial-row-main>div:nth-child(6),.commercial-row-main>div:nth-child(8),.commercial-row-main>.commercial-more-button{order:1} 2251 .commercial-row-main>div:nth-child(7){order:2;grid-column:1/-1;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:5px} 2252 .commercial-order-badge{width:28px;height:28px;font-size:11px} 2253 .commercial-row-muted{font-size:7px;letter-spacing:0;text-transform:uppercase;color:#7b8ca1;white-space:nowrap} 2254 .commercial-row-index,.commercial-row-strong{font-size:12px;line-height:1.05} 2255 .commercial-cons-avatar{width:28px;height:28px;padding:6px} 2256 .commercial-cons-avatar img{max-width:16px!important;max-height:16px!important} 2257 .commercial-row-main>.commercial-status button{display:block;min-height:25px;border-radius:7px;padding:4px 3px;font-size:8.5px;line-height:1.05;white-space:normal} 2258 .commercial-status-pill{display:none} 2259 .commercial-price{gap:4px} 2260 .commercial-mode{grid-template-columns:1fr 1fr;border-radius:7px} 2261 .commercial-mode button{min-height:24px;font-size:9px} 2262 .commercial-price input{min-height:30px;border-radius:7px;padding:6px 8px;font-size:12px} 2263 .commercial-more-button{width:30px;height:30px;border-radius:8px} 2264 .commercial-more-button img{width:15px;height:15px} 2265 .commercial-more{margin:0 9px;padding:0 12px;gap:10px} 2266 .commercial-more.is-open{margin:0 9px 9px;padding:12px} 2267 .commercial-more-status{display:none} 2268 .commercial-more-grid{grid-template-columns:1fr} 2269 .commercial-report-scroll{overflow:hidden;max-height:none;padding:8px;background:#f4f8fb} 2270 .commercial-report-table{display:block;width:100%;min-width:0;border-collapse:separate;font-size:11px} 2271 .commercial-report-table thead{display:none} 2272 .commercial-report-table tbody{display:grid;gap:8px} 2273 .commercial-report-table tbody tr{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px 12px;padding:12px;border:1px solid #dce7f2;border-radius:10px;background:#fff;box-shadow:0 5px 14px rgba(15,42,70,.05)} 2274 .commercial-report-table tbody td,.commercial-report-table tbody tr:nth-child(even) td,.commercial-report-table tbody tr:hover td{display:grid;min-width:0;gap:3px;padding:0;border:0;background:transparent;text-align:left;white-space:normal;overflow-wrap:anywhere} 2275 .commercial-report-table tbody td::before{content:attr(data-label);color:#7a899a;font-size:8px;font-weight:950;letter-spacing:.07em;text-transform:uppercase} 2276 .commercial-report-table tbody td[data-label="CategorÃa"],.commercial-report-table tbody td[data-label="Cabaña"],.commercial-report-table tbody td[data-label="Consignataria"]{grid-column:1/-1} 2277 .commercial-report-table .is-number{text-align:left;white-space:normal} 2278} 2279.mav-chart-wrap,.market-heat-table-wrap{max-width:100%} 2280@media(max-width:760px){ 2281 .mav-chart-wrap{overflow:hidden!important} 2282 .mav-chart-canvas{width:100%;min-width:0!important} 2283 .market-heat-table-wrap{overflow:hidden!important} 2284 .market-heat-table{width:100%!important;min-width:0!important;table-layout:fixed} 2285 .market-heat-table th,.market-heat-table td{min-width:0!important;padding:7px 3px!important;font-size:8.5px!important;white-space:normal!important;overflow-wrap:anywhere} 2286 .market-heat-table th:first-child,.market-heat-table td:first-child{position:static!important;width:27%;padding-left:7px!important;text-align:left!important} 2287 .market-heat-table td>div{padding:7px 2px!important;font-size:8.5px!important;border-radius:6px!important} 2288 .market-chip-list,.market-year-list{flex-wrap:wrap!important;overflow-x:visible!important;padding-bottom:0!important} 2289 .market-chip-list>*,.market-year-list>*{flex-shrink:1!important} 2290} 2291.advance-splash{min-height:100vh;display:grid;place-items:center;overflow:hidden;position:relative;background:#003568} 2292.advance-splash::before,.advance-splash::after{content:"";position:absolute;inset:-35%;background:radial-gradient(circle at 42% 48%,rgba(31,133,211,.5),transparent 36%),linear-gradient(115deg,transparent 32%,rgba(255,255,255,.08) 50%,transparent 68%);animation:advanceDrift 12s ease-in-out infinite alternate} 2293.advance-splash::after{inset:-20%;opacity:.45;animation-duration:18s;animation-direction:alternate-reverse} 2294.advance-splash-content{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;width:min(760px,88vw);text-align:center;color:#fff} 2295.advance-splash img{position:relative;width:min(360px,58vw);height:auto;filter:brightness(0) invert(1);animation:advanceLogo 3.8s ease-in-out infinite} 2296.advance-splash-copy{margin-top:clamp(32px,5vh,54px);animation:advanceCopyIn .8s .18s cubic-bezier(.2,.72,.18,1) both} 2297.advance-splash-copy h1{margin:0;font-size:clamp(25px,4.2vw,42px);font-weight:800;line-height:1.08;letter-spacing:-.025em;text-wrap:balance} 2298.advance-splash-copy p{margin:11px 0 0;color:rgba(255,255,255,.78);font-size:clamp(15px,2vw,20px);font-weight:500;line-height:1.45;text-wrap:balance} 2299.advance-splash-actions{display:flex;flex-direction:column;align-items:center;gap:10px;margin-top:clamp(25px,4vh,36px)} 2300.advance-splash-launch{display:inline-flex;align-items:center;justify-content:center;gap:10px;min-height:50px;padding:0 23px;border:0;border-radius:999px;background:#fff;color:#003568;font:850 14px 'Archivo',sans-serif;cursor:pointer;box-shadow:0 16px 38px rgba(0,20,45,.25);transition:transform .2s ease,box-shadow .2s ease,opacity .2s ease} 2301.advance-splash-launch svg{width:18px;height:18px}.advance-splash-launch:hover{transform:translateY(-2px);box-shadow:0 20px 45px rgba(0,20,45,.32)}.advance-splash-launch:disabled{cursor:wait;opacity:.72;transform:none} 2302.advance-splash-launch.is-subscribed{background:#6be2bd;color:#003d35;cursor:default} 2303.advance-splash-interest-note{max-width:520px!important;margin:2px 0 0!important;color:rgba(255,255,255,.72)!important;font-size:11.5px!important;line-height:1.45!important} 2304.advance-splash-interest-message{max-width:520px!important;margin:0!important;color:#fff!important;font-size:12px!important;font-weight:750!important} 2305.advance-splash-unsubscribe{border:0;background:transparent;color:rgba(255,255,255,.67);font:650 11.5px 'Archivo',sans-serif;text-decoration:underline;text-underline-offset:3px;
2305cursor:pointer}.advance-splash-unsubscribe:disabled{opacity:.5;cursor:wait} 2306.advance-splash-logout{display:inline-flex;align-items:center;justify-content:center;gap:9px;margin-top:clamp(26px,4vh,38px);min-height:44px;padding:0 19px;border:1px solid rgba(255,255,255,.28);border-radius:999px;background:rgba(255,255,255,.09);backdrop-filter:blur(12px);color:rgba(255,255,255,.9);font:700 13px 'Archivo',sans-serif;letter-spacing:.01em;cursor:pointer;transition:transform .2s ease,background .2s ease,border-color .2s ease,box-shadow .2s ease} 2307.advance-splash-logout svg{width:17px;height:17px;transition:transform .2s ease} 2308.advance-splash-logout:hover{transform:translateY(-2px);background:rgba(255,255,255,.17);border-color:rgba(255,255,255,.46);box-shadow:0 12px 30px rgba(0,22,49,.18)} 2309.advance-splash-logout:hover svg{transform:translateX(2px)} 2310.advance-splash-logout:active{transform:translateY(0) scale(.98)} 2311@keyframes advanceDrift{to{transform:translate3d(8%,4%,0) scale(1.08)}} 2312@keyframes advanceLogo{0%,100%{transform:translateY(0);opacity:.9}50%{transform:translateY(-8px);opacity:1}} 2313@keyframes advanceCopyIn{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}} 2314.app-welcome{min-height:100vh;position:relative;overflow:hidden;display:grid;place-items:center;padding:28px;box-sizing:border-box;background:radial-gradient(circle at 18% 18%,rgba(54,180,206,.34),transparent 31rem),radial-gradient(circle at 84% 78%,rgba(21,111,191,.5),transparent 36rem),linear-gradient(145deg,#001d3d 0%,#00457f 54%,#087f9d 100%);color:#fff} 2315::view-transition-old(root),::view-transition-new(root){mix-blend-mode:normal;animation-duration:.52s;animation-timing-function:cubic-bezier(.2,.72,.2,1)} 2316::view-transition-old(root){animation-name:rosganViewOut} 2317::view-transition-new(root){animation-name:rosganViewIn} 2318.app-welcome::before,.app-welcome::after{content:"";position:absolute;border:1px solid rgba(255,255,255,.16);border-radius:50%;animation:welcomeOrbit 9s ease-in-out infinite alternate} 2319.app-welcome::before{width:min(68vw,760px);aspect-ratio:1;left:-22vw;top:-38vw;box-shadow:0 0 0 70px rgba(255,255,255,.025),0 0 0 150px rgba(255,255,255,.018)} 2320.app-welcome::after{width:min(48vw,540px);aspect-ratio:1;right:-17vw;bottom:-25vw;animation-direction:alternate-reverse;animation-duration:12s} 2321.app-welcome-content{position:relative;z-index:1;width:min(720px,92vw);display:flex;flex-direction:column;align-items:center;text-align:center;animation:welcomeContentIn .72s cubic-bezier(.18,.76,.22,1) both} 2322.app-welcome-logo{width:min(210px,44vw);height:auto;filter:brightness(0) invert(1);animation:welcomeLogoFloat 3s ease-in-out infinite} 2323.app-welcome-lottie-slot{display:grid;place-items:center;width:clamp(168px,25vh,240px);height:clamp(168px,25vh,240px);margin:-2px 0 -14px} 2324.app-welcome-lottie,.app-connecting-lottie,dotlottie-wc[classname="app-welcome-lottie"],dotlottie-wc[classname="app-connecting-lottie"]{grid-area:1/1;display:block;width:100%;height:100%;filter:drop-shadow(0 18px 32px rgba(0,23,44,.18))} 2325.app-welcome-kicker{display:inline-flex;align-items:center;gap:8px;margin-top:0;padding:7px 13px;border:1px solid rgba(255,255,255,.22);border-radius:999px;background:rgba(255,255,255,.1);backdrop-filter:blur(12px);font-size:10px;font-weight:850;letter-spacing:.14em;text-transform:uppercase;color:#bfe9f2} 2326.app-welcome-kicker::before{content:"";width:7px;height:7px;border-radius:50%;background:#69e4bd;box-shadow:0 0 0 5px rgba(105,228,189,.12);animation:pulseDot 1.4s infinite} 2327.app-welcome h1{margin:18px 0 0;font-size:clamp(34px,6vw,64px);line-height:1;letter-spacing:-.04em;text-wrap:balance} 2328.app-welcome.app-connecting-screen{background:radial-gradient(ellipse at 50% 42%,#fff 0%,#f4f8fc 50%,#eaf1f8 100%);color:#002e5f;padding:32px 20px;overflow-y:auto} 2329.app-connecting-screen::before,.app-connecting-screen::after{display:none} 2330.app-connecting-screen .app-welcome-content{width:min(560px,100%);animation:none} 2331.app-connecting-screen .app-welcome-lottie-slot{width:min(420px,82vw,53svh);height:auto;aspect-ratio:1;margin:0 0 18px} 2332.app-connecting-screen .app-connecting-lottie{filter:none} 2333.app-connecting-screen .app-welcome-kicker{color:#217864;background:#e7f3ef;border-color:#d6eae3;font-size:9px;letter-spacing:.13em;padding:7px 12px} 2334.app-connecting-screen h1{font-size:clamp(25px,4vw,36px);line-height:1.15;margin:18px 0 0;letter-spacing:-.035em} 2335.app-connecting-description{max-width:340px;margin:12px 0 0;font-size:14px;line-height:1.6;color:#647a90;text-wrap:balance} 2336.app-connection-success .app-welcome-kicker::before{animation:none;background:#148660;box-shadow:none} 2337.app-connection-success h1,.app-connection-success .app-connecting-description{animation:connectionConfirmed .5s ease-out both} 2338.app-connection-success .app-welcome-lottie{filter:none} 2339@keyframes connectionConfirmed{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}} 2340@media(prefers-reduced-motion:reduce){.app-connection-success h1,.app-connection-success .app-connecting-description{animation:none}} 2341@keyframes welcomeOrbit{to{transform:translate3d(4%,6%,0) scale(1.08);opacity:.6}} 2342@keyframes welcomeContentIn{from{opacity:0;transform:translateY(22px) scale(.985)}to{opacity:1;transform:translateY(0) scale(1)}} 2343@keyframes welcomeLogoFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}} 2344@keyframes rosganViewOut{to{opacity:0;transform:translateY(-8px) scale(.992);filter:blur(2px)}} 2345@keyframes rosganViewIn{from{opacity:0;transform:translateY(12px) scale(.992);filter:blur(2px)}to{opacity:1;transform:none;filter:none}}
2346.auth-view-bridge{position:fixed;inset:0;z-index:30000;pointer-events:none;opacity:0;background:radial-gradient(circle at 50% 45%,rgba(41,178,201,.36),transparent 42%),linear-gradient(135deg,#00264f,#076d96);animation:authViewBridge .56s cubic-bezier(.2,.72,.2,1) both} 2347.auth-view-bridge::after{content:"";position:absolute;inset:0;background:radial-gradient(circle at 50% 50%,rgba(255,255,255,.11),transparent 28%);transform:scale(.72);animation:authBridgeGlow .56s ease both} 2348@keyframes authViewBridge{0%{opacity:0}38%,57%{opacity:1}100%{opacity:0}} 2349@keyframes authBridgeGlow{0%{opacity:0;transform:scale(.72)}48%{opacity:1;transform:scale(1)}100%{opacity:0;transform:scale(1.14)}} 2350@media(prefers-reduced-motion:reduce){::view-transition-old(root),::view-transition-new(root),.app-welcome::before,.app-welcome::after,.app-welcome-content,.app-welcome-logo,.auth-view-bridge,.auth-view-bridge::after{animation:none}} 2351.profile-company{display:flex;align-items:center;gap:14px;padding:12px 16px;background:#fff;border:1px solid #e3eaf1;border-left:3px solid #002e5f;border-radius:10px;margin-bottom:16px} 2352.profile-company img{width:64px;height:36px;object-fit:contain}.profile-company span,.profile-lot-cert{font-size:10px;letter-spacing:.04em;font-weight:700;color:#627b94}.profile-company h2{margin:3px 0 0;font-size:15px;color:#002e5f} 2353.profile-rosgan-lots{background:#fff;border:1px solid #e3eaf1;border-radius:12px;padding:16px 18px;margin-bottom:18px}.profile-rosgan-lots header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}.profile-rosgan-lots h2{margin:0;font-size:16px;font-weight:700}.profile-rosgan-lots p{font-size:12px;line-height:1.5;color:#627b94;margin:4px 0}.profile-rosgan-lots button,.profile-lot-actions a{font:inherit;font-size:12px;font-weight:600;border:1px solid #dce5ee;border-radius:7px;background:#fff;color:#002e5f;padding:6px 9px;cursor:pointer;text-decoration:none} 2354.profile-rosgan-row{display:grid;grid-template-columns:minmax(160px,1.2fr) 64px minmax(150px,1fr) auto;gap:16px;align-items:center;padding:11px 0;border-top:1px solid #edf1f5}.profile-rosgan-row h3{margin:2px 0;font-size:14px;font-weight:600}.profile-rosgan-row>div>span{display:block;color:#627b94;font-size:11px;margin-top:2px}.profile-rosgan-row>div>strong{font-size:12px;font-weight:600}.profile-rosgan-row>div:nth-child(2)>strong{font-size:16px}.profile-lot-status{text-transform:capitalize}.profile-rosgan-row .profile-lot-own{color:#007c66;font-weight:600}.profile-lot-actions{display:flex;gap:6px;flex-wrap:wrap} 2355@media(max-width:650px){.profile-company{padding:10px 12px;gap:12px}.profile-company h2{font-size:14px}.profile-rosgan-lots{padding:12px}.profile-rosgan-row{grid-template-columns:minmax(0,1fr) 64px;gap:8px 12px}.profile-rosgan-lots header{align-items:flex-start}.profile-lot-actions{justify-content:flex-end}} 2356.profile-area,.profile-hero,.profile-actions,.profile-action,.profile-kpi-grid,.profile-kpi-card{box-sizing:border-box} 2357.lot-wizard-page,.lot-wizard-page *{box-sizing:border-box} 2358.lot-wizard-page,.lot-wizard-header,.lot-wizard-grid,.lot-form-panel,.lot-fields-grid,.establishment-form-card,.establishment-form-grid{min-width:0;max-width:100%} 2359.lot-wizard-page input:not([type="checkbox"]):not([type="radio"]),.lot-wizard-page select,.lot-wizard-page textarea{width:100%;min-width:0;max-width:100%} 2360.establishment-cuit-field{grid-column:1/-1} 2361@media(max-width:700px){ 2362 .lot-wizard-page{width:100%;max-width:100vw!important;padding:0 14px calc(104px + env(safe-area-inset-bottom))!important;overflow-x:clip} 2363 .lot-wizard-header{align-items:flex-start!important;padding:18px 0 15px!important} 2364 .lot-wizard-header>div{flex:1 1 100%;min-width:0} 2365 .lot-wizard-header>span:last-child{margin-left:auto} 2366 .lot-wizard-steps{gap:5px!important;margin-bottom:16px!important} 2367 .lot-wizard-steps>sc-for{display:contents} 2368 .lot-wizard-step-copy{display:none!important} 2369 .lot-wizard-grid{grid-template-columns:minmax(0,1fr)!important;gap:14px!important} 2370 .lot-form-panel{width:100%;padding:20px!important;border-radius:18px!important} 2371 .lot-fields-grid,.establishment-form-grid,.lot-three-fields-grid{grid-template-columns:minmax(0,1fr)!important} 2372 .establishment-form-card{padding:16px!important} 2373 .establishment-cuit-field{grid-column:auto} 2374 .lot-wizard-footer button{min-width:0;padding:11px 16px!important} 2375 @supports not (overflow:clip){.lot-wizard-page{overflow-x:hidden}} 2376} 2377.profile-actions{flex-wrap:wrap;max-width:100%} 2378.profile-action.is-selected,.profile-action[aria-pressed="true"]{background:#fff!important;color:#07396a!important;border-color:#fff!important;box-shadow:0 0 0 3px rgba(126,205,255,.45),0 4px 12px #001f4033} 2379.profile-action.is-selected img,.profile-action[aria-pressed="true"] img{filter:brightness(0) saturate(100%) invert(17%) sepia(42%) saturate(1860%) hue-rotate(179deg)!important} 2380.profile-action:focus-visible{outline:3px solid #8bd8ff;outline-offset:3px} 2381.profile-actions>sc-if,.profile-kpi-grid>sc-for{display:contents} 2382 2383.feria-control-actions{display:flex;align-items:center;flex-wrap:wrap;gap:12px;margin:0 0 20px} 2384.feria-control-button{display:inline-flex;align-items:center;justify-content:center;gap:9px;box-sizing:border-box;height:46px;min-height:46px;margin:0;padding:0 16px;background:#fff;border:1px solid #dce6f0;border-radius:12px;color:#002e5f;font-family:inherit;font-size:13px;font-weight:600;line-height:20px;cursor:pointer} 2385.feria-control-button img{display:block;flex:none;object-fit:contain} 2386.feria-control-button.is-youtube{background:#002e5f;border-color:#002e5f;color:#fff} 2387.feria-control-button:hover{background:#edf4fa;border-color:#a9c4df} 2388.feria-control-button.is-youtube:hover{background:#074a88;border-color:#074a88} 2389.feria-control-button:focus-visible{outline:3px solid #79b9f1;outline-offset:3px} 2390@media(max-width:520px){.feria-control-button{width:100%}} 2391.profile-feria-actions{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:16px;margin-top:18px;padding-top:16px;border-top:1px solid #e7edf4} 2392.profile-feria-share-actions,.profile-feria-manage-actions{display:flex;align-items:center;flex-wrap:wrap;gap:8px}
2393.profile-feria-card .profile-feria-action{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;margin:0;padding:9px 13px;border:1px solid #d4e1ee;border-radius:10px;background:#f6f9fc;color:#174a73;font:700 12px/1.25 inherit;text-decoration:none;cursor:pointer;box-sizing:border-box;transition:background .15s,border-color .15s} 2394.profile-feria-action img{display:block;flex:none;width:16px;height:16px} 2395.profile-feria-card .profile-feria-action:hover{background:#eaf2fa;border-color:#a9c4df} 2396.profile-feria-card .profile-feria-action.is-primary{background:#075ca8;border-color:#075ca8;color:#fff}.profile-feria-action.is-primary img{filter:brightness(0) invert(1)} 2397.profile-feria-card .profile-feria-action.is-management{background:#fff} 2398.profile-feria-card .profile-feria-action.is-danger{background:#fff;color:#b42335;border-color:#f0d8dc}.profile-feria-action.is-danger img{filter:invert(22%) sepia(70%) saturate(2100%) hue-rotate(330deg)} 2399.profile-feria-card .profile-feria-action:focus-visible{outline:3px solid #8ac4ff;outline-offset:2px}.profile-feria-card .profile-feria-action:disabled{opacity:.55;cursor:wait} 2400@media(max-width:600px){.profile-feria-share-actions{width:100%}.profile-feria-share-actions .profile-feria-action{flex:1 1 auto}.profile-feria-manage-actions{width:100%;justify-content:flex-end}} 2401 2402.profile-feria-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,290px),1fr));gap:12px}.profile-feria-card{padding:16px;border:1px solid #dce7f0;border-radius:14px;background:#fff;min-width:0}.profile-feria-brand{display:flex;align-items:center;gap:8px;font-size:11px;color:#65758a}.profile-feria-brand img{width:32px;height:32px;object-fit:contain}.profile-feria-brand strong{margin-left:auto;color:#087664;background:#e5f5ef;padding:4px 7px;border-radius:6px;white-space:nowrap}.profile-feria-card h3{margin:12px 0 8px;font-size:16px;color:#112f4b}.profile-feria-card p{font-size:12px;color:#65758a;margin:5px 0}.profile-feria-card a{display:inline-block;margin-top:12px;font-size:12px;font-weight:700;color:#075ca8}.profile-feria-card small{display:block;margin-top:12px;color:#718397} 2403.lot-loading-switch{display:inline-flex;align-items:center;gap:8px}.lot-loading-track{width:30px;height:18px;border-radius:20px;background:#8797a8;display:inline-block;position:relative;flex:none}.lot-loading-track:after{content:"";position:absolute;width:12px;height:12px;top:3px;left:3px;background:white;border-radius:50%;transition:transform .15s}.lot-loading-switch[aria-checked="true"] .lot-loading-track{background:#087f65}.lot-loading-switch[aria-checked="true"] .lot-loading-track:after{transform:translateX(12px)} 2404.profile-email{min-width:0;max-width:100%;overflow-wrap:anywhere} 2405.ba-admin-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,440px),1fr));gap:16px;align-items:start} 2406.ba-admin-card{min-width:0;background:#fff;border-radius:16px;box-shadow:0 8px 26px rgba(0,30,60,.07);padding:16px 18px;display:flex;flex-direction:column;gap:14px} 2407.ba-admin-editor{min-width:0;display:grid;grid-template-columns:minmax(125px,.9fr) minmax(150px,1.15fr) auto;gap:10px;align-items:end} 2408.ba-admin-field{min-width:0;display:flex;flex-direction:column;gap:5px} 2409.ba-admin-field>span{font-size:10.5px;letter-spacing:.05em;color:#8593a6;font-weight:700;text-transform:uppercase} 2410.ba-admin-field>input{box-sizing:border-box;width:100%;min-width:0;height:38px;border:1px solid #dde7f2;border-radius:10px;padding:9px 10px;font:inherit;font-size:13px;color:#112033} 2411.ba-admin-save{box-sizing:border-box;height:38px;background:#0a5ca8;color:#fff;border:0;border-radius:9px;padding:0 13px;font:inherit;font-size:12px;font-weight:800;white-space:nowrap;cursor:pointer} 2412.ba-admin-save:disabled{cursor:wait;opacity:.65} 2413.credit-live-panel{background:#fff;border-radius:18px;box-shadow:0 8px 26px rgba(0,30,60,.07);overflow:hidden;margin-bottom:16px} 2414.credit-live-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:15px 20px;border-bottom:1px solid #edf2f7} 2415.credit-live-title{display:flex;align-items:center;gap:10px;min-width:0}.credit-live-title h3{margin:2px 0 0;color:#112033;font-size:18px;line-height:1.1}.credit-live-eyebrow{color:#8593a6;font-size:10px;font-weight:900;letter-spacing:.12em} 2416.credit-live-status{display:inline-flex;align-items:center;gap:7px;white-space:nowrap;color:#19755d;font-size:9.5px;font-weight:900;letter-spacing:.08em}.credit-live-status:before{content:"";width:7px;height:7px;border-radius:50%;background:#20c997;box-shadow:0 0 0 5px rgba(32,201,151,.12);animation:creditLivePulse 1.8s ease-in-out infinite} 2417.credit-live-history{min-height:34px;display:inline-flex;align-items:center;gap:6px;border:1px solid #c8d9e8;border-radius:9px;background:#fff;color:#075ca8;padding:7px 10px;font:800 10.5px inherit;cursor:pointer} 2418.credit-live-row{padding:13px 20px 15px;border-bottom:1px solid #edf2f7}.credit-live-row:last-child{border-bottom:0} 2419.credit-live-row-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}.credit-live-party{min-width:0}.credit-live-party strong{display:block;color:#112033;font-size:13.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2419.credit-live-party span{display:block;margin-top:2px;color:#7a8b9d;font-size:10.5px} 2420.credit-live-metrics{display:grid;grid-template-columns:minmax(150px,1.2fr) repeat(2,minmax(120px,.75fr));gap:8px}.credit-live-metric{min-width:0;padding:10px 12px;border:1px solid #e1eaf2;border-radius:11px;background:#f8fbfd}.credit-live-metric small{display:block;color:#718397;font-size:8.5px;font-weight:900;letter-spacing:.08em}.credit-live-metric strong{display:block;margin-top:4px;color:#12314f;font-size:16px;font-variant-numeric:tabular-nums;white-space:nowrap}.credit-live-metric.is-primary{background:linear-gradient(135deg,#edf9f4,#f6fcf9);border-color:#cbe8dc}.credit-live-metric.is-primary strong{color:#087a5c;font-size:20px}.credit-live-metric.is-used strong{color:#9b6513} 2421.credit-live-progress{height:5px;margin-top:8px;border-radius:999px;background:#dde7f0;overflow:hidden}.credit-live-progress>span{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,#0a5ca8,#20a37d);transition:width .55s cubic-bezier(.22,.72,.18,1)} 2422.credit-live-movements{display:flex;gap:7px;margin-top:9px;overflow:hidden}.credit-live-movement{min-width:0;flex:1;display:flex;align-items:center;justify-content:space-between;gap:7px;padding:8px 10px;border:1px solid #e3ebf3;border-radius:9px;background:#fff}.credit-live-movement>div{min-width:0}.credit-live-movement strong{display:block;color:#163651;font-size:10.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.credit-live-movement small{display:block;margin-top:2px;color:#7a8c9e;font-size:8.5px}.credit-live-movement b{color:#075ca8;font-size:10.5px;white-space:nowrap} 2423@keyframes creditLivePulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.48;transform:scale(.72)}} 2424.launch-admin-list{display:grid;gap:8px;max-height:420px;overflow:auto;padding-right:3px} 2425.launch-admin-row{display:grid;grid-template-columns:minmax(170px,1.2fr) minmax(210px,1.3fr) minmax(145px,.85fr) auto;gap:12px;align-items:center;padding:11px 13px;border:1px solid #e1eaf2;border-radius:11px;background:#f8fbfe} 2426.launch-admin-row>*{min-width:0}.launch-admin-row strong,.launch-admin-row span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.launch-admin-row strong{font-size:12.5px;color:#112033}.launch-admin-row span{font-size:11.5px;color:#65758a}.launch-admin-channels{display:flex;gap:5px;justify-content:flex-end}.launch-admin-channel{padding:4px 7px;border-radius:999px;background:#e8f5f0;color:#176c55;font-size:9.5px;font-weight:850;white-space:nowrap} 2427@media(max-width:640px){.ba-admin-card{padding:14px 16px}.ba-admin-editor{grid-template-columns:minmax(0,1fr) minmax(0,1.15fr)}.ba-admin-save{grid-column:1/-1;width:100%}} 2428@media(max-width:640px){.credit-live-head{padding:13px 14px}.credit-live-row{padding:12px 14px 14px}.credit-live-metrics{grid-template-columns:1fr 1fr}.credit-live-metric.is-primary{grid-column:1/-1}.credit-live-metric strong{font-size:14px}.credit-live-metric.is-primary strong{font-size:19px}.credit-live-movements{flex-direction:column}.credit-live-row-head{align-items:flex-start}.credit-live-history{padding:6px 8px}.credit-live-status{font-size:8.5px}} 2429@media(max-width:760px){.launch-admin-row{grid-template-columns:minmax(0,1fr) auto}.launch-admin-row .launch-admin-email,.launch-admin-row .launch-admin-phone{grid-column:1/-1}.launch-admin-channels{grid-column:2;grid-row:1;align-self:start}} 2430@media(max-width:360px){.ba-admin-editor{grid-template-columns:1fr}.ba-admin-save{grid-column:1}} 2431@media(max-width:1040px){.profile-actions{width:100%}} 2432@media(max-width:760px){ 2433 .profile-area{width:100%;max-width:100vw!important;padding:0 14px calc(112px + env(safe-area-inset-bottom))!important;overflow-x:clip} 2434 .profile-area>*,.profile-area section,.profile-area article,.profile-area details,.profile-area [data-reveal],.profile-area #profile-analytics-grid{min-width:0;max-width:100%;box-sizing:border-box} 2435 .profile-area [style*="display:grid"],.profile-area [style*="display:flex"]{min-width:0;max-width:100%} 2436 .profile-area input:not([type="checkbox"]):not([type="radio"]),.profile-area select,.profile-area textarea,.profile-area button{min-width:0;max-width:100%;box-sizing:border-box} 2437 .profile-area [style*="min-width:130px"],.profile-area [style*="min-width:140px"],.profile-area [style*="min-width:150px"],.profile-area [style*="min-width:170px"],.profile-area [style*="min-width:180px"],.profile-area [style*="min-width:210px"]{min-width:0!important;overflow-wrap:anywhere} 2438 .profile-area .buyer-credit-panel,.profile-area .buyer-credit-row,.profile-area .ba-admin-grid,.profile-area .ba-admin-card,.profile-area .ba-admin-editor,.profile-area .launch-admin-row{width:100%;min-width:0;max-width:100%;box-sizing:border-box} 2439 .profile-area .profile-panel-head,.profile-area .commercial-report-head{flex-wrap:wrap} 2440 @supports not (overflow:clip){.profile-area{overflow-x:hidden}} 2441 .profile-hero{display:grid!important;grid-template-columns:64px minmax(0,1fr);align-items:start!important;gap:13px!important;margin:14px 0!important;padding:18px!important;border-radius:20px!important} 2442 .profile-hero-avatar{grid-column:1;grid-row:1;width:64px!important;height:64px!important} 2443 .profile-identity{grid-column:2;grid-row:1;min-width:0!important;width:100%} 2444 .profile-kicker{font-size:9px!important;line-height:1.3;letter-spacing:.11em!important} 2445 .profile-identity h1{font-size:22px!important;line-height:1.05;margin-top:4px!important;overflow-wrap:anywhere} 2446 .profile-email{font-size:11.5px!important;line-height:1.3} 2447 .profile-email img{flex:0 0 auto} 2448 .profile-actions{grid-column:1/-1;grid-row:2;display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px!important;width:100%;margin-top:3px} 2449 .profile-action{width:100%;min-width:0;min-height:48px;padding:9px 8px!important;display:flex!important;align-items:center!important;justify-content:center!important;gap:6px!important;border-radius:11px!important;font-size:11.5px!important;font-weight:750!important;line-height:1.15;text-align:center;white-space:normal} 2450 .profile-action img{width:15px!important;height:15px!important;flex:0 0 auto} 2451 .profile-action-load,.profile-action-logout{font-size:12px!important} 2452 .profile-kpi-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:10px!important;margin-bottom:14px!important} 2453 .profile-kpi-card{min-width:0;min-height:124px;padding:15px!important;border-radius:16px!important} 2454 .profile-editor-overlay{place-items:end center!important;padding:0!important} 2455 .profile-editor-dialog{width:100%!important;max-height:calc(100dvh - 14px);overflow:auto!important;border-radius:22px 22px 0 0!important} 2456 .profile-editor-footer{position:sticky;bottom:0;z-index:2;padding:13px 16px calc(13px + env(safe-area-inset-bottom))!important} 2457 .profile-editor-footer button{flex:1;min-width:0} 2458} 2459@media(max-width:350px){.profile-kpi-grid,.profile-actions{grid-template-columns:1fr!important}} 2460html[data-public-advance="lacassina"] body{background:#f4f7fb} 2461html[data-public-advance="lacassina"] .advance-public-hidden{display:none!important} 2462html[data-public-advance="lacassina"] [data-public-blocked-overlay]{display:none!important} 2463html[data-public-route="lacassina-stats"] sc-if:not([value="{{ isCabanaStats }}"]){display:none!important} 2464html[data-public-route="lacassina-stats"] sc-if[value="{{ isHome }}"], 2465html[data-public-route="lacassina-stats"] sc-if[value="{{ isPool }}"], 2466html[data-public-route="lacassina-stats"] sc-if[value="{{ isLot }}"], 2467html[data-public-route="lacassina-stats"] sc-if[value="{{ isLoad }}"], 2468html[data-public-route="lacassina-stats"] sc-if[value="{{ isFeria }}"], 2469html[data-public-route="lacassina-stats"] sc-if[value="{{ isProfile }}"], 2470html[data-public-route="lacassina-stats"] sc-if[value="{{ isNotifications }}"], 2471html[data-public-route="lacassina-stats"] sc-if[value="{{ isRemates }}"], 2472html[data-public-route="lacassina-stats"] sc-if[value="{{ isRemateDetail }}"], 2473html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanas }}"], 2474html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanaDetail }}"], 2475html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanaLotDetail }}"], 2476html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanaLotCreate }}"], 2477html[data-public-route="lacassina-stats"] sc-if[value="{{ isRemateConfig }}"], 2478html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanaSalesOrder }}"], 2479html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanaCommercial }}"], 2480html[data-public-route="lacassina-stats"] sc-if[value="{{ isCabanaPriceBoard }}"], 2481html[data-public-route="lacassina-stats"] sc-if[value="{{ isChannel }}"], 2482html[data-public-route="lacassina-stats"] sc-if[value="{{ showStatistics }}"]{display:none!important} 2483html[data-public-route="cabana-tv"]{position:fixed;inset:0;width:100vw;height:100vh;min-width:0;min-height:0;max-width:100vw;max-height:100vh;overflow:hidden!important;overflow:clip!important;overscroll-behavior:none;background:transparent!important;scrollbar-width:none;touch-action:none} 2484html[data-public-route="cabana-tv"] *,html[data-public-route="cabana-tv"] *:before,html[data-public-route="cabana-tv"] *:after{box-sizing:border-box} 2485html[data-public-route="cabana-tv"]::-webkit-scrollbar,html[data-public-route="cabana-tv"] body::-webkit-scrollbar,html[data-public-route="cabana-tv"] x-dc::-webkit-scrollbar,html[data-public-route="cabana-tv"] #dc-root::-webkit-scrollbar,html[data-public-route="cabana-tv"] .sc-host::-webkit-scrollbar{display:none;width:0;height:0} 2486html[data-public-route="cabana-tv"] body{position:fixed;inset:0;width:100vw;height:100vh;min-width:0;min-height:0;max-width:100vw;max-height:100vh;margin:0;background:transparent!important;overflow:hidden!important;overflow:clip!important;overscroll-behavior:none;scrollbar-width:none;touch-action:none} 2487html[data-public-route="cabana-tv"] x-dc,html[data-public-route="cabana-tv"] #dc-root,html[data-public-route="cabana-tv"] .sc-host,html[data-public-route="cabana-tv"] .sc-host>div{position:relative;width:100vw!important;height:100vh!important;min-width:0!important;min-height:0!important;max-width:100vw!important;max-height:100vh!important;margin:0!important;background:transparent!important;overflow:hidden!important;overflow:clip!important;overscroll-behavior:none;scrollbar-width:none} 2488html[data-public-route="cabana-tv-config"] body{margin:0;background:#eef4fa} 2489.tv-config-category-fields{display:grid;gap:10px;max-height:640px;overflow:auto;padding-right:4px}.tv-config-category-card{border:1px solid #dce7f2;border-radius:10px;background:#fff;padding:10px}.tv-config-category-card h3{margin:0 0 8px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:#0a5ca8}.tv-config-card-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}.tv-config-card-head h2{margin:0}.tv-config-mini-toggle{display:flex;align-items:center;gap:9px;border:1px solid #dce7f2;border-radius:999px;background:#fff;padding:6px 7px 6px 11px;color:#173a5d;font-size:11px;font-weight:950}.tv-config-mini-toggle .tv-config-switch{width:45px;height:25px;padding:3px}.tv-config-mini-toggle .tv-config-switch span{width:19px;height:19px}.tv-config-mini-toggle .tv-config-switch.is-on span{transform:translateX(20px)}.tv-config-subforms{display:grid;gap:8px}.tv-config-subform{border:1px solid #e4edf6;border-radius:9px;background:#f8fbfe;padding:8px}.tv-config-subform h4{margin:0 0 7px;color:#65758a;font-size:10px;letter-spacing:.12em;text-transform:uppercase}.tv-config-category-card .tv-config-checks{grid-template-columns:repeat(auto-fit,minmax(118px,1fr));gap:7px}.tv-config-category-card .tv-config-check{border-radius:9px;padding:8px 9px} 2490.tv-lower-page{position:fixed;inset:0;z-index:2;width:100vw;height:100vh;min-width:0;min-height:0;max-width:100vw;max-height:100vh;background:transparent;color:#fff;font-family:'Archivo',Arial,sans-serif;overflow:hidden;overflow:clip;isolation:isolate;contain:layout paint size} 2491.tv-lower-stage{position:absolute;inset:0;width:100%;height:100%;min-width:0;min-height:0;max-width:100%;max-height:100%;background:transparent;overflow:hidden;overflow:clip;isolation:isolate;contain:layout paint size} 2492.tv-lower-control{position:absolute;top:18px;left:18px;z-index:10;display:flex;align-items:center;gap:8px;padding:10px 12px;border:1px solid rgba(255,255,255,.22);border-radius:999px;background:rgba(4,25,48,.72);backdrop-filter:blur(14px);box-shadow:0 12px 30px rgba(0,0,0,.22)} 2493.tv-lower-control button,.tv-lower-control select{height:34px;border:1px solid rgba(255,255,255,.22);border-radius:999px;background:rgba(255,255,255,.1);color:#fff;font:900 12px 'Archivo',sans-serif;padding:0 12px;outline:0}.tv-lower-control select{min-width:190px}.tv-lower-control option{color:#082f5c}
2493.tv-lower-control button{cursor:pointer} 2494.tv-lower-third{position:absolute;left:2.6vw;right:2.6vw;bottom:4.4vh;height:clamp(136px,23.5vh,254px);min-width:0;min-height:0;max-height:calc(100vh - 8.8vh);display:grid;grid-template-columns:clamp(164px,13vw,260px) minmax(0,1fr);grid-template-rows:clamp(34px,4.45vh,48px) minmax(0,1fr);align-items:stretch;filter:drop-shadow(0 24px 42px rgba(0,0,0,.36));z-index:3;overflow:visible} 2495.tv-lower-third:before{display:none} 2496.tv-lower-topline{grid-column:1 / -1;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:16px;min-width:0;padding:0 20px 0 22px;border-radius:14px 14px 0 0;background:linear-gradient(90deg,rgba(5,19,34,.92),rgba(0,46,95,.88) 58%,rgba(34,180,189,.76));border:1px solid rgba(255,255,255,.18);border-bottom:0;box-shadow:inset 0 1px rgba(255,255,255,.12)} 2497.tv-lower-topline:before{content:"";position:absolute;left:22px;right:22px;bottom:0;height:1px;background:linear-gradient(90deg,#22b4bd,rgba(255,255,255,.34),transparent)} 2498.tv-lower-title-line{min-width:0;display:flex;align-items:center;gap:13px}.tv-lower-title-line:before{content:"";width:9px;height:9px;border-radius:50%;background:#22b4bd;box-shadow:0 0 18px rgba(34,180,189,.88);flex:0 0 9px} 2499.tv-lower-title-line strong{min-width:0;color:#fff;font-size:clamp(14px,1.22vw,22px);font-weight:950;line-height:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-transform:uppercase;display:flex;align-items:center;gap:11px}.tv-title-count{display:inline-flex;align-items:center;gap:11px;color:#fff;font:inherit;letter-spacing:0}.tv-title-count b{font:inherit;color:#fff}.tv-title-dot{color:#22b4bd;font:inherit;line-height:1}.tv-deps-pill{display:inline-flex;align-items:center;height:24px;padding:0 10px;border-radius:999px;background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.22);color:#9eeaf0;font-size:clamp(9px,.62vw,12px);font-weight:950;letter-spacing:.08em} 2500.tv-lower-meta{display:flex;align-items:center;justify-content:flex-end;gap:10px;min-width:0;color:rgba(255,255,255,.88);font-size:clamp(10px,.78vw,13px);font-weight:950;letter-spacing:.02em;white-space:nowrap}.tv-channel-bug{position:absolute;top:4.2vh;left:3vw;z-index:4;display:grid;gap:9px;justify-items:start;width:clamp(520px,43vw,740px);filter:drop-shadow(0 18px 34px rgba(0,0,0,.28))}.tv-bug-strip{height:62px;display:grid;grid-template-columns:.95fr 1.2fr 1.16fr;align-items:stretch;overflow:hidden;border-radius:0 999px 999px 0;border:1px solid rgba(255,255,255,.3);background:rgba(255,255,255,.1);box-shadow:inset 0 1px rgba(255,255,255,.18)}.tv-bug-part{min-width:0;display:flex;align-items:center;justify-content:center;gap:10px;padding:0 18px;border-left:1px solid rgba(255,255,255,.18);color:#fff;overflow:hidden}.tv-bug-part:first-child{border-left:0}.tv-bug-rosgan{background:#fff;justify-content:flex-start;padding-left:22px}.tv-bug-cons{background:var(--tv-cons-color,#d91549)}.tv-bug-cabana{background:var(--tv-cabana-color,#0a5ca8)}.tv-bug-logo{max-width:100%;max-height:40px;object-fit:contain;display:block}.tv-bug-rosgan .tv-bug-logo{max-width:158px;max-height:39px}.tv-bug-cons .tv-bug-logo{width:39px;height:39px;object-fit:contain;filter:brightness(0) invert(1);flex:0 0 39px}.tv-bug-cabana .tv-bug-logo{max-width:190px;max-height:42px;filter:var(--tv-cabana-logo-filter,none)}.tv-bug-cons strong,.tv-bug-cabana strong{font-size:14px;font-weight:950;line-height:1;overflow:hidden;text-overflow:ellipsis}.tv-bug-cons-name{min-width:0;max-width:190px;overflow:hidden;text-overflow:ellipsis;font-size:15px;font-weight:950}.tv-bug-live{min-height:38px;display:flex;align-items:center;justify-content:flex-start;gap:9px;padding-left:0;color:#fff;font-size:16px;font-weight:950;letter-spacing:.02em;line-height:1;max-width:100%}.tv-bug-clock,.tv-bug-live-card{display:inline-flex;align-items:center;border:1px solid rgba(255,255,255,.24);backdrop-filter:blur(10px);box-shadow:0 12px 24px rgba(2,16,31,.18)}.tv-bug-clock{min-height:36px;padding:0 14px 0 16px;gap:8px;border-radius:0 999px 999px 0;background:rgba(4,22,39,.76);font-size:16px;letter-spacing:.03em;color:#fff}.tv-bug-clock:before{content:"";width:15px;height:15px;border:2px solid rgba(255,255,255,.92);border-radius:50%;box-sizing:border-box;box-shadow:inset 4px -4px 0 -3px rgba(255,255,255,.92);flex:0 0 15px}.tv-bug-live-card{min-height:36px;min-width:0;max-width:560px;padding:0 16px;gap:10px;border-radius:999px;background:linear-gradient(90deg,rgba(0,46,95,.78),rgba(34,180,189,.54));color:#fff}.tv-bug-live-card b{display:inline-flex;align-items:center;gap:8px;color:#fff;text-transform:uppercase;font-size:15px;flex:0 0 auto}.tv-bug-live-card b:before{content:"";width:8px;height:8px;border-radius:50%;background:#ef4444;box-shadow:0 0 13px rgba(239,68,68,.95)}.tv-bug-live-card span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:rgba(255,255,255,.92);font-size:16px} 2501.tv-lower-lot-panel{grid-row:2;display:grid;grid-template-rows:auto 1fr;align-items:center;min-width:0;padding:16px 20px;border-radius:0 0 0 14px;background:linear-gradient(140deg,#002e5f,#075c9d 72%,#22b4bd);box-shadow:inset -1px 0 rgba(255,255,255,.16);overflow:hidden} 2502.tv-lower-logo{height:40px;
2502display:flex;align-items:center;justify-content:center}.tv-lower-logo img{max-width:136px;max-height:38px;object-fit:contain;filter:brightness(0) invert(1)}.tv-lower-logo strong{max-width:100%;font-size:clamp(13px,1.1vw,18px);font-weight:950;line-height:1.05;text-align:center;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical} 2503.tv-lower-lot{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;font-size:clamp(28px,2.5vw,46px);font-weight:950;line-height:.95;white-space:nowrap;text-align:center}.tv-lower-lot small{font-size:clamp(13px,1.05vw,20px);font-weight:950;line-height:1;color:rgba(255,255,255,.88);letter-spacing:.04em}.tv-lower-rps{display:none} 2504.tv-lower-table{grid-row:2;min-width:0;min-height:0;display:grid;grid-template-rows:clamp(25px,3.34vh,36px);grid-auto-rows:minmax(0,1fr);border-radius:0 0 14px 0;overflow:hidden;background:rgba(28,26,28,.93);border:1px solid rgba(255,255,255,.16);border-left:0;box-shadow:0 18px 45px rgba(0,0,0,.22)} 2505.tv-lower-row{display:grid;grid-template-columns:var(--tv-cols,1.15fr .72fr .52fr .62fr 1.2fr .78fr);min-width:0}.tv-lower-row>*{min-width:0;display:flex;align-items:center;justify-content:center;padding:3px 9px;border-left:1px solid rgba(255,255,255,.12);border-bottom:1px solid rgba(255,255,255,.12);font-size:clamp(15px,1.08vw,21px);font-weight:900;line-height:1.08;white-space:normal;overflow:hidden;text-align:center;text-overflow:clip;overflow-wrap:anywhere}.tv-lower-row>* > i{display:flex;align-items:center;justify-content:center;width:100%;max-width:100%;max-height:3.24em;line-height:1.08;font-style:italic;white-space:normal;overflow:hidden;overflow-wrap:anywhere;text-align:center}.tv-lower-row>*:first-child{border-left:0}.tv-lower-head>*{padding:2px 8px;background:rgba(255,255,255,.055);font-size:clamp(13px,1vw,19px);font-weight:950;color:#fff;letter-spacing:.02em}.tv-lower-body>*{padding:5px 9px}.tv-lower-body>*.is-long{font-size:clamp(11px,.9vw,16px)}.tv-lower-body>*.is-very-long{font-size:clamp(9.5px,.76vw,13px)}.tv-lower-body:nth-child(odd)>*{background:rgba(255,255,255,.045)} 2506.tv-lower-row.is-sold>*{background:linear-gradient(90deg,rgba(0,145,96,.34),rgba(0,145,96,.16) 42%,rgba(255,255,255,.04))!important;box-shadow:inset 0 1px rgba(31,220,150,.24),inset 0 -1px rgba(31,220,150,.2);color:#fff}.tv-lower-row.is-lifted>*{position:relative;background:linear-gradient(90deg,rgba(94,103,115,.42),rgba(94,103,115,.19) 42%,rgba(255,255,255,.025))!important;box-shadow:inset 0 1px rgba(203,213,225,.22),inset 0 -1px rgba(203,213,225,.16);color:rgba(255,255,255,.48);filter:saturate(.55)}.tv-lower-row.is-lifted>* > i{text-decoration:line-through;text-decoration-thickness:2px;text-decoration-color:rgba(226,232,240,.72);text-underline-offset:0}.tv-lower-row.is-sold>*:first-child,.tv-lower-row.is-lifted>*:first-child{position:relative}.tv-lower-row.is-sold>*:first-child:before,.tv-lower-row.is-lifted>*:first-child:before{content:"";position:absolute;left:0;top:0;bottom:0;width:7px}.tv-lower-row.is-sold>*:first-child:before{background:#12b981;box-shadow:0 0 16px rgba(18,185,129,.68)}.tv-lower-row.is-lifted>*:first-child:before{background:#94a3b8;box-shadow:0 0 14px rgba(148,163,184,.42)} 2507.tv-lower-sale-cell{padding-left:8px!important;padding-right:8px!important;text-align:center}.tv-lower-sale-cell .tv-roll{width:100%;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;font-size:clamp(13px,.98vw,18px);font-style:normal;text-transform:uppercase;letter-spacing:.01em;overflow:visible;text-overflow:clip;white-space:normal;text-decoration:none!important}.tv-lower-sale-cell.is-sold{background:rgba(0,145,96,.96)!important;box-shadow:inset 0 0 0 1px rgba(255,255,255,.12),0 0 18px rgba(0,145,96,.18)}.tv-lower-sale-cell.is-lifted{background:rgba(100,116,139,.9)!important;box-shadow:inset 0 0 0 1px rgba(255,255,255,.1),0 0 14px rgba(100,116,139,.16)}.tv-lower-sale-cell.is-empty{background:rgba(255,255,255,.05)!important;color:rgba(255,255,255,.68)}.tv-lower-row.is-lifted .tv-lower-sale-cell .tv-roll{text-decoration:none!important;color:#fff} 2508.tv-lower-table.is-many .tv-lower-row>*{font-size:clamp(14px,1.02vw,20px);padding:0 8px}.tv-lower-table.is-many .tv-lower-head>*{font-size:clamp(11px,.84vw,16px)}.tv-lower-table.is-many .tv-lower-body>*.is-long{font-size:clamp(11px,.9vw,16px)}.tv-lower-table.is-many .tv-lower-body>*.is-very-long{font-size:clamp(9.5px,.76vw,13px)}.tv-lower-table.is-max .tv-lower-row>*{font-size:clamp(12px,.82vw,16px);padding:0 6px}.tv-lower-table.is-max .tv-lower-head>*{font-size:clamp(10px,.7vw,14px)}.tv-lower-table.is-max .tv-lower-body>*.is-long{font-size:clamp(10px,.76vw,13px)}.tv-lower-table.is-max .tv-lower-body>*.is-very-long{font-size:clamp(8.5px,.68vw,11.5px)} 2509.tv-lower-table.is-deps .tv-lower-row>*{font-size:clamp(15px,1.08vw,21px);font-style:normal}.tv-lower-table.is-deps.is-single-dep{grid-template-rows:repeat(3,minmax(0,1fr));grid-auto-rows:minmax(0,1fr)}.tv-lower-table.is-deps.is-single-dep .tv-lower-body>*:first-child{justify-content:center;padding-left:6px;padding-right:6px;color:#9eeaf0;text-transform:uppercase;letter-spacing:.04em;font-size:clamp(12px,.82vw,16px)}.tv-lower-table.is-deps.is-single-dep .tv-lower-body:first-of-type>*{background:rgba(255,255,255,.055);color:#fff;text-transform:uppercase;letter-spacing:.03em}.tv-lower-table.is-deps.is-single-dep .tv-lower-row>*{font-size:clamp(14px,1.02vw,20px);padding-left:9px;padding-right:9px}.tv-lower-table.is-deps.is-single-dep .tv-lower-body>* > i{font-style:normal} 2510.tv-lower-empty{position:absolute;
2510left:50%;bottom:8vh;transform:translateX(-50%);padding:18px 22px;border-radius:16px;background:rgba(0,46,95,.86);font-weight:900} 2511.tv-config-page{min-height:100vh;background:#eef4fa;color:#082033;font-family:'Archivo',Arial,sans-serif;padding:28px 22px 60px;box-sizing:border-box}.tv-config-shell{max-width:1180px;margin:0 auto}.tv-config-back{border:0;background:transparent;color:#0a5ca8;font:900 13px 'Archivo',sans-serif;cursor:pointer;padding:0;margin-bottom:14px}.tv-config-hero{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:18px;align-items:center;padding:22px;border-radius:16px;background:linear-gradient(135deg,#002e5f,#0a5ca8 64%,#22b4bd);color:#fff;box-shadow:0 18px 42px rgba(0,46,95,.16)}.tv-config-hero small{font-size:11px;font-weight:950;letter-spacing:.16em;text-transform:uppercase;color:#9eeaf0}.tv-config-hero h1{margin:6px 0 8px;font-size:clamp(30px,4vw,56px);line-height:.96}.tv-config-hero p{margin:0;max-width:720px;color:rgba(255,255,255,.86);font-size:14px}.tv-config-live{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:999px;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);font-weight:950}.tv-config-switch{width:58px;height:32px;border:0;border-radius:999px;background:#94a3b8;padding:4px;cursor:pointer;transition:background .2s}.tv-config-switch span{display:block;width:24px;height:24px;border-radius:50%;background:#fff;box-shadow:0 4px 10px rgba(0,0,0,.18);transition:transform .2s}.tv-config-switch.is-on{background:#12b981}.tv-config-switch.is-on span{transform:translateX(26px)}.tv-config-panel{margin-top:16px;border:1px solid #d8e5f1;border-radius:14px;background:#fff;box-shadow:0 14px 36px rgba(15,42,70,.07);overflow:hidden}.tv-config-toolbar{display:grid;grid-template-columns:auto minmax(260px,1fr) auto;gap:10px;align-items:center;padding:14px;border-bottom:1px solid #e1eaf2}.tv-config-toolbar button,.tv-config-toolbar select{height:42px;border:1px solid #cfe0f1;border-radius:12px;background:#fff;color:#082f5c;font:900 13px 'Archivo',sans-serif;padding:0 13px}
2511.tv-config-toolbar button{cursor:pointer;background:#f5f9fd}.tv-config-toolbar select{width:100%}.tv-config-fields{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:16px;align-items:start}.tv-config-card{border:1px solid #dce7f2;border-radius:12px;background:#f8fbfe;padding:14px}.tv-config-card h2{margin:0 0 10px;font-size:16px}.tv-config-checks{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px}.tv-config-check{display:flex;align-items:center;gap:8px;border:1px solid #dbe7f2;border-radius:10px;background:#fff;padding:9px 10px;color:#173a5d;font-weight:900;font-size:12px}.tv-config-check input{accent-color:#0a5ca8}.tv-config-list{display:grid;gap:6px;max-height:640px;overflow:auto;padding-right:4px}.tv-config-lot{display:grid;grid-template-columns:54px minmax(0,1fr) 42px;gap:8px;align-items:center;border:1px solid #dce7f2;border-radius:9px;background:#fff;padding:7px 9px;cursor:pointer;min-height:40px}.tv-config-lot.is-active{border-color:#0a5ca8;box-shadow:0 0 0 2px rgba(10,92,168,.1)}.tv-config-lot strong{font-size:15px;color:#002e5f}.tv-config-lot span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:900;font-size:12.5px}.tv-config-lot small{color:#65758a;font-weight:900;font-size:11px;text-align:right}@media(max-width:760px){.tv-config-page{padding:18px 10px 40px}.tv-config-hero,.tv-config-toolbar,.tv-config-fields{grid-template-columns:1fr}.tv-config-live{justify-content:space-between}.tv-config-toolbar{gap:8px}.tv-config-fields{padding:10px}.tv-config-lot{grid-template-columns:50px minmax(0,1fr) 38px;padding:7px 8px}} 2512.tv-roll{position:relative;display:inline-block;max-width:100%;overflow:visible;text-overflow:ellipsis;white-space:nowrap;line-height:1.22;vertical-align:middle;backface-visibility:hidden;padding-right:.08em}.tv-roll.is-a{animation:tvWipeInA .42s cubic-bezier(.2,.72,.24,1) both}.tv-roll.is-b{animation:tvWipeInB .42s cubic-bezier(.2,.72,.24,1) both}.tv-lower-row .tv-roll{display:flex;align-items:center;justify-content:center}.tv-lower-row>*{overflow:hidden}.tv-lower-row>* .tv-roll{max-width:calc(100% - 2px);overflow:hidden;text-overflow:clip;white-space:normal;overflow-wrap:anywhere;text-align:center}.tv-lower-title-line .tv-roll,.tv-lower-lot .tv-roll{max-width:100%} 2513@keyframes tvWipeInA{from{opacity:.18;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}@keyframes tvWipeInB{from{opacity:.18;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}} 2514html[data-public-advance="lacassina"] sc-if[value="{{ profileCropOpen }}"], 2515html[data-public-advance="lacassina"] sc-if[value="{{ profileEditorOpen }}"], 2516html[data-public-advance="lacassina"] sc-if[value="{{ profilePhotoViewerOpen }}"], 2517html[data-public-advance="lacassina"] sc-if[value="{{ adminUsersOpen }}"], 2518html[data-public-advance="lacassina"] sc-if[value="{{ notificationsOpen }}"]{display:none!important} 2519.dc-lot-contact-card{background:#fff;border:1px solid #e1e9f1;border-radius:15px;padding:15px;box-shadow:0 12px 30px rgba(15,42,70,.08)} 2520.dc-cabana-catalog-card{display:flex;flex-direction:column;min-width:0}.dc-cabana-card-rp{max-width:calc(100% - 112px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dc-cabana-card-title{display:-webkit-box!important;overflow:hidden;line-height:1.12;-webkit-box-orient:vertical;-webkit-line-clamp:2}.dc-cabana-card-fact{min-width:0}.dc-cabana-card-fact strong{display:-webkit-box;overflow:hidden;line-height:1.22;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow-wrap:anywhere} 2521.dc-prebid-side-card{position:relative;isolation:isolate;display:grid;gap:13px;overflow:hidden;padding:16px;border-radius:17px;background:linear-gradient(120deg,#002e5f,#0a5ca8,#087d79);background-size:190% 190%;color:#fff;box-shadow:0 16px 34px rgba(0,46,95,.2);animation:dcPrebidCardFlow 5s ease-in-out infinite} 2522.dc-prebid-side-card::after{content:"";position:absolute;z-index:0;inset:-55% auto -55% -45%;width:42%;transform:rotate(18deg) translateX(-220%);background:linear-gradient(90deg,transparent,rgba(255,255,255,.2),transparent);animation:dcPrebidCardSweep 4.8s ease-in-out infinite;pointer-events:none} 2523.dc-prebid-side-head{position:relative;z-index:1;display:flex;align-items:center;gap:10px;min-width:0} 2524.dc-prebid-side-icon{display:grid;place-items:center;width:38px;height:38px;flex:0 0 38px;border-radius:12px;background:rgba(255,255,255,.14)} 2525.dc-prebid-side-icon img{width:19px;height:19px;filter:brightness(0) invert(1)} 2526.dc-prebid-side-copy{min-width:0}.dc-prebid-side-copy small{display:flex;align-items:center;gap:6px;color:#9be6d0;font-size:9px;font-weight:900;letter-spacing:.12em}.dc-prebid-side-copy small::before{content:"";width:7px;height:7px;flex:0 0 7px;border-radius:50%;
2526background:#44e0a5;box-shadow:0 0 0 0 rgba(68,224,165,.6);animation:dcPrebidActivePulse 1.55s ease-out infinite}.dc-prebid-side-copy strong{display:block;margin-top:3px;font-size:15px;line-height:1.15} 2527.dc-prebid-side-actions{position:relative;z-index:1;display:grid;grid-template-columns:minmax(0,1fr);align-items:stretch;gap:9px} 2528.dc-prebid-history-button{display:flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:8px 12px;border:1px solid rgba(255,255,255,.35);border-radius:12px;background:transparent;color:#fff;font-family:inherit;font-size:12px;font-weight:700;cursor:pointer} 2529.dc-prebid-history-button:hover{background:rgba(255,255,255,.12)} 2530.dc-prebid-history-button:focus-visible{outline:2px solid #fff;outline-offset:2px} 2531.dc-prebid-side-amount{display:grid;align-content:center;gap:2px;min-width:0;padding:9px 11px;border:1px solid rgba(255,255,255,.2);border-radius:12px;background:rgba(255,255,255,.1)} 2532.dc-prebid-side-amount small{font-size:8px;font-weight:900;letter-spacing:.1em;opacity:.72}.dc-prebid-side-amount strong{overflow:hidden;font-size:17px;font-weight:950;font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap} 2533.dc-prebid-side-button{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:47px;border:0;border-radius:12px;padding:0 14px;background:#fff;color:#002e5f;cursor:pointer;font:900 12px inherit;box-shadow:0 9px 20px rgba(0,0,0,.15);transition:background-color .28s ease,color .28s ease,border-radius .32s ease,box-shadow .32s ease;animation:dcPrebidButtonBreathe 3.1s ease-in-out infinite} 2534.dc-prebid-side-button img{width:17px;height:17px} 2535.dc-prebid-side-button.is-position-transitioning{animation:none!important;will-change:transform,opacity} 2536main:has(.dc-prebid-side-card){animation:none!important;transform:none!important} 2537.dc-prebid-side-button.is-scroll-floating{position:fixed;z-index:56;top:76px;right:24px;bottom:auto;left:auto;width:auto;min-width:154px;min-height:50px;border:1px solid rgba(255,255,255,.36);border-radius:999px;padding:0 20px;background:linear-gradient(135deg,#087d55,#0aa36e);color:#fff;font-size:13px;box-shadow:0 17px 38px rgba(0,57,44,.3),0 0 0 5px rgba(18,185,129,.11);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);animation:dcPrebidMobileFloat 3.1s ease-in-out infinite} 2538.dc-prebid-side-button.is-scroll-floating img{width:20px;height:20px;filter:brightness(0) invert(1)} 2539@media(max-width:760px){ 2540 .dc-prebid-side-button.is-scroll-floating{top:auto;right:max(14px,env(safe-area-inset-right));bottom:calc(76px + env(safe-area-inset-bottom));min-width:148px;padding:0 19px} 2541 .dc-prebid-side-button.is-scroll-floating.is-above-load-fab{bottom:calc(140px + env(safe-area-inset-bottom))} 2542} 2543@keyframes dcPrebidCardFlow{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}} 2544@keyframes dcPrebidCardSweep{0%,52%{transform:rotate(18deg) translateX(-220%);opacity:0}64%{opacity:1}82%,100%{transform:rotate(18deg) translateX(520%);opacity:0}} 2545@keyframes dcPrebidActivePulse{0%{box-shadow:0 0 0 0 rgba(68,224,165,.62)}72%,100%{box-shadow:0 0 0 9px rgba(68,224,165,0)}} 2546@keyframes dcPrebidButtonBreathe{0%,72%,100%{transform:translateY(0);box-shadow:0 9px 20px rgba(0,0,0,.15)}82%{transform:translateY(-2px);box-shadow:0 13px 28px rgba(0,0,0,.24),0 0 0 4px rgba(255,255,255,.12)}} 2547@keyframes dcPrebidMobileFloat{0%,68%,100%{transform:translateY(0);box-shadow:0 17px 38px rgba(0,57,44,.3),0 0 0 5px rgba(18,185,129,.11)}80%{transform:translateY(-4px);box-shadow:0 22px 44px rgba(0,57,44,.38),0 0 0 8px rgba(18,185,129,.08)}} 2548@media(prefers-reduced-motion:reduce){.dc-prebid-side-card,.dc-prebid-side-card::after,.dc-prebid-side-copy small::before,.dc-prebid-side-button{animation:none!important;transition:none!important}} 2549.cb-prebid-admin-access{display:flex;justify-content:flex-end;margin:-4px 0 18px}.cb-prebid-admin-access button{display:inline-flex;align-items:center;gap:8px;min-height:43px;border:1px solid #b9d8f2;border-radius:12px;padding:9px 14px;background:linear-gradient(135deg,#002e5f,#0a5ca8);color:#fff;font:900 12px 'Archivo',sans-serif;box-shadow:0 11px 25px rgba(0,46,95,.18);cursor:pointer}.cb-prebid-admin-access button img{width:17px;height:17px;filter:brightness(0) invert(1)}.cb-prebid-admin-access button i{margin-left:3px}.cb-prebid-admin-live{display:inline-block;width:8px;height:8px;flex:0 0 8px;border-radius:50%;background:#36d69b;box-shadow:0 0 0 0 rgba(54,214,155,.55);animation:cbPrebidAdminPulse 1.7s ease-out infinite} 2550.cb-prebid-admin-panel{margin:0 0 25px;overflow:hidden;border:1px solid #cbdfee;border-radius:21px;background:#fff;box-shadow:0 18px 44px rgba(0,46,95,.11);animation:cbPrebidAdminIn .3s ease both}.cb-prebid-admin-head{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding:21px 22px;background:linear-gradient(125deg,#002e5f,#0a5ca8 62%,#087d79);color:#fff}.cb-prebid-admin-head small{color:#90e7d1;font-size:9px;font-weight:950;letter-spacing:.14em}.cb-prebid-admin-head h2{margin:5px 0 3px;font-size:24px}.cb-prebid-admin-head p{margin:0;color:rgba(255,255,255,.78);font-size:12px}.cb-prebid-admin-head-actions{display:flex;align-items:center;justify-content:flex-end;
2550gap:8px;flex-wrap:wrap}.cb-prebid-admin-head-actions>span{display:inline-flex;align-items:center;gap:7px;color:rgba(255,255,255,.82);font-size:11px;font-weight:800}.cb-prebid-admin-head-actions button{width:38px;height:38px;display:grid;place-items:center;border:1px solid rgba(255,255,255,.24);border-radius:11px;background:rgba(255,255,255,.11);cursor:pointer}.cb-prebid-admin-head-actions button img{width:16px;height:16px;filter:brightness(0) invert(1)} 2551.cb-prebid-admin-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;padding:16px 18px 0}.cb-prebid-admin-metrics article{min-width:0;padding:13px 14px;border:1px solid #e0eaf3;border-radius:13px;background:#f5f9fd}.cb-prebid-admin-metrics small{display:block;color:#75879a;font-size:9px;font-weight:900;letter-spacing:.07em;text-transform:uppercase}.cb-prebid-admin-metrics strong{display:block;margin-top:5px;color:#082e57;font-size:19px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} 2552.cb-prebid-admin-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px}.cb-prebid-admin-toolbar label{flex:1 1 360px;min-width:0;display:flex;align-items:center;gap:9px;border:1px solid #d7e3ed;border-radius:12px;background:#fff;padding:0 12px}.cb-prebid-admin-toolbar label img{width:16px;height:16px;opacity:.58}.cb-prebid-admin-toolbar input{width:100%;min-width:0;height:42px;border:0;outline:0;background:transparent;font:13px 'Archivo',sans-serif}.cb-prebid-admin-toolbar>div{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.cb-prebid-admin-filter{border:1px solid #d4e1ec;border-radius:999px;padding:8px 11px;background:#f7fafc;color:#486078;font:800 11px 'Archivo',sans-serif;cursor:pointer}.cb-prebid-admin-filter.is-active{border-color:#0a5ca8;background:#0a5ca8;color:#fff} 2553.cb-prebid-admin-list{display:grid;gap:10px;padding:0 18px 19px}.cb-prebid-admin-row{display:grid;grid-template-columns:minmax(150px,.6fr) minmax(210px,1fr) minmax(250px,1.35fr);gap:16px;align-items:start;padding:16px;border:1px solid #dce7f0;border-radius:15px;background:#fbfdff}.cb-prebid-admin-row small{display:block;color:#8090a2;font-size:9px;font-weight:900;letter-spacing:.08em}.cb-prebid-admin-offer{display:grid;justify-items:start;gap:5px}.cb-prebid-admin-offer>strong{color:#002e5f;font-size:24px}.cb-prebid-admin-offer>small{font-size:10px;letter-spacing:0}.cb-prebid-admin-status{display:inline-flex;border-radius:999px;padding:5px 8px;font-size:9px;font-weight:900}.cb-prebid-admin-status.is-current{background:#dff7ec;color:#08784f}.cb-prebid-admin-status.is-pending{background:#fff1cc;color:#906100}.cb-prebid-admin-status.is-outbid{background:#eaf0f6;color:#4b6278}.cb-prebid-admin-buyer,.cb-prebid-admin-lot{display:grid;gap:4px;min-width:0}.cb-prebid-admin-buyer strong,.cb-prebid-admin-lot strong{color:#10243a;font-size:14px;overflow-wrap:anywhere}.cb-prebid-admin-buyer a,.cb-prebid-admin-buyer span,.cb-prebid-admin-lot>span{color:#587087;font-size:11px;overflow-wrap:anywhere}.cb-prebid-admin-buyer em{width:max-content;max-width:100%;margin-top:3px;border-radius:8px;padding:5px 7px;background:#f2eafd;color:#6941a5;font-size:10px;font-style:normal;font-weight:800}.cb-prebid-admin-animals{display:grid;gap:5px;margin-top:6px}.cb-prebid-admin-animals>span{display:flex;align-items:flex-start;gap:7px;padding:7px 8px;border-radius:9px;background:#edf5fb;color:#496178;font-size:10px;line-height:1.35}.cb-prebid-admin-animals b{flex:0 0 auto;color:#075ea9}.cb-prebid-admin-message{margin:0 18px 18px;padding:24px;border:1px dashed #c9d9e7;border-radius:14px;background:#f8fbfd;color:#64788c;text-align:center;font-size:12px}.cb-prebid-admin-message.is-error{border-color:#efc5ca;background:#fff5f6;color:#ae2634} 2554.cb-prebid-page{min-height:100vh;background:#f3f7fb;padding:28px clamp(14px,4vw,36px) 110px}.cb-prebid-page-shell{width:min(1180px,100%);margin:0 auto}.cb-prebid-page-back{display:inline-flex;align-items:center;gap:7px;border:0;background:transparent;color:#075ea9;padding:0;font:850 13px 'Archivo',sans-serif;cursor:pointer}.cb-prebid-page-title{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;margin:18px 0 20px}.cb-prebid-page-title small{color:#087d79;font-size:10px;font-weight:950;letter-spacing:.15em}.cb-prebid-page-title h1{margin:5px 0 4px;color:#10243a;font-size:clamp(28px,4vw,42px);line-height:1.05}.cb-prebid-page-title p{margin:0;color:#64788c;font-size:13px}.cb-prebid-page-live{display:inline-flex;align-items:center;gap:8px;flex:0 0 auto;border:1px solid #c9e9df;border-radius:999px;background:#e9f8f3;color:#08784f;padding:9px 12px;font-size:11px;font-weight:850}.cb-prebid-admin-panel.is-page{margin-bottom:0} 2555.cb-prebid-lot-picker{margin:0 18px 16px;padding:16px;border:1px solid #d8e5ef;border-radius:17px;background:linear-gradient(145deg,#f8fbfe,#eef5fa)}.cb-prebid-lot-picker-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;margin-bottom:12px}.cb-prebid-lot-picker-head small{display:block;color:#087d79;font-size:9px;font-weight:950;letter-spacing:.14em}.cb-prebid-lot-picker-head strong{display:block;margin-top:3px;color:#17314a;font-size:16px}.cb-prebid-lot-picker-head span{color:#6a7f92;font-size:11px}.cb-prebid-lot-picker-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:9px;max-height:224px;overflow:auto;padding:1px 3px 3px 1px}.cb-prebid-lot-option{min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:12px;border:1px solid #d4e1eb;border-radius:13px;padding:11px 12px;background:#fff;color:#29445d;text-align:left;cursor:pointer;transition:border-color .18s ease,box-shadow .18s ease,transform .18s ease,background .18s ease}.cb-prebid-lot-option:hover{transform:translateY(-1px);border-color:#83b8de;box-shadow:0 8px 18px rgba(0,46,95,.08)}.cb-prebid-lot-option.is-active{border-color:#087d79;background:linear-gradient(135deg,#e9f8f3,#f5fbfa);box-shadow:0 0 0 2px rgba(8,125,121,.12),0 9px 22px rgba(8,125,121,.1)}.cb-prebid-lot-option-main{min-width:0}.cb-prebid-lot-option-main strong{display:block;overflow:hidden;color:#12304d;font-size:13px;text-overflow:ellipsis;white-space:nowrap}.cb-prebid-lot-option-main small{display:block;overflow:hidden;margin-top:4px;color:#718498;font-size:10px;text-overflow:ellipsis;white-space:nowrap}.cb-prebid-lot-option-side{text-align:right}.cb-prebid-lot-option-side b{display:block;color:#006b55;font-size:12px}
2555.cb-prebid-lot-option-side span{display:block;margin-top:3px;color:#718498;font-size:9px;font-weight:800}.cb-prebid-selected-context{display:flex;align-items:center;gap:8px;margin:0 18px 2px;color:#526b82;font-size:11px;font-weight:750}.cb-prebid-selected-context strong{color:#12304d} 2556@keyframes cbPrebidAdminPulse{70%,100%{box-shadow:0 0 0 8px rgba(54,214,155,0)}}@keyframes cbPrebidAdminIn{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}} 2557@media(max-width:820px){.cb-prebid-admin-head{display:grid}.cb-prebid-admin-head-actions{justify-content:flex-start}.cb-prebid-admin-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.cb-prebid-admin-toolbar{align-items:stretch;flex-direction:column}.cb-prebid-admin-toolbar label{flex-basis:auto}.cb-prebid-admin-row{grid-template-columns:1fr 1fr}.cb-prebid-admin-lot{grid-column:1/-1}} 2558@media(max-width:620px){.cb-prebid-page{padding-top:20px}.cb-prebid-page-title{display:grid;align-items:start}.cb-prebid-page-live{width:max-content}.cb-prebid-page-title h1{font-size:28px}.cb-prebid-lot-picker{margin:0 12px 13px;padding:13px}.cb-prebid-lot-picker-head{display:grid;gap:4px}.cb-prebid-lot-picker-grid{grid-template-columns:1fr;max-height:245px}.cb-prebid-selected-context{margin-inline:12px}} 2559@media(max-width:540px){.cb-prebid-admin-access{justify-content:stretch}.cb-prebid-admin-access button{width:100%;justify-content:center}.cb-prebid-admin-panel{border-radius:17px}.cb-prebid-admin-head{padding:18px}.cb-prebid-admin-metrics{padding:12px 12px 0;gap:7px}.cb-prebid-admin-metrics article{padding:11px}.cb-prebid-admin-metrics strong{font-size:16px}.cb-prebid-admin-toolbar{padding:12px}.cb-prebid-admin-toolbar>div{display:grid;grid-template-columns:repeat(2,1fr)}.cb-prebid-admin-filter{width:100%}.cb-prebid-admin-list{padding:0 12px 14px}.cb-prebid-admin-row{grid-template-columns:1fr;gap:13px;padding:14px}.cb-prebid-admin-lot{grid-column:auto}} 2560@media(prefers-reduced-motion:reduce){.cb-prebid-admin-live,.cb-prebid-admin-panel{animation:none!important}} 2561.dc-lot-contact-head{display:flex;align-items:center;gap:12px;min-width:0} 2562.dc-consignataria-logo-box{display:grid;place-items:center;box-sizing:border-box;padding:3px;line-height:0} 2563.dc-consignataria-logo-box img{display:block;width:auto!important;height:auto!important;max-width:100%!important;max-height:100%!important;object-fit:contain;object-position:center} 2564.rg-detail-list{display:grid;gap:0} 2565.rg-detail-row.dc-rosgan-field-card{grid-template-columns:42px minmax(0,1fr);min-height:52px;padding:8px 0;background:transparent;border:0;border-bottom:1px solid #e9eef4;border-radius:0;box-shadow:none} 2566.rg-detail-icon{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;flex:none;background:transparent;border:0;box-shadow:none;color:#446783} 2567.rg-detail-icon i{display:block;width:34px;height:34px;background-image:url('assets/remates/rosgan-field-icons-v2.png');background-repeat:no-repeat;background-size:800% 500%;background-color:transparent} 2568.rg-detail-row:last-child{border-bottom:0} 2569.rg-detail-row .dc-rosgan-field-copy{display:grid;grid-template-columns:minmax(120px,34%) minmax(0,1fr);align-items:center;gap:12px} 2570.rg-detail-row .dc-rosgan-field-copy strong{font-size:14px;overflow-wrap:normal;word-break:normal} 2571@media(max-width:480px){.rg-detail-row .dc-rosgan-field-copy{grid-template-columns:minmax(90px,38%) minmax(0,1fr);gap:8px}} 2572.dc-rosgan-field-card{display:grid;grid-template-columns:42px minmax(0,1fr);align-items:center;gap:10px;min-width:0;min-height:68px;background:#f5f8fb;border:1px solid #edf2f7;border-radius:12px;padding:9px 11px 9px 9px;box-sizing:border-box} 2573.dc-rosgan-field-icon-shell{display:grid;place-items:center;width:40px;height:40px;border-radius:11px;background:#fff;border:1px solid #dce7f1;box-shadow:0 4px 12px rgba(0,46,95,.06);overflow:hidden} 2574.dc-rosgan-field-icon{display:block;width:32px;height:32px;background-image:url('assets/remates/rosgan-field-icons-v1.png');background-repeat:no-repeat;background-size:800% 500%;background-color:transparent} 2575.dc-rosgan-field-copy{display:block;min-width:0} 2576.dc-rosgan-field-copy small{display:block;color:#7e90a6;font-size:11px;font-weight:800;line-height:1.15} 2577.dc-rosgan-field-copy strong{display:block;margin-top:2px;font-size:14px;line-height:1.18;color:#112033;overflow-wrap:anywhere} 2578.dc-rosgan-observations-head{display:flex;align-items:center;gap:9px;color:#8593a6;font-weight:900;letter-spacing:.11em} 2579.dc-rosgan-observations-head img{display:block;width:30px;height:30px;object-fit:contain;flex:none} 2580.dc-lot-contact-avatar{width:52px;height:52px;flex:0 0 52px;display:grid;place-items:center;border-radius:50%;padding:9px;box-sizing:border-box;box-shadow:0 7px 18px rgba(15,42,70,.16);line-height:0} 2581.dc-lot-contact-avatar img{display:block;width:auto!important;height:auto!important;max-width:28px!important;max-height:28px!important;object-fit:contain;object-position:center} 2582.dc-lot-contact-avatar.dc-representative-photo{padding:0;overflow:hidden} 2583.dc-lot-contact-avatar.dc-representative-photo img{width:52px!important;height:52px!important;max-width:none!important;max-height:none!important;object-fit:cover;border-radius:50%}
2584.rg-lot-navigation{display:flex;gap:8px;flex-wrap:wrap} 2585.rg-lot-navigation button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:9px 14px;border:1px solid #cfe0ef;border-radius:999px;background:#fff;color:#002e5f;font:inherit;font-size:12px;font-weight:750;cursor:pointer;box-shadow:0 3px 10px #002e5f06} 2586.rg-lot-navigation button:hover{background:#edf5fb;border-color:#8eaecb} 2587.rg-lot-navigation button:focus-visible{outline:3px solid #0a5ca8;outline-offset:3px} 2588.rg-lot-navigation img{width:15px;height:15px} 2589.dc-lot-contact-avatar strong{color:#fff;font-size:18px} 2590.dc-lot-contact-copy{display:grid;gap:2px;min-width:0} 2591.dc-lot-contact-copy small{color:#8493a7;font-size:9px;font-weight:900;letter-spacing:.1em} 2592.dc-lot-contact-copy strong{color:#10233b;font-size:16px;line-height:1.2} 2593.dc-lot-contact-copy span{color:#607188;font-size:12px;font-weight:600} 2594.dc-lot-contact-actions{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(92px,.65fr);gap:8px;margin-top:14px} 2595.dc-lot-contact-actions a{min-height:42px;display:flex;align-items:center;justify-content:center;gap:8px;border-radius:10px;padding:9px 11px;box-sizing:border-box;font:800 11px 'Archivo',sans-serif;text-decoration:none;cursor:pointer;transition:background-color .18s ease,transform .18s ease,box-shadow .18s ease} 2596.dc-lot-contact-actions a:hover{transform:translateY(-1px)} 2597.dc-lot-contact-actions img{width:16px;height:16px;flex:0 0 16px} 2598.dc-contact-whatsapp{border:1px solid #168c5a;background:#168c5a;color:#fff!important;box-shadow:0 8px 18px rgba(22,140,90,.2)} 2599.dc-contact-whatsapp:hover{background:#11784c;color:#fff!important;box-shadow:0 10px 22px rgba(22,140,90,.26)} 2600.dc-contact-call{border:1px solid #d5e2ef;background:#edf4fb;color:#003b73!important} 2601.dc-contact-call:hover{background:#e2edf8;color:#002e5f!important} 2602.dc-lot-social-actions{display:flex;align-items:center;gap:9px;flex-wrap:wrap} 2603.dc-lot-social-button{min-height:40px;display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1px solid #d2e2f1;border-radius:999px;background:#fff;color:#0b315d;padding:8px 13px;font:900 12px 'Archivo',sans-serif;box-shadow:0 8px 22px rgba(15,42,70,.08);cursor:pointer;transition:transform .18s ease,box-shadow .18s ease,border-color .18s ease,background-color .18s ease} 2604.dc-lot-social-button:hover{transform:translateY(-1px);box-shadow:0 12px 28px rgba(15,42,70,.12)} 2605.dc-lot-social-button img{width:16px;height:16px;flex:0 0 16px} 2606.dc-lot-social-button strong{min-width:18px;text-align:center;font-size:13px} 2607.dc-lot-social-button--instagram{border-color:rgba(255,255,255,.28);background:linear-gradient(135deg,#833ab4 0%,#fd1d1d 48%,#fcb045 100%);color:#fff;box-shadow:0 10px 24px rgba(193,53,132,.24)} 2608.dc-lot-social-button--instagram:hover{box-shadow:0 14px 30px rgba(193,53,132,.32)} 2609.dc-lot-social-button--whatsapp{border-color:#168c5a;background:#168c5a;color:#fff;box-shadow:0 10px 24px rgba(22,140,90,.22)} 2610.dc-lot-social-button--whatsapp:hover{background:#11784c;box-shadow:0 14px 30px rgba(22,140,90,.3)} 2611.dc-catalog-export-actions{display:flex;align-items:center;gap:9px;flex-wrap:wrap} 2612.dc-catalog-export-button{min-height:38px;display:inline-flex;align-items:center;justify-content:center;gap:8px;border:0;border-radius:11px;padding:9px 12px;color:#fff;font:900 12px 'Archivo',sans-serif;cursor:pointer;box-shadow:0 10px 24px rgba(15,42,70,.13);transition:transform .18s ease,box-shadow .18s ease,filter .18s ease} 2613.dc-catalog-export-button:hover{transform:translateY(-1px);box-shadow:0 14px 30px rgba(15,42,70,.18);filter:saturate(1.05)} 2614.dc-catalog-export-button img{width:16px;height:16px;flex:0 0 16px;filter:brightness(0) invert(1)} 2615.dc-catalog-export-button.is-excel{background:#107c41} 2616@media(max-width:680px){.dc-catalog-export-actions{width:100%;display:grid;grid-template-columns:1fr}.dc-catalog-export-button{width:100%;padding:10px 9px;font-size:11px}} 2617.dc-lucide{display:inline-block;width:1em;height:1em;flex:0 0 auto;background:currentColor;vertical-align:-.14em;-webkit-mask:var(--dc-icon) center/contain no-repeat;mask:var(--dc-icon) center/contain no-repeat} 2618.dc-lucide-play{--dc-icon:url('/assets/icons/lucide/play.svg')}.dc-lucide-x{--dc-icon:url('/assets/icons/lucide/x.svg')}.dc-lucide-check{--dc-icon:url('/assets/icons/lucide/check.svg')} 2619.dc-lucide-search{--dc-icon:url('/assets/icons/lucide/search.svg')}.dc-lucide-plus{--dc-icon:url('/assets/icons/lucide/plus.svg')}.dc-lucide-minus{--dc-icon:url('/assets/icons/lucide/minus.svg')} 2620.dc-lucide-arrow-right{--dc-icon:url('/assets/icons/lucide/arrow-right.svg')}.dc-lucide-arrow-left{--dc-icon:url('/assets/icons/lucide/arrow-left.svg')}.dc-lucide-chevron-down{--dc-icon:url('/assets/icons/lucide/chevron-down.svg')} 2621.dc-lucide-map-pin{--dc-icon:url('/assets/icons/lucide/map-pin.svg')}.dc-lucide-calendar{--dc-icon:url('/assets/icons/lucide/calendar-days.svg')}.dc-lucide-radio-tower{--dc-icon:url('/assets/icons/lucide/radio-tower.svg')} 2622.dc-lucide-rotate{--dc-icon:url('/assets/icons/lucide/rotate-ccw.svg')}.dc-lucide-refresh-cw{--dc-icon:url('/assets/icons/lucide/refresh-cw.svg')}.dc-lucide-grid{--dc-icon:url('/assets/icons/lucide/grid-2x2.svg')}.dc-lucide-list{--dc-icon:url('/assets/icons/lucide/list.svg')} 2623.dc-lucide-trending-up{--dc-icon:url('/assets/icons/lucide/trending-up.svg')}.dc-lucide-trending-down{--dc-icon:url('/assets/icons/lucide/trending-down.svg')}.dc-lucide-info{--dc-icon:url('/assets/icons/lucide/info.svg')}.dc-lucide-warning{--dc-icon:url('/assets/icons/lucide/triangle-alert.svg')} 2624.dc-lucide-circle-x{--dc-icon:url('/assets/icons/lucide/circle-x.svg')}.dc-lucide-bold{--dc-icon:url('/assets/icons/lucide/bold.svg')}.dc-lucide-italic{--dc-icon:url('/assets/icons/lucide/italic.svg')}.dc-lucide-underline{--dc-icon:url('/assets/icons/lucide/underline.svg')} 2625.dc-lucide-chevron-up{--dc-icon:url('/assets/icons/lucide/chevron-up.svg')}.dc-lucide-undo{--dc-icon:url('/assets/icons/lucide/undo-2.svg')}.dc-lucide-redo{--dc-icon:url('/assets/icons/lucide/redo-2.svg')} 2626.dc-lucide-upload-cloud{--dc-icon:url('/assets/icons/lucide/upload.svg')}.dc-lucide-file-video{--dc-icon:url('/assets/icons/lucide/video.svg')}
2626.dc-lucide-trash-2{--dc-icon:url('/assets/icons/lucide/trash-2.svg')} 2627.dc-lucide-circle-check{--dc-icon:url('/assets/icons/lucide/circle-check.svg')}.dc-lucide-loader-circle{--dc-icon:url('/assets/icons/lucide/loader-circle.svg');animation:spin .8s linear infinite} 2628.dc-lucide-receipt-text{--dc-icon:url('/assets/icons/lucide/clipboard-list.svg')}.dc-lucide-banknote{--dc-icon:url('/assets/icons/lucide/badge-dollar-sign.svg')}.dc-lucide-beef{--dc-icon:url('/assets/icons/lucide/beef.svg')}.dc-lucide-users{--dc-icon:url('/assets/icons/lucide/users-round.svg')}.dc-lucide-handshake{--dc-icon:url('/assets/icons/lucide/briefcase-business.svg')} 2629.dc-lucide-shopping-cart{--dc-icon:url('/assets/icons/lucide/shopping-cart.svg')}.dc-lucide-store{--dc-icon:url('/assets/icons/lucide/store.svg')} 2630.dc-lucide-shield-check{--dc-icon:url('/assets/icons/lucide/shield-check.svg')}.dc-lucide-circle-alert{--dc-icon:url('/assets/icons/lucide/circle-alert.svg')}.dc-lucide-phone,.dc-lucide-phone-off{--dc-icon:url('/assets/icons/lucide/phone.svg')}.dc-lucide-mail,.dc-lucide-mail-x{--dc-icon:url('/assets/icons/lucide/mail.svg')}.dc-lucide-circle-check-big{--dc-icon:url('/assets/icons/lucide/circle-check-big.svg')}.dc-lucide-arrow-up-right{--dc-icon:url('/assets/icons/lucide/arrow-up-right.svg')}.dc-lucide-calendar-check{--dc-icon:url('/assets/icons/lucide/calendar-days.svg')} 2631.dc-contact-whatsapp img,.dc-lot-social-button--instagram img,.dc-lot-social-button--whatsapp img,.price-board-story-export img,.sales-order-action.primary img{filter:brightness(0) invert(1)} 2632.dc-rp-variant-button{position:relative;width:104px;aspect-ratio:16/10;flex:0 0 104px;overflow:hidden;border:2px solid #cad7e5;border-radius:12px;background:#edf2f7;padding:0;cursor:pointer;box-shadow:none;transition:transform .16s ease,border-color .16s ease,box-shadow .16s ease} 2633.dc-rp-variant-button:hover{transform:translateY(-1px)} 2634.dc-rp-variant-button.is-active{border-color:#0a5ca8;box-shadow:0 0 0 3px rgba(10,92,168,.16),0 10px 24px rgba(0,46,95,.16)} 2635.dc-rp-variant-button.is-active:after{content:"";position:absolute;inset:0;border-radius:10px;box-shadow:inset 0 0 0 2px rgba(255,255,255,.78);pointer-events:none} 2636.dc-rp-variant-check{position:absolute;right:6px;top:6px;width:22px;height:22px;display:grid;place-items:center;border-radius:50%;background:#fff;box-shadow:0 7px 18px rgba(0,30,60,.2)} 2637.dc-rp-variant-check img{width:14px;height:14px} 2638.dc-rp-variant-label{position:absolute;left:6px;bottom:6px;max-width:calc(100% - 12px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:3px 7px;border-radius:999px;background:rgba(0,46,95,.86);color:#fff;font-size:10px;font-weight:900} 2639.dc-rp-variant-button.is-active .dc-rp-variant-label{background:#0a5ca8} 2640.dc-cabana-video-frame{position:relative;width:100%;max-width:100%;aspect-ratio:16/9;border-radius:18px;overflow:hidden;background:#001d3d;display:grid;place-items:center;color:#c8d8ea;box-shadow:0 18px 48px rgba(0,30,60,.18)} 2641.dc-cabana-empty-media{display:grid;justify-items:center;gap:12px;padding:24px;text-align:center}.dc-cabana-empty-media>span{font-size:13px}.dc-cabana-empty-upload{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:42px;border:1px solid rgba(255,255,255,.28);border-radius:11px;padding:10px 15px;background:#fff;color:#003b75;font:900 12px 'Archivo',sans-serif;cursor:pointer;box-shadow:0 10px 24px rgba(0,0,0,.18);transition:transform .16s,box-shadow .16s}.dc-cabana-empty-upload:hover{transform:translateY(-1px);box-shadow:0 14px 30px rgba(0,0,0,.24)}.dc-cabana-empty-upload img{width:17px;height:17px}.dc-cabana-empty-upload.is-busy{opacity:.72;cursor:wait;pointer-events:none}@media(max-width:600px){.dc-cabana-empty-media{width:100%;box-sizing:border-box;padding:18px}.dc-cabana-empty-upload{width:min(100%,280px)}} 2642.dc-cabana-upload-progress{display:grid;grid-template-columns:42px minmax(150px,230px);align-items:center;gap:12px;padding:12px 14px;border:1px solid rgba(255,255,255,.22);border-radius:14px;background:rgba(0,24,49,.62);box-shadow:0 12px 30px rgba(0,0,0,.22);backdrop-filter:blur(8px);text-align:left}.dc-cabana-upload-ring{width:42px;height:42px;border-radius:50%;display:grid;place-items:center;background:conic-gradient(#33d69f var(--upload-progress),rgba(255,255,255,.16) 0);position:relative}.dc-cabana-upload-ring:before{content:'';position:absolute;inset:5px;border-radius:50%;background:#062b50}.dc-cabana-upload-ring strong{position:relative;z-index:1;color:#fff;font:900 10px 'Archivo',sans-serif}.dc-cabana-upload-copy{display:grid;gap:6px}.dc-cabana-upload-copy strong{color:#fff;font:900 12px 'Archivo',sans-serif}.dc-cabana-upload-copy small{color:#bcd0e4;font-size:10px}.dc-cabana-upload-track{height:6px;overflow:hidden;border-radius:99px;background:rgba(255,255,255,.16)}
2642.dc-cabana-upload-track span{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,#20bd88,#55e5b5);transition:width .18s ease}@media(max-width:600px){.dc-cabana-upload-progress{grid-template-columns:38px minmax(0,1fr);width:min(100%,290px);box-sizing:border-box}.dc-cabana-upload-ring{width:38px;height:38px}} 2643.dc-cabana-card-upload{position:absolute;z-index:4;left:50%;top:50%;transform:translate(-50%,-50%);display:inline-flex;align-items:center;gap:7px;min-height:36px;padding:8px 11px;border:1px solid rgba(255,255,255,.5);border-radius:10px;background:rgba(255,255,255,.95);color:#003b75;font:900 10px 'Archivo',sans-serif;box-shadow:0 10px 24px rgba(0,0,0,.22);cursor:pointer;white-space:nowrap}.dc-cabana-card-upload img{width:15px;height:15px}.dc-cabana-card-upload:hover{background:#fff;transform:translate(-50%,calc(-50% - 1px))}.dc-cabana-card-upload-progress{position:absolute;z-index:4;left:12px;right:12px;top:50%;transform:translateY(-50%);display:grid;gap:7px;padding:10px;border:1px solid rgba(255,255,255,.24);border-radius:11px;background:rgba(0,24,49,.78);color:#fff;box-shadow:0 10px 26px rgba(0,0,0,.24);backdrop-filter:blur(8px)}.dc-cabana-card-upload-progress>span{display:flex;align-items:center;justify-content:space-between;gap:8px;font:850 10px 'Archivo',sans-serif}.dc-cabana-card-upload-progress small{color:#c8d8ea;font-size:9px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dc-cabana-card-upload-progress .dc-cabana-upload-track{height:7px} 2644@media(max-width:760px){ 2645.rd-responsive-toolbar{position:static!important;display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px!important;backdrop-filter:none!important} 2646.rd-responsive-toolbar .rd-search-field{grid-column:1/-1;min-width:0!important;box-sizing:border-box} 2647.rd-responsive-toolbar button{min-width:0!important;max-width:100%;margin:0!important;justify-content:center;min-height:42px;box-sizing:border-box} 2648.rd-responsive-toolbar .rosgan-ops-toolbar{grid-column:1/-1;display:flex;flex-wrap:wrap;width:100%;gap:8px} 2649.rd-responsive-toolbar .rosgan-ops-toolbar button{flex:1 1 110px} 2650.rd-responsive-toolbar .rd-view-switch{grid-column:1/-1;justify-self:end} 2651.rd-responsive-lot-row{display:grid!important;grid-template-columns:64px minmax(0,1fr) minmax(0,1fr) 36px;gap:12px 8px!important;padding:16px 12px!important;align-items:start!important} 2652.rd-row-thumb{grid-column:1;grid-row:1} 2653.rd-row-copy{grid-column:2/4;grid-row:1;overflow-wrap:anywhere} 2654.rd-row-copy>div:last-child{margin-top:5px;line-height:1.4} 2655.rd-row-brand{grid-column:4;grid-row:1} 2656.rd-row-number{grid-column:1;grid-row:2} 2657.rd-row-heads{grid-column:2;grid-row:2;min-width:0!important;text-align:left!important} 2658.rd-row-weight{grid-column:3/5;grid-row:2;min-width:0!important;text-align:left!important} 2659.rd-row-alert{grid-column:1/-1;grid-row:3;width:100%;min-height:40px!important;padding:8px 12px!important} 2660} 2661.rg-video-certifications{position:absolute;top:12px;left:12px;z-index:2;display:flex;gap:8px;pointer-events:none} 2662.rg-video-certification{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;width:64px;height:54px;box-sizing:border-box;border-radius:12px;border:1px solid rgba(255,255,255,.4);box-shadow:0 3px 12px rgba(0,0,0,.2);font-size:10px;font-weight:800;line-height:1} 2663.rg-video-certification img{display:block;width:28px;height:26px;object-fit:contain} 2664.rg-video-visec{background:#f7faef;color:#2b404c;border-color:#90b436} 2665.rg-video-china{background:#fdeef0;color:#9d2235;border-color:#f6ccd2} 2666@media(max-width:640px){.rg-video-certifications{top:9px;left:9px;gap:6px}.rg-video-certification{width:54px;height:42px;border-radius:10px;font-size:9px;gap:2px}.rg-video-certification img{width:23px;height:21px}} 2667.rg-video-cons-brand{position:absolute;top:12px;right:12px;z-index:2;display:grid;place-items:center;width:54px;height:54px;padding:10px;box-sizing:border-box;border-radius:12px;border:1px solid rgba(255,255,255,.4);box-shadow:0 3px 12px rgba(0,0,0,.2);pointer-events:none} 2668.rg-video-cons-brand img{display:block;width:100%;height:100%;object-fit:contain} 2669@media(max-width:640px){.rg-video-cons-brand{top:9px;right:9px;width:42px;height:42px;padding:8px;border-radius:10px}}
2670.dc-cabana-video-frame:fullscreen{width:100vw;height:100vh;aspect-ratio:auto;border-radius:0;background:#000;box-shadow:none} 2671.dc-cabana-video-frame:fullscreen video{width:100vw!important;height:100vh!important;object-fit:contain!important} 2672.dc-video-controls{display:grid;grid-template-columns:auto minmax(120px,1fr) auto auto minmax(72px,110px) auto;align-items:center;gap:9px;padding:9px 10px;border:1px solid #d7e4f0;border-radius:14px;background:#fff;box-shadow:0 10px 26px rgba(0,30,60,.07)} 2673.dc-video-control-button{width:38px;height:38px;display:grid;place-items:center;border:1px solid #d3e1ef;border-radius:12px;background:#f4f8fc;cursor:pointer;transition:transform .16s ease,background-color .16s ease,border-color .16s ease} 2674.dc-video-control-button:hover{transform:translateY(-1px);background:#eaf2fa;border-color:#bed1e5} 2675.dc-video-control-button img{width:18px;height:18px} 2676.dc-video-slider{width:100%;accent-color:#0a6ebd} 2677.dc-video-time{min-width:58px;color:#53677f;font-size:12px;font-weight:900;text-align:center} 2678@media(max-width:760px){.dc-video-controls{grid-template-columns:auto minmax(86px,1fr) auto auto auto;gap:7px;padding:8px}.dc-video-time{min-width:48px;font-size:11px}.dc-video-volume{display:none}.dc-rp-variant-button{width:96px;flex-basis:96px}} 2679.dc-catalog-contact-row{display:grid;grid-template-columns:minmax(0,1fr);gap:12px;margin:-8px 0 22px} 2680.dc-catalog-contact-row .dc-lot-contact-card{box-shadow:0 10px 28px rgba(15,42,70,.08)} 2681.dc-catalog-floating-contact{position:fixed;left:12px;right:12px;bottom:12px;z-index:88;display:none;align-items:center;gap:10px;padding:10px;border:1px solid #dbe7f2;border-radius:16px;background:rgba(255,255,255,.96);box-shadow:0 16px 38px rgba(0,30,60,.2);backdrop-filter:blur(12px);opacity:0;pointer-events:none;transform:translateY(18px) scale(.98);transition:opacity .22s ease,transform .22s ease} 2682.dc-catalog-floating-contact.is-visible{opacity:1;pointer-events:auto;transform:translateY(0) scale(1)} 2683.dc-catalog-floating-avatar{width:40px;height:40px;flex:0 0 40px;display:grid;place-items:center;border-radius:50%;padding:7px;box-sizing:border-box;box-shadow:0 8px 20px rgba(15,42,70,.18)} 2684.dc-catalog-floating-avatar img{display:block;width:100%;height:100%;object-fit:contain} 2685.dc-catalog-floating-avatar strong{color:#fff;font-size:15px} 2686.dc-catalog-floating-copy{min-width:0;display:grid;gap:1px;flex:1} 2687.dc-catalog-floating-copy small{color:#8493a7;font-size:9px;font-weight:900;letter-spacing:.1em;text-transform:uppercase} 2688.dc-catalog-floating-copy strong{overflow:hidden;color:#102033;font-size:13px;text-overflow:ellipsis;white-space:nowrap} 2689.dc-catalog-floating-contact a{width:40px;height:40px;flex:0 0 40px;display:grid;place-items:center;border-radius:12px;text-decoration:none} 2690.dc-catalog-floating-contact img{width:18px;height:18px} 2691@media(max-width:760px){html[data-public-advance="lacassina"][data-public-route="lacassina"] .dc-catalog-floating-contact{display:flex}html[data-public-advance="lacassina"][data-public-route="lacassina"] main{padding-bottom:86px}} 2692.cb-auditor{margin:0 0 24px;padding:20px;border:1px solid #dce5ef;border-radius:17px;background:#fff}.cb-auditor header{display:flex;justify-content:space-between;gap:14px;align-items:center;flex-wrap:wrap}.cb-auditor h2{margin:0;color:#112f4e}.cb-auditor p,.cb-auditor small{color:#65758a}.cb-auditor-search{width:100%;box-sizing:border-box;margin:16px 0;padding:12px;border:1px solid #cbd8e7;border-radius:10px;font:inherit}.cb-auditor-row{display:grid;grid-template-columns:minmax(170px,1fr) minmax(280px,1.3fr) minmax(210px,1fr);gap:20px;padding:18px 0;border-top:1px solid #e7edf4;align-items:center}.cb-auditor-row small{display:block;margin-top:4px}.cb-auditor-pricing{display:flex;align-items:end;gap:8px;flex-wrap:wrap}.cb-auditor-pricing label{flex:1;min-width:90px;font-size:11px;color:#65758a}.cb-auditor-pricing input{box-sizing:border-box;width:100%;margin-top:5px;border:1px solid #cbd8e7;border-radius:8px;padding:10px;font:inherit;color:#12304d}.cb-auditor button,.cb-auditor-upload{display:inline-flex;align-items:center;justify-content:center;gap:6px;border:1px solid #cbd8e7;border-radius:9px;padding:10px;background:#f4f8fc;color:#084b89;font:700 12px inherit;cursor:pointer}.cb-auditor button:disabled{opacity:.5;cursor:wait}.cb-auditor-actions{display:flex;gap:7px;flex-wrap:wrap}.cb-auditor-actions img{width:15px;height:15px}.cb-auditor-upload{position:relative;overflow:hidden}.cb-auditor-upload input{position:absolute;inset:0;opacity:0;width:100%;cursor:pointer}.cb-auditor-upload:focus-within{outline:2px solid #0a5ca8;outline-offset:2px}@media(max-width:950px){.cb-auditor-row{grid-template-columns:1fr}.cb-auditor{padding:15px}} 2693.cb-auditor-page .cb-auditor-actions{display:flex;align-items:center;gap:6px;flex-wrap:nowrap}.cb-auditor-page .cb-auditor-actions button,.cb-auditor-page .cb-auditor-upload{box-sizing:border-box;flex:0 0 auto;height:36px;min-height:36px;padding:0 10px;border:1px solid #d5e2ef;border-radius:8px;background:#fff;color:#16466f;font-family:inherit;font-size:12px;font-weight:600;line-height:1;white-space:nowrap;transition:background .15s,border-color .15s}.cb-auditor-page .cb-auditor-actions .cb-auditor-edit{width:36px;padding:0}.cb-auditor-page .cb-auditor-upload{background:#edf5fd;border-color:#cbdff2;color:#075ba4}.cb-auditor-page .cb-auditor-actions button:hover,.cb-auditor-page .cb-auditor-upload:hover{background:#e2eef9;border-color:#9ebddd}.cb-auditor-page .cb-auditor-actions img{display:block;width:15px;height:15px;flex:none}
2693.cb-auditor-page .cb-auditor-upload:has(input:disabled){opacity:.5;cursor:wait}.cb-auditor-page .cb-auditor-actions .rf-camera-entry{display:none} 2694.cb-auditor-page .cb-auditor-row{align-items:start}.cb-auditor-page .cb-auditor-pricing{margin:0;align-items:end}.cb-auditor-page .cb-auditor-pricing label{display:grid;gap:5px;line-height:16px}.cb-auditor-page .cb-auditor-pricing input{height:36px;margin:0;padding:0 10px}.cb-auditor-page .cb-auditor-pricing button{box-sizing:border-box;height:36px;min-height:36px;padding:0 12px;font-family:inherit;font-size:12px;line-height:1}.cb-auditor-page .cb-auditor-action-cell{padding-top:21px}.cb-auditor-page .cb-auditor-action-cell>small:empty{display:none}@media(max-width:950px){.cb-auditor-page .cb-auditor-action-cell{padding-top:0}} 2695.cb-auditor-page.ra-panel{position:relative;inset:auto;width:100%;height:auto;min-height:calc(100dvh - 65px);margin:0;border:0;border-radius:0;padding:0;overflow:visible;background:#f4f7fa}.cb-auditor-page .ra-content{display:flex}.cb-auditor-page .cb-auditor-row{padding:14px 16px;min-height:86px;gap:12px}.cb-auditor-page .cb-auditor-row:hover{background:#f7faff}.cb-auditor-columns{display:grid;grid-template-columns:minmax(170px,1fr) minmax(280px,1.3fr) minmax(210px,1fr);gap:12px;padding:12px 16px;background:#fafbfd;color:#687b8e;font-size:10px;letter-spacing:.65px}.cb-auditor-page .ra-list{min-height:240px}.cb-auditor-page .ra-top{flex-wrap:nowrap}.cb-auditor-page .ra-search input{margin:0}.cb-auditor-page .ra-private{white-space:nowrap}@media(max-width:950px){.cb-auditor-columns{display:none}.cb-auditor-page .ra-top,.cb-auditor-page .ra-content{padding:16px}.cb-auditor-page .ra-private{display:none}} 2696.cb-auction-subpages{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 16px}.cb-auction-subpages button{display:inline-flex;align-items:center;gap:7px;min-height:38px;border:1px solid #cbd8e7;border-radius:10px;padding:8px 11px;background:#fff;color:#12304d;font:800 12px inherit;cursor:pointer;transition:border-color .18s,box-shadow .18s,transform .18s}.cb-auction-subpages button:hover{border-color:#8faccc;box-shadow:0 6px 16px rgba(15,42,70,.08);transform:translateY(-1px)}.cb-auction-subpages img{width:15px;height:15px}.cb-auction-subpages .is-tv{border-color:#b9d9d7;background:#f0fbfa;color:#08756f}.cb-auction-subpages+div button[title="Ver estadÃsticas del remate"]{display:none!important}@media(max-width:520px){.cb-auction-subpages{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.cb-auction-subpages button{justify-content:center}.cb-auction-subpages button:last-child{grid-column:1/-1}} 2697.cb-catalog-access-setting{display:flex;align-items:center;justify-content:space-between;gap:20px;background:#fff;border:1px solid #d4e3ef;border-radius:18px;padding:22px;margin-bottom:16px}.cb-catalog-access-setting h2{font-size:17px;margin:0 0 8px}.cb-catalog-access-setting p{font-size:13px;color:#60758c;line-height:1.5;margin:0 0 10px;max-width:650px}.cb-catalog-access-setting strong{font-size:12px;color:#075da8}.cb-catalog-access-switch{flex:none;width:58px;height:36px;border:0;border-radius:99px;padding:4px;cursor:pointer}.cb-catalog-access-switch span{display:block;width:28px;height:28px;background:#fff;border-radius:50%;transition:transform .18s}.cb-catalog-access-switch:focus-visible{outline:3px solid #72b4ec;outline-offset:3px}.cb-catalog-registration{text-align:center;padding:36px 20px;background:#fff;border:1px solid #d4e3ef;border-radius:18px}.cb-catalog-registration>img{width:30px;height:30px}.cb-catalog-registration p{color:#60758c;line-height:1.5}.cb-catalog-registration button{min-height:44px;padding:12px 20px;border:0;border-radius:10px;background:#075da8;color:#fff;font-weight:700;cursor:pointer} 2698.cb-detail-hero{position:relative;isolation:isolate;overflow:hidden;color:var(--cb-hero-text);background:var(--cb-hero-color)} 2699.cb-detail-hero-shell{position:relative;z-index:3;max-width:1240px;margin:0 auto;padding:20px clamp(18px,4vw,44px) 30px} 2700.cb-detail-back{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--cb-hero-border);border-radius:999px;background:var(--cb-hero-surface);color:var(--cb-hero-text);padding:8px 12px;font:800 12px inherit;cursor:pointer;transition:background .18s,border-color .18s,transform .18s} 2701.cb-detail-back:hover{background:var(--cb-hero-surface-strong);transform:translateX(-2px)}
2702.cb-detail-hero-main{display:grid;grid-template-columns:minmax(0,1fr);gap:clamp(22px,4vw,58px);align-items:center;margin-top:20px} 2703.cb-detail-hero-main:has(.cb-detail-brand-panel){grid-template-columns:minmax(0,1.1fr) minmax(330px,.9fr)} 2704.cb-detail-copy{min-width:0} 2705.cb-detail-breed{display:inline-flex;align-items:center;gap:7px;padding:6px 11px;border:1px solid var(--cb-hero-border);border-radius:999px;background:var(--cb-hero-surface);font-size:10px;font-weight:950;letter-spacing:.11em;text-transform:uppercase} 2706.cb-detail-breed:before{content:"";width:6px;height:6px;border-radius:50%;background:var(--cb-hero-text);box-shadow:0 0 0 4px var(--cb-hero-surface-strong)} 2707.cb-detail-title{max-width:720px;margin:12px 0 15px;font-size:clamp(32px,4.2vw,52px);font-weight:950;letter-spacing:-.035em;line-height:.98;text-wrap:balance} 2708.cb-detail-meta{display:flex;gap:9px;flex-wrap:wrap} 2709.cb-detail-meta-item{min-height:39px;display:inline-flex;align-items:center;gap:9px;padding:8px 11px;border:1px solid var(--cb-hero-border);border-radius:11px;background:var(--cb-hero-surface);color:var(--cb-hero-text);font-size:12px;font-weight:800} 2710.cb-detail-meta-item img{width:16px;height:16px;filter:var(--cb-hero-icon-filter);opacity:.82} 2711.cb-detail-counts{display:flex;gap:9px;flex-wrap:wrap;margin-top:17px} 2712.cb-detail-count{min-width:112px;display:grid;grid-template-columns:auto 1fr;column-gap:9px;align-items:center;padding:10px 12px;border:1px solid var(--cb-hero-border);border-radius:13px;background:var(--cb-hero-surface)} 2713.cb-detail-count strong{grid-row:1/3;font-size:24px;line-height:1;font-weight:950}.cb-detail-count small{align-self:end;color:var(--cb-hero-muted);font-size:9px;font-weight:950;letter-spacing:.1em}.cb-detail-count span{align-self:start;color:var(--cb-hero-text);font-size:10px;font-weight:750;opacity:.86} 2714.cb-detail-counts.is-loading{display:grid;grid-template-columns:repeat(3,minmax(110px,132px))}.cb-detail-counts.is-loading .dc-skeleton{height:62px;border-radius:13px} 2715.cb-detail-brand-panel{min-width:0;display:grid;gap:14px;justify-items:center;align-content:center;padding:16px 8px} 2716.cb-detail-main-logos{min-height:104px;display:flex;align-items:center;justify-content:center;gap:22px;flex-wrap:wrap;width:100%} 2717.cb-detail-main-brand{width:clamp(160px,18vw,220px);height:104px;min-width:0;flex:0 1 220px;display:flex;align-items:center;justify-content:center;gap:11px}.cb-detail-main-brand img{display:block;width:100%;height:100%;object-fit:contain;filter:var(--cb-logo-filter,none)}.cb-detail-main-brand strong{color:var(--cb-hero-text);font-size:24px;text-align:center} 2718.cb-detail-guests{display:flex;align-items:center;justify-content:center;gap:11px;flex-wrap:wrap}.cb-detail-guests>span{color:var(--cb-hero-muted);font-size:9px;font-weight:950;letter-spacing:.12em}.cb-detail-guest-logo{height:34px;min-width:46px;max-width:126px;display:flex;align-items:center;justify-content:center;padding:2px 4px;border-radius:8px;color:var(--cb-hero-text)}.cb-detail-guest-logo img{width:auto;max-width:108px;height:30px;object-fit:contain}.cb-detail-guest-logo small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px;font-weight:900} 2719@media(max-width:820px){.cb-detail-hero-shell{padding:16px 16px 22px}.cb-detail-hero-main:has(.cb-detail-brand-panel){grid-template-columns:1fr;gap:16px}.cb-detail-title{font-size:clamp(31px,9vw,44px);margin-bottom:13px}.cb-detail-brand-panel{padding:4px 8px 0}.cb-detail-main-logos{min-height:72px}.cb-detail-main-brand{width:160px;height:72px;flex-basis:160px}} 2720@media(max-width:480px){.cb-detail-back{max-width:100%;padding:7px 10px;font-size:11px}.cb-detail-hero-main{margin-top:17px}.cb-detail-meta{display:grid;grid-template-columns:1fr}.cb-detail-meta-item{width:100%;box-sizing:border-box}.cb-detail-counts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}.cb-detail-count{min-width:0;display:block;padding:10px}.cb-detail-count strong{display:block;font-size:21px}.cb-detail-count small{display:block;margin-top:4px}.cb-detail-count span{display:none}.cb-detail-counts.is-loading{grid-template-columns:repeat(3,minmax(0,1fr))}} 2721 2722.cb-detail-brand-heading{display:flex;align-items:center;justify-content:space-between;width:100%;font-size:10px;font-weight:850;letter-spacing:.1em;color:var(--cb-hero-muted)} 2723.cb-detail-brand-heading span{border-radius:20px;padding:4px 8px;background:var(--cb-hero-surface-strong);color:var(--cb-hero-text);letter-spacing:0} 2724.cb-detail-brand-panel.is-compact{padding:8px 0;gap:12px;align-content:start;width:100%} 2725.cb-name-roll{width:100%;min-width:0} 2726.cb-name-roll-window{height:192px;overflow:auto;scrollbar-width:none;overscroll-behavior:contain;mask-image:linear-gradient(transparent,#000 15%,#000 85%,transparent)} 2727.cb-name-roll-window::-webkit-scrollbar{display:none} 2728.cb-name-roll-track{animation:cbNameRoll var(--cb-roll-duration,45s) linear infinite;will-change:transform}
2729.cb-name-roll-group{display:flex;flex-direction:column} 2730.cb-name-roll-item{display:flex;align-items:center;gap:14px;height:64px;flex:none;box-sizing:border-box;padding:8px 2px;border-bottom:1px solid var(--cb-hero-border)} 2731.cb-name-roll-item strong{font-size:clamp(16px,1.55vw,22px);font-weight:750;line-height:1.2;letter-spacing:-.02em;color:var(--cb-hero-text);overflow-wrap:anywhere} 2732.cb-name-roll:hover .cb-name-roll-track,.cb-name-roll:focus-within .cb-name-roll-track,.cb-name-roll.is-paused .cb-name-roll-track{animation-play-state:paused} 2733.cb-name-roll-footer{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:10px;font-size:11px;color:var(--cb-hero-muted)} 2734.cb-name-roll-footer button{min-height:36px;border:1px solid var(--cb-hero-border);border-radius:20px;padding:7px 13px;background:var(--cb-hero-surface);color:var(--cb-hero-text);font:700 11px inherit;cursor:pointer} 2735.cb-name-roll button:focus-visible,.cb-name-roll-window:focus-visible{outline:2px solid var(--cb-hero-text);outline-offset:3px} 2736@keyframes cbNameRoll{to{transform:translateY(-50%)}} 2737@media(max-width:820px){.cb-name-roll-window{height:168px}.cb-name-roll-item{height:56px}.cb-name-roll-item strong{font-size:17px}} 2738@media(prefers-reduced-motion:reduce){.cb-name-roll-track{animation:none;will-change:auto}.cb-name-roll-copy,.cb-name-roll-footer{display:none}.cb-name-roll-window{mask-image:none;scrollbar-width:thin}} 2739.tv-bug-live{min-height:46px;gap:12px;font-size:18px} 2740.tv-bug-clock{min-height:44px;padding:0 18px 0 20px;gap:10px;font-size:21px} 2741.tv-bug-clock:before{display:none} 2742.tv-bug-clock img{width:19px;height:19px;flex:0 0 19px;filter:brightness(0) invert(1);opacity:.92} 2743.tv-bug-live-card{min-height:44px;max-width:660px;padding:0 20px;gap:12px} 2744.tv-bug-live-card b{gap:9px;font-size:17px} 2745.tv-bug-live-card b:before{width:9px;height:9px} 2746.tv-bug-live-card span{font-size:20px} 2747.tv-weather-bug{position:absolute;top:4.2vh;right:3vw;z-index:4;min-height:58px;display:flex;align-items:center;gap:13px;padding:0 20px 0 18px;border:1px solid rgba(255,255,255,.24);border-radius:999px 0 0 999px;background:linear-gradient(90deg,rgba(4,22,39,.82),rgba(10,92,168,.72));color:#fff;box-shadow:0 18px 38px rgba(0,0,0,.22),inset 0 1px rgba(255,255,255,.18);backdrop-filter:blur(12px);filter:drop-shadow(0 16px 28px rgba(0,0,0,.22))} 2748.tv-weather-icon{width:30px;height:30px;flex:0 0 30px;display:block;filter:brightness(0) invert(1) drop-shadow(0 1px 2px rgba(0,0,0,.18));opacity:.96} 2749.tv-weather-temp{font-size:28px;font-weight:950;line-height:1;letter-spacing:.01em} 2750.tv-weather-copy{display:grid;gap:2px;min-width:0;line-height:1} 2751.tv-weather-copy strong{font-size:13px;font-weight:950;text-transform:uppercase;letter-spacing:.07em;color:rgba(255,255,255,.94)} 2752.tv-weather-copy small{max-width:190px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:850;color:rgba(255,255,255,.76)} 2753.tv-waiting-screen{--tv-wait-color:#00529b;position:fixed;inset:0;z-index:30;display:grid;place-items:center;width:100vw;height:100vh;overflow:hidden;background:linear-gradient(125deg,color-mix(in srgb,var(--tv-wait-color) 78%,transparent) 0%,rgba(0,20,48,.8) 34%,color-mix(in srgb,var(--tv-wait-color) 62%,transparent) 64%,rgba(0,72,95,.72) 100%);background-size:320% 320%;color:#fff;font-family:'Archivo',Arial,sans-serif;backdrop-filter:saturate(.7) contrast(.92);animation:tvWaitingGradient 8s cubic-bezier(.45,.05,.55,.95) infinite} 2754.tv-waiting-screen:before{content:"";position:absolute;inset:-30%;background:radial-gradient(circle at 20% 30%,rgba(255,255,255,.14),transparent 25%),radial-gradient(circle at 76% 28%,rgba(39,222,224,.2),transparent 27%),radial-gradient(circle at 52% 78%,rgba(255,255,255,.1),transparent 28%);filter:blur(10px);animation:tvWaitingAurora 10s ease-in-out infinite alternate} 2755.tv-waiting-screen:after{content:"";position:absolute;inset:0;opacity:.16;background-image:linear-gradient(rgba(255,255,255,.12) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.12) 1px,transparent 1px);background-size:72px 72px;mask-image:linear-gradient(to bottom,rgba(0,0,0,.8),transparent 86%);animation:tvWaitingGrid 12s linear infinite} 2756.tv-waiting-orb{position:absolute;z-index:0;width:32vw;aspect-ratio:1;border-radius:50%;border:1px solid rgba(255,255,255,.24);box-shadow:inset 0 0 90px rgba(255,255,255,.08),0 0 120px rgba(16,195,205,.2);backdrop-filter:blur(2px)} 2757.tv-waiting-orb.is-one{left:-12vw;top:-15vw;animation:tvWaitingOrbOne 9s ease-in-out infinite alternate}.tv-waiting-orb.is-two{right:-10vw;bottom:-17vw;width:38vw;animation:tvWaitingOrbTwo 11s ease-in-out infinite alternate} 2758.tv-waiting-frame{position:relative;z-index:2;width:min(92vw,164vh);min-height:min(82vh,820px);
2758display:grid;grid-template-rows:auto 1fr;gap:clamp(34px,4.5vh,62px);padding:clamp(32px,4.2vw,72px);box-sizing:border-box;border:1px solid rgba(255,255,255,.14);border-radius:clamp(28px,3vw,52px);background:linear-gradient(145deg,rgba(2,22,46,.23),rgba(0,28,58,.09));box-shadow:0 42px 120px rgba(0,16,38,.32),inset 0 1px rgba(255,255,255,.15);backdrop-filter:blur(7px)} 2759.tv-waiting-logos{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:clamp(38px,6vw,110px);align-items:center;padding:0 clamp(12px,3vw,52px)} 2760.tv-waiting-logo{position:relative;min-height:clamp(96px,12vh,150px);display:flex;align-items:center;justify-content:center;padding:0;box-sizing:border-box;animation:tvWaitingLogoFloat 4.8s ease-in-out infinite} 2761.tv-waiting-logo:nth-child(2){animation-delay:-1.3s}.tv-waiting-logo:nth-child(3){animation-delay:-2.7s} 2762.tv-waiting-logo img{display:block;max-width:100%;max-height:clamp(82px,10vh,126px);object-fit:contain;filter:var(--tv-wait-logo-filter,none) drop-shadow(0 16px 28px rgba(0,0,0,.36))}.tv-waiting-logo.is-rosgan img{max-width:92%;filter:brightness(0) invert(1) drop-shadow(0 16px 28px rgba(0,0,0,.34))}.tv-waiting-logo strong{max-width:100%;font-size:clamp(24px,2vw,38px);font-weight:950;text-align:center;text-transform:uppercase;text-shadow:0 15px 30px rgba(0,0,0,.35)} 2763.tv-waiting-body{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(410px,.95fr);gap:clamp(34px,5vw,84px);align-items:center} 2764.tv-waiting-copy{display:grid;align-content:center;gap:clamp(18px,2.2vh,28px);min-width:0}.tv-waiting-kicker{display:inline-flex;align-items:center;gap:12px;width:max-content;max-width:100%;font-size:clamp(12px,1vw,17px);font-weight:950;letter-spacing:.2em;text-transform:uppercase;color:rgba(255,255,255,.78)}.tv-waiting-kicker:before{content:"";width:38px;height:3px;border-radius:999px;background:#fff;box-shadow:0 0 22px rgba(255,255,255,.7)} 2765.tv-waiting-title{margin:0;max-width:900px;font-size:clamp(38px,4vw,72px);font-weight:950;line-height:.98;letter-spacing:-.045em;text-wrap:balance;text-shadow:0 12px 36px rgba(0,18,42,.28)} 2766.tv-waiting-facts{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,.7fr);gap:12px}.tv-waiting-fact{min-width:0;min-height:64px;display:grid;align-content:center;gap:5px;padding:12px 17px;border:1px solid rgba(255,255,255,.16);border-radius:17px;background:rgba(0,24,52,.2)}.tv-waiting-fact.is-location{grid-column:1/-1}.tv-waiting-fact small{color:rgba(255,255,255,.62);font-size:10px;font-weight:950;letter-spacing:.14em;text-transform:uppercase}.tv-waiting-fact strong{font-size:clamp(13px,1.05vw,18px);font-weight:900;line-height:1.15;text-wrap:balance}.tv-waiting-fact.is-location strong{font-size:clamp(12px,1vw,17px)} 2767.tv-waiting-countdown{position:relative;display:grid;gap:clamp(18px,2.2vh,28px);padding:clamp(28px,3.2vw,50px);border:1px solid rgba(255,255,255,.26);border-radius:32px;background:linear-gradient(150deg,rgba(255,255,255,.16),rgba(0,20,44,.28));box-shadow:0 28px 68px rgba(0,15,36,.28),inset 0 1px rgba(255,255,255,.22);backdrop-filter:blur(15px)} 2768.tv-waiting-countdown:before{content:"";position:absolute;inset:10px;border:1px solid rgba(255,255,255,.08);border-radius:24px;pointer-events:none}.tv-waiting-countdown-label{position:relative;color:rgba(255,255,255,.74);font-size:clamp(11px,.95vw,16px);font-weight:950;letter-spacing:.16em;text-align:center;text-transform:uppercase} 2769.tv-waiting-count-grid{position:relative;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:clamp(7px,.75vw,13px)}.tv-waiting-count-part{display:grid;gap:7px;text-align:center}.tv-waiting-count-part strong{display:grid;place-items:center;min-height:clamp(68px,7.6vw,116px);border:1px solid rgba(255,255,255,.22);border-radius:18px;background:rgba(0,20,44,.28);font-size:clamp(34px,3.5vw,62px);font-variant-numeric:tabular-nums;font-weight:950;line-height:1;box-shadow:inset 0 1px rgba(255,255,255,.16)}.tv-waiting-count-part small{color:rgba(255,255,255,.64);font-size:clamp(8px,.72vw,12px);font-weight:950;letter-spacing:.13em;text-transform:uppercase} 2770.tv-waiting-message{position:relative;display:flex;align-items:center;justify-content:center;min-height:clamp(120px,14vh,180px);font-size:clamp(30px,3.1vw,54px);font-weight:950;line-height:1.02;text-align:center;text-wrap:balance;text-shadow:0 10px 30px rgba(0,0,0,.2);animation:tvWaitingMessagePulse 2.2s ease-in-out infinite} 2771.tv-waiting-local-time{position:relative;display:flex;justify-content:center;gap:8px;color:rgba(255,255,255,.62);font-size:clamp(10px,.82vw,14px);font-weight:850}.tv-waiting-local-time strong{color:#fff;font-variant-numeric:tabular-nums} 2772.tv-end-frame{grid-template-rows:auto 1fr;gap:clamp(22px,3.2vh,44px)} 2773.tv-end-logos{gap:clamp(44px,7vw,132px);padding:0 clamp(8px,2vw,36px)} 2774.tv-end-logos .tv-waiting-logo{min-height:clamp(120px,17vh,210px)} 2775.tv-end-logos .tv-waiting-logo img{max-height:clamp(104px,15vh,188px)} 2776.tv-end-copy{position:relative;display:grid;align-content:center;justify-items:center;gap:clamp(15px,2vh,26px);padding-bottom:clamp(18px,3vh,42px);text-align:center} 2777.tv-end-kicker{display:flex;align-items:center;gap:16px;color:rgba(255,255,255,.7);font-size:clamp(12px,1vw,17px);font-weight:950;letter-spacing:.24em;text-transform:uppercase}.tv-end-kicker:before,.tv-end-kicker:after{content:"";width:clamp(38px,5vw,82px);height:2px;border-radius:999px;background:linear-gradient(90deg,transparent,#fff)}
2777.tv-end-kicker:after{transform:scaleX(-1)} 2778.tv-end-title{margin:0;color:#fff;font-size:clamp(64px,8vw,148px);font-weight:950;line-height:.86;letter-spacing:-.055em;text-wrap:balance;text-shadow:0 18px 52px rgba(0,12,30,.36)} 2779.tv-end-thanks{margin:0;color:rgba(255,255,255,.9);font-size:clamp(26px,3vw,54px);font-weight:850;line-height:1.05;letter-spacing:-.025em;text-shadow:0 12px 34px rgba(0,12,30,.3)} 2780.tv-end-copy:after{content:"";width:clamp(90px,12vw,210px);height:5px;margin-top:clamp(4px,1vh,12px);border-radius:999px;background:linear-gradient(90deg,transparent,rgba(255,255,255,.95),transparent);box-shadow:0 0 28px rgba(255,255,255,.32)} 2781@keyframes tvWaitingGradient{0%{background-position:0% 50%}50%{background-position:100% 45%}100%{background-position:0% 50%}}@keyframes tvWaitingAurora{0%{transform:translate3d(-4%,-2%,0) rotate(-4deg) scale(.95)}100%{transform:translate3d(5%,4%,0) rotate(7deg) scale(1.1)}}@keyframes tvWaitingGrid{to{background-position:72px 72px}}@keyframes tvWaitingOrbOne{to{transform:translate(10vw,7vh) scale(1.18)}}@keyframes tvWaitingOrbTwo{to{transform:translate(-8vw,-6vh) scale(.88)}}@keyframes tvWaitingLogoFloat{0%,100%{transform:translateY(0) rotateX(0)}50%{transform:translateY(-8px) rotateX(3deg)}}@keyframes tvWaitingMessagePulse{0%,100%{opacity:.78;transform:scale(.98)}50%{opacity:1;transform:scale(1)}} 2782@media (max-aspect-ratio:4/3){.tv-waiting-frame{width:92vw;min-height:88vh}.tv-waiting-body{grid-template-columns:1fr}.tv-waiting-title{text-align:center}.tv-waiting-kicker{margin:auto}.tv-waiting-copy{gap:16px}.tv-waiting-countdown{padding:24px}.tv-waiting-facts{display:none}.tv-end-frame{min-height:84vh}.tv-end-logos{gap:24px}.tv-end-logos .tv-waiting-logo{min-height:100px}} 2783.tv-waiting-broadcast{background:linear-gradient(90deg,rgba(3,16,29,.98),rgba(3,19,34,.94) 48%,rgba(3,20,33,.78));animation:none;backdrop-filter:saturate(.45)} 2784.tv-waiting-broadcast:before{inset:0;background:radial-gradient(ellipse at 100% 0%,rgba(47,172,188,.19),transparent 60%);filter:none;animation:none} 2785.tv-waiting-broadcast:after{inset:0;background:linear-gradient(115deg,transparent 65%,rgba(255,255,255,.035) 65%,transparent 88%);opacity:1;mask-image:none;animation:none} 2786.tv-waiting-broadcast .tv-waiting-orb{display:none} 2787.tv-waiting-broadcast .tv-waiting-frame{width:90vw;height:86vh;min-height:0;padding:0;grid-template-rows:auto 1fr auto;gap:5vh;border:0;border-radius:0;background:none;box-shadow:none;backdrop-filter:none} 2788.tv-waiting-broadcast .tv-waiting-logos{grid-template-columns:1fr 1fr 1fr;gap:4vw;padding:0 0 3vh;border-bottom:1px solid rgba(255,255,255,.18)} 2789.tv-waiting-broadcast .tv-waiting-logo{min-height:12vh;animation:tvWaitingLogoFloat 4.8s ease-in-out infinite;justify-content:center} 2790.tv-waiting-broadcast .tv-waiting-logo:nth-child(2){animation-delay:-1.3s}.tv-waiting-broadcast .tv-waiting-logo:nth-child(3){animation-delay:-2.7s} 2791.tv-waiting-broadcast .tv-waiting-logo.is-rosgan{justify-content:flex-start} 2792.tv-waiting-broadcast .tv-waiting-logo:last-child{justify-content:flex-end} 2793.tv-waiting-broadcast .tv-waiting-logo img{max-height:12vh;max-width:23vw;filter:var(--tv-wait-logo-filter,none)} 2794.tv-waiting-broadcast .tv-waiting-logo.is-rosgan img{filter:brightness(0) invert(1)} 2795.tv-waiting-broadcast .tv-waiting-logo strong{font-size:clamp(18px,2vw,34px);text-shadow:none} 2796.tv-waiting-broadcast .tv-waiting-body{grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);gap:5vw} 2797.tv-waiting-broadcast .tv-waiting-copy{gap:3vh} 2798.tv-waiting-broadcast .tv-waiting-kicker{font-size:clamp(15px,1.35vw,26px);letter-spacing:.12em;color:#7cd2d7} 2799.tv-waiting-broadcast .tv-waiting-kicker:before{width:24px;height:2px;background:#7cd2d7;box-shadow:none} 2800.tv-waiting-broadcast .tv-waiting-title{font-size:clamp(30px,4.4vw,86px);line-height:1.05;letter-spacing:-.04em;text-shadow:none;text-align:left} 2801.tv-waiting-broadcast .tv-waiting-facts{display:grid;gap:2vh 2vw;margin-top:1vh;grid-template-columns:1.65fr 1fr} 2802.tv-waiting-broadcast .tv-waiting-fact{padding:0;min-height:0;border:0;border-radius:0;background:none;gap:8px;align-content:start} 2803.tv-waiting-broadcast .tv-waiting-fact small{color:#b4cad8;font-size:clamp(14px,1.15vw,22px);letter-spacing:.1em} 2804.tv-waiting-broadcast .tv-waiting-fact strong{font-size:clamp(19px,1.8vw,34px);line-height:1.25;font-weight:700;text-wrap:pretty} 2805.tv-waiting-broadcast .tv-waiting-countdown{padding:4vh 2.5vw;
2805gap:3vh;border:1px solid rgba(123,211,217,.23);border-radius:5px;background:linear-gradient(140deg,rgba(24,59,77,.7),rgba(9,28,44,.85));box-shadow:0 22px 60px rgba(0,0,0,.2);backdrop-filter:none} 2806.tv-waiting-broadcast .tv-waiting-countdown:before{inset:0 0 auto;height:3px;border:0;border-radius:0;background:linear-gradient(90deg,#77d7d7,#2a748d)} 2807.tv-waiting-broadcast .tv-waiting-countdown-label{font-size:clamp(16px,1.4vw,27px);letter-spacing:.08em;color:#d4e6ef} 2808.tv-waiting-broadcast .tv-waiting-count-grid{gap:0} 2809.tv-waiting-broadcast .tv-waiting-count-part{gap:1.5vh;min-width:0;border-right:1px solid rgba(255,255,255,.12)} 2810.tv-waiting-broadcast .tv-waiting-count-part:last-child{border-right:0} 2811.tv-waiting-broadcast .tv-waiting-count-part strong{min-height:0;padding:1vh 0;background:none;border:0;border-radius:0;box-shadow:none;font-size:clamp(30px,4.3vw,82px);letter-spacing:-.035em;font-weight:800} 2812.tv-waiting-broadcast .tv-waiting-count-part:last-child strong{color:#7bd7d9} 2813.tv-waiting-broadcast .tv-waiting-count-part small{font-size:clamp(12px,1vw,19px);letter-spacing:.04em;color:#c0d6e2} 2814.tv-waiting-broadcast .tv-waiting-local-time{border-top:1px solid rgba(255,255,255,.1);padding-top:2vh;font-size:clamp(15px,1.3vw,25px);color:#b4cad8;flex-wrap:wrap} 2815.tv-waiting-broadcast .tv-waiting-message{font-size:clamp(28px,3vw,58px);animation:none} 2816.tv-waiting-broadcast-footer{display:flex;justify-content:space-between;align-items:center;gap:20px;border-top:1px solid rgba(255,255,255,.18);padding-top:2.5vh;font-size:clamp(14px,1.2vw,23px);letter-spacing:.06em;text-transform:uppercase;color:#b4cad8} 2817.tv-waiting-broadcast-footer>span:first-child{display:flex;align-items:center;gap:10px} 2818.tv-waiting-broadcast-footer i{height:6px;width:6px;border-radius:50%;background:#77d7d7} 2819@media(max-aspect-ratio:1/1){.tv-waiting-broadcast .tv-waiting-frame{height:90vh;gap:3vh}.tv-waiting-broadcast .tv-waiting-body{grid-template-columns:1fr;gap:3vh}.tv-waiting-broadcast .tv-waiting-title{font-size:6vw}.tv-waiting-broadcast .tv-waiting-kicker{margin:0}.tv-waiting-broadcast .tv-waiting-count-part strong{font-size:8vw}.tv-waiting-broadcast-footer{letter-spacing:.02em}} 2820@media (prefers-reduced-motion:reduce){.tv-waiting-screen,.tv-waiting-screen:before,.tv-waiting-screen:after,.tv-waiting-orb,.tv-waiting-logo,.tv-waiting-message{animation:none!important}} 2821.mailing-editor-toolbar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:9px;border:1px solid #cfdbe8;border-bottom:0;border-radius:10px 10px 0 0;background:#f4f8fb}.mailing-editor-tool{height:32px;min-width:32px;padding:0 8px;border:1px solid #cad8e5;border-radius:7px;background:#fff;color:#173a5d;font:800 11px inherit;cursor:pointer}.mailing-editor-tool:hover{border-color:#0a5ca8;background:#edf6fd}.mailing-editor-tool:disabled{opacity:.45;cursor:not-allowed}.mailing-editor-separator{width:1px;height:24px;background:#cfdae5;margin:0 2px}.mailing-rich-editor{min-height:280px;max-height:560px;overflow:auto;border:1px solid #cfdbe8;border-radius:0 0 10px 10px;padding:22px;background:#fff;color:#172b3f;font:15px/1.65 Arial,sans-serif;outline:none}.mailing-rich-editor:focus{border-color:#0a5ca8;box-shadow:inset 0 0 0 1px #0a5ca8}.mailing-rich-editor:empty:before{content:attr(data-placeholder);color:#8a99a9;pointer-events:none}.mailing-rich-editor img{max-width:100%;height:auto}.mailing-rich-editor a{color:#075ca8}.mailing-editor-is-expanded body{overflow:hidden!important}.mailing-editor-is-expanded section:has(.mailing-editor-fullscreen){transform:none!important}.mailing-editor-fullscreen{position:fixed!important;inset:18px!important;z-index:9998!important;margin:0!important;padding:18px!important;border-radius:16px!important;background:#fff!important;box-shadow:0 24px 80px rgba(0,20,45,.35)!important;overflow:auto!important}.mailing-editor-fullscreen .mailing-rich-editor{min-height:calc(100vh - 210px);max-height:none}.mailing-editor-dialog-backdrop{position:fixed;inset:0;z-index:10020;background:rgba(4,24,45,.64);display:grid;place-items:center;padding:20px}.mailing-editor-dialog{width:min(560px,100%);max-height:90vh;overflow:auto;border-radius:16px;background:#fff;box-shadow:0 24px 80px rgba(0,0,0,.28);padding:20px}.mailing-editor-source{width:100%;min-height:320px;resize:vertical;border:1px solid #cfdbe8;border-radius:0 0 10px 10px;padding:14px;box-sizing:border-box;background:#0c1b2a;color:#d7e5f2;font:12px/1.55 ui-monospace,Consolas,monospace;outline:none} 2822.mailing-editor-is-expanded section:has(.mailing-editor-fullscreen){overflow:visible!important} 2823.tv-video-portal{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden} 2824.tv-video-portal.is-prewarming{opacity:0;visibility:hidden} 2825.tv-video-bg{position:absolute;inset:0;z-index:0;pointer-events:none;background:radial-gradient(circle at 16% 20%,rgba(42,199,210,.58),transparent 30%),radial-gradient(circle at 88% 28%,rgba(0,88,178,.72),transparent 34%),linear-gradient(118deg,#001d3f 0%,#003e76 46%,#16acbb 100%);background-size:150% 150%;animation:tvVideoGlow 12s ease-in-out infinite alternate} 2826@keyframes tvVideoGlow{0%{background-position:0% 30%;filter:saturate(1)}100%{background-position:100% 70%;filter:saturate(1.12)}} 2827.tv-video-layer{position:absolute;z-index:1;pointer-events:none} 2828.tv-video-layer.is-single{inset:0;display:block} 2829.tv-video-layer.is-grid{left:2vw;right:2vw;top:max(18vh,calc(4.2vh + 131px));
2829bottom:max(30vh,calc(4.4vh + 196px));display:grid;gap:clamp(12px,1.35vw,24px);align-items:center;justify-content:center} 2830.tv-video-layer.is-grid.is-without-lower-third{bottom:2vw} 2831.tv-video-layer.is-grid.is-count-1{grid-template-columns:minmax(0,min(54vw,900px))} 2832.tv-video-layer.is-grid.is-count-2{grid-template-columns:repeat(2,minmax(0,1fr));width:96vw;margin-inline:auto} 2833.tv-video-layer.is-grid.is-count-3{grid-template-columns:repeat(3,minmax(0,1fr));width:96vw;margin-inline:auto} 2834.tv-video-layer.is-grid.is-count-4{left:50%;right:auto;grid-template-columns:repeat(2,minmax(0,1fr));grid-template-rows:repeat(2,minmax(0,1fr));width:auto;max-width:96vw;aspect-ratio:16/9;transform:translateX(-50%)} 2835.rosgan-remate-admin-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px} 2836.rg-active-remates{align-items:start} 2837.rg-active-remates article{padding:13px!important;border-radius:15px!important} 2838.rg-active-remates article>div:first-child{gap:7px!important} 2839.rg-active-remates article>div:first-child>span{width:34px!important;height:34px!important;border-radius:10px!important} 2840.rg-active-remates article>div:first-child>span img{width:28px!important;height:28px!important;object-fit:contain} 2841.rg-active-remates .remate-card-facts{margin:10px 0 0!important;gap:6px!important} 2842.rg-active-remates .remate-card-facts>div{padding:7px 9px!important;border-radius:9px!important} 2843.remate-card-facts{grid-template-columns:repeat(2,minmax(0,1fr))!important} 2844.remate-card-facts>div:last-child:nth-child(3){grid-column:1/-1} 2845.remate-card-facts strong{white-space:normal!important;overflow:visible!important;text-overflow:clip!important;overflow-wrap:anywhere;line-height:1.22} 2846.remate-card-consignataria{display:flex;align-items:center;gap:7px;margin-top:3px} 2847.remate-card-consignataria-logo{width:24px;height:24px;border:1px solid #dce6f0;border-radius:7px;background:#fff;display:grid;place-items:center;flex:none;overflow:hidden} 2848.remate-card-consignataria-logo img{width:20px;height:20px;object-fit:contain} 2849.remate-card-header-cons-logo{width:45px;height:45px;border:1px solid #dce6f0;border-radius:13px;background:#fff;display:grid;place-items:center;flex:none;overflow:hidden} 2850.remate-card-header-cons-logo img{width:36px;height:36px;object-fit:contain} 2851@media(max-width:1050px){.rosgan-remate-admin-grid{grid-template-columns:repeat(2,minmax(0,1fr))}} 2852@media(max-width:760px){.rosgan-remate-admin-grid{grid-template-columns:minmax(0,1fr)}} 2853/* Remates: compact list shared by active, upcoming and archive sections. */ 2854.rosgan-remate-admin-grid{grid-template-columns:minmax(0,1fr);gap:0;overflow:hidden;border:1px solid #e2eaf3;border-radius:15px;background:#fff;box-shadow:0 6px 20px rgba(0,30,60,.035)} 2855.rosgan-remate-admin-grid article{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr) 16px;align-items:center;gap:24px;padding:15px 18px!important;border:0!important;border-bottom:1px solid #eaf0f6!important;border-radius:0!important;box-shadow:none!important;background:transparent!important;transition:background .18s!important} 2856.rosgan-remate-admin-grid article:last-child{border-bottom:0!important} 2857.rosgan-remate-admin-grid article:hover{background:#f5f9fd!important;transform:none!important} 2858.rosgan-remate-admin-grid article:focus-visible{outline:2px solid #0a5ca8;outline-offset:-3px} 2859.rosgan-remate-admin-grid article::after{content:'âº';font-size:24px;font-weight:400;color:#7b95ad;text-align:right} 2860.rosgan-remate-admin-grid article>div:first-child{display:grid!important;grid-template-columns:38px 38px minmax(0,1fr);align-items:center!important;gap:10px!important} 2861.rosgan-remate-admin-grid article>div:first-child>span:first-child{grid-column:1;grid-row:1} 2862.rosgan-remate-admin-grid article>div:first-child>.remate-card-header-cons-logo{grid-column:2;grid-row:1} 2863.rosgan-remate-admin-grid article>div:first-child>div{grid-column:3;grid-row:1} 2864.rosgan-remate-admin-grid article>div:first-child>span{width:38px!important;height:38px!important;border-radius:10px!important} 2865.rosgan-remate-admin-grid article>div:first-child>span img{max-width:30px;max-height:30px;object-fit:contain} 2866.rosgan-remate-admin-grid article h3{font-size:15px!important;line-height:1.3!important;margin:3px 0!important} 2867.rosgan-remate-admin-grid .remate-card-facts{margin:0!important;gap:16px!important} 2868.rosgan-remate-admin-grid .remate-card-facts>div{background:transparent!important;border-radius:0!important;padding:0 0 0 14px!important;border-left:1px solid #e7edf4} 2869.rosgan-remate-admin-grid .remate-card-facts strong{font-size:12px!important} 2870.rosgan-remate-admin-grid .remate-card-facts small{margin-bottom:4px} 2871@media(max-width:760px){ 2872.rosgan-remate-admin-grid article{grid-template-columns:minmax(0,1fr) 14px;gap:12px;padding:14px!important} 2873.rosgan-remate-admin-grid article>div:first-child{grid-column:1;grid-row:1} 2874.rosgan-remate-admin-grid article::after{grid-column:2;grid-row:1/3;align-self:center} 2875.rosgan-remate-admin-grid .remate-card-facts{grid-column:1;grid-row:2;gap:12px!important} 2876.rosgan-remate-admin-grid .remate-card-facts>div{padding-left:0!important;border-left:0} 2877} 2878.rosgan-remate-admin-grid article{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr) 160px 16px;gap:16px} 2879.rosgan-remate-admin-grid article::after{grid-column:4;grid-row:1} 2880.remate-list-catalog{grid-column:3;grid-row:1} 2881.remate-list-catalog{position:relative;overflow:hidden;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:10px 12px;border:1px solid #16856b;border-radius:99px;background:linear-gradient(115deg,#08624e,#108366);color:#fff;font-family:inherit;font-size:11px;font-weight:750;white-space:nowrap;cursor:pointer;box-shadow:0 4px 12px rgba(8,98,78,.22);animation:remateCatalogGlow 3.6s ease-in-out infinite;transition:transform .2s,filter .2s} 2882.remate-list-catalog::before{content:"";position:absolute;inset:-50% auto -50% -60%;width:38%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.28),transparent);transform:skewX(-22deg);pointer-events:none;animation:remateCatalogShine 4.8s ease-in-out infinite} 2883.remate-list-catalog img{width:14px;
2883height:14px;filter:brightness(0) invert(1)} 2884.remate-list-catalog:hover{filter:brightness(1.12);transform:translateY(-2px)} 2885.remate-list-catalog:active{transform:translateY(0)} 2886@keyframes remateCatalogGlow{0%,100%{box-shadow:0 4px 12px rgba(8,98,78,.22),0 0 0 0 rgba(16,131,102,0)}50%{box-shadow:0 5px 18px rgba(8,98,78,.3),0 0 0 4px rgba(16,131,102,.1)}} 2887@keyframes remateCatalogShine{0%,65%{left:-60%}90%,100%{left:130%}} 2888@media(prefers-reduced-motion:reduce){.remate-list-catalog,.remate-list-catalog::before{animation:none;transition:none}.remate-list-catalog:hover{transform:none}} 2889.remate-list-catalog:focus-visible{outline:2px solid #0a5ca8;outline-offset:3px} 2890@media(max-width:760px){.rosgan-remate-admin-grid article{grid-template-columns:minmax(0,1fr) 14px;gap:12px}.rosgan-remate-admin-grid article::after{grid-column:2;grid-row:1/3}.remate-list-catalog{grid-column:1;grid-row:3;justify-self:start}} 2891 2892.pc-directory-tools{display:grid;grid-template-columns:minmax(220px,2fr) repeat(2,minmax(160px,1fr));gap:12px;background:#fff;border:1px solid #e1e9f2;border-radius:16px;padding:16px} 2893.pc-directory-tools label{display:grid;gap:7px;min-width:0;font-size:12px;font-weight:700;color:#60758c} 2894.pc-directory-tools input,.pc-directory-tools select{box-sizing:border-box;width:100%;min-width:0;min-height:44px;padding:10px 12px;border:1px solid #cfdeeb;border-radius:10px;background:#f8fafd;color:#102f50;font:inherit;font-size:14px;font-weight:400} 2895.pc-directory-tools :is(input,select):focus-visible,.pc-directory-row button:focus-visible{outline:2px solid #0a5ca8;outline-offset:2px} 2896.pc-directory-summary{display:flex;align-items:center;justify-content:space-between;min-height:50px;color:#60758c;font-size:13px} 2897.pc-directory-summary button,.pc-directory-row button,.pc-directory-pages button{min-height:40px;padding:8px 13px;border:1px solid #d2e1ef;border-radius:9px;background:#f7faff;color:#075da8;font:600 12px inherit;cursor:pointer} 2898.pc-directory-list{list-style:none;padding:0;margin:0;border:1px solid #e1e9f2;border-radius:16px;background:#fff;overflow:hidden} 2899.pc-directory-row{display:grid;grid-template-columns:minmax(0,2fr) minmax(120px,.8fr) minmax(0,1.3fr) auto;gap:20px;align-items:center;padding:17px 20px;border-bottom:1px solid #eaf0f6} 2900.pc-directory-row:last-child{border-bottom:0}.pc-directory-row:hover{background:#f8fbfe}.pc-directory-row>div{display:grid;gap:5px;min-width:0;overflow-wrap:anywhere}.pc-directory-name strong{font-size:15px;color:#102f50}.pc-directory-name span{font-size:12px;color:#6d8198}.pc-directory-row small{font-size:10px;color:#8593a6;text-transform:uppercase;letter-spacing:.05em}.pc-directory-cuit span,.pc-directory-location span{font-size:13px;color:#3d536c}.pc-directory-empty{padding:40px 20px;text-align:center;border:1px dashed #cfdbe9;border-radius:14px;color:#60758c}.pc-directory-pages{display:flex;gap:16px;align-items:center;justify-content:center;margin-top:18px;font-size:13px;color:#60758c}.pc-directory-pages button:disabled{opacity:.4;cursor:default} 2901@media(max-width:760px){.pc-directory-tools{grid-template-columns:1fr 1fr;padding:12px;gap:10px}.pc-search{grid-column:1/-1}.pc-directory-tools input,.pc-directory-tools select{font-size:16px}.pc-directory-row{grid-template-columns:1fr 1fr;padding:16px;gap:12px}.pc-directory-name{grid-column:1/-1}.pc-directory-row button{grid-column:1/-1;justify-self:start;min-height:44px}.pc-directory-name strong{font-size:16px}} 2902 2903.remate-list-catalog.rd-catalog-cta{grid-column:auto;grid-row:auto;min-height:44px;padding:12px 20px;gap:8px;font-size:13px;flex-shrink:0} 2904@keyframes companyCatalogGlow{0%,100%{box-shadow:0 4px 12px color-mix(in srgb,var(--company-color,#002e5f) 25%,transparent)}50%{box-shadow:0 4px 22px color-mix(in srgb,var(--company-color,#002e5f) 50%,transparent)}}.rd-company-catalog{animation-name:companyCatalogGlow;background:linear-gradient(115deg,var(--company-color,#002e5f),var(--company-color,#002e5f))!important;border-color:var(--company-color,#002e5f)!important}.rd-company-dialog{width:min(480px,calc(100% - 32px));box-sizing:border-box;border:1px solid #dce4ef;border-radius:18px;padding:26px;color:#002e5f}.rd-company-dialog::backdrop{background:rgba(0,29,61,.55)}.rd-company-dialog h2{font-size:21px;margin:0 0 22px}.rd-company-dialog label{display:grid;gap:8px;font-weight:700}.rd-company-dialog select{width:100%;padding:12px;border:1px solid #c6d8e9;border-radius:8px;font:inherit}.rd-company-dialog p{font-size:14px;line-height:1.5;color:#62748a}.rd-company-dialog-actions{display:flex;gap:10px;flex-wrap:wrap}.rd-company-dialog-actions button{flex:1;padding:14px;border:0;border-radius:9px;background:var(--company-color,#002e5f);color:white;font-weight:700;cursor:pointer}.rd-company-dialog-close{margin-top:18px;padding:10px 0;border:0;background:none;color:#002e5f;cursor:pointer}.rd-catalog-cta img{width:17px;height:17px} 2905.roslot-pool-page{box-sizing:border-box}.roslot-ops-shell{display:block;margin:0 0 16px} 2906.roslot-ops-panel,.roslot-live-tower{min-width:0;border:1px solid #dce6f0;border-radius:18px;background:rgba(255,255,255,.96);box-shadow:0 12px 34px rgba(0,30,60,.07)} 2907.roslot-ops-panel{padding:17px}.roslot-live-tower{position:static;overflow:hidden} 2908.roslot-ops-head,.roslot-live-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.roslot-ops-head{margin-bottom:9px}.roslot-ops-title small,.roslot-live-title small{display:block;color:#0a5ca8;font-size:9px;font-weight:950;letter-spacing:.13em;text-transform:uppercase}.roslot-ops-title h2,.roslot-live-title h2{margin:3px 0 0;color:#112033;font-size:18px;line-height:1.1}
2908.roslot-role-chips{display:flex;justify-content:flex-end;gap:5px;flex-wrap:wrap}.roslot-role-chip{display:inline-flex;align-items:center;gap:5px;padding:5px 8px;border-radius:999px;background:#edf4fb;color:#35516d;font-size:9px;font-weight:900;text-transform:uppercase}.roslot-role-chip:before{content:"";width:6px;height:6px;border-radius:50%;background:#1f9d78} 2909.roslot-whatsapp-control{display:flex;align-items:center;gap:11px;min-width:238px;margin:-3px 0 0;padding:7px 8px 7px 10px;border:1px solid #d7e3ed;border-radius:13px;background:#f6f9fc;color:#173958;font-family:'Archivo',sans-serif;text-align:left;cursor:pointer;transition:border-color .18s,background .18s,box-shadow .18s,transform .18s}.roslot-whatsapp-control:hover{transform:translateY(-1px);border-color:#a8c8bd;box-shadow:0 7px 18px rgba(0,60,45,.08)}.roslot-whatsapp-control:disabled{cursor:wait;opacity:.62;transform:none}.roslot-whatsapp-copy{display:grid;grid-template-columns:29px minmax(0,1fr);grid-template-rows:auto auto;column-gap:8px;min-width:0;flex:1}.roslot-whatsapp-copy>span{grid-row:1/-1;display:grid;place-items:center;width:29px;height:29px;border-radius:9px;background:#e4f5ee}.roslot-whatsapp-copy img{width:17px;height:17px}.roslot-whatsapp-copy strong{overflow:hidden;color:#173958;font-size:10px;line-height:1.2;text-overflow:ellipsis;white-space:nowrap}.roslot-whatsapp-copy small{margin-top:2px;color:#738499;font-size:8.5px;font-weight:750}.roslot-whatsapp-switch{position:relative;width:36px;height:21px;flex:0 0 36px;border-radius:999px;background:#aebbc8;box-shadow:inset 0 0 0 1px rgba(0,30,60,.08);transition:background .2s}.roslot-whatsapp-switch i{position:absolute;top:3px;left:3px;width:15px;height:15px;border-radius:50%;background:#fff;box-shadow:0 2px 5px rgba(0,30,60,.22);transition:transform .2s}.roslot-whatsapp-control.is-on{border-color:#9dd8c4;background:#f0fbf7}.roslot-whatsapp-control.is-on .roslot-whatsapp-copy>span{background:#d9f6ea}.roslot-whatsapp-control.is-on .roslot-whatsapp-copy small{color:#177258}.roslot-whatsapp-control.is-on .roslot-whatsapp-switch{background:#1f9d78}.roslot-whatsapp-control.is-on .roslot-whatsapp-switch i{transform:translateX(15px)}.roslot-whatsapp-control.is-unavailable{border-style:dashed}.roslot-whatsapp-control.is-unavailable .roslot-whatsapp-copy small{color:#a26020} 2910.roslot-day-summary{margin-top:10px;padding-top:11px;border-top:1px solid #e5edf5}.roslot-day-summary-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}.roslot-day-summary-head strong{color:#4d6175;font-size:9px;font-weight:950;letter-spacing:.12em;text-transform:uppercase}.roslot-day-summary-head span{color:#8593a6;font-size:9px;font-weight:750}.roslot-day-category-list{display:flex;flex-wrap:wrap;gap:6px}.roslot-day-category{display:inline-grid;grid-template-columns:auto auto;align-items:baseline;gap:1px 5px;padding:6px 9px;border:1px solid #dce6f0;border-radius:10px;background:#f5f8fc;color:#173958}.roslot-day-category strong{font-size:15px;font-weight:950;line-height:1}.roslot-day-category i{font-size:8px;font-style:normal;font-weight:850;text-transform:uppercase}.roslot-day-category em{grid-column:1/-1;overflow:hidden;max-width:210px;color:#65758a;font-size:9px;font-style:normal;font-weight:750;text-overflow:ellipsis;white-space:nowrap}.roslot-day-summary-empty{color:#8593a6;font-size:10px;font-weight:700} 2911.roslot-live-state{display:inline-flex;align-items:center;gap:6px;flex:none;padding:6px 9px;border-radius:999px;background:#e5f5ef;color:#16725c;font-size:9px;font-weight:950;text-transform:uppercase}.roslot-live-state i{width:7px;height:7px;border-radius:50%;background:#1f9d78;box-shadow:0 0 0 0 rgba(31,157,120,.4);animation:livePing 1.7s ease-out infinite} 2912.roslot-kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin:14px 0}.roslot-kpi{min-width:0;padding:11px;border:1px solid #e4ebf3;border-radius:12px;background:#f7f9fc}.roslot-kpi small{display:block;overflow:hidden;color:#738499;font-size:9px;font-weight:850;letter-spacing:.04em;text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase}.roslot-kpi strong{display:block;margin-top:5px;color:#112033;font-size:20px;font-variant-numeric:tabular-nums}.roslot-kpi em{display:block;overflow:hidden;margin-top:2px;color:#65758a;font-size:9px;font-style:normal;font-weight:700;text-overflow:ellipsis;white-space:nowrap} 2913.roslot-op-list{display:grid;gap:7px}.roslot-op-row{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;padding:10px 11px;border:1px solid #e3eaf2;border-radius:12px;background:#fff;transition:border-color .18s,box-shadow .18s}.roslot-op-row:hover{border-color:#bad0e6;box-shadow:0 8px 20px rgba(0,46,95,.07)}.roslot-op-row.is-outbid{overflow:hidden;border:2px solid #e63946;background:linear-gradient(105deg,#fff0f2 0%,#fff 68%);box-shadow:0 0 0 3px rgba(230,57,70,.1),0 12px 30px rgba(181,29,44,.18);animation:roslotOutbidAttention 1.8s ease-in-out infinite}.roslot-op-row.is-outbid:before{content:"";position:absolute;inset:0 auto 0 0;width:5px;background:#e63946}.roslot-op-copy{min-width:0}.roslot-op-topline{display:flex;align-items:center;gap:7px;min-width:0}.roslot-op-kind{flex:none;padding:3px 6px;border-radius:999px;font-size:8px;font-weight:950;letter-spacing:.05em;text-transform:uppercase}.roslot-op-copy strong{display:block;overflow:hidden;color:#112033;font-size:12px;text-overflow:ellipsis;white-space:nowrap}.roslot-op-copy p{overflow:hidden;margin:4px 0 0;color:#65758a;font-size:10px;font-weight:650;text-overflow:ellipsis;white-space:nowrap}.roslot-op-alert{display:flex;align-items:center;gap:6px;width:fit-content;margin:8px 0 6px;padding:6px 9px;border-radius:8px;background:#d91f32;color:#fff;box-shadow:0 6px 15px rgba(191,28,44,.24)}.roslot-op-alert i{font-size:14px}.roslot-op-alert strong{color:#fff;font-size:11px;letter-spacing:.05em}.roslot-op-alert span{font-size:9px;font-weight:750;opacity:.9}.roslot-op-price{display:flex;align-items:baseline;gap:7px;width:fit-content;margin-top:7px;padding:7px 10px;border:1px solid #d6e3ef;border-radius:10px;background:#f4f8fc}.roslot-op-price span{color:#65758a;font-size:8px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.roslot-op-price strong{overflow:visible;color:#003d78;font-size:23px;line-height:1;font-weight:950;font-variant-numeric:tabular-nums}
2913.roslot-op-price em{color:#65758a;font-size:9px;font-style:normal;font-weight:750}.roslot-op-row.is-outbid .roslot-op-price{border-color:#f1b6bd;background:#fff}.roslot-op-row.is-outbid .roslot-op-price strong{color:#c51f31}.roslot-op-status{display:inline-flex;align-items:center;gap:5px;margin-top:7px;font-size:9px;font-weight:900}.roslot-op-status:before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}.roslot-op-actions{display:flex;justify-content:flex-end;gap:5px;flex-wrap:wrap}.roslot-op-actions button{min-height:30px;border:1px solid #d7e2ed;border-radius:9px;background:#fff;color:#173958;padding:6px 9px;font:850 10px 'Archivo',sans-serif;cursor:pointer}.roslot-op-actions button.is-primary{border-color:#005ca8;background:#005ca8;color:#fff}.roslot-op-row.is-outbid .roslot-op-actions button{border-color:#e63946;color:#bc2031}.roslot-op-row.is-outbid .roslot-op-actions button.is-primary{background:#e63946;color:#fff}.roslot-op-actions button.is-success{border-color:#168064;background:#168064;color:#fff}.roslot-op-actions button.is-danger{border-color:#f0c3c8;background:#fff4f5;color:#bd2c39}.roslot-op-actions button:disabled{cursor:wait;opacity:.48}.roslot-ops-empty{padding:18px;border:1px dashed #cbd7e4;border-radius:12px;color:#65758a;font-size:11px;font-weight:750;text-align:center}@keyframes roslotOutbidAttention{0%,100%{box-shadow:0 0 0 3px rgba(230,57,70,.08),0 12px 30px rgba(181,29,44,.14)}50%{box-shadow:0 0 0 6px rgba(230,57,70,.2),0 14px 34px rgba(181,29,44,.26)}}@media(prefers-reduced-motion:reduce){.roslot-op-row.is-outbid{animation:none}} 2914.roslot-live-head{padding:15px;border-bottom:1px solid #e7edf4}.roslot-live-list{display:grid;max-height:174px;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin}.roslot-live-row{display:grid;grid-template-columns:32px minmax(0,1fr);gap:9px;padding:11px 13px;border:0;border-bottom:1px solid #edf1f5;background:#fff;text-align:left;cursor:pointer}.roslot-live-row:hover{background:#f7faff}.roslot-live-icon{width:32px;height:32px;display:grid;place-items:center;border-radius:10px}.roslot-live-icon img{width:15px;height:15px}.roslot-live-copy{min-width:0}.roslot-live-copy strong{display:block;overflow:hidden;color:#112033;font-size:11px;text-overflow:ellipsis;white-space:nowrap}.roslot-live-copy p{display:-webkit-box;overflow:hidden;margin:3px 0 0;color:#65758a;font-size:9.5px;line-height:1.35;-webkit-box-orient:vertical;-webkit-line-clamp:2}.roslot-live-meta{display:flex;justify-content:space-between;gap:8px;margin-top:5px;color:#8291a3;font-size:8.5px;font-weight:800}.roslot-live-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 13px;background:#f7f9fc;color:#65758a;font-size:9px;font-weight:800}.roslot-live-footer button{border:0;background:transparent;color:#075ca8;font:900 10px 'Archivo',sans-serif;cursor:pointer}.roslot-live-empty{padding:26px 16px;color:#718196;font-size:11px;font-weight:750;text-align:center} 2915.roslot-mobile-panel-head,.roslot-mobile-dock{display:none}.roslot-event-toast{position:fixed;z-index:96;left:14px;right:14px;bottom:calc(82px + env(safe-area-inset-bottom));display:grid;grid-template-columns:38px minmax(0,1fr) 16px;gap:10px;align-items:center;width:calc(100% - 28px);max-width:440px;margin:auto;padding:11px 12px;border:1px solid rgba(255,255,255,.2);border-radius:16px;background:linear-gradient(135deg,#00366c,#005ca8);color:#fff;box-shadow:0 18px 42px rgba(0,30,60,.3);text-align:left;font-family:'Archivo',sans-serif;cursor:pointer;animation:roslotToastIn .3s cubic-bezier(.22,.72,.18,1) both}.roslot-event-toast-icon{width:38px;height:38px;display:grid;place-items:center;border-radius:12px;background:rgba(255,255,255,.14)}.roslot-event-toast-icon img{width:18px;height:18px;filter:brightness(0) invert(1)}.roslot-event-toast-copy{min-width:0}.roslot-event-toast-copy strong{display:block;overflow:hidden;font-size:12px;text-overflow:ellipsis;white-space:nowrap}.roslot-event-toast-copy span{display:-webkit-box;overflow:hidden;margin-top:3px;color:#dcecff;font-size:10px;line-height:1.35;-webkit-box-orient:vertical;-webkit-line-clamp:2}.roslot-event-toast-arrow{font-size:18px;font-weight:900}@keyframes roslotToastIn{from{opacity:0;transform:translateY(24px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes roslotPanelIn{from{opacity:.35;transform:translateX(100%)}to{opacity:1;transform:translateX(0)}} 2916.roslot-live-row.is-arriving{position:relative;z-index:1;animation:roslotLiveArrival 1.1s cubic-bezier(.22,.72,.18,1) both}.roslot-live-row.is-arriving .roslot-live-icon{animation:roslotLiveIconPulse 1.1s ease-out both}@keyframes roslotLiveArrival{0%{opacity:0;transform:translateX(24px);background:#cfe9ff;box-shadow:inset 4px 0 #0a5ca8}55%{opacity:1;transform:translateX(0);background:#e8f5ff;box-shadow:inset 4px 0 #0a5ca8}100%{background:#fff;box-shadow:inset 0 0 transparent}}@keyframes roslotLiveIconPulse{0%,100%{transform:scale(1)}45%{transform:scale(1.18);box-shadow:0 0 0 7px rgba(10,92,168,.12)}} 2917@media(min-width:1181px){.roslot-pool-page{padding-right:clamp(370px,28vw,420px)!important}.roslot-live-tower{position:fixed;z-index:42;top:78px;right:clamp(14px,4vw,36px);width:clamp(320px,24vw,370px);max-height:calc(100vh - 96px)}.roslot-live-list{max-height:calc(100vh - 190px)}} 2918@media(max-width:1180px){.roslot-pool-page{padding-right:clamp(14px,4vw,36px)!important}.roslot-ops-shell{display:grid;grid-template-columns:minmax(0,1fr);gap:14px}.roslot-live-list{grid-template-columns:repeat(2,minmax(0,1fr))}.roslot-live-row:nth-child(odd):last-child{grid-column:1/-1}} 2919@media(max-width:760px){.roslot-ops-shell{display:none;position:fixed;z-index:88;inset:58px 0 calc(67px + env(safe-area-inset-bottom));gap:10px;margin:0;padding:14px;overflow-y:auto;overscroll-behavior:contain;background:#f4f7fb}.roslot-ops-shell.is-mobile-open{display:grid;animation:roslotPanelIn .28s cubic-bezier(.22,.72,.18,1) both}.roslot-mobile-panel-head{position:sticky;z-index:3;top:-14px;display:flex;align-items:center;justify-content:space-between;gap:12px;margin:-14px -14px 0;padding:13px 15px;border-bottom:1px solid #dce6f0;background:rgba(244,247,251,.96);backdrop-filter:blur(12px)}.roslot-mobile-panel-head strong{color:#112033;font-size:16px}.roslot-mobile-panel-head button{width:36px;height:36px;display:grid;place-items:center;border:1px solid #d5e0eb;border-radius:11px;background:#fff;color:#173958;cursor:pointer}.roslot-mobile-dock{position:fixed;z-index:57;top:50%;right:0;display:flex;align-items:center;gap:7px;transform:translateY(-50%);padding:12px 8px;border:0;border-radius:14px 0 0 14px;background:#003d78;color:#fff;box-shadow:0 10px 26px rgba(0,36,75,.25);font:850 10px 'Archivo',sans-serif;letter-spacing:.04em;writing-mode:vertical-rl;cursor:pointer}.roslot-mobile-dock.is-hidden{display:none}.roslot-mobile-dock img{width:16px;height:16px;filter:brightness(0) invert(1);transform:rotate(90deg)}.roslot-ops-panel{padding:13px;border-radius:15px}.roslot-ops-head{align-items:center}
2919.roslot-role-chips{display:none}.roslot-kpis{grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;margin:11px 0}.roslot-kpi{padding:9px}.roslot-kpi strong{font-size:18px}.roslot-op-row{grid-template-columns:minmax(0,1fr);gap:8px}.roslot-op-price{width:auto;justify-content:flex-start}.roslot-op-price strong{font-size:26px}.roslot-op-alert{width:auto}.roslot-op-actions{justify-content:flex-start}.roslot-op-actions button{flex:1;min-width:72px}.roslot-day-category{flex:1 1 120px}.roslot-day-category em{max-width:155px}.roslot-live-list{grid-template-columns:minmax(0,1fr);max-height:none}.roslot-live-row:nth-child(odd):last-child{grid-column:auto}} 2920@media(max-width:760px){.roslot-ops-head{align-items:flex-start;flex-direction:column}.roslot-whatsapp-control{width:100%;min-width:0;margin:2px 0 0;box-sizing:border-box}.roslot-whatsapp-copy strong{font-size:10.5px}} 2921.roslot-price-badge{transition:background .25s,border-color .25s,box-shadow .25s}.roslot-price-badge.has-offers{background:rgba(13,132,92,.88)!important;border-color:rgba(155,255,216,.76)!important;box-shadow:0 0 0 1px rgba(61,221,164,.18),0 0 18px rgba(29,202,139,.38);animation:roslotOfferLive 2.2s ease-in-out infinite}.roslot-price-live-dot{width:6px;height:6px;flex:none;border-radius:50%;background:#baffdf;box-shadow:0 0 0 0 rgba(186,255,223,.7);animation:roslotOfferDot 1.35s ease-out infinite}@keyframes roslotOfferLive{0%,100%{box-shadow:0 0 0 1px rgba(61,221,164,.18),0 0 12px rgba(29,202,139,.24)}50%{box-shadow:0 0 0 2px rgba(121,255,207,.35),0 0 24px rgba(29,202,139,.52)}}@keyframes roslotOfferDot{0%{box-shadow:0 0 0 0 rgba(186,255,223,.75)}75%,100%{box-shadow:0 0 0 7px rgba(186,255,223,0)}}.roslot-op-copy strong{display:-webkit-box;overflow:hidden;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2} 2922.roslot-report-entry{display:inline-flex;align-items:center;gap:7px;padding:9px 15px;border:1px solid #c8d8e7;border-radius:100px;background:#fff;color:#063d72;font:800 12.5px 'Archivo',sans-serif;cursor:pointer;box-shadow:0 5px 14px rgba(0,46,95,.06);transition:transform .16s,border-color .16s,box-shadow .16s}.roslot-report-entry:hover{transform:translateY(-2px);border-color:#73a2ca;box-shadow:0 9px 20px rgba(0,46,95,.12)} 2923.roslot-presence-strip{display:inline-flex;align-items:center;gap:6px;margin-top:9px;padding:5px;border:1px solid #d7e4ee;border-radius:16px;background:rgba(255,255,255,.94);box-shadow:0 7px 18px rgba(0,46,95,.07);color:#38546f;font-size:10px;font-weight:850;line-height:1}.roslot-presence-live,.roslot-presence-role{display:inline-flex;align-items:center;white-space:nowrap}.roslot-presence-live{gap:6px;min-height:32px;padding:0 10px;border-radius:11px;background:#e4f5ef;color:#16725c;font-size:8px;letter-spacing:.08em;text-transform:uppercase}.roslot-presence-live i{width:7px;height:7px;border-radius:50%;background:#1f9d78;box-shadow:0 0 0 0 rgba(31,157,120,.4);animation:livePing 1.7s ease-out infinite}.roslot-presence-role{gap:7px;min-height:32px;padding:3px 10px 3px 4px;border:1px solid #dfeaf4;border-radius:11px;background:#f2f7fc}.roslot-presence-role-icon{display:grid;place-items:center;width:26px;height:26px;flex:0 0 26px;border-radius:9px;background:#dcecff;color:#075ca8;font-size:13px}.roslot-presence-role-copy{display:flex;align-items:baseline;gap:4px}.roslot-presence-role strong{color:#073e73;font-size:13px;font-variant-numeric:tabular-nums}.roslot-presence-role small{color:#4d647a;font-size:9.5px;font-weight:800}.roslot-presence-role.is-seller{border-color:#d9ece5;background:#f0f8f5}.roslot-presence-role.is-seller .roslot-presence-role-icon{background:#dcefe8;color:#14745f}.roslot-presence-role.is-seller strong{color:#14745f} 2924@media(max-width:520px){.roslot-presence-strip{display:flex;width:max-content;max-width:100%;flex-wrap:wrap}.roslot-presence-live{min-height:30px;padding:0 8px}.roslot-presence-role{min-height:30px;padding-right:8px}.roslot-presence-role-icon{width:24px;height:24px;flex-basis:24px}} 2925.roslot-report-page{box-sizing:border-box;width:min(1240px,calc(100% - 32px));margin:0 auto;padding:24px 0 54px;color:#112033;animation:pageIn 420ms cubic-bezier(.22,.72,.18,1)}.roslot-report-header{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:20px}.roslot-report-back{display:inline-flex;align-items:center;gap:6px;margin:0 0 16px;padding:0;border:0;background:transparent;color:#075ca8;font:800 12px 'Archivo',sans-serif;cursor:pointer}.roslot-report-back:hover{text-decoration:underline}.roslot-report-eyebrow{color:#e63946;font-size:10px;font-weight:950;letter-spacing:.16em}.roslot-report-header h1{margin:5px 0 5px;font-size:clamp(27px,3.4vw,42px);line-height:1.02;letter-spacing:-.035em}.roslot-report-header>div:first-child>p{max-width:700px;margin:0;color:#65758a;font-size:13px;line-height:1.55}.roslot-report-date-control{min-width:300px;padding:12px;border:1px solid #dce6f0;border-radius:15px;background:#fff;box-shadow:0 9px 25px rgba(0,30,60,.06)}.roslot-report-date-control>label{display:block;margin:0 0 6px;color:#65758a;font-size:9px;font-weight:900;letter-spacing:.09em;text-transform:uppercase}.roslot-report-date-control>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px}.roslot-report-date-control input,.roslot-report-date-control button{min-height:38px;box-sizing:border-box;border:1px solid #d7e2ed;border-radius:10px;background:#f7f9fc;color:#173958;font:750 12px 'Archivo',sans-serif}.roslot-report-date-control input{min-width:0;padding:0 10px}.roslot-report-date-control button{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 12px;background:#075ca8;color:#fff;border-color:#075ca8;cursor:pointer} 2926.roslot-report-hero{position:relative;overflow:hidden;display:grid;grid-template-columns:minmax(0,1.2fr) minmax(240px,.8fr) minmax(260px,.85fr);gap:28px;align-items:center;
2926padding:28px 30px;border-radius:24px;background:linear-gradient(125deg,#002f61 0%,#064d89 56%,#087e87 100%);color:#fff;box-shadow:0 20px 46px rgba(0,49,96,.2)}.roslot-report-hero:after{content:"";position:absolute;right:-80px;bottom:-140px;width:360px;height:360px;border:1px solid rgba(255,255,255,.14);border-radius:50%;box-shadow:0 0 0 44px rgba(255,255,255,.025),0 0 0 88px rgba(255,255,255,.02)}.roslot-report-hero-copy,.roslot-report-hero-date,.roslot-report-hero-note{position:relative;z-index:1}.roslot-report-hero-copy>span{font-size:10px;font-weight:900;letter-spacing:.13em;text-transform:uppercase;color:#8ee6d6}.roslot-report-hero-copy h2{margin:7px 0 4px;font-size:clamp(30px,4vw,50px);line-height:1;font-variant-numeric:tabular-nums;letter-spacing:-.04em}.roslot-report-hero-copy p{margin:0;color:#cae3f3;font-size:11px;font-weight:650}.roslot-report-hero-date{padding-left:25px;border-left:1px solid rgba(255,255,255,.22)}.roslot-report-hero-date small{display:block;margin-bottom:7px;color:#9fc7df;font-size:9px;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.roslot-report-hero-date strong{display:block;max-width:260px;font-size:17px;line-height:1.3}.roslot-report-hero-note{display:flex;align-items:flex-start;gap:10px;padding:13px;border:1px solid rgba(255,255,255,.17);border-radius:14px;background:rgba(255,255,255,.09);backdrop-filter:blur(8px)}.roslot-report-hero-note>i{flex:none;margin-top:2px;color:#91f0d8;font-size:19px}.roslot-report-hero-note p{margin:0}.roslot-report-hero-note strong,.roslot-report-hero-note span{display:block}.roslot-report-hero-note strong{font-size:11px}.roslot-report-hero-note span{margin-top:3px;color:#d5e8f4;font-size:9.5px;line-height:1.4} 2927.roslot-report-kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:14px 0}.roslot-report-kpi{display:flex;align-items:flex-start;gap:11px;min-width:0;padding:17px;border:1px solid #dce6f0;border-radius:16px;background:#fff;box-shadow:0 8px 23px rgba(0,30,60,.055)}.roslot-report-kpi>span{display:grid;place-items:center;width:36px;height:36px;flex:0 0 36px;border-radius:11px;background:#edf5fc;color:#075ca8;font-size:17px}.roslot-report-kpi>div{min-width:0}.roslot-report-kpi small{display:block;color:#718196;font-size:8.5px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.roslot-report-kpi strong{display:block;overflow:hidden;margin-top:3px;color:#092b4c;font-size:21px;line-height:1.1;font-variant-numeric:tabular-nums;text-overflow:ellipsis}.roslot-report-kpi p{margin:4px 0 0;color:#7d8c9e;font-size:9.5px;line-height:1.35} 2928.roslot-report-ledger{padding:22px;border:1px solid #dce6f0;border-radius:20px;background:#fff;box-shadow:0 13px 34px rgba(0,30,60,.065)}.roslot-report-ledger-head{display:flex;align-items:flex-end;justify-content:space-between;gap:18px}.roslot-report-ledger-head>div:first-child>span{color:#087966;font-size:9px;font-weight:950;letter-spacing:.13em}.roslot-report-ledger-head h2{margin:4px 0 2px;font-size:21px;letter-spacing:-.02em}.roslot-report-ledger-head p{margin:0;color:#728197;font-size:10.5px}.roslot-report-search{display:grid;grid-template-columns:18px minmax(0,1fr);align-items:center;width:min(310px,100%);padding:0 11px;border:1px solid #d7e2ed;border-radius:11px;background:#f7f9fc;color:#76899d}.roslot-report-search input{min-width:0;height:39px;border:0;outline:0;background:transparent;color:#173958;font:650 11px 'Archivo',sans-serif}.roslot-report-filters{display:flex;gap:6px;margin-top:15px;padding-top:14px;border-top:1px solid #edf1f5}.roslot-report-filter{padding:7px 11px;border:1px solid #d9e4ed;border-radius:999px;background:#fff;color:#526579;font:800 10px 'Archivo',sans-serif;cursor:pointer}.roslot-report-filter.is-active{border-color:#075ca8;background:#075ca8;color:#fff}.roslot-report-transactions{display:grid;gap:12px;margin-top:16px}.roslot-report-transaction{overflow:hidden;border:1px solid #dce5ee;border-radius:17px;background:#fff;transition:transform .16s,box-shadow .16s,border-color .16s}.roslot-report-transaction:hover{transform:translateY(-1px);border-color:#bad0e4;box-shadow:0 12px 26px rgba(0,38,75,.08)}.roslot-report-transaction>header{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:16px 17px}.roslot-report-lot-id{display:flex;align-items:center;gap:13px;min-width:0}.roslot-report-lot-id>span{flex:none;padding:6px 8px;border-radius:8px;background:#092f57;color:#fff;font-size:9px;font-weight:950;letter-spacing:.05em}.roslot-report-lot-id>div{min-width:0}.roslot-report-lot-id h3{margin:0;color:#112033;font-size:15px}.roslot-report-lot-id p{overflow:hidden;margin:3px 0 0;color:#718196;font-size:9.5px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-transaction-total{flex:none;text-align:right}.roslot-report-transaction-total small,.roslot-report-price-strip small{display:block;
2928color:#77889a;font-size:8px;font-weight:900;letter-spacing:.08em}.roslot-report-transaction-total strong{display:block;margin-top:2px;color:#087966;font-size:22px;line-height:1;font-variant-numeric:tabular-nums}.roslot-report-transaction-total span{display:block;margin-top:5px;color:#64758a;font-size:9px;font-weight:750}.roslot-report-price-strip{display:grid;grid-template-columns:1fr 1.25fr .65fr;gap:1px;background:#e3eaf1;border-top:1px solid #e3eaf1;border-bottom:1px solid #e3eaf1}.roslot-report-price-strip>div{min-width:0;padding:11px 16px;background:#f7f9fc}.roslot-report-price-strip strong{display:block;overflow:hidden;margin-top:3px;color:#173958;font-size:11px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-price-strip strong>em{color:#718196;font-size:9px;font-style:normal}.roslot-report-contacts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;padding:14px 16px}.roslot-report-contact{min-width:0;padding:13px;border:1px solid #dce5ed;border-radius:13px;background:#fbfcfe}.roslot-report-contact.is-buyer{border-left:3px solid #0a75bc}.roslot-report-contact.is-seller{border-left:3px solid #168064}.roslot-report-contact-title{display:flex;align-items:center;gap:10px}.roslot-report-contact-title>span{display:grid;place-items:center;width:34px;height:34px;flex:0 0 34px;border-radius:50%;background:#e5f1fb;color:#075ca8;font-size:10px;font-weight:950}.roslot-report-contact.is-seller .roslot-report-contact-title>span{background:#e3f4ee;color:#13745d}.roslot-report-contact-title div{min-width:0}.roslot-report-contact-title small{display:block;color:#718196;font-size:8px;font-weight:900;letter-spacing:.09em;text-transform:uppercase}.roslot-report-contact-title strong{display:block;overflow:hidden;margin-top:2px;color:#112033;font-size:11.5px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-contact-links{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;margin-top:10px}.roslot-report-contact-links a,.roslot-report-contact-links>span{display:flex;align-items:center;gap:6px;min-width:0;padding:7px 8px;border-radius:8px;background:#f0f4f8;color:#38536d;font-size:9px;font-weight:700;text-decoration:none}.roslot-report-contact-links a:hover{background:#e2eef8;color:#075ca8}.roslot-report-contact-links i{flex:none}.roslot-report-contact-links a,.roslot-report-contact-links>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.roslot-report-contact-links>span{color:#8a98a7;font-weight:600}.roslot-report-transaction>footer{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;border-top:1px solid #edf1f5;background:#fbfcfe}.roslot-report-transaction>footer>span{display:inline-flex;align-items:center;gap:6px;color:#168064;font-size:9px;font-weight:900}.roslot-report-transaction>footer button{display:inline-flex;align-items:center;gap:5px;border:0;background:transparent;color:#075ca8;font:900 10px 'Archivo',sans-serif;cursor:pointer}.roslot-report-empty{padding:46px 20px;text-align:center}.roslot-report-empty>span{display:grid;place-items:center;width:46px;height:46px;margin:0 auto 11px;border-radius:15px;background:#edf5fc;color:#075ca8;font-size:21px}.roslot-report-empty h3{margin:0;color:#173958;font-size:16px}.roslot-report-empty p{margin:5px 0 0;color:#718196;font-size:11px}.roslot-report-error{display:grid;grid-template-columns:46px minmax(0,1fr) auto;gap:13px;align-items:center;padding:18px;border:1px solid #f0c5c9;border-radius:17px;background:#fff5f6}.roslot-report-error>span{display:grid;place-items:center;width:46px;height:46px;border-radius:14px;background:#fee4e7;color:#c92b3a;font-size:20px}.roslot-report-error strong{color:#932130;font-size:14px}.roslot-report-error p{margin:3px 0 0;color:#8c5860;font-size:10.5px}.roslot-report-error button{padding:9px 13px;border:0;border-radius:9px;background:#c52b3a;color:#fff;font:800 10px 'Archivo',sans-serif;cursor:pointer}.roslot-report-loading{display:grid;gap:14px}.roslot-report-loading-hero,.roslot-report-loading-grid>span,.roslot-report-loading-ledger{position:relative;overflow:hidden;background:#e7edf3}.roslot-report-loading-hero:after,.roslot-report-loading-grid>span:after,.roslot-report-loading-ledger:after{content:"";position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,rgba(255,255,255,.72),transparent);animation:roslotReportShimmer 1.25s infinite}.roslot-report-loading-hero{height:150px;border-radius:24px}.roslot-report-loading-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.roslot-report-loading-grid>span{height:92px;border-radius:16px}.roslot-report-loading-ledger{height:260px;border-radius:20px}@keyframes roslotReportShimmer{to{transform:translateX(100%)}} 2929@media(max-width:930px){.roslot-report-hero{grid-template-columns:minmax(0,1fr) minmax(230px,.75fr)}.roslot-report-hero-note{grid-column:1/-1}.roslot-report-kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.roslot-report-contact-links{grid-template-columns:minmax(0,1fr)}} 2930@media(max-width:680px){.roslot-report-page{width:min(100% - 22px,1240px);padding-top:16px}.roslot-report-header{align-items:stretch;flex-direction:column;gap:14px}.roslot-report-header h1{font-size:29px}.roslot-report-back{margin-bottom:11px}.roslot-report-date-control{min-width:0}
2930.roslot-report-date-control button span{display:none}.roslot-report-hero{grid-template-columns:minmax(0,1fr);gap:18px;padding:22px;border-radius:19px}.roslot-report-hero-date{padding:15px 0 0;border-top:1px solid rgba(255,255,255,.2);border-left:0}.roslot-report-hero-note{grid-column:auto}.roslot-report-kpis,.roslot-report-loading-grid{grid-template-columns:minmax(0,1fr)}.roslot-report-ledger{padding:14px;border-radius:17px}.roslot-report-ledger-head{align-items:stretch;flex-direction:column}.roslot-report-search{width:auto}.roslot-report-transaction>header{align-items:flex-start;flex-direction:column}.roslot-report-transaction-total{text-align:left}.roslot-report-price-strip{grid-template-columns:minmax(0,1fr)}.roslot-report-contacts{grid-template-columns:minmax(0,1fr);padding:11px}.roslot-report-contact-links{grid-template-columns:minmax(0,1fr)}.roslot-report-error{grid-template-columns:38px minmax(0,1fr)}.roslot-report-error>button{grid-column:1/-1}.roslot-report-entry{padding:9px 12px}} 2931.roslot-report-row{display:grid;grid-template-columns:86px minmax(138px,1.05fr) minmax(150px,.9fr) repeat(2,minmax(158px,1fr)) auto;gap:11px;align-items:center;min-width:0;padding:10px;border:1px solid #dce5ee;border-radius:15px;background:#fff;transition:transform .16s,border-color .16s,box-shadow .16s}.roslot-report-row:hover{transform:translateY(-1px);border-color:#b8cee2;box-shadow:0 10px 22px rgba(0,38,75,.075)}.roslot-report-row-thumb{position:relative;overflow:hidden;width:86px;height:74px;border-radius:11px;background:linear-gradient(135deg,#dceaf4,#b9d0df);color:#35627f}.roslot-report-row-thumb img{width:100%;height:100%;display:block;object-fit:cover}.roslot-report-row-thumb>i{position:absolute;inset:0;margin:auto;width:23px;height:23px}.roslot-report-row-thumb>span{position:absolute;top:5px;left:5px;padding:3px 5px;border-radius:999px;background:rgba(19,116,93,.92);color:#fff;font-size:6.5px;font-weight:950;letter-spacing:.06em}.roslot-report-row-lot,.roslot-report-row-money,.roslot-report-row-contact{min-width:0}.roslot-report-row-lot>small,.roslot-report-row-money>small,.roslot-report-row-contact header small{display:block;color:#718196;font-size:7.5px;font-weight:950;letter-spacing:.09em}.roslot-report-row-lot>small{color:#075ca8}.roslot-report-row-lot h3{overflow:hidden;margin:3px 0 0;color:#112033;font-size:13px;line-height:1.15;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-lot>p{overflow:hidden;margin:3px 0 0;color:#7a899b;font-size:8px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-lot>div{display:flex;gap:7px;margin-top:7px;color:#4d647a;font-size:8px;font-weight:800}.roslot-report-row-lot>div span+span{overflow:hidden;padding-left:7px;border-left:1px solid #d9e3ec;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-money{padding-left:11px;border-left:1px solid #e3eaf0}.roslot-report-row-money>strong{display:block;overflow:hidden;margin-top:3px;color:#087966;font-size:17px;line-height:1.05;font-variant-numeric:tabular-nums;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-money>p{margin:4px 0 0;color:#173958;font-size:9px;font-weight:900}.roslot-report-row-money>p em{color:#718196;font-size:7.5px;font-style:normal}.roslot-report-row-money>span{display:block;overflow:hidden;margin-top:4px;color:#7a899b;font-size:7.5px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-contact{align-self:stretch;display:grid;grid-template-columns:minmax(0,1fr) auto;grid-template-rows:auto auto;column-gap:8px;align-content:center;padding:8px 9px;border:1px solid #dfe7ee;border-left:3px solid #0a75bc;border-radius:11px;background:#fafcfe}.roslot-report-row-contact.is-seller{border-left-color:#168064}.roslot-report-row-contact header{grid-column:1;display:flex;align-items:center;gap:7px;min-width:0}.roslot-report-row-contact header>span{display:grid;place-items:center;width:26px;height:26px;flex:0 0 26px;border-radius:50%;background:#e5f1fb;color:#075ca8;font-size:8px;font-weight:950}.roslot-report-row-contact.is-seller header>span{background:#e3f4ee;color:#13745d}.roslot-report-row-contact header>div{min-width:0}.roslot-report-row-contact header strong{display:block;overflow:hidden;margin-top:1px;color:#112033;font-size:9.5px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-contact-data{grid-column:1;display:grid;gap:2px;min-width:0;margin-top:5px}.roslot-report-row-contact-data span{overflow:hidden;color:#66798c;font-size:7.5px;text-overflow:ellipsis;white-space:nowrap}.roslot-report-row-contact-actions{grid-column:2;grid-row:1/-1;display:grid;gap:5px;align-content:center}.roslot-report-row-contact-actions a{display:inline-flex;align-items:center;justify-content:center;gap:4px;min-width:52px;padding:5px 6px;border:1px solid #c9dcea;border-radius:7px;background:#fff;color:#075ca8;font-size:7.5px;font-weight:900;text-decoration:none;transition:background .15s,color .15s,border-color .15s}.roslot-report-row-contact-actions a:hover{border-color:#075ca8;background:#075ca8;color:#fff}.roslot-report-row-open{display:grid;justify-items:center;gap:4px;min-width:60px;padding:9px 7px;border:1px solid #d5e2ec;border-radius:9px;background:#f6f9fc;color:#075ca8;font:900 8px 'Archivo',sans-serif;cursor:pointer}.roslot-report-row-open:hover{border-color:#075ca8;background:#075ca8;color:#fff}.roslot-report-row-open i{font-size:13px} 2932@media(max-width:1080px){.roslot-report-row{grid-template-columns:76px minmax(0,1fr) minmax(155px,.7fr) auto}.roslot-report-row-thumb{width:76px;height:70px}.roslot-report-row-contact{grid-row:2}.roslot-report-row-contact.is-buyer{grid-column:2}.roslot-report-row-contact.is-seller{grid-column:3}.roslot-report-row-open{grid-column:4;grid-row:1/3}.roslot-report-row-lot{grid-column:2}.roslot-report-row-money{grid-column:3}} 2933@media(max-width:680px){.roslot-report-row{grid-template-columns:64px minmax(0,1fr);gap:9px;padding:9px}.roslot-report-row-thumb{grid-column:1;width:64px;
2933height:62px}.roslot-report-row-lot{grid-column:2}.roslot-report-row-money{grid-column:1/-1;padding:8px 0 0;border-top:1px solid #e3eaf0;border-left:0}.roslot-report-row-contact,.roslot-report-row-contact.is-buyer,.roslot-report-row-contact.is-seller{grid-column:1/-1;grid-row:auto}.roslot-report-row-open{grid-column:1/-1;grid-row:auto;display:flex;justify-content:center;min-height:34px}.roslot-report-row-contact-actions{display:flex}.roslot-report-row-contact-actions a{min-width:60px}} 2934@media print{.app-light-topbar,.app-dark-topbar,.app-mobile-bottom-nav,.roslot-report-back,.roslot-report-date-control button,.roslot-report-search,.roslot-report-filters,.roslot-report-transaction>footer{display:none!important}.roslot-report-page{width:100%;padding:0}.roslot-report-header{margin-bottom:12px}.roslot-report-date-control{box-shadow:none}.roslot-report-hero,.roslot-report-kpi,.roslot-report-ledger{box-shadow:none}.roslot-report-transaction{break-inside:avoid}} 2935.roslot-results-layout{display:grid;grid-template-columns:230px minmax(0,1fr);gap:18px;align-items:start}.roslot-results-main{min-width:0}.roslot-filter-sidebar{position:sticky;top:128px;display:flex;flex-direction:column;gap:16px;max-height:calc(100vh - 148px);overflow-y:auto;overscroll-behavior:contain;padding:16px;border:1px solid #dce6f0;border-radius:16px;background:#fff;box-shadow:0 10px 28px rgba(0,30,60,.07);scrollbar-width:thin}.roslot-filter-sidebar-head{position:sticky;z-index:2;top:-16px;display:flex;align-items:center;justify-content:space-between;gap:10px;margin:-16px -16px 0;padding:15px 16px 12px;border-bottom:1px solid #edf1f5;background:#fff}.roslot-filter-sidebar-head strong{color:#112033;font-size:15px}.roslot-filter-sidebar-head span{padding:3px 7px;border-radius:999px;background:#eef4fb;color:#075ca8;font-size:10px;font-weight:900}.roslot-filter-trigger{display:none!important} 2936.roslot-view-toggle{display:flex;flex:none;gap:2px;padding:4px;border:1px solid #e3eaf2;border-radius:12px;background:#fff;box-shadow:0 4px 14px rgba(0,30,60,.04)}.roslot-view-toggle span{width:38px;height:34px;display:grid;place-items:center;border-radius:9px;cursor:pointer;font-size:15px;transition:background .16s,color .16s,transform .16s}.roslot-view-toggle span:hover{transform:translateY(-1px)} 2937.roslot-lot-results-grid.is-grid{grid-template-columns:repeat(3,minmax(0,1fr))!important} 2938.roslot-lot-results-grid.is-list{grid-template-columns:minmax(0,1fr)!important}.roslot-lot-results-grid.is-list .roslot-lot-card{display:grid!important;grid-template-columns:minmax(190px,25%) minmax(0,1fr);min-height:90px}.roslot-lot-results-grid.is-list .roslot-lot-card-media{height:auto!important;min-height:90px}.roslot-lot-results-grid.is-list .roslot-lot-card-body{display:grid!important;grid-template-columns:minmax(150px,.75fr) minmax(200px,1.25fr) 104px;grid-template-rows:1fr auto;gap:4px 12px!important;align-content:center;padding:7px 10px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-stats{grid-column:1;grid-row:1;gap:5px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-stats>div{padding:5px 8px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-stats>div>div:first-child{font-size:8.5px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-stats>div>div:last-child{font-size:13px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-details{grid-column:2;grid-row:1/span 2;gap:2px!important;font-size:10.5px!important;justify-content:center}.roslot-lot-results-grid.is-list .roslot-lot-card-china{grid-column:1;grid-row:2;padding:2px 7px!important;font-size:9px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-actions{grid-column:3;grid-row:1/span 2;display:grid!important;align-content:center;margin-top:0!important;padding-top:0!important}.roslot-lot-results-grid.is-list .roslot-lot-card-actions button{padding:8px 6px!important;font-size:11px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-media>span{top:6px!important;padding:3px 7px!important;font-size:8px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child{left:8px!important;right:8px!important;bottom:6px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child>div:first-child>div:first-child{font-size:12px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child>div:first-child>div:last-child{font-size:9px!important}.roslot-lot-results-grid.is-list .roslot-price-badge{padding:3px 6px!important}.roslot-lot-results-grid.is-list .roslot-price-badge>div:first-child{font-size:6.5px!important}.roslot-lot-results-grid.is-list .roslot-price-badge>div:last-child{font-size:12px!important} 2939@media(max-width:1050px){.roslot-lot-results-grid.is-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important}} 2940@media(max-width:760px){.roslot-ops-shell.is-mobile-open{align-content:start}.roslot-results-layout{display:block}.roslot-filter-sidebar{display:none;position:relative;top:auto;max-height:48vh;margin-top:12px}.roslot-filter-sidebar.is-mobile-open{display:flex}.roslot-filter-trigger{display:inline-flex!important}} 2941@media(max-width:760px){.roslot-view-toggle{margin-left:auto}.roslot-lot-results-grid.is-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:10px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card{border-radius:14px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-media{height:142px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-body{padding:10px!important;gap:7px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-stats{gap:5px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-stats>div{padding:6px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-stats>div>div:first-child{font-size:8px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-stats>div>div:last-child{font-size:13px!important}
2941.roslot-lot-results-grid.is-grid .roslot-lot-card-details{font-size:10.5px!important}.roslot-lot-results-grid.is-grid .roslot-lot-card-actions button{padding:9px 5px!important;font-size:11px!important}.roslot-lot-results-grid.is-list .roslot-lot-card{grid-template-columns:128px minmax(0,1fr);min-height:176px}.roslot-lot-results-grid.is-list .roslot-lot-card-media{min-height:176px}.roslot-lot-results-grid.is-list .roslot-lot-card-body{grid-template-columns:minmax(0,1fr);gap:8px!important;padding:11px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-stats{grid-column:1}.roslot-lot-results-grid.is-list .roslot-lot-card-details,.roslot-lot-results-grid.is-list .roslot-lot-card-china{display:none!important}.roslot-lot-results-grid.is-list .roslot-lot-card-actions{grid-column:1}.roslot-lot-results-grid.is-list .roslot-lot-card-actions button{padding:9px!important;font-size:12px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-media>span:first-of-type{font-size:9px!important;padding:4px 7px!important}.roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child{left:8px!important;right:8px!important;bottom:8px!important;display:grid!important}.roslot-lot-results-grid.is-list .roslot-price-badge{justify-self:start;text-align:left!important;margin-top:5px}.roslot-lot-results-grid.is-list .roslot-price-badge>div{justify-content:flex-start!important}} 2942@media(max-width:760px){ 2943 body:has(.roslot-pool-page) .app-mobile-load-fab{display:none!important} 2944 .roslot-mobile-dock{top:auto!important;right:12px!important;bottom:calc(78px + env(safe-area-inset-bottom))!important;width:42px;height:42px;justify-content:center!important;padding:0!important;border-radius:50%!important;transform:none!important;font-size:0!important;letter-spacing:0!important;writing-mode:horizontal-tb!important} 2945 .roslot-mobile-dock img{width:18px!important;height:18px!important;transform:none!important} 2946 .roslot-search-toolbar{display:grid!important;grid-template-columns:42px minmax(0,1fr) 78px;gap:8px!important} 2947 .roslot-search-box{grid-column:1/-1;min-width:0!important} 2948 .roslot-search-toolbar>.roslot-filter-trigger{width:42px;min-width:42px;justify-content:center;padding:11px 0!important;gap:0!important;font-size:0!important} 2949 .roslot-search-toolbar>select{width:100%;min-width:0;padding-left:10px!important;padding-right:6px!important} 2950 .roslot-search-toolbar>.roslot-view-toggle{width:78px;box-sizing:border-box;margin:0!important;padding:4px 3px!important;justify-content:space-between} 2951 .roslot-search-toolbar>.roslot-view-toggle span{width:33px;height:32px} 2952 .roslot-lot-results-grid.is-grid .roslot-lot-card-media{height:168px!important} 2953 .roslot-lot-results-grid.is-grid .roslot-lot-card-media>span{top:7px!important;padding:3px 6px!important;font-size:7.5px!important} 2954 .roslot-lot-results-grid.is-grid .roslot-lot-card-media>span:first-of-type{left:7px!important} 2955 .roslot-lot-results-grid.is-grid .roslot-lot-card-media>span:nth-of-type(2){right:7px!important;left:auto!important;gap:3px!important} 2956 .roslot-lot-results-grid.is-grid .roslot-lot-card-media>div:last-child{left:8px!important;right:8px!important;bottom:7px!important;display:block!important} 2957 .roslot-lot-results-grid.is-grid .roslot-lot-card-media>div:last-child>div:first-child{min-width:0;width:100%} 2958 .roslot-lot-results-grid.is-grid .roslot-lot-card-media>div:last-child>div:first-child>div:first-child{display:-webkit-box;overflow:hidden;font-size:12px!important;line-height:1.08!important;-webkit-box-orient:vertical;-webkit-line-clamp:3} 2959 .roslot-lot-results-grid.is-grid .roslot-lot-card-media>div:last-child>div:first-child>div:last-child{margin-top:2px;font-size:9px!important} 2960 .roslot-lot-results-grid.is-grid .roslot-price-badge{display:inline-block!important;margin-top:5px;padding:3px 6px!important;text-align:left!important} 2961 .roslot-lot-results-grid.is-grid .roslot-price-badge>div{justify-content:flex-start!important} 2962 .roslot-lot-results-grid.is-grid .roslot-price-badge>div:first-child{font-size:6.5px!important} 2963 .roslot-lot-results-grid.is-grid .roslot-price-badge>div:last-child{font-size:12px!important}
2964 .roslot-lot-results-grid.is-grid .roslot-lot-card-details>div:not(:nth-child(2)){display:none!important} 2965 .roslot-lot-results-grid.is-grid .roslot-lot-card-details>div:nth-child(2){font-size:10px!important} 2966 .roslot-lot-results-grid.is-grid .roslot-lot-card-china{padding:3px 7px!important;font-size:9px!important} 2967 .roslot-lot-results-grid.is-grid .roslot-lot-card-china img{width:13px!important;height:10px!important} 2968 .roslot-lot-results-grid.is-list .roslot-lot-card{grid-template-columns:122px minmax(0,1fr)!important;min-height:168px!important;border-radius:14px!important} 2969 .roslot-lot-results-grid.is-list .roslot-lot-card-media{height:168px!important;min-height:168px!important} 2970 .roslot-lot-results-grid.is-list .roslot-lot-card-media>span:first-of-type{top:7px!important;left:7px!important;padding:3px 5px!important;font-size:7px!important} 2971 .roslot-lot-results-grid.is-list .roslot-lot-card-media>span:nth-of-type(2){top:26px!important;right:7px!important;left:auto!important;padding:3px 5px!important;font-size:7px!important;gap:3px!important} 2972 .roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child{left:8px!important;right:8px!important;bottom:7px!important;display:block!important} 2973 .roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child>div:first-child>div:first-child{display:-webkit-box;overflow:hidden;font-size:11px!important;line-height:1.08!important;-webkit-box-orient:vertical;-webkit-line-clamp:3} 2974 .roslot-lot-results-grid.is-list .roslot-lot-card-media>div:last-child>div:first-child>div:last-child{margin-top:2px;font-size:8px!important} 2975 .roslot-lot-results-grid.is-list .roslot-price-badge{display:inline-block!important;margin-top:5px!important;padding:3px 6px!important;text-align:left!important} 2976 .roslot-lot-results-grid.is-list .roslot-price-badge>div{justify-content:flex-start!important} 2977 .roslot-lot-results-grid.is-list .roslot-price-badge>div:first-child{font-size:6px!important} 2978 .roslot-lot-results-grid.is-list .roslot-price-badge>div:last-child{font-size:11px!important} 2979 .roslot-lot-results-grid.is-list .roslot-lot-card-body{display:grid!important;grid-template-columns:minmax(0,1fr)!important;grid-template-rows:auto auto auto auto!important;align-content:center;gap:6px!important;padding:8px!important} 2980 .roslot-lot-results-grid.is-list .roslot-lot-card-stats{grid-column:1!important;grid-row:auto!important;gap:5px!important} 2981 .roslot-lot-results-grid.is-list .roslot-lot-card-stats>div{padding:5px 6px!important} 2982 .roslot-lot-results-grid.is-list .roslot-lot-card-stats>div>div:first-child{font-size:7.5px!important} 2983 .roslot-lot-results-grid.is-list .roslot-lot-card-stats>div>div:last-child{font-size:12px!important} 2984 .roslot-lot-results-grid.is-list .roslot-lot-card-details{display:flex!important;grid-column:1!important;grid-row:auto!important;font-size:9.5px!important} 2985 .roslot-lot-results-grid.is-list .roslot-lot-card-details>div:not(:nth-child(2)){display:none!important} 2986 .roslot-lot-results-grid.is-list .roslot-lot-card-china{display:inline-flex!important;grid-column:1!important;grid-row:auto!important;padding:2px 6px!important;font-size:8.5px!important} 2987 .roslot-lot-results-grid.is-list .roslot-lot-card-china img{width:12px!important;height:9px!important} 2988 .roslot-lot-results-grid.is-list .roslot-lot-card-actions{display:flex!important;grid-column:1!important;grid-row:auto!important;margin:0!important;padding:0!important} 2989 .roslot-lot-results-grid.is-list .roslot-lot-card-actions button{padding:7px 5px!important;font-size:10.5px!important} 2990} 2991.tv-video-tile{position:relative;min-width:0;overflow:hidden;border-radius:18px;background:#031426;border:1px solid rgba(255,255,255,.24);box-shadow:0 24px 54px rgba(0,0,0,.3)} 2992.tv-video-layer.is-single .tv-video-tile{position:absolute;inset:0;width:100%;height:100%;border:0;border-radius:0;box-shadow:none;background:transparent;opacity:0;transition:opacity .28s ease} 2993.tv-video-layer.is-single .tv-video-tile.is-active{opacity:1} 2994.tv-video-layer.is-waiting-background .tv-video-tile{transition:opacity 1.1s ease} 2995.tv-video-layer.is-waiting-background .tv-video-tile video{filter:saturate(.58) contrast(.88);transform:scale(1.035)} 2996.tv-video-layer.is-waiting-background .tv-video-badge,.tv-video-layer.is-waiting-background .tv-video-sale-badge{display:none} 2997.tv-video-layer.is-grid .tv-video-tile{aspect-ratio:16/9;width:100%} 2998.tv-video-layer.is-grid.is-count-4 .tv-video-tile{height:100%;aspect-ratio:auto} 2999.tv-video-tile video{display:block;width:100%;height:100%;object-fit:cover;background:transparent;opacity:0;transition:opacity .18s ease} 3000.tv-video-tile video.is-ready{opacity:1} 3001.tv-video-tile video{filter:grayscale(0);transition:opacity .18s ease,filter .72s cubic-bezier(.2,.72,.24,1)} 3002.tv-video-tile.is-sold video{filter:grayscale(1)} 3003.tv-video-badge{position:absolute;
3003left:14px;top:14px;z-index:2;display:inline-flex;align-items:center;min-height:32px;padding:0 13px;border-radius:999px;background:rgba(0,46,95,.92);border:1px solid rgba(255,255,255,.24);color:#fff;font-size:clamp(13px,1vw,18px);font-weight:950;letter-spacing:.04em;text-transform:uppercase;box-shadow:0 12px 28px rgba(0,0,0,.24)} 3004.tv-video-sale-badge{position:absolute;left:50%;bottom:clamp(14px,1.5vw,24px);z-index:2;transform:translate(-50%,12px) scale(.92);display:inline-flex;align-items:center;justify-content:center;gap:clamp(10px,.8vw,15px);min-width:min(72%,360px);min-height:clamp(48px,4.2vw,68px);box-sizing:border-box;padding:8px clamp(20px,2vw,34px) 8px 10px;border-radius:999px;background:rgba(0,145,96,.97);border:2px solid rgba(255,255,255,.78);color:#fff;font-size:clamp(16px,1.35vw,24px);font-weight:950;letter-spacing:.07em;text-align:center;text-transform:uppercase;white-space:nowrap;opacity:0;box-shadow:0 16px 38px rgba(0,0,0,.38),0 0 24px rgba(0,145,96,.3);transition:opacity .36s ease,transform .48s cubic-bezier(.2,.82,.24,1)}.tv-video-sale-badge.is-visible{opacity:1;transform:translate(-50%,0) scale(1)}.tv-video-sale-badge i{width:clamp(30px,2.5vw,40px);height:clamp(30px,2.5vw,40px);display:grid;place-items:center;flex:0 0 auto;border-radius:50%;background:#fff;color:#00875a;font-size:clamp(20px,1.7vw,28px);font-style:normal;font-weight:950;line-height:1;box-shadow:0 5px 14px rgba(0,73,49,.24)} 3005.tv-video-layer.is-single .tv-video-badge{left:3vw;top:auto;bottom:max(29.1vh,calc(4.4vh + 196px))} 3006.tv-video-layer.is-single.is-without-lower-third .tv-video-badge{bottom:3vh} 3007@media(max-height:760px){.tv-lower-third{bottom:3vh;height:clamp(128px,25vh,190px);max-height:calc(100vh - 6vh);grid-template-columns:clamp(138px,13vw,220px) minmax(0,1fr);grid-template-rows:clamp(30px,5vh,40px) minmax(0,1fr)}.tv-lower-topline{gap:10px;padding-left:14px;padding-right:14px}.tv-lower-title-line{gap:8px}.tv-lower-title-line:before{width:7px;height:7px;flex-basis:7px}.tv-lower-meta{gap:6px}.tv-lower-lot-panel{padding:7px 11px}.tv-lower-logo{height:30px}.tv-lower-logo img{max-width:112px;max-height:28px}.tv-lower-logo strong{font-size:15px}.tv-lower-lot{gap:3px;font-size:clamp(20px,2.5vw,34px)}.tv-lower-lot small{font-size:clamp(10px,1.05vw,15px)}.tv-lower-table{grid-template-rows:clamp(23px,4vh,31px)}.tv-lower-row>*{padding-left:7px;padding-right:7px;font-size:clamp(12px,1.26vw,20px)}.tv-lower-head>*{font-size:clamp(10px,1vw,15px)}.tv-lower-table.is-many .tv-lower-row>*{font-size:clamp(11px,1.02vw,16px);padding-left:5px;padding-right:5px}.tv-lower-table.is-max .tv-lower-row>*{font-size:clamp(10px,.82vw,14px);padding-left:4px;padding-right:4px}.tv-channel-bug{top:3vh;gap:6px;width:min(58vw,620px)}.tv-bug-strip{height:clamp(40px,7vh,54px)}.tv-bug-part{gap:7px;padding-left:11px;padding-right:11px}.tv-bug-rosgan{padding-left:14px}.tv-bug-logo{max-height:34px}.tv-bug-cons .tv-bug-logo{width:31px;height:31px;flex-basis:31px}.tv-bug-live{min-height:34px;gap:7px}.tv-bug-clock,.tv-bug-live-card{min-height:32px}.tv-bug-clock{padding-left:12px;padding-right:11px;font-size:15px}.tv-bug-clock:before{width:14px;height:14px;flex-basis:14px}.tv-bug-live-card{padding-left:12px;padding-right:12px;gap:7px}.tv-bug-live-card b{font-size:12px}.tv-bug-live-card span{font-size:14px}.tv-weather-bug{top:3vh;min-height:clamp(40px,7vh,54px);gap:8px;padding-left:12px;padding-right:14px}.tv-weather-icon{width:23px;height:23px;flex-basis:23px}.tv-weather-temp{font-size:22px}.tv-weather-copy strong,.tv-weather-copy small{font-size:10px}.tv-video-layer.is-grid{top:max(16vh,calc(3vh + 104px));bottom:max(28vh,calc(3vh + 164px))}.tv-video-layer.is-single .tv-video-badge{bottom:max(28vh,calc(3vh + 164px))}} 3008 3009.tv-channel-bug .tv-bug-strip{width:100%;box-sizing:border-box} 3010.tv-bug-cabana{background:var(--tv-cabana-logo-background,#fff);color:#123858} 3011.tv-bug-cabana .tv-bug-logo{width:100%;height:44px;max-width:190px;object-fit:contain} 3012.tv-bug-cabana strong{white-space:normal;overflow:visible;text-overflow:clip;line-height:1.15;text-align:center;font-size:clamp(12px,1.15vw,17px)} 3013.tv-channel-bug .tv-bug-live{width:100%;align-items:flex-start} 3014.tv-bug-live .tv-bug-clock{flex:0 0 auto;white-space:nowrap} 3015.tv-bug-live .tv-bug-live-card{flex:1;max-width:none;padding-top:10px;padding-bottom:10px;border-radius:22px;box-sizing:border-box} 3016.tv-bug-live-card span{white-space:normal;overflow:visible;text-overflow:clip;overflow-wrap:anywhere;line-height:1.22;font-size:clamp(14px,1.3vw,20px)}
3017@media(max-width:1100px){.tv-channel-bug{left:2vw;width:min(68vw,620px)}.tv-weather-bug{right:2vw}.tv-lower-third{left:2vw;right:2vw;grid-template-columns:clamp(132px,16vw,190px) minmax(0,1fr)}.tv-lower-topline{padding-left:12px;padding-right:12px}.tv-lower-meta{display:none}.tv-lower-row>*{padding-left:5px;padding-right:5px}} 3018.tv-config-screen-elements{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px} 3019.tv-config-contacts{display:grid;gap:8px;margin-top:18px;font-weight:700}.tv-config-contacts textarea{width:100%;box-sizing:border-box;min-height:85px;padding:12px;border:1px solid #c8daec;border-radius:10px;font:inherit;resize:vertical}.tv-config-contacts small{font-weight:400;color:#54718c} 3020.tv-phone-ticker{position:absolute;left:2.6vw;right:2.6vw;bottom:.6vh;height:3.2vh;display:flex;align-items:center;gap:1.5vw;padding:0 1vw;box-sizing:border-box;background:#f3f7f8;color:#123d5b;border-radius:4px;z-index:4;font-size:2.2vh;font-weight:800;overflow:hidden}.tv-phone-ticker>strong{flex:none;font-size:1.3vh;letter-spacing:.08em}.tv-phone-ticker-window{flex:1;min-width:0;overflow:hidden}.tv-phone-ticker-track{display:flex;width:max-content;animation:tvPhoneTicker 35s linear infinite}.tv-phone-ticker-track span{white-space:nowrap;padding-right:5vw}@keyframes tvPhoneTicker{to{transform:translateX(-50%)}} 3021.tv-config-side{display:grid;gap:12px;align-content:start} 3022.tv-config-screen-toggle{height:42px;border:1px solid #dbe7f2;border-radius:11px;background:#fff;color:#173a5d;font:900 12px 'Archivo',sans-serif;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:0 10px 0 12px;cursor:pointer} 3023.tv-config-screen-toggle i{width:42px;height:24px;border-radius:999px;background:#94a3b8;padding:3px;box-sizing:border-box;transition:background .18s;flex:0 0 42px} 3024.tv-config-screen-toggle b{display:block;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 3px 8px rgba(0,0,0,.16);transition:transform .18s} 3025.tv-config-screen-toggle.is-on{border-color:#bfe6d5;background:#f0fbf6;color:#087244} 3026.tv-config-screen-toggle.is-on i{background:#12b981} 3027.tv-config-screen-toggle.is-on b{transform:translateX(18px)} 3028.tv-config-screen-toggle:disabled{cursor:not-allowed;opacity:.48;filter:saturate(.45)} 3029.tv-config-lot{grid-template-columns:54px minmax(0,1fr) 42px 24px} 3030.tv-config-lot-status{width:22px;height:22px;display:grid;place-items:center;border-radius:50%;font-size:14px;font-style:normal;font-weight:950;line-height:1} 3031.tv-config-lot-status.is-complete{background:#12b981;color:#fff;box-shadow:0 5px 12px rgba(18,185,129,.25)} 3032.tv-config-lot-status.is-empty{visibility:hidden} 3033.tv-config-lot.is-all-lifted{border-color:#d5dde6;background:#edf1f5;color:#8995a3;box-shadow:none;cursor:not-allowed;filter:grayscale(1)} 3034.tv-config-lot.is-all-lifted strong,.tv-config-lot.is-all-lifted span,.tv-config-lot.is-all-lifted small{color:#8995a3} 3035.tv-config-lot.is-all-lifted .tv-config-lot-status{background:#a5afba;color:#fff} 3036.tv-config-lot:disabled{opacity:.72} 3037.tv-config-video-mode{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px} 3038.tv-config-video-mode button{height:34px;border:1px solid #d2e1ef;border-radius:999px;background:#fff;color:#173a5d;padding:0 13px;font:900 12px 'Archivo',sans-serif;cursor:pointer} 3039.tv-config-video-mode button.is-active{background:#002e5f;border-color:#002e5f;color:#fff} 3040.tv-config-transport{display:grid;gap:10px;margin-top:12px;padding:12px;border:1px solid #cfe0f1;border-radius:12px;background:linear-gradient(145deg,#fff,#f1f7fd)} 3041.tv-config-transport-head{display:flex;align-items:end;justify-content:space-between;gap:12px}.tv-config-transport-head span{display:grid;gap:2px;min-width:0}.tv-config-transport-head small{color:#71839a;font-size:9px;font-weight:950;letter-spacing:.11em;text-transform:uppercase}.tv-config-transport-head strong{overflow:hidden;color:#002e5f;font-size:15px;text-overflow:ellipsis;white-space:nowrap}.tv-config-transport-head em{color:#087244;font-size:11px;font-style:normal;font-weight:950;white-space:nowrap} 3042.tv-config-transport select{width:100%;height:38px;border:1px solid #cfe0f1;border-radius:10px;background:#fff;color:#173a5d;padding:0 10px;font:900 12px 'Archivo',sans-serif;outline:0}
3043.tv-config-transport-controls{display:grid;grid-template-columns:1fr auto auto 1fr;gap:7px}.tv-config-transport-controls button{min-height:38px;border:1px solid #cfe0f1;border-radius:10px;background:#fff;color:#173a5d;padding:0 10px;font:900 11px 'Archivo',sans-serif;cursor:pointer}.tv-config-transport-controls button.is-primary{min-width:94px;border-color:#002e5f;background:#002e5f;color:#fff}.tv-config-transport-controls button:disabled,.tv-config-transport select:disabled{cursor:not-allowed;opacity:.48} 3044@media(max-width:520px){.tv-config-transport-controls{grid-template-columns:1fr 1fr}.tv-config-transport-controls button.is-primary{min-width:0}} 3045@media(max-width:360px){.dc-lot-contact-actions{grid-template-columns:1fr}} 3046 3047/* Navegación y formularios móviles: una sola geometrÃa estable en todas las rutas. */ 3048.app-light-topbar, 3049.app-mobile-bottom-nav { 3050 width:100%; 3051 max-width:100vw; 3052 min-width:0; 3053 box-sizing:border-box; 3054} 3055.app-mobile-bottom-nav { 3056 isolation:isolate; 3057 overscroll-behavior:contain; 3058} 3059.app-mobile-bottom-nav > span { 3060 min-width:0; 3061 max-width:20%; 3062} 3063.feria-creator-layout, 3064.feria-form-card, 3065.feria-preview-card, 3066.feria-field-grid, 3067.feria-consignataria-grid, 3068.feria-form-card label { 3069 min-width:0; 3070 max-width:100%; 3071} 3072.feria-form-card input, 3073.feria-form-card select, 3074.feria-form-card textarea { 3075 width:100%; 3076 min-width:0; 3077} 3078@media(max-width:760px){ 3079 .app-light-topbar { 3080 left:0!important; 3081 right:0!important; 3082 padding:10px max(14px,env(safe-area-inset-right)) 10px max(14px,env(safe-area-inset-left))!important; 3083 } 3084 .app-mobile-bottom-nav { 3085 left:0!important; 3086 right:0!important; 3087 bottom:0!important; 3088 min-height:68px; 3089 padding:8px max(6px,env(safe-area-inset-right)) max(10px,env(safe-area-inset-bottom)) max(6px,env(safe-area-inset-left))!important; 3090 border-radius:0!important; 3091 transform:none!important; 3092 box-shadow:0 -8px 28px rgba(0,29,61,.1); 3093 } 3094 .app-mobile-load-fab { 3095 right:max(12px,env(safe-area-inset-right))!important; 3096 bottom:calc(76px + env(safe-area-inset-bottom))!important; 3097 max-width:calc(100vw - 24px)!important; 3098 } 3099 .feria-creator-layout { 3100 grid-template-columns:minmax(0,1fr)!important; 3101 } 3102 .feria-form-card, 3103 .feria-preview-card, 3104 .feria-field-grid, 3105 .feria-consignataria-grid { 3106 width:100%; 3107 box-sizing:border-box; 3108 } 3109 .feria-preview-card { position:static!important; } 3110} 3111@media(max-width:360px){ 3112 .feria-form-card { 3113 padding:18px!important; 3114 border-radius:16px!important; 3115 } 3116 .feria-consignataria-grid { 3117 grid-template-columns:minmax(0,1fr)!important; 3118 } 3119 .app-mobile-load-fab { 3120 padding:12px 17px!important; 3121 font-size:13px!important; 3122 } 3123} 3124@supports (-webkit-touch-callout:none){ 3125 .app-light-topbar, 3126 .app-mobile-bottom-nav { 3127 -webkit-backdrop-filter:none!important; 3128 backdrop-filter:none!important; 3129 background:#fff!important; 3130 } 3131} 3132</style> 3133<style id="roslot-operations-compact-layout"> 3134.roslot-op-row{grid-template-columns:minmax(0,1fr) 145px auto!important;min-height:58px!important;padding:8px 10px!important} 3135.roslot-op-topline{min-height:22px!important}.roslot-op-meta-line{display:flex!important;align-items:center!important;gap:10px!important;min-width:0!important;margin-top:3px!important}.roslot-op-meta-line p{margin:0!important;min-width:0!important}.roslot-op-meta-line .roslot-op-status{flex:none!important;margin:0!important} 3136.roslot-op-alert{flex:none!important;width:auto!important;margin:0 0 0 auto!important;padding:4px 7px!important;border-radius:7px!important;box-shadow:0 4px 10px rgba(191,28,44,.2)!important}.roslot-op-alert i{font-size:12px!important}.roslot-op-alert strong{font-size:9px!important}.roslot-op-alert span{font-size:8px!important} 3137.roslot-op-price{align-self:stretch!important;display:grid!important;grid-template-columns:auto!important;align-content:center!important;justify-items:end!important;gap:2px!important;min-width:0!important;width:auto!important;margin:0!important;padding:6px 11px!important;border-radius:10px!important}.roslot-op-price>span{font-size:8px!important}.roslot-op-price strong{font-size:24px!important;line-height:.95!important}.roslot-op-price strong>.sc-interp{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}.roslot-op-price em{font-size:8px!important} 3138.roslot-op-row.is-winning .roslot-op-price,.roslot-op-row.is-seller .roslot-op-price{border-color:#bcded3!important;background:#f0faf6!important}.roslot-op-row.is-winning .roslot-op-price strong,.roslot-op-row.is-seller .roslot-op-price strong{color:#14745f!important} 3139@media(max-width:760px){.roslot-op-row{grid-template-columns:minmax(0,1fr) minmax(118px,auto)!important;gap:7px!important;min-height:64px!important}.roslot-op-topline{flex-wrap:wrap!important}.roslot-op-alert{margin-left:0!important}
3139.roslot-op-alert span{display:none!important}.roslot-op-price{min-width:108px!important}.roslot-op-price strong{font-size:22px!important}.roslot-op-actions{grid-column:1/-1!important}.roslot-op-meta-line{align-items:flex-start!important;flex-direction:column!important;gap:3px!important}} 3140</style> 3141<style id="rosgan-remate-operations-styles"> 3142.rosgan-ops-toolbar{display:flex;gap:6px;flex-wrap:wrap}.rosgan-ops-toolbar button{min-height:42px;display:inline-flex;align-items:center;gap:7px;padding:0 11px;border:1px solid #cbd9e7;border-radius:11px;background:#fff;color:#173b5c;font:800 11px inherit;cursor:pointer}.rosgan-ops-page{min-height:100vh;background:#f2f6fa;color:#10233a;padding:26px 18px 90px}.rosgan-ops-shell{width:min(1180px,100%);margin:0 auto}.rosgan-ops-back{display:inline-flex;align-items:center;gap:7px;border:0;background:transparent;color:#0a5ca8;font:800 13px inherit;cursor:pointer;padding:4px 0 16px}.rosgan-ops-hero{display:flex;align-items:center;justify-content:space-between;gap:22px;padding:24px 27px;border-radius:18px;background:linear-gradient(120deg,#002e5f,#0a5ca8);color:#fff;box-shadow:0 16px 36px rgba(0,46,95,.18)}.rosgan-ops-hero small{display:block;color:#9fc5e8;font-size:10px;font-weight:900;letter-spacing:.12em}.rosgan-ops-hero h1{margin:5px 0 4px;font-size:clamp(26px,4vw,38px)}.rosgan-ops-hero p{margin:0;color:#d1e1ef;font-size:13px}.rosgan-ops-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.rosgan-ops-actions button{min-height:39px;border:1px solid rgba(255,255,255,.28);border-radius:10px;background:rgba(255,255,255,.1);color:#fff;padding:0 12px;font:800 11px inherit;cursor:pointer}.rosgan-ops-actions button.is-primary{background:#fff;color:#003f78}.rosgan-ops-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:14px 0}.rosgan-ops-metric{padding:16px 18px;border:1px solid #dbe5ef;border-radius:14px;background:#fff}.rosgan-ops-metric small{display:block;color:#7c8da2;font-size:10px;font-weight:900;letter-spacing:.07em}.rosgan-ops-metric strong{display:block;margin-top:5px;font-size:24px}.rosgan-commercial-list{display:grid;gap:8px}.rosgan-commercial-row{display:grid;grid-template-columns:72px minmax(190px,1.4fr) minmax(130px,.8fr) minmax(210px,1fr) minmax(210px,1fr);align-items:center;gap:12px;padding:13px;border:1px solid #dce6ef;border-radius:14px;background:#fff}.rosgan-commercial-id small,.rosgan-commercial-copy small,.rosgan-commercial-price small{display:block;color:#8494a8;font-size:9px;font-weight:900;letter-spacing:.07em}.rosgan-commercial-id strong{display:block;font-size:17px}.rosgan-commercial-copy strong{display:block;font-size:14px}.rosgan-commercial-copy span{display:block;margin-top:3px;color:#687b91;font-size:11px}.rosgan-ops-status{display:inline-flex;justify-content:center;padding:6px 9px;border-radius:99px;background:#eef3f7;color:#536980;font-size:10px;font-weight:900}.rosgan-ops-status.is-vendido{background:#e4f6ef;color:#08745a}.rosgan-ops-status.is-levantado{background:#fff0f1;color:#ad2c39}.rosgan-commercial-statuses,.rosgan-commercial-modes{display:flex;gap:4px}.rosgan-commercial-statuses button,.rosgan-commercial-modes button{min-height:28px;border:1px solid #d4e0eb;border-radius:8px;background:#fff;color:#526b83;padding:0 7px;font:800 9px inherit;cursor:pointer}.rosgan-commercial-modes button.is-active{border-color:#0a5ca8;background:#e8f3fc;color:#07528f}.rosgan-commercial-price input{width:100%;box-sizing:border-box;margin-top:5px;border:1px solid #ccd9e6;border-radius:9px;padding:9px 10px;font:800 13px inherit;outline:none}.rosgan-price-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px;margin-top:16px}.rosgan-price-card{overflow:hidden;border:1px solid #dce6ef;border-radius:18px;background:#fff;box-shadow:0 8px 24px rgba(0,35,70,.07)}.rosgan-price-card>header{display:block!important;padding:17px 19px!important;background:#002e5f!important;color:#fff!important}.rosgan-price-card>header small{display:block!important;padding:0!important;background:transparent!important;color:#9fc5e8!important;font-size:9px;font-weight:900;letter-spacing:.1em}.rosgan-price-card>header strong{display:block!important;margin-top:4px;padding:0!important;background:transparent!important;color:#fff!important;font-size:20px}.rosgan-price-values{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:#e2eaf2}.rosgan-price-values>span{padding:16px 12px;background:#fff}.rosgan-price-values>span small{display:block;color:#8292a6;font-size:9px;font-weight:900}.rosgan-price-values>span strong{display:block;margin-top:5px;font-size:17px}.rosgan-ops-empty{margin-top:15px;padding:36px;border:1px dashed #c9d7e5;border-radius:16px;background:#fff;color:#6b7e92;text-align:center;font-size:13px}.rosgan-tv-config-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(300px,.7fr);gap:14px;margin-top:14px}.rosgan-tv-config-card{padding:20px;border:1px solid #dbe5ef;border-radius:16px;background:#fff}.rosgan-tv-config-card h2{margin:0 0 15px;font-size:18px}.rosgan-tv-selector{display:grid;grid-template-columns:auto 1fr auto;gap:8px}.rosgan-tv-selector button,.rosgan-tv-selector select{min-height:42px;border:1px solid #cad8e6;border-radius:10px;background:#fff;color:#183b5a;padding:0 11px;font:800 12px inherit}.rosgan-tv-toggle-list{display:grid;gap:8px}.rosgan-tv-toggle{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:44px;border:1px solid #d7e2ec;border-radius:11px;background:#f8fafc;color:#243f5a;padding:0 12px;font:800 12px inherit;cursor:pointer}.rosgan-tv-toggle i{position:relative;width:36px;height:20px;border-radius:99px;background:#bdcad7}.rosgan-tv-toggle i:after{content:"";position:absolute;width:14px;
3142height:14px;left:3px;top:3px;border-radius:50%;background:#fff;transition:.18s}.rosgan-tv-toggle.is-on i{background:#0a5ca8}.rosgan-tv-toggle.is-on i:after{left:19px}.rosgan-tv-contact{display:block;margin-top:15px;color:#63788e;font-size:10px;font-weight:900}.rosgan-tv-contact input{width:100%;box-sizing:border-box;margin-top:6px;border:1px solid #cad8e6;border-radius:10px;padding:11px;font:700 12px inherit}.rosgan-tv-url{display:block;margin-top:12px;padding:11px;border-radius:9px;background:#eef4f9;color:#426078;font:700 11px monospace;word-break:break-all}.rosgan-tv-output{position:fixed;inset:0;z-index:120;background:#07111d;overflow:hidden}.rosgan-tv-output video,.rosgan-tv-fallback{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.rosgan-tv-fallback{background-position:center;background-size:cover}.rosgan-tv-fallback:after{content:"";position:absolute;inset:0;background:rgba(0,20,40,.16)}.rosgan-tv-brand-bug{position:absolute;z-index:3;left:2.2vw;top:5.5vh;height:7.2vh;min-height:52px;display:flex;align-items:stretch;border-radius:4px;overflow:hidden;box-shadow:0 4px 15px rgba(0,0,0,.24)}.rosgan-tv-brand-bug>span{display:grid;place-items:center;padding:8px 18px;background:#003e76}.rosgan-tv-brand-bug .rosgan-tv-rosgan{width:145px;box-sizing:border-box}.rosgan-tv-brand-bug img{display:block;max-width:100%;max-height:100%;object-fit:contain}.rosgan-tv-brand-bug .rosgan-tv-cons{min-width:125px;background:var(--rosgan-cons-color);color:#fff;font-size:22px;font-weight:900}.rosgan-tv-lot-chip{position:absolute;z-index:3;left:50%;top:6vh;transform:translateX(-50%);min-width:170px;padding:16px 32px;border-radius:2px;background:#003b72;color:#fff;text-align:center;font-size:22px;font-weight:900;box-shadow:0 4px 15px rgba(0,0,0,.22)}.rosgan-tv-lower{position:absolute;z-index:3;left:2.2vw;right:2.2vw;bottom:3.8vh;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.2)}.rosgan-tv-mainline{display:grid;grid-template-columns:94px 115px minmax(210px,.7fr) minmax(300px,1.25fr);align-items:stretch;min-height:54px}.rosgan-tv-lot-block{grid-row:1/3;display:grid;place-items:center;border-radius:16px 0 0 16px;background:#113d70;text-align:center}.rosgan-tv-lot-block small{display:block;font-size:15px}.rosgan-tv-lot-block strong{display:block;font-size:28px}.rosgan-tv-cert,.rosgan-tv-category,.rosgan-tv-quantity{display:flex;align-items:center;padding:0 20px;font-size:18px;font-weight:900}.rosgan-tv-cert{background:#173f73}.rosgan-tv-category{background:#241e20}.rosgan-tv-quantity{background:#f8f8f7;color:#17191c;text-shadow:none;font-size:20px}.rosgan-tv-subline{display:grid;grid-template-columns:94px 260px 1fr;min-height:48px}.rosgan-tv-subline:before{content:""}.rosgan-tv-location,.rosgan-tv-conditions{display:flex;align-items:center;padding:0 20px;font-size:17px;font-weight:850}.rosgan-tv-location{background:#173f73}.rosgan-tv-conditions{background:#241e20}.rosgan-tv-contacts{margin-top:4px;padding:5px 12px;border-radius:0 12px 12px 0;background:#241e20;font-size:17px;font-weight:900;letter-spacing:.02em;white-space:nowrap;overflow:hidden}.rosgan-tv-no-video{position:absolute;z-index:2;inset:0;display:grid;place-items:center;color:#fff;font-size:16px;font-weight:800}.rosgan-tv-no-lot{position:absolute;inset:0;display:grid;place-items:center;color:#fff;font-size:18px}.rosgan-tv-preview-note{margin-top:12px;color:#64798e;font-size:12px;line-height:1.45} 3143@media(max-width:900px){.rosgan-ops-metrics{grid-template-columns:repeat(2,1fr)}.rosgan-commercial-row{grid-template-columns:64px 1fr 1fr}.rosgan-commercial-row>*:nth-child(n+4){grid-column:span 1}.rosgan-tv-config-grid{grid-template-columns:1fr}.rosgan-tv-mainline{grid-template-columns:70px 90px minmax(130px,.7fr) 1.2fr}.rosgan-tv-subline{grid-template-columns:70px 190px 1fr}.rosgan-tv-cert,.rosgan-tv-category,.rosgan-tv-quantity,.rosgan-tv-location,.rosgan-tv-conditions{padding:0 10px;font-size:13px}.rosgan-tv-contacts{font-size:12px}} 3144@media(max-width:620px){.rosgan-ops-page{padding:16px 12px 86px}.rosgan-ops-hero{display:block;padding:21px}.rosgan-ops-actions{justify-content:flex-start;margin-top:16px}.rosgan-ops-metrics{gap:7px}.rosgan-commercial-row{grid-template-columns:56px 1fr}.rosgan-commercial-row>*:nth-child(n+3){grid-column:1/-1}.rosgan-price-card div{grid-template-columns:1fr}.rosgan-tv-brand-bug{transform:scale(.68);transform-origin:left top}.rosgan-tv-lot-chip{top:4vh;min-width:100px;padding:10px 16px;font-size:14px}.rosgan-tv-lower{left:10px;right:10px}.rosgan-tv-mainline{grid-template-columns:58px 72px 1fr}.rosgan-tv-quantity{grid-column:2/-1;min-height:38px}.rosgan-tv-lot-block{grid-row:1/3}.rosgan-tv-subline{grid-template-columns:58px 1fr}.rosgan-tv-conditions{grid-column:2/-1;min-height:38px}.rosgan-tv-contacts{font-size:9px}} 3145 .rosgan-tv-brand-bug .rosgan-tv-rosgan{background:#fff} 3146/* Broadcast identity: viewport-scaled type, explicit logo boxes and a single aligned lower third. */ 3147html:has(.rosgan-tv-output),body:has(.rosgan-tv-output){overflow:hidden!important} 3148.rtv-brand{position:absolute;z-index:3;left:4.5vw;top:5vh;display:flex;align-items:center;height:4.6vw;min-height:38px;max-height:88px;filter:drop-shadow(0 6px 16px #00172a30)} 3149.rtv-brand-rosgan{display:flex;align-items:center;justify-content:center;width:10vw;height:100%;padding:1vw;background:#fff;border-radius:.55vw 0 0 .55vw;box-sizing:border-box} 3150.rtv-brand-rosgan img{width:100%;height:100%;object-fit:contain} 3151.rtv-brand-cons{width:4.6vw;height:100%;min-width:38px;display:flex;align-items:center;justify-content:center;background:var(--rosgan-cons-color);box-sizing:border-box;padding:.7vw;color:#fff;font-size:1.8vw;font-weight:800} 3152.rtv-brand-cons img{display:block!important;width:3.2vw!important;height:3.2vw!important;max-width:100%!important;max-height:100%!important;object-fit:contain!important} 3153.rtv-brand-name{height:100%;box-sizing:border-box;display:flex;flex-direction:column;justify-content:center;gap:.3vw;padding:0 1.25vw;background:#092b49;color:#fff;border-radius:0 .55vw .55vw 0;max-width:25vw} 3154.rtv-brand-name small{font-size:clamp(7px,.55vw,12px);letter-spacing:.17em;color:#9fc0d7;font-weight:700}.rtv-brand-name strong{font-size:clamp(10px,.96vw,21px);font-weight:700;line-height:1.2} 3155.rtv-lower{position:absolute;z-index:3;left:4.5vw;right:4.5vw;bottom:5vh;color:#fff;font-family:Arial,Helvetica,sans-serif;text-shadow:none;filter:drop-shadow(0 8px 22px #00142840)} 3156.rtv-panel{border-radius:.7vw;overflow:hidden;background:#092b49;border-top:.22vw solid #92dfca} 3157.rtv-primary{display:flex;align-items:stretch;min-height:6.5vw}
3158.rtv-number{flex:0 0 7vw;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.15vw;background:#123d5b;border-right:1px solid #ffffff1f;padding:.6vw;box-sizing:border-box} 3159.rtv-number small{font-size:clamp(9px,.72vw,16px);font-weight:700;letter-spacing:.16em;color:#b5d6e6}.rtv-number strong{font-size:clamp(26px,3.25vw,70px);font-weight:800;line-height:1;letter-spacing:-.045em;font-variant-numeric:tabular-nums;color:#fff} 3160.rtv-identity{flex:1;min-width:0;padding:1vw 1.6vw;display:flex;flex-direction:column;justify-content:center;gap:.28vw} 3161.rtv-certificate{font-size:clamp(8px,.67vw,15px);font-weight:700;letter-spacing:.1em;color:#92dfca}.rtv-title{margin:0!important;font-size:clamp(19px,2.05vw,46px)!important;font-weight:800!important;line-height:1.08!important;color:#fff!important;letter-spacing:-.025em;overflow-wrap:anywhere} 3162.rtv-breed{font-size:clamp(10px,.98vw,22px);line-height:1.25;color:#c2d8e7;font-weight:500} 3163.rtv-metrics{display:flex;align-items:stretch;flex:0 0 auto;background:#f3f7f8;color:#092b49} 3164.rtv-metric{display:flex;flex-direction:column;justify-content:center;gap:.4vw;min-width:11vw;padding:.8vw 1.7vw;box-sizing:border-box;border-left:1px solid #092b491a} 3165.rtv-metric small{font-size:clamp(8px,.67vw,15px);letter-spacing:.1em;font-weight:700;color:#496576}.rtv-metric strong{font-size:clamp(22px,2.6vw,56px);line-height:1;font-weight:800;letter-spacing:-.035em;white-space:nowrap;font-variant-numeric:tabular-nums} 3166.rtv-secondary{display:flex;align-items:stretch;border-top:1px solid #ffffff20;background:#061f36} 3167.rtv-detail{display:flex;flex-direction:column;justify-content:center;gap:.3vw;padding:.7vw 1.6vw;border-right:1px solid #ffffff1c;min-width:0;flex:1}.rtv-detail:first-child{flex:1.2}.rtv-detail:last-child{flex:.65;border:0} 3168.rtv-detail small{font-size:clamp(7px,.57vw,13px);font-weight:700;letter-spacing:.13em;color:#90b2c8}.rtv-detail strong{font-size:clamp(10px,1.04vw,23px);font-weight:600;line-height:1.2;overflow-wrap:anywhere} 3169.rtv-contacts{display:flex;align-items:center;gap:1.4vw;margin-top:.4vw;padding:.6vw 1.25vw;border-radius:.35vw;background:#f3f7f8;color:#123d5b;font-size:clamp(9px,.92vw,20px);font-weight:700;line-height:1.35}.rtv-contacts-label{flex:none;font-size:clamp(7px,.6vw,13px);letter-spacing:.12em;color:#4b6778}.rtv-contacts-copy{overflow-wrap:anywhere} 3170.rtv-brand sc-if,.rtv-metrics sc-if,.rtv-secondary sc-if{display:contents}.rtv-lower .sc-interp,.rtv-brand .sc-interp{font:inherit;color:inherit;letter-spacing:inherit;line-height:inherit} 3171@media(max-aspect-ratio:1/1){.rtv-lower{left:4vw;right:4vw}.rtv-primary{flex-wrap:wrap}.rtv-number{flex-basis:15%;min-height:70px}.rtv-identity{flex-basis:70%;padding:12px}.rtv-metrics{width:100%}.rtv-metric{flex:1;padding:10px 14px}.rtv-detail{padding:10px;gap:5px}.rtv-brand-name{max-width:45vw}.rtv-brand-rosgan{width:24vw;padding:8px}.rtv-brand-cons{width:38px;padding:6px}.rtv-brand-cons img{width:26px!important;height:26px!important}} 3172/* TV height budget including contacts stays below one sixth of the viewport. */ 3173.rtv-lower{max-height:16.6667vh} 3174.rtv-panel{border-top-width:.25vh} 3175.rtv-primary{height:9vh;min-height:0;flex-wrap:nowrap} 3176.rtv-number{min-height:0;flex-basis:7vw;padding:.4vh .6vw;gap:.2vh} 3177.rtv-number small{font-size:1.1vh}.rtv-number strong{font-size:4.5vh} 3178.rtv-identity{flex-basis:auto;padding:.6vh 1.6vw;gap:.2vh;overflow:hidden} 3179.rtv-certificate{font-size:1.05vh}.rtv-title{font-size:3.1vh!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.rtv-breed{font-size:1.6vh;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} 3180.rtv-metrics{width:auto}.rtv-metric{padding:.6vh 1.7vw;gap:.45vh}.rtv-metric small{font-size:1.05vh}.rtv-metric strong{font-size:3.8vh} 3181.rtv-secondary{height:4.3vh;box-sizing:border-box} 3182.rtv-lower .rtv-metric strong.rtv-quantity{font-size:4vh;line-height:1} 3183.rtv-quantity-breakdown{display:flex;gap:.35em;font-size:3.2vh;font-weight:700;line-height:1;color:#092b49;font-variant-numeric:tabular-nums} 3184.rtv-quantity-breakdown>span+span::before{content:'/';margin-right:.35em} 3185.rtv-quantity-breakdown span{white-space:nowrap} 3186.rtv-primary:has(.rtv-quantity-breakdown){min-height:10.6vh} 3187.rtv-visec-badge{background:#16734e;color:#fff}.rtv-visec-badge img{filter:brightness(0) invert(1)} 3188.rtv-lower:has(.rtv-china-badge:not(.rtv-visec-badge)) .rtv-visec-badge{left:14vw} 3189.rtv-detail{padding:.35vh 1.6vw;gap:.15vh;overflow:hidden}.rtv-detail small{font-size:.9vh}.rtv-detail strong{font-size:1.65vh;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} 3190.rtv-contacts{height:2.5vh;box-sizing:border-box;margin-top:.3vh;padding:0 1.25vw;font-size:1.4vh;overflow:hidden}.rtv-contacts-label{font-size:.95vh}.rtv-contacts-copy{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} 3191.rtv-lower:not(:has(.rtv-contacts)) .rtv-primary{height:11.8vh} 3192.rtv-lower:not(:has(.rtv-contacts)) .rtv-number strong{font-size:5.3vh} 3193.rtv-lower:not(:has(.rtv-contacts)) .rtv-title{font-size:3.7vh!important} 3194.rtv-lower:not(:has(.rtv-contacts)) .rtv-breed{font-size:1.8vh} 3195.rtv-lower:not(:has(.rtv-contacts)) .rtv-metric strong{font-size:4.5vh} 3196.rtv-secondary{height:5.5vh} 3197.rtv-detail{gap:.25vh}.rtv-detail small{font-size:1.15vh}.rtv-detail strong{font-size:2.2vh} 3198.rtv-primary{height:7.8vh}.rtv-lower:not(:has(.rtv-contacts)) .rtv-primary{height:10.6vh} 3199.rtv-lower:has(.rtv-contacts) .rtv-secondary{height:4.3vh} 3200.rtv-contacts{height:3.7vh;font-size:2.5vh;font-weight:800;line-height:1.1;gap:1vw} 3201.rtv-contacts-label{font-size:1.1vh;letter-spacing:.08em} 3202.rtv-identity{padding:.6vh 2.1vw} 3203.rtv-metric{padding:.6vh 2.1vw} 3204.rtv-detail{flex-direction:row;align-items:center;justify-content:flex-start;gap:.8vw;padding:.75vh 2.1vw;box-sizing:border-box} 3205.rtv-detail small{flex-shrink:0;font-size:1.7vh;line-height:1.2;font-weight:800;letter-spacing:.035em} 3206.rtv-detail strong{min-width:0;font-size:2.2vh;line-height:1.2} 3207.rtv-secondary .rtv-detail--location{flex:1 1 0}.rtv-secondary .rtv-detail--payment{flex:1.5 1 0} 3208.rtv-contacts{padding:.6vh 2.1vw;gap:1.5vw;font-size:2.2vh;line-height:1.1} 3209.rtv-identity{flex-direction:row;align-items:stretch;gap:1.5vw} 3210.rtv-identity-fixed{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:.2vh} 3211.rtv-identity sc-if{display:contents} 3212.rtv-rotating{flex:0 0 42%;min-width:0;box-sizing:border-box;border-left:1px solid #ffffff30;padding:.35vh 0 .35vh 1.5vw;
3212display:flex;flex-direction:column;justify-content:center;gap:.55vh} 3213.rtv-rotating-label{display:flex;align-items:center;justify-content:space-between;gap:1vw;font-size:1.7vh;font-weight:800;line-height:1.15;letter-spacing:.045em;color:#b5f2e1} 3214.rtv-rotating-value{font-size:2.2vh;font-weight:600;line-height:1.2;color:#fff;overflow-wrap:anywhere} 3215.rtv-observations{position:absolute;bottom:calc(100% + .45vh);left:0;right:0;height:3.4vh;display:flex;align-items:center;overflow:hidden;border-radius:.35vw;background:#061f36ed;color:#fff} 3216.rtv-observations-label{align-self:stretch;display:flex;align-items:center;flex-shrink:0;padding:0 1.25vw;background:#123d5b;color:#92dfca;font-size:1.1vh;font-weight:700;letter-spacing:.09em} 3217.rtv-observations-window{min-width:0;flex:1;overflow:hidden} 3218.rtv-observations-track{display:flex;width:max-content;animation:rtv-observations-roll var(--roll-duration,35s) linear infinite;will-change:transform} 3219.rtv-observations-copy{flex:none;white-space:nowrap;padding-right:5vw;font-size:2.2vh;font-weight:600;line-height:1.2} 3220@keyframes rtv-observations-roll{from{transform:translateX(0)}to{transform:translateX(-50%)}} 3221.rosgan-tv-output video[data-rosgan-tv-deck]{opacity:0;z-index:0;pointer-events:none} 3222.rosgan-tv-output video[data-rosgan-tv-deck].is-on-air{opacity:1;z-index:1} 3223.rosgan-tv-output video[data-rosgan-tv-multi-deck]{opacity:0;z-index:2;pointer-events:none} 3224.rosgan-tv-output.is-multi video[data-rosgan-tv-multi-deck].is-on-air{opacity:1} 3225.rosgan-tv-output.is-multi video[data-rosgan-tv-deck]{opacity:0}
3225 3226.ros-tv-air-pane{position:absolute;width:100%;height:100%;transform-origin:top left;z-index:4;pointer-events:none} 3227.rg-commercial-page .commercial-summary sc-for{display:contents}.rg-commercial-page .commercial-hero{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.rg-commercial-head,.rg-commercial-row{display:grid;grid-template-columns:50px minmax(200px,1.45fr) minmax(155px,1fr) minmax(180px,1fr) minmax(180px,1fr);gap:18px;align-items:center;padding:16px 20px}.rg-commercial-head{position:sticky;top:64px;z-index:10;background:#f6f9fc;border-radius:8px 8px 0 0;color:#71839a;font-size:10px;font-weight:850;text-transform:uppercase;letter-spacing:.07em;border-bottom:1px solid #dce7f2}.rg-commercial-row{border-left:3px solid transparent;transition:background .15s}.rg-commercial-row:hover{background:#fbfdff}.rg-commercial-row.is-sold{border-left-color:#17704d}.rg-commercial-row.is-lifted{border-left-color:#94a3b8;background:#f8fafc}.rg-commercial-identity{display:grid;gap:5px;min-width:0}.rg-commercial-identity>small{color:#7a8da1;font-size:10px;font-weight:800}.rg-commercial-identity>strong{font-size:15px;color:#10233b}.rg-commercial-breed{font-size:11px;color:#65758a}.rg-commercial-cons{display:flex;align-items:center;gap:8px;margin-top:3px;color:#526b82;font-size:11px}.rg-commercial-cons .commercial-cons-avatar{flex-shrink:0;box-shadow:none}.rg-commercial-data{display:grid;gap:6px;color:#526b82;font-size:12px;line-height:1.4}.rg-commercial-data strong{color:#173b5c;font-size:13px}.rg-commercial-data small{font-size:11px}.rg-commercial-status{display:grid;gap:8px}.rg-commercial-status>.rosgan-ops-status{justify-self:start}.rg-commercial-status .commercial-status{grid-template-columns:repeat(3,minmax(0,1fr));gap:4px}.rg-commercial-status .commercial-status button{min-height:32px;font-size:10px}.rg-commercial-tools{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap}.rg-commercial-search{display:flex;align-items:center;gap:9px;background:#fff;border:1px solid #d8e5f1;border-radius:9px;padding:0 12px;width:min(330px,100%);box-sizing:border-box}.rg-commercial-search img{width:17px;height:17px}.rg-commercial-search input{width:100%;min-height:40px;outline:none;border:0;background:transparent;font:inherit;font-size:12px;color:#173b5c}.rg-commercial-search:focus-within{outline:2px solid #0a5ca8}.rg-commercial-caption{display:flex;justify-content:space-between;gap:12px;margin:14px 0;color:#71839a;font-size:12px}.rg-commercial-caption strong{color:#173b5c}.rg-commercial-page .commercial-price input{font-size:15px;min-height:40px;font-variant-numeric:tabular-nums}.rg-commercial-page .commercial-price{gap:8px}@media(max-width:1050px){.rg-commercial-page .commercial-hero{grid-template-columns:1fr}.rg-commercial-page .commercial-hero-actions{justify-content:flex-start}.rg-commercial-head{display:none}.rg-commercial-row{grid-template-columns:38px minmax(0,1.4fr) minmax(0,1fr);gap:14px}.rg-commercial-status{grid-column:2}.rg-commercial-row>.commercial-price{grid-column:3}.rg-commercial-order{align-self:start}}@media(max-width:600px){.rg-commercial-tools{display:block}.rg-commercial-search{width:100%;margin-bottom:12px}.rg-commercial-row{grid-template-columns:32px minmax(0,1fr);padding:16px 12px;gap:12px}.rg-commercial-data,.rg-commercial-status,.rg-commercial-row>.commercial-price{grid-column:2}.rg-commercial-data{padding:9px 0;border-top:1px solid #e7eef5}.rg-commercial-page .commercial-hero-actions{gap:7px}.rg-commercial-page .commercial-hero-actions button{font-size:11px}} 3228.ros-tv-lot-row.is-lifted{opacity:.5;background:#f1f4f7;cursor:not-allowed}.ros-tv-lot-main:disabled{cursor:not-allowed}.ros-tv-sold-check{display:inline-flex;vertical-align:middle;margin-left:5px;color:#17704d}.ros-tv-lifted-label{margin-left:6px;color:#687b91;font-weight:700} 3229.ros-tv-contacts-section{margin-top:18px}.ros-tv-contact-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.ros-tv-contact-grid sc-for{display:contents}.ros-tv-contact-card{display:grid;gap:9px;padding:16px;border:1px solid #dbe5ef;border-radius:12px;background:#f8fafc}.ros-tv-contact-identity{display:flex;align-items:center;gap:12px;min-width:0;color:#12334e;font-size:14px}.ros-tv-contact-logo{display:grid;place-items:center;flex-shrink:0;width:48px;height:48px;padding:7px;box-sizing:border-box;border-radius:9px;color:#fff;font-weight:800}.ros-tv-contact-logo img{max-width:100%;max-height:100%;object-fit:contain}.ros-tv-contact-caption{font-size:11px;font-weight:700;color:#63788e}.ros-tv-contact-card input{width:100%;box-sizing:border-box;padding:11px;border:1px solid #cad8e6;border-radius:9px;background:#fff;color:#12334e;font:inherit;font-size:13px}.ros-tv-contact-card input:focus{outline:2px solid #0a5ca8;outline-offset:1px}@media(max-width:650px){.ros-tv-contact-grid{grid-template-columns:1fr}} 3230.rosgan-tv-output{background-color:#002e5f;background-image:repeating-linear-gradient(135deg,transparent 0,transparent 55px,rgba(255,255,255,.035) 55px,rgba(255,255,255,.035) 56px),radial-gradient(ellipse at 20% 15%,#084c80 0,transparent 65%)} 3231html[data-rosgan-tv-output="true"] #rosgan-static-boot,html[data-rosgan-tv-output="true"] .rosgan-fast-loader{display:none!important} 3232</style> 3233</helmet> 3234 3235<!-- Permite confirmar el montaje de React aun durante una restauracion de sesion silenciosa. --> 3236<span aria-hidden="true" style="position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap;">Interfaz principal de Rosgan preparada</span> 3237 3238<!-- ===================== APP WELCOME ===================== --> 3239<sc-if value="{{ showConnectingScreen }}" hint-placeholder-val="{{ false }}"> 3240 <main class="app-welcome app-connecting-screen" aria-label="Conectando con Rosgan" aria-busy="true"> 3241 <div class="app-welcome-content"> 3242 <div class="app-welcome-lottie-slot"> 3243 <dotlottie-wc class="app-connecting-lottie" data-dc-attr-src="{{ connectingLottieSrc }}" autoplay loop speed="1" aria-label="Conectando con los servidores"></dotlottie-wc> 3244 </div>
3245 <span class="app-welcome-kicker">Conexión segura</span> 3246 <h1>Conectando con Rosgan</h1> 3247 <p class="app-connecting-description" role="status">Estamos preparando tu acceso al mercado ganadero.</p> 3248 </div> 3249 </main> 3250</sc-if> 3251 3252<sc-if value="{{ showWelcomeScreen }}" hint-placeholder-val="{{ false }}"> 3253 <main class="app-welcome app-connecting-screen app-connection-success" aria-label="Bienvenida a Rosgan"> 3254 <div class="app-welcome-content"> 3255 <div class="app-welcome-lottie-slot"> 3256 <dotlottie-wc class="app-welcome-lottie" data-dc-attr-src="{{ welcomeLottieSrc }}" autoplay speed="1" aria-label="Acceso confirmado"></dotlottie-wc> 3257 </div> 3258 <span class="app-welcome-kicker">Conexión confirmada</span> 3259 <h1>{{ advanceGreeting }}</h1> 3260 <p class="app-connecting-description" role="status">Ya estás conectado. Bienvenido a Rosgan.</p> 3261 </div> 3262 </main> 3263</sc-if> 3264 3265<sc-if value="{{ authTransitioning }}" hint-placeholder-val="{{ false }}"> 3266 <div class="auth-view-bridge" aria-hidden="true"></div> 3267</sc-if> 3268 3269<!-- ===================== AUTH RESTORING ===================== --> 3270<sc-if value="{{ isAuthRestoring }}" hint-placeholder-val="{{ false }}"> 3271 <main class="rosgan-fast-loader" aria-label="Cargando Rosgan"> 3272 <span class="rosgan-fast-loader-spinner" aria-hidden="true"></span> 3273 </main> 3274</sc-if> 3275 3276<!-- ===================== LOGIN ===================== --> 3277<sc-if value="{{ isLogin }}" hint-placeholder-val="{{ true }}"> 3278 <div class="rosgan-auth-screen" style="min-height:100vh; min-height:100svh; position:relative; overflow:hidden; display:grid; place-items:center; padding:24px; box-sizing:border-box;"> 3279 <img class="rosgan-auth-background" src="assets/angus-field.png" alt="" style="position:absolute; inset:0; width:100%; height:100%; object-fit:cover; animation:kenburns 24s ease-in-out infinite alternate;"> 3280 <div class="rosgan-auth-shade" style="position:absolute; inset:0; background:linear-gradient(120deg, rgba(0,29,61,0.96) 30%, rgba(0,46,95,0.6) 70%, rgba(10,92,168,0.3));"></div> 3281 <form class="rosgan-auth-card" onSubmit="{{ submitAuthForm }}" style="position:relative; width:min(400px, 100%); background:rgba(255,255,255,0.1); backdrop-filter:blur(22px); -webkit-backdrop-filter:blur(22px); border:1px solid rgba(255,255,255,0.22); border-radius:22px; padding:clamp(26px, 4vw, 38px) clamp(22px, 4vw, 34px); color:#fff; display:flex; flex-direction:column; gap:15px; animation:pageIn 500ms cubic-bezier(0.22,0.72,0.18,1) both;"> 3282 <img class="rosgan-auth-logo" src="assets/rosgan-logo-blue.png" alt="Rosgan" style="filter:brightness(0) invert(1); margin:0 auto;"> 3283 <div style="text-align:center;"> 3284 <div style="font-size:22px; font-weight:800; letter-spacing:-0.01em;">{{ loginTitle }}</div> 3285 <div style="font-size:13px; color:#c8d8ea; margin-top:2px;">{{ loginSubtitle }}</div> 3286 </div> 3287 <sc-if value="{{ showAccountAssistantLauncher }}" hint-placeholder-val="{{ true }}"> 3288 <button type="button" class="rosgan-account-help-launcher" onClick="{{ openAccountAssistant }}"><span aria-hidden="true" style="width:21px;height:21px;border:2px solid currentColor;border-radius:50%;display:grid;place-items:center;font-size:13px;line-height:1;font-weight:950;">?</span>Necesito ayuda para crear mi cuenta</button> 3289 </sc-if> 3290 <sc-if value="{{ accountAssistantOpen }}" hint-placeholder-val="{{ false }}"> 3291 <section class="rosgan-account-assistant" role="region" aria-live="polite" aria-label="Asistente para crear una cuenta"> 3292 <div class="rosgan-account-assistant-head"> 3293 <span class="rosgan-account-assistant-avatar" aria-hidden="true">ð</span> 3294 <div class="rosgan-account-assistant-copy"><strong>{{ accountAssistantTitle }}</strong><span>Estoy acá para acompañarte. Vamos de a un paso por vez.</span></div> 3295 <button type="button" class="rosgan-account-assistant-close" onClick="{{ closeAccountAssistant }}" aria-label="Cerrar la ayuda" title="Cerrar">Ã</button> 3296 </div> 3297 <sc-if value="{{ accountAssistantHasProgress }}" hint-placeholder-val="{{ false }}"> 3298 <div class="rosgan-account-assistant-progress"><b>{{ accountAssistantProgressLabel }}</b><span style="--account-help-progress:{{ accountAssistantProgress }};"><i></i></span></div> 3299 </sc-if> 3300 <p class="rosgan-account-assistant-text">{{ accountAssistantText }}</p> 3301 <sc-if value="{{ accountAssistantHasTip }}" hint-placeholder-val="{{ true }}"><div class="rosgan-account-assistant-tip">{{ accountAssistantTip }}</div></sc-if> 3302 <sc-if value="{{ accountAssistantMessage }}" hint-placeholder-val="{{ false }}"><div class="rosgan-account-assistant-error" role="alert">{{ accountAssistantMessage }}</div></sc-if> 3303 <sc-if value="{{ accountAssistantIsChoice }}" hint-placeholder-val="{{ true }}"> 3304 <div class="rosgan-account-assistant-actions"> 3305 <button type="button" class="rosgan-account-assistant-primary" onClick="{{ chooseAccountAssistantGoogle }}"><img src="assets/google-logo.webp" alt="" style="width:19px;height:19px;vertical-align:middle;background:#fff;border-radius:50%;padding:2px;"> Usar mi cuenta de Google</button> 3306 <button type="button" class="rosgan-account-assistant-secondary" onClick="{{ chooseAccountAssistantEmail }}"><img src="assets/icons/lucide/mail.svg" alt="" style="width:18px;height:18px;vertical-align:middle;"> Usar correo y contraseña</button> 3307 </div> 3308 </sc-if> 3309 <sc-if value="{{ accountAssistantIsGoogle }}" hint-placeholder-val="{{ false }}"> 3310 <div class="rosgan-account-assistant-actions"> 3311 <button type="button" class="rosgan-account-assistant-secondary" onClick="{{ backAccountAssistant }}">Volver</button> 3312 <button type="button" class="rosgan-account-assistant-primary" onClick="{{ googleAuth }}"><img src="assets/google-logo.webp" alt="" style="width:19px;height:19px;vertical-align:middle;background:#fff;border-radius:50%;padding:2px;"> Continuar con Google</button> 3313 </div> 3314 </sc-if> 3315 <sc-if value="{{ accountAssistantIsEmail }}" hint-placeholder-val="{{ false }}"> 3316 <div class="rosgan-account-assistant-actions"> 3317 <button type="button" class="rosgan-account-assistant-secondary" onClick="{{ backAccountAssistant }}">{{ accountAssistantBackLabel }}</button> 3318 <button type="button" class="rosgan-account-assistant-primary" onClick="{{ nextAccountAssistant }}" disabled="{{ accountAssistantNextDisabled }}">{{ accountAssistantNextLabel }}</button> 3319 </div> 3320 </sc-if> 3321 </section> 3322 </sc-if> 3323 <sc-if value="{{ showAuthNameField }}" hint-placeholder-val="{{ false }}"> 3324 <input class="{{ authNameClass }}" data-account-field="name" value="" data-dc-attr-value="{{ formName }}" onInput="{{ onName }}" onKeyDown="{{ submitAuthOnEnter }}" placeholder="Nombre y apellido" autocomplete="name" style="background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.22); border-radius:12px; padding:13px 14px; font-size:14px; color:#fff; font-family:inherit; outline:none; box-sizing:border-box; width:100%;" style-focus="border-color:rgba(255,255,255,0.6);"> 3325 </sc-if> 3326 <sc-if value="{{ showAuthPhoneField }}" hint-placeholder-val="{{ false }}"> 3327 <input class="{{ authPhoneClass }}" data-account-field="phone" value="" data-dc-attr-value="{{ formPhone }}" onInput="{{ onFormPhone }}" onKeyDown="{{ submitAuthOnEnter }}" type="tel" inputmode="tel" placeholder="+54 9 341 000 0000" autocomplete="tel" aria-label="Teléfono móvil" style="background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.22); border-radius:12px; padding:13px 14px; font-size:14px; color:#fff; font-family:inherit; outline:none; box-sizing:border-box; width:100%;" style-focus="border-color:rgba(255,255,255,0.6);"> 3328 </sc-if> 3329 <sc-if value="{{ showAuthEmailField }}" hint-placeholder-val="{{ true }}"> 3330 <input class="{{ authEmailClass }}" data-account-field="email" value="" data-dc-attr-value="{{ formEmail }}" onInput="{{ onEmail }}" onKeyDown="{{ submitAuthOnEnter }}" type="email" placeholder="[email protected]" autocomplete="email" style="background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.22); border-radius:12px; padding:13px 14px; font-size:14px; color:#fff; font-family:inherit; outline:none; box-sizing:border-box; width:100%;" style-focus="border-color:rgba(255,255,255,0.6);"> 3331 </sc-if> 3332 <sc-if value="{{ showAuthPasswordField }}" hint-placeholder-val="{{ true }}"> 3333 <sc-if value="{{ showPassword }}" hint-placeholder-val="{{ true }}"> 3334 <div style="position:relative; width:100%;"> 3335 <input class="{{ authPasswordClass }}" data-account-field="password" value="" data-dc-attr-value="{{ formPassword }}" onInput="{{ onPassword }}" onKeyDown="{{ submitAuthOnEnter }}" type="{{ passwordInputType }}" placeholder="Contraseña" autocomplete="{{ passwordAutocomplete }}" style="background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.22); border-radius:12px; padding:13px 50px 13px 14px; font-size:14px; color:#fff; font-family:inherit; outline:none; box-sizing:border-box; width:100%;" style-focus="border-color:rgba(255,255,255,0.6);"> 3336 <button type="button" onClick="{{ toggleAuthPasswordVisibility }}" aria-label="{{ passwordVisibilityLabel }}" aria-pressed="{{ authPasswordVisible }}" title="{{ passwordVisibilityLabel }}" style="position:absolute; top:50%; right:6px; width:38px; height:38px; transform:translateY(-50%); display:grid; place-items:center; padding:0; border:0; border-radius:9px; background:transparent; color:#fff; cursor:pointer; opacity:.9;" style-hover="background:rgba(255,255,255,.12); opacity:1;">
3337 <span aria-hidden="true" style="position:relative; width:20px; height:20px; display:grid; place-items:center;"><img src="assets/icons/lucide/eye.svg" alt="" style="width:19px; height:19px; filter:brightness(0) invert(1);"><sc-if value="{{ authPasswordVisible }}" hint-placeholder-val="false"><span style="position:absolute; width:22px; height:2px; border-radius:999px; background:#fff; transform:rotate(-45deg);"></span></sc-if></span> 3338 </button> 3339 </div> 3340 <sc-if value="{{ isRegister }}" hint-placeholder-val="{{ false }}"> 3341 <div aria-label="Requisitos de contraseña" style="display:grid; gap:7px; padding:1px 3px 2px; font-size:12px; color:#d6e4f2;"> 3342 <div style="display:flex; align-items:center; gap:8px; color:{{ passwordLengthColor }}; transition:color 180ms ease;"> 3343 <span aria-hidden="true" style="width:19px; height:19px; display:grid; place-items:center; flex:0 0 19px; border-radius:50%; border:1px solid {{ passwordLengthBorder }}; background:{{ passwordLengthBg }}; color:{{ passwordLengthIconColor }}; font-size:12px; font-weight:950; transition:all 180ms ease;"><sc-if value="{{ passwordLengthValid }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check"></i></sc-if></span> 3344 MÃnimo 8 caracteres 3345 </div> 3346 <div style="display:flex; align-items:center; gap:8px; color:{{ passwordUpperColor }}; transition:color 180ms ease;"> 3347 <span aria-hidden="true" style="width:19px; height:19px; display:grid; place-items:center; flex:0 0 19px; border-radius:50%; border:1px solid {{ passwordUpperBorder }}; background:{{ passwordUpperBg }}; color:{{ passwordUpperIconColor }}; font-size:12px; font-weight:950; transition:all 180ms ease;"><sc-if value="{{ passwordUpperValid }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check"></i></sc-if></span> 3348 Al menos 1 letra mayúscula 3349 </div> 3350 </div> 3351 </sc-if> 3352 </sc-if> 3353 <sc-if value="{{ showPrimaryAuthSubmit }}" hint-placeholder-val="{{ true }}"><button class="{{ authSubmitClass }}" data-account-field="submit" type="submit" disabled="{{ authBusy }}" style="background:#fff; color:#002e5f; border:none; padding:14px; border-radius:12px; font-weight:700; font-size:15px; font-family:inherit; cursor:pointer; transition:transform 160ms; opacity:{{ authBtnOpacity }};" style-hover="transform:translateY(-2px);" style-active="transform:scale(0.98);">{{ submitLabel }}</button></sc-if> 3354 </sc-if> 3355 <sc-if value="{{ showDefaultGoogleAuth }}" hint-placeholder-val="{{ true }}"> 3356 <div style="display:flex; align-items:center; gap:10px;"><span style="flex:1; height:1px; background:rgba(255,255,255,0.2);"></span><span style="font-size:12px; color:#c8d8ea;">o continuá con</span><span style="flex:1; height:1px; background:rgba(255,255,255,0.2);"></span></div> 3357 <button type="button" onClick="{{ googleAuth }}" style="background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.22); border-radius:12px; padding:12px; display:flex; justify-content:center; gap:10px; align-items:center; font-size:14px; color:#fff; font-family:inherit; cursor:pointer;" style-hover="background:rgba(255,255,255,0.16);"><img src="assets/google-logo.webp" alt="" style="width:20px; height:20px; background:#fff; border-radius:50%; padding:2px;">Continuar con Google</button> 3358 </sc-if> 3359 <sc-if value="{{ authMessage }}" hint-placeholder-val="{{ false }}"> 3360 <div style="font-size:13px; text-align:center; padding:10px 12px; border-radius:10px; background:rgba(230,57,70,0.25); border:1px solid rgba(230,57,70,0.4); color:#ffd3d7;">{{ authMessage }}</div> 3361 </sc-if> 3362 <sc-if value="{{ showAuthFooterLinks }}" hint-placeholder-val="{{ true }}"><div style="display:flex; justify-content:center; gap:18px; font-size:12.5px; color:#c8d8ea;"> 3363 <span onClick="{{ toggleReset }}" style="cursor:pointer;" style-hover="color:#ffffff;">{{ resetLink }}</span> 3364 <span onClick="{{ toggleRegister }}" style="cursor:pointer;" style-hover="color:#ffffff;">{{ registerLink }}</span> 3365 </div></sc-if> 3366 </form> 3367 </div> 3368</sc-if> 3369 3370<!-- ===================== EMAIL VERIFICATION ===================== --> 3371<sc-if value="{{ isEmailVerification }}" hint-placeholder-val="{{ false }}">
3372 <div class="rosgan-auth-screen" style="min-height:100vh; min-height:100svh; position:relative; overflow:hidden; display:grid; place-items:center; padding:24px; box-sizing:border-box;"> 3373 <img class="rosgan-auth-background" src="assets/angus-field.png" alt="" style="position:absolute; inset:0; width:100%; height:100%; object-fit:cover; animation:kenburns 24s ease-in-out infinite alternate;"> 3374 <div class="rosgan-auth-shade" style="position:absolute; inset:0; background:linear-gradient(120deg, rgba(0,29,61,0.97) 30%, rgba(0,46,95,0.72) 70%, rgba(10,92,168,0.42));"></div> 3375 <section class="rosgan-auth-card" aria-labelledby="email-verification-title" style="position:relative; width:min(440px, 100%); box-sizing:border-box; background:rgba(255,255,255,0.11); backdrop-filter:blur(22px); -webkit-backdrop-filter:blur(22px); border:1px solid rgba(255,255,255,0.24); border-radius:22px; padding:clamp(26px, 4vw, 38px) clamp(22px, 4vw, 34px); color:#fff; display:flex; flex-direction:column; gap:16px; animation:pageIn 500ms cubic-bezier(0.22,0.72,0.18,1) both; box-shadow:0 28px 70px rgba(0,14,34,.32);"> 3376 <img class="rosgan-auth-logo" src="assets/rosgan-logo-blue.png" alt="Rosgan" style="filter:brightness(0) invert(1); margin:0 auto 2px;"> 3377 <div style="width:58px; height:58px; display:grid; place-items:center; align-self:center; border-radius:50%; background:rgba(255,255,255,.13); border:1px solid rgba(255,255,255,.26);"><img src="assets/icons/lucide/mail.svg" alt="" style="width:26px;height:26px;filter:brightness(0) invert(1);"></div> 3378 <div style="text-align:center;"> 3379 <div id="email-verification-title" style="font-size:24px; font-weight:850; letter-spacing:-0.02em;">Verificá tu correo</div> 3380 <div style="font-size:13px; line-height:1.55; color:#c8d8ea; margin-top:6px;">Enviamos un enlace de confirmación a <strong style="color:#fff;">{{ emailVerificationEmail }}</strong>. Abrilo y después volvé acá: lo comprobaremos automáticamente.</div> 3381 </div> 3382 <sc-if value="{{ accountAssistantOpen }}" hint-placeholder-val="{{ false }}"> 3383 <div class="rosgan-account-verification-help" role="status"><strong>Te acompaño en este paso</strong><span>1. Abrà tu correo. 2. Buscá el mensaje de Rosgan. 3. Tocá el enlace de confirmación. 4. Volvé a esta pantalla y presioná âYa verifiqué mi correoâ. Si no aparece, revisá Spam.</span><button type="button" class="rosgan-account-assistant-close" onClick="{{ closeAccountAssistant }}" aria-label="Cerrar la ayuda" style="justify-self:end;">Ã</button></div> 3384 </sc-if> 3385 <button onClick="{{ checkEmailVerification }}" disabled="{{ emailVerificationBusy }}" style="background:#fff; color:#002e5f; border:none; padding:14px; border-radius:12px; font-weight:800; font-size:15px; font-family:inherit; cursor:pointer; opacity:{{ emailVerificationOpacity }};">{{ emailVerificationCheckLabel }}</button> 3386 <button onClick="{{ resendEmailVerification }}" disabled="{{ emailVerificationBusy }}" style="background:transparent; color:#e4f0fb; border:1px solid rgba(255,255,255,.3); padding:11px; border-radius:12px; font-weight:750; font-size:13px; font-family:inherit; cursor:pointer; opacity:{{ emailVerificationOpacity }};">{{ emailVerificationResendLabel }}</button> 3387 <sc-if value="{{ emailVerificationMessage }}" hint-placeholder-val="{{ false }}"> 3388 <div role="status" style="font-size:13px; line-height:1.45; text-align:center; padding:10px 12px; border-radius:10px; background:rgba(22,184,133,0.18); border:1px solid rgba(95,230,187,0.34); color:#e6fff7;">{{ emailVerificationMessage }}</div> 3389 </sc-if> 3390 <div style="font-size:11.5px; line-height:1.45; text-align:center; color:#b9cce0;">Si no lo encontrás, revisá Spam o Correo no deseado.</div> 3391 <button onClick="{{ logoutFromEmailVerification }}" disabled="{{ emailVerificationBusy }}" style="align-self:center; background:none; border:0; color:#c8d8ea; font:700 12px inherit; cursor:pointer; text-decoration:underline; text-underline-offset:3px;">Salir y usar otra cuenta</button> 3392 </section> 3393 </div> 3394</sc-if> 3395 3396<!-- ===================== PHONE VERIFICATION ===================== --> 3397<sc-if value="{{ isPhoneVerification }}" hint-placeholder-val="{{ false }}">
3398 <div class="rosgan-auth-screen" style="min-height:100vh; min-height:100svh; position:relative; overflow:hidden; display:grid; place-items:center; padding:24px; box-sizing:border-box;"> 3399 <img class="rosgan-auth-background" src="assets/angus-field.png" alt="" style="position:absolute; inset:0; width:100%; height:100%; object-fit:cover; animation:kenburns 24s ease-in-out infinite alternate;"> 3400 <div class="rosgan-auth-shade" style="position:absolute; inset:0; background:linear-gradient(120deg, rgba(0,29,61,0.97) 30%, rgba(0,46,95,0.72) 70%, rgba(10,92,168,0.42));"></div> 3401 <section class="rosgan-auth-card" aria-labelledby="phone-verification-title" style="position:relative; width:min(440px, 100%); box-sizing:border-box; background:rgba(255,255,255,0.11); backdrop-filter:blur(22px); -webkit-backdrop-filter:blur(22px); border:1px solid rgba(255,255,255,0.24); border-radius:22px; padding:clamp(26px, 4vw, 38px) clamp(22px, 4vw, 34px); color:#fff; display:flex; flex-direction:column; gap:16px; animation:pageIn 500ms cubic-bezier(0.22,0.72,0.18,1) both; box-shadow:0 28px 70px rgba(0,14,34,.32);"> 3402 <img class="rosgan-auth-logo" src="assets/rosgan-logo-blue.png" alt="Rosgan" style="filter:brightness(0) invert(1); margin:0 auto 2px;"> 3403 <div style="text-align:center;"> 3404 <div id="phone-verification-title" style="font-size:24px; font-weight:850; letter-spacing:-0.02em;">Ingresá tu teléfono</div> 3405 <div style="font-size:13px; line-height:1.55; color:#c8d8ea; margin-top:6px;">Guardaremos este número como teléfono de contacto para tu cuenta.</div> 3406 </div> 3407 <sc-if value="{{ isPhoneEntryStep }}" hint-placeholder-val="{{ true }}"> 3408 <label style="display:grid; gap:7px; font-size:12px; font-weight:800; color:#eaf3fc;"> 3409 Número móvil en formato internacional 3410 <input value="" data-dc-attr-value="{{ phoneVerificationNumber }}" onInput="{{ onPhoneVerificationNumber }}" type="tel" inputmode="tel" placeholder="+54 9 341 000 0000" autocomplete="tel" style="background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.24); border-radius:12px; padding:13px 14px; font-size:15px; color:#fff; font-family:inherit; outline:none; box-sizing:border-box; width:100%;" style-focus="border-color:rgba(255,255,255,0.72);"> 3411 </label> 3412 <button onClick="{{ savePhoneAndContinue }}" disabled="{{ phoneVerificationBusy }}" style="background:#fff; color:#002e5f; border:none; padding:14px; border-radius:12px; font-weight:800; font-size:15px; font-family:inherit; cursor:pointer; opacity:{{ phoneVerificationOpacity }};">{{ phoneVerificationSendLabel }}</button> 3413 </sc-if> 3414 <sc-if value="{{ phoneVerificationMessage }}" hint-placeholder-val="{{ false }}"> 3415 <div role="alert" style="font-size:13px; line-height:1.45; text-align:center; padding:10px 12px; border-radius:10px; background:rgba(230,57,70,0.24); border:1px solid rgba(255,121,131,0.42); color:#ffe1e4;">{{ phoneVerificationMessage }}</div> 3416 </sc-if> 3417 <button onClick="{{ logoutFromPhoneVerification }}" disabled="{{ phoneVerificationBusy }}" style="align-self:center; background:none; border:0; color:#c8d8ea; font:700 12px inherit; cursor:pointer; text-decoration:underline; text-underline-offset:3px;">Salir y usar otra cuenta</button> 3418 </section> 3419 </div> 3420</sc-if> 3421 3422<!-- ===================== APP SHELL ===================== --> 3423<sc-if value="{{ isApp }}" hint-placeholder-val="{{ false }}"> 3424 <div style="width:100%; max-width:100vw; min-width:0; min-height:100vh; overflow-x:clip; padding-bottom:{{ homePadBottom }};"> 3425 3426 <!-- LIGHT TOPBAR (pool / lot) --> 3427 <sc-if value="{{ showLightBar }}" hint-placeholder-val="{{ false }}"> 3428 <div class="app-light-topbar" style="position:sticky; top:0; z-index:40; background:rgba(255,255,255,0.9); backdrop-filter:blur(16px); border-bottom:1px solid #e3eaf2; display:flex; align-items:center; justify-content:space-between; padding:12px clamp(16px, 4vw, 36px); gap:12px;"> 3429 <img src="assets/rosgan-logo-blue.png" alt="Rosgan" style="height:{{ logoH }}; cursor:pointer;" onClick="{{ goHome }}"> 3430 <sc-if value="{{ desktopNavigation }}" hint-placeholder-val="{{ true }}"> 3431 <nav style="display:flex; gap:4px; background:#eef3f9; border:1px solid #e3eaf2; border-radius:100px; padding:5px; font-size:13.5px;"> 3432 <sc-for list="{{ navItems }}" as="n" hint-placeholder-count="4">
3433 <span onClick="{{ n.go }}" style="padding:8px 18px; border-radius:100px; cursor:pointer; background:{{ n.lightBg }}; color:{{ n.lightColor }}; font-weight:{{ n.weight }}; transition:background 200ms;" style-hover="background:#dde7f2;">{{ n.label }}</span> 3434 </sc-for> 3435 </nav> 3436 </sc-if> 3437 <div style="display:flex; align-items:center; gap:12px;"> 3438 <button onClick="{{ toggleNotifications }}" title="{{ notificationBellTitle }}" aria-label="Notificaciones" style="position:relative; width:38px; height:38px; border-radius:50%; border:1px solid #d9e3ee; background:{{ notificationBellBg }}; display:grid; place-items:center; cursor:pointer; padding:0; font-family:inherit;" style-hover="background:#e6eef7;"> 3439 <img src="assets/icons/lucide/bell.svg" alt="" style="width:18px; height:18px;"> 3440 <sc-if value="{{ hasUnreadNotifications }}" hint-placeholder-val="{{ false }}"> 3441 <span style="position:absolute; top:-4px; right:-4px; min-width:18px; height:18px; padding:0 4px; box-sizing:border-box; border-radius:9px; background:#e63946; color:#fff; border:2px solid #fff; display:grid; place-items:center; font-size:9px; line-height:1; font-weight:800;">{{ notificationsUnreadLabel }}</span> 3442 </sc-if> 3443 </button> 3444 <sc-if value="{{ showDesktopLoad }}" hint-placeholder-val="{{ false }}"> 3445 <button onClick="{{ goLotDestination }}" style="display:inline-flex; align-items:center; gap:8px; background:#e63946; color:#fff; border:none; border-radius:100px; padding:10px 20px; font-size:13.5px; font-weight:800; font-family:inherit; cursor:pointer; letter-spacing:0.01em; box-shadow:0 8px 22px rgba(230,57,70,0.36); transition:transform 160ms, box-shadow 160ms;" style-hover="transform:translateY(-2px); box-shadow:0 12px 28px rgba(230,57,70,0.46);" style-active="transform:scale(0.97);"><span style="font-size:16px; line-height:1;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></span>Cargar lote</button> 3446 </sc-if> 3447 <span onClick="{{ goProfile }}" title="Mi perfil" style="width:36px; height:36px; border-radius:50%; background:{{ userAvatarBg }}; border:1px solid {{ userAvatarBorder }}; color:#fff; display:grid; place-items:center; font-size:13px; font-weight:700; cursor:pointer; overflow:hidden;" style-hover="transform:translateY(-1px);"> 3448 <sc-if value="{{ userHasConsignatariaAvatar }}" hint-placeholder-val="false"><img data-profile-avatar="true" data-dc-attr-src="{{ userConsignatariaAvatar }}" alt="{{ userConsignatariaName }}" title="Ir a mi perfil" style="width:{{ userAvatarNavSize }}; height:{{ userAvatarNavSize }}; object-fit:{{ userAvatarObjectFit }}; object-position:center; cursor:pointer;"></sc-if> 3449 <sc-if value="{{ userShowsInitials }}" hint-placeholder-val="true">{{ userInitials }}</sc-if> 3450 </span> 3451 </div> 3452 </div> 3453 </sc-if> 3454 3455 <sc-if data-public-blocked-overlay value="{{ notificationsOpen }}" hint-placeholder-val="{{ false }}"> 3456 <section style="position:fixed; z-index:70; top:68px; right:clamp(12px, 3vw, 36px); width:min(390px, calc(100vw - 24px)); max-height:min(620px, calc(100vh - 88px)); overflow:hidden; background:#fff; border:1px solid #dbe5ef; border-radius:8px; box-shadow:0 22px 60px rgba(0,46,95,0.22); display:flex; flex-direction:column;"> 3457 <header style="padding:16px 16px 13px; display:flex; align-items:center; justify-content:space-between; gap:12px; border-bottom:1px solid #e7edf4;"> 3458 <div> 3459 <div style="font-size:16px; color:#002e5f; font-weight:800;">Notificaciones</div> 3460 <div style="font-size:11.5px; color:#718197; margin-top:2px;">Actividad reciente de tu cuenta</div> 3461 </div> 3462 <div style="display:flex; align-items:center; gap:10px;"> 3463 <sc-if value="{{ hasUnreadNotifications }}" hint-placeholder-val="{{ false }}"> 3464 <button onClick="{{ markAllNotificationsRead }}" style="border:none; background:transparent; color:#0a5ca8; font-size:11px; font-weight:800; font-family:inherit; cursor:pointer; padding:5px;">Marcar leÃdas</button> 3465 </sc-if> 3466 <button onClick="{{ toggleNotifications }}" aria-label="Cerrar notificaciones" title="Cerrar" style="width:30px; height:30px; border:1px solid #dbe5ef; background:#f5f8fb; border-radius:50%; color:#334b67; display:grid; place-items:center; cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="" style="width:16px;height:16px;"></button> 3467 </div> 3468 </header> 3469 <div style="overflow-y:auto; overscroll-behavior:contain;"> 3470 <sc-if value="{{ notificationsLoading }}" hint-placeholder-val="{{ false }}"> 3471 <div style="padding:34px 18px; color:#718197; text-align:center; font-size:13px;">Cargando notificaciones...</div> 3472 </sc-if>
3473 <sc-if value="{{ notificationsHasError }}" hint-placeholder-val="{{ false }}"> 3474 <div style="margin:14px; padding:12px; border-radius:7px; background:#fff0f1; color:#a42632; font-size:12.5px; font-weight:700;">{{ notificationsError }}</div> 3475 </sc-if> 3476 <sc-if value="{{ notificationsEmpty }}" hint-placeholder-val="{{ false }}"> 3477 <div style="padding:42px 22px; text-align:center;"> 3478 <img src="assets/icons/lucide/bell-ring.svg" alt="" style="width:28px; height:28px; opacity:0.42;"> 3479 <div style="font-size:14px; color:#002e5f; font-weight:800; margin-top:10px;">TodavÃa no hay notificaciones</div> 3480 <div style="font-size:12px; color:#718197; margin-top:4px;">Los avisos de Roslot aparecerán acá.</div> 3481 </div> 3482 </sc-if> 3483 <sc-for list="{{ notificationItems }}" as="notification" hint-placeholder-count="3"> 3484 <div onClick="{{ notification.open }}" style="display:grid; grid-template-columns:36px minmax(0,1fr) 8px; gap:11px; align-items:start; padding:14px 16px; border-bottom:1px solid #edf1f5; background:{{ notification.bg }}; cursor:pointer;" style-hover="background:#f2f7fc;"> 3485 <span style="width:36px; height:36px; border-radius:{{ notification.iconRadius }}; display:grid; place-items:center; overflow:hidden; background:{{ notification.iconBg }};"> 3486 <img data-dc-attr-src="{{ notification.icon }}" alt="{{ notification.iconLabel }}" style="width:{{ notification.iconSize }}; height:{{ notification.iconSize }}; object-fit:contain;"> 3487 </span> 3488 <span style="min-width:0;"> 3489 <strong style="display:block; font-size:13px; line-height:1.3; color:#002e5f;">{{ notification.title }}</strong> 3490 <span style="display:block; font-size:12px; line-height:1.4; color:#596c83; margin-top:3px;">{{ notification.body }}</span> 3491 <span style="display:block; font-size:10.5px; color:#8a98aa; margin-top:7px;">{{ notification.time }}</span> 3492 </span> 3493 <span style="width:7px; height:7px; border-radius:50%; background:{{ notification.dotBg }}; margin-top:6px;"></span> 3494 </div> 3495 </sc-for> 3496 </div> 3497 <footer style="padding:11px 14px; border-top:1px solid #e7edf4; background:#f8fafc;"> 3498 <button onClick="{{ goNotificationCenter }}" style="width:100%; border:1px solid #ccd9e7; background:#fff; color:#002e5f; border-radius:7px; padding:10px 12px; font-size:12.5px; font-weight:800; font-family:inherit; cursor:pointer;">Ir al centro de notificaciones</button> 3499 </footer> 3500 </section> 3501 </sc-if> 3502 3503 <sc-if value="{{ isNotifications }}" hint-placeholder-val="{{ false }}"> 3504 <main style="max-width:980px; margin:0 auto; padding:clamp(24px, 4vw, 46px) clamp(14px, 3vw, 26px) 90px; animation:pageIn 320ms ease;"> 3505 <div style="display:flex; align-items:flex-end; justify-content:space-between; gap:18px; margin-bottom:22px; flex-wrap:wrap;"> 3506 <div> 3507 <div style="font-size:10px; letter-spacing:0.16em; color:#0a5ca8; font-weight:800; text-transform:uppercase;">Mi actividad</div> 3508 <h1 style="font-size:clamp(26px, 4vw, 38px); line-height:1.05; color:#002e5f; margin:7px 0 6px;">Centro de notificaciones</h1> 3509 <p style="margin:0; color:#65758a; font-size:13.5px;">Avisos nuevos y actividad anterior de tu cuenta.</p> 3510 </div> 3511 <sc-if value="{{ hasUnreadNotifications }}" hint-placeholder-val="{{ false }}"> 3512 <button onClick="{{ markAllNotificationsRead }}" style="border:1px solid #cbd9e7; background:#fff; color:#0a5ca8; border-radius:7px; padding:10px 14px; font-size:12px; font-weight:800; font-family:inherit; cursor:pointer;">Marcar todas como leÃdas</button> 3513 </sc-if> 3514 </div> 3515 3516 <section style="background:#fff; border:1px solid #dbe5ef; border-radius:8px; overflow:hidden; box-shadow:0 14px 34px rgba(0,46,95,0.08);"> 3517 <sc-if value="{{ notificationHistoryLoading }}" hint-placeholder-val="{{ false }}"> 3518 <div style="padding:48px 20px; text-align:center; color:#718197; font-size:13px;">Cargando historial...</div> 3519 </sc-if> 3520 <sc-if value="{{ notificationHistoryHasError }}" hint-placeholder-val="{{ false }}"> 3521 <div style="margin:16px; padding:13px; border-radius:7px; background:#fff0f1; color:#a42632; font-size:13px; font-weight:700;">{{ notificationHistoryError }}</div> 3522 </sc-if> 3523 <sc-if value="{{ notificationHistoryEmpty }}" hint-placeholder-val="{{ false }}"> 3524 <div style="padding:58px 22px; text-align:center;"> 3525 <img src="assets/icons/lucide/bell-ring.svg" alt="" style="width:34px; height:34px; opacity:0.4;"> 3526 <div style="font-size:16px; color:#002e5f; font-weight:800; margin-top:12px;">No hay actividad para mostrar</div> 3527 <div style="font-size:12.5px; color:#718197; margin-top:4px;">Cuando ocurra algo importante en Roslot aparecerá acá.</div> 3528 </div> 3529 </sc-if> 3530 <sc-for list="{{ notificationHistoryItems }}" as="notification" hint-placeholder-count="5"> 3531 <div onClick="{{ notification.open }}" style="display:grid; grid-template-columns:42px minmax(0,1fr) auto; gap:13px; align-items:center; padding:16px clamp(14px, 2.5vw, 22px); border-bottom:1px solid #edf1f5; background:{{ notification.bg }}; cursor:pointer;" style-hover="background:#f3f7fb;">
3532 <span style="width:42px; height:42px; border-radius:{{ notification.iconRadius }}; display:grid; place-items:center; overflow:hidden; background:{{ notification.iconBg }};"> 3533 <img data-dc-attr-src="{{ notification.icon }}" alt="{{ notification.iconLabel }}" style="width:{{ notification.historyIconSize }}; height:{{ notification.historyIconSize }}; object-fit:contain;"> 3534 </span> 3535 <span style="min-width:0;"> 3536 <strong style="display:block; color:#002e5f; font-size:14px; line-height:1.3;">{{ notification.title }}</strong> 3537 <span style="display:block; color:#596c83; font-size:12.5px; line-height:1.4; margin-top:3px;">{{ notification.body }}</span> 3538 <span style="display:block; color:#8a98aa; font-size:10.5px; margin-top:7px;">{{ notification.time }}</span> 3539 </span> 3540 <span style="background:{{ notification.statusBg }}; color:{{ notification.statusColor }}; border-radius:100px; padding:5px 9px; font-size:10px; font-weight:800;">{{ notification.status }}</span> 3541 </div> 3542 </sc-for> 3543 </section> 3544 </main> 3545 </sc-if> 3546 3547 <style> 3548 .home-agenda-flyer{scroll-snap-align:start;background:#fff;border:1px solid #dce5ed;border-radius:20px;overflow:hidden;color:#112d47;display:flex;flex-direction:column;box-shadow:0 8px 24px #002e5f0c;transition:transform .2s,box-shadow .2s} 3549 .home-agenda-flyer:hover{transform:translateY(-3px);box-shadow:0 14px 30px #002e5f20;color:#112d47} 3550 .home-agenda-flyer:focus-visible{outline:3px solid #0a5ca8;outline-offset:3px} 3551 .home-agenda-flyer .home-month-card-media{height:156px!important;background:#082842;flex-shrink:0} 3552 .home-agenda-flyer [data-drag-img]{opacity:.8!important;filter:none!important} 3553 .home-agenda-flyer [data-drag-img]+div{background:linear-gradient(180deg,#00172a44,#00172add)!important} 3554 .home-agenda-flyer .home-month-card-logos{top:auto!important;bottom:18px;left:20px!important;transform:none!important;justify-content:flex-start!important;width:80%!important;height:44px!important} 3555 .home-flyer-body{padding:20px;display:flex;flex-direction:column;gap:12px;flex:1} 3556 .home-flyer-schedule{display:flex;gap:12px;align-items:center} 3557 .home-flyer-date{display:flex;flex-direction:column;align-items:center;justify-content:center;min-width:58px;min-height:64px;border-radius:12px;background:#edf4f9;color:#003866;line-height:1.1} 3558 .home-flyer-date strong{font-size:27px;font-weight:850}.home-flyer-date>span{text-transform:uppercase;font-size:11px;font-weight:800;margin-top:3px} 3559 .home-flyer-schedule>span:last-child{display:flex;flex-direction:column;gap:5px;font-size:14px}.home-flyer-schedule>span:last-child>span{color:#64758a;font-size:12px;line-height:1.4} 3560 .home-flyer-title{font-size:19px;font-weight:800;line-height:1.25;letter-spacing:-.025em;overflow-wrap:anywhere} 3561 .home-flyer-cons{font-size:12px;line-height:1.4;color:#64758a} 3562 .home-flyer-cta{margin-top:auto;padding-top:16px;border-top:1px solid #e8edf2;display:flex;justify-content:space-between;align-items:center;font-weight:750;font-size:13px;color:#07569b}.home-flyer-cta img{width:18px;height:18px} 3563 .home-hero{height:auto!important;min-height:clamp(620px,55vw,700px)!important;box-sizing:border-box} 3564 .home-hero-content{height:auto;min-height:min-content;flex-shrink:0!important} 3565 .home-hero-main{flex-shrink:0} 3566 @media(max-width:700px){ 3567 body:has(.home-hero) .app-mobile-load-fab{display:none!important} 3568 .home-hero{aspect-ratio:auto!important;height:auto!important;min-height:clamp(680px,125vw,740px)!important;padding-bottom:0!important} 3569 .home-hero [data-hero-img]{inset:62px 0 auto!important;height:clamp(200px,68vw,330px)!important;transform:none!important;will-change:auto!important} 3570 .home-hero [data-hero-img]>div{inset:0!important;background-size:contain!important;background-position:center!important;background-repeat:no-repeat!important;animation:heroImageReveal 900ms ease both!important} 3571 .home-hero-content{height:auto!important;overflow:visible!important} 3572 .home-hero-main{margin-top:calc(clamp(200px,68vw,330px) + 8px)!important;flex:0 0 auto!important;justify-content:flex-start!important;padding:24px 18px 20px!important;gap:0!important;overflow:visible!important} 3573 .home-hero-brand{width:190px!important;height:42px!important;margin-bottom:18px!important} 3574 .home-hero-badge{font-size:10px!important;padding:6px 10px!important} 3575 .home-hero-title{display:block!important;max-width:100%!important;font-size:clamp(26px,7vw,34px)!important;line-height:1.12!important;margin:14px 0 12px!important;overflow:visible!important} 3576 .home-hero-subtitle{display:block!important;font-size:13px!important;white-space:normal!important;line-height:1.5!important;min-height:0!important} 3577 .home-hero-actions{margin-top:20px!important;gap:10px!important}.home-hero-actions>a,.home-hero-actions>span{font-size:13px!important;padding:12px 16px!important;border-radius:10px!important;min-height:44px;box-sizing:border-box} 3578 .home-hero-indices{display:none!important} 3579 } 3580 </style> 3581 <style> 3582 .home-hero-main{touch-action:pan-y;user-select:none;cursor:grab}.home-hero-main:active{cursor:grabbing} 3583 .home-hero-copy{position:relative} 3584 .home-flyer-brand-name{font-size:16px;line-height:1.2;font-weight:800;color:#fff;text-shadow:0 2px 8px #00172a;overflow-wrap:anywhere} 3585 .home-agenda-flyer .home-month-card-logos{width:calc(100% - 40px)!important;height:auto!important;min-height:48px} 3586 </style> 3587 <style> 3588 .home-report-track{display:grid;grid-auto-flow:column;grid-auto-columns:clamp(280px,29vw,350px);
3588gap:18px;overflow-x:auto;padding:4px 2px 18px;scroll-snap-type:x mandatory;scrollbar-width:thin;scrollbar-color:#c6d6e3 transparent} 3589 .home-report-card{scroll-snap-align:start;display:flex;flex-direction:column;align-items:flex-start;gap:18px;min-height:300px;box-sizing:border-box;padding:24px;background:#fff;border:1px solid #dfe7ed;border-radius:18px;color:#102f49;box-shadow:0 4px 16px #17395406;transition:transform .2s,border-color .2s,box-shadow .2s} 3590 .home-report-card:hover{transform:translateY(-3px);border-color:#83b6c3;box-shadow:0 12px 28px #17395412;color:#102f49} 3591 .home-report-card:focus-visible{outline:3px solid #0a5ca8;outline-offset:2px} 3592 .home-report-meta{display:flex;align-items:center;gap:12px;font-size:12px;color:#667d90;font-weight:600;width:100%} 3593 .home-report-icon{display:grid;place-items:center;width:38px;height:42px;border-radius:9px;background:#eff5f8;flex:none}.home-report-icon img{width:20px;height:20px;opacity:.65} 3594 .home-report-latest{color:#137266;background:#edf8f4;padding:5px 9px;border-radius:6px;font-size:10px;letter-spacing:.05em;font-weight:750;text-transform:uppercase} 3595 .home-report-card h3{font-size:20px;line-height:1.35;letter-spacing:-.025em;font-weight:750;margin:0 0 8px;overflow-wrap:anywhere;text-wrap:pretty} 3596 .home-report-footer{margin-top:auto;border-top:1px solid #e8eef2;padding-top:16px;display:flex;align-items:center;justify-content:space-between;gap:16px;width:100%;font-size:13px;font-weight:750;color:#0a5ca8} 3597 .home-report-arrow{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;background:#eef5f9}.home-report-arrow img{width:16px;height:16px} 3598 @media(max-width:700px){.home-report-track{grid-auto-columns:min(84vw,330px);gap:14px}.home-report-card{padding:22px;min-height:290px}.home-report-card h3{font-size:19px}} 3599 @media(prefers-reduced-motion:reduce){.home-report-card{transition:none}} 3600 </style> 3601 <style> 3602.remate-past-toggle{display:flex;align-items:center;gap:14px;width:fit-content;max-width:100%;min-height:60px;margin:28px auto 22px;padding:10px 20px 10px 10px;border:1px solid #d3e3ee;border-radius:16px;background:#fff;color:#063963;font-family:inherit;font-size:15px;font-weight:750;cursor:pointer;box-shadow:0 6px 20px #00345d0c;transition:background .2s,box-shadow .2s,transform .2s} 3603.remate-past-toggle:hover{background:#f0f7fb;box-shadow:0 10px 24px #00345d18;transform:translateY(-2px)} 3604.remate-past-toggle:focus-visible{outline:3px solid #0a5ca8;outline-offset:3px} 3605.remate-past-icon{display:grid;place-items:center;width:40px;height:40px;border-radius:11px;background:#e9f3fa}.remate-past-icon img{width:20px;height:20px} 3606.remate-past-chevron{width:18px;height:18px;margin-left:12px;transition:transform .2s}.remate-past-toggle[aria-expanded="true"] .remate-past-chevron{transform:rotate(180deg)} 3607@media(max-width:600px){.remate-past-toggle{width:100%;justify-content:flex-start}.remate-past-chevron{margin-left:auto}} 3608 </style> 3609 <link rel="stylesheet" href="assets/congreso-2026/banner.css"> 3610 <link rel="stylesheet" href="assets/home-hero-mobile.css"> 3611 <!-- ============ HOME ============ --> 3612 <sc-if value="{{ isHome }}" hint-placeholder-val="{{ false }}"> 3613 <div style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1);"> 3614 <div class="home-hero {{ heroCongressClass }}" style="position:relative;overflow:hidden;display:flex;flex-direction:column;background:#002e5f;padding-bottom:{{ heroPadBottom }};"> 3615 <sc-if value="{{ heroIsAuction }}"> <sc-if value="{{ homeHeroReady }}"> 3616 <div data-hero-img style="position:absolute; inset:-2%; transform:translateY({{ parallaxY }}); will-change:transform;"> 3617 <img class="home-hero-photo" key="{{ heroTextKey }}" data-dc-attr-src="{{ heroImage }}" alt="" fetchpriority="high" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:{{ heroImagePosition }};--home-hero-mobile-position:{{ heroImageMobilePosition }};--home-hero-mobile-height:{{ heroImageMobileHeight }};animation:heroImageReveal 900ms ease both,kenburns 18s ease-in-out infinite alternate;"> 3618 </div> 3619 <div class="home-hero-overlay" style="position:absolute; inset:0; background:{{ heroOverlay }};"></div> 3620 </sc-if> 3621 <div style="position:absolute; inset:0; background:rgba(0,0,0,.07); pointer-events:none;"></div> 3622</sc-if> 3623 <div class="home-hero-content" style="position:relative; flex:1; display:flex; flex-direction:column;"> 3624 <div class="home-hero-nav" style="display:flex; align-items:center; justify-content:space-between; padding:18px clamp(16px, 4vw, 36px); gap:12px;"> 3625 <img src="assets/rosgan-logo-blue.png" alt="Rosgan" style="height:{{ logoH }}
3625; filter:brightness(0) invert(1);"> 3626 <sc-if value="{{ desktopNavigation }}" hint-placeholder-val="{{ true }}"> 3627 <nav style="display:flex; gap:4px; background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.16); border-radius:100px; padding:5px; font-size:13.5px; color:#c8d8ea;"> 3628 <sc-for list="{{ navItems }}" as="n" hint-placeholder-count="4"> 3629 <span onClick="{{ n.go }}" style="padding:8px 18px; border-radius:100px; cursor:pointer; background:{{ n.bg }}; color:{{ n.color }}; font-weight:{{ n.weight }}; transition:background 200ms;" style-hover="background:rgba(255,255,255,0.18);">{{ n.label }}</span> 3630 </sc-for> 3631 </nav> 3632 </sc-if> 3633 <div style="display:flex; align-items:center; gap:12px;"> 3634 <button onClick="{{ toggleNotifications }}" title="{{ notificationBellTitle }}" aria-label="Notificaciones" style="position:relative; width:38px; height:38px; border-radius:50%; border:1px solid rgba(255,255,255,0.3); background:rgba(255,255,255,0.14); display:grid; place-items:center; cursor:pointer; padding:0;" style-hover="background:rgba(255,255,255,0.24);"> 3635 <img src="assets/icons/lucide/bell.svg" alt="" style="width:18px; height:18px; filter:brightness(0) invert(1);"> 3636 <sc-if value="{{ hasUnreadNotifications }}" hint-placeholder-val="{{ false }}"> 3637 <span style="position:absolute; top:-4px; right:-4px; min-width:18px; height:18px; padding:0 4px; box-sizing:border-box; border-radius:9px; background:#e63946; color:#fff; border:2px solid #174f88; display:grid; place-items:center; font-size:9px; line-height:1; font-weight:800;">{{ notificationsUnreadLabel }}</span> 3638 </sc-if> 3639 </button> 3640 <sc-if value="{{ showDesktopLoad }}" hint-placeholder-val="{{ false }}"> 3641 <button onClick="{{ goLotDestination }}" style="display:inline-flex; align-items:center; gap:8px; background:#e63946; color:#fff; border:none; border-radius:100px; padding:10px 20px; font-size:13.5px; font-weight:800; font-family:inherit; cursor:pointer; letter-spacing:0.01em; box-shadow:0 8px 22px rgba(230,57,70,0.45); transition:transform 160ms, box-shadow 160ms;" style-hover="transform:translateY(-2px); box-shadow:0 12px 30px rgba(230,57,70,0.6);" style-active="transform:scale(0.97);"><span style="font-size:16px; line-height:1;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></span>Cargar lote</button> 3642 </sc-if> 3643 <span onClick="{{ goProfile }}" title="Mi perfil" style="width:36px; height:36px; border-radius:50%; background:{{ userAvatarBg }}; border:1px solid {{ userAvatarBorder }}; color:#fff; display:grid; place-items:center; font-size:13px; font-weight:700; cursor:pointer; overflow:hidden;" style-hover="transform:translateY(-1px);"> 3644 <sc-if value="{{ userHasConsignatariaAvatar }}" hint-placeholder-val="false"><img data-profile-avatar="true" data-dc-attr-src="{{ userConsignatariaAvatar }}" alt="{{ userConsignatariaName }}" title="Ir a mi perfil" style="width:{{ userAvatarNavSize }}; height:{{ userAvatarNavSize }}; object-fit:{{ userAvatarObjectFit }}; object-position:center; cursor:pointer;"></sc-if> 3645 <sc-if value="{{ userShowsInitials }}" hint-placeholder-val="true">{{ userInitials }}</sc-if> 3646 </span> 3647 </div> 3648 </div> 3649 <sc-if value="{{ heroIsAuction }}"> <div class="home-hero-main" role="region" aria-label="Remates destacados" tabindex="0" onPointerDown="{{ heroPointerDown }}" onPointerUp="{{ heroPointerUp }}" onPointerCancel="{{ heroPointerCancel }}" onKeyDown="{{ heroKeyDown }}" style="padding:clamp(28px, 5vw, 52px) clamp(16px, 4vw, 36px) 30px; color:#fff; display:flex; justify-content:space-between; align-items:flex-end; gap:40px; flex-wrap:wrap; margin-top:auto;"> 3650 <div class="home-hero-copy" style="width:min(860px, 100%); max-width:860px; flex:1 1 680px; min-width:0;"> 3651 <sc-if value="{{ homeHeroReady }}"> 3652 <div class="home-hero-slide-copy" key="{{ heroTextKey }}
3652" style="animation:heroTextFade 1200ms ease both;"> 3653 <sc-if value="{{ heroHasConsLogo }}" hint-placeholder-val="false"> 3654 <div class="home-hero-brand" style="width:min(290px,72vw); height:clamp(48px,7vw,72px); margin-bottom:18px; display:flex; align-items:center; justify-content:flex-start; filter:drop-shadow(0 4px 18px rgba(0,0,0,.34));"> 3655 <img src="{{ heroConsLogo }}" alt="{{ heroConsName }}" style="display:block; width:auto; max-width:100%; max-height:100%; object-fit:contain; object-position:left center; filter:{{ heroConsLogoFilter }};"> 3656 </div> 3657 </sc-if> 3658 <span class="home-hero-badge" style="display:inline-flex; align-items:center; gap:8px; background:{{ heroBadgeBg }}; font-size:12px; font-weight:700; padding:6px 14px; border-radius:100px; letter-spacing:0.08em;"><span class="home-hero-status-dot" aria-hidden="true" style="width:7px; height:7px; border-radius:50%; background:#fff; animation:pulseDot 1.4s infinite;"></span><span class="home-hero-status-text">{{ heroBadge }}</span></span> 3659 <sc-if value="{{ heroPrebidsActive }}" hint-placeholder-val="false"><button type="button" class="prebid-active-badge is-hero" onClick="{{ heroOpenLots }}"><span class="prebid-active-dot" aria-hidden="true"></span>Preofertas activas<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"/></svg></button></sc-if> 3660 <h1 class="home-hero-title" style="font-size:clamp(30px, 5vw, 50px); font-weight:800; letter-spacing:-0.025em; line-height:1.05; margin:16px 0 10px; text-wrap:balance; max-width:100%; overflow-wrap:anywhere;">{{ heroTitle }}</h1> 3661 <p class="home-hero-subtitle" style="margin:0; font-size:clamp(14px, 1.6vw, 16px); color:#c8d8ea; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; min-height:3em;">{{ heroSubtitle }}</p> 3662 <sc-if value="{{ heroPrebidsUpcoming }}" hint-placeholder-val="false"><section class="home-prebid-countdown"><span class="home-prebid-countdown-label">Las preofertas comienzan</span><strong class="home-prebid-countdown-date">{{ heroPrebidStartLabel }}</strong><prebid-start-countdown start="{{ heroPrebidStart }}"></prebid-start-countdown></section></sc-if> 3663 <div class="home-hero-actions" style="display:flex; gap:12px; margin-top:22px; flex-wrap:wrap;"> 3664 <a href="{{ heroHref }}" target="_blank" rel="noopener" style="background:#fff; color:#002e5f; padding:13px 26px; border-radius:12px; font-weight:700; font-size:15px; transition:transform 160ms;" style-hover="transform:translateY(-2px); color:#002e5f;">Ver transmisión</a> 3665 <span onClick="{{ heroOpenLots }}" style="background:rgba(255,255,255,0.12); border:1px solid rgba(255,255,255,0.25); padding:13px 26px; border-radius:12px; font-size:15px; cursor:pointer;" style-hover="background:rgba(255,255,255,0.22);">Ver Lotes</span> 3666 </div> 3667 </div> 3668 </sc-if> 3669 <sc-if value="{{ heroHasSlides }}" hint-placeholder-val="{{ false }}"> 3670 <div class="home-hero-dots" style="display:flex; gap:8px; margin-top:24px; align-items:center;"> 3671 <sc-for list="{{ heroDots }}" as="d" hint-placeholder-count="5"> 3672 <span onClick="{{ d.go }}" style="height:6px; width:{{ d.w }}; border-radius:100px; background:{{ d.bg }}; cursor:pointer; transition:width 300ms ease;"></span> 3673 </sc-for> 3674 </div> 3675 </sc-if> 3676 </div> 3677 <div class="home-hero-indices" style="display:flex; gap:12px; flex-wrap:wrap;"> 3678 <sc-if value="{{ homeIndicesReady }}"> 3679 <sc-for list="{{ heroIndices }}" as="s" hint-placeholder-count="3"> 3680 <div class="home-hero-index-card" style="position:relative; background:rgba(255,255,255,0.1); backdrop-filter:blur(12px); border:1px solid rgba(255,255,255,0.2); border-radius:16px; padding:14px 18px; min-width:132px; overflow:hidden;"> 3681 <div style="display:flex; align-items:center; justify-content:space-between; gap:10px;">
3682 <span style="font-size:10.5px; color:#c8d8ea; letter-spacing:0.06em; font-weight:700; text-transform:uppercase;">{{ s.label }}</span> 3683 <span style="display:inline-flex; align-items:center; gap:2px; font-size:11.5px; font-weight:800; color:{{ s.color }}; background:rgba(255,255,255,0.08); padding:2px 7px 2px 4px; border-radius:100px;"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="{{ s.color }}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M0 0" data-dc-svg-d="{{ s.arrow }}"></path></svg>{{ s.pctFmt }}</span> 3684 </div> 3685 <div class="home-hero-index-value" style="font-size:clamp(20px, 2.6vw, 27px); font-weight:800; font-variant-numeric:tabular-nums; margin-top:6px;">{{ s.rollEl }}<span class="home-hero-index-unit" style="font-size:12px; font-weight:600; color:#9db4cc;">{{ s.unit }}</span></div> 3686 <div class="home-hero-index-month" style="font-size:10px; color:#8fa6bf; margin-top:1px;">vs. anterior · {{ s.mes }}</div> 3687 </div> 3688 </sc-for> 3689 </sc-if> 3690 <sc-if value="{{ homeIndicesEmpty }}"> 3691 <div style="background:rgba(255,255,255,.09); border:1px solid rgba(255,255,255,.16); border-radius:16px; padding:15px 18px; color:#c8d8ea; font-size:12px; min-width:210px;">Ãndices temporalmente no disponibles</div> 3692 </sc-if> 3693 </div> 3694 </div> 3695</sc-if> 3696 <sc-if value="{{ heroIsCongress }}"><section class="home-congress-banner" aria-label="5° Congreso Federal Ganadero" tabindex="0" onPointerDown="{{ heroPointerDown }}" onPointerUp="{{ heroPointerUp }}" onPointerCancel="{{ heroPointerCancel }}" onKeyDown="{{ heroKeyDown }}"> 3697 <div class="home-congress-copy"> 3698 <img class="home-congress-logo" src="assets/congreso-2026/logo.webp" width="630" height="96" alt="Rosgan · 5° Congreso Federal Ganadero"> 3699 <h1>Quo vadis,<br><span>ganaderÃa</span></h1> 3700 <p class="home-congress-lead">Una jornada para definir el futuro de la ganaderÃa argentina.</p> 3701 <p class="home-congress-date"><strong>12 NOVIEMBRE 2026</strong><span>Desde las 8:30 h · Bolsa de Comercio de Rosario</span></p> 3702 <div class="home-congress-actions"><a class="home-congress-register" href="https://inscripcion.ar/CFG2026/" target="_blank" rel="noopener">Inscribite aquà <img src="assets/icons/lucide/arrow-up-right.svg" alt="" aria-hidden="true" width="16" height="16"></a><a class="home-congress-program" href="https://congresofederalganadero.com.ar/" target="_blank" rel="noopener">Conocé el Congreso <img src="assets/icons/lucide/arrow-up-right.svg" alt="" aria-hidden="true" width="16" height="16"></a></div> 3703 <small class="home-congress-note">Inscripción gratuita · Cupos limitados</small> 3704 <sc-if value="{{ heroHasSlides }}"><div class="home-hero-dots congress-dots"><sc-for list="{{ heroDots }}" as="d"><button type="button" onClick="{{ d.go }}" aria-label="{{ d.label }}" aria-pressed="{{ d.active }}" style="width:{{ d.w }};background:{{ d.bg }};"></button></sc-for></div></sc-if> 3705 </div> 3706 <div class="home-congress-visual"><img src="assets/congreso-2026/hero-bovino.webp" width="720" height="677" fetchpriority="high" alt="Bovino sobre cÃrculo verde, imagen del Congreso Federal Ganadero"></div> 3707 </section></sc-if> 3708 </div> 3709 </div> 3710 <style> 3711 .home-access{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;padding:0 clamp(16px,4vw,36px);position:relative} 3712 .home-access-card{appearance:none;width:100%;min-width:0;display:flex;align-items:center;gap:14px;padding:22px 20px;border:1px solid #e0e8f0;border-radius:18px;background:#fff;color:#112c46;text-align:left;font:inherit;cursor:pointer;box-shadow:0 8px 24px rgba(10,40,70,.065);transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease} 3713 .home-access-icon{width:48px;height:48px;flex:none;display:grid;place-items:center;border-radius:15px;background:#f1f5f9;color:#143d62;border:1px solid #e5ecf3}
3714 .home-access-icon img{display:block;width:40px;height:40px;object-fit:contain} 3715 .home-access-copy{display:flex;flex:1;min-width:0;flex-direction:column;gap:5px} 3716 .home-access-copy strong{font-size:17px;font-weight:750;letter-spacing:-.025em;line-height:1.2} 3717 .home-access-copy small{font-size:12px;line-height:1.45;color:#64788b;text-wrap:balance} 3718 .home-access-arrow{width:17px;height:17px;flex:none;color:#91a2b3;transition:transform .2s ease,color .2s ease} 3719 .home-access-card:focus-visible{outline:3px solid #2079c5;outline-offset:4px} 3720 @media(hover:hover){.home-access-card:hover{transform:translateY(-3px);border-color:var(--access-color);box-shadow:0 12px 28px rgba(10,40,70,.1)}.home-access-card:hover .home-access-arrow{color:var(--access-color);transform:translateX(3px)}} 3721 .home-access-card:active{transform:translateY(0);box-shadow:0 3px 12px rgba(10,40,70,.06)} 3722 @media(max-width:1100px){.home-access{grid-template-columns:repeat(2,minmax(0,1fr))}} 3723 @media(max-width:540px){.home-access{gap:10px}.home-access-card{padding:16px 13px;gap:10px;align-items:flex-start;flex-wrap:wrap}.home-access-icon{width:38px;height:38px;border-radius:12px}.home-access-icon img{width:32px;height:32px}.home-access-copy{flex-basis:calc(100% - 48px)}.home-access-copy strong{font-size:15px}.home-access-copy small{font-size:11px;text-wrap:initial}.home-access-arrow{display:none}} 3724 @media(prefers-reduced-motion:reduce){.home-access-card,.home-access-arrow{transition:none}} 3725 </style> 3726 <div class="home-access" aria-label="Explorar Rosgan" style="margin-top:{{ accessPull }};"> 3727 <sc-for list="{{ accessCards }}" as="c" hint-placeholder-count="4"> 3728 <button type="button" class="home-access-card" onClick="{{ c.go }}" style="--access-color:{{ c.chipBg }};--access-tint:{{ c.tint }};"> 3729 <span class="home-access-icon" aria-hidden="true"><img data-dc-attr-src="{{ c.iconImage }}" alt="" width="40" height="40"></span> 3730 <span class="home-access-copy"><strong>{{ c.title }}</strong><small>{{ c.text }}</small></span>
3731 <svg class="home-access-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"></path></svg> 3732 </button> 3733 </sc-for> 3734 </div> 3735 <section class="home-channel" aria-label="Canal de Rosgan"> 3736 <header><div><small>ROSGAN TV</small><h2>El canal de hoy</h2></div><sc-if value="{{ hcManual }}"><button type="button" onClick="{{ hcAutomatic }}">Volver a programación automática</button></sc-if></header> 3737 <div class="home-channel-layout"> 3738 <div class="home-channel-screen" id="home-channel-player"> 3739 <sc-if value="{{ hcHasYoutube }}"><iframe key="{{ hcPlayerKey }}" src="{{ hcEmbed }}" title="{{ hcTitle }}" loading="eager" allow="autoplay; encrypted-media; picture-in-picture; fullscreen" allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe></sc-if> 3740 <sc-if value="{{ hcIsSpot }}"><video key="{{ hcPlayerKey }}" src="{{ hcSpotUrl }}" data-home-spot-player="true" data-spot-loop="{{ hcSpotLoop }}" autoplay muted playsinline controls preload="auto" onEnded="{{ hcSpotEnded }}" onError="{{ hcSpotError }}" aria-label="{{ hcTitle }}"></video></sc-if> 3741 <sc-if value="{{ hcEmpty }}"><div class="home-channel-empty"><img src="assets/rosgan-logo-blue.png" alt="Rosgan"><h3>{{ hcEmptyTitle }}</h3><p>Cuando haya programación, la transmisión aparecerá acá automáticamente.</p></div></sc-if> 3742 </div> 3743 <aside><div class="home-channel-current"><span class="home-channel-badge {{ hcLiveClass }}">{{ hcBadge }}</span><h3>{{ hcTitle }}</h3><p>{{ hcReason }}</p><sc-if value="{{ hcHasVideo }}"><small>Reproducción automática silenciada. Activá el sonido desde el reproductor.</small><sc-if value="{{ hcHasYoutube }}"><div class="home-channel-links"><a class="home-channel-youtube" href="{{ hcHref }}" target="_blank" rel="noopener" aria-label="Ver en YouTube (abre en otra pestaña)"><svg width="25" height="18" viewBox="0 0 28 20" aria-hidden="true"><rect width="28" height="20" rx="6" fill="#ff0033"/><path d="M11 5.5 19 10l-8 4.5Z" fill="white"/></svg><span>Ver en YouTube</span><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M10 14 21 3M21 14v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h6"/></svg></a></div></sc-if></sc-if></div> 3744 <sc-if value="{{ hcHasToday }}"><div class="home-channel-today home-channel-picker"><div class="home-channel-picker-heading"><strong>Canales de hoy</strong></div><nav class="home-channel-programs" aria-label="Canales de hoy"><sc-for list="{{ hcToday }}" as="program"><button class="home-channel-chip" type="button" onClick="{{ program.select }}" aria-label="{{ program.title }}" title="{{ program.title }}" aria-pressed="{{ program.selected }}"><img src="{{ program.thumb }}" alt=""><span><strong>{{ program.label }}</strong><small>{{ program.time }}</small></span></button></sc-for></nav></div></sc-if> 3745 <sc-if value="{{ hcHasSpots }}"><div class="home-channel-today home-channel-picker"><div class="home-channel-picker-heading"><strong>Spots institucionales</strong></div><nav aria-label="Spots institucionales"><sc-for list="{{ hcSpots }}" as="spot"><button class="home-channel-chip" type="button" onClick="{{ spot.select }}" aria-pressed="{{ spot.selected }}"><img src="assets/rosgan-logo-blue.png" alt="" style="object-fit:contain;background:#fff;padding:8px;box-sizing:border-box;"><span><strong>{{ spot.title }}</strong><small>ⶠVer spot · reproducción continua</small></span></button></sc-for></nav></div></sc-if> 3746 </aside> 3747 </div> 3748 </section> 3749 <sc-if value="{{ homeMonthLoading }}"> 3750 <div aria-label="Cargando próximos remates" style="padding:28px clamp(16px, 4vw, 36px) 4px;"> 3751 <div style="display:grid; gap:7px; margin-bottom:16px;"><span class="home-skeleton" style="width:126px; height:12px; border-radius:6px;"></span><span class="home-skeleton" style="width:270px; max-width:72vw; height:25px; border-radius:8px;"></span></div> 3752 <div style="display:grid; grid-auto-flow:column; grid-auto-columns:clamp(260px,30vw,360px); gap:16px; overflow:hidden; padding-bottom:14px;"> 3753 <sc-for list="{{ homeSectionSkeletons }}" as="sk" hint-placeholder-count="4"><div class="home-loader-card"><div class="home-skeleton" style="aspect-ratio:16/9;"></div><div style="display:flex; justify-content:space-between; padding:12px 15px; gap:18px;"><span class="home-skeleton" style="width:62%; height:13px; border-radius:6px;"></span><span class="home-skeleton" style="width:25%; height:13px; border-radius:6px;"></span></div></div></sc-for> 3754 </div> 3755 </div> 3756 </sc-if> 3757 <sc-if value="{{ hasMonthCarousel }}" hint-placeholder-val="{{ false }}"> 3758 <div style="padding:28px clamp(16px, 4vw, 36px) 4px;"> 3759 <div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:16px;"> 3760 <div><div style="font-size:11.5px; letter-spacing:0.14em; color:#e63946; font-weight:700;">AGENDA DE REMATES</div><h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:700; margin:2px 0 0; color:#112033;">Próximos Remates</h2></div>
3761 <span onClick="{{ goRemates }}" style="display:inline-flex;align-items:center;gap:5px;font-size:13.5px; color:#0a5ca8; font-weight:600; cursor:pointer;">Ver archivo <img src="assets/icons/lucide/arrow-right.svg" alt="" style="width:14px;height:14px;"></span> 3762 </div> 3763 <div data-drag-scroll style="display:grid; grid-auto-flow:column; grid-auto-columns:clamp(260px, 30vw, 360px); gap:16px; overflow-x:auto; padding-bottom:14px; cursor:grab; scrollbar-width:none; -ms-overflow-style:none; scroll-snap-type:x mandatory;"> 3764 <sc-for list="{{ monthCarousel }}" as="m" hint-placeholder-count="4"> 3765 <a class="home-agenda-flyer" onClick="{{ m.select }}" href="{{ m.href }}" target="_blank" rel="noopener" data-drag-item style="--flyer-brand:{{ m.brandColor }};"> 3766 <div class="home-month-card-media" style="position:relative; height:clamp(180px,17vw,220px); border-top:4px solid {{ m.brandColor }}; box-sizing:border-box;"> 3767 <div data-drag-img style="position:absolute; inset:0; background-image:url('{{ m.thumb }}'); background-size:cover; background-position:{{ m.thumbPosition }}; opacity:.42; filter:saturate(.68) contrast(1.06); transition:transform 600ms cubic-bezier(0.22,0.72,0.18,1);"></div> 3768 <div style="position:absolute; inset:0; background:{{ m.thumbOverlay }};"></div> 3769 <span style="position:absolute; top:12px; left:12px; background:{{ m.badgeBg }}; color:{{ m.badgeColor }}; font-size:10px; font-weight:850; padding:5px 10px; border-radius:100px; letter-spacing:.07em; display:inline-flex; align-items:center; gap:6px; box-shadow:0 4px 14px rgba(0,0,0,.14);"><sc-if value="{{ m.live }}" hint-placeholder-val="{{ false }}"><span style="position:relative; width:7px; height:7px;"><span style="position:absolute; inset:0; border-radius:50%; background:#fff;"></span><span style="position:absolute; inset:0; border-radius:50%; background:#fff; animation:livePing 1.6s ease-out infinite;"></span></span></sc-if>{{ m.badge }}</span> 3770 <div class="home-month-card-logos" style="position:absolute; left:50%; top:32%; transform:translate(-50%,-50%); width:min(76%,244px); height:38px; display:flex; align-items:center; justify-content:center; gap:14px; pointer-events:none; filter:drop-shadow(0 4px 14px rgba(0,0,0,.3));"> 3771 <sc-if value="{{ m.hasConsLogo }}" hint-placeholder-val="false"><img src="{{ m.consLogo }}" alt="" style="width:48px; height:48px; flex:none; object-fit:contain; filter:{{ m.consLogoFilter }};"></sc-if> 3772 <strong class="home-flyer-brand-name">{{ m.cons }}</strong> 3773 </div> 3774 </div> 3775 <div class="home-flyer-body"> 3776 <div class="home-flyer-schedule"><span class="home-flyer-date"><strong>{{ m.day }}</strong><span>{{ m.month }}</span></span><span><strong>{{ m.time }}</strong><span>{{ m.location }}</span></span></div> 3777 <strong class="home-flyer-title">{{ m.title }}</strong> 3778 <span class="home-flyer-cta">{{ m.cta }} <img src="assets/icons/lucide/arrow-right.svg" alt=""></span> 3779 </div> 3780 </a> 3781 </sc-for> 3782 </div> 3783 <sc-if value="{{ monthHasDots }}" hint-placeholder-val="{{ false }}"> 3784 <div style="display:flex; gap:8px; align-items:center; justify-content:center; margin-top:12px;"> 3785 <sc-for list="{{ monthDots }}" as="d" hint-placeholder-count="4"> 3786 <span onClick="{{ d.go }}" style="height:6px; width:{{ d.w }}; border-radius:100px; background:{{ d.bg }}; cursor:pointer; transition:all 300ms;"></span> 3787 </sc-for> 3788 </div> 3789 </sc-if> 3790 </div> 3791 </sc-if> 3792 <sc-if value="{{ homeCafeVideosLoading }}"> 3793 <div aria-label="Cargando videos de Café Ganadero" style="padding:22px clamp(16px, 4vw, 36px) 8px;"> 3794 <div style="display:grid; gap:7px; margin-bottom:16px;"><span class="home-skeleton" style="width:110px; height:12px; border-radius:6px;"></span><span class="home-skeleton" style="width:280px; max-width:78vw; height:25px; border-radius:8px;"></span></div> 3795 <div style="display:grid; grid-auto-flow:column; grid-auto-columns:clamp(240px,26vw,320px); gap:16px; overflow:hidden; padding-bottom:14px;"><sc-for list="{{ homeSectionSkeletons }}" as="sk" hint-placeholder-count="4"><div class="home-loader-card"><div class="home-skeleton" style="aspect-ratio:16/9;"></div><div style="padding:13px 14px; display:grid; gap:9px;"><span class="home-skeleton" style="width:84%; height:14px; border-radius:7px;"></span><span class="home-skeleton" style="width:46%; height:11px; border-radius:6px;"></span></div></div></sc-for></div> 3796 </div> 3797 </sc-if> 3798 <sc-if value="{{ hasCafeVideos }}" hint-placeholder-val="{{ false }}"> 3799 <div style="padding:22px clamp(16px, 4vw, 36px) 8px;"> 3800 <div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:16px;"> 3801 <div><h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:700; margin:0; color:#112033;">Café Ganadero · Videos recientes</h2><p style="font-size:14px; line-height:1.5; color:#65758a; margin:6px 0 0;">{{ cafeText }}</p></div> 3802 </div> 3803 <div style="display:grid; grid-auto-flow:column; grid-auto-columns:clamp(240px, 26vw, 320px); gap:16px; overflow-x:auto; padding-bottom:14px; scroll-snap-type:x mandatory;"> 3804 <sc-for list="{{ cafeCards }}" as="c" hint-placeholder-count="4"> 3805 <a href="{{ c.href }}" onClick="{{ c.select }}" target="_blank" rel="noopener" data-reveal style="scroll-snap-align:start; background:#fff; border-radius:16px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.08); color:#112033; display:block; transition:transform 220ms;" style-hover="transform:translateY(-4px); color:#112033;"> 3806 <div style="aspect-ratio:16/9; background-image:url('{{ c.thumb }}'); background-size:cover; background-position:center; position:relative;">
3807 <span style="position:absolute; inset:0; display:grid; place-items:center;"><span style="width:44px; height:44px; border-radius:50%; background:rgba(230,57,70,0.9); display:grid; place-items:center; color:#fff; font-size:15px; padding-left:3px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span></span> 3808 </div> 3809 <div style="padding:12px 14px;"><strong style="font-size:14px; line-height:1.35; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">{{ c.title }}</strong><span style="font-size:12px; color:#65758a;">{{ c.meta }}</span></div> 3810 </a> 3811 </sc-for> 3812 </div> 3813 </div> 3814 </sc-if> 3815 <sc-if value="{{ hasCafePlaylistFallback }}" hint-placeholder-val="{{ false }}"> 3816 <div style="padding:22px clamp(16px, 4vw, 36px) 8px;"> 3817 <div style="display:flex; flex-wrap:wrap; justify-content:space-between; align-items:baseline; margin-bottom:16px; gap:14px;"> 3818 <div><h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:700; margin:0; color:#112033;">Café Ganadero · Ãltimos programas</h2><p style="font-size:14px; line-height:1.5; color:#65758a; margin:6px 0 0;">{{ cafeText }}</p></div> 3819 <a href="{{ cafePlaylistHref }}" target="_blank" rel="noopener" style="color:#005ca8; font-size:13px; font-weight:750; white-space:nowrap;">Ver en YouTube â</a> 3820 </div> 3821 <div data-reveal style="display:grid; grid-template-columns:{{ cafePlaylistGrid }}; background:#fff; border:1px solid #dce7f2; border-radius:20px; overflow:hidden; box-shadow:0 12px 34px rgba(0,30,60,.09);"> 3822 <iframe src="{{ cafePlaylistEmbed }}" title="Playlist oficial de Café Ganadero" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen style="width:100%; aspect-ratio:16/9; border:0; background:#001d3d;"></iframe> 3823 <div style="padding:clamp(20px, 3vw, 32px); display:flex; flex-direction:column; justify-content:center; gap:10px;"> 3824 <span style="width:42px; height:42px; border-radius:13px; display:grid; place-items:center; background:#e63946; color:#fff;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span> 3825 <strong style="font-size:clamp(18px, 2vw, 23px); line-height:1.15; color:#112033;">Todos los programas, en un solo lugar</strong> 3826 <span style="font-size:14px; line-height:1.55; color:#65758a;">Reproducà los últimos episodios de Café Ganadero directamente desde la playlist oficial de Rosgan.</span> 3827 </div> 3828 </div> 3829 </div> 3830 </sc-if> 3831 <sc-if value="{{ homeLoteNewsLoading }}"> 3832 <div aria-label="Cargando Lote de Noticias" style="padding:8px clamp(16px, 4vw, 36px) 30px;"> 3833 <div style="display:grid; gap:7px; margin-bottom:16px;"><span class="home-skeleton" style="width:125px; height:12px; border-radius:6px;"></span><span class="home-skeleton" style="width:180px; height:25px; border-radius:8px;"></span></div> 3834 <div style="display:grid; grid-auto-flow:column; grid-auto-columns:clamp(230px,30vw,300px); gap:14px; overflow:hidden; padding-bottom:14px;"><sc-for list="{{ homeSectionSkeletons }}" as="sk" hint-placeholder-count="4"><div class="home-skeleton" style="min-height:190px; border-radius:18px;"></div></sc-for></div> 3835 </div> 3836 </sc-if> 3837 <sc-if value="{{ hasLoteNoticias }}" hint-placeholder-val="{{ false }}"> 3838 <div style="padding:8px clamp(16px, 4vw, 36px) 30px;"> 3839 <div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:16px; gap:12px;"> 3840 <div><div style="font-size:11.5px; letter-spacing:0.14em; color:#e63946; font-weight:700;">INFORME SEMANAL</div><h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:800; margin:2px 0 0; color:#112033;">Lote de Noticias</h2></div> 3841 </div> 3842 <div class="home-report-track"> 3843 <sc-for list="{{ loteCards }}" as="l" hint-placeholder-count="4"> 3844 <a class="home-report-card" href="{{ l.href }}" target="{{ l.target }}" rel="noopener"> 3845 <div class="home-report-meta"><span class="home-report-icon"><img src="assets/icons/lucide/file-text.svg" alt=""></span><span>{{ l.date }}</span></div> 3846 <sc-if value="{{ l.featured }}" hint-placeholder-val="false"><span class="home-report-latest">Ãltima edición</span></sc-if> 3847 <h3>{{ l.title }}</h3> 3848 <div class="home-report-footer"><span>Leer informe</span><span class="home-report-arrow"><img src="assets/icons/lucide/arrow-up-right.svg" alt=""></span></div> 3849 </a> 3850 </sc-for> 3851 </div> 3852 </div> 3853 </sc-if> 3854 3855 <sc-if value="{{ homeNewsLoading }}"> 3856 <div aria-label="Cargando noticias" style="padding:8px clamp(16px, 4vw, 36px) 34px;"> 3857 <div style="display:grid; gap:7px; margin-bottom:16px;"><span class="home-skeleton" style="width:135px; height:12px; border-radius:6px;"></span><span class="home-skeleton" style="width:105px; height:25px; border-radius:8px;"></span></div> 3858 <div style="display:grid; grid-template-columns:{{ noticiasGrid }}; gap:16px;"> 3859 <div class="home-loader-card"><div class="home-skeleton" style="min-height:clamp(280px,34vw,380px);"></div></div> 3860 <div class="home-loader-card"><div class="home-skeleton" style="aspect-ratio:16/9;"></div><div style="padding:16px; display:grid; gap:10px;"><span class="home-skeleton" style="width:35%; height:11px; border-radius:6px;"></span><span class="home-skeleton" style="width:88%; height:17px; border-radius:7px;"></span><span class="home-skeleton" style="width:72%; height:13px; border-radius:6px;"></span></div></div> 3861 </div> 3862 </div> 3863 </sc-if> 3864 <sc-if value="{{ hasNoticias }}" hint-placeholder-val="{{ false }}"> 3865 <div style="padding:8px clamp(16px, 4vw, 36px) 34px;"> 3866 <div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:16px; gap:12px;"> 3867 <div><div style="font-size:11.5px; letter-spacing:0.14em; color:#8593a6; font-weight:600;">ACTUALIDAD ROSGAN</div><h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:800; margin:2px 0 0; color:#112033;">Noticias</h2></div> 3868 </div> 3869 <div style="display:grid; grid-template-columns:{{ noticiasGrid }}; gap:16px;"> 3870 <a href="{{ noticiaLead.href }}" target="_blank" rel="noopener" data-reveal onMouseEnter="{{ leadHoverIn }}" onMouseLeave="{{ leadHoverOut }}" style="position:relative; display:flex; flex-direction:column; justify-content:flex-end; min-height:clamp(280px, 34vw, 380px); border-radius:20px; overflow:hidden; color:#fff; box-shadow:0 12px 34px rgba(0,30,60,0.16); transition:transform 240ms;" style-hover="transform:translateY(-4px); color:#fff;"> 3871 <div data-lead-img style="position:absolute; inset:0; background-image:url('{{ noticiaLead.hero }}'); background-size:cover; background-position:center; transition:transform 500ms;"></div> 3872 <div style="position:absolute; inset:0; background:linear-gradient(to top, rgba(3,18,38,0.92) 8%, rgba(3,18,38,0.35) 55%, rgba(3,18,38,0.1));"></div> 3873 <div style="position:relative; padding:clamp(20px, 3vw, 30px);">
3874 <span style="display:inline-block; background:#e63946; font-size:10.5px; font-weight:800; letter-spacing:0.07em; padding:5px 12px; border-radius:100px; text-transform:uppercase; margin-bottom:12px;">{{ noticiaLead.category }}</span> 3875 <h3 style="font-size:clamp(20px, 2.6vw, 27px); font-weight:800; line-height:1.15; margin:0 0 10px; text-wrap:balance; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;">{{ noticiaLead.title }}</h3> 3876 <p style="margin:0 0 12px; font-size:14px; line-height:1.5; color:#d6e2f0; max-width:640px; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">{{ noticiaLead.excerpt }}</p> 3877 <span style="font-size:12px; font-weight:600; color:#a9c3de;">{{ noticiaLead.date }}</span> 3878 </div> 3879 </a> 3880 <div style="display:flex; flex-direction:column; gap:12px;"> 3881 <a href="{{ notiCurrent.href }}" target="_blank" rel="noopener" key="{{ notiKey }}" style="flex:1; display:flex; flex-direction:column; background:#fff; border:1px solid #eef3f9; border-radius:16px; overflow:hidden; color:#112033; box-shadow:0 8px 26px rgba(0,30,60,0.08); animation:heroTextFade 700ms ease both; transition:transform 220ms, box-shadow 220ms;" style-hover="transform:translateY(-4px); box-shadow:0 16px 38px rgba(0,30,60,0.15); color:#112033;"> 3882 <div style="aspect-ratio:16/9; background-image:url('{{ notiCurrent.hero }}'); background-size:cover; background-position:center;"></div> 3883 <div style="padding:15px 16px; display:flex; flex-direction:column; flex:1;"> 3884 <span style="font-size:9.5px; font-weight:800; letter-spacing:0.06em; color:#e63946; text-transform:uppercase; display:-webkit-box; -webkit-line-clamp:1; -webkit-box-orient:vertical; overflow:hidden;">{{ notiCurrent.category }}</span> 3885 <strong style="font-size:16px; line-height:1.28; margin:6px 0 8px; text-wrap:pretty; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">{{ notiCurrent.title }}</strong> 3886 <p style="margin:0 0 10px; font-size:13px; line-height:1.5; color:#65758a; display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;">{{ notiCurrent.excerpt }}</p> 3887 <span style="font-size:11.5px; color:#8593a6; margin-top:auto;">{{ notiCurrent.date }}</span> 3888 </div> 3889 </a> 3890 <sc-if value="{{ hasNotiDots }}" hint-placeholder-val="{{ false }}"> 3891 <div style="display:flex; gap:8px; justify-content:center; align-items:center; padding:2px 0;"> 3892 <sc-for list="{{ notiDots }}" as="d" hint-placeholder-count="4"> 3893 <span onClick="{{ d.go }}" style="height:7px; width:{{ d.w }}; border-radius:100px; background:{{ d.bg }}; cursor:pointer; transition:width 300ms ease, background 300ms ease;"></span> 3894 </sc-for> 3895 </div> 3896 </sc-if> 3897 </div> 3898 </div> 3899 </div> 3900 </sc-if> 3901 3902 <div style="padding:0 clamp(16px, 4vw, 36px) 40px;"> 3903 <div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:16px;"> 3904 <div><div style="font-size:11.5px; letter-spacing:0.14em; color:#8593a6; font-weight:600;">CONSIGNATARIAS</div><h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:700; margin:2px 0 0; color:#112033;">Sitios web oficiales</h2></div> 3905 </div> 3906 <sc-if value="{{ homeChannelsLoading }}"> 3907 <div aria-label="Cargando consignatarias" style="display:grid; grid-template-columns:repeat(auto-fill,minmax(210px,1fr)); gap:12px;"> 3908 <sc-for list="{{ homeChannelSkeletons }}" as="sk" hint-placeholder-count="6"><div class="home-loader-card" style="padding:14px 16px; display:flex; align-items:center; gap:12px;"><span class="home-skeleton" style="width:44px; height:44px; border-radius:12px; flex:none;"></span><span style="display:grid; gap:8px; flex:1;"><span class="home-skeleton" style="width:78%; height:14px; border-radius:6px;"></span><span class="home-skeleton" style="width:48%; height:11px; border-radius:6px;"></span></span></div></sc-for> 3909 </div> 3910 </sc-if> 3911 <sc-if value="{{ homeChannelsReady }}"> 3912 <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(210px, 1fr)); gap:12px;"> 3913 <sc-for list="{{ channelCards }}" as="ch" hint-placeholder-count="6"> 3914 <a data-reveal data-dc-attr-href="{{ ch.href }}" target="_blank" rel="noopener noreferrer" style="text-decoration:none; background:#fff; border-radius:14px; padding:14px 16px; box-shadow:0 6px 20px rgba(0,30,60,0.07); display:flex; align-items:center; gap:12px; cursor:pointer; transition:transform 200ms; border:1px solid #eef3f9;" style-hover="transform:translateY(-3px); box-shadow:0 14px 34px rgba(0,30,60,0.13); border-color:#dde7f2;">
3915 <span style="width:44px; height:44px; border-radius:12px; background:{{ ch.chipBg }}; color:#fff; display:grid; place-items:center; font-size:14px; font-weight:800; flex-shrink:0; overflow:hidden;"> 3916 <sc-if value="{{ ch.hasLogo }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ ch.logo }}" alt="" aria-hidden="true" style="display:block; width:34px; height:34px; object-fit:contain; filter:brightness(0) invert(1);"></sc-if> 3917 <sc-if value="{{ ch.noLogo }}" hint-placeholder-val="{{ true }}">{{ ch.initial }}</sc-if> 3918 </span> 3919 <div style="min-width:0;"><strong style="font-size:14px; color:#112033; display:block; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ ch.name }}</strong><span style="font-size:12px; color:#65758a;">Sitio oficial</span></div> 3920 </a> 3921 </sc-for> 3922 </div> 3923 </sc-if> 3924 </div> 3925 </div> 3926 </sc-if> 3927 3928 <!-- ============ MERCADO · ESTADÃSTICAS ============ --> 3929 <sc-if value="{{ showStatistics }}" hint-placeholder-val="{{ false }}"><rosgan-historical-statistics></rosgan-historical-statistics><rosgan-statistics-dashboard></rosgan-statistics-dashboard></sc-if> 3930 3931 <!-- ============ POOL ROSLOT ============ --> 3932 <sc-if value="{{ isPool }}" hint-placeholder-val="{{ false }}"> 3933 <div class="roslot-pool-page" style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1); padding:0 clamp(14px, 4vw, 36px);"> 3934 <div style="display:flex; justify-content:space-between; align-items:flex-end; gap:16px; flex-wrap:wrap; padding:26px 0 18px;"> 3935 <div> 3936 <div style="font-size:11.5px; letter-spacing:0.14em; color:#e63946; font-weight:700;">ROSLOT · MERCADO DIGITAL</div> 3937 <h1 style="font-size:clamp(24px, 3.4vw, 34px); font-weight:800; letter-spacing:-0.02em; margin:4px 0 0; color:#112033;">Buscador de Lotes</h1> 3938 <div class="roslot-presence-strip" role="status" aria-live="polite" title="Usuarios autenticados con actividad durante los últimos minutos"> 3939 <span class="roslot-presence-live"><i></i>Mirando ahora</span> 3940 <span class="roslot-presence-role"><span class="roslot-presence-role-icon"><i class="dc-lucide dc-lucide-shopping-cart" aria-hidden="true"></i></span><span class="roslot-presence-role-copy"><strong>{{ roslotPresenceBuyers }}</strong><small>{{ roslotPresenceBuyerLabel }}</small></span></span> 3941 <span class="roslot-presence-role is-seller"><span class="roslot-presence-role-icon"><i class="dc-lucide dc-lucide-store" aria-hidden="true"></i></span><span class="roslot-presence-role-copy"><strong>{{ roslotPresenceSellers }}</strong><small>{{ roslotPresenceSellerLabel }}</small></span></span> 3942 </div> 3943 <div style="width:min(100%,360px); margin-top:9px;"> 3944 <div style="display:flex; align-items:center; gap:9px; flex-wrap:wrap;"> 3945 <span style="display:inline-flex; align-items:center; gap:7px; padding:6px 10px; border-radius:100px; background:{{ marketStatusBg }}; color:{{ marketStatusColor }}; border:1px solid {{ marketStatusBorder }}; font-size:11.5px; font-weight:800; letter-spacing:0.02em;"> 3946 <img data-dc-attr-src="{{ marketStatusIcon }}" alt="" aria-hidden="true" width="14" height="14" style="width:14px; height:14px;"> 3947 {{ marketStatusLabel }} 3948 </span> 3949 <strong data-roslot-market-countdown style="font-size:11.5px; color:{{ marketTimerColor }};">{{ marketCountdownLabel }}</strong> 3950 </div> 3951 <div data-roslot-market-progress role="progressbar" aria-label="Tiempo hasta el próximo cambio del mercado" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ marketProgressValue }}" style="height:6px; margin-top:8px; overflow:hidden; border-radius:100px; background:#dfe7f0; box-shadow:inset 0 1px 2px rgba(17,32,51,.08);"> 3952 <span data-roslot-market-progress-fill style="display:block; width:{{ marketProgress }}; height:100%; border-radius:inherit; background:{{ marketProgressBg }}; transition:width 1s linear;"></span> 3953 </div> 3954 <small data-roslot-market-schedule style="display:block; margin-top:5px; color:#687a8f; font-size:10.5px; font-weight:650;">{{ marketScheduleLabel }}</small> 3955 </div> 3956 </div> 3957 <div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;"> 3958 <button class="roslot-report-entry" onClick="{{ goRoslotSummary }}"><i class="dc-lucide dc-lucide-receipt-text" aria-hidden="true"></i>Resumen del dÃa</button> 3959 <sc-if value="{{ canLoadLot }}" hint-placeholder-val="{{ false }}"><button onClick="{{ goLoad }}" style="display:inline-flex; align-items:center; gap:7px; background:#e63946; color:#fff; border:none; border-radius:100px; padding:9px 18px; font-size:13px; font-weight:700; font-family:inherit; cursor:pointer; transition:transform 160ms;" style-hover="transform:translateY(-2px);"><span style="font-size:15px; line-height:1;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></span>Cargar lote</button></sc-if> 3960 <sc-for list="{{ statusTabs }}" as="t" hint-placeholder-count="2">
3961 <span onClick="{{ t.go }}" style="display:inline-flex; align-items:center; gap:8px; padding:9px 16px; border-radius:100px; font-size:13px; font-weight:600; cursor:pointer; border:1px solid {{ t.border }}; background:{{ t.bg }}; color:{{ t.color }}; transition:transform 160ms;" style-hover="transform:translateY(-2px);"> 3962 <sc-if value="{{ t.live }}" hint-placeholder-val="{{ false }}"><span style="position:relative; width:7px; height:7px;"><span style="position:absolute; inset:0; border-radius:50%; background:#e63946;"></span><span style="position:absolute; inset:0; border-radius:50%; background:#e63946; animation:livePing 1.6s ease-out infinite;"></span></span></sc-if> 3963 {{ t.label }}<strong style="font-variant-numeric:tabular-nums;">{{ t.count }}</strong> 3964 </span> 3965 </sc-for> 3966 </div> 3967 </div> 3968 <sc-if value="{{ showRoslotOperations }}" hint-placeholder-val="true"> 3969 <button class="roslot-mobile-dock {{ roslotMobileDockClass }}" onClick="{{ openRoslotMobilePanel }}" aria-label="Abrir mis operaciones y actividad"><img src="assets/icons/lucide/radio-tower.svg" alt="">Actividad</button> 3970 <section class="roslot-ops-shell {{ roslotMobilePanelClass }}" aria-label="Panel de operaciones personales en vivo"> 3971 <div class="roslot-mobile-panel-head"><strong>Mi panel Roslot</strong><button onClick="{{ closeRoslotMobilePanel }}" aria-label="Cerrar panel"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></div> 3972 <article class="roslot-ops-panel"> 3973 <div class="roslot-ops-head"> 3974 <div class="roslot-ops-title"><small>PANEL PERSONAL</small><h2>Mis operaciones</h2></div> 3975 <button type="button" class="roslot-whatsapp-control {{ roslotWhatsappToggleClass }}" onClick="{{ toggleRoslotWhatsapp }}" disabled="{{ roslotWhatsappBusy }}" role="switch" aria-checked="{{ roslotWhatsappAriaChecked }}" title="{{ roslotWhatsappTitle }}"> 3976 <span class="roslot-whatsapp-copy"><span><img src="assets/icons/brands/whatsapp.svg" alt="" aria-hidden="true"></span><strong>Notificaciones por WhatsApp</strong><small>{{ roslotWhatsappStatus }}</small></span> 3977 <span class="roslot-whatsapp-switch" aria-hidden="true"><i></i></span> 3978 </button> 3979 </div> 3980 <sc-if value="{{ roslotHasOperations }}" hint-placeholder-val="true"><div class="roslot-op-list"><sc-for list="{{ roslotOperations }}" as="operation" hint-placeholder-count="3"> 3981 <div class="roslot-op-row {{ operation.rowClass }}"> 3982 <div class="roslot-op-copy"> 3983 <div class="roslot-op-topline"><span class="roslot-op-kind" style="background:{{ operation.kindBg }}; color:{{ operation.kindColor }};">{{ operation.kind }}</span><strong>{{ operation.title }}</strong><sc-if value="{{ operation.isOutbid }}" hint-placeholder-val="false"><span class="roslot-op-alert"><strong>TE SUPERARON</strong><span>Volvé a ofertar</span></span></sc-if></div> 3984 <div class="roslot-op-meta-line"><p>{{ operation.meta }}</p><span class="roslot-op-status" style="color:{{ operation.statusColor }};">{{ operation.status }}</span></div> 3985 </div> 3986 <sc-if value="{{ operation.showPrice }}" hint-placeholder-val="false"><div class="roslot-op-price"><span>{{ operation.priceLabel }}</span><strong>{{ operation.priceValue }}</strong><em>{{ operation.priceDetail }}</em></div></sc-if> 3987 <div class="roslot-op-actions"> 3988 <sc-if value="{{ operation.canBid }}" hint-placeholder-val="false"><button onClick="{{ operation.bid20 }}" disabled="{{ operation.disabled }}">+$20</button><button class="is-primary" onClick="{{ operation.bid50 }}" disabled="{{ operation.disabled }}">+$50</button></sc-if> 3989 <sc-if value="{{ operation.canClose }}" hint-placeholder-val="false"><button class="is-hammer" onClick="{{ operation.close }}" disabled="{{ operation.disabled }}"><img src="assets/icons/lucide/gavel.svg" alt="" aria-hidden="true" style="width:14px;
3989height:14px;filter:brightness(0) invert(1);">{{ operation.closeLabel }}</button></sc-if> 3990 <sc-if value="{{ operation.canApprove }}" hint-placeholder-val="false"><button class="is-danger" onClick="{{ operation.reject }}" disabled="{{ operation.disabled }}">Rechazar</button><button class="is-success" onClick="{{ operation.approve }}" disabled="{{ operation.disabled }}">Aprobar</button></sc-if> 3991 <button onClick="{{ operation.open }}">Ver</button> 3992 </div> 3993 <sc-if value="{{ operation.bidSuccess }}" hint-placeholder-val="false"><div class="roslot-op-success" role="status" aria-live="assertive" aria-label="Oferta confirmada"><dotlottie-wc class="roslot-op-success-lottie" src="assets/auth/done.lottie" autoplay speed="1.45" aria-hidden="true"></dotlottie-wc><span class="roslot-op-success-copy"><small>OFERTA CONFIRMADA</small><strong>{{ operation.bidSuccessAmount }}</strong><span>Lote {{ operation.cert }}</span></span></div></sc-if> 3994 </div> 3995 </sc-for></div></sc-if> 3996 <sc-if value="{{ roslotOperationsEmpty }}" hint-placeholder-val="false"><div class="roslot-ops-empty">No tenés operaciones activas. Las ofertas y lotes propios aparecerán acá en tiempo real.</div></sc-if> 3997 <div class="roslot-day-summary"> 3998 <div class="roslot-day-summary-head"><strong>Hacienda del dÃa por categorÃa</strong><span>{{ roslotDailyCategoriesLabel }}</span></div> 3999 <sc-if value="{{ roslotHasDailyCategories }}" hint-placeholder-val="true"><div class="roslot-day-category-list"><sc-for list="{{ roslotDailyCategories }}" as="category" hint-placeholder-count="3"> 4000 <span class="roslot-day-category"><strong>{{ category.quantity }}</strong><em>{{ category.label }} · {{ category.lotsLabel }}</em></span> 4001 </sc-for></div></sc-if> 4002 <sc-if value="{{ roslotDailyCategoriesEmpty }}" hint-placeholder-val="false"><div class="roslot-day-summary-empty">No hay lotes programados para hoy.</div></sc-if> 4003 </div> 4004 </article> 4005 <aside class="roslot-live-tower" aria-label="Actividad en vivo"> 4006 <div class="roslot-live-head"><div class="roslot-live-title"><small>EVENTOS PERSONALES</small><h2>Historial de hoy</h2></div><span class="roslot-live-state"><i></i>En lÃnea</span></div> 4007 <sc-if value="{{ roslotActivityLoading }}" hint-placeholder-val="false"><div class="roslot-live-empty">Sincronizando tu actividad recienteâ¦</div></sc-if> 4008 <sc-if value="{{ roslotHasActivity }}" hint-placeholder-val="true"><div class="roslot-live-list"><sc-for list="{{ roslotActivity }}" as="event" hint-placeholder-count="4"><button class="roslot-live-row {{ event.rowClass }}" onClick="{{ event.open }}"><span class="roslot-live-icon" style="background:{{ event.iconBg }};"><img data-dc-attr-src="{{ event.icon }}" alt=""></span><span class="roslot-live-copy"><strong>{{ event.title }}</strong><p>{{ event.body }}</p><span class="roslot-live-meta"><span>{{ event.time }}</span><span style="color:{{ event.statusColor }};">{{ event.status }}</span></span></span></button></sc-for></div></sc-if> 4009 <sc-if value="{{ roslotActivityEmpty }}" hint-placeholder-val="false"><div class="roslot-live-empty">Sin novedades de hoy. Esta torre mostrará superaciones, ofertas recibidas y cierres del dÃa.</div></sc-if> 4010 <div class="roslot-live-footer"><span>{{ roslotRealtimeLabel }}</span><button onClick="{{ goNotificationCenter }}">Ver historial</button></div> 4011 </aside> 4012 </section> 4013 </sc-if> 4014 <!-- search + filter bar --> 4015 <div class="roslot-search-toolbar" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center; position:sticky; top:{{ poolBarTop }}; z-index:30; background:rgba(244,247,251,0.92); backdrop-filter:blur(12px); padding:10px 0; border-bottom:1px solid #e3eaf2;"> 4016 <div class="roslot-search-box" style="flex:1; min-width:220px; display:flex; align-items:center; gap:10px; background:#fff; border:1px solid #e3eaf2; border-radius:12px; padding:11px 14px; box-shadow:0 4px 14px rgba(0,30,60,0.05);">
4017 <span style="color:#8593a6; font-size:15px;"><i class="dc-lucide dc-lucide-search" aria-hidden="true"></i></span> 4018 <input value="" data-dc-attr-value="{{ poolQuery }}" onInput="{{ onPoolQuery }}" placeholder="Buscar por categorÃa, raza, provincia o certificadoâ¦" style="border:none; outline:none; font-size:14px; font-family:inherit; width:100%; background:none; color:#112033;"> 4019 </div> 4020 <button class="roslot-filter-trigger" onClick="{{ toggleFilters }}" style="display:inline-flex; align-items:center; gap:8px; background:{{ filterBtnBg }}; color:{{ filterBtnColor }}; border:1px solid {{ filterBtnBorder }}; border-radius:12px; padding:11px 18px; font-size:14px; font-weight:600; font-family:inherit; cursor:pointer; transition:transform 160ms;" style-hover="transform:translateY(-2px);"> 4021 <img src="assets/icons/icon-filter.svg" alt="" style="width:15px; height:15px; filter:{{ filterIconFilter }};">Filtros 4022 <sc-if value="{{ activeFilterCount }}" hint-placeholder-val="{{ false }}"><span style="background:#e63946; color:#fff; font-size:11px; font-weight:800; min-width:19px; height:19px; border-radius:100px; display:grid; place-items:center;">{{ activeFilterCount }}</span></sc-if> 4023 </button> 4024 <select onChange="{{ onSort }}" value="" data-dc-attr-value="{{ poolSort }}" style="background:#fff; border:1px solid #e3eaf2; border-radius:12px; padding:11px 12px; font-size:13.5px; font-family:inherit; color:#112033; outline:none;"> 4025 <option value="recientes">Más recientes</option> 4026 <option value="precio-asc">Precio: menor a mayor</option> 4027 <option value="precio-desc">Precio: mayor a menor</option> 4028 <option value="peso">Peso promedio</option> 4029 <option value="cabezas">Cantidad de cabezas</option> 4030 </select> 4031 <div class="roslot-view-toggle" role="group" aria-label="Vista de lotes"> 4032 <span onClick="{{ setPoolGridView }}" title="Grilla" role="button" aria-label="Ver lotes en grilla" aria-pressed="{{ poolGridPressed }}" style="background:{{ poolGridViewBg }}; color:{{ poolGridViewColor }};"><i class="dc-lucide dc-lucide-grid" aria-hidden="true"></i></span> 4033 <span onClick="{{ setPoolListView }}" title="Lista" role="button" aria-label="Ver lotes en lista" aria-pressed="{{ poolListPressed }}" style="background:{{ poolListViewBg }}; color:{{ poolListViewColor }};"><i class="dc-lucide dc-lucide-list" aria-hidden="true"></i></span> 4034 </div> 4035 </div> 4036 <sc-if value="{{ showPoolOfferTracker }}" hint-placeholder-val="{{ true }}"> 4037 <div style="display:flex; align-items:center; justify-content:space-between; gap:14px; flex-wrap:wrap; margin-top:14px; padding:13px 16px; background:#fff; border:1px solid #e3eaf2; border-radius:14px; box-shadow:0 6px 20px rgba(0,30,60,0.05);"> 4038 <div style="display:flex; align-items:center; gap:10px; min-width:220px;"> 4039 <span style="width:34px; height:34px; border-radius:10px; background:#eef4fb; color:#0a5ca8; display:grid; place-items:center; flex-shrink:0;"> 4040 <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 17 9 11 13 15 21 7"></polyline><polyline points="15 7 21 7 21 13"></polyline></svg> 4041 </span> 4042 <div><div style="font-size:11px; letter-spacing:0.1em; color:#8593a6; font-weight:800;">SEGUIMIENTO DE OFERTAS</div><div style="font-size:13.5px; color:#112033; font-weight:700; margin-top:2px;">{{ poolOfferTrackerText }}</div></div> 4043 </div> 4044 <div style="display:flex; align-items:center; gap:7px; flex-wrap:wrap;"> 4045 <span style="display:inline-flex; align-items:center; gap:6px; background:rgba(31,122,99,0.14); color:#1f7a63; border-radius:9px; padding:8px 11px; font-size:11.5px; font-weight:800;"> 4046 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 17 9 11 13 15 21 7"></polyline><polyline points="15 7 21 7 21 13"></polyline></svg>Vas ganando 4047 </span>
4048 <span style="display:inline-flex; align-items:center; gap:6px; background:rgba(230,57,70,0.14); color:#c22; border-radius:9px; padding:8px 11px; font-size:11.5px; font-weight:800;"> 4049 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 7 9 13 13 9 21 17"></polyline><polyline points="15 17 21 17 21 11"></polyline></svg>Te superaron 4050 </span> 4051 </div> 4052 </div> 4053 </sc-if> 4054 <!-- filters + results --> 4055 <div class="roslot-results-layout"> 4056 <aside class="roslot-filter-sidebar {{ filterSidebarClass }}" aria-label="Filtros de lotes"> 4057 <div class="roslot-filter-sidebar-head"><strong>Filtros</strong><sc-if value="{{ activeFilterCount }}" hint-placeholder-val="false"><span>{{ activeFilterCount }} activos</span></sc-if></div> 4058 <sc-for list="{{ filterGroups }}" as="g" hint-placeholder-count="4"> 4059 <div> 4060 <div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700; margin-bottom:8px;">{{ g.label }}</div> 4061 <div style="display:flex; gap:8px; flex-wrap:wrap;"> 4062 <sc-for list="{{ g.options }}" as="o" hint-placeholder-count="4"> 4063 <span onClick="{{ o.toggle }}" style="padding:7px 14px; border-radius:100px; font-size:13px; font-weight:600; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }}; transition:all 160ms;" style-hover="border-color:#0a5ca8;">{{ o.label }}</span> 4064 </sc-for> 4065 </div> 4066 </div> 4067 </sc-for> 4068 <div style="display:flex; flex-direction:column; align-items:flex-start; gap:8px; border-top:1px solid #eef3f9; padding-top:12px;"> 4069 <span onClick="{{ toggleChina }}" style="display:inline-flex; align-items:center; gap:6px; padding:5px 10px; border-radius:100px; font-size:11.5px; font-weight:650; line-height:1; cursor:pointer; border:1px solid {{ chinaBorder }}; background:{{ chinaBg }}; color:{{ chinaColor }};"><img src="assets/icons/china-flag.svg" alt="" aria-hidden="true" width="14" height="10" style="display:block; width:14px; height:10px; object-fit:cover; border-radius:2px;">Apto China</span> 4070 <span onClick="{{ clearFilters }}" style="display:block; padding-left:2px; font-size:11.5px; line-height:1.2; color:#e63946; font-weight:650; cursor:pointer;">Limpiar filtros</span> 4071 </div> 4072 </aside> 4073 <main class="roslot-results-main"> 4074 <!-- results --> 4075 <div style="display:flex; justify-content:space-between; align-items:baseline; padding:18px 2px 14px;"> 4076 <span style="font-size:13.5px; color:#65758a;"><strong style="color:#112033; font-variant-numeric:tabular-nums;">{{ resultCount }}</strong> {{ resultLabel }}</span> 4077 <sc-if value="{{ lotsSourceNote }}" hint-placeholder-val="{{ false }}"><span style="font-size:12px; color:#8593a6;">{{ lotsSourceNote }}</span></sc-if> 4078 </div> 4079 <sc-if value="{{ lotsLoading }}" hint-placeholder-val="{{ false }}"> 4080 <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(272px, 1fr)); gap:18px;"> 4081 <sc-for list="{{ skeletons }}" as="k" hint-placeholder-count="6"> 4082 <div style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.06);"> 4083 <div style="height:170px; background:linear-gradient(90deg, #eef3f9 25%, #f8fafd 50%, #eef3f9 75%); background-size:800px 100%; animation:shimmer 1.4s linear infinite;"></div> 4084 <div style="padding:16px; display:flex; flex-direction:column; gap:10px;"> 4085 <div style="height:14px; width:70%; border-radius:6px; background:#eef3f9;"></div> 4086 <div style="height:11px; width:45%; border-radius:6px; background:#eef3f9;"></div> 4087 </div> 4088 </div> 4089 </sc-for> 4090 </div> 4091 </sc-if> 4092 <sc-if value="{{ showLots }}" hint-placeholder-val="{{ true }}"> 4093 <div class="roslot-lot-results-grid {{ poolViewClass }}" style="display:grid; grid-template-columns:repeat(auto-fill, minmax(272px, 1fr)); gap:18px;"> 4094 <sc-for list="{{ lotCards }}" as="l" hint-placeholder-count="6"> 4095 <div class="roslot-lot-card" data-reveal onMouseEnter="{{ l.hoverIn }}" onMouseLeave="{{ l.hoverOut }}" style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.08); display:flex; flex-direction:column; transition:transform 240ms cubic-bezier(0.22,0.72,0.18,1), box-shadow 240ms;" style-hover="transform:translateY(-6px); box-shadow:0 26px 60px rgba(0,30,60,0.18);"> 4096 <div class="roslot-lot-card-media" onClick="{{ l.open }}" style="position:relative; height:180px; cursor:pointer; overflow:hidden;"> 4097 <div data-lot-img style="position:absolute; inset:0; background-image:url('{{ l.image }}'); background-size:cover; background-position:center; transition:transform 600ms cubic-bezier(0.22,0.72,0.18,1);"></div> 4098 <sc-if value="{{ l.videoPreview }}" hint-placeholder-val="{{ false }}"> 4099 <video data-lot-preview data-src="{{ l.videoPreview }}" muted loop playsinline preload="none" style="position:absolute; inset:0; width:100%; height:100%; object-fit:cover; opacity:0; transition:opacity 300ms;"></video> 4100 </sc-if> 4101 <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,29,61,0.05) 40%, rgba(0,29,61,0.78));"></div>
4102 <span style="position:absolute; top:10px; left:10px; background:rgba(0,29,61,0.82); color:#fff; font-size:11px; font-weight:700; padding:5px 10px; border-radius:8px; letter-spacing:0.04em;">CERT. {{ l.cert }}</span> 4103 <sc-if value="{{ l.isLive }}" hint-placeholder-val="{{ false }}"> 4104 <span style="position:absolute; top:10px; right:10px; display:inline-flex; align-items:center; gap:6px; background:rgba(230,57,70,0.94); color:#fff; font-size:10.5px; font-weight:800; padding:5px 10px; border-radius:100px; letter-spacing:0.06em;"><span style="width:6px; height:6px; border-radius:50%; background:#fff; animation:pulseDot 1.2s infinite;"></span>EN VIVO</span> 4105 </sc-if> 4106 <sc-if value="{{ l.isSold }}" hint-placeholder-val="{{ false }}"> 4107 <span style="position:absolute; top:10px; right:10px; display:inline-flex; align-items:center; gap:6px; background:rgba(31,122,99,0.96); color:#fff; font-size:10.5px; font-weight:800; padding:5px 10px; border-radius:100px; letter-spacing:0.06em;"><img src="assets/icons/lucide/gavel.svg" alt="" aria-hidden="true" style="width:13px;height:13px;filter:brightness(0) invert(1);">VENDIDO</span> 4108 </sc-if> 4109 <div style="position:absolute; bottom:10px; left:12px; right:12px; display:flex; justify-content:space-between; align-items:flex-end; gap:8px; color:#fff;"> 4110 <div><div style="font-size:16px; font-weight:800; line-height:1.15;">{{ l.categoria }}</div><div style="font-size:11.5px; color:#c8d8ea;">{{ l.origen }}</div></div> 4111 <div class="roslot-price-badge {{ l.priceBadgeClass }}" style="text-align:right; background:rgba(255,255,255,0.14); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.24); border-radius:10px; padding:6px 10px;"> 4112 <div style="display:flex;align-items:center;justify-content:flex-end;gap:5px;font-size:8px; color:#dbe6f2; letter-spacing:0.08em; font-weight:700;"><sc-if value="{{ l.hasOffers }}" hint-placeholder-val="false"><span class="roslot-price-live-dot"></span></sc-if>{{ l.priceLabel }}</div> 4113 <div style="font-size:15px; font-weight:800; font-variant-numeric:tabular-nums; white-space:nowrap;">{{ l.currentPrice }}</div> 4114 </div> 4115 </div> 4116 </div> 4117 <div class="roslot-lot-card-body" style="padding:14px 16px 16px; display:flex; flex-direction:column; gap:9px; flex:1;"> 4118 <div class="roslot-lot-card-stats" style="display:flex; gap:8px;"> 4119 <div style="flex:1; background:#f2f6fb; border-radius:10px; padding:9px 12px;"><div style="font-size:10.5px; color:#8593a6; letter-spacing:0.06em;">CABEZAS</div><div style="font-size:16px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ l.cabezas }}</div></div> 4120 <div style="flex:1; background:#f2f6fb; border-radius:10px; padding:9px 12px;"><div style="font-size:10.5px; color:#8593a6; letter-spacing:0.06em;">PESO PROM.</div><div style="font-size:16px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ l.peso }}</div></div> 4121 </div> 4122 <div class="roslot-lot-card-details" style="display:flex; flex-direction:column; gap:5px; font-size:12.5px;"> 4123 <div style="display:flex; justify-content:space-between;"><span style="color:#8593a6;">Raza</span><span style="color:#112033; font-weight:600; text-align:right;">{{ l.razas }}</span></div> 4124 <div style="display:flex; justify-content:space-between;"><span style="color:#8593a6;">Destino</span><span style="color:{{ l.destinoColor }}; font-weight:700;">{{ l.destino }}</span></div> 4125 <div style="display:flex; justify-content:space-between;"><span style="color:#8593a6;">Plazo</span><span style="color:#112033; font-weight:600;">{{ l.plazo }}</span></div> 4126 <sc-if value="{{ l.showPlazoDetalle }}" hint-placeholder-val="false"><div style="display:flex; justify-content:space-between; gap:10px;"><span style="color:#8593a6;">Detalle del plazo</span><span style="color:#112033; font-weight:600; text-align:right;">{{ l.plazoDetalle }}</span></div></sc-if> 4127 <div style="display:flex; justify-content:space-between; gap:10px;"><span style="color:#8593a6;">Condición</span><span style="color:#112033; font-weight:600; text-align:right;">{{ l.condicion }}</span></div> 4128 </div> 4129 <sc-if value="{{ l.aptoChina }}" hint-placeholder-val="{{ false }}">
4130 <span class="roslot-lot-card-china" style="width:fit-content; display:inline-flex; align-items:center; gap:6px; font-size:11.5px; font-weight:700; color:#9d2235; background:#fdeef0; border:1px solid #f6ccd2; padding:4px 10px; border-radius:100px;"><img src="assets/icons/china-flag.svg" alt="" aria-hidden="true" width="16" height="12" style="display:block; width:16px; height:12px; object-fit:cover; border-radius:2px; box-shadow:0 0 0 1px rgba(157,34,53,0.16);">Apto China</span> 4131 </sc-if> 4132 <div class="roslot-lot-card-actions" style="display:flex; gap:8px; margin-top:auto; padding-top:6px;"> 4133 <sc-if value="{{ l.standing }}" hint-placeholder-val="{{ false }}"> 4134 <span style="flex:1; display:inline-flex; align-items:center; justify-content:center; gap:7px; background:{{ l.standingBg }}; color:{{ l.standingColor }}; border-radius:10px; padding:11px; font-size:13px; font-weight:800; animation:badgePop 340ms cubic-bezier(0.34,1.56,0.64,1) both;"> 4135 <sc-if value="{{ l.standingWinning }}" hint-placeholder-val="{{ true }}"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 17 9 11 13 15 21 7"></polyline><polyline points="15 7 21 7 21 13"></polyline></svg></sc-if> 4136 <sc-if value="{{ l.standingOutbid }}" hint-placeholder-val="{{ false }}"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 7 9 13 13 9 21 17"></polyline><polyline points="15 17 21 17 21 11"></polyline></svg></sc-if> 4137 {{ l.standingLabel }} 4138 </span> 4139 </sc-if> 4140 <button onClick="{{ l.open }}" style="flex:1; display:inline-flex;align-items:center;justify-content:center;gap:6px;background:#eef3f9; color:#002e5f; border:1px solid #dde7f2; border-radius:10px; padding:11px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="background:#dde7f2;">Ver lote <i class="dc-lucide dc-lucide-arrow-right" aria-hidden="true"></i></button> 4141 </div> 4142 </div> 4143 </div> 4144 </sc-for> 4145 </div> 4146 </sc-if> 4147 <sc-if value="{{ showEmpty }}" hint-placeholder-val="{{ false }}"> 4148 <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:48px 24px; text-align:center; color:#65758a;"> 4149 <div style="font-size:17px; font-weight:700; color:#112033;">{{ poolEmptyTitle }}</div> 4150 <div style="font-size:13.5px; margin-top:4px;">{{ poolEmptyText }}</div> 4151 <button onClick="{{ clearFilters }}" style="margin-top:16px; background:#002e5f; color:#fff; border:none; border-radius:10px; padding:11px 22px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;">Limpiar filtros</button> 4152 </div> 4153 </sc-if> 4154 </main> 4155 </div> 4156 <div style="height:34px;"></div> 4157 </div> 4158 </sc-if> 4159 4160 <!-- ============ ROSLOT DAILY COMMERCIAL SUMMARY ============ --> 4161 <sc-if value="{{ isRoslotSummary }}" hint-placeholder-val="{{ false }}"> 4162 <main class="roslot-report-page"> 4163 <header class="roslot-report-header"> 4164 <div> 4165 <button class="roslot-report-back" onClick="{{ goRoslotPool }}"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i>Volver al mercado</button> 4166 <div class="roslot-report-eyebrow">ROSLOT · CIERRE COMERCIAL</div> 4167 <h1>Resumen de compras y ventas</h1> 4168 <p>Una lectura clara de las operaciones adjudicadas, con los datos de contacto de cada contraparte.</p> 4169 </div> 4170 <div class="roslot-report-date-control"> 4171 <label for="roslot-report-date">Fecha de operaciones</label> 4172 <div> 4173 <input id="roslot-report-date" type="date" value="{{ roslotReportDate }}" onChange="{{ onRoslotReportDate }}"> 4174 <button onClick="{{ refreshRoslotReport }}" title="Actualizar resumen"><i class="dc-lucide dc-lucide-refresh-cw" aria-hidden="true"></i><span>Actualizar</span></button> 4175 </div> 4176 </div> 4177 </header> 4178 4179 <sc-if value="{{ roslotReportLoading }}" hint-placeholder-val="{{ true }}"> 4180 <section class="roslot-report-loading" aria-label="Preparando resumen comercial"> 4181 <div class="roslot-report-loading-hero"></div> 4182 <div class="roslot-report-loading-grid"><span></span><span></span><span></span><span></span></div> 4183 <div class="roslot-report-loading-ledger"></div> 4184 </section> 4185 </sc-if> 4186 4187 <sc-if value="{{ roslotReportHasError }}" hint-placeholder-val="{{ false }}"> 4188 <section class="roslot-report-error">
4189 <span><i class="dc-lucide dc-lucide-circle-alert" aria-hidden="true"></i></span> 4190 <div><strong>No pudimos preparar el resumen</strong><p>{{ roslotReportError }}</p></div> 4191 <button onClick="{{ refreshRoslotReport }}">Reintentar</button> 4192 </section> 4193 </sc-if> 4194 4195 <sc-if value="{{ roslotReportReady }}" hint-placeholder-val="{{ false }}"> 4196 <section class="roslot-report-hero"> 4197 <div class="roslot-report-hero-copy"> 4198 <span>{{ roslotReportScopeLabel }}</span> 4199 <h2>{{ roslotReportHeadline }}</h2> 4200 <p>{{ roslotReportHeadlineDetail }}</p> 4201 </div> 4202 <div class="roslot-report-hero-date"><small>Jornada</small><strong>{{ roslotReportDateLabel }}</strong></div> 4203 <div class="roslot-report-hero-note"><i class="dc-lucide dc-lucide-shield-check" aria-hidden="true"></i><p><strong>Información privada</strong><span>Solo se muestran operaciones propias, salvo perfiles administradores.</span></p></div> 4204 </section> 4205 4206 <section class="roslot-report-kpis"> 4207 <sc-for list="{{ roslotReportKpis }}" as="kpi" hint-placeholder-count="4"> 4208 <article class="roslot-report-kpi"><span><i class="dc-lucide {{ kpi.icon }}" aria-hidden="true"></i></span><div><small>{{ kpi.label }}</small><strong>{{ kpi.value }}</strong><p>{{ kpi.detail }}</p></div></article> 4209 </sc-for> 4210 </section> 4211 4212 <section class="roslot-report-ledger"> 4213 <header class="roslot-report-ledger-head"> 4214 <div><span>DETALLE OPERATIVO</span><h2>Operaciones adjudicadas</h2><p>Precio, volumen y contacto comercial en una sola lectura.</p></div> 4215 <div class="roslot-report-search"><i class="dc-lucide dc-lucide-search" aria-hidden="true"></i><input type="search" placeholder="Buscar lote o contraparteâ¦" value="{{ roslotReportSearch }}" onInput="{{ onRoslotReportSearch }}"></div> 4216 </header> 4217 <sc-if value="{{ roslotReportShowRoleFilters }}" hint-placeholder-val="{{ true }}"> 4218 <nav class="roslot-report-filters" aria-label="Filtrar operaciones"> 4219 <sc-for list="{{ roslotReportFilters }}" as="filter" hint-placeholder-count="3"><button class="roslot-report-filter{{ filter.cls }}" onClick="{{ filter.go }}">{{ filter.label }}</button></sc-for> 4220 </nav> 4221 </sc-if> 4222 4223 <div class="roslot-report-transactions"> 4224 <sc-for list="{{ roslotReportRows }}" as="row" hint-placeholder-count="2"> 4225 <article class="roslot-report-row"> 4226 <div class="roslot-report-row-thumb"> 4227 <sc-if value="{{ row.hasThumbnail }}" hint-placeholder-val="{{ true }}"><img data-dc-attr-src="{{ row.thumbnail }}" alt=""></sc-if> 4228 <sc-if value="{{ row.hasNoThumbnail }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-beef" aria-hidden="true"></i></sc-if> 4229 <span>VENDIDO</span> 4230 </div> 4231 <div class="roslot-report-row-lot"> 4232 <small>CERT. {{ row.cert }}</small> 4233 <h3>{{ row.category }}</h3> 4234 <p>{{ row.establishment }} · {{ row.location }}</p> 4235 <div><span>{{ row.heads }} cab.</span><span>{{ row.weight }}</span></div> 4236 </div> 4237 <div class="roslot-report-row-money"> 4238 <small>IMPORTE ESTIMADO</small><strong>{{ row.total }}</strong> 4239 <p>{{ row.price }} <em>{{ row.priceUnit }}</em></p> 4240 <span>{{ row.condition }} · {{ row.closedTime }}</span> 4241 </div> 4242 <section class="roslot-report-row-contact is-buyer"> 4243 <header><span>{{ row.buyer.initials }}</span><div><small>{{ row.buyer.type }}</small><strong>{{ row.buyer.name }}</strong></div></header> 4244 <div class="roslot-report-row-contact-data"><span title="{{ row.buyer.phone }}">{{ row.buyer.phone }}</span><span title="{{ row.buyer.email }}">{{ row.buyer.email }}</span></div> 4245 <div class="roslot-report-row-contact-actions"> 4246 <sc-if value="{{ row.buyer.hasPhone }}" hint-placeholder-val="{{ true }}"><a href="{{ row.buyer.phoneHref }}" aria-label="Llamar a {{ row.buyer.name }}"><i class="dc-lucide dc-lucide-phone" aria-hidden="true"></i>Llamar</a></sc-if> 4247 <sc-if value="{{ row.buyer.hasEmail }}" hint-placeholder-val="{{ true }}"><a href="{{ row.buyer.emailHref }}" aria-label="Enviar email a {{ row.buyer.name }}"><i class="dc-lucide dc-lucide-mail" aria-hidden="true"></i>Email</a></sc-if> 4248 </div> 4249 </section> 4250 <section class="roslot-report-row-contact is-seller"> 4251 <header><span>{{ row.seller.initials }}</span><div><small>{{ row.seller.type }}</small><strong>{{ row.seller.name }}</strong></div></header> 4252 <div class="roslot-report-row-contact-data"><span title="{{ row.seller.phone }}">{{ row.seller.phone }}</span><span title="{{ row.seller.email }}">{{ row.seller.email }}</span></div> 4253 <div class="roslot-report-row-contact-actions"> 4254 <sc-if value="{{ row.seller.hasPhone }}" hint-placeholder-val="{{ true }}"><a href="{{ row.seller.phoneHref }}" aria-label="Llamar a {{ row.seller.name }}"><i class="dc-lucide dc-lucide-phone" aria-hidden="true"></i>Llamar</a></sc-if> 4255 <sc-if value="{{ row.seller.hasEmail }}" hint-placeholder-val="{{ true }}"><a href="{{ row.seller.emailHref }}" aria-label="Enviar email a {{ row.seller.name }}"><i class="dc-lucide dc-lucide-mail" aria-hidden="true"></i>Email</a></sc-if> 4256 </div> 4257 </section> 4258 <button class="roslot-report-row-open" onClick="{{ row.open }}">Ver lote<i class="dc-lucide dc-lucide-arrow-up-right" aria-hidden="true"></i></button> 4259 </article> 4260 </sc-for> 4261 </div> 4262 4263 <sc-if value="{{ roslotReportEmpty }}" hint-placeholder-val="{{ false }}"> 4264 <div class="roslot-report-empty"><span><i class="dc-lucide dc-lucide-calendar-check" aria-hidden="true"></i></span><h3>No hay operaciones para mostrar</h3><p>No encontramos compras o ventas adjudicadas para esta fecha y este filtro.</p></div> 4265 </sc-if> 4266 </section> 4267 </sc-if> 4268 </main> 4269 </sc-if> 4270 4271 <!-- ============ LOT DETAIL ============ --> 4272 <sc-if value="{{ isLot }}" hint-placeholder-val="{{ false }}"> 4273 <div style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1); padding:0 clamp(14px, 4vw, 36px); max-width:1240px; margin:0 auto;"> 4274 <div style="display:flex; justify-content:space-between; align-items:flex-end; gap:14px; flex-wrap:wrap; padding:24px 0 18px;"> 4275 <div>
4276 <span onClick="{{ goPool }}" style="display:inline-flex;align-items:center;gap:6px;font-size:13px; color:#0a5ca8; font-weight:600; cursor:pointer;"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al pool</span> 4277 <h1 style="font-size:clamp(23px, 3.2vw, 32px); font-weight:800; letter-spacing:-0.02em; margin:8px 0 2px; color:#112033;">{{ lotTitle }}</h1> 4278 <span style="font-size:13.5px; color:#65758a;">Cert. {{ lotCert }} · {{ lotOrigen }}</span> 4279 </div> 4280 <span style="display:inline-flex; align-items:center; gap:8px; background:{{ lotStatusBg }}; color:#fff; font-size:12px; font-weight:800; padding:7px 16px; border-radius:100px; letter-spacing:0.06em;"><img data-dc-attr-src="{{ lotStatusIcon }}" alt="" width="15" height="15" style="filter:brightness(0) invert(1);">{{ lotStatus }}</span> 4281 </div> 4282 <div style="background:#fff; border:1px solid #e1e9f1; border-radius:16px; padding:14px 16px; margin-bottom:18px; box-shadow:0 8px 26px rgba(0,30,60,0.07);"> 4283 <div style="display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;"> 4284 <span style="display:inline-flex; align-items:center; gap:7px; padding:6px 10px; border-radius:100px; background:{{ marketStatusBg }}; color:{{ marketStatusColor }}; border:1px solid {{ marketStatusBorder }}; font-size:11.5px; font-weight:800; letter-spacing:.02em;"> 4285 <img data-dc-attr-src="{{ marketStatusIcon }}" alt="" aria-hidden="true" width="14" height="14" style="width:14px; height:14px;"> 4286 {{ marketStatusLabel }} 4287 </span> 4288 <strong data-roslot-market-countdown style="font-size:11.5px; color:{{ marketTimerColor }}; font-variant-numeric:tabular-nums;">{{ marketCountdownLabel }}</strong> 4289 </div> 4290 <div data-roslot-market-progress role="progressbar" aria-label="Tiempo hasta el próximo cambio del mercado" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ marketProgressValue }}" style="height:6px; margin-top:9px; overflow:hidden; border-radius:100px; background:#dfe7f0; box-shadow:inset 0 1px 2px rgba(17,32,51,.08);"> 4291 <span data-roslot-market-progress-fill style="display:block; width:{{ marketProgress }}; height:100%; border-radius:inherit; background:{{ marketProgressBg }}; transition:width 1s linear;"></span> 4292 </div> 4293 <small data-roslot-market-schedule style="display:block; margin-top:6px; color:#687a8f; font-size:10.5px; font-weight:650;">{{ marketScheduleLabel }}</small> 4294 </div> 4295 <div style="display:grid; grid-template-columns:{{ lotLayoutCols }}; gap:20px; align-items:start;"> 4296 <!-- media --> 4297 <div style="display:flex; flex-direction:column; gap:12px;"> 4298 <div class="dc-cabana-video-frame" style="border-radius:20px; box-shadow:0 22px 60px rgba(0,30,60,0.2);"> 4299 <sc-if value="{{ lotVideoUrl }}" hint-placeholder-val="{{ false }}"> 4300 <video class="roslot-lot-player" data-lot-player data-src="{{ lotVideoUrl }}" onLoadedMetadata="{{ lotVideoMeta }}" onTimeUpdate="{{ lotVideoTimeUpdate }}" onVolumeChange="{{ lotVideoVolumeChange }}" onPause="{{ lotVideoPause }}" onPlay="{{ lotVideoPlay }}" onPlaying="{{ lotVideoPlay }}" onEnded="{{ lotVideoEnded }}" controls autoplay muted playsinline preload="auto" style="display:block; width:100%; height:100%; object-fit:cover; background:#001d3d;"></video> 4301 </sc-if> 4302 <sc-if value="{{ lotNoVideo }}" hint-placeholder-val="{{ true }}"> 4303 <div style="width:100%; aspect-ratio:16/9; background-image:url('{{ lotImage }}'); background-size:cover; background-position:center;"></div> 4304 </sc-if> 4305 </div> 4306 <sc-if value="{{ lotVideoUrl }}" hint-placeholder-val="{{ false }}"><div class="dc-video-controls" aria-label="Controles del video"><button class="dc-video-control-button" onClick="{{ lotTogglePlay }}" title="{{ lotPlayLabel }}" aria-label="{{ lotPlayLabel }}"><img data-dc-attr-src="{{ lotPlayIcon }}" alt=""></button><input class="dc-video-slider" type="range" min="0" max="100" step="0.1" value="{{ lotVideoProgress }}" onInput="{{ lotSeekVideo }}" onChange="{{ lotSeekVideo }}" aria-label="Avance del video"><span class="dc-video-time">{{ lotVideoTime }}</span><button class="dc-video-control-button" onClick="{{ lotToggleMute }}" title="{{ lotMuteLabel }}" aria-label="{{ lotMuteLabel }}"><img data-dc-attr-src="{{ lotVolumeIcon }}" alt=""></button><input class="dc-video-slider dc-video-volume" type="range" min="0" max="1" step="0.05" value="{{ lotVideoVolume }}" onInput="{{ lotSetVolume }}" onChange="{{ lotSetVolume }}" aria-label="Volumen"><button class="dc-video-control-button" onClick="{{ lotFullscreen }}" title="Pantalla completa" aria-label="Pantalla completa"><img src="assets/icons/lucide/maximize-2.svg" alt=""></button></div></sc-if> 4307 <sc-if value="{{ hasVideoThumbs }}
4307" hint-placeholder-val="{{ false }}"> 4308 <div style="display:flex; gap:10px; overflow-x:auto; padding-bottom:4px;"> 4309 <sc-for list="{{ videoThumbs }}" as="vt" hint-placeholder-count="3"> 4310 <div onClick="{{ vt.select }}" style="position:relative; width:110px; aspect-ratio:16/10; border-radius:10px; overflow:hidden; cursor:pointer; flex-shrink:0; border:2px solid {{ vt.border }}; transition:transform 180ms;" style-hover="transform:translateY(-2px);"> 4311 <sc-if value="{{ vt.hasThumb }}" hint-placeholder-val="{{ true }}"> 4312 <img data-dc-attr-src="{{ vt.thumb }}" alt="Video {{ vt.n }}" style="position:absolute; inset:0; width:100%; height:100%; object-fit:cover;"> 4313 </sc-if> 4314 <sc-if value="{{ vt.loading }}" hint-placeholder-val="{{ false }}"> 4315 <div style="position:absolute; inset:0; display:grid; place-items:center; background:#e8eef5;"><span style="width:19px; height:19px; border:2px solid #9fb0c3; border-top-color:#0a5ca8; border-radius:50%; animation:spin 800ms linear infinite;"></span></div> 4316 </sc-if> 4317 <span style="position:absolute; bottom:4px; right:6px; color:#fff; font-size:11px; font-weight:800; text-shadow:0 1px 4px rgba(0,0,0,0.6);">{{ vt.n }}</span> 4318 </div> 4319 </sc-for> 4320 </div> 4321 </sc-if> 4322 <!-- data groups --> 4323 <sc-for list="{{ lotGroups }}" as="g" hint-placeholder-count="3"> 4324 <div data-reveal style="background:#fff; border-radius:18px; padding:18px 20px; box-shadow:0 8px 26px rgba(0,30,60,0.07);"> 4325 <div style="font-size:12px; letter-spacing:0.12em; color:#8593a6; font-weight:700; margin-bottom:12px;">{{ g.label }}</div> 4326 <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:10px;"> 4327 <sc-for list="{{ g.cards }}" as="d" hint-placeholder-count="4"> 4328 <div style="background:#f6f9fc; border-radius:12px; padding:11px 13px;"><div style="font-size:10.5px; color:#8593a6; letter-spacing:0.06em;">{{ d.label }}</div><div style="font-size:15px; font-weight:700; color:#112033; margin-top:2px;">{{ d.value }}</div></div> 4329 </sc-for> 4330 </div> 4331 </div> 4332 </sc-for> 4333 <div data-reveal style="background:#fff; border-radius:18px; padding:18px 20px; box-shadow:0 8px 26px rgba(0,30,60,0.07);"> 4334 <div style="font-size:12px; letter-spacing:0.12em; color:#8593a6; font-weight:700; margin-bottom:10px;">OBSERVACIONES</div> 4335 <p style="margin:0; font-size:14px; color:#3d4c5f; line-height:1.55;">{{ lotObs }}</p> 4336 </div> 4337 </div> 4338 <!-- bid panel --> 4339 <div style="display:flex; flex-direction:column; gap:12px; position:{{ bidPanelPos }}; top:76px;"> 4340 <div style="background:#fff; border-radius:16px; padding:14px 16px; box-shadow:0 8px 26px rgba(0,30,60,0.07); display:flex; align-items:center; gap:12px;"> 4341 <span style="width:42px; height:42px; flex:0 0 42px; border-radius:12px; background:{{ lotPublisherAvatarBg }}; color:#fff; display:grid; place-items:center; box-sizing:border-box; padding:5px; font-size:13px; font-weight:900; overflow:hidden;"> 4342 <sc-if value="{{ lotPublisherHasLogo }}" hint-placeholder-val="false"><img data-dc-attr-src="{{ lotPublisherLogo }}" alt="{{ lotPublisherName }}" style="display:block; width:100%; height:100%; object-fit:contain;"></sc-if> 4343 <sc-if value="{{ lotPublisherNoLogo }}" hint-placeholder-val="true">{{ lotPublisherInitials }}</sc-if> 4344 </span> 4345 <div style="min-width:0;"><div style="font-size:11px; color:#8593a6;">
4345Publicado por</div><strong style="font-size:14px; color:#112033;">{{ lotPublisherName }}</strong></div> 4346 <div style="margin-left:auto; text-align:right;"><div style="font-size:11px; color:#8593a6;">Vistas</div><strong style="font-size:15px; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ lotViews }}</strong></div> 4347 </div> 4348 <div class="roslot-bid-card" style="background:linear-gradient(160deg, #001d3d, #0a3e78); border-radius:20px; padding:22px; color:#fff; box-shadow:0 22px 60px rgba(0,30,60,0.3);"> 4349 <div style="font-size:11.5px; letter-spacing:0.12em; color:#9fc0e2; font-weight:700;">{{ bidPriceTitle }}</div> 4350 <div style="font-size:36px; font-weight:900; letter-spacing:-0.02em; margin:6px 0 2px; font-variant-numeric:tabular-nums; display:flex; align-items:baseline; gap:4px;">{{ currentBidRoll }}</div> 4351 <div style="font-size:12px; color:#9fc0e2;">{{ bidPriceSubtitle }}</div> 4352 <div style="height:1px; background:rgba(255,255,255,0.14); margin:16px 0;"></div> 4353 <sc-if value="{{ bidControlsVisible }}" hint-placeholder-val="true"> 4354 <div style="display:flex; gap:10px; align-items:stretch; opacity:{{ bidControlsOpacity }}; pointer-events:{{ bidControlsPointer }};"> 4355 <div style="display:flex; flex-direction:column; gap:6px;"> 4356 <button onClick="{{ bidPlus }}" style="width:44px; flex:1; background:{{ plusBg }}; color:#fff; border:1px solid rgba(255,255,255,0.25); border-radius:10px; font-size:19px; font-weight:800; font-family:inherit; cursor:pointer; transition:transform 140ms;" style-hover="transform:scale(1.06);" style-active="transform:scale(0.94);"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></button> 4357 <button onClick="{{ bidMinus }}" aria-label="Disminuir oferta" style="width:44px; flex:1; background:{{ minusBg }}; color:#fff; border:1px solid rgba(255,255,255,0.25); border-radius:10px; font-size:19px; font-weight:800; font-family:inherit; cursor:pointer; transition:transform 140ms;" style-hover="transform:scale(1.06);" style-active="transform:scale(0.94);"><i class="dc-lucide dc-lucide-minus" aria-hidden="true"></i></button> 4358 </div> 4359 <div style="flex:1; display:flex; flex-direction:column; gap:6px;"> 4360 <sc-for list="{{ bidIncrements }}" as="bi" hint-placeholder-count="2"> 4361 <button onClick="{{ bi.pick }}" style="flex:1; background:{{ bi.bg }}; color:{{ bi.color }}; border:1px solid {{ bi.border }}; border-radius:10px; font-size:15px; font-weight:800; font-family:inherit; cursor:pointer; font-variant-numeric:tabular-nums; transition:all 160ms;" style-hover="border-color:#ffffff;">{{ bi.label }}</button> 4362 </sc-for> 4363 </div> 4364 </div> 4365 <div style="margin-top:16px; background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.16); border-radius:14px; padding:13px 16px; display:flex; justify-content:space-between; align-items:center;"> 4366 <div><div style="font-size:11px; color:#9fc0e2;">{{ bidOffsetLabel }}</div><div style="font-size:22px; font-weight:900; font-variant-numeric:tabular-nums; display:flex; align-items:baseline; gap:3px;">{{ bidTargetRoll }}</div></div> 4367 <span style="font-size:11px; color:#9fc0e2; text-align:right;">{{ bidPriceUnitLabel }}</span> 4368 </div> 4369 <div style="margin-top:9px;padding:11px 13px;border:1px solid rgba(117,211,194,.28);border-radius:13px;background:rgba(31,122,99,.16);"> 4370 <div style="margin-bottom:8px;font-size:9.5px;letter-spacing:.1em;color:#a9e7d9;font-weight:900;">ESTIMACIÃN DE TU OFERTA</div> 4371 <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:11px;color:#dceaf5;"><span>Costo / bulto</span><strong style="display:flex;align-items:baseline;justify-content:flex-end;font-size:13px;line-height:1.15;color:#fff;font-variant-numeric:tabular-nums;white-space:nowrap;">{{ bidEstimatedUnitCost }}</strong></div> 4372 <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:6px;padding-top:6px;border-top:1px solid rgba(255,255,255,.1);font-size:11px;color:#dceaf5;"><span>Costo lote completo</span><strong style="display:flex;align-items:baseline;justify-content:flex-end;font-size:13px;line-height:1.15;color:#fff;font-variant-numeric:tabular-nums;white-space:nowrap;">{{ bidEstimatedTotalCost }}</strong></div> 4373 <div style="margin-top:7px;font-size:9px;line-height:1.3;color:#a9bfd6;">{{ bidEstimatedNote }}</div> 4374 </div> 4375 </sc-if> 4376 <sc-if value="{{ showSellerHammerPanel }}" hint-placeholder-val="false"><div style="padding:14px 15px;border:1px solid rgba(255,201,99,.38);border-radius:14px;background:rgba(184,109,0,.18);"><strong style="display:flex;align-items:center;gap:7px;color:#ffe0a2;font-size:14px;"><img src="assets/icons/lucide/gavel.svg" alt="" aria-hidden="true" style="width:16px;height:16px;filter:brightness(0) invert(1);">Cierre de subasta</strong><span style="display:block;margin-top:6px;color:#dceaf5;font-size:11.5px;line-height:1.45;">{{ sellerHammerMessage }}</span></div></sc-if> 4377 <sc-if value="{{ showWinningNotice }}" hint-placeholder-val="false"><div style="padding:14px 15px;border:1px solid rgba(117,211,194,.38);border-radius:14px;background:rgba(31,122,99,.22);"><strong style="display:flex;align-items:center;gap:7px;color:#c6f5e6;font-size:15px;"><img src="assets/icons/lucide/circle-check-big.svg" alt="" aria-hidden="true" style="width:17px;height:17px;filter:brightness(0) invert(1);">¡Vas ganando!</strong><span style="display:block;margin-top:6px;color:#dceaf5;font-size:11.5px;line-height:1.45;">Tu oferta sigue siendo la lÃder. No necesitás ni podés volver a ofertar mientras mantengas esta posición.</span></div></sc-if> 4378 <button onClick="{{ bidPrimaryAction }}" disabled="{{ bidDisabled }}" style="width:100%; margin-top:14px; display:flex; align-items:center; justify-content:center; gap:8px; background:{{ bidBtnBg }}; color:#fff; border:none; border-radius:14px; padding:16px; font-size:16px; font-weight:800; font-family:inherit; cursor:{{ bidBtnCursor }}; opacity:{{ bidBtnOpacity }}; letter-spacing:0.01em; transition:transform 160ms; animation:{{ bidBtnAnim }};" style-hover="transform:translateY(-2px);" style-active="transform:scale(0.97);"><sc-if value="{{ bidShowsHammer }}" hint-placeholder-val="false"><img src="assets/icons/lucide/gavel.svg" alt="" aria-hidden="true" style="width:17px;height:17px;filter:brightness(0) invert(1);"></sc-if>{{ bidBtnLabel }}</button> 4379 <sc-if value="{{ bidMessage }}" hint-placeholder-val="{{ false }}"> 4380 <div style="margin-top:10px; font-size:12.5px; text-align:center; padding:9px 12px; border-radius:10px; background:{{ bidMsgBg }}; color:{{ bidMsgColor }};">{{ bidMessage }}</div> 4381 </sc-if> 4382 <sc-if value="{{ lotBidSuccess }}" hint-placeholder-val="false">
4382<div class="roslot-op-success is-detail" role="status" aria-live="assertive" aria-label="Oferta confirmada"><dotlottie-wc class="roslot-op-success-lottie" src="assets/auth/done.lottie" autoplay speed="1.45" aria-hidden="true"></dotlottie-wc><span class="roslot-op-success-copy"><small>OFERTA CONFIRMADA</small><strong>{{ lotBidSuccessAmount }}</strong><span>Lote {{ lotCert }}</span></span></div></sc-if> 4383 </div> 4384 <sc-if value="{{ lotBidsHas }}" hint-placeholder-val="{{ false }}"> 4385 <div style="background:#fff; border-radius:16px; padding:16px 18px; box-shadow:0 8px 26px rgba(0,30,60,0.07);"> 4386 <div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:10px;"> 4387 <div style="font-size:12px; letter-spacing:0.12em; color:#8593a6; font-weight:700;">OFERTAS RECIBIDAS</div> 4388 <span style="font-size:12px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ lotBidsCount }}</span> 4389 </div> 4390 <div style="display:flex; flex-direction:column;"> 4391 <sc-for list="{{ lotBidRows }}" as="b" hint-placeholder-count="3"> 4392 <div style="display:flex; align-items:center; gap:10px; padding:9px 0; border-top:1px solid #f0f4f9;"> 4393 <div style="flex:1; min-width:0;"><div style="font-size:13.5px; font-weight:700; color:#112033; font-variant-numeric:tabular-nums;">{{ b.amount }}</div><div style="font-size:11px; color:#8593a6;">{{ b.bidder }} · {{ b.date }}</div></div> 4394 <span style="font-size:10.5px; font-weight:800; padding:4px 10px; border-radius:100px; background:{{ b.statusBg }}; color:{{ b.statusColor }};">{{ b.status }}</span> 4395 </div> 4396 </sc-for> 4397 </div> 4398 </div> 4399 </sc-if> 4400 <sc-if value="{{ lotBidsLoading }}" hint-placeholder-val="{{ false }}"> 4401 <div style="background:#fff; border-radius:16px; padding:20px; box-shadow:0 8px 26px rgba(0,30,60,0.07); text-align:center; color:#8593a6; font-size:12.5px;">Cargando ofertasâ¦</div> 4402 </sc-if> 4403 </div> 4404 </div> 4405 <div style="height:40px;"></div> 4406 </div> 4407 </sc-if> 4408 4409 <!-- ============ CARGAR LOTE ROSGAN ============ --> 4410 <sc-if value="{{ isRosganLotLoad }}" hint-placeholder-val="{{ false }}"> 4411 <style> 4412 .rg-wizard-panel section{background:transparent!important;border:0!important;border-radius:0!important;padding:0!important;box-shadow:none!important} 4413 .rg-wizard-panel,.rg-wizard-panel *{box-sizing:border-box} 4414 .rg-wizard-layout,.rg-wizard-layout>*{min-width:0} 4415 .rg-wizard-panel>div,.rg-wizard-panel section,.rg-provenance-grid,.rg-provenance-grid>*{min-width:0;max-width:100%} 4416 .rg-wizard-panel label{font-size:12.5px!important;font-weight:600!important;color:#3d4c5f!important;letter-spacing:0!important} 4417 .rg-wizard-panel input,.rg-wizard-panel select,.rg-wizard-panel textarea{display:block;width:100%;min-width:0;max-width:100%;font-family:inherit!important;font-size:14px!important;outline:none!important} 4418 .rg-wizard-panel select{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} 4419 .rg-wizard-panel button{max-width:100%} 4420 .rg-field-label-row{min-width:0;flex-wrap:wrap}.rg-field-label-row button{flex:0 0 auto;white-space:nowrap} 4421 .rg-wizard-panel input:focus,.rg-wizard-panel select:focus,.rg-wizard-panel textarea:focus{border-color:#0a5ca8!important;box-shadow:0 0 0 3px rgba(10,92,168,.08)} 4422 .rg-wizard-panel section.rg-specific-panel{background:#f6f9fc!important;color:#112033!important;border:1px solid #e3eaf2!important;border-radius:14px!important;padding:18px!important;box-shadow:none!important;margin-top:18px} 4423 .rg-specific-panel input,.rg-specific-panel select{background:#fff!important;color:#112033!important;border:1px solid #c4d3e3!important;min-width:0;width:100%;box-sizing:border-box;color-scheme:light} 4424 .rg-specific-panel select{appearance:auto;cursor:pointer;font:500 13px inherit} 4425 .rg-specific-panel select option{background:#fff;color:#112033}
4426 .rg-specific-panel input:focus-visible,.rg-specific-panel select:focus-visible{outline:2px solid #0a5ca8;outline-offset:2px} 4427 .rg-specific-panel>div:first-child{color:#0a5ca8!important}.rg-specific-panel h2{color:#112033!important} 4428 .rg-characteristics-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px 14px;align-items:start} 4429 .rg-characteristics-grid>*{min-width:0} 4430 .rg-characteristics-grid label,.rg-age-fields label{min-width:0;line-height:1.25;overflow-wrap:anywhere} 4431 .rg-characteristics-grid input,.rg-characteristics-grid select{display:block;width:100%;min-width:0} 4432 .rg-age-fields{grid-column:span 2;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.78fr);gap:10px;min-width:0} 4433 .rg-cons-picker{position:relative;display:grid;grid-template-columns:minmax(0,1fr);gap:6px;min-width:0;max-width:100%;overflow:visible} 4434 .rg-cons-picker-label{font-size:11px;font-weight:800;color:#60758c;letter-spacing:.05em} 4435 .rg-cons-picker-button{width:100%;min-width:0;max-width:100%;min-height:46px;display:flex;align-items:center;gap:10px;overflow:hidden;border:1px solid #ccdae8;border-radius:12px;background:#fff;color:#102f50;padding:6px 11px;text-align:left;font:600 13px inherit;cursor:pointer;transition:border-color .16s,box-shadow .16s} 4436 .rg-cons-picker-button:hover,.rg-cons-picker-button:focus{border-color:#0a5ca8;box-shadow:0 0 0 3px rgba(10,92,168,.08)} 4437 .rg-cons-picker-logo{width:31px;height:31px;flex:0 0 31px;display:grid;place-items:center;overflow:hidden;border:1px solid #e1e8f0;border-radius:9px;background:#fff;color:#0a5ca8;font-size:10px;font-weight:900} 4438 .rg-cons-picker-logo img{display:block;width:100%;height:100%;object-fit:contain;padding:3px;box-sizing:border-box} 4439 .rg-cons-picker-name{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} 4440 .rg-cons-picker-menu{position:absolute;top:calc(100% + 7px);left:0;right:0;z-index:45;display:grid;gap:4px;width:100%;max-width:100%;max-height:min(310px,55vh);overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;padding:7px;border:1px solid #d5e1ed;border-radius:14px;background:#fff;box-shadow:0 18px 44px rgba(0,36,78,.2)} 4441 .rg-cons-picker-option{width:100%;min-width:0;max-width:100%;min-height:46px;display:flex;align-items:center;gap:10px;overflow:hidden;border:1px solid transparent;border-radius:10px;background:transparent;color:#102f50;padding:6px 8px;text-align:left;font:650 12.5px inherit;cursor:pointer} 4442 .rg-cons-picker-option:hover{background:#f0f6fb}.rg-cons-picker-option.is-selected{border-color:#b9d5ee;background:#eaf4fc;color:#07518f} 4443 .rg-provenance-grid{grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:22px 16px!important;align-items:start} 4444 .rg-provenance-field{display:grid;align-content:start;gap:7px;min-width:0} 4445 .rg-provenance-field>label,.rg-provenance-field>.rg-field-label-row,.rg-cons-picker-label{color:#536b83!important;font-size:11px!important;font-weight:850!important;letter-spacing:.07em!important;line-height:1.3;text-transform:uppercase} 4446 .rg-provenance-field>label:has(select){display:grid;align-content:start;gap:7px} 4447 .rg-provenance-field select,.rg-provenance-field input,.rg-cons-picker-button{height:48px;min-height:48px!important} 4448 .rg-provenance-help{min-height:15px;color:#718296;font-size:11px;line-height:1.35;font-weight:500;letter-spacing:0;text-transform:none} 4449 .rg-provenance-section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:21px} 4450 .rg-provenance-section-head>div{display:flex;align-items:center;gap:12px;min-width:0} 4451 .rg-provenance-section-head button{flex:none} 4452 .rg-provenance-action{display:inline-flex;align-items:center;gap:5px;border:0;background:transparent;color:#0a5ca8;padding:0;font:800 11px inherit;cursor:pointer;white-space:nowrap} 4453 .rg-provenance-action:disabled{cursor:not-allowed} 4454 .rg-create-button{min-height:38px;padding:8px 12px;background:#e9f3ff;border:1px solid #bad5f0;border-radius:10px;font-size:12px} 4455 .rg-inline-create{scroll-margin-top:110px;animation:pageIn 220ms both} 4456 .rg-provenance-field:has(.rg-inline-create){grid-column:1/-1} 4457 @media(max-width:700px){.rg-create-button{min-height:46px;padding:11px 14px;background:#0a5ca8;color:#fff;font-size:14px;white-space:normal}.rg-field-label-row{flex-wrap:wrap}.rg-inline-create button{min-height:44px!important}.rg-inline-create .rg-form-grid{grid-template-columns:1fr!important}} 4458 4459 .rg-provenance-optional{grid-column:1/-1;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;padding-top:18px;border-top:1px solid #e7edf4} 4460 .rg-lot-type-choices{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:9px;margin-bottom:15px} 4461 .rg-lot-type-choice{min-width:0;min-height:43px;display:flex;align-items:center;justify-content:center;gap:7px;border:1px solid var(--rg-type-border);border-radius:11px;background:var(--rg-type-bg);color:var(--rg-type-color);padding:8px 12px;font:800 12.5px inherit;cursor:pointer;transition:border-color .16s,background .16s,box-shadow .16s} 4462 .rg-lot-type-choice.is-selected{box-shadow:0 6px 16px rgba(10,92,168,.15)} 4463 .rg-state-toggles{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:17px} 4464 .rg-state-toggle{min-height:42px;display:inline-flex;align-items:center;justify-content:center;gap:9px;border:1px solid;border-radius:12px;padding:8px 14px;font:800 12.5px inherit;cursor:pointer;transition:transform .15s,box-shadow .15s,background .15s,border-color .15s} 4465 .rg-state-toggle:hover{transform:translateY(-1px)} 4466 .rg-state-toggle img{display:block;flex:0 0 auto;object-fit:contain} 4467 .rg-state-toggle-check{width:17px;height:17px;
4467display:grid;place-items:center;border-radius:50%;font-size:11px;font-weight:900} 4468 .rg-description-options{display:flex;flex-wrap:wrap;gap:8px;margin-top:9px} 4469 .rg-description-choice{min-height:36px;display:inline-flex;align-items:center;gap:7px;border:1px solid;border-radius:11px;padding:7px 12px;font:750 11.5px inherit;cursor:pointer;transition:transform .14s,box-shadow .14s,background .14s,border-color .14s} 4470 .rg-payment-chip{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:40px;padding:9px 16px;border:1px solid #d4e2f0;border-radius:999px;background:#f5f8fc;color:#47617d;font-family:inherit;font-size:13px;font-weight:650;line-height:1;white-space:nowrap;cursor:pointer;transition:background .15s,border-color .15s,color .15s,box-shadow .15s} 4471 .rg-payment-chip:hover{background:#eaf2fb;border-color:#95bce2} 4472 .rg-payment-chip[aria-pressed="true"]{background:#0a5ca8;border-color:#0a5ca8;color:#fff;box-shadow:0 3px 8px #0a5ca824} 4473 .rg-payment-chip:focus-visible{outline:2px solid #0a5ca8;outline-offset:3px} 4474 .rg-payment-chip-mark{display:none;font-size:13px}.rg-payment-chip[aria-pressed="true"] .rg-payment-chip-mark{display:inline} 4475 .rg-description-choice:hover{transform:translateY(-1px);box-shadow:0 5px 12px rgba(15,58,96,.09)} 4476 .rg-description-choice-mark{width:17px;height:17px;display:grid;place-items:center;flex:0 0 17px;border-radius:5px;font-size:11px;font-weight:900} 4477 @media(max-width:1100px){.rg-wizard-layout{grid-template-columns:minmax(0,1fr)!important}.rg-preview-panel{position:static!important;width:min(420px,100%);justify-self:center}.rg-characteristics-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.rg-age-fields{grid-column:1/-1}} 4478 @media(max-width:720px){.rg-form-grid,.rg-provenance-grid,.rg-provenance-optional{grid-template-columns:1fr!important}.rg-provenance-optional{grid-column:auto}.rg-provenance-section-head{align-items:flex-start}.rg-step-copy{display:none!important}.rg-characteristics-grid{grid-template-columns:1fr}.rg-age-fields{grid-column:auto;grid-template-columns:minmax(0,1fr) minmax(0,.8fr)}.rg-wizard-panel{padding:18px 14px!important}.rg-wizard-footer{flex-wrap:wrap}.rg-wizard-footer button{flex:1 1 140px;min-width:0!important;padding-left:14px!important;padding-right:14px!important}} 4479 @media(max-width:420px){.rg-age-fields{grid-template-columns:1fr}.rg-wizard-panel input,.rg-wizard-panel select,.rg-wizard-panel textarea{font-size:16px!important}.rg-cons-picker-menu{max-height:48vh}.rg-wizard-footer button{flex-basis:100%}.rg-lot-type-choices{grid-template-columns:1fr}.rg-lot-type-choice{min-height:40px}} 4480 </style> 4481 <div style="min-height:calc(100vh - 72px); background:#f4f7fb; padding:0 clamp(14px,4vw,36px) 52px;"> 4482 <div style="max-width:1240px; margin:0 auto; animation:pageIn 360ms ease both;"> 4483 <div style="display:flex; align-items:flex-end; justify-content:space-between; gap:18px; flex-wrap:wrap; padding:24px 0 20px;"> 4484 <div> 4485 <span onClick="{{ goRemates }}" style="display:inline-flex; align-items:center; gap:6px; color:#0a5ca8; font-size:13px; font-weight:700; cursor:pointer;"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i> Volver a remates</span> 4486 <h1 style="font-size:clamp(23px,3.2vw,32px); font-weight:800; letter-spacing:-.02em; margin:8px 0 2px; color:#112033;">{{ rgWizardTitle }}</h1> 4487 <a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guÃa en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">â</span></a> 4488 <p style="margin:0; color:#65758a; font-size:13.5px;">{{ rgWizardSubtitle }}</p> 4489 </div> 4490 <div style="display:flex; align-items:center; gap:14px; flex-wrap:wrap;"> 4491 <sc-if value="{{ rgCanViewPanel }}" hint-placeholder-val="{{ false }}"><button type="button" onClick="{{ openRgPanel }}" disabled="{{ rgPanelDisabled }}" title="{{ rgPanelHint }}" style="display:inline-flex; align-items:center; gap:8px; padding:11px 17px; border:1px solid #0a5ca8; border-radius:100px; background:#0a5ca8; color:#fff; font:700 13px inherit; cursor:pointer; opacity:{{ rgPanelOpacity }};"><img src="/assets/icons/lucide/clipboard-list.svg" width="17" height="17" alt="" aria-hidden="true" style="display:block; flex-shrink:0; filter:brightness(0) invert(1);">{{ rgPanelLabel }}</button></sc-if>
4492 <span onClick="{{ resetRosganLot }}" style="font-size:13px; color:#e63946; font-weight:700; cursor:pointer;">Limpiar formulario</span> 4493 </div> 4494 </div> 4495 4496 <sc-if value="{{ rosganLotMessageVisible }}" hint-placeholder-val="{{ false }}"><div style="margin-bottom:16px; padding:13px 16px; border-radius:13px; background:{{ rosganLotMessageBg }}; color:{{ rosganLotMessageColor }}; font-size:13.5px; font-weight:700;">{{ rosganLotMessage }}</div></sc-if> 4497 4498 <sc-if value="{{ rosganLotLoading }}" hint-placeholder-val="{{ false }}"> 4499 <div style="display:grid; gap:14px;"> 4500 <div style="height:170px; border-radius:22px; background:linear-gradient(90deg,#e5edf5 25%,#f9fbfd 50%,#e5edf5 75%); background-size:900px 100%; animation:shimmer 1.2s linear infinite;"></div> 4501 <div style="height:330px; border-radius:22px; background:linear-gradient(90deg,#e5edf5 25%,#f9fbfd 50%,#e5edf5 75%); background-size:900px 100%; animation:shimmer 1.2s linear infinite;"></div> 4502 </div> 4503 </sc-if> 4504 4505 <sc-if value="{{ rosganLotReady }}" hint-placeholder-val="{{ true }}"> 4506 <div style="display:flex; gap:8px; margin-bottom:20px;"> 4507 <sc-for list="{{ rgWizardSteps }}" as="w" hint-placeholder-count="3"><div class="rg-wizard-step" onClick="{{ w.go }}" style="flex:1; min-width:0; cursor:pointer;"><div style="height:5px; border-radius:100px; background:{{ w.barBg }}; transition:background 260ms;"></div><div style="display:flex; align-items:center; gap:8px; margin-top:9px;"><span style="width:24px; height:24px; border-radius:50%; display:grid; place-items:center; font-size:12px; font-weight:800; flex-shrink:0; background:{{ w.dotBg }}; color:{{ w.dotColor }}; border:1px solid {{ w.dotBorder }};"><sc-if value="{{ w.done }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if><sc-if value="{{ w.numberVisible }}" hint-placeholder-val="{{ true }}">{{ w.number }}</sc-if></span><div class="rg-step-copy" style="min-width:0;"><div style="font-size:13px; font-weight:700; color:{{ w.titleColor }}; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ w.title }}</div><div style="font-size:11px; color:#8593a6; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ w.text }}</div></div></div></div></sc-for> 4508 </div> 4509 <div class="rg-wizard-layout" style="display:grid; grid-template-columns:{{ rgLayoutCols }}; gap:20px; align-items:start;"> 4510 <div class="rg-wizard-panel" style="background:#fff; border-radius:20px; box-shadow:0 12px 40px rgba(0,30,60,.1); padding:clamp(20px,3vw,30px); min-height:470px; display:flex; flex-direction:column;"> 4511 <div key="{{ rgStepKey }}" style="animation:pageIn 300ms cubic-bezier(.22,.72,.18,1) both; flex:1;"> 4512 <sc-if value="{{ rgIsStep0 }}" hint-placeholder-val="{{ true }}"> 4513 <section> 4514 <div class="rg-provenance-section-head"><div><span style="width:34px; height:34px; flex:none; border-radius:11px; background:#e6f1fb; color:#0a5ca8; display:grid; place-items:center; font-size:14px; font-weight:900;">1</span><div><h2 style="margin:0; color:#102f50; font-size:18px;">Remate y procedencia</h2><p style="margin:2px 0 0; color:#718296; font-size:12.5px;">Definà dónde se publica y a quién pertenece el lote.</p></div></div></div> 4515 <div class="rg-provenance-grid" style="display:grid;"> 4516 <div class="rg-provenance-field"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c; letter-spacing:.05em;">REMATE * 4517 <select value="" data-dc-attr-value="{{ rgRemateId }}" onChange="{{ onRgRemate }}" disabled="{{ rgEditingLot }}" style="width:100%; min-height:46px; border:1px solid #ccdae8; border-radius:12px; background:#fff; color:#102f50; padding:0 13px; font:600 13px inherit; outline:none;"><option value="">Seleccionar remateâ¦</option><sc-for list="{{ rosganLotRemates }}" as="r" hint-placeholder-count="2"><option value="" data-dc-attr-value="{{ r.id }}">{{ r.name }} · {{ r.meta }}</option></sc-for></select> 4518 </label></div> 4519 <div class="rg-cons-picker rg-provenance-field">
4520 <span class="rg-cons-picker-label">CONSIGNATARIA *</span> 4521 <button class="rg-cons-picker-button" disabled="{{ rgConsignatariaDisabled }}" type="button" onClick="{{ toggleRgConsignatariaMenu }}" aria-label="Seleccionar consignataria" aria-expanded="{{ rgConsignatariaMenuExpanded }}"> 4522 <span class="rg-cons-picker-logo"> 4523 <sc-if value="{{ rgSelectedConsignatariaHasLogo }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ rgSelectedConsignatariaLogo }}" alt=""></sc-if> 4524 <sc-if value="{{ rgSelectedConsignatariaShowsInitials }}" hint-placeholder-val="{{ true }}">{{ rgSelectedConsignatariaInitials }}</sc-if> 4525 </span> 4526 <span class="rg-cons-picker-name">{{ rgSelectedConsignatariaName }}</span> 4527 <span aria-hidden="true" style="color:#6f8297;font-size:13px;transform:rotate({{ rgConsignatariaChevronRotation }});transition:transform .16s;"><i class="dc-lucide dc-lucide-chevron-down"></i></span> 4528 </button> 4529 <sc-if value="{{ rgConsignatariaMenuOpen }}" hint-placeholder-val="{{ false }}"> 4530 <div class="rg-cons-picker-menu" role="listbox" aria-label="Consignatarias"> 4531 <sc-for list="{{ rosganLotConsignatarias }}" as="c" hint-placeholder-count="3"> 4532 <button class="{{ c.className }}" type="button" onClick="{{ c.pick }}" role="option" aria-selected="{{ c.selected }}"> 4533 <span class="rg-cons-picker-logo"><sc-if value="{{ c.hasLogo }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ c.logo }}" alt=""></sc-if><sc-if value="{{ c.showsInitials }}" hint-placeholder-val="{{ true }}">{{ c.initials }}</sc-if></span> 4534 <span class="rg-cons-picker-name">{{ c.name }}</span> 4535 <sc-if value="{{ c.selected }}" hint-placeholder-val="{{ false }}"><span aria-hidden="true" style="color:#1f7a63;font-size:16px;font-weight:900;"><i class="dc-lucide dc-lucide-check"></i></span></sc-if> 4536 </button> 4537 </sc-for> 4538 </div> 4539 </sc-if> 4540 </div> 4541 <div class="rg-provenance-field"> 4542 <span class="rg-field-label-row" style="display:flex;align-items:center;justify-content:space-between;gap:8px;color:#60758c;font-size:11px;font-weight:800;letter-spacing:.05em;">COMITENTE *<sc-if value="{{ rgCanCreateComitente }}" hint-placeholder-val="false"><button class="rg-provenance-action rg-create-button" type="button" onClick="{{ toggleRgComitenteForm }}" disabled="{{ rgComitenteCreateDisabled }}" style="opacity:{{ rgComitenteCreateOpacity }};"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Crear comitente</button></sc-if></span> 4543 <div data-rg-comitente-picker style="position:relative; min-width:0;"> 4544 <input id="rg-comitente-search" type="search" value="" data-dc-attr-value="{{ rgComitenteSearch }}" onInput="{{ onRgComitenteSearch }}" onFocus="{{ openRgComitenteMenu }}" onBlur="{{ closeRgComitenteMenu }}" aria-label="Buscar comitente" aria-describedby="confidential-comitente-search" disabled="{{ rgComitenteSearchDisabled }}" autocomplete="off" placeholder="Buscar por alias, razón social o CUIT" style="width:100%; min-height:46px; border:1px solid #ccdae8; border-radius:12px; background:#fff; color:#102f50; padding:0 38px 0 13px; font:600 13px inherit; outline:none;"> 4545 <span aria-hidden="true" style="position:absolute; right:13px; top:14px; color:#718296; pointer-events:none;"><i class="dc-lucide dc-lucide-search" aria-hidden="true"></i></span> 4546 <sc-if value="{{ rgComitenteMenuOpen }}" hint-placeholder-val="{{ false }}"> 4547 <div role="listbox" aria-label="Comitentes" style="position:absolute; z-index:35; left:0; right:0; top:calc(100% + 6px); max-height:230px; overflow:auto; padding:6px; border:1px solid #d4e0ec; border-radius:12px; background:#fff; box-shadow:0 16px 34px rgba(0,38,78,.18);"> 4548 <sc-for list="{{ rgFilteredComitentes }}" as="c" hint-placeholder-count="4"><button type="button" role="option" aria-selected="{{ c.selected }}" onMouseDown="{{ keepRgComitenteMenu }}" onBlur="{{ closeRgComitenteMenu }}" onClick="{{ c.pick }}" style="width:100%; display:grid; gap:2px; border:0; border-radius:9px; background:{{ c.bg }}; color:#102f50; padding:9px 10px; text-align:left; cursor:pointer;"><span style="display:flex;align-items:center;gap:8px;"><strong style="font-size:12.5px;">{{ c.name }}</strong></span><sc-if value="{{ c.hasMeta }}" hint-placeholder-val="{{ false }}"><span style="font-size:10.5px; color:#718296;">{{ c.meta }}</span></sc-if></button></sc-for> 4549 <sc-if value="{{ rgComitenteHasMore }}"><button type="button" class="rg-est-more" onMouseDown="{{ keepRgComitenteMenu }}" onBlur="{{ closeRgComitenteMenu }}" onClick="{{ moreRgComitentes }}">Mostrar más alias</button></sc-if> 4550 <sc-if value="{{ rgComitenteNoResults }}" hint-placeholder-val="{{ false }}"><div style="padding:12px 10px; color:#718296; font-size:12px; text-align:center;">No encontramos comitentes con ese nombre.</div></sc-if> 4551 </div> 4552 </sc-if> 4553 </div> 4554 <small id="confidential-comitente-search" style="display:inline-flex;align-items:flex-start;gap:7px;box-sizing:border-box;max-width:100%;width:fit-content;margin-top:6px;padding:7px 10px;border:1px solid #f0cf86;border-radius:9px;background:#fff7e6;color:#805411;font-size:11px;font-weight:600;line-height:1.45;letter-spacing:normal;text-transform:none;white-space:normal;text-align:left;"><span aria-hidden="true" style="flex:none;font-size:15px;line-height:1.1;color:#a56b08;">⚠</span><span>Dato confidencial perteneciente a la firma consignataria</span></small> 4555 <sc-if value="{{ rgCanCreateComitente }}" hint-placeholder-val="false">
4555<button class="rg-provenance-action" type="button" onClick="{{ openRgLegacyComitentes }}" disabled="{{ rgComitenteCreateDisabled }}" style="margin-top:6px;min-height:32px;">Completar datos fiscales (opcional)</button></sc-if><sc-if value="{{ rgCanEditComitenteAlias }}"><button type="button" class="rg-provenance-action" onClick="{{ editRgComitenteAlias }}" style="margin-top:6px;min-height:32px;">Editar alias</button></sc-if> 4556 <sc-if value="{{ rgCanCreateComitente }}"><button type="button" class="rg-provenance-action" onClick="{{ openRgComitenteMerge }}" disabled="{{ rgComitenteCreateDisabled }}" style="margin-top:6px;min-height:32px;">Fusionar comitentes</button></sc-if> 4557 <sc-if value="{{ rgComitenteFormOpen }}" hint-placeholder-val="{{ false }}"> 4558 <div id="rg-inline-comitente" class="rg-inline-create" style="margin-top:16px; padding:17px 18px; border:1px solid #dbe7f2; border-radius:14px; background:#f6f9fc;"> 4559 <div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:13px;"><div><div style="color:#102f50; font-size:15px; font-weight:850;">Nuevo comitente</div><div style="margin-top:3px; color:#718296; font-size:11.5px;">Primero creá el comitente; después vas a asociarle un establecimiento.</div></div><button type="button" onClick="{{ toggleRgComitenteForm }}" aria-label="Cerrar alta de comitente" style="width:30px; height:30px; border:1px solid #d5e1ec; border-radius:9px; background:#fff; color:#65758a; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></div> 4560 <div class="rg-form-grid" style="display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px;"> 4561 <label style="display:grid;gap:6px;color:#60758c;font-size:12px;font-weight:700;">ALIAS *<input required type="text" value="" data-dc-attr-value="{{ rgComitenteAlias }}" onInput="{{ onRgComitenteAlias }}" placeholder="Ej.: Estancia El Ombú" style="box-sizing:border-box;min-width:0;width:100%;height:43px;border:1px solid #ccdae8;border-radius:11px;padding:0 12px;font:inherit;background:#fff;"></label> 4562 <label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">CUIT (opcional)<span style="position:relative; display:block;"><input aria-describedby="confidential-rgComitenteCuit" inputmode="numeric" maxlength="11" value="" data-dc-attr-value="{{ rgComitenteCuit }}" onInput="{{ onRgComitenteCuit }}" placeholder="11 dÃgitos" style="box-sizing:border-box; width:100%; height:43px; border:1px solid #ccdae8; border-radius:11px; padding:0 42px 0 12px; background:#fff;"><sc-if value="{{ rgComitenteLookupLoading }}" hint-placeholder-val="false"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true" style="position:absolute; right:13px; top:13px; color:#0a5ca8;"></i></sc-if></span><small id="confidential-rgComitenteCuit" style="display:inline-flex;align-items:flex-start;gap:7px;box-sizing:border-box;max-width:100%;width:fit-content;margin-top:6px;padding:7px 10px;border:1px solid #f0cf86;border-radius:9px;background:#fff7e6;color:#805411;font-size:11px;font-weight:600;line-height:1.45;letter-spacing:normal;text-transform:none;white-space:normal;text-align:left;"><span aria-hidden="true" style="flex:none;font-size:15px;line-height:1.1;color:#a56b08;">⚠</span><span>Dato confidencial perteneciente a la firma consignataria</span></small></label> 4563<label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">RAZÃN SOCIAL (opcional)<input value="" data-dc-attr-value="{{ rgComitenteFiscalName }}" readonly="true" placeholder="Se completa automáticamente desde ARCA" style="height:43px; border:1px solid #ccdae8; border-radius:11px; padding:0 12px; background:#eef2f6;"></label> 4564 <label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">PROVINCIA FISCAL (opcional)<input value="" data-dc-attr-value="{{ rgComitenteProvince }}" readonly="true" placeholder="Se completa desde ARCA" style="height:43px; border:1px solid #ccdae8; border-radius:11px; padding:0 12px; background:#eef2f6;"></label> 4565 <label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">LOCALIDAD FISCAL (opcional)<input value="" data-dc-attr-value="{{ rgComitenteLocality }}" readonly="true" placeholder="Se completa desde ARCA" style="height:43px; border:1px solid #ccdae8; border-radius:11px; padding:0 12px; background:#eef2f6;"></label> 4566 </div> 4567 <sc-if value="{{ rgComitenteMessageVisible }}" hint-placeholder-val="{{ false }}"><div style="margin-top:11px; padding:9px 11px; border-radius:9px; background:{{ rgComitenteMessageBg }}; color:{{ rgComitenteMessageColor }}; font-size:12px; font-weight:700;">{{ rgComitenteMessage }}</div></sc-if> 4568 <div style="display:flex; justify-content:flex-end; gap:9px; margin-top:14px;"><button type="button" onClick="{{ toggleRgComitenteForm }}" disabled="{{ rgComitenteBusy }}" style="min-height:39px; padding:0 16px; border:1px solid #d5e0eb; border-radius:10px; background:#fff; color:#526276; font:750 12px inherit; cursor:pointer;">Cancelar</button><button type="button" onClick="{{ saveRgComitente }}" disabled="{{ rgComitenteSaveDisabled }}" style="min-height:39px; padding:0 18px; border:0; border-radius:10px; background:#1f7a63; color:#fff; font:800 12px inherit; cursor:{{ rgComitenteSaveCursor }}; opacity:{{ rgComitenteSaveOpacity }};">{{ rgComitenteSaveLabel }}</button></div> 4569 </div> 4570 </sc-if> 4571 </div> 4572 <div class="rg-provenance-field">
4573 <span class="rg-field-label-row" style="display:flex; align-items:center; justify-content:space-between; gap:8px; font-size:11px; font-weight:800; color:#60758c; letter-spacing:.05em;">ESTABLECIMIENTO *<sc-if value="{{ rgCanCreateEstablishment }}" hint-placeholder-val="false"><button class="rg-provenance-action rg-create-button" type="button" onClick="{{ toggleRgEstablishmentForm }}" disabled="{{ rgEstablishmentDisabled }}" style="opacity:{{ rgComitenteCreateOpacity }};"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Crear establecimiento</button></sc-if></span> 4574 <div class="rg-est-picker" onKeyDown="{{ onRgEstablishmentPickerKey }}" onBlur="{{ blurRgEstablishmentPicker }}"> 4575 <button class="rg-est-trigger" type="button" aria-label="Seleccionar establecimiento" aria-haspopup="listbox" aria-expanded="{{ rgEstablishmentMenuOpen }}" aria-controls="rg-establishment-options" onClick="{{ toggleRgEstablishmentMenu }}" disabled="{{ rgEstablishmentDisabled }}"> 4576 4577 <span class="rg-est-label">{{ rgEstablishmentButtonLabel }}</span><i class="dc-lucide dc-lucide-chevron-down" aria-hidden="true"></i> 4578 </button> 4579 <sc-if value="{{ rgEstablishmentMenuOpen }}" hint-placeholder-val="false"> 4580 <div class="rg-est-menu"> 4581 <input type="search" class="rg-est-search" aria-label="Buscar establecimiento" placeholder="Buscar nombre o localidadâ¦" value="" data-dc-attr-value="{{ rgEstablishmentSearch }}" onInput="{{ onRgEstablishmentSearch }}" autocomplete="off"> 4582 <div id="rg-establishment-options" role="listbox" aria-label="Establecimientos" class="rg-est-options"> 4583 <sc-for list="{{ rgEstablishmentOptions }}" as="e" hint-placeholder-count="3"><button type="button" class="rg-est-option" data-rg-est-choice="true" role="option" aria-selected="{{ e.selected }}" onClick="{{ e.pick }}"><span class="rg-est-option-text"><strong>{{ e.name }}</strong><small>{{ e.meta }}</small></span></button></sc-for> 4584 </div> 4585 <sc-if value="{{ rgEstablishmentNoMatches }}" hint-placeholder-val="false"><p class="rg-est-count">No encontramos establecimientos.</p></sc-if> 4586 <sc-if value="{{ rgEstablishmentHasMore }}" hint-placeholder-val="false"><button type="button" class="rg-est-more" onClick="{{ moreRgEstablishments }}">Mostrar más establecimientos</button></sc-if> 4587 <p class="rg-est-count">{{ rgEstablishmentCountLabel }}</p> 4588 </div> 4589 </sc-if> 4590 </div> 4591 <sc-if value="{{ rgEstablishmentFormOpen }}" hint-placeholder-val="{{ false }}"> 4592 <div id="rg-inline-establishment" class="rg-inline-create" style="margin-top:16px; padding:17px 18px; border:1px solid #bcd9cf; border-radius:14px; background:#f1faf7;"> 4593 <div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:13px;"><div><div style="color:#102f50; font-size:15px; font-weight:850;">Nuevo establecimiento</div><div style="margin-top:3px; color:#5f766f; font-size:11.5px;">Quedará asociado al comitente seleccionado.</div></div><button type="button" onClick="{{ toggleRgEstablishmentForm }}" aria-label="Cerrar alta de establecimiento" style="width:30px; height:30px; border:1px solid #cbded8; border-radius:9px; background:#fff; color:#65758a; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></div> 4594 <div class="rg-form-grid" style="display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px;"> 4595 <label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">NOMBRE DEL ESTABLECIMIENTO *<input value="" data-dc-attr-value="{{ rgEstablishmentName }}" onInput="{{ onRgEstablishmentName }}" placeholder="Ej. Campo La Prueba" style="height:43px; border:1px solid #c8ddd6; border-radius:11px; padding:0 12px; background:#fff;"></label> 4596 <label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">RENSPA <span style="font-weight:500; color:#8a9aac;">Opcional</span><input value="" data-dc-attr-value="{{ rgEstablishmentRenspa }}" onInput="{{ onRgEstablishmentRenspa }}" placeholder="Podés dejarlo vacÃo" style="height:43px; border:1px solid #c8ddd6; border-radius:11px; padding:0 12px; background:#fff;"></label> 4597 <label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">PROVINCIA *<select value="" data-dc-attr-value="{{ rgEstablishmentProvince }}" onChange="{{ onRgEstablishmentProvince }}" style="height:43px; border:1px solid #c8ddd6; border-radius:11px; padding:0 10px; background:#fff;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgEstablishmentProvinceOptions }}" as="p" hint-placeholder-count="6"><option value="" data-dc-attr-value="{{ p }}">{{ p }}</option></sc-for></select></label> 4598 <label style="position:relative;display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;"><span style="display:flex;align-items:center;justify-content:space-between;"><span>LOCALIDAD *</span><small style="display:inline-flex;align-items:center;gap:4px;color:#718296;font-weight:650;text-transform:none;"><img src="assets/google-logo.webp" alt="" style="width:12px;height:12px;object-fit:contain;">Google</small></span><input value="" data-dc-attr-value="{{ rgEstablishmentLocality }}" onInput="{{ onRgEstablishmentLocality }}" onFocus="{{ openRgEstablishmentPlaces }}" onBlur="{{ closeRgEstablishmentPlaces }}" disabled="{{ rgEstablishmentLocalityDisabled }}" autocomplete="off" placeholder="Escribà una localidad" style="height:43px;border:1px solid #c8ddd6;border-radius:11px;padding:0 38px 0 12px;background:{{ rgEstablishmentLocalityBg }};"><sc-if value="{{ rgEstablishmentPlaceLoading }}" hint-placeholder-val="false"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true" style="position:absolute;right:13px;top:35px;color:#0a5ca8;"></i></sc-if><sc-if value="{{ rgEstablishmentPlacesOpen }}" hint-placeholder-val="false"><div style="position:absolute;z-index:45;left:0;right:0;top:calc(100% + 5px);padding:5px;border:1px solid #cbded8;border-radius:11px;background:#fff;box-shadow:0 14px 30px rgba(0,35,70,.18);max-height:220px;overflow:auto;"><sc-for list="{{ rgEstablishmentPlaces }}" as="p" hint-placeholder-count="4"><button type="button" onClick="{{ p.pick }}" style="width:100%;display:grid;gap:2px;border:0;border-radius:8px;background:#fff;padding:8px 9px;text-align:left;cursor:pointer;"><strong style="font-size:11.5px;color:#153955;">{{ p.label }}</strong><span style="font-size:9.5px;color:#718296;">{{ p.address }}</span></button></sc-for></div></sc-if></label> 4599 </div> 4600 <sc-if value="{{ rgEstablishmentPlaceError }}" hint-placeholder-val="false"><div style="margin-top:9px;color:#8a5a00;font-size:10.5px;">{{ rgEstablishmentPlaceError }}</div></sc-if> 4601 <sc-if value="{{ rgEstablishmentMessageVisible }}" hint-placeholder-val="{{ false }}"><div style="margin-top:11px; padding:9px 11px; border-radius:9px; background:{{ rgEstablishmentMessageBg }}; color:{{ rgEstablishmentMessageColor }}; font-size:12px; font-weight:700;">{{ rgEstablishmentMessage }}</div></sc-if> 4602 <div style="display:flex; justify-content:flex-end; gap:9px; margin-top:14px;"><button type="button" onClick="{{ toggleRgEstablishmentForm }}" disabled="{{ rgEstablishmentSaveDisabled }}" style="min-height:39px; padding:0 16px; border:1px solid #d5e0eb; border-radius:10px; background:#fff; color:#526276; font:750 12px inherit; cursor:pointer;">Cancelar</button><button type="button" onClick="{{ saveRgEstablishment }}" disabled="{{ rgEstablishmentSaveDisabled }}" style="min-height:39px; padding:0 18px; border:0; border-radius:10px; background:#1f7a63; color:#fff; font:800 12px inherit; cursor:pointer;">{{ rgEstablishmentSaveLabel }}</button></div> 4603 </div> 4604 </sc-if> 4605 </div> 4606 <div class="rg-provenance-optional"><div class="rg-provenance-field"><label id="rg-commercial-label">
4606RESPONSABLE COMERCIAL (OPCIONAL)</label> 4607 <div style="position:relative;" onKeyDown="{{ onRgCertifierKey }}"> 4608 <button type="button" aria-labelledby="rg-commercial-label rg-commercial-value" aria-expanded="{{ rgCertifierMenuOpen }}" aria-controls="rg-commercial-options" onClick="{{ toggleRgCertifierMenu }}" disabled="{{ rgCertifierDisabled }}" style="width:100%;min-height:46px;border:1px solid #ccdae8;border-radius:12px;padding:8px 13px;background:#fff;color:#102f50;display:flex;align-items:center;gap:9px;text-align:left;font:13px inherit;cursor:pointer;"> 4609 <sc-if value="{{ rgSelectedCertifier.hasAvatar }}"><span style="display:grid;place-items:center;width:28px;height:28px;border-radius:50%;overflow:hidden;flex:0 0 28px;background:{{ rgSelectedCertifier.bg }};"><img data-dc-attr-src="{{ rgSelectedCertifier.avatar }}" onError="{{ rgSelectedCertifier.avatarError }}" alt="" style="width:{{ rgSelectedCertifier.size }};height:{{ rgSelectedCertifier.size }};object-fit:{{ rgSelectedCertifier.fit }};"></span></sc-if> 4610 <span id="rg-commercial-value" style="flex:1;">{{ rgSelectedCertifier.label }}</span><span aria-hidden="true">â</span> 4611 </button> 4612 <sc-if value="{{ rgCertifierMenuOpen }}"><div id="rg-commercial-options" role="group" aria-labelledby="rg-commercial-label" style="position:absolute;top:calc(100% + 5px);left:0;right:0;z-index:30;max-height:240px;overflow:auto;background:white;border:1px solid #d7e2ed;border-radius:12px;padding:5px;box-shadow:0 12px 30px #002e5f22;"> 4613 <sc-for list="{{ rgCertifierOptions }}" as="certifier"><button type="button" onClick="{{ certifier.pick }}" aria-pressed="{{ certifier.selected }}" style="display:flex;align-items:center;gap:9px;width:100%;border:0;border-radius:8px;padding:9px;background:{{ certifier.rowBg }};color:#102f50;text-align:left;font:13px inherit;cursor:pointer;"> 4614 <span style="display:grid;place-items:center;width:28px;height:28px;border-radius:50%;overflow:hidden;flex:0 0 28px;background:{{ certifier.bg }};"><sc-if value="{{ certifier.hasAvatar }}"><img data-dc-attr-src="{{ certifier.avatar }}" onError="{{ certifier.avatarError }}" alt="" style="width:{{ certifier.size }};height:{{ certifier.size }};object-fit:{{ certifier.fit }};"></sc-if><sc-if value="{{ certifier.noAvatar }}"><span>{{ certifier.initial }}</span></sc-if></span> 4615 <span style="flex:1;">{{ certifier.label }}</span><span aria-hidden="true">{{ certifier.check }}</span> 4616 </button></sc-for> 4617 </div></sc-if> 4618 </div> 4619 <span class="rg-provenance-help" role="status">{{ rgCertifierStatus }}</span></div></div> 4620 </div> 4621 4622 4623 <sc-if value="{{ rgRelationsLoading }}" hint-placeholder-val="{{ false }}"><div style="margin-top:11px; color:#0a5ca8; font-size:12px; font-weight:700;">Cargando comitentes y establecimientosâ¦</div></sc-if> 4624 </section> 4625 </sc-if> 4626 4627 <sc-if value="{{ rgIsStep1 }}" hint-placeholder-val="{{ false }}"> 4628 <section> 4629 <div style="display:flex; align-items:center; gap:12px; margin-bottom:19px;"><span style="width:34px; height:34px; border-radius:11px; background:#e6f4ef; color:#1f7a63; display:grid; place-items:center; font-size:14px; font-weight:900;">2</span><div><h2 style="margin:0; color:#102f50; font-size:18px;">CategorÃa, raza y cantidad</h2><p style="margin:2px 0 0; color:#718296; font-size:12.5px;">La categorÃa activa automáticamente el formulario correspondiente.</p></div></div> 4630 <div class="rg-lot-type-choices" role="group" aria-label="Tipo de lote"> 4631 <sc-for list="{{ rgLotTypeOptions }}" as="t" hint-placeholder-count="3"><button type="button" class="rg-lot-type-choice {{ t.className }}" onClick="{{ t.pick }}" aria-pressed="{{ t.selected }}" style="--rg-type-bg:{{ t.bg }};--rg-type-color:{{ t.color }};--rg-type-border:{{ t.border }};">{{ t.label }}</button></sc-for> 4632 </div> 4633 <div class="rg-form-grid rg-category-quantity-grid" style="display:grid; grid-template-columns:minmax(240px,1.15fr) minmax(210px,.85fr); gap:14px; align-items:start;"> 4634 <div style="display:grid; gap:10px; align-content:start; min-width:0;"> 4635 <label style="display:grid; gap:6px; min-width:0; font-size:11px; font-weight:800; color:#60758c; letter-spacing:.05em;">CATEGORÃA * 4636 <select value="" data-dc-attr-value="{{ rgCategoryId }}" onChange="{{ onRgCategory }}" disabled="{{ rgCategoryDisabled }}" style="width:100%; min-width:0; min-height:46px; border:1px solid #ccdae8; border-radius:12px; background:{{ rgCategoryBg }}; color:#102f50; padding:0 13px; font:600 13px inherit; outline:none;"><option value="">{{ rgCategoryPlaceholder }}</option><sc-for list="{{ rosganLotCategories }}" as="c" hint-placeholder-count="8"><option value="" data-dc-attr-value="{{ c.id }}">{{ c.name }}</option></sc-for></select> 4637 </label> 4638 <sc-if value="{{ rgAllowsTermino }}" hint-placeholder-val="{{ false }}"><div class="rg-state-toggles" style="margin:0;"><button type="button" class="rg-state-toggle" onClick="{{ toggleRgTermino }}" aria-pressed="{{ rgTerminoPressed }}" style="color:{{ rgTerminoColor }};background:{{ rgTerminoBg }};border-color:{{ rgTerminoBorder }};box-shadow:{{ rgTerminoShadow }};"><img src="assets/icons/lucide/clock-3.svg" alt="" aria-hidden="true" width="21" height="21" style="width:21px;height:21px;filter:{{ rgTerminoIconFilter }};opacity:{{ rgTerminoIconOpacity }};"><span>Venta a Término</span><span class="rg-state-toggle-check" aria-hidden="true" style="background:{{ rgTerminoCheckBg }};color:{{ rgTerminoCheckColor }};">{{ rgTerminoCheck }}</span></button></div></sc-if> 4639 </div> 4640 <sc-if value="{{ rgIsTotalQuantity }}" hint-placeholder-val="{{ true }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c; letter-spacing:.05em;">CANTIDAD *<input type="number" min="1" value="" data-dc-attr-value="{{ rgQuantity }}" onInput="{{ onRgQuantity }}" placeholder="Ej. 45" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 13px; font:700 14px inherit; color:#102f50; outline:none;"></label></sc-if> 4641 <sc-if value="{{ rgIsMaleFemale }}" hint-placeholder-val="{{ false }}"><div class="rg-form-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:10px;"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">MACHOS *<input type="number" min="0" value="" data-dc-attr-value="{{ rgMaleQuantity }}" onInput="{{ onRgMaleQuantity }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">HEMBRAS *<input type="number" min="0" value="" data-dc-attr-value="{{ rgFemaleQuantity }}" onInput="{{ onRgFemaleQuantity }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></label></div></sc-if> 4642 <sc-if value="{{ rgIsMothersCalves }}" hint-placeholder-val="{{ false }}"><div class="rg-form-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:10px;"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">MADRES *<input type="number" min="1" value="" data-dc-attr-value="{{ rgMotherQuantity }}" onInput="{{ onRgMotherQuantity }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">CRÃAS *<input type="number" min="1" value="" data-dc-attr-value="{{ rgCalfQuantity }}" onInput="{{ onRgCalfQuantity }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></label></div></sc-if> 4643 </div> 4644 <div style="margin-top:18px; display:grid; gap:10px;"> 4645 <div style="display:flex; align-items:center; justify-content:space-between; gap:10px;"><div style="font-size:12.5px; font-weight:750; color:#3d4c5f;">Razas</div><span style="color:#78899d; font-size:11.5px; font-weight:650;">Máximo 3 · {{ rgBreedsCount }}
4645/3</span></div> 4646 <label style="position:relative; display:block;"><span style="position:absolute; left:14px; top:50%; transform:translateY(-50%); color:#6f8297; pointer-events:none; font-size:15px;"><i class="dc-lucide dc-lucide-search" aria-hidden="true"></i></span><input type="search" aria-label="Buscar raza" value="" data-dc-attr-value="{{ rgBreedSearch }}" onInput="{{ onRgBreedSearch }}" placeholder="Empezá a escribir la razaâ¦" autocomplete="off" style="width:100%; height:46px; border:1px solid #c8d8e8; border-radius:13px; background:#fff; padding:0 42px; color:#102f50; font:650 13px inherit; outline:none; box-shadow:0 3px 10px rgba(14,67,113,.04);"></label> 4647 <sc-if value="{{ rgBreedSearchHint }}" hint-placeholder-val="{{ true }}"><div style="font-size:11.5px; line-height:1.45; color:#7b8da0;">Empezá a escribir para buscar una raza.</div></sc-if> 4648 <sc-if value="{{ rgBreedSearchHasResults }}" hint-placeholder-val="{{ true }}"><div style="display:flex; flex-wrap:wrap; gap:8px;"><sc-for list="{{ rgBreedSearchResults }}" as="b" hint-placeholder-count="5"><button type="button" onClick="{{ b.pick }}" style="display:inline-flex; align-items:center; gap:7px; min-height:38px; padding:2px 13px 2px 7px; border-radius:999px; border:1px solid {{ b.border }}; background:{{ b.bg }}; color:{{ b.color }}; font:750 12px inherit; cursor:pointer; transition:transform 140ms,box-shadow 140ms;"><sc-if value="{{ b.hasIcon }}" hint-placeholder-val="{{ false }}"><img src="" data-dc-attr-src="{{ b.icon }}" alt="" style="width:30px; height:30px; object-fit:contain;"></sc-if><sc-if value="{{ b.hasColors }}" hint-placeholder-val="{{ false }}"><span aria-hidden="true" style="display:inline-flex; width:18px; height:10px; overflow:hidden; border-radius:999px; box-shadow:0 0 0 1px rgba(9,30,52,.12);"><i style="display:block; width:50%; background:#17191d;"></i><i style="display:block; width:50%; background:#a84735;"></i></span></sc-if>{{ b.name }}</button></sc-for></div></sc-if> 4649 <sc-if value="{{ rgBreedNoResults }}" hint-placeholder-val="{{ false }}"><div style="padding:10px 12px; border-radius:11px; background:#f6f8fb; color:#6e7f92; font-size:12px;">No encontramos una raza con ese nombre.</div></sc-if> 4650 <sc-if value="{{ rgHasSelectedBreedFamilies }}" hint-placeholder-val="{{ false }}"><div style="display:grid; gap:8px; padding-top:3px;"><span style="font-size:10.5px; font-weight:850; color:#7a8ca0; letter-spacing:.08em; text-transform:uppercase;">Seleccionadas</span><div style="display:grid; gap:8px;"><sc-for list="{{ rgSelectedBreedFamilies }}" as="b" hint-placeholder-count="3"><div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; min-height:54px; padding:7px 9px 7px 12px; border:1px solid #d7e3ed; border-radius:14px; background:linear-gradient(135deg,#f8fbfd,#eef4f8);"><span style="display:inline-flex; align-items:center; min-height:39px; overflow:hidden; border:1px solid #8eb9df; border-radius:999px; background:#eaf4ff; color:#07589d; box-shadow:0 3px 9px rgba(10,92,168,.07);"><span style="display:inline-flex; align-items:center; gap:5px; height:38px; padding:0 5px; font-size:12px; font-weight:800;"><sc-if value="{{ b.hasIcon }}" hint-placeholder-val="{{ false }}"><img src="" data-dc-attr-src="{{ b.icon }}" alt="" style="width:32px; height:32px; object-fit:contain;"></sc-if><sc-if value="{{ b.showBlack }}" hint-placeholder-val="{{ false }}"><i aria-label="Negros seleccionados" style="display:block; width:10px; height:10px; border-radius:50%; background:#17191d; box-shadow:0 0 0 1px rgba(9,30,52,.16);"></i></sc-if><sc-if value="{{ b.showRed }}" hint-placeholder-val="{{ false }}"><i aria-label="Colorados seleccionados" style="display:block; width:10px; height:10px; border-radius:50%; background:#a84735; box-shadow:0 0 0 1px rgba(109,37,24,.16);"></i></sc-if>{{ b.name }}</span><button type="button" onClick="{{ b.remove }}" aria-label="Quitar raza" style="width:31px; height:38px; border:0; background:transparent; color:#3d78a9; font:800 17px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></span><div style="display:flex; flex:1; align-items:center; justify-content:flex-start; gap:10px; flex-wrap:wrap;"><sc-if value="{{ b.hasColors }}" hint-placeholder-val="{{ false }}"><div style="display:flex; align-items:center; gap:7px; flex-wrap:wrap; margin-left:auto; padding:4px 8px; border-radius:10px; background:rgba(255,255,255,.62);"><span style="font-size:10.5px; font-weight:800; color:#718397; text-transform:uppercase; letter-spacing:.05em;">Color</span><sc-if value="{{ b.blackAvailable }}" hint-placeholder-val="{{ true }}"><button type="button" onClick="{{ b.toggleBlack }}" style="display:inline-flex;align-items:center;gap:6px;min-height:34px; padding:0 13px; border-radius:999px; border:1px solid {{ b.blackBorder }}; background:{{ b.blackBg }}; color:{{ b.blackColor }}; font:800 11.5px inherit; cursor:pointer;"><i aria-hidden="true" style="width:8px;height:8px;border-radius:50%;background:currentColor;"></i>Negros</button></sc-if><sc-if value="{{ b.redAvailable }}" hint-placeholder-val="{{ true }}"><button type="button" onClick="{{ b.toggleRed }}" style="display:inline-flex;align-items:center;gap:6px;min-height:34px; padding:0 13px; border-radius:999px; border:1px solid {{ b.redBorder }}; background:{{ b.redBg }}; color:{{ b.redColor }}; font:800 11.5px inherit; cursor:pointer;"><i aria-hidden="true" style="width:8px;height:8px;border-radius:50%;background:currentColor;"></i>Colorados</button></sc-if></div></sc-if><div style="display:flex; order:-1; align-items:center; gap:7px; padding:4px;"><span style="font-size:10.5px; font-weight:800; color:#718397; text-transform:uppercase; letter-spacing:.05em;">Cruzas</span><button type="button" onClick="{{ b.toggleCrosses }}" style="display:inline-flex;align-items:center;gap:5px;min-height:34px; padding:0 14px; border-radius:999px; border:1px solid {{ b.crossesBorder }}; background:{{ b.crossesBg }}; color:{{ b.crossesColor }}; font:800 11.5px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i>Y sus cruzas</button></div></div></div></sc-for></div></div></sc-if> 4651 </div> 4652 </section> 4653 </sc-if> 4654 4655 <sc-if value="{{ rgIsStep2 }}" hint-placeholder-val="{{ false }}"> 4656 <section style="background:#fff; border:1px solid #dce7f2; border-radius:22px; padding:clamp(18px,3vw,28px); box-shadow:0 12px 34px rgba(0,39,82,.07);"> 4657 <div style="display:flex; align-items:center; gap:12px; margin-bottom:19px;"><span style="width:34px; height:34px; border-radius:11px; background:#fff0dd; color:#a86300; display:grid; place-items:center; font-size:14px; font-weight:900;">3</span><div><h2 style="margin:0; color:#102f50; font-size:18px;">CaracterÃsticas del lote</h2><p style="margin:2px 0 0; color:#718296; font-size:12.5px;">Solo se muestran los datos compatibles con la categorÃa elegida.</p></div></div> 4658 <div class="rg-state-toggles"> 4659 <sc-if value="{{ rgAllowsAptoChina }}" hint-placeholder-val="{{ false }}"><button type="button" class="rg-state-toggle" onClick="{{ toggleRgChina }}" aria-pressed="{{ rgChinaPressed }}" style="color:{{ rgChinaColor }};background:{{ rgChinaBg }};border-color:{{ rgChinaBorder }};box-shadow:{{ rgChinaShadow }};"><img src="assets/icons/china-flag.svg" alt="" aria-hidden="true" width="23" height="16" style="width:23px;height:16px;border-radius:3px;filter:{{ rgChinaFlagFilter }};opacity:{{ rgChinaFlagOpacity }};"><span>Apto China</span><span class="rg-state-toggle-check" aria-hidden="true" style="background:{{ rgChinaCheckBg }};color:{{ rgChinaCheckColor }};">{{ rgChinaCheck }}</span></button></sc-if> 4660 <sc-if value="{{ rgNotFaena }}" hint-placeholder-val="{{ false }}"><label style="display:grid;gap:6px;font-size:11px;font-weight:800;color:#60758c;">MÃO-MÃO *<select required aria-required="true" value="" data-dc-attr-value="{{ rgMioValue }}" onChange="{{ onRgMio }}" style="height:44px;min-width:170px;border:1px solid #ccdae8;border-radius:12px;padding:0 12px;background:#fff;"><option value="">Seleccionarâ¦</option><option value="yes">SÃ</option><option value="no">No</option></select></label></sc-if> 4661 </div> 4662 <div class="rg-characteristics-grid"> 4663 <sc-if value="{{ rgAllowsAge }}" hint-placeholder-val="{{ false }}"><div class="rg-age-fields"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">EDAD *<input required aria-required="true" type="number" min="1" value="" data-dc-attr-value="{{ rgAgeValue }}" onInput="{{ onRgAgeValue }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">UNIDAD<select value="" data-dc-attr-value="{{ rgAgeUnit }}" onChange="{{ onRgAgeUnit }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 9px; background:#fff;"><sc-for list="{{ rgAgeUnitOptions }}" as="o" hint-placeholder-count="2"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label></div></sc-if> 4664 <sc-if value="{{ rgAllowsDentition }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">DENTICIÃN<sc-if value="{{ rgHasDentitionOptions }}" hint-placeholder-val="{{ false }}"><select value="" data-dc-attr-value="{{ rgDentition }}" onChange="{{ onRgDentition }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgDentitionOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoDentitionOptions }}" hint-placeholder-val="{{ true }}"><input value="" data-dc-attr-value="{{ rgDentition }}" onInput="{{ onRgDentition }}" placeholder="Ej. 4 dientes" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if> 4665 <sc-if value="{{ rgAllowsReproductiveStatus }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">ESTADO REPRODUCTIVO<sc-if value="{{ rgHasReproductiveStatusOptions }}" hint-placeholder-val="{{ false }}"><select value="" data-dc-attr-value="{{ rgReproductiveStatus }}" onChange="{{ onRgReproductiveStatus }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgReproductiveStatusOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoReproductiveStatusOptions }}" hint-placeholder-val="{{ true }}"><input value="" data-dc-attr-value="{{ rgReproductiveStatus }}" onInput="{{ onRgReproductiveStatus }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if> 4666 <sc-if value="{{ rgAllowsDestination }}" hint-placeholder-val="{{ false }}"><div style="grid-column:1/-1;"><div style="font-size:11px;font-weight:800;color:#60758c;margin-bottom:8px;">{{ rgDestinationLabel }}</div><div style="display:flex;gap:8px;flex-wrap:wrap;" role="group" aria-label="Destino"><sc-for list="{{ rgDestinationChoices }}" as="o" hint-placeholder-count="4"><button type="button" onClick="{{ o.pick }}" aria-pressed="{{ o.selected }}" style="padding:9px 16px;border-radius:100px;font:600 13px inherit;cursor:pointer;border:1px solid {{ o.border }};background:{{ o.bg }};color:{{ o.color }};">{{ o.label }}</button></sc-for></div></div></sc-if> 4667 <sc-if value="{{ rgNotFaena }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">ESTADO *<sc-if value="{{ rgHasBodyConditionOptions }}" hint-placeholder-val="{{ false }}"><select required aria-required="true" value="" data-dc-attr-value="{{ rgBodyCondition }}" onChange="{{ onRgBodyCondition }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgBodyConditionOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoBodyConditionOptions }}" hint-placeholder-val="{{ true }}"><input required aria-required="true" value="" data-dc-attr-value="{{ rgBodyCondition }}" onInput="{{ onRgBodyCondition }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if> 4668 <sc-if value="{{ rgNotFaena }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">CLASE *<sc-if value="{{ rgHasAnimalClassOptions }}" hint-placeholder-val="{{ false }}"><select required aria-required="true" value="" data-dc-attr-value="{{ rgAnimalClass }}" onChange="{{ onRgAnimalClass }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgAnimalClassOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoAnimalClassOptions }}" hint-placeholder-val="{{ true }}"><input required aria-required="true" value="" data-dc-attr-value="{{ rgAnimalClass }}" onInput="{{ onRgAnimalClass }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if> 4669 <sc-if value="{{ rgNotFaena }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">GARRAPATA *<sc-if value="{{ rgHasTickStatusOptions }}" hint-placeholder-val="{{ false }}"><select required aria-required="true" value="" data-dc-attr-value="{{ rgTickStatus }}" onChange="{{ onRgTickStatus }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgTickStatusOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoTickStatusOptions }}" hint-placeholder-val="{{ true }}"><input required aria-required="true" value="" data-dc-attr-value="{{ rgTickStatus }}" onInput="{{ onRgTickStatus }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if> 4670 <sc-if value="{{ rgNotFaena }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">SANIDAD *<sc-if value="{{ rgHasHealthOptions }}" hint-placeholder-val="{{ false }}"><select required aria-required="true" value="" data-dc-attr-value="{{ rgHealth }}" onChange="{{ onRgHealth }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgHealthOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoHealthOptions }}" hint-placeholder-val="{{ true }}"><input required aria-required="true" value="" data-dc-attr-value="{{ rgHealth }}" onInput="{{ onRgHealth }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if> 4671 <sc-if value="{{ rgNotFaena }}" hint-placeholder-val="{{ false }}"><label style="display:grid; gap:6px; font-size:11px; font-weight:800; color:#60758c;">NUTRICIÃN *<sc-if value="{{ rgHasNutritionOptions }}" hint-placeholder-val="{{ false }}"><select required aria-required="true" value="" data-dc-
4671attr-value="{{ rgNutrition }}" onChange="{{ onRgNutrition }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 10px; background:#fff;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgNutritionOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></sc-if><sc-if value="{{ rgHasNoNutritionOptions }}" hint-placeholder-val="{{ true }}"><input required aria-required="true" value="" data-dc-attr-value="{{ rgNutrition }}" onInput="{{ onRgNutrition }}" style="height:44px; border:1px solid #ccdae8; border-radius:12px; padding:0 12px;"></sc-if></label></sc-if> 4672 <sc-if value="{{ rgShowConfinement }}" hint-placeholder-val="{{ false }}"><label style="display:grid;gap:6px;font-size:11px;font-weight:800;color:#60758c;">DÃAS DE ENCIERRE<input type="number" min="0" step="1" value="" data-dc-attr-value="{{ rgDaysOfConfinement }}" onInput="{{ onRgDaysOfConfinement }}" style="height:44px;border:1px solid #ccdae8;border-radius:12px;padding:0 12px;"></label></sc-if> 4673 </div> 4674 <fieldset style="margin:14px 0 0;padding:14px;border:1px solid #ccdae8;border-radius:12px;"> 4675 <legend style="font-size:11px;font-weight:800;color:#60758c;">PLAZO DE PAGO</legend> 4676 <div role="group" aria-label="DÃas del plazo de pago" style="display:flex;flex-wrap:wrap;gap:8px;"> 4677 <sc-for list="{{ rgPaymentDays }}" as="day"><button type="button" class="rg-payment-chip" aria-pressed="{{ day.checked }}" onClick="{{ day.toggle }}"><span class="rg-payment-chip-mark" aria-hidden="true">â</span>{{ day.label }}</button></sc-for> 4678 </div> 4679 <sc-if value="{{ rgOtherPaymentDay }}"><label style="display:grid;gap:6px;margin-top:12px;">Otros dÃas<input aria-label="Otros dÃas de pago" value="" data-dc-attr-value="{{ rgOtherPaymentDayValue }}" onInput="{{ onRgOtherPaymentDay }}" placeholder="Ej.: 45 o 15 y 45" style="min-height:44px;border:1px solid #ccdae8;border-radius:10px;padding:0 12px;"></label></sc-if> 4680 <label style="display:grid;gap:6px;margin-top:12px;">Referencia del plazo<select aria-label="Referencia del plazo" value="" data-dc-attr-value="{{ rgPaymentReference }}" onChange="{{ onRgPaymentReference }}" style="min-height:44px;border:1px solid #ccdae8;border-radius:10px;padding:0 12px;background:white;"><option value="">Seleccionarâ¦</option><option value="del remate">Del remate</option><option value="de la carga">De la carga</option><option value="other">Otro</option></select></label> 4681 <sc-if value="{{ rgOtherPaymentReference }}"><label style="display:grid;gap:6px;margin-top:12px;">Otra referencia<input aria-label="Otra referencia del plazo" value="" data-dc-attr-value="{{ rgOtherPaymentReferenceValue }}" onInput="{{ onRgOtherPaymentReference }}" placeholder="Ej.: de la entrega" style="min-height:44px;border:1px solid #ccdae8;border-radius:10px;padding:0 12px;"></label></sc-if> 4682 <p style="margin:10px 0 0;color:#60758c;font-size:13px;">{{ rgPreviewPaymentTerm }}</p> 4683 </fieldset> 4684 <sc-if value="{{ rgHasDescriptionOptions }}" hint-placeholder-val="{{ false }}"><div style="margin-top:14px;"><div style="font-size:11px;font-weight:800;color:#60758c;letter-spacing:.05em;">CARACTERÃSTICAS</div><div style="margin-top:3px;font-size:11.5px;color:#8a9aac;">Seleccioná todas las que correspondan.</div><div class="rg-description-options" role="group" aria-label="CaracterÃsticas del lote"><sc-for list="{{ rgDescriptionOptions }}" as="o" hint-placeholder-count="5"><button type="button" class="rg-description-choice" onClick="{{ o.pick }}" aria-pressed="{{ o.selected }}" style="border-color:{{ o.border }};background:{{ o.bg }};color:{{ o.color }};box-shadow:{{ o.shadow }};"><span class="rg-description-choice-mark" aria-hidden="true" style="background:{{ o.markBg }};color:{{ o.markColor }};">{{ o.mark }}</span>{{ o.label }}</button></sc-for></div></div></sc-if> 4685 <label style="display:grid; gap:6px; margin-top:15px; font-size:11px; font-weight:800; color:#60758c;">OBSERVACIONES ADICIONALES <span style="font-weight:500; color:#8a9aac;">Texto libre opcional.</span><textarea id="rg-lot-description" name="rosganLotDescription" value="" data-dc-attr-value="{{ rgDescription }}" onInput="{{ onRgDescription }}" onChange="{{ onRgDescription }}" onBlur="{{ onRgDescription }}" rows="4" placeholder="Estado general, manejo u otra observaciónâ¦" style="resize:vertical; border:1px solid #ccdae8; border-radius:12px; padding:12px; font:500 13px/1.5 inherit; color:#102f50;"></textarea></label> 4686 </section> 4687 4688 <section class="rg-specific-panel"> 4689 <div style="font-size:11px; letter-spacing:.14em; color:#8bd9d4; font-weight:800;">DATOS ESPECÃFICOS</div> 4690 <sc-if value="{{ rgIsInvernada }}" hint-placeholder-val="{{ false }}"><h2 style="font-size:21px; margin:5px 0 17px;">Invernada</h2></sc-if> 4691 <sc-if value="{{ rgIsVientres }}" hint-placeholder-val="{{ false }}"><h2 style="font-size:21px; margin:5px 0 17px;">Vientres</h2></sc-if> 4692 <sc-if value="{{ rgIsFaena }}" hint-placeholder-val="{{ false }}"><h2 style="font-size:21px; margin:5px 0 17px;">Faena</h2></sc-if> 4693 <sc-if value="{{ rgIsInvernada }}" hint-placeholder-val="{{ false }}"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:12px;"><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESO PROMEDIO<input type="number" value="" data-dc-attr-value="{{ rgAverageWeight }}" onInput="{{ onRgAverageWeight }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESO MÃNIMO<input type="number" value="" data-dc-attr-value="{{ rgMinWeight }}" onInput="{{ onRgMinWeight }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESO MÃXIMO<input type="number" value="" data-dc-attr-value="{{ rgMaxWeight }}" onInput="{{ onRgMaxWeight }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESAJE<select value="" data-dc-attr-value="{{ rgWeighing }}" onChange="{{ onRgWeighing }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgWeighingOptions }}" as="o"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">DESBASTE<input value="" data-dc-attr-value="{{ rgDesbaste }}" onInput="{{ onRgDesbaste }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label></div><div class="rg-state-toggles" style="margin-top:16px;margin-bottom:0;"><button type="button" class="rg-state-toggle" onClick="{{ toggleRgLiquid }}" aria-pressed="{{ rgLiquidPressed }}" style="color:{{ rgLiquidColor }};background:{{ rgLiquidBg }};border-color:{{ rgLiquidBorder }};box-shadow:{{ rgLiquidShadow }};"><img src="assets/icons/lucide/tag.svg" alt="" aria-hidden="true" width="21" height="21" style="width:21px;height:21px;filter:{{ rgLiquidIconFilter }};opacity:{{ rgLiquidIconOpacity }};"><span>Marca lÃquida</span><span class="rg-state-toggle-check" aria-hidden="true" style="background:{{ rgLiquidCheckBg }};color:{{ rgLiquidCheckColor }};">{{ rgLiquidCheck }}</span></button><button type="button" class="rg-state-toggle" onClick="{{ toggleRgVisec }}" aria-pressed="{{ rgVisecPressed }}" style="color:{{ rgVisecColor }};background:{{ rgVisecBg }};border-color:{{ rgVisecBorder }};box-shadow:{{ rgVisecShadow }};"><img src="assets/icons/visec-mark.svg" alt="" aria-hidden="true" width="23" height="23" style="width:23px;height:23px;filter:{{ rgVisecIconFilter }};opacity:{{ rgVisecIconOpacity }};"><span>Apto VISEC</span><span class="rg-state-toggle-check" aria-hidden="true" style="background:{{ rgVisecCheckBg }};color:{{ rgVisecCheckColor }};">{{ rgVisecCheck }}</span></button></div></sc-if> 4694 <sc-if value="{{ rgIsVientres }}" hint-placeholder-val="{{ false }}"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:12px;"><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">SERVICIO<input type="text" value="" data-dc-attr-value="{{ rgService }}" onInput="{{ onRgService }}" placeholder="Ingresar servicio" style="height:44px;min-width:0;width:100%;box-sizing:border-box;border:1px solid rgba(255,255,255,.34);background:rgba(255,255,255,.1);color:inherit;border-radius:11px;padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PREÃEZ<input type="text" value="" data-dc-attr-value="{{ rgPregnancy }}
4694" onInput="{{ onRgPregnancy }}" placeholder="Ingresar preñez" style="height:44px;min-width:0;width:100%;box-sizing:border-box;border:1px solid rgba(255,255,255,.34);background:rgba(255,255,255,.1);color:inherit;border-radius:11px;padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PARICIÃN<input type="text" value="" data-dc-attr-value="{{ rgCalving }}" onInput="{{ onRgCalving }}" placeholder="Ingresar parición" style="height:44px;min-width:0;width:100%;box-sizing:border-box;border:1px solid rgba(255,255,255,.34);background:rgba(255,255,255,.1);color:inherit;border-radius:11px;padding:0 12px;"></label></div></sc-if> 4695 <sc-if value="{{ rgIsFaena }}" hint-placeholder-val="{{ false }}"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:12px;"><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">SUBCATEGORÃA<select value="" data-dc-attr-value="{{ rgSubCategory }}" onChange="{{ onRgSubCategory }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgSubCategoryOptions }}" as="o"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESO PROMEDIO<input type="number" value="" data-dc-attr-value="{{ rgAverageWeight }}" onInput="{{ onRgAverageWeight }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESO MÃNIMO<input type="number" value="" data-dc-attr-value="{{ rgMinWeight }}" onInput="{{ onRgMinWeight }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESO MÃXIMO<input type="number" value="" data-dc-attr-value="{{ rgMaxWeight }}" onInput="{{ onRgMaxWeight }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">CONDICIÃN<select value="" data-dc-attr-value="{{ rgConditionType }}" onChange="{{ onRgConditionType }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgConditionTypeOptions }}" as="o"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">ENTREGA<select value="" data-dc-attr-value="{{ rgDeliveryType }}" onChange="{{ onRgDeliveryType }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgDeliveryTypeOptions }}" as="o"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">TERMINACIÃN<select value="" data-dc-attr-value="{{ rgFinishing }}" onChange="{{ onRgFinishing }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgFinishingOptions }}" as="o"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">RENDIMIENTO ESTIMADO %<input type="number" step="0.1" value="" data-dc-attr-value="{{ rgEstimatedYield }}" onInput="{{ onRgEstimatedYield }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"></label><label style="display:grid; gap:6px; font-size:11px; font-weight:800;">PESAJE<select value="" data-dc-attr-value="{{ rgWeighing }}" onChange="{{ onRgWeighing }}" style="height:44px; border:1px solid rgba(255,255,255,.34); background:rgba(255,255,255,.1); color:#fff; border-radius:11px; padding:0 12px;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rgWeighingOptions }}" as="o"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select></label></div></sc-if> 4696 </section> 4697 </sc-if> 4698 4699 <sc-if value="{{ rgIsStep3 }}" hint-placeholder-val="{{ false }}"> 4700 <section> 4701 <div style="display:flex; align-items:center; gap:12px; margin-bottom:19px;"><span style="width:34px; height:34px; border-radius:11px; background:#e9efff; color:#3158a8; display:grid; place-items:center; font-size:14px; font-weight:900;">4</span><div><div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;"><h2 style="margin:0; color:#102f50; font-size:18px;">Videos del lote</h2><span style="padding:3px 8px;border-radius:999px;background:#eef2f6;color:#66788b;font-size:10px;font-weight:850;letter-spacing:.06em;text-transform:uppercase;">Opcional</span></div><p style="margin:3px 0 0; color:#718296; font-size:12.5px;">Podés agregar videos ahora o finalizar y cargarlos más adelante.</p></div></div> 4702 <p style="margin:0 0 16px;font-size:13px;"><a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guÃa en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">â</span></a></p> 4703 <div style="display:flex; align-items:center; gap:10px; padding:12px 14px; border:1px solid #bfe2d5; border-radius:13px; background:#effaf6; color:#176c58;"><i class="dc-lucide dc-lucide-circle-check" aria-hidden="true" style="font-size:19px;"></i><div><strong style="display:block; font-size:13px;">Lote cargado para control</strong><span style="display:block; margin-top:2px; font-size:11.5px;">Certificado {{ rgCreatedLotCert }}</span></div></div> 4704 <sc-if value="{{ rgCreatedLotCanUpload }}" hint-placeholder-val="{{ true }}"> 4705 <div style="margin-top:16px;"><button class="rg-filming-guide" type="button" onClick="{{ prepareCreatedFieldLot }}" style="margin:0;"><img src="/assets/icons/lucide/camera.svg" alt="" aria-hidden="true" style="width:18px;height:18px;">Modo filmación sin internet</button><p style="max-width:600px;margin:8px 0 0;color:#60758c;font-size:12px;line-height:1.5;">Preparalo con conexión antes de salir. Después podés filmar sin señal: los videos se guardan en este dispositivo y se suben cuando vuelva internet.</p></div> 4706 <label style="display:grid; place-items:center; min-height:190px; margin-top:16px; border:2px dashed #9fc2df; border-radius:17px; background:linear-gradient(145deg,#f8fbfe,#eef6fc); color:#0a5ca8; text-align:center; cursor:pointer; padding:22px; box-sizing:border-box;"><input type="file" multiple accept="video/*,.mp4,.mov,.m4v,.webm,.avi,.mpeg,.mpg" disabled="{{ rgCreatedVideoBusy }}" onChange="{{ uploadRgCreatedLotVideos }}" style="display:none;"><span><i class="dc-lucide dc-lucide-upload-cloud" aria-hidden="true" style="font-size:34px;"></i><strong style="display:block; margin-top:10px; font-size:15px;
4706 color:#10375d;">Seleccionar videos</strong><small style="display:block; margin-top:5px; color:#6e8296; line-height:1.45;">Podés elegir varios archivos a la vez.<br>Hasta 10 videos por vez · Máximo 1 GB cada uno.</small></span></label> 4707 </sc-if> 4708 <sc-if value="{{ rgCreatedLotCannotUpload }}" hint-placeholder-val="{{ false }}"><div style="margin-top:15px; padding:13px 14px; border:1px solid #efc7cb; border-radius:12px; background:#fff4f5; color:#a32934; font-size:12.5px;">El lote fue creado, pero el API no devolvió su certificado. Podés finalizar y cargar los videos luego desde el remate.</div></sc-if> 4709 <sc-if value="{{ rgCreatedVideoHasError }}" hint-placeholder-val="{{ false }}"><div style="margin-top:12px; padding:10px 12px; border-radius:10px; background:#fff3f4; border:1px solid #efc6ca; color:#a32934; font-size:12.5px;">{{ rgCreatedVideoError }}</div></sc-if> 4710 <sc-if value="{{ rgCreatedVideoLoading }}" hint-placeholder-val="{{ false }}"><div style="display:flex; align-items:center; justify-content:center; gap:9px; margin-top:14px; padding:13px; border-radius:11px; background:#f6f9fc; color:#536b83; font-size:12px;"><i class="dc-lucide dc-lucide-loader-circle rg-spin" aria-hidden="true"></i>Cargando videos del loteâ¦</div></sc-if> 4711 <sc-if value="{{ rgCreatedVideoHasUploads }}" hint-placeholder-val="{{ false }}"><div style="display:grid; gap:9px; margin-top:14px;"><sc-for list="{{ rgCreatedVideoUploads }}" as="u" hint-placeholder-count="2"><div style="border:1px solid #dce7f2; border-radius:12px; padding:11px 13px; background:#f8fbfe;"><div style="display:flex; justify-content:space-between; gap:12px; font-size:12px;"><strong style="color:#26384d; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ u.name }}</strong><span style="color:{{ u.color }}; flex:none;">{{ u.status }}</span></div><div style="height:6px; border-radius:10px; overflow:hidden; background:#e1eaf3; margin-top:8px;"><span style="display:block; width:{{ u.progress }}%; height:100%; border-radius:inherit; background:{{ u.bar }}; transition:width .2s ease;"></span></div><div style="font-size:10.5px; color:#7b8ca1; margin-top:5px;">{{ u.size }}</div><sc-if value="{{ u.hasError }}"><div style="font-size:11.5px;color:#b42332;margin-top:7px;">{{ u.error }}</div></sc-if><sc-if value="{{ u.canRetry }}"><button type="button" onClick="{{ u.retry }}" style="margin-top:8px;border:1px solid #bbd1e8;border-radius:9px;padding:8px 11px;background:#fff;color:#0a5ca8;font-weight:700;">{{ u.retryLabel }}</button></sc-if></div></sc-for></div></sc-if> 4712 <sc-if value="{{ rgCreatedVideosHasItems }}" hint-placeholder-val="{{ false }}"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:10px; margin-top:14px;"><sc-for list="{{ rgCreatedVideos }}" as="v" hint-placeholder-count="2"><article style="border:1px solid #dce7f2; border-radius:13px; overflow:hidden; background:#fff;"><sc-if value="{{ v.hasUrl }}" hint-placeholder-val="true"><video controls preload="metadata" data-dc-attr-src="{{ v.url }}" style="width:100%; aspect-ratio:16/9; display:block; background:#061b32;"></video></sc-if><sc-if value="{{ v.noUrl }}" hint-placeholder-val="false"><div style="aspect-ratio:16/9; display:grid; place-items:center; background:#eef4fa; color:#0a5ca8;"><i class="dc-lucide dc-lucide-file-video" aria-hidden="true" style="font-size:27px;"></i></div></sc-if><div style="display:flex; align-items:center; gap:8px; padding:9px 10px;"><div style="min-width:0; flex:1;"><strong style="display:block; font-size:11.5px; color:#26384d; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ v.name }}</strong><span style="font-size:10px; color:#7b8ca1;">{{ v.size }}</span></div><button type="button" onClick="{{ v.remove }}" disabled="{{ v.deleting }}" title="Eliminar video" style="width:31px; height:31px; border-radius:9px; border:1px solid #f0c8cc; background:#fff5f5; color:#ba2d3a; display:grid; place-items:center; cursor:pointer;"><i class="dc-lucide dc-lucide-trash-2" aria-hidden="true"></i></button></div></article></sc-for></div></sc-if> 4713 <sc-if value="{{ rgCreatedVideosEmpty }}" hint-placeholder-val="{{ false }}"><div style="margin-top:14px; padding:12px; border-radius:11px; background:#f6f9fc; color:#536b83; font-size:12px; text-align:center;"><strong style="color:#26384d;">El lote quedó en estado Cargado.</strong> Los videos son opcionales: podés finalizar ahora y agregarlos más tarde durante la carga.</div></sc-if> 4714 </section> 4715 </sc-if> 4716 4717 </div> 4718 <div class="rg-wizard-footer" style="display:flex; justify-content:space-between; gap:12px; margin-top:22px; padding-top:18px; border-top:1px solid #eef3f9;"><button type="button" onClick="{{ rgPrevStep }}" style="visibility:{{ rgPrevVisible }}; display:inline-flex;align-items:center;gap:6px;background:#eef3f9; color:#002e5f; border:1px solid #dde7f2; border-radius:12px; padding:12px 22px; font-size:14px; font-weight:700; font-family:inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i>Atrás</button><button type="button" onClick="{{ rgNextStep }}" disabled="{{ rgNextDisabled }}" style="display:inline-flex;align-items:center;justify-content:center;gap:7px;background:{{ rgNextBg }}; color:#fff; border:none; border-radius:12px; padding:12px 26px; min-width:150px; font-size:14px; font-weight:800; font-family:inherit; cursor:pointer; box-shadow:0 8px 20px rgba(10,92,168,.18); opacity:{{ rosganLotPublishOpacity }};">{{ rgNextLabel }}<sc-if value="{{ rgNextShowsArrow }}" hint-placeholder-val="{{ true }}"><i class="dc-lucide dc-lucide-arrow-right" aria-hidden="true"></i></sc-if></button></div> 4719 </div> 4720 <sc-if value="{{ rgShowPreview }}" hint-placeholder-val="{{ true }}"> 4721 <aside class="rg-preview-panel" style="position:sticky; top:82px;"> 4722 <div style="font-size:11.5px; letter-spacing:.12em; color:#8593a6; font-weight:700; margin-bottom:10px;">PREVISUALIZACIÃN</div> 4723 <div style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 18px 50px rgba(0,30,60,.14);"> 4724 <div style="position:relative; min-height:180px; padding:22px 20px; background:{{ rgPreviewBrandColor }}; color:#fff; display:flex; flex-direction:column; justify-content:flex-end; overflow:hidden;"> 4725 <sc-if value="{{ rgPreviewHasLogo }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ rgPreviewLogo }}" alt="{{ rgPreviewConsignataria }}" style="position:absolute;
4725left:50%;top:45px;transform:translateX(-50%);width:116px;height:88px;object-fit:contain;"></sc-if> 4726 <sc-if value="{{ rgPreviewHasThumbnail }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ rgPreviewThumbnail }}" alt="" onError="{{ rgPreviewImageError }}" onLoad="{{ rgPreviewImageLoaded }}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"></sc-if> 4727 <div aria-hidden="true" style="position:absolute;inset:0;background:linear-gradient(180deg,transparent 45%,rgba(0,20,40,.65));pointer-events:none;"></div> 4728 <span style="position:absolute; top:13px; left:14px; border:1px solid rgba(255,255,255,.25); background:rgba(0,20,40,.22); border-radius:100px; padding:5px 10px; font-size:10px; font-weight:800; letter-spacing:.08em;">{{ rgPreviewType }}</span> 4729 <div style="position:relative;font-size:19px; font-weight:850; line-height:1.08;">{{ rgPreviewCategory }}</div><div style="position:relative;font-size:11.5px; color:#e5eef7; margin-top:5px;">{{ rgPreviewRemate }}</div> 4730 </div> 4731 <div style="padding:15px 16px; display:grid; gap:12px;"> 4732 <div style="display:grid; grid-template-columns:1fr 1fr; gap:8px;"><div style="background:#f2f6fb; border-radius:10px; padding:10px 12px;"><div style="font-size:10px; color:#8593a6;">CABEZAS</div><div style="font-size:17px; font-weight:800; color:#112033;">{{ rgPreviewHeads }}</div></div><div style="background:#f2f6fb; border-radius:10px; padding:10px 12px;"><div style="font-size:10px; color:#8593a6;">PESO PROM.</div><div style="font-size:17px; font-weight:800; color:#112033;">{{ rgPreviewWeight }}</div></div></div> 4733 <div style="display:grid; gap:7px; font-size:12px;"><div style="display:flex; justify-content:space-between; gap:10px;"><span style="color:#8593a6;">Consignataria</span><strong style="color:#112033; text-align:right;">{{ rgPreviewConsignataria }}</strong></div><div style="display:flex; justify-content:space-between; gap:10px;"><span style="color:#8593a6;">Comitente</span><strong style="color:#112033; text-align:right;">{{ rgPreviewComitente }}</strong></div><div style="display:flex; justify-content:space-between; gap:10px;"><span style="color:#8593a6;">Establecimiento</span><strong style="color:#112033; text-align:right;">{{ rgPreviewEstablishment }}</strong></div><div style="display:flex; justify-content:space-between; gap:10px;"><span style="color:#8593a6;">Raza</span><strong style="color:#112033; text-align:right;">{{ rgPreviewBreeds }}</strong></div></div> 4734 <div style="display:flex;justify-content:space-between;gap:10px;font-size:12px;"><span style="color:#8593a6;">Plazo de pago</span><strong style="text-align:right;color:#112033;">{{ rgPreviewPaymentTerm }}</strong></div><div><div style="display:flex; justify-content:space-between; margin-bottom:6px; font-size:11px; font-weight:700;"><span style="color:#8593a6;">Completitud</span><span style="color:#0a5ca8;">{{ rgPreviewCompleteness }}</span></div><div style="height:6px; background:#e7eef6; border-radius:100px; overflow:hidden;"><div style="height:100%; width:{{ rgPreviewProgress }}; border-radius:100px; background:linear-gradient(90deg,#0a5ca8,#24b4bd); transition:width 240ms;"></div></div></div> 4735 </div> 4736 </div> 4737 </aside> 4738 </sc-if> 4739 </div> 4740 </sc-if> 4741 </div> 4742 </div> 4743 </sc-if> 4744 4745 <!-- ============ CARGAR LOTE (WIZARD) ============ --> 4746 <sc-if value="{{ isLoad }}" hint-placeholder-val="{{ false }}"> 4747 <div class="lot-wizard-page" style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1); padding:0 clamp(14px, 4vw, 36px); max-width:1240px; margin:0 auto;"> 4748 <div class="lot-wizard-header" style="display:flex; justify-content:space-between; align-items:flex-end; gap:14px; flex-wrap:wrap; padding:24px 0 20px;"> 4749 <div> 4750 <span onClick="{{ wizardBack }}" style="display:inline-flex; align-items:center; gap:6px; font-size:13px; color:#0a5ca8; font-weight:600; cursor:pointer;"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i>{{ wizardBackLabel }}</span> 4751 <h1 style="font-size:clamp(23px, 3.2vw, 32px); font-weight:800; letter-spacing:-0.02em; margin:8px 0 2px; color:#112033;">{{ wizardTitle }}</h1>
4752 <span style="font-size:13.5px; color:#65758a;">{{ wizardSubtitle }}</span> 4753 </div> 4754 <span onClick="{{ resetForm }}" style="font-size:13px; color:#e63946; font-weight:600; cursor:pointer;">{{ wizardResetLabel }}</span> 4755 </div> 4756 <!-- step progress --> 4757 <div class="lot-wizard-steps" style="display:flex; gap:8px; margin-bottom:20px;"> 4758 <sc-for list="{{ wizardSteps }}" as="w" hint-placeholder-count="4"> 4759 <div class="rg-wizard-step" onClick="{{ w.go }}" style="flex:1; min-width:0; cursor:pointer;"> 4760 <div style="height:5px; border-radius:100px; background:{{ w.barBg }}; transition:background 300ms;"></div> 4761 <div style="display:flex; align-items:center; gap:8px; margin-top:9px;"> 4762 <span style="width:24px; height:24px; border-radius:50%; display:grid; place-items:center; font-size:12px; font-weight:800; flex-shrink:0; background:{{ w.dotBg }}; color:{{ w.dotColor }}; border:1px solid {{ w.dotBorder }}; transition:all 260ms;"><sc-if value="{{ w.done }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if><sc-if value="{{ w.numberVisible }}" hint-placeholder-val="{{ true }}">{{ w.number }}</sc-if></span> 4763 <div class="lot-wizard-step-copy" style="min-width:0;"><div style="font-size:13px; font-weight:700; color:{{ w.titleColor }}; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ w.title }}</div><div style="font-size:11px; color:#8593a6; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ w.text }}</div></div> 4764 </div> 4765 </div> 4766 </sc-for> 4767 </div> 4768 <div class="lot-wizard-grid" style="display:grid; grid-template-columns:{{ loadLayoutCols }}; gap:20px; align-items:start;"> 4769 <!-- form panel --> 4770 <div class="lot-form-panel" style="background:#fff; border-radius:20px; box-shadow:0 12px 40px rgba(0,30,60,0.1); padding:clamp(20px, 3vw, 30px); min-height:420px; display:flex; flex-direction:column;"> 4771 <div style="animation:pageIn 340ms cubic-bezier(0.22,0.72,0.18,1) both; flex:1;" key="{{ stepKey }}"> 4772 4773 <!-- STEP 0 --> 4774 <sc-if value="{{ isStep0 }}" hint-placeholder-val="{{ true }}"> 4775 <div style="font-size:11.5px; letter-spacing:0.12em; color:#e63946; font-weight:700;">ETAPA 1</div> 4776 <h2 style="font-size:20px; font-weight:800; margin:4px 0 18px; color:#112033;">Establecimiento, categorÃa y cantidad</h2> 4777 <div style="display:flex; align-items:flex-end; gap:12px; flex-wrap:wrap; margin-bottom:16px;"> 4778 <sc-if value="{{ estSelectorHas }}" hint-placeholder-val="{{ false }}"> 4779 <div style="flex:1; min-width:220px;"> 4780 <div style="font-size:12px; font-weight:700; color:#3d4c5f; margin-bottom:6px;">Usar un establecimiento guardado</div> 4781 <select value="" data-dc-attr-value="{{ selectedEstId }}" onChange="{{ pickEst }}" style="width:100%; border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#fff;" style-focus="border-color:#0a5ca8;"> 4782 <option value="">Seleccioná un establecimiento</option> 4783 <sc-for list="{{ estSelectOptions }}" as="o" hint-placeholder-count="2"><option value="" data-dc-attr-value="{{ o.id }}">{{ o.label }}</option></sc-for> 4784 </select> 4785 </div> 4786 </sc-if> 4787 <sc-if value="{{ canCreateEstablishment }}" hint-placeholder-val="{{ false }}"><button onClick="{{ wzEstToggle }}" style="background:#0a5ca8; color:#fff; border:none; border-radius:10px; padding:11px 18px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer; white-space:nowrap;" style-hover="background:#084b8a;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Nuevo establecimiento</button></sc-if> 4788 <sc-if value="{{ estIsSaved }}" hint-placeholder-val="{{ false }}">
4789 <span style="display:inline-flex; align-items:center; gap:6px; font-size:12px; font-weight:700; color:#1f7a63; background:rgba(31,122,99,0.12); padding:9px 13px; border-radius:100px;"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i>Guardado</span> 4790 </sc-if> 4791 </div> 4792 <sc-if value="{{ wzEstOpen }}" hint-placeholder-val="{{ false }}"> 4793 <div class="establishment-form-card" style="background:#f6f9fc; border:1px solid #e3eaf2; border-radius:14px; padding:18px 20px; margin-bottom:18px;"> 4794 <div style="display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px;"><div><div style="font-size:14.5px; font-weight:800; color:#112033;">Nuevo establecimiento</div><div style="margin-top:3px; font-size:11.5px; color:#65758a;">Primero elegà o creá el comitente al que pertenece.</div></div><button type="button" onClick="{{ wzToggleComitente }}" style="display:inline-flex; align-items:center; gap:7px; border:1px solid #b9d2e8; border-radius:9px; background:#fff; color:#0a5ca8; padding:8px 11px; font:800 11px inherit; cursor:pointer; white-space:nowrap;"><img src="assets/icons/lucide/users-round.svg" alt="" style="width:15px; height:15px;"> Crear comitente</button></div> 4795 <sc-if value="{{ wzComitenteOpen }}" hint-placeholder-val="{{ false }}"> 4796 <div style="margin-bottom:13px; padding:13px; border:1px solid #bdd8eb; border-radius:12px; background:#fff;"> 4797 <div style="display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:10px;"><div><strong style="display:block; color:#123b61; font-size:13px;">Alta de comitente</strong><span style="display:block; margin-top:2px; color:#718296; font-size:10.5px;">Ingresá el CUIT: razón social y domicilio fiscal se completan desde ARCA.</span></div><button type="button" onClick="{{ wzToggleComitente }}" aria-label="Cerrar" style="width:28px;height:28px;border:0;border-radius:8px;background:#eef4f9;color:#52677c;cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="" style="width:14px;height:14px;"></button></div> 4798 <div class="establishment-form-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:10px;"> 4799 <label style="display:flex;flex-direction:column;gap:5px;"><span style="font-size:11px;font-weight:700;color:#3d4c5f;">CUIT (opcional)</span><span style="position:relative;"><input aria-describedby="confidential-wz_cf_cuit" value="" data-dc-attr-value="{{ wz_cf_cuit }}" onInput="{{ wz_set_cf_cuit }}" inputmode="numeric" maxlength="11" placeholder="11 dÃgitos" style="box-sizing:border-box;width:100%;border:1px solid #d5e2ed;border-radius:9px;padding:10px 36px 10px 11px;font:650 13px inherit;color:#112033;"><sc-if value="{{ wzComitenteLookup }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true" style="position:absolute;right:11px;top:11px;color:#0a5ca8;"></i></sc-if></span><small id="confidential-wz_cf_cuit" style="display:inline-flex;align-items:flex-start;gap:7px;box-sizing:border-box;max-width:100%;width:fit-content;margin-top:6px;padding:7px 10px;border:1px solid #f0cf86;border-radius:9px;background:#fff7e6;color:#805411;font-size:11px;font-weight:600;line-height:1.45;letter-spacing:normal;text-transform:none;white-space:normal;text-align:left;"><span aria-hidden="true" style="flex:none;font-size:15px;line-height:1.1;color:#a56b08;">⚠</span><span>Dato confidencial perteneciente a la firma consignataria</span></small></label> 4800 <label style="display:grid;gap:6px;color:#60758c;font-size:12px;font-weight:700;">Alias (opcional)<input type="text" value="" data-dc-attr-value="{{ wz_cf_alias }}" onInput="{{ wz_set_cf_alias }}" placeholder="Ej.: Estancia El Ombú" style="box-sizing:border-box;min-width:0;width:100%;height:43px;border:1px solid #ccdae8;border-radius:11px;padding:0 12px;font:inherit;background:#fff;"></label> 4801<label style="display:flex;flex-direction:column;gap:5px;"><span style="font-size:11px;font-weight:700;color:#3d4c5f;">RAZÃN SOCIAL</span><input value="" data-dc-attr-value="{{ wz_cf_razon }}" readonly="true" placeholder="Se completa desde ARCA" style="border:1px solid #d5e2ed;border-radius:9px;padding:10px 11px;font:650 13px inherit;color:#112033;background:#eef3f7;"></label> 4802 <label style="display:flex;flex-direction:column;gap:5px;"><span style="font-size:11px;font-weight:700;color:#3d4c5f;">PROVINCIA FISCAL</span><input value="" data-dc-attr-value="{{ wz_cf_provincia }}" readonly="true" placeholder="Se completa desde ARCA" style="border:1px solid #d5e2ed;border-radius:9px;padding:10px 11px;font:650 13px inherit;color:#112033;background:#eef3f7;"></label> 4803 <label style="display:flex;flex-direction:column;gap:5px;"><span style="font-size:11px;font-weight:700;color:#3d4c5f;">LOCALIDAD FISCAL</span><input value="" data-dc-attr-value="{{ wz_cf_localidad }}" readonly="true" placeholder="Se completa desde ARCA" style="border:1px solid #d5e2ed;border-radius:9px;padding:10px 11px;font:650 13px inherit;color:#112033;background:#eef3f7;"></label> 4804 </div> 4805 <sc-if value="{{ wzComitenteMsg }}" hint-placeholder-val="{{ false }}"><div style="margin-top:9px;padding:8px 10px;border-radius:8px;background:{{ wzComitenteMsgBg }};color:{{ wzComitenteMsgColor }};font-size:11px;font-weight:700;">{{ wzComitenteMsg }}</div></sc-if> 4806 <div style="display:flex;justify-content:flex-end;margin-top:10px;"><button type="button" onClick="{{ wzSaveComitente }}" disabled="{{ wzComitenteSaveDisabled }}" style="border:0;border-radius:9px;background:#1f7a63;color:#fff;padding:10px 15px;font:800 11.5px inherit;cursor:pointer;opacity:{{ wzComitenteSaveOpacity }};">{{ wzComitenteSaveLabel }}</button></div> 4807 </div> 4808 </sc-if> 4809 <sc-if value="{{ wzNoComitentes }}" hint-placeholder-val="{{ false }}"><div style="margin-bottom:12px; padding:10px 12px; border:1px solid #f0d4a6; border-radius:10px; background:#fff8e8; color:#805617; font-size:12px; font-weight:700;">TodavÃa no tenés comitentes. Podés crear el primero acá mismo.</div></sc-if> 4810 <div class="establishment-form-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:12px;"> 4811 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Comitente *</span><select value="" data-dc-attr-value="{{ wz_ef_comitenteId }}" onChange="{{ wz_set_ef_comitenteId }}" style="border:1px solid #dde7f2; border-radius:10px; padding:10px 12px; font-size:14px; font-family:inherit; outline:none;
4811 color:#112033; background:#fff;"><option value="">Seleccionar comitenteâ¦</option><sc-for list="{{ wzEfComitenteOpts }}" as="c" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ c.id }}">{{ c.label }}</option></sc-for></select></label> 4812 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Nombre del establecimiento *</span><input value="" data-dc-attr-value="{{ wz_ef_nombre }}" onInput="{{ wz_set_ef_nombre }}" placeholder="Ej: Estancia El Ceibo" style="border:1px solid #dde7f2; border-radius:10px; padding:10px 12px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label> 4813 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Provincia *</span><select value="" data-dc-attr-value="{{ wz_ef_provincia }}" onChange="{{ wz_set_ef_provincia }}" style="border:1px solid #dde7f2; border-radius:10px; padding:10px 12px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#fff;"><option value="">{{ wzEfProvPlaceholder }}</option><sc-for list="{{ wzEfProvinceOpts }}" as="p" hint-placeholder-count="6"><option value="" data-dc-attr-value="{{ p }}">{{ p }}</option></sc-for></select></label> 4814 <label style="position:relative;display:flex; flex-direction:column; gap:6px;"><span style="display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px; font-weight:600; color:#3d4c5f;"><span>Localidad *</span><small style="display:inline-flex;align-items:center;gap:4px;color:#718296;font-weight:600;"><img src="assets/google-logo.webp" alt="" style="width:12px;height:12px;object-fit:contain;">Google</small></span><input value="" data-dc-attr-value="{{ wz_ef_localidad }}" onInput="{{ wz_set_ef_localidad }}" onFocus="{{ wzOpenPlaceResults }}" onBlur="{{ wzClosePlaceResults }}" disabled="{{ wzEfLocDisabled }}" autocomplete="off" placeholder="Escribà una localidad" style="border:1px solid #dde7f2;border-radius:10px;padding:10px 36px 10px 12px;font-size:14px;font-family:inherit;outline:none;color:#112033;background:{{ wzEfLocBg }};"><sc-if value="{{ wzPlaceLoading }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true" style="position:absolute;right:12px;top:35px;color:#0a5ca8;"></i></sc-if><sc-if value="{{ wzPlaceResultsOpen }}" hint-placeholder-val="{{ false }}"><div style="position:absolute;z-index:50;left:0;right:0;top:calc(100% + 5px);padding:5px;border:1px solid #d6e2ed;border-radius:11px;background:#fff;box-shadow:0 14px 30px rgba(0,35,70,.18);max-height:220px;overflow:auto;"><sc-for list="{{ wzPlaceResults }}" as="p" hint-placeholder-count="4"><button type="button" onClick="{{ p.pick }}" style="width:100%;display:grid;gap:2px;border:0;border-radius:8px;background:#fff;padding:8px 9px;text-align:left;cursor:pointer;"><strong style="font-size:11.5px;color:#153955;">{{ p.label }}</strong><span style="font-size:9.5px;color:#718296;">{{ p.address }}</span></button></sc-for></div></sc-if></label> 4815 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">RENSPA <small style="font-weight:500;color:#8593a6;">opcional</small></span><input value="" data-dc-attr-value="{{ wz_ef_renspa }}" onInput="{{ wz_set_ef_renspa }}" placeholder="Podés dejarlo vacÃo" style="border:1px solid #dde7f2; border-radius:10px; padding:10px 12px; font-size:14px; font-family:inherit; outline:none; color:#112033;"></label> 4816 </div> 4817 <sc-if value="{{ wzEstFormMsg }}" hint-placeholder-val="{{ false }}"> 4818 <div style="margin-top:12px; font-size:12.5px; padding:9px 12px; border-radius:10px; background:{{ wzEstFormMsgBg }}; color:{{ wzEstFormMsgColor }};">{{ wzEstFormMsg }}</div> 4819 </sc-if> 4820 <sc-if value="{{ wzPlaceError }}" hint-placeholder-val="{{ false }}"><div style="margin-top:9px;font-size:10.5px;color:#8a5a00;">{{ wzPlaceError }}</div></sc-if> 4821 <div style="display:flex; gap:10px; margin-top:14px;"> 4822 <button onClick="{{ wzSaveEst }}" disabled="{{ wzEstSaveDisabled }}" style="background:#1f7a63; color:#fff; border:none; border-radius:10px; padding:10px 18px; font-size:13px; font-weight:700; font-family:inherit; cursor:{{ wzEstSaveCursor }}; opacity:{{ wzEstSaveOpacity }};">{{ wzEstSaveLabel }}</button> 4823 <button onClick="{{ wzEstToggle }}" style="background:#fff; color:#3d4c5f; border:1px solid #dde7f2; border-radius:10px; padding:10px 18px; font-size:13px; font-weight:700; font-family:inherit; cursor:pointer;">Cancelar</button> 4824 </div> 4825 </div> 4826 </sc-if> 4827 <div class="lot-fields-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:14px;"> 4828 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Nombre del establecimiento</span><input value="" data-dc-attr-value="{{ f_estName }}" disabled="{{ manualEstLocked }}" placeholder="Seleccioná un establecimiento validado" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;
4828 color:#112033; background:#eef3f8;"></label> 4829 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Comitente</span><input value="" data-dc-attr-value="{{ f_comitente }}" disabled="{{ manualEstLocked }}" placeholder="Se obtiene del establecimiento" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#eef3f8;"></label> 4830 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Provincia</span><input value="" data-dc-attr-value="{{ f_prov }}" disabled="{{ manualEstLocked }}" placeholder="Se obtiene del establecimiento" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#eef3f8;"></label> 4831 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Localidad</span><input value="" data-dc-attr-value="{{ f_loc }}" disabled="{{ manualEstLocked }}" placeholder="Se obtiene del establecimiento" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#eef3f8;"></label> 4832 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Fecha de subasta</span><input value="" data-dc-attr-value="{{ f_fecha }}" onInput="{{ set_fecha }}" type="date" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label> 4833 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">CategorÃa</span><select value="" data-dc-attr-value="{{ f_cat }}" onChange="{{ set_cat }}" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#fff;"><option value="">Seleccioná</option><sc-for list="{{ categoryOptions }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.id }}">{{ o.label }}</option></sc-for></select></label> 4834 <sc-if value="{{ subcatHas }}" hint-placeholder-val="{{ false }}"> 4835 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">SubcategorÃa</span><select value="" data-dc-attr-value="{{ f_subcat }}" onChange="{{ set_subcat }}" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#fff;" style-focus="border-color:#0a5ca8;"><option value="">Seleccioná</option><sc-for list="{{ subcatOptions }}" as="o" hint-placeholder-count="3"><option value="" data-dc-attr-value="{{ o.id }}">{{ o.label }}</option></sc-for></select></label> 4836 </sc-if> 4837 <sc-if value="{{ catNotMixed }}" hint-placeholder-val="{{ true }}"> 4838 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">{{ cantLabel }}</span><input value="" data-dc-attr-value="{{ f_cant }}" onInput="{{ set_cant }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label> 4839 </sc-if> 4840 <sc-if value="{{ catIsMixed }}" hint-placeholder-val="{{ false }}"> 4841 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Cantidad machos</span><input value="" data-dc-attr-value="{{ f_cantMachos }}" onInput="{{ set_cantMachos }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;
4841 color:#112033;" style-focus="border-color:#0a5ca8;"></label> 4842 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Cantidad hembras</span><input value="" data-dc-attr-value="{{ f_cantHembras }}" onInput="{{ set_cantHembras }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label> 4843 </sc-if> 4844 </div> 4845 <div style="margin-top:18px;"> 4846 <div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">Razas <span style="color:#8593a6; font-weight:500;">(máximo 3 · {{ razasCount }}/3)</span></div> 4847 <div style="display:flex; gap:8px; flex-wrap:wrap;"> 4848 <sc-for list="{{ breedOptions }}" as="b" hint-placeholder-count="6"> 4849 <span onClick="{{ b.toggle }}" style="padding:7px 14px; border-radius:100px; font-size:12.5px; font-weight:600; cursor:pointer; border:1px solid {{ b.border }}; background:{{ b.bg }}; color:{{ b.color }}; transition:all 150ms;" style-hover="border-color:#0a5ca8;">{{ b.label }}</span> 4850 </sc-for> 4851 </div> 4852 </div> 4853 </sc-if> 4854 4855 <!-- STEP 1 --> 4856 <sc-if value="{{ isStep1 }}" hint-placeholder-val="{{ false }}"> 4857 <div style="font-size:11.5px; letter-spacing:0.12em; color:#e63946; font-weight:700;">ETAPA 2</div> 4858 <h2 style="font-size:20px; font-weight:800; margin:4px 0 18px; color:#112033;">Engorde, destino y pesos</h2> 4859 <div style="display:flex; flex-direction:column; gap:18px;"> 4860 <div> 4861 <div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">Tipo de engorde</div> 4862 <div style="display:flex; gap:8px; flex-wrap:wrap;"> 4863 <sc-for list="{{ fatteningOptions }}" as="o" hint-placeholder-count="3"><span onClick="{{ o.pick }}" style="padding:9px 16px; border-radius:100px; font-size:13px; font-weight:600; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }}; transition:all 150ms;">{{ o.label }}</span></sc-for> 4864 </div> 4865 <sc-if value="{{ showDiasEncierre }}" hint-placeholder-val="{{ false }}"> 4866 <label style="display:flex; flex-direction:column; gap:6px; margin-top:12px; max-width:220px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">DÃas de encierre</span><input value="" data-dc-attr-value="{{ f_diasEncierre }}" onInput="{{ set_diasEncierre }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label> 4867 </sc-if> 4868 </div> 4869 <div> 4870 <div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">Destino</div> 4871 <div style="display:flex; gap:8px; flex-wrap:wrap;"> 4872 <sc-for list="{{ destinationOptions }}" as="o" hint-placeholder-count="4"><span onClick="{{ o.pick }}" style="padding:9px 16px; border-radius:100px; font-size:13px; font-weight:600; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }}; transition:all 150ms;">{{ o.label }}</span></sc-for> 4873 </div> 4874 <sc-if value="{{ hiltonRestricted }}" hint-placeholder-val="{{ false }}"><div style="margin-top:8px; color:#8a5a00; font-size:11.5px; line-height:1.4;"><i class="dc-lucide dc-lucide-info" aria-hidden="true"></i> Ración o Feedlot no pueden tener destino Exportación Hilton.</div></sc-if> 4875 </div> 4876 <div> 4877 <div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">¿Apto China?</div> 4878 <div style="display:flex; gap:8px;"> 4879 <sc-for list="{{ chinaOptions }}" as="o" hint-placeholder-count="2"><span onClick="{{ o.pick }}" style="padding:9px 20px; border-radius:100px; font-size:13px; font-weight:700; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }};">{{ o.label }}</span></sc-for> 4880 </div> 4881 </div> 4882 <div class="lot-three-fields-grid" style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:14px;"> 4883 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Peso promedio (kg)</span><input value="" data-dc-attr-value="{{ f_pesoProm }}" onInput="{{ set_pesoProm }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label> 4884 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Peso mÃnimo (kg)</span><input value="" data-dc-attr-value="{{ f_pesoMin }}" onInput="{{ set_pesoMin }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label> 4885 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Peso máximo (kg)</span><input value="" data-dc-attr-value="{{ f_pesoMax }}" onInput="{{ set_pesoMax }}" type="number" min="0" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label> 4886 </div> 4887 </div> 4888 </sc-if> 4889 4890 <!-- STEP 2 --> 4891 <sc-if value="{{ isStep2 }}" hint-placeholder-val="{{ false }}"> 4892 <div style="font-size:11.5px; letter-spacing:0.12em; color:#e63946; font-weight:700;">ETAPA 3</div> 4893 <h2 style="font-size:20px; font-weight:800; margin:4px 0 18px; color:#112033;">Condición comercial y disponibilidad</h2> 4894 <div style="display:flex; flex-direction:column; gap:18px;"> 4895 <div> 4896 <div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">Condición de venta</div> 4897 <div style="display:flex; gap:8px; flex-wrap:wrap;"> 4898 <sc-for list="{{ conditionOptions }}" as="o" hint-placeholder-count="2"><span onClick="{{ o.pick }}" style="padding:9px 16px; border-radius:100px; font-size:13px; font-weight:600; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }};">{{ o.label }}</span></sc-for> 4899 </div> 4900 </div> 4901 <div class="lot-fields-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:14px;"> 4902 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Precio sugerido</span><input value="" data-dc-attr-value="{{ f_precio }}" onInput="{{ set_precio }}" type="text" inputmode="numeric" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label> 4903 <div style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Disponibilidad de carga</span><button type="button" onClick="{{ toggleListoParaCargar }}" aria-pressed="{{ f_listoParaCargar }}" style="min-height:43px; border:1px solid {{ f_listoBorder }}; border-radius:10px; padding:9px 13px; background:{{ f_listoBg }}; color:{{ f_listoColor }}; text-align:left; font:700 13px inherit; cursor:pointer;">{{ f_listoLabel }}</button><span style="font-size:11px; color:#65758a;">Si está listo, la carga queda disponible desde la fecha de subasta.</span></div>
4904 <sc-if value="{{ f_showFechaCarga }}" hint-placeholder-val="true"><label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Fecha disponible de carga *</span><input value="" data-dc-attr-value="{{ f_dispo }}" data-dc-attr-min="{{ f_dispoMin }}" data-dc-attr-max="{{ f_dispoMax }}" onInput="{{ set_dispo }}" type="date" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"><span style="font-size:11px; color:#65758a;">Entre 1 y 7 dÃas corridos después de la subasta.</span></label></sc-if> 4905 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Desbaste (%)</span><input value="" data-dc-attr-value="{{ f_desbaste }}" onInput="{{ set_desbaste }}" type="number" min="0" max="100" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label> 4906 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Rendimiento estimado (%)</span><input value="" data-dc-attr-value="{{ f_rinde }}" onInput="{{ set_rinde }}" type="number" min="1" max="100" step="0.1" placeholder="Ej. 56" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#0a5ca8;"></label> 4907 </div> 4908 <div class="lot-fields-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:14px;"> 4909 <div><div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">Pesaje</div><div style="display:flex; gap:8px; flex-wrap:wrap;"><sc-for list="{{ weighingOptions }}" as="o" hint-placeholder-count="3"><span onClick="{{ o.pick }}" style="padding:8px 14px; border-radius:100px; font-size:12.5px; font-weight:600; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }};">{{ o.label }}</span></sc-for></div></div> 4910 <div><div style="font-size:12.5px; font-weight:600; color:#3d4c5f; margin-bottom:8px;">Plazo de pago</div><div style="display:flex; gap:8px; flex-wrap:wrap;"><sc-for list="{{ termOptions }}" as="o" hint-placeholder-count="4"><span onClick="{{ o.pick }}" style="padding:8px 14px; border-radius:100px; font-size:12.5px; font-weight:600; cursor:pointer; border:1px solid {{ o.border }}; background:{{ o.bg }}; color:{{ o.color }};">{{ o.label }}</span></sc-for></div></div> 4911 </div> 4912 <sc-if value="{{ showPlazoOtro }}" hint-placeholder-val="{{ false }}"> 4913 <label style="display:flex; flex-direction:column; gap:6px; max-width:320px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Detalle del plazo</span><input value="" data-dc-attr-value="{{ f_plazoOtro }}" onInput="{{ set_plazoOtro }}" placeholder="Especificá el plazo acordado" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label> 4914 </sc-if> 4915 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Observaciones</span><textarea value="" data-dc-attr-value="{{ f_obs }}" onInput="{{ set_obs }}" rows="3" placeholder="Estado sanitario, caracterÃsticas, certificadosâ¦" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; resize:vertical; color:#112033;" style-focus="border-color:#0a5ca8;"></textarea></label> 4916 </div> 4917 </sc-if> 4918 4919 <!-- STEP 3: VIDEOS --> 4920 <sc-if value="{{ isStep3 }}" hint-placeholder-val="{{ false }}"> 4921 <div style="font-size:11.5px; letter-spacing:0.12em; color:#e63946; font-weight:700;">FINAL</div> 4922 <h2 style="font-size:20px; font-weight:800; margin:4px 0 6px; color:#112033;">Videos del lote</h2> 4923 <div style="display:flex; align-items:flex-end; justify-content:space-between; gap:12px; flex-wrap:wrap; margin:4px 0 14px;"> 4924 <p style="font-size:13px; color:#65758a; margin:0; max-width:420px;">Subà hasta 3 videos 16:9. Elegà una portada por video y marcá cuál representa al lote.</p> 4925 <sc-if value="{{ videosAnyPending }}" hint-placeholder-val="{{ true }}"> 4926 <button onClick="{{ uploadAllVideos }}" style="display:inline-flex; align-items:center; gap:7px; background:#0a5ca8; color:#fff; border:none; border-radius:10px; padding:9px 16px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:pointer; white-space:nowrap;" style-hover="background:#084b8a;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Seleccionar varios videos</button><a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guÃa en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">â</span></a><button class="rf-camera-entry" type="button" onClick="{{ filmAssistedVideo }}">â Filmar con asistente</button> 4927 </sc-if> 4928 </div> 4929 <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(180px, 1fr)); gap:12px;"> 4930 <sc-for list="{{ videoSlots }}" as="v" hint-placeholder-count="3"> 4931 <div style="border:1px solid #e6edf5; border-radius:14px; background:#fff; overflow:hidden; box-shadow:0 4px 14px rgba(0,30,60,0.05);"> 4932 <!-- 16:9 stage --> 4933 <div style="position:relative; aspect-ratio:16/9; background:#001427; overflow:hidden;"> 4934 <sc-if value="{{ v.empty }}" hint-placeholder-val="{{ true }}"> 4935 <div onClick="{{ v.upload }}" style="position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; cursor:pointer; border:2px dashed rgba(255,255,255,0.16); border-radius:10px; margin:8px; background:radial-gradient(120% 120% at 50% 0%, rgba(10,92,168,0.28), transparent 70%);" style-hover="border-color:rgba(90,170,255,0.6);">
4936 <span style="width:34px; height:34px; border-radius:50%; background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.22); display:grid; place-items:center; color:#bcd8f5; font-size:16px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span> 4937 <span style="color:#e6f0fb; font-size:12px; font-weight:700;">Video {{ v.n }}</span> 4938 </div> 4939 </sc-if> 4940 <sc-if value="{{ v.uploading }}" hint-placeholder-val="{{ false }}"> 4941 <div style="position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:9px; padding:0 16px;"> 4942 <span style="width:15px; height:15px; border:2px solid rgba(255,255,255,0.25); border-top-color:#5aa8ff; border-radius:50%; animation:spin 0.7s linear infinite; display:inline-block;"></span> 4943 <div style="width:100%; height:6px; border-radius:100px; background:rgba(255,255,255,0.12); overflow:hidden; position:relative;"> 4944 <div style="height:100%; width:{{ v.progressPct }}; border-radius:100px; background:linear-gradient(90deg,#0a5ca8,#5aa8ff); transition:width 180ms ease;"></div> 4945 <div style="position:absolute; top:0; bottom:0; width:40%; background:linear-gradient(90deg, transparent, rgba(255,255,255,0.45), transparent); animation:loadSlide 1.1s ease-in-out infinite;"></div> 4946 </div> 4947 <div style="font-size:15px; font-weight:800; color:#fff; font-variant-numeric:tabular-nums;">{{ v.progressPct }}</div> 4948 </div> 4949 </sc-if> 4950 <sc-if value="{{ v.filled }}" hint-placeholder-val="{{ false }}"> 4951 <div style="position:absolute; inset:0; animation:fadeScale 320ms ease both;"> 4952 <div style="position:absolute; inset:0; background-image:url('{{ v.poster }}'); background-size:cover; background-position:center;"></div> 4953 <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,20,39,0.1) 40%, rgba(0,20,39,0.62));"></div> 4954 <span onClick="{{ v.remove }}" style="position:absolute; top:6px; right:6px; width:22px; height:22px; border-radius:50%; background:rgba(0,20,39,0.6); color:#fff; display:grid; place-items:center; font-size:13px; cursor:pointer;" style-hover="background:#e63946;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></span> 4955 <span style="position:absolute; inset:0; margin:auto; width:40px; height:40px; border-radius:50%; background:rgba(255,255,255,0.16); backdrop-filter:blur(6px); border:1px solid rgba(255,255,255,0.4); display:grid; place-items:center; color:#fff; font-size:15px; padding-left:3px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span> 4956 <span style="position:absolute; bottom:6px; left:8px; color:#fff; font-size:11px; font-weight:700;">Video {{ v.n }}</span> 4957 </div> 4958 </sc-if> 4959 </div> 4960 <!-- filled: cover toggle + poster row --> 4961 <sc-if value="{{ v.filled }}" hint-placeholder-val="{{ false }}"> 4962 <div style="padding:9px 10px; background:#fafcfe;"> 4963 <span onClick="{{ v.setLotCover }}" style="display:flex;align-items:center;justify-content:center;gap:5px;text-align:center; font-size:10.5px; font-weight:700; padding:6px; border-radius:8px; cursor:pointer; background:{{ v.coverBg }}; color:{{ v.coverColor }}; border:1px solid {{ v.coverBorder }}; margin-bottom:8px; transition:all 150ms;"><sc-if value="{{ v.isLotCover }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if>{{ v.coverLabel }}</span> 4964 <div style="display:flex; gap:6px;"> 4965 <sc-for list="{{ v.posterOpts }}" as="p" hint-placeholder-count="3"> 4966 <div onClick="{{ p.pick }}" style="position:relative; flex:1; min-width:0; aspect-ratio:16/9; border-radius:7px; overflow:hidden; cursor:pointer; outline:2px solid {{ p.ring }}; outline-offset:1px; transition:outline-color 150ms;"> 4967 <div style="position:absolute; inset:0; background-image:url('{{ p.url }}'); background-size:cover; background-position:center;"></div> 4968 <sc-if value="{{ p.selected }}" hint-placeholder-val="{{ false }}">
4969 <span style="position:absolute; inset:0; background:rgba(10,92,168,0.3);"></span> 4970 <span style="position:absolute; top:3px; right:3px; width:15px; height:15px; border-radius:50%; background:#0a5ca8; color:#fff; display:grid; place-items:center; font-size:9px; font-weight:800;"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></span> 4971 </sc-if> 4972 </div> 4973 </sc-for> 4974 </div> 4975 </div> 4976 </sc-if> 4977 <sc-if value="{{ v.uploading }}" hint-placeholder-val="{{ false }}"> 4978 <div style="padding:9px 10px; background:#fafcfe; display:flex; gap:6px;"> 4979 <div style="flex:1; aspect-ratio:16/9; border-radius:7px; background:linear-gradient(90deg,#eef2f7 25%,#e2e8f0 37%,#eef2f7 63%); background-size:500px 100%; animation:shimmer 1.3s linear infinite;"></div> 4980 <div style="flex:1; aspect-ratio:16/9; border-radius:7px; background:linear-gradient(90deg,#eef2f7 25%,#e2e8f0 37%,#eef2f7 63%); background-size:500px 100%; animation:shimmer 1.3s linear infinite;"></div> 4981 <div style="flex:1; aspect-ratio:16/9; border-radius:7px; background:linear-gradient(90deg,#eef2f7 25%,#e2e8f0 37%,#eef2f7 63%); background-size:500px 100%; animation:shimmer 1.3s linear infinite;"></div> 4982 </div> 4983 </sc-if> 4984 </div> 4985 </sc-for> 4986 </div> 4987 </sc-if> 4988 </div> 4989 <sc-if value="{{ formMessage }}" hint-placeholder-val="{{ false }}"> 4990 <div style="margin-top:16px; font-size:13px; text-align:center; padding:11px; border-radius:10px; background:{{ formMsgBg }}; color:{{ formMsgColor }};">{{ formMessage }}</div> 4991 </sc-if> 4992 <div class="lot-wizard-footer" style="display:flex; justify-content:space-between; gap:12px; margin-top:22px; padding-top:18px; border-top:1px solid #eef3f9;"> 4993 <button onClick="{{ prevStep }}" style="display:inline-flex;align-items:center;gap:6px;background:#eef3f9; color:#002e5f; border:1px solid #dde7f2; border-radius:12px; padding:12px 22px; font-size:14px; font-weight:700; font-family:inherit; cursor:pointer; visibility:{{ prevVisible }};" style-hover="background:#dde7f2;"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i>Atrás</button> 4994 <button onClick="{{ nextStep }}" style="display:inline-flex;align-items:center;gap:7px;background:{{ nextBg }}; color:#fff; border:none; border-radius:12px; padding:12px 26px; font-size:14px; font-weight:800; font-family:inherit; cursor:pointer; transition:transform 160ms;" style-hover="transform:translateY(-2px);" style-active="transform:scale(0.97);">{{ nextLabel }}<sc-if value="{{ nextShowsArrow }}" hint-placeholder-val="{{ true }}"><i class="dc-lucide dc-lucide-arrow-right" aria-hidden="true"></i></sc-if></button> 4995 </div> 4996 </div> 4997 <!-- LIVE PREVIEW --> 4998 <sc-if value="{{ showPreviewCard }}" hint-placeholder-val="{{ true }}"> 4999 <div style="position:{{ bidPanelPos }}; top:76px;"> 5000 <div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700; margin-bottom:10px;">PREVISUALIZACIÃN</div> 5001 <div style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 18px 50px rgba(0,30,60,0.14);"> 5002 <div style="position:relative; height:180px; background:#001d3d;"> 5003 <div style="position:absolute; inset:0; background-image:url('{{ pv_image }}'); background-size:cover; background-position:center;"></div> 5004 <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,29,61,0.05) 40%, rgba(0,29,61,0.78));"></div> 5005 <span style="position:absolute; top:10px; left:10px; background:rgba(0,29,61,0.82); color:#fff; font-size:11px; font-weight:700; padding:5px 10px; border-radius:8px;">CERT. {{ pv_cert }}</span> 5006 <div style="position:absolute; bottom:10px; left:12px; right:12px; display:flex; justify-content:space-between; align-items:flex-end; gap:8px; color:#fff;"> 5007 <div><div style="font-size:16px; font-weight:800; line-height:1.15;">{{ pv_cat }}</div><div style="font-size:11.5px; color:#c8d8ea;">{{ pv_origen }}</div></div> 5008 <div style="text-align:right; background:rgba(255,255,255,0.14); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.24); border-radius:10px; padding:6px 10px;"><div style="font-size:15px; font-weight:800; white-space:nowrap;">{{ pv_precio }}</div><div style="font-size:9.5px; color:#dbe6f2;">{{ pv_cond }}</div></div> 5009 </div> 5010 </div> 5011 <div style="padding:14px 16px; display:flex; flex-direction:column; gap:9px;"> 5012 <div style="display:flex; gap:8px;"> 5013 <div style="flex:1; background:#f2f6fb; border-radius:10px; padding:9px 12px;"><div style="font-size:10.5px; color:#8593a6;">CABEZAS</div><div style="font-size:16px; font-weight:800; color:#112033;">{{ pv_cabezas }}</div></div> 5014 <div style="flex:1; background:#f2f6fb; border-radius:10px; padding:9px 12px;"><div style="font-size:10.5px; color:#8593a6;">PESO PROM.</div><div style="font-size:16px; font-weight:800; color:#112033;">{{ pv_peso }}</div></div> 5015 </div> 5016 <div style="display:flex; flex-direction:column; gap:5px; font-size:12.5px;"> 5017 <div style="display:flex; justify-content:space-between;"><span style="color:#8593a6;">Raza</span><span style="color:#112033; font-weight:600; text-align:right;">{{ pv_razas }}</span></div> 5018 <div style="display:flex; justify-content:space-between;"><span style="color:#8593a6;">Destino</span><span style="color:#112033; font-weight:600;">{{ pv_destino }}</span></div> 5019 <div style="display:flex; justify-content:space-between;"><span style="color:#8593a6;">Completitud</span><span style="color:#0a5ca8; font-weight:700;">{{ pv_completeness }}</span></div> 5020 </div> 5021 <sc-if value="{{ pv_china }}" hint-placeholder-val="{{ false }}"><span style="width:fit-content; display:inline-flex; align-items:center; gap:6px; font-size:11.5px; font-weight:700; color:#9d2235; background:#fdeef0; border:1px solid #f6ccd2; padding:4px 10px; border-radius:100px;"><img src="assets/icons/china-flag.svg" alt="" aria-hidden="true" width="16" height="12" style="display:block; width:16px; height:12px; object-fit:cover; border-radius:2px;">Apto China</span></sc-if> 5022 </div> 5023 </div> 5024 </div> 5025 </sc-if> 5026 </div> 5027 <div style="height:40px;"></div> 5028 </div> 5029 </sc-if> 5030 5031 5032 <!-- ============ FERIA ? CREADOR DE REMATE ============ --> 5033 <sc-if class="advance-public-hidden" value="{{ isFeria }}" hint-placeholder-val="{{ false }}"> 5034 <div style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1);"> 5035 <style> 5036 .feria-result-screen{position:fixed;inset:0;z-index:90;box-sizing:border-box;overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;background:linear-gradient(150deg,#001328,#002e5f 55%,#0a5ca8);display:flex;padding:clamp(16px,3vh,32px) max(16px,env(safe-area-inset-right)) max(20px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));} 5037 .feria-result-content{width:100%;max-width:560px;min-width:0;margin:auto;text-align:center;color:#fff;overflow-wrap:anywhere;} 5038 .feria-result-back{display:inline-flex;align-items:center;gap:8px;min-height:44px;padding:10px 14px;margin:0 0 16px;border:1px solid #ffffff50;border-radius:10px;background:#ffffff12;color:#fff;font:600 14px inherit;cursor:pointer;} 5039 .feria-result-back:focus-visible{outline:3px solid #fff;outline-offset:3px;} 5040 .feria-result-screen .feria-success-symbol{width:64px!important;height:64px!important;font-size:32px!important;margin-bottom:12px!important;} 5041 .feria-result-screen .feria-result-image{aspect-ratio:16/9;max-height:240px;min-height:130px;} 5042 .feria-result-actions{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;margin-top:18px;} 5043 .feria-result-actions>a,.feria-result-actions>button{box-sizing:border-box;flex:1 1 160px;min-height:44px;justify-content:center;text-align:center;} 5044 .feria-result-data{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:16px;} 5045 @media(max-width:480px){.feria-result-screen h1{font-size:24px!important;margin-bottom:16px!important}.feria-result-data{grid-template-columns:1fr}.feria-result-actions>a,.feria-result-actions>button{flex-basis:100%}.feria-result-image strong{font-size:17px!important}.feria-result-body{padding:14px!important}} 5046 @media(max-height:600px){.feria-result-screen .feria-success-symbol{width:44px!important;height:44px!important;font-size:24px!important}.feria-result-screen .feria-result-image{max-height:170px}.feria-result-screen{padding-top:12px;}} 5047 </style> 5048 <!-- PROCESSING OVERLAY --> 5049 <sc-if value="{{ feriaProcessing }}" hint-placeholder-val="{{ false }}"> 5050 <div class="feria-result-screen" role="region" aria-label="Estado de la transmisión"> 5051 <div class="feria-result-content"> 5052 <sc-if value="{{ feriaBusy }}" hint-placeholder-val="{{ true }}"> 5053 <div style="width:120px; height:120px; margin:0 auto 26px; position:relative;"> 5054 <div style="position:absolute; inset:0; border-radius:50%; border:4px solid rgba(255,255,255,0.12);"></div> 5055 <div style="position:absolute; inset:0; border-radius:50%; border:4px solid transparent; border-top-color:#fff; border-right-color:rgba(255,255,255,0.6); animation:spin 1s linear infinite;"></div> 5056 <div style="position:absolute; inset:22px; border-radius:50%; background:rgba(255,255,255,0.08); display:grid; place-items:center; font-size:34px;"><i class="dc-lucide dc-lucide-radio-tower" aria-hidden="true"></i></div>
5057 <span style="position:absolute; inset:0; border-radius:50%; box-shadow:0 0 0 0 rgba(255,255,255,0.4); animation:bidPulse 2s ease-out infinite;"></span> 5058 </div> 5059 <div style="display:flex; gap:8px; justify-content:center; margin-bottom:20px;"> 5060 <sc-for list="{{ feriaSteps }}" as="ps" hint-placeholder-count="3"> 5061 <div style="display:flex; align-items:center; gap:8px; font-size:12.5px; color:{{ ps.color }}; font-weight:600;"><span style="width:20px; height:20px; border-radius:50%; background:{{ ps.dotBg }}; color:{{ ps.dotColor }}; display:grid; place-items:center; font-size:11px;"><sc-if value="{{ ps.done }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if><sc-if value="{{ ps.numberVisible }}" hint-placeholder-val="{{ true }}">{{ ps.number }}</sc-if></span>{{ ps.label }}</div> 5062 </sc-for> 5063 </div> 5064 <h1 style="font-size:clamp(24px, 3.4vw, 32px); font-weight:800; letter-spacing:-0.02em; margin:0 0 8px;">{{ feriaPhaseTitle }}</h1> 5065 <p style="font-size:14.5px; color:#c8d8ea; margin:0; line-height:1.5; max-width:400px; margin:0 auto;">{{ feriaPhaseText }}</p> 5066 </sc-if> 5067 <sc-if value="{{ feriaDone }}" hint-placeholder-val="{{ false }}"><div style="text-align:left;"><button type="button" class="feria-result-back" onClick="{{ backFromFeriaResult }}"><span aria-hidden="true">â</span> Volver a mi perfil</button></div> 5068 <div class="feria-success-symbol" style="width:96px; height:96px; margin:0 auto 22px; border-radius:50%; background:rgba(31,214,143,0.16); border:2px solid #5fd68f; color:#38b977; display:grid; place-items:center; font-size:44px; animation:fadeScale 500ms cubic-bezier(0.22,0.72,0.18,1) both;"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></div> 5069 <div style="font-size:11.5px; letter-spacing:0.14em; color:#9fc0e2; font-weight:700;">TRANSMISIÃN CREADA</div> 5070 <h1 style="font-size:clamp(24px, 3.4vw, 32px); font-weight:800; letter-spacing:-0.02em; margin:4px 0 22px;">Tu transmisión está lista</h1> 5071 <div style="background:#fff; border-radius:20px; overflow:hidden; box-shadow:0 30px 80px rgba(0,20,45,0.4); color:#112033; text-align:left; animation:floatIn 500ms cubic-bezier(0.22,0.72,0.18,1) 120ms both;"> 5072 <div class="feria-result-image" style="position:relative;"> 5073 <div style="position:absolute; inset:0; background-image:url('{{ resultThumb }}'); background-size:cover; background-position:center;"></div> 5074 <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,29,61,0.04) 45%, rgba(0,29,61,0.78));"></div> 5075 <span style="position:absolute; top:12px; left:12px; display:inline-flex; align-items:center; gap:7px; background:rgba(230,57,70,0.94); color:#fff; font-size:11.5px; font-weight:800; padding:6px 13px; border-radius:100px; letter-spacing:0.05em;"><span style="width:7px; height:7px; border-radius:50%; background:#fff; animation:pulseDot 1.2s infinite;"></span>EN VIVO</span> 5076 <sc-if value="{{ resultHasLogo }}" hint-placeholder-val="{{ false }}"><span style="position:absolute; top:12px; right:12px; width:42px; height:42px; border-radius:11px; background:#fff; overflow:hidden;"><div style="width:100%; height:100%; background-image:url('{{ resultLogo }}'); background-size:contain; background-position:center; background-repeat:no-repeat; padding:5px; box-sizing:border-box;"></div></span></sc-if> 5077 <strong style="position:absolute; bottom:12px; left:16px; right:16px; color:#fff; font-size:19px; text-shadow:0 1px 6px rgba(0,0,0,0.4);">{{ resultTitle }}</strong> 5078 </div> 5079 <div class="feria-result-body" style="padding:18px 20px;"> 5080 <div style="font-size:11px; color:#8593a6; letter-spacing:0.06em; font-weight:600;">ENLACE DE TRANSMISIÃN</div> 5081 <div style="display:flex; gap:8px; margin-top:8px;"> 5082 <input value="" data-dc-attr-value="{{ resultUrl }}" readonly style="flex:1; min-width:0; border:1px solid #e3eaf2; border-radius:10px; padding:11px 13px; font-size:13px; font-family:inherit; color:#0a5ca8; background:#f6f9fc; outline:none;">
5083 <button onClick="{{ copyResult }}" style="background:#002e5f; color:#fff; border:none; border-radius:10px; padding:11px 18px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer; white-space:nowrap;" style-hover="background:#0a5ca8;">Copiar</button> 5084 </div> 5085 <div class="feria-result-data"> 5086 <div style="background:#f6f9fc; border-radius:10px; padding:10px 13px;"><div style="font-size:10.5px; color:#8593a6;">CONSIGNATARIA</div><div style="font-size:13.5px; font-weight:700; color:#112033;">{{ resultCons }}</div></div> 5087 <div style="background:#f6f9fc; border-radius:10px; padding:10px 13px;"><div style="font-size:10.5px; color:#8593a6;">FECHA</div><div style="font-size:13.5px; font-weight:700; color:#112033;">{{ resultDate }}</div></div> 5088 </div> 5089 </div> 5090 </div> 5091 <div class="feria-result-actions"> 5092 <a href="{{ resultUrl }}" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:7px;background:#fff; color:#002e5f; padding:13px 26px; border-radius:12px; font-weight:700; font-size:14.5px;" style-hover="transform:translateY(-2px); color:#002e5f;">Abrir en YouTube <i class="dc-lucide dc-lucide-arrow-right" aria-hidden="true"></i></a> 5093 <button onClick="{{ shareFeriaFlyer }}" style="display:inline-flex; align-items:center; justify-content:center; gap:8px; background:#fff; color:#002e5f; border:0; padding:13px 22px; border-radius:12px; font-weight:700; font-size:14.5px; font-family:inherit; cursor:pointer;"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" style="display:block;flex-shrink:0;"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 10.5 6.8-4m-6.8 7 6.8 4"/></svg><span>Compartir flyer</span></button> 5094 <button onClick="{{ resetCreator }}" style="background:rgba(255,255,255,0.12); border:1px solid rgba(255,255,255,0.3); color:#fff; padding:13px 26px; border-radius:12px; font-weight:700; font-size:14.5px; font-family:inherit; cursor:pointer;" style-hover="background:rgba(255,255,255,0.22);">Crear otra feria</button> 5095 </div> 5096 </sc-if> 5097 <sc-if value="{{ feriaFailed }}" hint-placeholder-val="{{ false }}"> 5098 <div style="width:96px; height:96px; margin:0 auto 22px; border-radius:50%; background:rgba(230,57,70,0.16); border:2px solid #ff8f98; color:#e63946;display:grid; place-items:center; font-size:44px;"><i class="dc-lucide dc-lucide-circle-x" aria-hidden="true"></i></div> 5099 <h1 style="font-size:clamp(22px, 3vw, 28px); font-weight:800; margin:0 0 8px;">{{ feriaFailureTitle }}</h1> 5100 <p style="font-size:14px; color:#ffd3d7; margin:0 0 22px;">{{ feriaError }}</p> 5101 <sc-if value="{{ feriaCanRetry }}"><button onClick="{{ retryFeriaYoutube }}" style="background:#fff;color:#002e5f;padding:13px 26px;border-radius:12px;font-weight:700;margin:0 12px 12px 0;cursor:pointer;">{{ feriaRetryLabel }}</button><button onClick="{{ deleteFeria }}" disabled="{{ feriaDeleting }}" style="background:transparent;color:#ffd3d7;border:1px solid #ff8f98;padding:13px 26px;border-radius:12px;font-weight:700;margin:0 12px 12px 0;cursor:pointer;">{{ feriaDeleteLabel }}</button></sc-if> 5102 <button onClick="{{ resetCreator }}" style="background:#fff; color:#002e5f; padding:13px 26px; border-radius:12px; font-weight:700; font-size:14.5px; font-family:inherit; cursor:pointer;">Volver al formulario</button> 5103 </sc-if> 5104 </div> 5105 </div> 5106 </sc-if> 5107 5108 <!-- CREATOR --> 5109 <div style="padding:0 clamp(14px, 4vw, 36px); max-width:1200px; margin:0 auto;"> 5110 <div style="padding:26px 0 18px;"> 5111 <div style="font-size:11.5px; letter-spacing:0.14em; color:#1f7a63; font-weight:700;">ROSGAN FERIA</div> 5112 <h1 style="font-size:clamp(26px, 3.6vw, 38px); font-weight:800; letter-spacing:-0.025em; margin:4px 0 6px; color:#112033;">{{ feriaHeading }}</h1><sc-if value="{{ feriaEditingId }}"><button type="button" onClick="{{ cancelFeriaEdit }}" style="border:1px solid #ccdceb;border-radius:9px;background:#fff;color:#003767;padding:8px 12px;cursor:pointer;">Cancelar edición</button></sc-if> 5113 <p style="font-size:14.5px; color:#65758a; margin:0; max-width:560px; line-height:1.5;">Cargá los datos y generamos la transmisión en vivo de YouTube con su portada automáticamente.</p> 5114 </div> 5115 <sc-if value="{{ feriaDevicesAllowed }}"> 5116 <div class="feria-control-actions"> 5117 <button type="button" class="feria-control-button is-youtube" onClick="{{ c
5117ontrolFeriaYoutube }}"><img src="/assets/icons/youtube.svg" alt="" aria-hidden="true" width="22" height="22"><span>Controlar transmisiones de YouTube</span></button> 5118 <button type="button" class="feria-control-button" onClick="{{ goFeriaConfig }}"><img src="/assets/icons/lucide/sliders-horizontal.svg" alt="" aria-hidden="true" width="18" height="18"><span>Control de dispositivos</span><span aria-hidden="true">â</span></button> 5119 </div> 5120 </sc-if> 5121 <div class="feria-creator-layout" style="display:grid; grid-template-columns:{{ feriaCols }}; gap:22px; align-items:start; padding-bottom:44px;"> 5122 <!-- FORM --> 5123 <div class="feria-form-card" style="background:#fff; border-radius:22px; padding:clamp(20px, 2.8vw, 30px); box-shadow:0 14px 44px rgba(0,30,60,0.09); display:flex; flex-direction:column; gap:20px;"> 5124 <div> 5125 <div style="font-size:12px; font-weight:700; color:#3d4c5f; margin-bottom:10px;">Consignataria *</div> 5126 <div class="feria-consignataria-grid" style="display:grid; grid-template-columns:repeat(auto-fill, minmax(130px, 1fr)); gap:10px;"> 5127 <sc-for list="{{ consChips }}" as="c" hint-placeholder-count="6"> 5128 <div onClick="{{ c.pick }}" style="display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:12px; cursor:{{ c.cursor }}; border:1.5px solid {{ c.border }}; background:{{ c.bg }}; transition:all 160ms;" style-hover="border-color:#1f7a63;"> 5129 <span style="width:32px; height:32px; border-radius:8px; background:{{ c.logoBg }}; overflow:hidden; flex-shrink:0; display:grid; place-items:center; padding:4px; box-sizing:border-box;"><img data-dc-attr-src="{{ c.whiteLogo }}" alt="" aria-hidden="true" style="display:block; width:100%; height:100%; object-fit:contain; filter:brightness(0) invert(1);"></span> 5130 <span style="font-size:12px; font-weight:600; color:#112033; line-height:1.2; overflow:hidden;">{{ c.name }}</span> 5131 </div> 5132 </sc-for> 5133 </div> 5134 <sc-if value="{{ feriaConsScopeText }}"><div style="margin-top:8px; color:{{ feriaConsScopeColor }}; font-size:11.5px; line-height:1.4;">{{ feriaConsScopeText }}</div></sc-if> 5135 </div> 5136 <sc-if value="{{ feriaHasConsignataria }}"> 5137 <div class="feria-favorites" style="--feria-brand:{{ feriaFavoriteColor }};"> 5138 <strong style="font-size:13px; color:#153955;">Tus Ferias</strong> 5139 <p style="font-size:12px; color:#65758a; margin:5px 0 12px;">{{ feriaFavoritesHint }}</p> 5140 <style> 5141 .feria-favorite-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px} 5142 .feria-favorite{position:relative;display:flex;align-items:center;gap:12px;text-align:left;padding:14px 12px;min-height:94px;border:1px solid #dce7ee;border-radius:16px;background:linear-gradient(135deg,#fff,#f1f7f8);color:#153955;font:inherit;cursor:pointer;transition:border-color .18s,box-shadow .18s} 5143 .feria-favorite{background:linear-gradient(135deg,#fff,color-mix(in srgb,var(--feria-brand) 4%,white))} 5144 .feria-favorite:hover{border-color:var(--feria-brand);box-shadow:0 5px 16px #123c4910}.feria-favorite:focus-visible{outline:3px solid #338dba;outline-offset:3px} 5145 .feria-favorite[aria-pressed="true"]{border-color:var(--feria-brand);background:color-mix(in srgb,var(--feria-brand) 7%,white);box-shadow:inset 0 0 0 1px var(--feria-brand)} 5146 .feria-favorite-emblem{position:relative;width:58px;height:58px;flex:0 0 58px} 5147 .feria-favorite-map{display:block;width:100%;height:100%;background:var(--feria-brand);opacity:.65;mask:var(--feria-map) center/contain no-repeat;-webkit-mask:var(--feria-map) center/contain no-repeat} 5148 .feria-favorite[aria-pressed="true"] .feria-favorite-map{opacity:1} 5149 .feria-favorite-logo{position:absolute;right:-2px;bottom:-2px;display:grid;place-items:center;width:26px;height:26px;padding:3px;box-sizing:border-box;border-radius:9px;background:#fff;border:1px solid color-mix(in srgb,var(--feria-brand) 20%,white);box-shadow:0 2px 5px #11203314} 5150 .feria-favorite-logo img{width:100%;height:100%;object-fit:contain} 5151 .feria-favorite-copy{display:grid;gap:4px;min-width:0}.feria-favorite-copy strong{font-size:13px;line-height:1.3;overflow-wrap:anywhere}.feria-favorite-copy small{font-size:10px;color:#617d8c;line-height:1.3}.feria-favorite-check{position:absolute;right:8px;top:6px;color:var(--feria-brand);font-size:12px} 5152 @media(max-width:400px){.feria-favorite-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.feria-favorite{gap:7px;padding:12px 9px}.feria-favorite-emblem{width:42px;height:48px;flex-basis:42px}.feria-favorite-logo{width:22px;height:22px;border-radius:7px}} 5153 </style> 5154 <div class="feria-favorite-grid"> 5155 <sc-for list="{{ feriaFavorites }}" as="f"> 5156 <button type="button" class="feria-favorite" onClick="{{ f.pick }}" aria-pressed="{{ f.selected }}">
5157 <span class="feria-favorite-emblem" aria-hidden="true"> 5158 <span class="feria-favorite-map" style="--feria-map:url('{{ f.mapSrc }}');"></span> 5159 <sc-if value="{{ feriaFavoriteLogo }}"><span class="feria-favorite-logo"><img src="{{ feriaFavoriteLogo }}" alt="" width="20" height="20" /></span></sc-if> 5160 </span> 5161 <span class="feria-favorite-copy"><strong>{{ f.localidad }}</strong><small>{{ f.provincia }}</small><sc-if value="{{ f.predio }}"><small>{{ f.predio }}</small></sc-if></span> 5162 <sc-if value="{{ f.selected }}"><span class="feria-favorite-check" aria-hidden="true">â</span></sc-if> 5163 </button> 5164 </sc-for> 5165 </div> 5166 <button type="button" onClick="{{ feriaManualLocation }}" style="margin-top:10px; padding:8px 0; border:0; background:transparent; color:#1f7a63; font:inherit; font-size:12px; font-weight:700; cursor:pointer;">Usar otra ubicación</button> 5167 </div> 5168 </sc-if> 5169 <sc-if value="{{ feriaManualOpen }}"> 5170 <div style="display:grid;gap:20px;"> 5171 <div class="feria-field-grid" style="display:grid; grid-template-columns:{{ feriaGrid2 }}; gap:14px;"> 5172 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Provincia *</span> 5173 <select value="" data-dc-attr-value="{{ ff_provincia }}" onChange="{{ ff_setProvincia }}" style="border:1px solid #e3eaf2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; background:#fff;" style-focus="border-color:#1f7a63;"> 5174 <option value="">{{ provPlaceholder }}</option> 5175 <sc-for list="{{ provinceOpts }}" as="p" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ p }}">{{ p }}</option></sc-for> 5176 </select></label> 5177 <label style="position:relative; display:flex; flex-direction:column; gap:6px;"><span style="display:flex; align-items:center; justify-content:space-between; gap:8px; font-size:12px; font-weight:600; color:#3d4c5f;"><span>Localidad *</span><small style="display:inline-flex; align-items:center; gap:4px; color:#718296; font-weight:600;"><img src="assets/google-logo.webp" alt="" style="width:12px; height:12px; object-fit:contain;">Google</small></span> 5178 <input value="" data-dc-attr-value="{{ ff_localidad }}" onInput="{{ ff_setLocalidad }}" onFocus="{{ ff_openLocalidades }}" onBlur="{{ ff_closeLocalidades }}" disabled="{{ locDisabled }}" autocomplete="off" placeholder="{{ locPlaceholder }}" style="border:1px solid #e3eaf2; border-radius:10px; padding:11px 38px 11px 13px; font-size:14px; font-family:inherit; outline:none; background:{{ locBg }};" style-focus="border-color:#1f7a63;"> 5179 <sc-if value="{{ feriaPlaceLoading }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true" style="position:absolute; right:13px; top:38px; color:#1f7a63;"></i></sc-if> 5180 <sc-if value="{{ feriaPlaceResultsOpen }}" hint-placeholder-val="{{ false }}"><div role="listbox" aria-label="Localidades sugeridas por Google" style="position:absolute; z-index:50; left:0; right:0; top:calc(100% + 5px); padding:5px; border:1px solid #d6e2ed; border-radius:11px; background:#fff; box-shadow:0 14px 30px rgba(0,35,70,.18); max-height:220px; overflow:auto;"><sc-for list="{{ feriaPlaceResults }}" as="p" hint-placeholder-count="4"><button type="button" role="option" onClick="{{ p.pick }}" style="width:100%; display:grid; gap:2px; border:0; border-radius:8px; background:#fff; padding:8px 9px; text-align:left; cursor:pointer;"><strong style="font-size:11.5px; color:#153955;">{{ p.label }}</strong><span style="font-size:9.5px; color:#718296;">{{ p.address }}</span></button></sc-for></div></sc-if> 5181 </label> 5182 </div> 5183 <sc-if value="{{ feriaPlaceError }}" hint-placeholder-val="{{ false }}">
5183<div style="margin-top:-7px; color:#8a5a00; font-size:10.5px;">{{ feriaPlaceError }}</div></sc-if> 5184 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Nombre del remate</span> 5185 <input value="" data-dc-attr-value="{{ ff_nombre }}" onInput="{{ ff_setNombre }}" placeholder="Se autocompleta con la localidad" style="border:1px solid #e3eaf2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#1f7a63;"></label> 5186 </div> 5187 </sc-if> 5188 <div class="feria-field-grid" style="display:grid; grid-template-columns:{{ feriaGrid3 }}; gap:14px;"> 5189 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Fecha *</span> 5190 <input type="date" value="" data-dc-attr-value="{{ ff_fecha }}" onInput="{{ ff_setFecha }}" style="border:1px solid #e3eaf2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#1f7a63;"></label> 5191 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Hora *</span> 5192 <input type="time" value="" data-dc-attr-value="{{ ff_hora }}" onInput="{{ ff_setHora }}" style="border:1px solid #e3eaf2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#1f7a63;"></label> 5193 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12px; font-weight:600; color:#3d4c5f;">Cabezas *</span> 5194 <input type="number" min="0" value="" data-dc-attr-value="{{ ff_cabezas }}" onInput="{{ ff_setCabezas }}" placeholder="1200" style="border:1px solid #e3eaf2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;" style-focus="border-color:#1f7a63;"></label> 5195 </div> 5196 <sc-if value="{{ feriaFormMessage }}" hint-placeholder-val="{{ false }}"><div style="font-size:13px; padding:11px 14px; border-radius:10px; background:rgba(230,57,70,0.1); color:#c22;">{{ feriaFormMessage }}</div></sc-if> 5197 <button onClick="{{ submitFeria }}" style="background:{{ feriaBtnBg }}; color:#fff; border:none; border-radius:14px; padding:16px; font-size:16px; font-weight:800; font-family:inherit; cursor:pointer; letter-spacing:0.01em; transition:transform 160ms; display:flex; align-items:center; justify-content:center; gap:10px;" style-hover="transform:translateY(-2px);" style-active="transform:scale(0.98);"><span style="font-size:18px;"><i class="dc-lucide dc-lucide-radio-tower" aria-hidden="true"></i></span>{{ feriaSubmitLabel }}</button> 5198 </div> 5199 <!-- LIVE PREVIEW --> 5200 <div class="feria-preview-card" style="position:{{ feriaPreviewPos }}; top:20px; display:flex; flex-direction:column; gap:14px;"> 5201 <div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700; padding-left:2px;">VISTA PREVIA DE LA PORTADA DE YOUTUBE</div> 5202 <rosgan-feria-thumbnail auction="{{ feriaThumbnailData }}" style="display:block;aspect-ratio:16/9;border-radius:20px;overflow:hidden;box-shadow:0 20px 56px rgba(0,30,60,.15);"></rosgan-feria-thumbnail> 5203 <div style="background:#fff; border-radius:14px; padding:14px 16px; box-shadow:0 8px 26px rgba(0,30,60,0.07);"> 5204 <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;"><span style="font-size:12px; font-weight:700; color:#3d4c5f;">Datos completos</span><span style="font-size:13px; font-weight:800; color:#1f7a63;">{{ feriaPct }}</span></div> 5205 <div style="height:7px; border-radius:100px; background:#eef3f9; overflow:hidden;"><div style="height:100%; width:{{ feriaPct }}; border-radius:100px; background:linear-gradient(90deg,#1f7a63,#0a5ca8); transition:width 400ms cubic-bezier(0.22,0.72,0.18,1);"></div></div> 5206 </div> 5207 <sc-if value="{{ hasCreatedFerias }}" hint-placeholder-val="{{ false }}"> 5208 <div style="background:#fff; border-radius:14px; padding:14px 16px; box-shadow:0 8px 26px rgba(0,30,60,0.07);"> 5209 <div style="font-size:11.5px; letter-spacing:0.1em; color:#8593a6; font-weight:700; margin-bottom:10px;">CREADAS EN ESTA SESIÃN</div> 5210 <div style="display:flex; flex-direction:column; gap:9px;"> 5211 <sc-for list="{{ createdFeriaList }}" as="cf" hint-placeholder-count="2"> 5212 <a href="{{ cf.href }}" target="_blank" rel="noopener" style="display:flex; align-items:center; gap:10px; text-decoration:none;">
5213 <span style="width:9px; height:9px; border-radius:50%; background:#1f7a63; flex-shrink:0;"></span> 5214 <span style="flex:1; min-width:0; font-size:13px; font-weight:600; color:#112033; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ cf.title }}</span> 5215 <span style="display:inline-flex;align-items:center;gap:4px;font-size:12px; color:#0a5ca8; font-weight:600;">Ver <i class="dc-lucide dc-lucide-arrow-right" aria-hidden="true"></i></span> 5216 </a> 5217 </sc-for> 5218 </div> 5219 </div> 5220 </sc-if> 5221 </div> 5222 </div> 5223 </div> 5224 </div> 5225 </sc-if> 5226 5227 5228 <sc-if value="{{ isFeriaConfig }}" hint-placeholder-val="{{ false }}"> 5229 <div style="max-width:1240px;margin:0 auto;padding:28px clamp(16px,4vw,36px) 56px;animation:pageIn 300ms ease;"> 5230 <button onClick="{{ goFeria }}" style="border:none;background:transparent;padding:0;margin:0 0 20px;color:#64758a;font:600 13px inherit;cursor:pointer;">â Volver a Feria</button> 5231 <rosgan-feria-devices></rosgan-feria-devices> 5232 </div> 5233 </sc-if> 5234 5235 <!-- ============ PERFIL ? DASHBOARD DE MERCADO ============ --> 5236 <sc-if value="{{ isDevActivityPage }}"><rosgan-dev-activity-page></rosgan-dev-activity-page></sc-if> 5237 <sc-if value="{{ isProfileArea }}" hint-placeholder-val="{{ false }}"> 5238 <div class="profile-area" style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1); padding:0 clamp(14px, 4vw, 36px); max-width:1240px; margin:0 auto;"> 5239 <sc-if value="{{ profileDashboardVisible }}" hint-placeholder-val="{{ true }}"> 5240 <!-- hero --> 5241 <div class="profile-hero" style="position:relative; overflow:hidden; border-radius:22px; margin:24px 0 18px; padding:clamp(20px, 3vw, 30px); background:linear-gradient(120deg, #001d3d 0%, #0a3e78 60%, #0a5ca8 100%); color:#fff; display:flex; align-items:center; gap:18px; flex-wrap:wrap;"> 5242 <div style="position:absolute; right:-40px; top:-40px; width:220px; height:220px; border-radius:50%; background:radial-gradient(circle, rgba(255,255,255,0.1), transparent 70%);"></div> 5243 <span class="profile-hero-avatar" style="color:#fff; width:66px; height:66px; border-radius:50%; background:{{ userAvatarBg }}; border:1px solid {{ userAvatarBorder }}; display:grid; place-items:center; font-size:23px; font-weight:800; flex-shrink:0; z-index:1; overflow:hidden;"> 5244 <sc-if value="{{ userHasConsignatariaAvatar }}" hint-placeholder-val="false"><img data-profile-avatar="true" data-dc-attr-src="{{ userConsignatariaAvatar }}" alt="{{ userConsignatariaName }}" onClick="{{ openProfilePhotoViewer }}" title="Ver foto completa" style="width:{{ userAvatarHeroSize }}; height:{{ userAvatarHeroSize }}; object-fit:{{ userAvatarObjectFit }}; object-position:center; cursor:{{ profileAvatarCursor }};"></sc-if> 5245 <sc-if value="{{ userShowsInitials }}" hint-placeholder-val="true">{{ userInitials }}</sc-if> 5246 </span> 5247 <div class="profile-identity" style="flex:1; min-width:180px; z-index:1;"> 5248 <div class="profile-kicker" style="font-size:11px; letter-spacing:0.14em; color:#9fc0e2; font-weight:700;">{{ profileKicker }}</div> 5249 <h1 style="font-size:clamp(22px, 3vw, 30px); font-weight:800; letter-spacing:-0.02em; margin:3px 0 3px;">{{ profileName }}</h1> 5250 <sc-if value="{{ profileIsDev }}"><button type="button" onClick="{{ openDevActivity }}" style="margin:12px 0 6px;padding:11px 16px;border:1px solid #cbdbea;border-radius:10px;background:#073d70;color:white;font:inherit;cursor:pointer;">Panel Dev · Actividad y usuarios online</button><span style="display:block;margin:0 0 12px;font-size:12px;color:#c4def4;overflow-wrap:anywhere;">{{ profileCurrentVersionLabel }}</span></sc-if> 5251 <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:5px;"> 5252 <span class="profile-email" style="display:inline-flex; align-items:center; gap:6px; font-size:13px; color:#c8d8ea;"><img src="assets/icons/lucide/mail.svg" alt="" style="width:14px; height:14px; filter:brightness(0) invert(1); opacity:0.82;">{{ profileEmail }}</span> 5253 <sc-for list="{{ profileRoleBadges }}" as="role" hint-placeholder-count="1">
5254 <span style="display:inline-flex; align-items:center; gap:6px; padding:5px 9px; border-radius:100px; background:{{ role.badgeBg }}; border:1px solid {{ role.badgeBorder }}; color:{{ role.badgeColor }}; font-size:11px; font-weight:800;"> 5255 <img data-dc-attr-src="{{ role.icon }}" alt="" title="{{ role.iconTitle }}" style="width:15px; height:15px; padding:{{ role.iconPadding }}; border-radius:{{ role.iconRadius }}; background:{{ role.iconBg }}; object-fit:contain; filter:{{ role.iconFilter }};">{{ role.label }} 5256 </span> 5257 </sc-for> 5258 </div> 5259 </div> 5260 <div class="profile-actions" style="display:flex; gap:10px; z-index:1;"> 5261 <sc-if value="{{ profileCanManageUsers }}" hint-placeholder-val="{{ false }}"> 5262 <button class="profile-action {{ profileUsersSelected }}" data-dc-attr-aria-pressed="{{ adminUsersOpen }}" data-dc-attr-aria-expanded="{{ adminUsersOpen }}" onClick="{{ toggleAdminUsers }}" title="Administrar usuarios" style="display:inline-flex; align-items:center; gap:7px; background:rgba(255,255,255,0.14); color:#fff; border:1px solid rgba(255,255,255,0.25); padding:10px 14px; border-radius:12px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:pointer;"> 5263 <img src="assets/icons/lucide/users-round.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">Usuarios 5264 </button> 5265 <button class="profile-action {{ profileMailingSelected }}" data-dc-attr-aria-pressed="{{ mailingOpen }}" data-dc-attr-aria-expanded="{{ mailingOpen }}" onClick="{{ toggleMailing }}" title="Administrar mailing" style="display:inline-flex; align-items:center; gap:7px; background:rgba(255,255,255,0.14); color:#fff; border:1px solid rgba(255,255,255,0.25); padding:10px 14px; border-radius:12px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:pointer;"> 5266 <img src="assets/icons/lucide/mails.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">Mailing 5267 </button> 5268 </sc-if> 5269 <button class="profile-action {{ profileEditorSelected }}" data-dc-attr-aria-pressed="{{ profileEditorOpen }}" data-dc-attr-aria-expanded="{{ profileEditorOpen }}" onClick="{{ openProfileEditor }}" title="Editar perfil" style="display:inline-flex; align-items:center; gap:7px; background:rgba(255,255,255,0.14); color:#fff; border:1px solid rgba(255,255,255,0.25); padding:10px 14px; border-radius:12px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:pointer;"> 5270 <img src="assets/icons/lucide/user-pen.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">Editar perfil 5271 </button> 5272 <button class="profile-action" onClick="{{ enableNotifications }}" disabled="{{ notificationsBusy }}" title="{{ notificationsTitle }}" style="display:inline-flex; align-items:center; gap:7px; background:{{ notificationsBg }}; color:#fff; border:1px solid rgba(255,255,255,0.25); padding:10px 14px; border-radius:12px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:{{ notificationsCursor }}; opacity:{{ notificationsOpacity }};"> 5273 <img src="assets/icons/lucide/bell.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">{{ notificationsLabel }} 5274 </button> 5275 <button class="profile-action" onClick="{{ toggleWhatsAppNotifications }}" disabled="{{ whatsappNotificationsBusy }}" title="{{ whatsappNotificationsTitle }}" style="display:inline-flex; align-items:center; gap:7px; background:{{ whatsappNotificationsBg }}; color:#fff; border:1px solid rgba(255,255,255,0.25); padding:10px 14px; border-radius:12px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:{{ whatsappNotificationsCursor }}; opacity:{{ whatsappNotificationsOpacity }};"> 5276 <img src="assets/icons/lucide/phone.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">{{ whatsappNotificationsLabel }} 5277 </button> 5278 <sc-if value="{{ profileCanSell }}" hint-placeholder-val="{{ false }}"><button type="button" class="profile-action profile-action-load" onClick="{{ goLoad }}" style="display:inline-flex;align-items:center;justify-content:center;gap:7px;background:#fff;color:#003d73;border:1px solid #fff;padding:10px 18px;border-radius:12px;font-family:inherit;font-size:13.5px;font-weight:700;cursor:pointer;" style-hover="background:#e7f1fb;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i>Cargar lote</button></sc-if>
5279 <span class="profile-action profile-action-logout" onClick="{{ logout }}" style="background:#fff; color:#002e5f; padding:10px 18px; border-radius:12px; font-size:13.5px; font-weight:700; cursor:pointer;" style-hover="transform:translateY(-2px);">Salir</span> 5280 </div> 5281 </div> 5282 5283 <sc-if data-public-blocked-overlay value="{{ profileEditorOpen }}" hint-placeholder-val="{{ false }}"> 5284 <div class="profile-editor-overlay" style="position:fixed; inset:0; z-index:120; background:rgba(0,20,45,0.58); backdrop-filter:blur(5px); display:grid; place-items:center; padding:18px; overflow:auto;"> 5285 <section class="profile-editor-dialog" role="dialog" aria-modal="true" aria-label="Editar perfil" style="width:min(620px, 100%); background:#fff; border-radius:12px; box-shadow:0 24px 70px rgba(0,20,45,0.32); overflow:hidden; animation:floatIn 240ms both;"> 5286 <header style="display:flex; align-items:center; justify-content:space-between; gap:16px; padding:18px 20px; border-bottom:1px solid #e6edf5;"> 5287 <div><div style="font-size:10.5px; letter-spacing:0.13em; color:#0a5ca8; font-weight:800;">MI CUENTA</div><h2 style="font-size:21px; margin:3px 0 0; color:#112033;">Editar perfil</h2></div> 5288 <button onClick="{{ closeProfileEditor }}" disabled="{{ profileEditorSaving }}" title="Cerrar" style="width:36px; height:36px; border:1px solid #d7e2ee; border-radius:50%; background:#fff; display:grid; place-items:center; cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="Cerrar" style="width:18px; height:18px;"></button> 5289 </header> 5290 <div style="padding:20px; display:grid; gap:18px;"> 5291 <div style="display:flex; align-items:center; gap:16px; flex-wrap:wrap;"> 5292 <span style="width:78px; height:78px; border-radius:50%; background:#edf4fb; border:1px solid #d4e2ef; display:grid; place-items:center; overflow:hidden; color:#003b72; font-size:22px; font-weight:850; flex-shrink:0;"> 5293 <sc-if value="{{ profileEditorHasAvatar }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ profileEditorAvatar }}" alt="Foto de perfil" style="width:100%; height:100%; object-fit:cover;"></sc-if> 5294 <sc-if value="{{ profileEditorShowsInitials }}" hint-placeholder-val="{{ true }}">{{ profileEditorInitials }}</sc-if> 5295 </span> 5296 <div style="display:grid; gap:6px;"> 5297 <label style="display:inline-flex; width:max-content; align-items:center; gap:7px; padding:9px 13px; border-radius:8px; background:#edf5fc; color:#00457f; border:1px solid #c9dced; font-size:12.5px; font-weight:750; cursor:pointer;"> 5298 <img src="assets/icons/lucide/camera.svg" alt="" style="width:16px; height:16px;">Cambiar foto 5299 <input type="file" accept="image/png,image/jpeg,image/webp" onChange="{{ onProfilePhoto }}" style="display:none;"> 5300 </label> 5301 <span style="font-size:11.5px; color:#748398;">PNG, JPG o WebP. Máximo 5 MB.</span> 5302 </div> 5303 </div> 5304 <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:14px;"> 5305 <label class="cb-contract-field">CUIT / CUIL<input inputmode="numeric" maxlength="13" data-dc-attr-value="{{ profileEditorCuit }}" onInput="{{ onProfileCuit }}" placeholder="20-18347370-1"></label> 5306 <label style="display:grid; gap:6px; font-size:12px; font-weight:700; color:#33445a;">Nombre 5307 <input value="" data-dc-attr-value="{{ profileEditorFirstName }}" onInput="{{ onProfileFirstName }}" autocomplete="given-name" placeholder="Tu nombre" style="height:42px; border:1px solid #cfdbe8; border-radius:8px; padding:0 12px; font:inherit; color:#112033; outline:none;"> 5308 </label> 5309 <label style="display:grid; gap:6px; font-size:12px; font-weight:700; color:#33445a;">Apellido 5310 <input value="" data-dc-attr-value="{{ profileEditorLastName }}" onInput="{{ onProfileLastName }}" autocomplete="family-name" placeholder="Tu apellido" style="height:42px; border:1px solid #cfdbe8; border-radius:8px; padding:0 12px; font:inherit; color:#112033; outline:none;"> 5311 </label> 5312 <label style="display:grid; gap:6px; font-size:12px; font-weight:700; color:#33445a;">Teléfono verificado 5313 <input value="" data-dc-attr-value="{{ profileEditorPhoneNumber }}" onInput="{{ onProfilePhoneNumber }}" disabled="{{ profilePhoneReadOnly }}" type="tel" autocomplete="tel" placeholder="Ej. +54 341 555 0000" style="height:42px; border:1px solid #cfdbe8; border-radius:8px; padding:0 12px; font:inherit; color:#112033; outline:none; background:{{ profilePhoneBg }};"> 5314 <small style="display:inline-flex;align-items:center;gap:5px;color:#19815f; font-weight:750;"><sc-if value="{{ profilePhoneVerified }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if>{{ profilePhoneStatusText }}</small> 5315 </label> 5316 <label style="display:grid; gap:6px; font-size:12px; font-weight:700; color:#33445a;">Email 5317 <input value="" data-dc-attr-value="{{ profileEmail }}" disabled style="height:42px; border:1px solid #dce5ee; border-radius:8px; padding:0 12px; font:inherit; color:#65758a; background:#f5f8fb;"> 5318 </label> 5319 </div> 5320 <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;"><span style="font-size:11.5px; color:#748398;">Roles asignados:</span><sc-for list="{{ profileRoleBadges }}" as="role" hint-placeholder-count="1"><span style="padding:4px 8px; border-radius:100px; background:#edf3f8; color:#40536a; font-size:10.5px; font-weight:750;">{{ role.label }}</span></sc-for></div> 5321 <sc-if value="{{ profileEditorHasError }}" hint-placeholder-val="{{ false }}"><div style="padding:10px 12px; border-radius:8px; background:#fff0f1; color:#b42335; border:1px solid #f3c4ca; font-size:12.5px; font-weight:650;">{{ profileEditorError }}</div></sc-if> 5322 </div> 5323 <footer class="profile-editor-footer" style="display:flex; justify-content:flex-end; gap:10px; padding:15px 20px; border-top:1px solid #e6edf5; background:#f8fafc;"> 5324 <button onClick="{{ closeProfileEditor }}" disabled="{{ profileEditorSaving }}" style="height:40px; padding:0 16px; border:1px solid #d3deea; border-radius:8px; background:#fff; color:#40536a; font:inherit; font-size:12.5px; font-weight:750; cursor:pointer;">Cancelar</button> 5325 <button onClick="{{ saveProfileEditor }}" disabled="{{ profileEditorSaving }}" style="height:40px; padding:0 17px; border:0; border-radius:8px; background:#0a5ca8; color:#fff; font:inherit; font-size:12.5px; font-weight:800; cursor:pointer; opacity:{{ profileEditorSaveOpacity }};">{{ profileEditorSaveLabel }}</button> 5326 </footer> 5327 </section> 5328 </div> 5329 </sc-if> 5330 5331 <sc-if data-public-blocked-overlay value="{{ profileCropOpen }}" hint-placeholder-val="{{ false }}"> 5332 <div style="position:fixed; inset:0; z-index:150; background:rgba(0,20,45,0.68);
5332 backdrop-filter:blur(7px); display:grid; place-items:center; padding:18px; overflow:auto;"> 5333 <section role="dialog" aria-modal="true" aria-label="Ajustar foto de perfil" style="width:min(520px, 100%); background:#fff; border-radius:12px; box-shadow:0 28px 80px rgba(0,20,45,0.38); overflow:hidden; animation:floatIn 220ms both;"> 5334 <header style="display:flex; align-items:center; justify-content:space-between; gap:16px; padding:18px 20px; border-bottom:1px solid #e6edf5;"> 5335 <div><div style="font-size:10.5px; letter-spacing:0.13em; color:#0a5ca8; font-weight:800;">FOTO DE PERFIL</div><h2 style="font-size:21px; margin:3px 0 0; color:#112033;">Ajustar foto</h2></div> 5336 <button onClick="{{ cancelProfileCrop }}" title="Cerrar" style="width:36px; height:36px; border:1px solid #d7e2ee; border-radius:50%; background:#fff; display:grid; place-items:center; cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="Cerrar" style="width:18px; height:18px;"></button> 5337 </header> 5338 <div style="padding:20px; display:grid; gap:18px;"> 5339 <div style="display:grid; place-items:center; padding:8px 0 4px;"> 5340 <div style="width:min(286px, 74vw); aspect-ratio:1; border-radius:50%; overflow:hidden; border:4px solid #fff; outline:2px solid #0a5ca8; box-shadow:0 14px 38px rgba(0,48,90,0.2); background:#edf4fb;"> 5341 <canvas data-profile-crop-canvas="true" width="512" height="512" style="display:block; width:100%; height:100%;"></canvas> 5342 </div> 5343 </div> 5344 <div style="display:grid; gap:13px;"> 5345 <label style="display:grid; grid-template-columns:20px 86px 1fr; align-items:center; gap:9px; color:#40536a; font-size:12px; font-weight:700;"><img src="assets/icons/lucide/zoom-in.svg" alt="" style="width:17px; height:17px;">Zoom<input type="range" min="1" max="3" step="0.05" value="" data-dc-attr-value="{{ profileCropZoom }}" onInput="{{ onProfileCropZoom }}" style="width:100%; accent-color:#0a5ca8;"></label> 5346 <label style="display:grid; grid-template-columns:20px 86px 1fr; align-items:center; gap:9px; color:#40536a; font-size:12px; font-weight:700;"><img src="assets/icons/lucide/move-horizontal.svg" alt="" style="width:17px; height:17px;">Horizontal<input type="range" min="0" max="100" step="1" value="" data-dc-attr-value="{{ profileCropX }}" onInput="{{ onProfileCropX }}" style="width:100%; accent-color:#0a5ca8;"></label> 5347 <label style="display:grid; grid-template-columns:20px 86px 1fr; align-items:center; gap:9px; color:#40536a; font-size:12px; font-weight:700;"><img src="assets/icons/lucide/move-vertical.svg" alt="" style="width:17px; height:17px;">Vertical<input type="range" min="0" max="100" step="1" value="" data-dc-attr-value="{{ profileCropY }}" onInput="{{ onProfileCropY }}" style="width:100%; accent-color:#0a5ca8;"></label> 5348 </div> 5349 </div> 5350 <footer style="display:flex; justify-content:flex-end; gap:10px; padding:15px 20px; border-top:1px solid #e6edf5; background:#f8fafc;"> 5351 <button onClick="{{ cancelProfileCrop }}" style="height:40px; padding:0 16px; border:1px solid #d3deea; border-radius:8px; background:#fff; color:#40536a; font:inherit; font-size:12.5px; font-weight:750; cursor:pointer;">Cancelar</button> 5352 <button onClick="{{ confirmProfileCrop }}" style="height:40px; padding:0 17px; border:0; border-radius:8px; background:#0a5ca8; color:#fff; font:inherit; font-size:12.5px; font-weight:800; cursor:pointer; display:inline-flex; align-items:center; gap:7px;"><img src="assets/icons/lucide/check.svg" alt="" style="width:16px; height:16px; filter:brightness(0) invert(1);">Usar este recorte</button> 5353 </footer> 5354 </section> 5355 </div> 5356 </sc-if> 5357 5358 <sc-if data-public-blocked-overlay value="{{ adminUsersOpen }}" hint-placeholder-val="{{ false }}"> 5359 <section data-reveal style="background:#fff; border-radius:18px; box-shadow:0 10px 32px rgba(0,30,60,0.09); overflow:hidden; margin-bottom:18px; border:1px solid #dce7f1;"> 5360 <header style="display:flex; align-items:center; justify-content:space-between; gap:14px; padding:18px 20px; border-bottom:1px solid #e5edf5;"> 5361 <div><div style="font-size:10.5px; letter-spacing:0.13em; color:#0a5ca8; font-weight:800;">ADMINISTRACIÓN</div><h2 style="font-size:20px; margin:3px 0 0; color:#112033;">Usuarios y roles</h2></div> 5362 <button onClick="{{ toggleAdminUsers }}" title="Cerrar panel" style="width:36px; height:36px; border:1px solid #d7e2ee; border-radius:50%; background:#fff; display:grid; place-items:center; cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="Cerrar" style="width:17px; height:17px;"></button> 5363 </header> 5364 <div style="display:flex;flex-wrap:wrap;gap:12px;padding:14px 18px;border-bottom:1px solid #e5edf5;background:#f8fafc;"> 5365 <label style="flex:1 1 230px;min-width:0;display:grid;gap:5px;font-size:11px;font-weight:750;color:#60758c;">Usuarios<select aria-label="Grupo de usuarios" value="" data-dc-attr-value="{{ adminDirectoryGroup }}
5365" onChange="{{ onAdminDirectoryGroup }}" style="width:100%;min-width:0;height:42px;border:1px solid #d4e1ed;border-radius:10px;padding:0 10px;background:#fff;color:#123553;font:inherit;font-size:13px;"><sc-for list="{{ adminDirectoryFilters }}" as="filter"><option value="" data-dc-attr-value="{{ filter.id }}">{{ filter.label }} ({{ filter.count }})</option></sc-for></select></label> 5366 <div style="position:relative;align-self:flex-start;flex:1 1 250px;min-width:0;display:grid;gap:5px;font-size:11px;font-weight:750;color:#60758c;" onKeyDown="{{ onAdminCompanyKey }}"><span>Consignataria</span><button type="button" aria-label="Filtrar por consignataria" aria-expanded="{{ adminCompanyMenuExpanded }}" aria-controls="admin-company-options" onClick="{{ toggleAdminCompanyMenu }}" style="display:flex;align-items:center;gap:10px;width:100%;min-height:42px;border:1px solid #d4e1ed;border-radius:10px;padding:6px 12px;background:#fff;color:#123553;font:inherit;font-size:13px;text-align:left;cursor:pointer;"><sc-if value="{{ adminDirectoryCompanyHasLogo }}"><img data-dc-attr-src="{{ adminDirectoryCompanyLogo }}" alt="" style="width:28px;height:28px;object-fit:contain;"></sc-if><span style="flex:1;">{{ adminDirectoryCompanyLabel }}</span><span aria-hidden="true">â</span></button> 5367 <sc-if value="{{ adminCompanyMenuOpen }}"><div id="admin-company-options" aria-label="Consignatarias" style="position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:30;display:grid;gap:3px;max-height:280px;overflow:auto;padding:6px;border:1px solid #d4e1ed;border-radius:10px;background:#fff;box-shadow:0 12px 28px rgba(15,42,68,.16);"><sc-for list="{{ adminDirectoryCompanies }}" as="company"><button type="button" onClick="{{ company.select }}" aria-pressed="{{ company.selected }}" style="display:flex;align-items:center;gap:10px;min-height:44px;border:0;border-radius:7px;padding:7px 10px;background:{{ company.bg }};color:#123553;font:inherit;font-size:12px;text-align:left;cursor:pointer;"><sc-if value="{{ company.hasLogo }}"><img data-dc-attr-src="{{ company.logo }}" alt="" style="width:32px;height:30px;object-fit:contain;flex-shrink:0;"></sc-if><span style="flex:1;">{{ company.label }}</span><span style="font-variant-numeric:tabular-nums;">{{ company.count }}</span></button></sc-for></div></sc-if></div> 5368 </div> 5369 <sc-if value="{{ adminDirectoryCanSort }}"><label style="display:flex;align-items:center;gap:10px;padding:12px 18px;color:#60758c;font-size:12px;">Ordenar por<select aria-label="Orden de usuarios" data-dc-attr-value="{{ adminDirectorySort }}
5369" onChange="{{ onAdminDirectorySort }}" style="min-height:42px;border:1px solid #d4e1ed;border-radius:10px;padding:0 10px;background:#fff;color:#123553;font:inherit;"><option value="alphabetical">Nombre: AâZ</option><option value="newest">Registro: más recientes primero</option><option value="oldest">Registro: más antiguos primero</option></select></label></sc-if> 5370 <div style="display:grid; grid-template-columns:{{ adminUsersGridCols }}; min-height:360px;"> 5371 <div style="padding:18px; border-right:{{ adminUsersDivider }}; border-bottom:{{ adminUsersMobileDivider }};"> 5372 <label style="height:42px; display:flex; align-items:center; gap:9px; border:1px solid #cedbe8; border-radius:10px; padding:0 12px; background:#f9fbfd;"> 5373 <img src="assets/icons/lucide/search.svg" alt="" style="width:17px; height:17px; opacity:.65;"> 5374 <input value="" data-dc-attr-value="{{ adminUsersSearch }}" onInput="{{ onAdminUsersSearch }}" aria-label="Buscar usuarios" placeholder="Nombre, email o consignataria" style="min-width:0; flex:1; border:0; outline:0; background:transparent; color:#112033; font:inherit; font-size:13px;"> 5375 </label> 5376 <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin:12px 0;font-size:12px;color:#60758c;"><span>{{ adminDirectorySummary }}</span><button type="button" onClick="{{ refreshAdminDirectory }}" disabled="{{ adminUsersLoading }}" style="border:1px solid #dce6ef;background:#fff;border-radius:8px;padding:7px 10px;color:#075a9c;cursor:pointer;">Actualizar</button></div> 5377 <sc-if value="{{ adminRecentMissingDates }}"><p style="font-size:12px;color:#60758c;">{{ adminMissingDateText }}</p></sc-if> 5378 <sc-if value="{{ adminUsersLoading }}" hint-placeholder-val="{{ false }}"><div style="padding:34px 10px; text-align:center; color:#65758a; font-size:13px;">Cargando usuarios...</div></sc-if> 5379 <sc-if value="{{ adminUsersHasError }}" hint-placeholder-val="{{ false }}"><div style="margin-top:12px; padding:11px 12px; border-radius:9px; background:#fff0f1; border:1px solid #f2c7cc; color:#b42335; font-size:12px;">{{ adminUsersError }}</div></sc-if> 5380 <sc-if value="{{ adminUsersEmpty }}" hint-placeholder-val="{{ false }}"><div style="padding:34px 10px; text-align:center; color:#65758a; font-size:13px;">No encontramos usuarios.</div></sc-if> 5381 <div style="display:grid; gap:8px; margin-top:12px; max-height:560px; overflow:auto; padding-right:3px;"> 5382 <sc-for list="{{ adminUsersList }}" as="account" hint-placeholder-count="2"> 5383 <button onClick="{{ account.select }}" style="width:100%; display:flex; align-items:center; gap:11px; text-align:left; border:1px solid {{ account.border }}; background:{{ account.bg }}; border-radius:10px; padding:10px; cursor:pointer; font-family:inherit; color:#112033;"> 5384 <span style="width:38px; height:38px; border-radius:50%; overflow:hidden; display:grid; place-items:center; flex-shrink:0; background:#e8f2fb; color:#075a9c; font-weight:850;"> 5385 <sc-if value="{{ account.hasPhoto }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ account.photoUrl }}" alt="" style="width:100%; height:100%; object-fit:cover;"></sc-if> 5386 <sc-if value="{{ account.showsInitials }}" hint-placeholder-val="{{ true }}">{{ account.initials }}</sc-if> 5387 </span> 5388 <span style="min-width:0; flex:1;"><strong style="display:block; font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ account.displayName }}</strong><span style="display:block; margin-top:2px; font-size:11px; color:#66778c; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ account.email }}</span><span style="display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;"><sc-for list="{{ account.roleBadges }}" as="badge"><span style="display:inline-flex;align-items:center;gap:4px;padding:3px 6px;border-radius:6px;background:{{ badge.bg }};color:{{ badge.color }};font-size:10px;font-weight:700;"><img data-dc-attr-src="{{ badge.icon }}" alt="" style="width:12px;height:12px;">{{ badge.label }}</span></sc-for><sc-for list="{{ account.companyBadges }}" as="company"><span style="display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:3px 6px;border-radius:6px;background:color-mix(in srgb, {{ company.color }} 10%, white);color:color-mix(in srgb, {{ company.color }} 75%, #17314d);font-size:10px;font-weight:700;"><sc-if value="{{ company.hasLogo }}"><img data-dc-attr-src="{{ company.logo }}" alt="" style="width:12px;height:12px;object-fit:contain;flex-shrink:0;"></sc-if><span style="overflow-wrap:anywhere;">{{ company.name }}</span></span></sc-for></span><span style="display:block;margin-top:4px;font-size:10px;color:#738397;">{{ account.registrationLabel }}</span><span class="admin-version-list"><sc-for list="{{ account.versionDevices }}" as="device"><span class="admin-version-item" style="background:{{ device.bg }};color:{{ device.color }};"><strong>{{ device.label }}</strong><small>{{ device.detail }}</small></span></sc-for></span></span> 5389 5390 </button> 5391 </sc-for> 5392 </div> 5393 <sc-if value="{{ adminDirectoryPaginated }}"><nav aria-label="Páginas de usuarios" style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:12px;margin-top:12px;border-top:1px solid #e5edf5;font-size:12px;color:#60758c;"><button type="button" onClick="{{ adminDirectoryPrevious }}" disabled="{{ adminDirectoryFirstPage }}" style="border:1px solid #d4e1ed;background:#fff;color:#123553;padding:9px 12px;border-radius:8px;
5393cursor:pointer;">Anterior</button><span>{{ adminDirectoryPageLabel }}</span><button type="button" onClick="{{ adminDirectoryNext }}" disabled="{{ adminDirectoryLastPage }}" style="border:1px solid #d4e1ed;background:#fff;color:#123553;padding:9px 12px;border-radius:8px;cursor:pointer;">Siguiente</button></nav></sc-if> 5394 </div> 5395 <div style="padding:20px;"> 5396 <sc-if value="{{ adminUserNotSelected }}" hint-placeholder-val="{{ true }}"><div style="height:100%; min-height:280px; display:grid; place-items:center; text-align:center; color:#65758a;"><div><img src="assets/icons/lucide/user-cog.svg" alt="" style="width:34px; height:34px; opacity:.48;"><p style="margin:10px 0 0; font-size:13px;">Seleccioná una cuenta para administrar sus permisos.</p></div></div></sc-if> 5397 <sc-if value="{{ adminUserSelected }}" hint-placeholder-val="{{ false }}"> 5398 <div style="display:grid; gap:19px;"> 5399 <div><div style="font-size:10.5px; letter-spacing:.11em; color:#8090a5; font-weight:800;">CUENTA SELECCIONADA</div><h3 style="font-size:19px; margin:4px 0 2px; color:#112033;">{{ adminSelectedName }}</h3><div style="font-size:12px; color:#65758a;">{{ adminSelectedEmail }}</div></div> 5400 <div><div style="font-size:11px; font-weight:800; color:#40536a; margin-bottom:8px;">Roles</div><div style="display:flex; flex-wrap:wrap; gap:8px;"><sc-for list="{{ adminRoleOptions }}" as="role" hint-placeholder-count="4"><button onClick="{{ role.toggle }}" style="display:inline-flex; align-items:center; gap:7px; height:36px; padding:0 12px; border-radius:100px; border:1px solid {{ role.border }}; background:{{ role.bg }}; color:{{ role.color }}; font:inherit; font-size:11.5px; font-weight:800; cursor:pointer;"><img data-dc-attr-src="{{ role.icon }}" alt="" style="width:14px; height:14px; filter:{{ role.iconFilter }};">{{ role.label }}</button></sc-for></div></div> 5401 <sc-if value="{{ adminNeedsConsignataria }}" hint-placeholder-val="{{ false }}"> 5402 <div><div style="font-size:11px; font-weight:800; color:#40536a; margin-bottom:4px;">Consignatarias</div><p style="font-size:11.5px; color:#748398; margin:0 0 9px;">Elegí una o más consignatarias activas para esta cuenta.</p><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:8px;"><sc-for list="{{ adminConsignatariaOptions }}" as="cons" hint-placeholder-count="3"><button onClick="{{ cons.select }}" aria-pressed="{{ cons.selected }}" style="min-height:48px; display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:9px; border:1px solid {{ cons.border }}; background:{{ cons.bg }}; color:#17314d; font:inherit; font-size:11px; font-weight:750; cursor:pointer; text-align:left;"><span style="width:30px; height:30px; border-radius:50%; background:#fff; display:grid; place-items:center; flex-shrink:0; border:1px solid #dbe5ee;"><img data-dc-attr-src="{{ cons.logo }}" alt="" style="max-width:23px; max-height:23px; object-fit:contain;"></span>{{ cons.name }}</button></sc-for></div></div> 5403 </sc-if> 5404 <sc-if value="{{ adminRelationLoading }}" hint-placeholder-val="{{ false }}"><div style="font-size:12px; color:#65758a;">Consultando asignaciones...</div></sc-if> 5405 <sc-if value="{{ adminUserHasMessage }}" hint-placeholder-val="{{ false }}"><div style="padding:10px 12px; border-radius:9px; background:{{ adminUserMessageBg }}; border:1px solid {{ adminUserMessageBorder }}; color:{{ adminUserMessageColor }}; font-size:12px; font-weight:650;">{{ adminUserMessageText }}</div></sc-if> 5406 <div style="display:flex; justify-content:flex-end;"><button onClick="{{ saveAdminUser }}" disabled="{{ adminUserSaving }}" style="height:40px; display:inline-flex; align-items:center; gap:8px; padding:0 16px; border:0; border-radius:9px; background:#0a5ca8; color:#fff; font:inherit; font-size:12.5px; font-weight:800; cursor:pointer; opacity:{{ adminUserSaveOpacity }};"><img src="assets/icons/lucide/save.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">{{ adminUserSaveLabel }}</button></div> 5407 </div> 5408 </sc-if> 5409 </div> 5410 </div> 5411 </section> 5412 </sc-if> 5413 </sc-if> 5414 5415 <sc-if data-public-blocked-overlay value="{{ mailingOpen }}" hint-placeholder-val="{{ false }}"> 5416 <section data-reveal style="background:#fff; border-radius:18px; box-shadow:0 10px 32px rgba(0,30,60,0.09); overflow:hidden; margin:24px 0 18px; border:1px solid #dce7f1;"> 5417 <header style="display:flex; align-items:center; justify-content:space-between; gap:14px; padding:18px 20px; border-bottom:1px solid #e5edf5; background:linear-gradient(120deg,#001d3d,#075ca8); color:#fff;"> 5418 <div><div style="font-size:10.5px; letter-spacing:.13em; color:#8fdce8; font-weight:800;">MAILGUN · ADMINISTRACIÓN</div><h2 style="font-size:21px; margin:3px 0 0;">Mailing Rosgan</h2><p style="font-size:12px; margin:5px 0 0; color:#c8d9e9;">Contactos, campañas, pruebas y envíos masivos desde [email protected].</p></div> 5419 <div style="display:flex; align-items:center; gap:10px;"><span style="padding:7px 10px; border-radius:100px; background:{{ mailingStatusBg }}; border:1px solid rgba(255,255,255,.25); font-size:11px; font-weight:800;">{{ mailingStatusLabel }}</span><button onClick="{{ toggleMailing }}" title="Cerrar panel" style="width:36px; height:36px; border:1px solid rgba(255,255,255,.28); border-radius:50%; background:rgba(255,255,255,.12); display:grid; place-items:center; cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="Cerrar" style="width:17px; height:17px; filter:brightness(0) invert(1);"></button></div> 5420 </header> 5421 <sc-if value="{{ mailingLoading }}" hint-placeholder-val="{{ false }}"><div style="padding:34px; text-align:center; color:#65758a; font-size:13px;">Cargando mailing...</div></sc-if> 5422 <sc-if value="{{ mailingHasError }}" hint-placeholder-val="{{ false }}"><div style="margin:16px 18px 0; padding:11px 13px; border-radius:9px; background:#fff0f1; border:1px solid #f2c7cc; color:#b42335; font-size:12px; font-weight:650;">{{ mailingError }}</div></sc-if> 5423 <sc-if value="{{ mailingHasMessage }}" hint-placeholder-val="{{ false }}"><div style="margin:16px 18px 0; padding:11px 13px; border-radius:9px; background:{{ mailingMessageBg }}; border:1px solid {{ mailingMessageBorder }}; color:{{ mailingMessageColor }}; font-size:12px; font-weight:650;">{{ mailingMessageText }}</div></sc-if> 5424 <div style="padding:18px; display:grid; grid-template-columns:{{ mailingGridCols }}; gap:16px;"> 5425 <aside style="display:grid; align-content:start; gap:14px;"> 5426 <article style="border:1px solid #dce7f1; border-radius:13px; padding:15px; background:#f8fbfd;"> 5427 <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:10px;"><div aria-live="polite"><div style="font-size:10px; letter-spacing:.1em; color:#6a7d92; font-weight:800;">BASE DE CONTACTOS</div><strong style="display:block; margin-top:3px; font-size:25px; color:#07345f;">{{ mailingContactCount }}</strong><span style="font-size:11px; color:#6a7d92;">{{ mailingContactsLabel }}</span></div><span style="width:40px; height:40px; border-radius:12px; background:#e3f1fb; display:grid; place-items:center;"><img src="assets/icons/lucide/contact-round.svg" alt="" style="width:20px; height:20px;"></span></div> 5428 <button type="button" onClick="{{ toggleMailingConsent }}" style="width:100%; margin-top:14px; padding:9px 10px; display:flex; align-items:flex-start; gap:9px; text-align:left; border:1px solid {{ mailingConsentBorder }}; border-radius:9px; background:{{ mailingConsentBg }}; color:#40536a; font:inherit; font-size:10.5px; line-height:1.45; cursor:pointer;"><span style="width:18px; height:18px; flex:0 0 18px; margin-top:1px; border-radius:5px; display:grid; place-items:center; background:{{ mailingConsentCheckBg }}; border:1px solid {{ mailingConsentBorder }}; color:#fff; font-weight:900;"><sc-if value="{{ mailingConsentChecked }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if></span><span>Confirmo que todos los contactos aceptaron recibir comunicaciones de Rosgan y que puedo acreditar ese consentimiento.</span></button> 5429 <label style="margin-top:9px; height:40px; display:flex; align-items:center; justify-content:center; gap:8px; border-radius:9px; background:#075ca8; color:#fff; font-size:12px; font-weight:800; cursor:pointer; opacity:{{ mailingImportOpacity }};"><img src="assets/icons/lucide/file-up.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">Importar CSV<input type="file" accept=".csv,text/csv" onChange="{{ importMailingCsv }}" disabled="{{ mailingImportDisabled }}" style="display:none;"></label> 5430 <p style="font-size:10.5px; line-height:1.5;
5430 color:#77889b; margin:9px 0 0;">Columnas aceptadas: <strong>email</strong>, nombre, origen, estado, validación y rebotes. Las bases grandes se cargan completas: el sistema divide el archivo automáticamente en bloques técnicos. La campaña incluye a todos los contactos suscriptos.</p> 5431 </article> 5432 <article style="border:1px solid #dce7f1; border-radius:13px; overflow:hidden;"> 5433 <div style="padding:12px 13px; background:#f4f8fb; border-bottom:1px solid #dce7f1; font-size:11px; font-weight:850; color:#40536a;">CAMPAÑAS</div> 5434 <sc-if value="{{ mailingCampaignsPending }}" hint-placeholder-val="{{ true }}"><div role="status" style="padding:18px 14px;color:#526b82;font-size:12px;">Consultando campañasâ¦</div></sc-if> 5435 <sc-if value="{{ mailingHasError }}" hint-placeholder-val="{{ false }}"><button onClick="{{ retryMailing }}" disabled="{{ mailingLoading }}" style="margin:10px;padding:8px 12px;border:1px solid #cbd8e5;border-radius:8px;background:white;color:#0a5ca8;">Reintentar consultas</button></sc-if> 5436 <sc-if value="{{ mailingCampaignsEmpty }}" hint-placeholder-val="{{ false }}"><div style="padding:22px 14px; text-align:center; color:#7a8b9d; font-size:11.5px;">Todavía no hay campañas.</div></sc-if> 5437 <div style="max-height:310px; overflow:auto; padding:8px; display:grid; gap:7px;"> 5438 <sc-for list="{{ mailingCampaignList }}" as="campaign" hint-placeholder-count="2"> 5439 <div style="display:flex; align-items:stretch; border:1px solid {{ campaign.border }}; background:{{ campaign.bg }}; border-radius:9px; overflow:hidden;"> 5440 <button onClick="{{ campaign.select }}" style="min-width:0; flex:1; text-align:left; border:0; background:transparent; padding:10px; cursor:pointer; font-family:inherit;"><strong style="display:block; font-size:12px; color:#12314f; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ campaign.name }}</strong><span style="display:flex; justify-content:space-between; gap:8px; margin-top:4px; font-size:10px; color:#718397;"><span>{{ campaign.statusLabel }}</span><span>{{ campaign.progress }}</span></span><span style="display:block; margin-top:5px; font-size:9.5px; color:#526b82;">{{ campaign.audienceLabel }} · {{ campaign.sendMeta }}</span></button> 5441 <button type="button" onClick="{{ campaign.remove }}" disabled="{{ campaign.deleteDisabled }}" title="Eliminar campaña" style="width:38px; flex:0 0 38px; border:0; border-left:1px solid #dce6ef; background:transparent; display:grid; place-items:center; cursor:pointer; opacity:{{ campaign.deleteOpacity }};"><img src="assets/icons/lucide/trash-2.svg" alt="Eliminar" style="width:15px; height:15px;"></button> 5442 </div> 5443 </sc-for> 5444 </div> 5445 <button onClick="{{ newMailingCampaign }}" style="width:calc(100% - 16px); height:36px; margin:0 8px 8px; border:1px dashed #86aacb; border-radius:8px; background:#fff; color:#075ca8; font:inherit; font-size:11.5px; font-weight:800; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Nueva campaña</button> 5446 <button onClick="{{ newLaunchMailingCampaign }}" style="width:calc(100% - 16px); min-height:44px; margin:0 8px 8px; padding:8px 11px; border:0; border-radius:10px; background:linear-gradient(120deg,#003b71,#0875a7 64%,#21aeb7); color:#fff; font:inherit; font-size:11.5px; line-height:1.25; font-weight:850; cursor:pointer; box-shadow:0 7px 17px rgba(0,67,116,.18);">Crear campaña de lanzamiento<br><span style="font-size:9.5px;font-weight:650;opacity:.82;">{{ mailingLaunchAudienceCount }}</span></button> 5447 </article> 5448 </aside> 5449 <div style="border:1px solid #dce7f1; border-radius:13px; padding:17px; display:grid; gap:13px; align-content:start;"> 5450 <div style="display:flex; justify-content:space-between; align-items:center; gap:12px;"><div><div style="font-size:10px; letter-spacing:.1em; color:#6a7d92; font-weight:800;">COMPOSITOR</div><h3 style="font-size:18px; color:#12314f; margin:3px 0 0;">{{ mailingComposerTitle }}</h3></div><span style="padding:5px 9px; border-radius:100px; background:#edf3f8; color:#536b82; font-size:10px; font-weight:800;">{{ mailingDraftStatus }}</span></div> 5451 <div style="display:grid;gap:5px;font-size:11px;font-weight:750;color:#40536a;">Audiencia<div style="min-height:40px;
5451display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 11px;border:1px solid {{ mailingAudienceBorder }};border-radius:8px;background:{{ mailingAudienceBg }};"><strong style="font-size:12px;color:{{ mailingAudienceColor }};">{{ mailingAudienceLabel }}</strong><span style="padding:4px 7px;border-radius:100px;background:#fff;border:1px solid {{ mailingAudienceBorder }};font-size:9px;color:{{ mailingAudienceColor }};white-space:nowrap;">Audiencia fija</span></div><small style="color:#788b9e;font-size:10px;font-weight:550;line-height:1.4;">{{ mailingAudienceHelp }}</small></div> 5452 <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:11px;"> 5453 <label style="display:grid; gap:5px; font-size:11px; font-weight:750; color:#40536a;">Nombre interno<input value="" data-dc-attr-value="{{ mailingDraftName }}" onInput="{{ onMailingName }}" placeholder="Ej. Remate de septiembre" disabled="{{ mailingDraftLocked }}" style="height:40px; border:1px solid #cfdbe8; border-radius:8px; padding:0 11px; font:inherit; font-size:12px;"></label> 5454 <label style="display:grid; gap:5px; font-size:11px; font-weight:750; color:#40536a;">Asunto<input value="" data-dc-attr-value="{{ mailingDraftSubject }}" onInput="{{ onMailingSubject }}" placeholder="Asunto del correo" disabled="{{ mailingDraftLocked }}" style="height:40px; border:1px solid #cfdbe8; border-radius:8px; padding:0 11px; font:inherit; font-size:12px;"></label> 5455 </div> 5456 <label style="display:grid; gap:5px; font-size:11px; font-weight:750; color:#40536a;">Texto previo<input value="" data-dc-attr-value="{{ mailingDraftPreview }}" onInput="{{ onMailingPreview }}" placeholder="Resumen corto que aparece junto al asunto" disabled="{{ mailingDraftLocked }}" style="height:40px; border:1px solid #cfdbe8; border-radius:8px; padding:0 11px; font:inherit; font-size:12px;"></label> 5457 <div class="{{ mailingEditorShellClass }}" style="display:grid; gap:6px; font-size:11px; font-weight:750; color:#40536a;"> 5458 <div style="display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;"><span>Contenido del correo</span><div style="display:flex; gap:6px;"><button type="button" onClick="{{ previewMailingHtml }}" class="mailing-editor-tool" title="Vista previa del correo">Vista previa</button><button type="button" onClick="{{ toggleMailingSource }}" class="mailing-editor-tool" title="Alternar editor visual y HTML">{{ mailingEditorModeLabel }}</button><button type="button" onClick="{{ toggleMailingExpanded }}" class="mailing-editor-tool" title="Ampliar editor">{{ mailingEditorExpandLabel }}</button></div></div> 5459 <sc-if value="{{ mailingEditorVisualMode }}" hint-placeholder-val="{{ true }}"><div class="mailing-editor-toolbar"> 5460 <button type="button" onMouseDown="{{ mailingEditorUndo }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Deshacer" aria-label="Deshacer"><span class="dc-lucide dc-lucide-undo" aria-hidden="true"></span></button><button type="button" onMouseDown="{{ mailingEditorRedo }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Rehacer" aria-label="Rehacer"><span class="dc-lucide dc-lucide-redo" aria-hidden="true"></span></button><span class="mailing-editor-separator"></span> 5461 <select onChange="{{ mailingEditorFormat }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Formato de párrafo"><option value="p">Párrafo</option><option value="h1">Título 1</option><option value="h2">Título 2</option><option value="h3">Título 3</option><option value="blockquote">Cita</option></select><select onChange="{{ mailingEditorFont }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="TipografÃa"><option value="Arial">Arial</option><option value="Verdana">Verdana</option><option value="Georgia">Georgia</option><option value="Times New Roman">Times</option><option value="Courier New">Courier</option></select><select onChange="{{ mailingEditorFontSize }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Tamaño de texto"><option value="3">Normal</option><option value="2">Chico</option><option value="4">Grande</option><option value="5">Muy grande</option><option value="6">Destacado</option></select> 5462 <button type="button" onMouseDown="{{ mailingEditorBold }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Negrita" aria-label="Negrita"><i class="dc-lucide dc-lucide-bold" aria-hidden="true"></i></button><button type="button" onMouseDown="{{ mailingEditorItalic }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Cursiva" aria-label="Cursiva"><i class="dc-lucide dc-lucide-italic" aria-hidden="true"></i></button><button type="button" onMouseDown="{{ mailingEditorUnderline }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Subrayado" aria-label="Subrayado"><i class="dc-lucide dc-lucide-underline" aria-hidden="true"></i></button>
5463 <span class="mailing-editor-separator"></span><button type="button" onMouseDown="{{ mailingEditorAlignLeft }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Alinear a la izquierda">Izq.</button><button type="button" onMouseDown="{{ mailingEditorAlignCenter }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Centrar">Centro</button><button type="button" onMouseDown="{{ mailingEditorAlignRight }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Alinear a la derecha">Der.</button> 5464 <span class="mailing-editor-separator"></span><button type="button" onMouseDown="{{ mailingEditorBullets }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Lista con viñetas">• Lista</button><button type="button" onMouseDown="{{ mailingEditorNumbers }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool" title="Lista numerada">1. Lista</button> 5465 <span class="mailing-editor-separator"></span><label class="mailing-editor-tool" title="Color de texto" style="display:flex;align-items:center;gap:5px;">Texto <input type="color" value="#173a5d" onInput="{{ mailingEditorColor }}" disabled="{{ mailingDraftLocked }}" style="width:20px;height:20px;border:0;padding:0;background:transparent;"></label><label class="mailing-editor-tool" title="Color de fondo" style="display:flex;align-items:center;gap:5px;">Fondo <input type="color" value="#fff2a8" onInput="{{ mailingEditorBackground }}" disabled="{{ mailingDraftLocked }}" style="width:20px;height:20px;border:0;padding:0;background:transparent;"></label> 5466 <span class="mailing-editor-separator"></span><button type="button" onClick="{{ openMailingLink }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool">Enlace</button><button type="button" onClick="{{ openMailingButton }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool">Botón</button><button type="button" onClick="{{ openMailingImage }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool">Imagen</button><button type="button" onMouseDown="{{ mailingEditorDivider }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool">Separador</button><button type="button" onMouseDown="{{ mailingEditorName }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool">Nombre</button><button type="button" onMouseDown="{{ mailingEditorClear }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-tool">Limpiar formato</button> 5467 </div></sc-if> 5468 <sc-if value="{{ mailingEditorVisualMode }}" hint-placeholder-val="{{ true }}"><div data-mailing-rich-editor="true" contenteditable="{{ mailingEditorEditable }}" onInput="{{ onMailingVisualInput }}" onKeyUp="{{ saveMailingSelection }}" onMouseUp="{{ saveMailingSelection }}" onBlur="{{ saveMailingSelection }}" class="mailing-rich-editor" data-placeholder="Escribà el correo como en un procesador de textos. Podés agregar imágenes, enlaces y botones."></div></sc-if> 5469 <sc-if value="{{ mailingEditorSourceMode }}" hint-placeholder-val="{{ false }}"><textarea value="" data-dc-attr-value="{{ mailingDraftHtml }}" onInput="{{ onMailingHtmlSource }}" disabled="{{ mailingDraftLocked }}" class="mailing-editor-source" spellcheck="false" placeholder="HTML del correo"></textarea></sc-if> 5470 <small style="color:#7a8b9d;font-weight:500;line-height:1.45;">La firma institucional y la desuscripción se agregan automáticamente al enviar.</small> 5471 </div> 5472 <sc-if value="{{ mailingInsertDialogOpen }}" hint-placeholder-val="{{ false }}"> 5473 <div class="mailing-editor-dialog-backdrop"> 5474 <article class="mailing-editor-dialog"> 5475 <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;"><div><small style="color:#0a5ca8;font-weight:900;letter-spacing:.1em;">EDITOR DE CORREO</small><h3 style="margin:4px 0 0;color:#12314f;font-size:20px;">{{ mailingInsertDialogTitle }}</h3></div><button type="button" onClick="{{ closeMailingInsert }}" aria-label="Cerrar" title="Cerrar" style="width:36px;height:36px;display:grid;place-items:center;border:1px solid #d5e1ec;border-radius:50%;background:#fff;color:#12314f;cursor:pointer;"><span class="dc-lucide dc-lucide-x" aria-hidden="true"></span></button></div> 5476 <div style="display:grid;gap:12px;margin-top:18px;"> 5477 <sc-if value="{{ mailingInsertIsImage }}" hint-placeholder-val="{{ false }}"><label style="height:76px;border:1px dashed #7ba8cc;border-radius:10px;background:#f1f8fd;color:#075ca8;display:grid;place-items:center;text-align:center;font-size:12px;font-weight:850;cursor:pointer;">{{ mailingImageUploadLabel }}<input type="file" accept="image/jpeg,image/png,image/webp,image/gif" onChange="{{ uploadMailingImage }}" disabled="{{ mailingImageUploading }}" style="display:none;"></label></sc-if> 5478 <label style="display:grid;gap:5px;color:#40536a;font-size:11px;font-weight:750;">{{ mailingInsertUrlLabel }}<input value="" data-dc-attr-value="{{ mailingInsertUrl }}" onInput="{{ onMailingInsertUrl }}" placeholder="{{ mailingInsertUrlPlaceholder }}" style="height:42px;border:1px solid #cfdbe8;border-radius:8px;padding:0 11px;font:inherit;font-size:12px;"></label> 5479 <label style="display:grid;gap:5px;color:#40536a;font-size:11px;font-weight:750;">{{ mailingInsertTextLabel }}<input value="" data-dc-attr-value="{{ mailingInsertText }}" onInput="{{ onMailingInsertText }}" placeholder="{{ mailingInsertTextPlaceholder }}" style="height:42px;border:1px solid #cfdbe8;border-radius:8px;padding:0 11px;font:inherit;font-size:12px;"></label> 5480 <sc-if value="{{ mailingInsertIsImage }}" hint-placeholder-val="{{ false }}"><div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px;align-items:start;"> 5481 <label style="display:grid;gap:5px;color:#40536a;font-size:11px;font-weight:750;">Tamaño<select value="" data-dc-attr-value="{{ mailingInsertImageSize }}" onChange="{{ onMailingImageSize }}" style="height:42px;border:1px solid #cfdbe8;border-radius:8px;padding:0 10px;background:#fff;color:#40536a;font:inherit;font-size:12px;"><option value="small">Pequeña · 320 px</option><option value="medium">Mediana · 480 px</option><option value="large">Grande · 640 px</option><option value="full">Ancho completo</option></select></label> 5482 <label style="display:grid;gap:5px;color:#40536a;font-size:11px;font-weight:750;">Enlace al hacer clic <span style="font-weight:500;color:#8493a3;">(opcional)</span><input value="" data-dc-attr-value="{{ mailingInsertImageLink }}" onInput="{{ onMailingImageLink }}" placeholder="https://sitio.com/destino" style="height:42px;border:1px solid {{ mailingImageLinkBorder }};border-radius:8px;padding:0 11px;font:inherit;font-size:12px;"><small style="min-height:14px;
5482color:{{ mailingImageLinkHintColor }};font-weight:550;">{{ mailingImageLinkHint }}</small></label> 5483 </div></sc-if> 5484 <sc-if value="{{ mailingInsertIsImage }}" hint-placeholder-val="{{ false }}"><sc-if value="{{ mailingImageHasPreview }}" hint-placeholder-val="{{ false }}"><div style="border:1px solid #dce6ef;border-radius:10px;padding:16px;background:#f8fbfd;text-align:center;overflow:auto;"><img data-dc-attr-src="{{ mailingInsertUrl }}" alt="Vista previa" style="display:block;width:100%;max-width:{{ mailingImagePreviewMaxWidth }};max-height:260px;height:auto;object-fit:contain;margin:auto;"></div></sc-if></sc-if> 5485 <sc-if value="{{ mailingImageHasError }}" hint-placeholder-val="{{ false }}"><div style="padding:9px 10px;border-radius:8px;background:#fff0f1;border:1px solid #f2c7cc;color:#b42335;font-size:11px;">{{ mailingImageError }}</div></sc-if> 5486 </div> 5487 <div style="display:flex;justify-content:flex-end;gap:8px;margin-top:18px;"><button type="button" onClick="{{ closeMailingInsert }}" style="height:40px;padding:0 14px;border:1px solid #cbd8e5;border-radius:8px;background:#fff;color:#40536a;font-weight:800;cursor:pointer;">Cancelar</button><button type="button" onClick="{{ confirmMailingInsert }}" disabled="{{ mailingInsertDisabled }}" style="height:40px;padding:0 16px;border:0;border-radius:8px;background:#075ca8;color:#fff;font-weight:850;cursor:pointer;opacity:{{ mailingInsertOpacity }};">{{ mailingInsertConfirmLabel }}</button></div> 5488 </article> 5489 </div> 5490 </sc-if> 5491 <label style="display:grid; gap:5px; font-size:11px; font-weight:750; color:#40536a;">Versión de texto <span style="font-weight:500; color:#8493a3;">(opcional)</span><textarea value="" data-dc-attr-value="{{ mailingDraftText }}" onInput="{{ onMailingText }}" placeholder="Alternativa sin HTML" disabled="{{ mailingDraftLocked }}" style="min-height:76px; resize:vertical; border:1px solid #cfdbe8; border-radius:8px; padding:11px; font:12px/1.5 inherit;"></textarea></label> 5492 <div style="display:grid; grid-template-columns:minmax(0,1fr) auto; gap:10px; align-items:end;"><label style="display:grid; gap:5px; font-size:11px; font-weight:750; color:#40536a;">Email de prueba<input value="" data-dc-attr-value="{{ mailingTestEmail }}" onInput="{{ onMailingTestEmail }}" type="email" placeholder="[email protected]" style="height:40px; border:1px solid #cfdbe8; border-radius:8px; padding:0 11px; font:inherit; font-size:12px;"></label><button onClick="{{ sendMailingTest }}" disabled="{{ mailingTestDisabled }}" style="height:40px; padding:0 13px; border:1px solid #a9bfd4; border-radius:8px; background:#fff; color:#075ca8; font:inherit; font-size:11.5px; font-weight:800; cursor:pointer; opacity:{{ mailingTestOpacity }};">Enviar prueba</button></div> 5493 <div style="display:flex; justify-content:flex-end; gap:9px; flex-wrap:wrap; padding-top:4px; border-top:1px solid #e8eef4;"><button onClick="{{ saveMailingDraft }}" disabled="{{ mailingSaveDisabled }}" style="height:40px; padding:0 15px; border:1px solid #b8cadd; border-radius:8px; background:#fff; color:#355069; font:inherit; font-size:12px; font-weight:800; cursor:pointer; opacity:{{ mailingSaveOpacity }};">Guardar campaña</button><button onClick="{{ queueMailingCampaign }}" disabled="{{ mailingQueueDisabled }}" style="height:40px; display:inline-flex; align-items:center; gap:7px; padding:0 16px; border:0; border-radius:8px; background:#0b7a59; color:#fff; font:inherit; font-size:12px; font-weight:850; cursor:pointer; opacity:{{ mailingQueueOpacity }};"><img src="assets/icons/lucide/send.svg" alt="" style="width:15px; height:15px; filter:brightness(0) invert(1);">{{ mailingQueueLabel }}</button></div> 5494 </div> 5495 </div> 5496 <sc-if value="{{ mailingReportVisible }}" hint-placeholder-val="{{ false }}"> 5497 <article style="margin:0 18px 18px; border:1px solid #dce7f1; border-radius:14px; overflow:hidden; background:#fff;"> 5498 <header style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; padding:15px 17px; background:#f4f8fb; border-bottom:1px solid #dce7f1;"> 5499 <div><div style="font-size:10px; letter-spacing:.12em; color:#0a5ca8; font-weight:850;">TRACKING DE MARKETING</div><h3 style="margin:3px 0 0; font-size:17px; color:#12314f;">
5499Reporte por campaña</h3></div> 5500 <div style="display:flex; gap:8px; flex-wrap:wrap;"><button onClick="{{ refreshMailingReport }}" disabled="{{ mailingReportLoading }}" style="height:36px; padding:0 12px; border:1px solid #b8cadd; border-radius:8px; background:#fff; color:#355069; font:inherit; font-size:11px; font-weight:800; cursor:pointer;">Actualizar</button><button onClick="{{ exportMailingReport }}" disabled="{{ mailingReportExportDisabled }}" style="height:36px; display:inline-flex; align-items:center; gap:7px; padding:0 13px; border:0; border-radius:8px; background:#075ca8; color:#fff; font:inherit; font-size:11px; font-weight:850; cursor:pointer; opacity:{{ mailingReportExportOpacity }};"><img src="assets/icons/lucide/download.svg" alt="" style="width:14px; height:14px; filter:brightness(0) invert(1);">{{ mailingReportExportLabel }}</button></div> 5501 </header> 5502 <sc-if value="{{ mailingReportLoading }}" hint-placeholder-val="{{ false }}"><div style="padding:28px; text-align:center; color:#6c7f92; font-size:12px;">Actualizando métricas...</div></sc-if> 5503 <sc-if value="{{ mailingReportHasError }}" hint-placeholder-val="{{ false }}"><div style="margin:14px 16px 0; padding:10px 12px; border-radius:8px; background:#fff0f1; border:1px solid #f2c7cc; color:#b42335; font-size:11px;">{{ mailingReportError }}</div></sc-if> 5504 <sc-if value="{{ mailingReportReady }}" hint-placeholder-val="{{ false }}"> 5505 <div style="padding:16px; display:grid; gap:14px;"> 5506 <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(130px,1fr)); gap:9px;"> 5507 <sc-for list="{{ mailingReportKpis }}" as="metric" hint-placeholder-count="6"><div style="border:1px solid {{ metric.border }}; background:{{ metric.bg }}; border-radius:10px; padding:11px 12px;"><span style="display:block; font-size:9px; letter-spacing:.08em; color:{{ metric.color }}; font-weight:850;">{{ metric.label }}</span><strong style="display:block; margin-top:4px; color:#12314f; font-size:21px;">{{ metric.value }}</strong><small style="display:block; margin-top:2px; color:#6d8094; font-size:9.5px;">{{ metric.detail }}</small></div></sc-for> 5508 </div> 5509 <div style="display:flex; gap:7px; flex-wrap:wrap;"><sc-for list="{{ mailingReportRates }}" as="rate" hint-placeholder-count="5"><span style="padding:6px 9px; border-radius:999px; border:1px solid #d4e1ec; background:#f8fbfd; color:#49627a; font-size:10px; font-weight:750;"><strong style="color:#075ca8;">{{ rate.value }}</strong> {{ rate.label }}</span></sc-for></div> 5510 <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:9px; align-items:center;"> 5511 <label style="position:relative; display:block;"><img src="assets/icons/lucide/search.svg" alt="" style="position:absolute; left:11px; top:50%; width:15px; height:15px; transform:translateY(-50%); opacity:.55;"><input type="search" value="" data-dc-attr-value="{{ mailingReportSearch }}" onInput="{{ onMailingReportSearch }}" placeholder="Buscar por email o nombre" style="width:100%; height:40px; box-sizing:border-box; border:1px solid #cbd9e6; border-radius:9px; padding:0 12px 0 34px; background:#fff; color:#183650; font:inherit; font-size:11.5px;"></label> 5512 <select value="" data-dc-attr-value="{{ mailingReportStatusFilter }}" onChange="{{ onMailingReportStatus }}" style="width:100%; height:40px; border:1px solid #cbd9e6; border-radius:9px; padding:0 11px; background:#fff; color:#355069; font:inherit; font-size:11px; font-weight:750;"><option value="all">Todos los estados</option><option value="accepted">Aceptados</option><option value="delivered">Entregados</option><option value="opened">Abiertos</option><option value="clicked">Con clic</option><option value="temporary_fail">Rebote temporal</option><option value="permanent_fail">Rebote definitivo</option><option value="unsubscribed">Desuscriptos</option><option value="complained">Quejas</option></select> 5513 </div> 5514 <div style="display:flex; align-items:center; gap:5px; overflow-x:auto; padding:2px 0 5px; scrollbar-width:thin;" aria-label="Filtrar contactos por inicial"><sc-for list="{{ mailingReportLetters }}" as="letter" hint-placeholder-count="28"><button type="button" onClick="{{ letter.select }}" disabled="{{ letter.disabled }}" title="{{ letter.count }} contactos" style="flex:0 0 auto; min-width:30px; height:30px; padding:0 8px; border:1px solid {{ letter.border }}; border-radius:8px; background:{{ letter.bg }}; color:{{ letter.color }}; opacity:{{ letter.opacity }}; font:inherit; font-size:10px; font-weight:850; cursor:pointer;">{{ letter.label }}</button></sc-for></div> 5515 <div style="overflow:auto; border:1px solid #e0e8f0; border-radius:10px; max-height:390px;"> 5516 <table style="width:100%; min-width:980px; border-collapse:collapse; font-size:10.5px;"> 5517 <thead style="position:sticky; top:0; z-index:1; background:#edf3f8;"><tr><th style="padding:9px 10px; text-align:left;">Contacto</th><th style="padding:9px 10px; text-align:left;">Tipo</th><th style="padding:9px 10px; text-align:left;">Estado</th><th style="padding:9px 10px; text-align:left;">Enviado</th><th style="padding:9px 10px; text-align:left;">Entregado</th><th style="padding:9px 10px; text-align:left;">Aperturas</th><th style="padding:9px 10px; text-align:left;">Clics</th><th style="padding:9px 10px; text-align:left;">Detalle</th></tr></thead> 5518 <tbody><sc-for list="{{ mailingReportRows }}" as="row" hint-placeholder-count="4"><tr style="border-top:1px solid #e7edf3;"><td style="padding:9px 10px;"><strong style="display:block; color:#153450;">{{ row.email }}</strong><span style="color:#718397;">{{ row.name }}</span></td><td style="padding:9px 10px;"><span style="display:inline-block; padding:4px 7px; border-radius:999px; background:{{ row.kindBg }}; color:{{ row.kindColor }}; font-size:9px; font-weight:850;">{{ row.kindLabel }}</span></td><td style="padding:9px 10px;"><span style="display:inline-block; padding:4px 7px; border-radius:999px; background:{{ row.statusBg }}; color:{{ row.statusColor }}; font-size:9px; font-weight:850;">{{ row.statusLabel }}</span></td><td style="padding:9px 10px; color:#52687d;">{{ row.sentAt }}</td><td style="padding:9px 10px; color:#52687d;">{{ row.deliveredAt }}</td><td style="padding:9px 10px; color:#52687d;"><strong>{{ row.openCount }}</strong><br><small>{{ row.openedAt }}</small></td><td style="padding:9px 10px; color:#52687d;"><strong>{{ row.clickCount }}</strong><br><small>{{ row.clickedAt }}</small></td><td style="padding:9px 10px; color:#7a5160; max-width:260px; white-space:normal;">{{ row.error }}</td></tr></sc-for></tbody> 5519 </table> 5520 <sc-if value="{{ mailingReportRowsEmpty }}" hint-placeholder-val="{{ false }}"><div style="padding:30px 18px; text-align:center; color:#718397; font-size:11px;">No hay envíos que coincidan con la búsqueda y los filtros.</div></sc-if> 5521 </div> 5522 <div style="display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;"><div style="font-size:10px; color:#718397;">Mostrando {{ mailingReportRange }} de {{ mailingReportFilteredTotal }} resultados filtrados · {{ mailingReportTotal }} envíos registrados.</div><div style="display:flex; align-items:center; gap:7px;"><button type="button" onClick="{{ mailingReportPrev }}" disabled="{{ mailingReportPrevDisabled }}" style="height:32px; padding:0 11px; border:1px solid #cbd9e6; border-radius:8px; background:#fff; color:#355069; opacity:{{ mailingReportPrevOpacity }}; font:inherit; font-size:10px; font-weight:800; cursor:pointer;">Anterior</button><span style="min-width:88px; text-align:center; color:#52687d; font-size:10px; font-weight:750;">{{ mailingReportPageLabel }}</span><button type="button" onClick="{{ mailingReportNext }}" disabled="{{ mailingReportNextDisabled }}" style="height:32px; padding:0 11px; border:1px solid #cbd9e6; border-radius:8px; background:#fff; color:#355069; opacity:{{ mailingReportNextOpacity }}; font:inherit; font-size:10px; font-weight:800; cursor:pointer;">Siguiente</button></div></div> 5523 </div> 5524 </sc-if> 5525 </article> 5526 </sc-if> 5527 </section> 5528 </sc-if> 5529 5530 <sc-if value="{{ profileActivityVisible }}" hint-placeholder-val="{{ false }}"> 5531 <sc-if value="{{ profileRoslotVisible }}" hint-placeholder-val="{{ false }}"> 5532 <sc-if value="{{ showBuyerCreditPanel }}" hint-placeholder-val="{{ false }}"> 5533 <section data-reveal class="credit-live-panel"> 5534 <div class="credit-live-head"> 5535 <div class="credit-live-title"><div><div class="credit-live-eyebrow">MI CRÉDITO</div><h3>Crédito de compra</h3></div><span class="credit-live-status">EN TIEMPO REAL</span></div> 5536 <button type="button" onClick="{{ buyerCreditHistory }}" class="credit-live-history"><i class="dc-lucide dc-lucide-rotate" aria-hidden="true"></i> Historial</button> 5537 </div> 5538 <sc-if value="{{ buyerCreditLoading }}" hint-placeholder-val="{{ false }}"> 5539 <div style="display:flex; align-items:center; justify-content:center; gap:10px; padding:30px 20px; border-top:1px solid #f0f4f9; color:#65758a; font-size:13.5px; font-weight:600;"><span style="width:18px; height:18px; border:2px solid #d9e4ef; border-top-color:#0a5ca8; border-radius:50%; animation:spin 0.8s linear infinite;"></span>Actualizando crédito...</div> 5540 </sc-if> 5541 <sc-if value="{{ buyerCreditHas }}" hint-placeholder-val="{{ false }}"> 5542 <sc-for list="{{ buyerCreditRows }}" as="c" hint-placeholder-count="2"> 5543 <div class="credit-live-row"> 5544 <div class="credit-live-row-head"> 5545 <div class="credit-live-party"><strong>{{ c.seller }}</strong><span>{{ c.periodLabel }}</span></div> 5546 <button type="button" onClick="{{ c.openHistory }}" disabled="{{ c.summaryDisabled }}" class="credit-live-history" style="opacity:{{ c.summaryOpacity }};">Anterior</button> 5547 </div> 5548 <sc-if value="{{ c.inlineLoading }}" hint-placeholder-val="{{ false }}"><div style="display:flex; align-items:center; gap:8px; margin-top:14px; padding:15px; border-radius:12px; background:#f7fafc; color:#65758a; font-size:12px; font-weight:700;"><span style="width:16px; height:16px; border:2px solid #d8e4ef; border-top-color:#0a5ca8; border-radius:50%; animation:spin .8s linear infinite;"></span>Cargando actividad del crédito vigente...</div></sc-if> 5549 <sc-if value="{{ c.inlineHasError }}" hint-placeholder-val="{{ false }}"><div style="margin-top:14px; padding:13px 14px; border:1px solid #f0c7cc; border-radius:12px; background:#fff5f6; color:#9d2837; font-size:12px;"><strong>No se pudo cargar la actividad vigente.</strong><button type="button" onClick="{{ c.refreshInline }}" style="margin-left:10px; border:0; background:transparent; color:#075ca8; font:800 11px inherit; cursor:pointer;">Reintentar</button></div></sc-if> 5550 <sc-if value="{{ c.inlineReady }}" hint-placeholder-val="{{ false }}"> 5551 <div class="credit-live-metrics"><div class="credit-live-metric is-primary"><small>DISPONIBLE</small><strong>{{ c.inlineAvailable }}</strong></div><div class="credit-live-metric is-used"><small>USADO</small><strong>{{ c.inlineUsed }}</strong></div><div class="credit-live-metric"><small>AUTORIZADO</small><strong>{{ c.inlineAuthorized }}</strong></div></div> 5552 <div class="credit-live-progress" title="{{ c.inlineProgressLabel }} utilizado"><span style="width:{{ c.inlineProgress }};"></span></div> 5553 <sc-if value="{{ c.inlineHasMovements }}" hint-placeholder-val="{{ false }}"><div class="credit-live-movements"><sc-for list="{{ c.inlineMovements }}" as="movement" hint-placeholder-count="3"><article class="credit-live-movement"><div><strong>{{ movement.lot }} · {{ movement.statusLabel }}</strong><small>{{ movement.date }}</small></div><b>{{ movement.consumedAmount }}</b></article></sc-for></div></sc-if> 5554 <sc-if value="{{ c.inlineEmpty }}" hint-placeholder-val="{{ false }}"><div style="margin-top:8px; color:#718397; font-size:10.5px;">Sin movimientos en esta vigencia.</div></sc-if> 5555 </sc-if> 5556 </div> 5557 </sc-for> 5558 </sc-if> 5559 <sc-if value="{{ buyerCreditEmpty }}" hint-placeholder-val="{{ false }}"> 5560 <div style="padding:28px 20px; border-top:1px solid #f0f4f9; text-align:center;"><div style="font-size:15px; font-weight:800; color:#112033;">No tenés crédito asignado</div><div style="font-size:13px; color:#65758a; margin-top:4px;">Cuando un vendedor te autorice, vas a ver acá el monto semanal y la vigencia del crédito.</div></div> 5561 </sc-if> 5562 </section> 5563 </sc-if> 5564 5565 <sc-if value="{{ creditHistoryVisible }}" hint-placeholder-val="{{ false }}"> 5566 <div onClick="{{ closeCreditHistoryBackdrop }}" style="position:fixed; inset:0; z-index:11990; display:flex; align-items:center; justify-content:center; padding:18px; background:rgba(0,32,63,.58); backdrop-filter:blur(8px); animation:fadeIn 180ms ease both;"> 5567 <section onClick="{{ stopCreditHistoryClick }}" role="dialog" aria-modal="true" aria-label="Histórico de créditos" style="width:min(820p
5567x,100%); max-height:min(760px,calc(100vh - 36px)); overflow:auto; background:#fff; border:1px solid #dbe7f2; border-radius:22px; box-shadow:0 28px 80px rgba(0,25,55,.28); animation:floatIn 260ms cubic-bezier(.22,.72,.18,1) both;"> 5568 <header style="position:sticky; top:0; z-index:2; display:flex; justify-content:space-between; align-items:flex-start; gap:14px; padding:20px 22px; background:linear-gradient(135deg,#002e5f,#0a65ad); color:#fff;"> 5569 <div style="min-width:0;"><div style="font-size:10px; letter-spacing:.14em; font-weight:900; color:#8bd7ff;">HISTÃRICO DE CRÃDITOS · ROSLOT</div><h3 style="margin:4px 0 0; font-size:clamp(20px,3vw,27px); line-height:1.12;">{{ creditHistoryTitle }}</h3><div style="margin-top:6px; color:#d8eafa; font-size:12.5px;">Créditos finalizados o desactivados. El crédito vigente permanece visible en el perfil.</div></div> 5570 <button type="button" onClick="{{ closeCreditHistory }}" aria-label="Cerrar" style="width:38px; height:38px; flex:0 0 auto; display:grid; place-items:center; border:1px solid rgba(255,255,255,.3); border-radius:11px; background:rgba(255,255,255,.12); color:#fff; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button> 5571 </header> 5572 <div style="padding:18px 20px 22px;"> 5573 <sc-if value="{{ creditHistoryHas }}" hint-placeholder-val="{{ false }}"><div style="display:flex; flex-direction:column; gap:9px;"><sc-for list="{{ creditHistoryRows }}" as="history" hint-placeholder-count="4"><article style="display:grid; grid-template-columns:repeat(auto-fit,minmax(135px,1fr)); gap:10px; align-items:center; padding:13px 14px; border:1px solid #e0e9f2; border-radius:13px; background:#fff;"><div style="min-width:0;"><span style="display:block; color:#718397; font-size:8.5px; font-weight:900; letter-spacing:.08em;">{{ creditHistoryPartyLabel }}</span><strong style="display:block; margin-top:3px; color:#173652; font-size:12.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ history.party }}</strong></div><div><span style="display:block; color:#718397; font-size:8.5px; font-weight:900; letter-spacing:.08em;">VIGENCIA</span><strong style="display:block; margin-top:3px; color:#173652; font-size:11.5px;">{{ history.period }}</strong></div><div><span style="display:block; color:#718397; font-size:8.5px; font-weight:900; letter-spacing:.08em;">CRÃDITO SEMANAL</span><strong style="display:block; margin-top:3px; color:#075ca8; font-size:14px;">{{ history.amount }}</strong></div><div style="display:flex; align-items:center; justify-content:flex-end; gap:8px; flex-wrap:wrap;"><span style="padding:5px 8px; border-radius:999px; background:{{ history.statusBg }}; color:{{ history.statusColor }}; font-size:9px; font-weight:900;">{{ history.statusLabel }}</span><button type="button" onClick="{{ history.openDetail }}" disabled="{{ history.detailDisabled }}" style="min-height:34px; border:1px solid #bfd3e7; border-radius:9px; background:#fff; color:#075ca8; padding:7px 10px; font:800 10.5px inherit; cursor:pointer; opacity:{{ history.detailOpacity }};">Ver detalle</button></div></article></sc-for></div></sc-if> 5574 <sc-if value="{{ creditHistoryEmpty }}" hint-placeholder-val="{{ false }}"><div style="padding:42px 20px; border:1px dashed #cad8e6; border-radius:14px; text-align:center; color:#6e8194;"><i class="dc-lucide dc-lucide-rotate" aria-hidden="true" style="font-size:24px; color:#0a5ca8;"></i><strong style="display:block; margin-top:10px; color:#183750; font-size:15px;">TodavÃa no hay créditos anteriores</strong><span style="display:block; margin-top:4px; font-size:12px;">Cuando finalice una vigencia, aparecerá en este histórico.</span></div></sc-if> 5575 </div> 5576 </section> 5577 </div> 5578 </sc-if> 5579 5580 <sc-if value="{{ creditSummaryVisible }}" hint-placeholder-val="{{ false }}"> 5581 <div onClick="{{ closeCreditSummaryBackdrop }}" style="position:fixed; inset:0; z-index:12000; display:flex; align-items:center; justify-content:center; padding:18px; background:rgba(0,32,63,.58); backdrop-filter:blur(8px); animation:fadeIn 180ms ease both;"> 5582 <section onClick="{{ stopCreditSummaryClick }}" role="dialog" aria-modal="true" aria-label="Historial de crédito" style="width:min(920px,100%);
5582 max-height:min(820px,calc(100vh - 36px)); overflow:auto; background:#fff; border:1px solid #dbe7f2; border-radius:22px; box-shadow:0 28px 80px rgba(0,25,55,.28); animation:floatIn 260ms cubic-bezier(.22,.72,.18,1) both;"> 5583 <header style="position:sticky; top:0; z-index:2; display:flex; justify-content:space-between; align-items:flex-start; gap:14px; padding:20px 22px; background:linear-gradient(135deg,#002e5f,#0a65ad); color:#fff;"> 5584 <div style="min-width:0;"><div style="font-size:10px; letter-spacing:.14em; font-weight:900; color:#8bd7ff;">HISTORIAL DE CRÃDITO · ROSLOT</div><h3 style="margin:4px 0 0; font-size:clamp(20px,3vw,27px); line-height:1.12;">{{ creditSummaryTitle }}</h3><div style="margin-top:6px; color:#d8eafa; font-size:12.5px;">{{ creditSummarySubtitle }}</div></div> 5585 <button type="button" onClick="{{ closeCreditSummary }}" aria-label="Cerrar" style="width:38px; height:38px; flex:0 0 auto; display:grid; place-items:center; border:1px solid rgba(255,255,255,.3); border-radius:11px; background:rgba(255,255,255,.12); color:#fff; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button> 5586 </header> 5587 <div style="padding:20px 22px 24px;"> 5588 <sc-if value="{{ creditSummaryLoading }}" hint-placeholder-val="{{ false }}"><div style="display:flex; align-items:center; justify-content:center; gap:10px; min-height:230px; color:#65758a; font-size:13px; font-weight:700;"><span style="width:20px; height:20px; border:2px solid #d9e4ef; border-top-color:#0a5ca8; border-radius:50%; animation:spin .8s linear infinite;"></span>Cargando movimientos...</div></sc-if> 5589 <sc-if value="{{ creditSummaryHasError }}" hint-placeholder-val="{{ false }}"><div style="padding:18px; border:1px solid #f0c7cc; border-radius:14px; background:#fff4f5; color:#a12636;"><strong style="display:block;">No se pudo cargar el historial</strong><span style="display:block; margin-top:4px; font-size:12px;">{{ creditSummaryError }}</span><button type="button" onClick="{{ refreshCreditSummary }}" style="margin-top:12px; border:1px solid #d9aeb5; border-radius:9px; background:#fff; color:#a12636; padding:8px 12px; font:800 11px inherit; cursor:pointer;">Reintentar</button></div></sc-if> 5590 <sc-if value="{{ creditSummaryReady }}" hint-placeholder-val="{{ false }}"> 5591 <div style="display:grid; grid-template-columns:{{ creditSummaryStatsCols }}; gap:10px;"> 5592 <sc-for list="{{ creditSummaryStats }}" as="stat" hint-placeholder-count="3"><div style="padding:14px 15px; border:1px solid {{ stat.border }}; border-radius:14px; background:{{ stat.bg }};"><span style="display:block; font-size:9.5px; letter-spacing:.09em; color:{{ stat.color }}; font-weight:900;">{{ stat.label }}</span><strong style="display:block; margin-top:6px; color:#102f4c; font-size:clamp(20px,3vw,27px); font-variant-numeric:tabular-nums;">{{ stat.value }}</strong></div></sc-for> 5593 </div> 5594 <div style="margin-top:15px; padding:14px 15px; border:1px solid #dfe9f3; border-radius:14px; background:#f8fbfd;"><div style="display:flex; justify-content:space-between; gap:12px; margin-bottom:8px; color:#536a80; font-size:11px; font-weight:800;"><span>CRÃDITO UTILIZADO</span><span>{{ creditSummaryProgressLabel }}</span></div><div style="height:10px; overflow:hidden; border-radius:999px; background:#dce7f1;"><div style="width:{{ creditSummaryProgress }}; height:100%; border-radius:inherit; background:linear-gradient(90deg,#0a5ca8,#1f9b7a); transition:width 500ms cubic-bezier(.22,.72,.18,1);"></div></div></div> 5595 <div style="display:flex; align-items:center; justify-content:space-between; gap:12px; margin:22px 0 10px;"><div><div style="font-size:10px; letter-spacing:.12em; color:#0a5ca8; font-weight:900;">MOVIMIENTOS</div><h4 style="margin:3px 0 0; color:#112033; font-size:17px;">Actividad del crédito</h4></div><button type="button" onClick="{{ refreshCreditSummary }}" style="height:36px; border:1px solid #cbdbea; border-radius:9px; background:#fff; color:#075ca8; padding:0 12px; font:800 10.5px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-refresh-cw" aria-hidden="true"></i> Actualizar</button></div> 5596 <sc-if value="{{ creditSummaryHasMovements }}" hint-placeholder-val="{{ false }}"><div style="display:flex; flex-direction:column; gap:8px;"><sc-for list="{{ creditSummaryMovements }}" as="movement" hint-placeholder-count="4"><article style="display:grid; grid-template-columns:{{ creditMovementCols }}; gap:10px; align-items:center; padding:12px 13px; border:1px solid #e1eaf3; border-radius:12px; background:#fff;"><div style="min-width:0;"><strong style="display:block; color:#163651; font-size:12.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ movement.lot }}</strong><span style="display:block; margin-top:3px; color:#7a8c9e; font-size:10.5px;">{{ movement.date }}</span></div><span style="justify-self:start; padding:5px 8px; border-radius:999px; background:{{ movement.statusBg }}; color:{{ movement.statusColor }}; font-size:9.5px; font-weight:900;">{{ movement.statusLabel }}</span><div><span style="display:block; color:#7a8c9e; font-size:9px; font-weight:800;">IMPORTE OFERTADO</span><strong style="display:block; margin-top:3px; color:#12314f; font-size:13px;">{{ movement.bidAmount }}</strong></div><div><span style="display:block; color:#7a8c9e; font-size:9px; font-weight:800;">CRÃDITO CONSUMIDO</span><strong style="display:block; margin-top:3px; color:#075ca8; font-size:13px;">{{ movement.consumedAmount }}</strong></div></article></sc-for></div></sc-if> 5597 <sc-if value="{{ creditSummaryEmpty }}" hint-placeholder-val="{{ false }}"><div style="padding:34px 18px; border:1px dashed #cad8e6; border-radius:14px; text-align:center; color:#6e8194;"><strong style="display:block; color:#183750;">TodavÃa no hay movimientos</strong><span style="display:block; margin-top:4px; font-size:12px;">Las ofertas vigentes y los lotes ganados aparecerán acá.</span></div></sc-if> 5598 </sc-if> 5599 </div> 5600 </section> 5601 </div> 5602 </sc-if> 5603 5604 <sc-if value="{{ showLiveOffersPanel }}" hint-placeholder-val="{{ true }}"> 5605 <div data-reveal style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.07); overflow:hidden; margin-bottom:16px;"> 5606 <div style="display:flex; justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap; padding:16px 20px 12px;"> 5607 <div><div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700;">MIS OFERTAS</div><h3 style="font-size:19px; font-weight:800; margin:3px 0 0; color:#112033;">Seguimiento en vivo</h3></div>
5608 <span style="display:inline-flex; align-items:center; gap:7px; font-size:11px; font-weight:800; color:{{ livePanelStatusColor }}; background:{{ livePanelStatusBg }}; padding:6px 12px; border-radius:100px;"><span style="width:7px; height:7px; border-radius:50%; background:{{ livePanelStatusColor }}; animation:{{ livePanelDotAnimation }};"></span>{{ livePanelStatus }}</span> 5609 </div> 5610 <sc-for list="{{ liveOffers }}" as="o" hint-placeholder-count="3"> 5611 <div style="display:flex; align-items:center; gap:14px; padding:14px 20px; border-top:1px solid #f0f4f9; flex-wrap:wrap;"> 5612 <div onClick="{{ o.open }}" style="width:52px; height:52px; border-radius:12px; background-image:url('{{ o.image }}'); background-size:cover; background-position:center; flex-shrink:0; cursor:pointer;"></div> 5613 <div onClick="{{ o.open }}" style="flex:1; min-width:130px; cursor:pointer;"><div style="font-size:14.5px; font-weight:700; color:#112033;">{{ o.categoria }}</div><div style="font-size:12px; color:#65758a;">Cert. {{ o.cert }} · {{ o.origen }}</div></div> 5614 <div style="text-align:right;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">MI OFERTA</div><div style="font-size:16px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ o.myRoll }}</div></div> 5615 <div style="text-align:right; min-width:96px;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">MEJOR OFERTA</div><div style="font-size:16px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ o.marketRoll }}</div></div> 5616 <span style="display:inline-flex; align-items:center; gap:6px; font-size:11px; font-weight:800; padding:6px 12px; border-radius:100px; background:{{ o.standBg }}; color:{{ o.standColor }};"> 5617 <sc-if value="{{ o.winning }}" hint-placeholder-val="{{ true }}"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 17 9 11 13 15 21 7"></polyline><polyline points="15 7 21 7 21 13"></polyline></svg></sc-if> 5618 <sc-if value="{{ o.showOutbidIcon }}" hint-placeholder-val="{{ false }}"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 7 9 13 13 9 21 17"></polyline><polyline points="15 17 21 17 21 11"></polyline></svg></sc-if> 5619 {{ o.standLabel }} 5620 </span> 5621 <sc-if value="{{ o.outbid }}" hint-placeholder-val="{{ false }}"> 5622 <div style="display:flex; align-items:center; gap:8px; background:#f6f9fc; border:1px solid #e6edf5; border-radius:12px; padding:6px 8px;"> 5623 <div style="display:flex; gap:4px;"><sc-for list="{{ o.incOpts }}" as="inc" hint-placeholder-count="2"><span onClick="{{ inc.pick }}" style="font-size:11px; font-weight:800; padding:5px 9px; border-radius:8px; cursor:pointer; background:{{ inc.activeBg }}; color:{{ inc.activeColor }}; border:1px solid {{ inc.activeBorder }};">{{ inc.label }}</span></sc-for></div> 5624 <div style="display:flex; align-items:center; gap:6px;"> 5625 <span onClick="{{ o.minus }}" style="width:26px; height:26px; border-radius:8px; background:#fff; border:1px solid #dde7f2; color:#c22; display:grid; place-items:center; font-size:16px; font-weight:800; cursor:pointer; user-select:none;" style-active="transform:scale(0.9);"><i class="dc-lucide dc-lucide-minus" aria-hidden="true"></i></span> 5626 <span style="min-width:58px; text-align:center; font-size:13px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ o.offsetLabel }}</span> 5627 <span onClick="{{ o.plus }}" style="width:26px; height:26px; border-radius:8px; background:#fff; border:1px solid #dde7f2; color:#1f7a63; display:grid; place-items:center; font-size:16px; font-weight:800; cursor:pointer; user-select:none;" style-active="transform:scale(0.9);"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></span> 5628 </div> 5629 <button onClick="{{ o.confirm }}" disabled="{{ o.submitting }}" style="display:inline-flex; align-items:center; gap:6px; background:#e63946; color:#fff; border:none; border-radius:9px; padding:8px 14px; font-size:12.5px; font-weight:800; font-family:inherit; cursor:pointer; white-space:nowrap; opacity:{{ o.submitOpacity }};" style-hover="background:#cf2f3c;" style-active="transform:scale(0.96);">{{ o.confirmLabel }}</button> 5630 </div> 5631 </sc-if> 5632 </div> 5633 </sc-for> 5634 <sc-if value="{{ buyerClosedHas }}" hint-placeholder-val="{{ false }}"> 5635 <details style="border-top:1px solid #e5edf5; background:#f8fafc;"> 5636 <summary style="display:flex; align-items:center; justify-content:space-between; gap:12px; padding:14px 20px; color:#3d4c5f; font-size:13px; font-weight:800; cursor:pointer; user-select:none; list-style:none;">
5637 <span>Ofertas cerradas</span><span style="background:#e8eef5; color:#65758a; border-radius:100px; padding:4px 9px;">{{ buyerClosedCount }}</span> 5638 </summary> 5639 <sc-for list="{{ buyerClosedOfferRows }}" as="o" hint-placeholder-count="2"> 5640 <div style="display:flex; align-items:center; gap:14px; padding:14px 20px; border-top:1px solid #e8eef5; flex-wrap:wrap; background:#fff;"> 5641 <div onClick="{{ o.open }}" style="width:52px; height:52px; border-radius:12px; background-image:url('{{ o.image }}'); background-size:cover; background-position:center; flex-shrink:0; cursor:pointer; filter:saturate(0.75);"></div> 5642 <span style="font-size:10.5px; font-weight:800; padding:6px 10px; border-radius:100px; color:#65758a; background:#e8eef5;">OFERTA CERRADA</span> 5643 <div onClick="{{ o.open }}" style="flex:1; min-width:150px; cursor:pointer;"><div style="font-size:14.5px; font-weight:700; color:#112033;">{{ o.categoria }}</div><div style="font-size:12px; color:#65758a;">Cert. {{ o.cert }} · {{ o.origen }}</div></div> 5644 <div style="text-align:right;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">{{ o.amountLabel }}</div><div style="font-size:16px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ o.amount }}</div></div> 5645 <span style="font-size:11px; font-weight:800; padding:6px 12px; border-radius:100px; background:{{ o.resultBg }}; color:{{ o.resultColor }};">{{ o.resultLabel }}</span> 5646 </div> 5647 </sc-for> 5648 <sc-if value="{{ buyerClosedPaginated }}" hint-placeholder-val="{{ false }}"> 5649 <div style="display:flex; align-items:center; justify-content:center; gap:12px; padding:14px 20px; border-top:1px solid #e8eef5;"> 5650 <button onClick="{{ buyerClosedPrev }}" disabled="{{ buyerClosedPrevDisabled }}" style="border:1px solid #d8e3ee; background:#fff; color:#003d73; border-radius:9px; padding:8px 12px; font:inherit; font-size:12px; font-weight:800; cursor:pointer;">Anterior</button> 5651 <span style="font-size:12px; color:#65758a; font-weight:700;">{{ buyerClosedPageLabel }}</span> 5652 <button onClick="{{ buyerClosedNext }}" disabled="{{ buyerClosedNextDisabled }}" style="border:1px solid #d8e3ee; background:#fff; color:#003d73; border-radius:9px; padding:8px 12px; font:inherit; font-size:12px; font-weight:800; cursor:pointer;">Siguiente</button> 5653 </div> 5654 </sc-if> 5655 </details> 5656 </sc-if> 5657 <sc-if value="{{ liveOffersLoading }}" hint-placeholder-val="{{ false }}"> 5658 <div style="display:flex; align-items:center; justify-content:center; gap:10px; padding:28px 20px; border-top:1px solid #f0f4f9; color:#65758a; font-size:13.5px; font-weight:600;"> 5659 <span style="width:18px; height:18px; border:2px solid #d9e4ef; border-top-color:#0a5ca8; border-radius:50%; animation:spin 0.8s linear infinite;"></span> 5660 Actualizando ofertas... 5661 </div> 5662 </sc-if> 5663 <sc-if value="{{ liveOffersEmpty }}" hint-placeholder-val="{{ false }}"> 5664 <div style="padding:30px 20px; border-top:1px solid #f0f4f9; text-align:center;"> 5665 <div style="font-size:15px; font-weight:800; color:#112033;">No tenés ofertas activas</div> 5666 <div style="font-size:13px; color:#65758a; margin-top:4px;">Cuando ofertes, vas a ver acá si seguÃs ganando o si te superaron.</div> 5667 </div> 5668 </sc-if> 5669 </div> 5670 </sc-if> 5671 5672 <sc-if value="{{ showSellerOffersPanel }}" hint-placeholder-val="{{ false }}"> 5673 <div id="seller-offers-panel" data-reveal style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.07); overflow:hidden; margin-bottom:16px;"> 5674 <div style="display:flex; justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap; padding:16px 20px 12px;"> 5675 <div><div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700;">OFERTAS RECIBIDAS</div><h3 style="font-size:19px; font-weight:800; margin:3px 0 0; color:#112033;">Seguimiento de mis lotes</h3></div>
5676 <span style="display:inline-flex; align-items:center; gap:7px; font-size:11px; font-weight:800; color:{{ sellerOffersStatusColor }}; background:{{ sellerOffersStatusBg }}; padding:6px 12px; border-radius:100px;"><span style="width:7px; height:7px; border-radius:50%; background:{{ sellerOffersStatusColor }}; animation:{{ sellerOffersStatusAnimation }};"></span>{{ sellerOffersStatus }}</span> 5677 </div> 5678 <sc-for list="{{ sellerOpenOfferRows }}" as="o" hint-placeholder-count="2"> 5679 <div style="display:flex; align-items:center; gap:14px; padding:14px 20px; border-top:1px solid #f0f4f9; flex-wrap:wrap;"> 5680 <div onClick="{{ o.open }}" style="width:52px; height:52px; border-radius:12px; background-image:url('{{ o.image }}'); background-size:cover; background-position:center; flex-shrink:0; cursor:pointer;"></div> 5681 <span style="display:inline-flex; align-items:center; gap:6px; font-size:10.5px; font-weight:800; padding:6px 10px; border-radius:100px; color:#1f7a63; background:rgba(31,122,99,0.12);"><span style="width:6px; height:6px; border-radius:50%; background:#1f7a63; animation:pulseDot 1.2s infinite;"></span>OFERTA ABIERTA</span> 5682 <div onClick="{{ o.open }}" style="flex:1; min-width:170px; cursor:pointer;"><div style="font-size:14.5px; font-weight:700; color:#112033;">{{ o.categoria }}</div><div style="font-size:12px; color:#65758a;">Cert. {{ o.cert }} · {{ o.origen }}</div></div> 5683 <div style="text-align:right;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">BASE</div><div style="font-size:15px; font-weight:800; color:#3d4c5f; font-variant-numeric:tabular-nums;">{{ o.base }}</div></div> 5684 <div style="min-width:130px;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">{{ o.buyerLabel }}</div><div style="font-size:13px; font-weight:700; color:#112033;">{{ o.buyer }}</div></div> 5685 <div style="text-align:right;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">{{ o.amountLabel }}</div><div style="font-size:17px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ o.amount }}</div></div> 5686 <span style="display:inline-flex;align-items:center;gap:4px;font-size:11px; font-weight:800; padding:6px 10px; border-radius:100px; color:{{ o.variationColor }}; background:{{ o.variationBg }};"><sc-if value="{{ o.variationUp }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-trending-up" aria-hidden="true"></i></sc-if><sc-if value="{{ o.variationDown }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-trending-down" aria-hidden="true"></i></sc-if>{{ o.variation }}</span> 5687 <span style="font-size:11px; font-weight:800; padding:6px 11px; border-radius:100px; background:#eef4fb; color:#0a5ca8;">{{ o.count }}</span> 5688 <button onClick="{{ o.open }}" style="background:#002e5f; color:#fff; border:none; border-radius:9px; padding:9px 14px; font-size:12.5px; font-weight:800; font-family:inherit; cursor:pointer;">Ver ofertas</button> 5689 </div> 5690 </sc-for> 5691 <sc-if value="{{ sellerOffersLoading }}" hint-placeholder-val="{{ false }}"> 5692 <div style="display:flex; align-items:center; justify-content:center; gap:10px; padding:28px 20px; border-top:1px solid #f0f4f9; color:#65758a; font-size:13.5px; font-weight:600;"><span style="width:18px; height:18px; border:2px solid #d9e4ef; border-top-color:#0a5ca8; border-radius:50%; animation:spin 0.8s linear infinite;"></span>Actualizando ofertas recibidas...</div> 5693 </sc-if> 5694 <sc-if value="{{ sellerOpenOffersEmpty }}" hint-placeholder-val="{{ false }}"> 5695 <div style="padding:24px 20px; border-top:1px solid #f0f4f9; text-align:center;"><div style="font-size:15px; font-weight:800; color:#112033;">No hay ofertas abiertas en este momento</div><div style="font-size:13px; color:#65758a; margin-top:4px;">Las nuevas ofertas aparecerán acá automáticamente.</div></div> 5696 </sc-if> 5697 <sc-if value="{{ sellerClosedHas }}" hint-placeholder-val="{{ false }}"> 5698 <details style="border-top:1px solid #e5edf5; background:#f8fafc;"> 5699 <summary style="display:flex; align-items:center; justify-content:space-between; gap:12px; padding:14px 20px; color:#3d4c5f; font-size:13px; font-weight:800; cursor:pointer; user-select:none; list-style:none;">
5700 <span>Ofertas cerradas</span><span style="background:#e8eef5; color:#65758a; border-radius:100px; padding:4px 9px;">{{ sellerClosedCount }}</span> 5701 </summary> 5702 <sc-for list="{{ sellerClosedOfferRows }}" as="o" hint-placeholder-count="2"> 5703 <div style="display:flex; align-items:center; gap:14px; padding:14px 20px; border-top:1px solid #e8eef5; flex-wrap:wrap; background:#fff;"> 5704 <div onClick="{{ o.open }}" style="width:52px; height:52px; border-radius:12px; background-image:url('{{ o.image }}'); background-size:cover; background-position:center; flex-shrink:0; cursor:pointer; filter:saturate(0.75);"></div> 5705 <span style="font-size:10.5px; font-weight:800; padding:6px 10px; border-radius:100px; color:#65758a; background:#e8eef5;">OFERTA CERRADA</span> 5706 <div onClick="{{ o.open }}" style="flex:1; min-width:170px; cursor:pointer;"><div style="font-size:14.5px; font-weight:700; color:#112033;">{{ o.categoria }}</div><div style="font-size:12px; color:#65758a;">Cert. {{ o.cert }} · {{ o.origen }}</div></div> 5707 <div style="text-align:right;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">BASE</div><div style="font-size:15px; font-weight:800; color:#3d4c5f; font-variant-numeric:tabular-nums;">{{ o.base }}</div></div> 5708 <div style="min-width:130px;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">{{ o.buyerLabel }}</div><div style="font-size:13px; font-weight:700; color:#112033;">{{ o.buyer }}</div></div> 5709 <div style="text-align:right;"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">{{ o.amountLabel }}</div><div style="font-size:17px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ o.amount }}</div></div> 5710 <span style="display:inline-flex;align-items:center;gap:4px;font-size:11px; font-weight:800; padding:6px 10px; border-radius:100px; color:{{ o.variationColor }}; background:{{ o.variationBg }};"><sc-if value="{{ o.variationUp }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-trending-up" aria-hidden="true"></i></sc-if><sc-if value="{{ o.variationDown }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-trending-down" aria-hidden="true"></i></sc-if>{{ o.variation }}</span> 5711 <span style="font-size:11px; font-weight:800; padding:6px 11px; border-radius:100px; background:#eef4fb; color:#0a5ca8;">{{ o.count }}</span> 5712 <button onClick="{{ o.open }}" style="background:#002e5f; color:#fff; border:none; border-radius:9px; padding:9px 14px; font-size:12.5px; font-weight:800; font-family:inherit; cursor:pointer;">Ver ofertas</button> 5713 </div> 5714 </sc-for> 5715 <sc-if value="{{ sellerClosedPaginated }}" hint-placeholder-val="{{ false }}"> 5716 <div style="display:flex; align-items:center; justify-content:center; gap:12px; padding:14px 20px; border-top:1px solid #e8eef5;"> 5717 <button onClick="{{ sellerClosedPrev }}" disabled="{{ sellerClosedPrevDisabled }}" style="border:1px solid #d8e3ee; background:#fff; color:#003d73; border-radius:9px; padding:8px 12px; font:inherit; font-size:12px; font-weight:800; cursor:pointer;">Anterior</button> 5718 <span style="font-size:12px; color:#65758a; font-weight:700;">{{ sellerClosedPageLabel }}</span> 5719 <button onClick="{{ sellerClosedNext }}" disabled="{{ sellerClosedNextDisabled }}" style="border:1px solid #d8e3ee; background:#fff; color:#003d73; border-radius:9px; padding:8px 12px; font:inherit; font-size:12px; font-weight:800; cursor:pointer;">Siguiente</button> 5720 </div> 5721 </sc-if> 5722 </details> 5723 </sc-if> 5724 </div> 5725 </sc-if> 5726 5727 <!-- MAIN ANALYTICS GRID --> 5728 <div id="profile-analytics-grid" style="display:grid; grid-template-columns:{{ dashCols }}; gap:16px; margin-bottom:16px;"> 5729 <!-- activity area chart --> 5730 <div data-reveal style="background:#fff; border-radius:18px; padding:20px 22px; box-shadow:0 8px 26px rgba(0,30,60,0.07);"> 5731 <div style="display:flex; justify-content:space-between; align-items:baseline; flex-wrap:wrap; gap:8px;"> 5732 <div><div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700;">{{ activityEyebrow }}</div><h3 style="font-size:19px; font-weight:800; margin:3px 0 0; color:#112033;">{{ activityTitle }}</h3></div> 5733 <div style="text-align:right;"><div style="font-size:22px; font-weight:900; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ activityTotal }}</div><div style="font-size:11px; color:#65758a;">{{ activityUnit }}</div></div> 5734 </div> 5735 <svg viewBox="0 0 560 200" preserveAspectRatio="none" style="width:100%; height:190px; margin-top:14px; display:block; overflow:visible;"> 5736 <defs><linearGradient id="areaFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#0a5ca8" stop-opacity="0.28"></stop><stop offset="100%" stop-color="#0a5ca8" stop-opacity="0"></stop></linearGradient></defs> 5737 <sc-for list="{{ gridLines }}" as="g" hint-placeholder-count="4"><line x1="0" y1="0" data-dc-svg-y1="{{ g }}" x2="560" y2="0" data-dc-svg-y2="{{ g }}" stroke="#eef3f9" stroke-width="1"></line></sc-for> 5738 <path d="M0 0" data-dc-svg-d="{{ areaD }}" fill="url(#areaFill)" style="opacity:0; animation:fadeScale 1s ease forwards 0.6s;"></path> 5739 <path d="M0 0" data-dc-svg-d="{{ lineD }}" fill="none" stroke="#0a5ca8" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" pathLength="1" style="stroke-dasharray:1; stroke-dashoffset:1; animation:drawLine 1.6s cubic-bezier(0.22,0.72,0.18,1) forwards 0.3s;"></path> 5740 <path d="M0 0" data-dc-svg-d="{{ lineD2 }}" fill="none" stroke="#1f7a63" stroke-width="2" stroke-dasharray="4 4" stroke-linecap="round" pathLength="1" style="stroke-dasharray:1; stroke-dashoffset:1; animation:drawLine 1.6s cubic-bezier(0.22,0.72,0.18,1) forwards 0.5s;"></path> 5741 </svg> 5742 <div style="display:flex; gap:18px; margin-top:8px; font-size:12px;">
5743 <span style="display:inline-flex; align-items:center; gap:6px; color:#3d4c5f;"><span style="width:14px; height:3px; border-radius:2px; background:#0a5ca8;"></span>{{ activityPrimaryLegend }}</span> 5744 <span style="display:inline-flex; align-items:center; gap:6px; color:#3d4c5f;"><span style="width:14px; height:3px; border-radius:2px; background:#1f7a63;"></span>{{ activityMarketLegend }}</span> 5745 </div> 5746 </div> 5747 <!-- donut distribution --> 5748 <div data-reveal style="background:#fff; border-radius:18px; padding:20px 22px; box-shadow:0 8px 26px rgba(0,30,60,0.07); display:flex; flex-direction:column;"> 5749 <div style="font-size:11.5px; letter-spacing:0.12em; color:#8593a6; font-weight:700;">{{ distributionEyebrow }}</div> 5750 <h3 style="font-size:19px; font-weight:800; margin:3px 0 14px; color:#112033;">{{ distributionTitle }}</h3> 5751 <div style="display:flex; align-items:center; justify-content:center; margin:6px 0 14px;"> 5752 <div style="position:relative; width:150px; height:150px; border-radius:50%; background:{{ donutGradient }}; animation:spinIn 1s cubic-bezier(0.22,0.72,0.18,1) both 0.3s;"> 5753 <div style="position:absolute; inset:22px; border-radius:50%; background:#fff; display:grid; place-items:center; text-align:center;"><div><div style="font-size:24px; font-weight:900; color:#112033; font-variant-numeric:tabular-nums;">{{ donutTotal }}</div><div style="font-size:10.5px; color:#65758a;">{{ distributionUnit }}</div></div></div> 5754 </div> 5755 </div> 5756 <div style="display:flex; flex-direction:column; gap:8px;"> 5757 <sc-for list="{{ donutLegend }}" as="d" hint-placeholder-count="3"> 5758 <div style="display:flex; align-items:center; gap:8px; font-size:13px;"><span style="width:10px; height:10px; border-radius:3px; background:{{ d.color }}; flex-shrink:0;"></span><span style="flex:1; color:#3d4c5f;">{{ d.label }}</span><strong style="color:#112033; font-variant-numeric:tabular-nums;">{{ d.pct }}</strong></div> 5759 </sc-for> 5760 </div> 5761 </div> 5762 </div> 5763 5764 </sc-if> 5765 5766 <sc-if value="{{ profileRosganVisible }}" hint-placeholder-val="{{ false }}"> 5767 <section class="profile-company" style="border-left-color:{{ profileCompanyColor }};"> 5768 <sc-if value="{{ profileCompanyLogo }}" hint-placeholder-val="{{ false }}"><img data-dc-attr-src="{{ profileCompanyLogo }}" alt="{{ profileCompanyName }}"></sc-if> 5769 <div><span>MI CONSIGNATARIA</span><h2>{{ profileCompanyName }}</h2></div> 5770 </section> 5771 </sc-if> 5772 <sc-if value="{{ profileFeriasVisible }}" hint-placeholder-val="{{ false }}"> 5773 <section class="profile-rosgan-lots profile-ferias"> 5774 <header><div><h2>{{ profileFeriasTitle }}</h2><p>Ferias de hoy y próximos remates.</p></div><button type="button" onClick="{{ refreshProfileFerias }}">Actualizar</button></header> 5775 <sc-if value="{{ profileFeriasLoading }}" hint-placeholder-val="{{ false }}"><p role="status">Cargando remates feriaâ¦</p></sc-if> 5776 <sc-if value="{{ profileFeriasError }}" hint-placeholder-val="{{ false }}"><p role="alert">{{ profileFeriasError }}</p></sc-if> 5777 <sc-if value="{{ profileFeriasEmpty }}" hint-placeholder-val="{{ false }}"><p>No hay ferias de hoy ni próximas para mostrar.</p></sc-if> 5778 <div class="profile-feria-grid"><sc-for list="{{ profileFeriaRows }}" as="f" hint-placeholder-count="0"><article class="profile-feria-card"> 5779 <div class="profile-feria-brand"><sc-if value="{{ f.hasLogo }}"><img data-dc-attr-src="{{ f.logo }}" alt="{{ f.company }}"></sc-if><span>{{ f.company }}</span><strong>{{ f.status }}</strong></div> 5780 <h3>{{ f.title }}</h3><p>{{ f.date }}</p><p>{{ f.location }}</p> 5781 <div class="profile-feria-actions"> 5782 <div class="profile-feria-share-actions"> 5783 <sc-if value="{{ f.hasLink }}"><a class="profile-feria-action is-primary" href="{{ f.href }}" target="_blank" rel="noopener noreferrer"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true" width="16" height="16"><span>Ver transmisión</span></a></sc-if> 5784 <button type="button" class="profile-feria-action" onClick="{{ f.downloadFlyer }}"><img src="/assets/icons/lucide/download.svg" alt="" aria-hidden="true" width="16" height="16"><span>Descargar flyer</span></button> 5785 <sc-if value="{{ f.hasLink }}"><button type="button" class="profile-feria-action" onClick="{{ f.copyTransmission }}" title="Copiar URL de transmisión"><img src="/assets/icons/lucide/copy.svg" alt="" aria-hidden="true" width="16" height="16"><span>Copiar enlace</span></button></sc-if> 5786 <sc-if value="{{ !f.hasLink }}"><button type="button" class="profile-feria-action" onClick="{{ f.reviewTransmission }}"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true" width="16" height="16"><span>Revisar transmisión</span></button></sc-if> 5787 </div> 5788 <div class="profile-feria-manage-actions"> 5789 <sc-if value="{{ f.canControlYoutube }}"><button type="button" class="profile-feria-action is-management" onClick="{{ f.controlYoutube }}">Controlar transmisión</button></sc-if> 5790 <button type="button" class="profile-feria-action is-management" onClick="{{ f.editFeria }}"><img src="/assets/icons/lucide/pencil.svg" alt="" aria-hidden="true" width="16" height="16"><span>Editar</span></button> 5791 <button type="button" class="profile-feria-action is-danger" onClick="{{ f.deleteFeria }}" disabled="{{ feriaDeleting }}">
5791<img src="/assets/icons/lucide/trash-2.svg" alt="" aria-hidden="true" width="16" height="16"><span>Eliminar</span></button> 5792 </div> 5793 </div> 5794 <sc-if value="{{ !f.hasLink }}"><small>{{ f.youtubeMessage }}</small></sc-if> 5795 </article></sc-for></div> 5796 </section> 5797 </sc-if> 5798 <!-- TABS: lotes / ofertas --> 5799 <sc-if value="{{ showProfileTabs }}" hint-placeholder-val="{{ true }}"> 5800 <div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:16px;"> 5801 <sc-for list="{{ profileTabs }}" as="t" hint-placeholder-count="2"> 5802 <span onClick="{{ t.go }}" style="padding:10px 20px; border-radius:100px; font-size:14px; font-weight:700; cursor:pointer; border:1px solid {{ t.border }}; background:{{ t.bg }}; color:{{ t.color }}; transition:transform 160ms;" style-hover="transform:translateY(-2px);">{{ t.label }}<strong style="font-variant-numeric:tabular-nums; margin-left:6px;">{{ t.count }}</strong></span> 5803 </sc-for> 5804 </div> 5805 </sc-if> 5806 <sc-if value="{{ profileErrorMsg }}" hint-placeholder-val="{{ false }}"> 5807 <div style="background:rgba(230,57,70,0.1); border:1px solid rgba(230,57,70,0.25); border-radius:14px; padding:12px 14px; color:#9f2330; font-size:13px; font-weight:700; margin-bottom:16px; line-height:1.35;">{{ profileErrorMsg }}</div> 5808 </sc-if> 5809 <sc-if value="{{ profileEmptyDebug }}" hint-placeholder-val="{{ false }}"> 5810 <div style="background:#fff; border:1px solid #d7e2ef; border-radius:14px; padding:12px 14px; color:#516176; font-size:12px; font-weight:700; margin-bottom:16px; line-height:1.45;"> 5811 {{ profileEmptyDebug }} 5812 </div> 5813 </sc-if> 5814 5815 <sc-if value="{{ profileShowRosganLots }}" hint-placeholder-val="{{ false }}"> 5816 <section class="profile-rosgan-lots"> 5817 <header><div><h2>Lotes · Próximos remates</h2><p>Tus cargas primero, luego las del equipo.</p></div><button onClick="{{ refreshProfileRosgan }}">Actualizar</button></header> 5818 <sc-if value="{{ profileRosganLoading }}" hint-placeholder-val="{{ false }}"><p role="status">Cargando lotes de tus consignatariasâ¦</p></sc-if> 5819 <sc-if value="{{ profileRosganError }}" hint-placeholder-val="{{ false }}"><p role="alert">{{ profileRosganError }}</p></sc-if> 5820 <sc-if value="{{ profileRosganEmpty }}" hint-placeholder-val="{{ false }}"><p>{{ profileRosganEmptyText }}</p></sc-if> 5821 <div class="profile-rosgan-list"> 5822 <sc-for list="{{ profileRosganRows }}" as="l" hint-placeholder-count="0"> 5823 <article class="profile-rosgan-row"> 5824 <div><span class="profile-lot-cert">CERT. {{ l.cert }}</span><h3>{{ l.category }}</h3><span>{{ l.location }}</span></div> 5825 <div><strong>{{ l.heads }}</strong><span>cabezas</span></div> 5826 <div><strong>{{ l.remate }}</strong><span class="profile-lot-status">{{ l.status }}</span><sc-if value="{{ l.own }}" hint-placeholder-val="{{ false }}"><span class="profile-lot-own">Cargado por vos</span></sc-if></div> 5827 <div class="profile-lot-actions"><a data-dc-attr-href="{{ l.href }}">Ver lote â</a><sc-if value="{{ l.canEdit }}" hint-placeholder-val="{{ false }}"><button onClick="{{ l.edit }}">Editar</button></sc-if></div> 5828 </article> 5829 </sc-for> 5830 </div> 5831 </section> 5832 </sc-if> 5833 <sc-if value="{{ profileShowLots }}" hint-placeholder-val="{{ true }}"> 5834 <sc-if value="{{ profileLotsLoading }}" hint-placeholder-val="{{ false }}"> 5835 <div style="background:#fff; border:1px solid #e3eaf2; border-radius:18px; padding:42px 24px; text-align:center; color:#65758a; font-size:13.5px;"> 5836 <span style="display:inline-block; width:22px; height:22px; border:3px solid #dce6f0; border-top-color:#0a5ca8; border-radius:50%; animation:spin 800ms linear infinite; vertical-align:middle; margin-right:9px;"></span> 5837 Cargando tus lotes... 5838 </div> 5839 </sc-if> 5840 <sc-if value="{{ hasMyLots }}" hint-placeholder-val="{{ true }}"> 5841 <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(260px, 1fr)); gap:16px;"> 5842 <sc-for list="{{ myLotCards }}" as="l" hint-placeholder-count="4"> 5843 <div onClick="{{ l.open }}" data-reveal style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.08); cursor:pointer; transition:transform 240ms, box-shadow 240ms;" style-hover="transform:translateY(-5px); box-shadow:0 24px 56px rgba(0,30,60,0.16);"> 5844 <div style="position:relative; height:130px;"> 5845 <div style="position:absolute; inset:0; background-image:url('{{ l.image }}'); background-size:cover; background-position:center;"></div> 5846 <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,29,61,0.05) 45%, rgba(0,29,61,0.72));"></div>
5847 <span style="position:absolute; top:10px; left:10px; background:{{ l.stateBg }}; color:{{ l.stateColor }}; font-size:11px; font-weight:800; padding:5px 11px; border-radius:100px;">{{ l.state }}</span> 5848 <div style="position:absolute; bottom:8px; left:12px; right:12px; color:#fff;"><div style="font-size:14.5px; font-weight:800; line-height:1.15;">{{ l.categoria }}</div><div style="font-size:11px; color:#c8d8ea;">Cert. {{ l.cert }} · {{ l.origen }}</div></div> 5849 </div> 5850 <div style="padding:12px 14px;"> 5851 <div style="display:flex; justify-content:space-between; align-items:center;"> 5852 <div><div style="font-size:10.5px; color:#8593a6;">{{ l.bidTitle }}</div><div style="font-size:16px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ l.bid }}</div></div> 5853 <div style="text-align:right;"><div style="font-size:10.5px; color:#8593a6;">CABEZAS</div><div style="font-size:16px; font-weight:800; color:#112033;">{{ l.cabezas }}</div></div> 5854 </div> 5855 <sc-if value="{{ l.notExpired }}" hint-placeholder-val="{{ true }}"> 5856 <div style="margin-top:10px; height:6px; background:#eef3f9; border-radius:100px; overflow:hidden;"><div style="height:100%; width:{{ l.progress }}; border-radius:100px; background:linear-gradient(90deg,#0a5ca8,#1f7a63); transform:scaleX(0); transform-origin:left; animation:growBar 0.9s cubic-bezier(0.22,0.72,0.18,1) forwards 0.2s;"></div></div> 5857 <div style="font-size:11px; color:#65758a; margin-top:5px;">{{ l.progressLabel }}</div> 5858 <sc-if value="{{ l.canEdit }}" hint-placeholder-val="{{ false }}"> 5859 <button onClick="{{ l.edit }}" style="margin-top:10px; width:100%; display:inline-flex; align-items:center; justify-content:center; gap:8px; background:#fff; color:#002e5f; border:1px solid #cbd9e8; border-radius:10px; padding:10px; font-size:13px; font-weight:800; font-family:inherit; cursor:pointer; transition:all 160ms;" style-hover="background:#eef5fb; border-color:#0a5ca8; transform:translateY(-1px);" style-active="transform:scale(0.98);"><img src="assets/icons/lucide/pencil.svg" alt="" style="width:15px; height:15px;">Editar lote</button> 5860 </sc-if> 5861 </sc-if> 5862 <sc-if value="{{ l.expired }}" hint-placeholder-val="{{ false }}"> 5863 <div style="font-size:11.5px; color:#8593a6; margin-top:9px;">La subasta caducó. Podés programar una nueva fecha.</div> 5864 <sc-if value="{{ l.canRepublish }}" hint-placeholder-val="{{ false }}"> 5865 <button onClick="{{ l.republish }}" style="margin-top:10px; width:100%; display:inline-flex; align-items:center; justify-content:center; gap:8px; background:#002e5f; color:#fff; border:none; border-radius:10px; padding:11px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="transform:translateY(-2px);" style-active="transform:scale(0.97);"><i class="dc-lucide dc-lucide-rotate" aria-hidden="true"></i> Republicar</button> 5866 </sc-if> 5867 </sc-if> 5868 </div> 5869 </div> 5870 </sc-for> 5871 </div> 5872 </sc-if> 5873 <sc-if value="{{ myLotsEmpty }}" hint-placeholder-val="{{ false }}"> 5874 <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;"> 5875 <div style="font-size:16px; font-weight:700; color:#112033;">TodavÃa no cargaste lotes</div> 5876 <div style="font-size:13.5px; margin-top:4px;">Publicá tu primer lote y aparecerá acá con su rendimiento.</div> 5877 <button onClick="{{ goLoad }}" style="margin-top:16px; background:#e63946; color:#fff; border:none; border-radius:10px; padding:11px 22px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Cargar lote</button> 5878 </div> 5879 </sc-if> 5880 </sc-if> 5881 5882 <sc-if value="{{ profileShowOffers }}" hint-placeholder-val="{{ false }}"> 5883 <sc-if value="{{ myBidsLoading }}" hint-placeholder-val="{{ false }}"> 5884 <div style="background:#fff; border:1px solid #eef3f9; border-radius:18px; padding:40px; text-align:center; color:#65758a; font-size:13.5px;">Cargando tus ofertasâ¦</div> 5885 </sc-if> 5886 <sc-if value="{{ hasOffers }}" hint-placeholder-val="{{ false }}"> 5887 <div style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.07); overflow:hidden;"> 5888 <sc-for list="{{ offerRows }}" as="o" hint-placeholder-count="4"> 5889 <div onClick="{{ o.open }}" data-reveal style="display:flex; align-items:center; gap:14px; padding:14px 18px; border-bottom:1px solid #eef3f9; cursor:pointer; transition:background 160ms;" style-hover="background:#f6f9fc;"> 5890 <div style="width:56px; height:56px; border-radius:12px; background-image:url('{{ o.image }}'); background-size:cover; background-position:center; flex-shrink:0;"></div> 5891 <div style="flex:1; min-width:0;"><div style="font-size:15px; font-weight:700; color:#112033;">{{ o.categoria }}</div><div style="font-size:12.5px; color:#65758a;">Cert. {{ o.cert }} · {{ o.origen }}</div></div> 5892 <div style="text-align:right;"><div style="font-size:10.5px; color:#8593a6;">MI OFERTA</div><div style="font-size:16px; font-weight:800; color:#0a5ca8; font-variant-numeric:tabular-nums;">{{ o.amount }}</div></div> 5893 <sc-if value="{{ o.standing }}" hint-placeholder-val="{{ false }}"><span style="font-size:11px; font-weight:800; padding:5px 11px; border-radius:100px; background:{{ o.standBg }}; color:{{ o.standColor }}; animation:badgePop 340ms cubic-bezier(0.34,1.56,0.64,1) both;">{{ o.standLabel }}</span></sc-if>
5894 <span style="font-size:11px; font-weight:800; padding:5px 11px; border-radius:100px; background:{{ o.statusBg }}; color:{{ o.statusColor }};">{{ o.status }}</span> 5895 </div> 5896 </sc-for> 5897 </div> 5898 </sc-if> 5899 <sc-if value="{{ offersEmpty }}" hint-placeholder-val="{{ false }}"> 5900 <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;"> 5901 <div style="font-size:16px; font-weight:700; color:#112033;">TodavÃa no hiciste ofertas</div> 5902 <div style="font-size:13.5px; margin-top:4px;">Explorá el pool de lotes y ofertá por el que te interese.</div> 5903 <button onClick="{{ goPool }}" style="margin-top:16px; background:#e63946; color:#fff; border:none; border-radius:10px; padding:11px 22px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;">Ver pool de lotes</button> 5904 </div> 5905 </sc-if> 5906 </sc-if> 5907 5908 <sc-if value="{{ profileShowPending }}" hint-placeholder-val="{{ false }}"> 5909 <sc-if value="{{ pendingLoading }}" hint-placeholder-val="{{ false }}"> 5910 <div style="background:#fff; border:1px solid #eef3f9; border-radius:18px; padding:40px; text-align:center; color:#65758a; font-size:13.5px;">Cargando ofertas por aprobarâ¦</div> 5911 </sc-if> 5912 <sc-if value="{{ hasPending }}" hint-placeholder-val="{{ false }}"> 5913 <div style="display:flex; flex-direction:column; gap:12px;"> 5914 <sc-for list="{{ pendingRows }}" as="p" hint-placeholder-count="3"> 5915 <div data-reveal style="background:#fff; border-radius:16px; box-shadow:0 8px 26px rgba(0,30,60,0.07); padding:14px 18px; display:flex; align-items:center; gap:14px; flex-wrap:wrap;"> 5916 <div onClick="{{ p.open }}" style="width:56px; height:56px; border-radius:12px; background-image:url('{{ p.image }}'); background-size:cover; background-position:center; flex-shrink:0; cursor:pointer;"></div> 5917 <div onClick="{{ p.open }}" style="flex:1; min-width:140px; cursor:pointer;"><div style="font-size:15px; font-weight:700; color:#112033;">{{ p.categoria }}</div><div style="font-size:12.5px; color:#65758a;">Cert. {{ p.cert }} · {{ p.bidder }}</div></div> 5918 <div style="text-align:right;"><div style="font-size:10.5px; color:#8593a6;">OFERTA</div><div style="font-size:17px; font-weight:800; color:#a8730a; font-variant-numeric:tabular-nums;">{{ p.amount }}</div></div> 5919 <div style="display:flex; gap:8px;"> 5920 <button onClick="{{ p.reject }}" disabled="{{ p.disabled }}" style="background:#fff; color:#c22; border:1px solid #f2c4c8; border-radius:10px; padding:10px 16px; font-size:13px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="background:#fdeef0;">Rechazar</button> 5921 <button onClick="{{ p.approve }}" disabled="{{ p.disabled }}" style="background:#1f7a63; color:#fff; border:none; border-radius:10px; padding:10px 18px; font-size:13px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="background:#186552;">{{ p.approveLabel }}</button> 5922 </div> 5923 </div> 5924 </sc-for> 5925 </div> 5926 </sc-if> 5927 <sc-if value="{{ pendingEmpty }}" hint-placeholder-val="{{ false }}"> 5928 <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;"> 5929 <div style="font-size:16px; font-weight:700; color:#112033;">No hay ofertas pendientes</div> 5930 <div style="font-size:13.5px; margin-top:4px;">Las ofertas de tus compradores aparecerán acá para aprobar o rechazar.</div> 5931 </div> 5932 </sc-if> 5933 </sc-if> 5934 5935 <sc-if value="{{ profileShowComitentes }}" hint-placeholder-val="{{ false }}"> 5936 <div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:14px;">
5937 <span style="font-size:13.5px; color:#65758a;">Comitentes de tus consignatarias.</span> 5938 <sc-if value="{{ profileCanEditRelations }}" hint-placeholder-val="false"><button type="button" onClick="{{ openProfileComitenteMerge }}" style="background:#eef5fc;color:#073d70;border:1px solid #cadbea;border-radius:10px;padding:10px 18px;font:inherit;cursor:pointer;">Fusionar comitentes</button><button onClick="{{ toggleComitenteForm }}" style="background:#e63946; color:#fff; border:none; border-radius:10px; padding:10px 18px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Nuevo comitente</button></sc-if> 5939 </div> 5940 <sc-if value="{{ comitenteFormOpen }}" hint-placeholder-val="{{ false }}"> 5941 <div style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.08); padding:20px 22px; margin-bottom:16px;"> 5942 <div style="font-size:15px; font-weight:800; color:#112033; margin-bottom:14px;">Alta de comitente</div> 5943 <div class="establishment-form-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:14px;"> 5944 <label style="display:flex; flex-direction:column; gap:6px;"><span style="display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12.5px; font-weight:600; color:#3d4c5f;"><span>CUIT (opcional)</span><small style="color:#0a5ca8;">{{ cfLookupStatus }}</small></span><input aria-describedby="confidential-cf_cuitCuil" value="" data-dc-attr-value="{{ cf_cuitCuil }}" onInput="{{ set_cf_cuitCuil }}" inputmode="numeric" maxlength="11" placeholder="11 dÃgitos" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;"><small id="confidential-cf_cuitCuil" style="display:inline-flex;align-items:flex-start;gap:7px;box-sizing:border-box;max-width:100%;width:fit-content;margin-top:6px;padding:7px 10px;border:1px solid #f0cf86;border-radius:9px;background:#fff7e6;color:#805411;font-size:11px;font-weight:600;line-height:1.45;letter-spacing:normal;text-transform:none;white-space:normal;text-align:left;"><span aria-hidden="true" style="flex:none;font-size:15px;line-height:1.1;color:#a56b08;">⚠</span><span>Dato confidencial perteneciente a la firma consignataria</span></small></label> 5945 <label style="display:grid;gap:6px;color:#60758c;font-size:12px;font-weight:700;">Alias (opcional)<input type="text" value="" data-dc-attr-value="{{ cf_alias }}" onInput="{{ set_cf_alias }}" placeholder="Ej.: Estancia El Ombú" style="box-sizing:border-box;min-width:0;width:100%;height:43px;border:1px solid #ccdae8;border-radius:11px;padding:0 12px;font:inherit;background:#fff;"></label> 5946<label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Razón social</span><input value="" data-dc-attr-value="{{ cf_nombreFiscal }}" readonly="true" placeholder="Se completa desde ARCA" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#eef3f7;"></label> 5947 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Provincia fiscal</span><input value="" data-dc-attr-value="{{ cf_provincia }}" readonly="true" placeholder="Se completa desde ARCA" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#eef3f7;"></label> 5948 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Localidad fiscal</span><input value="" data-dc-attr-value="{{ cf_localidad }}" readonly="true" placeholder="Se completa desde ARCA" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#eef3f7;"></label> 5949 </div> 5950 <sc-if value="{{ comitenteFormMsg }}" hint-placeholder-val="{{ false }}"><div style="margin-top:12px; font-size:12.5px; padding:9px 12px; border-radius:10px; background:{{ comitenteFormMsgBg }}; color:{{ comitenteFormMsgColor }};">{{ comitenteFormMsg }}</div></sc-if> 5951 <div style="display:flex; gap:10px; margin-top:16px;">
5951<button onClick="{{ comitenteSave }}" disabled="{{ comitenteSaveDisabled }}" style="background:#1f7a63; color:#fff; border:none; border-radius:10px; padding:11px 20px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;">{{ comitenteSaveLabel }}</button><button onClick="{{ toggleComitenteForm }}" style="background:#eef3f9; color:#3d4c5f; border:1px solid #dde7f2; border-radius:10px; padding:11px 20px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;">Cancelar</button></div> 5952 </div> 5953 </sc-if> 5954 <sc-if value="{{ comitentesLoading }}" hint-placeholder-val="{{ false }}"><div style="background:#fff; border:1px solid #eef3f9; border-radius:18px; padding:40px; text-align:center; color:#65758a; font-size:13.5px;">Cargando comitentesâ¦</div></sc-if> 5955 <div class="pc-directory-tools"> 5956 <label class="pc-search"><span>Buscar comitente</span><input type="search" value="" data-dc-attr-value="{{ pcQuery }}" onInput="{{ pcSearch }}" placeholder="Alias, nombre fiscal o CUIT"></label> 5957 <label><span>Provincia</span><select value="" aria-label="Filtrar por provincia" data-dc-attr-value="{{ pcProvince }}" onChange="{{ pcSetProvince }}"><option value="">Todas las provincias</option><sc-for list="{{ pcProvinces }}" as="p"><option value="" data-dc-attr-value="{{ p.value }}" label="{{ p.value }}">{{ p.value }}</option></sc-for></select></label> 5958 <label><span>Localidad</span><select value="" aria-label="Filtrar por localidad" data-dc-attr-value="{{ pcLocality }}" onChange="{{ pcSetLocality }}"><option value="">Todas las localidades</option><sc-for list="{{ pcLocalities }}" as="l"><option value="" data-dc-attr-value="{{ l.value }}" label="{{ l.value }}">{{ l.value }}</option></sc-for></select></label> 5959 </div> 5960 <div class="pc-directory-summary"><span role="status">{{ pcCount }}</span><sc-if value="{{ pcHasFilters }}"><button type="button" onClick="{{ pcClear }}">Limpiar filtros</button></sc-if></div> 5961 <sc-if value="{{ comitentesHas }}"><ul class="pc-directory-list" aria-label="Comitentes"><sc-for list="{{ comitenteRows }}" as="c"><li class="pc-directory-row"><div class="pc-directory-name"><strong>{{ c.razonSocial }}</strong><span>{{ c.fiscalName }}</span></div><div class="pc-directory-cuit"><small>CUIT</small><span>{{ c.cuitCuil }}</span></div><div class="pc-directory-location"><small>Ubicación</small><span>{{ c.meta }}</span></div><sc-if value="{{ profileCanEditRelations }}"><button type="button" onClick="{{ c.editAlias }}" aria-label="Editar alias de {{ c.razonSocial }}">Editar alias</button></sc-if></li></sc-for></ul></sc-if> 5962 <sc-if value="{{ comitentesEmpty }}"><div class="pc-directory-empty">{{ pcEmptyLabel }}</div></sc-if> 5963 <sc-if value="{{ pcPaginated }}"><nav class="pc-directory-pages" aria-label="Páginas de comitentes"><button type="button" onClick="{{ pcPrev }}" disabled="{{ pcPrevDisabled }}">Anterior</button><span>{{ pcPageLabel }}</span><button type="button" onClick="{{ pcNext }}" disabled="{{ pcNextDisabled }}">Siguiente</button></nav></sc-if> 5964 </sc-if> 5965 5966 <sc-if value="{{ profileShowEst }}" hint-placeholder-val="{{ false }}"> 5967 <div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:14px;"> 5968 <span style="font-size:13.5px; color:#65758a;">Establecimientos guardados para reutilizar al cargar lotes.</span> 5969 <sc-if value="{{ canCreateEstablishment }}" hint-placeholder-val="{{ false }}"><button onClick="{{ toggleEstForm }}" style="background:#e63946; color:#fff; border:none; border-radius:10px; padding:10px 18px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Nuevo establecimiento</button></sc-if> 5970 </div> 5971 <sc-if value="{{ estFormOpen }}" hint-placeholder-val="{{ false }}"> 5972 <div style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.08); padding:20px 22px; margin-bottom:16px;"> 5973 <div style="font-size:15px; font-weight:800; color:#112033; margin-bottom:14px;">Nuevo establecimiento</div> 5974 <div class="establishment-form-grid" style="display:grid; grid-template-columns:1fr 1fr; gap:14px;"> 5975 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Comitente *</span><select value="" data-dc-attr-value="{{ ef_comitenteId }}" onChange="{{ set_ef_comitenteId }}" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;
5975 color:#112033; background:#fff;"><option value="">Seleccionar comitenteâ¦</option><sc-for list="{{ efComitenteOpts }}" as="c" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ c.id }}">{{ c.label }}</option></sc-for></select></label> 5976 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Nombre del establecimiento *</span><input value="" data-dc-attr-value="{{ ef_nombre }}" onInput="{{ set_ef_nombre }}" placeholder="Ej: Estancia El Ceibo" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label> 5977 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Provincia *</span><select value="" data-dc-attr-value="{{ ef_provincia }}" onChange="{{ set_ef_provincia }}" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#fff;"><option value="">{{ efProvPlaceholder }}</option><sc-for list="{{ efProvinceOpts }}" as="p" hint-placeholder-count="6"><option value="" data-dc-attr-value="{{ p }}">{{ p }}</option></sc-for></select></label> 5978 <label style="position:relative;display:flex; flex-direction:column; gap:6px;"><span style="display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12.5px; font-weight:600; color:#3d4c5f;"><span>Localidad *</span><small style="display:inline-flex;align-items:center;gap:4px;color:#718296;"><img src="assets/google-logo.webp" alt="" style="width:12px;height:12px;object-fit:contain;">Google</small></span><input value="" data-dc-attr-value="{{ ef_localidad }}" onInput="{{ set_ef_localidad }}" onFocus="{{ openEfPlaces }}" onBlur="{{ closeEfPlaces }}" disabled="{{ efLocDisabled }}" autocomplete="off" placeholder="Escribà una localidad" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 38px 11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:{{ efLocBg }};"><sc-if value="{{ efPlaceLoading }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true" style="position:absolute;right:13px;top:39px;color:#0a5ca8;"></i></sc-if><sc-if value="{{ efPlaceResultsOpen }}" hint-placeholder-val="{{ false }}"><div style="position:absolute;z-index:50;left:0;right:0;top:calc(100% + 5px);padding:5px;border:1px solid #d6e2ed;border-radius:11px;background:#fff;box-shadow:0 14px 30px rgba(0,35,70,.18);max-height:220px;overflow:auto;"><sc-for list="{{ efPlaceResults }}" as="p" hint-placeholder-count="4"><button type="button" onClick="{{ p.pick }}" style="width:100%;display:grid;gap:2px;border:0;border-radius:8px;background:#fff;padding:8px 9px;text-align:left;cursor:pointer;"><strong style="font-size:11.5px;color:#153955;">{{ p.label }}</strong><span style="font-size:9.5px;color:#718296;">{{ p.address }}</span></button></sc-for></div></sc-if></label> 5979 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">RENSPA <small style="font-weight:500; color:#8593a6;">opcional</small></span><input value="" data-dc-attr-value="{{ ef_renspa }}" onInput="{{ set_ef_renspa }}" placeholder="Podés dejarlo vacÃo" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;"></label> 5980 </div> 5981 <sc-if value="{{ estFormMsg }}" hint-placeholder-val="{{ false }}"> 5982 <div style="margin-top:12px; font-size:12.5px; padding:9px 12px; border-radius:10px; background:{{ estFormMsgBg }}; color:{{ estFormMsgColor }};">{{ estFormMsg }}</div> 5983 </sc-if> 5984 <div style="display:flex; gap:10px; margin-top:16px;"> 5985 <button onClick="{{ saveEst }}" disabled="{{ estSaveDisabled }}" style="background:#1f7a63; color:#fff; border:none; border-radius:10px; padding:11px 20px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:{{ estSaveCursor }}; opacity:{{ estSaveOpacity }};">{{ estSaveLabel }}</button> 5986 <button onClick="{{ toggleEstForm }}" style="background:#eef3f9; color:#3d4c5f; border:1px solid #dde7f2; border-radius:10px; padding:11px 20px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;">Cancelar</button> 5987 </div> 5988 </div> 5989 </sc-if> 5990 <sc-if value="{{ estLoading }}" hint-placeholder-val="{{ false }}"> 5991 <div style="background:#fff; border:1px solid #eef3f9; border-radius:18px; padding:40px; text-align:center; color:#65758a; font-size:13.5px;">Cargando establecimientosâ¦</div> 5992 </sc-if> 5993 <sc-if value="{{ estHas }}" hint-placeholder-val="{{ false }}"> 5994 <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)); gap:16px;"> 5995 <sc-for list="{{ estRowsList }}" as="e" hint-placeholder-count="3"> 5996 <div data-reveal style="background:#fff; border-radius:16px; box-shadow:0 8px 26px rgba(0,30,60,0.07); padding:18px 20px; display:flex; flex-direction:column; gap:6px;"> 5997 <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:10px;"> 5998 <div style="min-width:0;"><div style="font-size:16px; font-weight:800; color:#112033;">{{ e.nombre }}</div><div style="font-size:12.5px; color:#65758a;">{{ e.meta }}</div></div> 5999 <sc-if value="{{ profileCanEditRelations }}" hint-placeholder-val="false"><button onClick="{{ e.remove }}" disabled="{{ e.disabled }}" title="Eliminar" style="flex-shrink:0; background:#fff; color:#c22; border:1px solid #f2c4c8; border-radius:9px; padding:7px 11px; font-size:12px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="background:#fdeef0;">Eliminar</button></sc-if> 6000 </div> 6001 <sc-if value="{{ e.comitente }}" hint-placeholder-val="{{ false }}"><div style="font-size:12.5px; color:#3d4c5f;">Comitente: <strong>{{ e.comitente }}</strong></div></sc-if> 6002 <sc-if value="{{ e.comitenteCuitCuil }}" hint-placeholder-val="{{ false }}"><div style="font-size:11.5px; color:#65758a;">CUIT/CUIL {{ e.comitenteCuitCuil }}</div></sc-if> 6003 <sc-if value="{{ e.renspa }}" hint-placeholder-val="{{ false }}"><div style="font-size:11.5px; color:#65758a;">RENSPA {{ e.renspa }}</div></sc-if> 6004 <sc-if value="{{ e.hasCoords }}" hint-placeholder-val="{{ false }}"><div style="font-size:11.5px; color:#8593a6; font-variant-numeric:tabular-nums;"><i class="dc-lucide dc-lucide-map-pin" aria-hidden="true"></i> {{ e.coords }}</div></sc-if> 6005 </div> 6006 </sc-for> 6007 </div> 6008 </sc-if> 6009 <sc-if value="{{ estEmpty }}" hint-placeholder-val="{{ false }}"> 6010 <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;"> 6011 <div style="font-size:16px; font-weight:700; color:#112033;">TodavÃa no tenés establecimientos</div> 6012 <div style="font-size:13.5px; margin-top:4px;">Cargá uno para reutilizarlo al publicar lotes.</div> 6013 </div> 6014 </sc-if> 6015 </sc-if> 6016 6017 <!-- PANEL: COMPRADORES (autorizaciones) --> 6018 <sc-if value="{{ profileShowBuyers }}" hint-placeholder-val="{{ false }}"> 6019 <section id="seller-buyers-panel" style="background:#f8fafc; border:1px solid #e3eaf2; border-radius:18px; padding:20px; margin-bottom:16px;"> 6020 <div style="display:flex; align-items:end; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-bottom:14px;"> 6021 <div> 6022 <div style="font-size:11px; letter-spacing:0.12em; color:#0a5ca8; font-weight:800; text-transform:uppercase;">Autorizació
6022n y créditos</div> 6023 <h3 style="font-size:20px; color:#112033; margin:3px 0 0;">Cuentas comprador</h3> 6024 </div> 6025 </div> 6026 <label style="display:flex; align-items:center; gap:10px; background:#fff; border:1px solid #dbe5ef; border-radius:12px; padding:10px 13px; margin-bottom:12px;"> 6027 <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#65758a" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-4-4"></path></svg> 6028 <input value="" data-dc-attr-value="{{ buyerDirectorySearch }}" onInput="{{ setBuyerDirectorySearch }}" placeholder="Buscar comprador por nombre o email" style="border:none; outline:none; width:100%; background:transparent; color:#112033; font:inherit; font-size:13.5px;"> 6029 </label> 6030 <sc-if value="{{ buyerDirectoryLoading }}" hint-placeholder-val="{{ false }}"> 6031 <div style="background:#fff; border:1px solid #eef3f9; border-radius:16px; padding:34px; text-align:center; color:#65758a; font-size:13.5px;">Cargando cuentas comprador...</div> 6032 </sc-if> 6033 <sc-if value="{{ buyerDirectoryHas }}" hint-placeholder-val="{{ false }}"> 6034 <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(260px, 1fr)); gap:12px; margin-bottom:26px;"> 6035 <sc-for list="{{ buyerDirectoryRows }}" as="u" hint-placeholder-count="4"> 6036 <div style="background:#fff; border:1px solid #e3eaf2; border-radius:14px; padding:14px 15px; display:flex; align-items:center; gap:11px; min-width:0;"> 6037 <span style="width:40px; height:40px; border-radius:50%; background:#e7f1fb; color:#003d73; display:grid; place-items:center; font-size:13px; font-weight:800; flex-shrink:0;">{{ u.initials }}</span> 6038 <div style="flex:1; min-width:0;"> 6039 <div style="font-size:14px; font-weight:800; color:#112033; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ u.name }}</div> 6040 <div style="font-size:11.5px; color:#65758a; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ u.email }}</div> 6041 <div style="font-size:10.5px; color:#8a98aa; margin-top:3px;">{{ u.lastAccess }}</div> 6042 </div> 6043 <button onClick="{{ u.authorize }}" title="Autorizar comprador" style="background:#eef4fb; color:#003d73; border:1px solid #d5e2ef; border-radius:9px; padding:8px 10px; font-size:11.5px; font-weight:800; font-family:inherit; cursor:pointer; flex-shrink:0;">Autorizar</button> 6044 </div> 6045 </sc-for> 6046 </div> 6047 </sc-if> 6048 <sc-if value="{{ buyerDirectoryEmpty }}" hint-placeholder-val="{{ false }}"> 6049 <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:16px; padding:34px 20px; text-align:center; color:#65758a; margin-bottom:26px;">{{ buyerDirectoryEmptyText }}</div> 6050 </sc-if> 6051 <div style="border-top:1px solid #dfe7f0; padding-top:22px; margin-top:4px;"> 6052 <div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:14px;"> 6053 <div><div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;"><div style="font-size:11px; letter-spacing:0.12em; color:#1f7a63; font-weight:800; text-transform:uppercase;">Créditos semanales vigentes</div><span class="credit-live-status">EN TIEMPO REAL</span></div><span style="font-size:13.5px; color:#65758a; max-width:620px;">Seleccioná un comprador, definà el inicio y el monto. El crédito vence a los 7 dÃas corridos, incluyendo la fecha inicial.</span></div> 6054 <button type="button" onClick="{{ sellerCreditHistory }}" style="min-height:40px; display:inline-flex; align-items:center; gap:7px; border:1px solid #bfd3e7; border-radius:10px; background:#fff; color:#075ca8; padding:8px 13px; font:800 11.5px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-rotate" aria-hidden="true"></i> Historial</button> 6055 </div> 6056 <sc-if value="{{ baFormOpen }}" hint-placeholder-val="{{ false }}"> 6057 <div id="buyer-auth-form" style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.08); padding:20px 22px; margin-bottom:16px; animation:fadeScale 260ms ease both;"> 6058 <div style="font-size:15px; font-weight:800; color:#112033; margin-bottom:14px;">Nuevo crédito semanal</div> 6059 <div style="display:grid; grid-template-columns:1fr 1fr; gap:14px;"> 6060 <div style="background:#f4f8fc; border:1px solid #dde7f2; border-radius:12px; padding:11px 13px;"><div style="font-size:10.5px; color:#8593a6; font-weight:800; text-transform:uppercase;">Comprador seleccionado</div><strong style="display:block; margin-top:4px; font-size:14px; color:#112033;">{{ ba_selectedBuyer }}</strong><span style="display:block; margin-top:2px; font-size:11.5px; color:#65758a;">Autorización por crédito semanal</span></div> 6061 <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;"> 6062 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Inicio del crédito</span><input value="" data-dc-attr-value="{{ ba_creditStartDate }}" onInput="{{ set_ba_creditStartDate }}" type="date" required style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;
6062 color:#112033;" style-focus="border-color:#0a5ca8;"></label> 6063 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Crédito semanal</span><input value="" data-dc-attr-value="{{ ba_weeklyCreditAmount }}" onInput="{{ set_ba_weeklyCreditAmount }}" type="text" inputmode="numeric" required placeholder="Ej. 50.000.000" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;" style-focus="border-color:#0a5ca8;"></label> 6064 </div> 6065 </div> 6066 <sc-if value="{{ baFormMsg }}" hint-placeholder-val="{{ false }}"> 6067 <div style="margin-top:12px; font-size:12.5px; padding:9px 12px; border-radius:10px; background:{{ baFormMsgBg }}; color:{{ baFormMsgColor }};">{{ baFormMsg }}</div> 6068 </sc-if> 6069 <div style="display:flex; gap:10px; margin-top:16px;"> 6070 <button onClick="{{ saveBuyerAuth }}" disabled="{{ baSaving }}" style="background:#1f7a63; color:#fff; border:none; border-radius:10px; padding:11px 20px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="background:#186552;">{{ baSaveLabel }}</button> 6071 <button onClick="{{ toggleBaForm }}" style="background:#eef3f9; color:#3d4c5f; border:1px solid #dde7f2; border-radius:10px; padding:11px 20px; font-size:13.5px; font-weight:700; font-family:inherit; cursor:pointer;">Cancelar</button> 6072 </div> 6073 </div> 6074 </sc-if> 6075 <sc-if value="{{ baLoading }}" hint-placeholder-val="{{ false }}"> 6076 <div style="background:#fff; border:1px solid #eef3f9; border-radius:18px; padding:40px; text-align:center; color:#65758a; font-size:13.5px;">Cargando compradoresâ¦</div> 6077 </sc-if> 6078 <sc-if value="{{ baHas }}" hint-placeholder-val="{{ false }}"> 6079 <div class="ba-admin-grid"> 6080 <sc-for list="{{ baRows }}" as="b" hint-placeholder-count="3"> 6081 <div class="ba-admin-card" data-reveal style="opacity:{{ b.cardOpacity }}; transition:opacity 260ms;"> 6082 <div style="display:flex; align-items:center; gap:12px;"> 6083 <span style="width:42px; height:42px; border-radius:12px; background:{{ b.avatarBg }}; color:#fff; display:grid; place-items:center; font-size:14px; font-weight:800; flex-shrink:0; transition:background 260ms;">{{ b.initials }}</span> 6084 <div style="flex:1; min-width:0;"> 6085 <div style="font-size:14.5px; font-weight:800; color:#112033; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ b.buyer }}</div> 6086 <sc-if value="{{ b.hasBuyerDetail }}" hint-placeholder-val="{{ false }}"><div style="font-size:11px; color:#65758a; margin-top:2px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ b.buyerDetail }}</div></sc-if> 6087 <div style="font-size:11.5px; color:#8593a6;">{{ b.validityLabel }}</div> 6088 </div> 6089 <span style="font-size:10.5px; font-weight:800; padding:4px 10px; border-radius:100px; background:{{ b.statusBg }}; color:{{ b.statusColor }}; white-space:nowrap; transition:all 200ms;">{{ b.statusLabel }}</span> 6090 </div> 6091 <div class="ba-admin-editor"> 6092 <label class="ba-admin-field"><span>Inicio del crédito</span><input value="" data-dc-attr-value="{{ b.creditStartDate }}" onInput="{{ b.setStartDate }}" type="date"></label> 6093 <label class="ba-admin-field"><span>Crédito semanal</span><input value="" data-dc-attr-value="{{ b.weeklyCreditAmount }}" onInput="{{ b.setWeeklyCreditAmount }}" type="text" inputmode="numeric"></label> 6094 <button class="ba-admin-save" onClick="{{ b.save }}" disabled="{{ b.busy }}">{{ b.saveLabel }}</button> 6095 </div> 6096 <sc-if value="{{ b.inlineLoading }}" hint-placeholder-val="{{ false }}"><div style="display:flex; align-items:center; gap:8px; margin-top:12px; padding:12px; border-radius:11px; background:#f7fafc; color:#65758a; font-size:11px; font-weight:700;"><span style="width:15px; height:15px; border:2px solid #d8e4ef; border-top-color:#0a5ca8; border-radius:50%; animation:spin .8s linear infinite;"></span>Cargando uso vigente...</div></sc-if> 6097 <sc-if value="{{ b.inlineHasError }}" hint-placeholder-val="{{ false }}"><div style="margin-top:12px; padding:11px 12px; border:1px solid #f0c7cc; border-radius:11px; background:#fff5f6; color:#9d2837; font-size:11px;">No se pudo cargar el uso vigente.<button type="button" onClick="{{ b.refreshInline }}" style="margin-left:8px; border:0; background:transparent; color:#075ca8; font:800 10px inherit; cursor:pointer;">Reintentar</button></div></sc-if> 6098 <sc-if value="{{ b.inlineReady }}" hint-placeholder-val="{{ false }}"> 6099 <div style="margin-top:12px; padding:12px; border:1px solid #dfe9f3; border-radius:12px; background:#f8fbfd;"> 6100 <div style="display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:7px;"><div><span style="display:block; color:#718397; font-size:8px; font-weight:900;">AUTORIZADO</span><strong style="display:block; margin-top:3px; color:#12314f; font-size:12px;">{{ b.inlineAuthorized }}</strong></div><div><span style="display:block; color:#718397; font-size:8px; font-weight:900;">USADO</span><strong style="display:block; margin-top:3px; color:#9b6513; font-size:12px;">{{ b.inlineUsed }}</strong></div><div><span style="display:block; color:#718397; font-size:8px; font-weight:900;">DISPONIBLE</span><strong style="display:block; margin-top:3px; color:#1f7a63; font-size:12px;">{{ b.inlineAvailable }}</strong></div></div> 6101 <div style="display:flex; justify-content:space-between; gap:8px; margin:10px 0 5px; color:#65758a; font-size:9px; font-weight:850;"><span>USO VIGENTE</span><span>{{ b.inlineProgressLabel }}</span></div><div style="height:7px; border-radius:999px; background:#dce7f1; overflow:hidden;"><div style="width:{{ b.inlineProgress }}; height:100%; border-radius:inherit; background:linear-gradient(90deg,#0a5ca8,#1f9b7a); transition:width 500ms ease;"></div></div> 6102 <div style="margin-top:11px; color:#0a5ca8; font-size:8.5px; letter-spacing:.08em; font-weight:900;">MOVIMIENTOS VIGENTES</div> 6103 <sc-if value="{{ b.inlineHasMovements }}" hint-placeholder-val="{{ false }}"><div style="display:flex; flex-direction:column; gap:6px; margin-top:7px;"><sc-for list="{{ b.inlineMovements }}" as="movement" hint-placeholder-count="2"><article style="display:flex; align-items:center; justify-content:space-between; gap:7px; flex-wrap:wrap; padding:8px 9px; border:1px solid #e2eaf2; border-radius:9px; background:#fff;"><div style="min-width:105px; flex:1;"><strong style="display:block; color:#163651; font-size:10.5px;">{{ movement.lot }}</strong><span style="display:block; margin-top:2px; color:#7a8c9e; font-size:8.5px;">{{ movement.date }}</span></div><span style="padding:3px 6px; border-radius:999px; background:{{ movement.statusBg }}; color:{{ movement.statusColor }}; font-size:8px; font-weight:900;">{{ movement.statusLabel }}</span><strong style="color:#075ca8; font-size:10.5px;">{{ movement.consumedAmount }}</strong></article></sc-for></div></sc-if> 6104 <sc-if value="{{ b.inlineEmpty }}" hint-placeholder-val="{{ false }}"><div style="margin-top:7px; color:#718397; font-size:10px;">TodavÃa no hay movimientos en este crédito.</div></sc-if> 6105 </div> 6106 </sc-if> 6107 <div style="display:flex; align-items:center; justify-content:space-between; gap:10px; border-top:1px solid #f0f4f9; padding-top:12px;"> 6108 <div onClick="{{ b.toggle }}" style="display:flex; align-items:center; gap:9px; cursor:pointer; user-select:none;">
6109 <span style="position:relative; width:40px; height:22px; border-radius:100px; background:{{ b.switchBg }}; transition:background 240ms; flex-shrink:0;"> 6110 <span style="position:absolute; top:2px; left:0; transform:translateX({{ b.knobX }}); width:18px; height:18px; border-radius:50%; background:#fff; box-shadow:0 2px 5px rgba(0,0,0,0.25); transition:transform 240ms cubic-bezier(0.34,1.56,0.64,1);"></span> 6111 </span> 6112 <span style="font-size:12.5px; font-weight:700; color:{{ b.statusColor }};">{{ b.statusLabel }}</span> 6113 </div> 6114 <div style="display:flex; align-items:center; gap:7px;"><button type="button" onClick="{{ b.openHistory }}" disabled="{{ b.summaryDisabled }}" style="background:#eef5fb; color:#075ca8; border:1px solid #cadeee; border-radius:9px; padding:8px 11px; font-size:11.5px; font-weight:800; font-family:inherit; cursor:pointer; opacity:{{ b.summaryOpacity }};">Historial anterior</button><button onClick="{{ b.remove }}" disabled="{{ b.busy }}" title="Eliminar autorización" style="background:#fff; color:#c22; border:1px solid #f2c4c8; border-radius:9px; padding:8px 13px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:pointer;" style-hover="background:#fdeef0;">Eliminar</button></div> 6115 </div> 6116 </div> 6117 </sc-for> 6118 </div> 6119 </sc-if> 6120 <sc-if value="{{ baEmpty }}" hint-placeholder-val="{{ false }}"> 6121 <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;"> 6122 <div style="font-size:16px; font-weight:700; color:#112033;">No hay créditos vigentes</div> 6123 <div style="font-size:13.5px; margin-top:4px;">Podés crear uno nuevo o consultar los anteriores desde Historial.</div> 6124 </div> 6125 </sc-if> 6126 </div> 6127 </section> 6128 </sc-if> 6129 6130 <sc-if value="{{ launchAdminVisible }}" hint-placeholder-val="{{ false }}"> 6131 <section style="margin-top:18px;background:#fff;border:1px solid #dfe8f1;border-radius:18px;box-shadow:0 10px 30px rgba(0,30,60,.06);overflow:hidden;"> 6132 <div style="display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;padding:16px 18px;border-bottom:1px solid #e7eef5;background:#f8fbfe;"> 6133 <div><div style="font-size:10px;letter-spacing:.12em;color:#0a5ca8;font-weight:900;">LANZAMIENTO ROSGAN</div><h3 style="margin:3px 0 0;color:#112033;font-size:17px;">Usuarios que quieren recibir el aviso</h3><span style="display:block;margin-top:3px;color:#65758a;font-size:11.5px;">{{ launchAdminCountLabel }}</span></div> 6134 <button type="button" onClick="{{ exportLaunchInterests }}" disabled="{{ launchAdminExportDisabled }}" style="height:38px;padding:0 14px;border:1px solid #bfd2e3;border-radius:9px;background:#fff;color:#075ca8;font:800 11.5px inherit;cursor:pointer;opacity:{{ launchAdminExportOpacity }};">Exportar CSV</button> 6135 </div> 6136 <div style="padding:14px 18px 18px;"> 6137 <sc-if value="{{ launchAdminLoading }}" hint-placeholder-val="{{ false }}"><div style="padding:24px;text-align:center;color:#65758a;font-size:12.5px;">Cargando interesadosâ¦</div></sc-if> 6138 <sc-if value="{{ launchAdminHasError }}" hint-placeholder-val="{{ false }}"><div style="padding:11px 13px;border-radius:9px;background:#fff0f1;color:#b42335;font-size:12px;">{{ launchAdminError }}</div></sc-if> 6139 <sc-if value="{{ launchAdminHasRows }}" hint-placeholder-val="{{ false }}"> 6140 <div class="launch-admin-list"><sc-for list="{{ launchAdminRows }}" as="interest" hint-placeholder-count="4"> 6141 <div class="launch-admin-row"><div><strong style="display:block;">{{ interest.name }}</strong><span style="display:block;margin-top:2px;font-size:10px;">{{ interest.date }}</span></div><span class="launch-admin-email">{{ interest.email }}</span><span class="launch-admin-phone">{{ interest.phone }}</span><div class="launch-admin-channels"><sc-if value="{{ interest.emailOptIn }}" hint-placeholder-val="{{ true }}"><small class="launch-admin-channel">EMAIL</small></sc-if><sc-if value="{{ interest.whatsappOptIn }}" hint-placeholder-val="{{ true }}"><small class="launch-admin-channel">WHATSAPP</small></sc-if></div></div> 6142 </sc-for></div> 6143 </sc-if> 6144 <sc-if value="{{ launchAdminEmpty }}" hint-placeholder-val="{{ false }}"><div style="padding:24px;text-align:center;color:#65758a;font-size:12.5px;">TodavÃa nadie pidió recibir el aviso de lanzamiento.</div></sc-if> 6145 </div> 6146 </section> 6147 </sc-if> 6148
6149 <sc-if value="{{ republishOpen }}" hint-placeholder-val="{{ false }}"> 6150 <div onClick="{{ closeRepublish }}" style="position:fixed; inset:0; z-index:60; background:rgba(3,18,38,0.55); backdrop-filter:blur(3px); display:flex; align-items:center; justify-content:center; padding:20px; animation:fadeScale 220ms ease both;"> 6151 <div onClick="{{ stopProp }}" style="background:#fff; border-radius:20px; box-shadow:0 30px 80px rgba(0,20,50,0.35); width:100%; max-width:460px; overflow:hidden; animation:badgePop 300ms cubic-bezier(0.34,1.56,0.64,1) both;"> 6152 <div style="padding:20px 22px; border-bottom:1px solid #f0f4f9; display:flex; align-items:flex-start; justify-content:space-between; gap:12px;"> 6153 <div><div style="font-size:11px; letter-spacing:0.12em; color:#e63946; font-weight:700;">REPUBLICAR LOTE</div><div style="font-size:16px; font-weight:800; color:#112033; margin-top:3px;">{{ republishTitle }}</div></div> 6154 <button onClick="{{ closeRepublish }}" title="Cerrar" style="width:32px; height:32px; border-radius:9px; background:#f6f9fc; border:1px solid #e6edf5; color:#65758a; display:grid; place-items:center; font-size:18px; cursor:pointer; flex-shrink:0;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button> 6155 </div> 6156 <div style="padding:20px 22px; display:flex; flex-direction:column; gap:14px;"> 6157 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Fecha de subasta *</span><input value="" data-dc-attr-value="{{ rp_fecha }}" onInput="{{ set_rp_fecha }}" type="date" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;"></label> 6158 <button type="button" onClick="{{ toggleRpListoParaCargar }}" aria-pressed="{{ rp_listoParaCargar }}" style="border:1px solid {{ rp_listoBorder }}; border-radius:10px; padding:11px 13px; background:{{ rp_listoBg }}; color:{{ rp_listoColor }}; text-align:left; font:700 13px inherit; cursor:pointer;">{{ rp_listoLabel }}</button> 6159 <sc-if value="{{ rp_showFechaCarga }}" hint-placeholder-val="true"><label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Fecha disponible de carga *</span><input value="" data-dc-attr-value="{{ rp_fechaCarga }}" data-dc-attr-min="{{ rp_fechaCargaMin }}" data-dc-attr-max="{{ rp_fechaCargaMax }}" onInput="{{ set_rp_fechaCarga }}" type="date" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;"><span style="font-size:11px; color:#65758a;">Entre 1 y 7 dÃas corridos después de la nueva subasta.</span></label></sc-if> 6160 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Precio sugerido *</span><input value="" data-dc-attr-value="{{ rp_precio }}" onInput="{{ set_rp_precio }}" inputmode="numeric" placeholder="0" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033;"></label> 6161 <label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Plazo de pago *</span><select value="" data-dc-attr-value="{{ rp_plazo }}" onChange="{{ set_rp_plazo }}" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none; color:#112033; background:#fff;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rpPlazoOpts }}" as="o" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ o.id }}">{{ o.label }}</option></sc-for></select></label> 6162 <sc-if value="{{ rpShowOtro }}" hint-placeholder-val="{{ false }}"><label style="display:flex; flex-direction:column; gap:6px;"><span style="font-size:12.5px; font-weight:600; color:#3d4c5f;">Especificar plazo *</span><input value="" data-dc-attr-value="{{ rp_plazoOtro }}" onInput="{{ set_rp_plazoOtro }}" placeholder="Ej: 45 dÃas" style="border:1px solid #dde7f2; border-radius:10px; padding:11px 13px; font-size:14px; font-family:inherit; outline:none;
6162 color:#112033;"></label></sc-if> 6163 <sc-if value="{{ rpMsg }}" hint-placeholder-val="{{ false }}"><div style="font-size:12.5px; padding:9px 12px; border-radius:10px; background:{{ rpMsgBg }}; color:{{ rpMsgColor }};">{{ rpMsg }}</div></sc-if> 6164 </div> 6165 <div style="padding:0 22px 20px; display:flex; gap:10px;"> 6166 <button onClick="{{ submitRepublish }}" disabled="{{ rpSaving }}" style="flex:1; background:#e63946; color:#fff; border:none; border-radius:10px; padding:12px; font-size:14px; font-weight:700; font-family:inherit; cursor:pointer;">{{ rpSaveLabel }}</button> 6167 <button onClick="{{ closeRepublish }}" style="background:#eef3f9; color:#3d4c5f; border:1px solid #dde7f2; border-radius:10px; padding:12px 20px; font-size:14px; font-weight:700; font-family:inherit; cursor:pointer;">Cancelar</button> 6168 </div> 6169 </div> 6170 </div> 6171 </sc-if> 6172 </sc-if> 6173 <div style="height:40px;"></div> 6174 </div> 6175 </sc-if> 6176 6177 6178 <sc-if value="{{ isRosganAuditor }}" hint-placeholder-val="{{ false }}"> 6179 <main class="ra-route-loading" aria-busy="{{ rgAuditorRouteBusy }}"> 6180 <div class="ra-brand">Rosgan · Gestión de remates</div> 6181 <h1>Panel Auditor</h1> 6182 <p role="status">{{ rgAuditorRouteMessage }}</p> 6183 <sc-if value="{{ rgAuditorRouteError }}" hint-placeholder-val="{{ false }}"> 6184 <button class="ra-button" onClick="{{ retryRgAuditor }}">Reintentar</button> 6185 <button class="ra-button" onClick="{{ exitRgAuditor }}">Volver a remates</button> 6186 </sc-if> 6187 </main> 6188 </sc-if> 6189 <!-- ============ REMATES (HISTÃRICO) ============ --> 6190 <sc-if value="{{ isRemates }}" hint-placeholder-val="{{ false }}"> 6191 <div style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1); padding:0 clamp(14px, 4vw, 36px); max-width:1240px; margin:0 auto;"> 6192 <sc-if value="{{ isRematesIndex }}" hint-placeholder-val="true"> 6193 <div style="padding:26px 0 18px;"> 6194 <div style="display:flex; justify-content:space-between; align-items:flex-end; gap:16px; flex-wrap:wrap; margin-top:4px;"> 6195 <div><h1 style="font-size:clamp(24px, 3.4vw, 34px); font-weight:800; letter-spacing:-0.02em; margin:0; color:#112033;">Remates</h1></div> 6196 <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;"> 6197 <sc-if value="{{ rgCanViewPanel }}" hint-placeholder-val="false"><button type="button" class="rosgan-page-action" onClick="{{ toggleRematePanelPicker }}" aria-haspopup="dialog" aria-controls="remate-panel-picker"><img src="/assets/icons/lucide/clipboard-list.svg" width="17" height="17" alt="" aria-hidden="true">{{ rgPanelLabel }}</button></sc-if> 6198 <sc-if value="{{ canAdminRosganRemates }}" hint-placeholder-val="false"><button onClick="{{ openRosganRemateCreate }}" style="display:inline-flex; align-items:center; gap:7px; border:none; border-radius:12px; padding:10px 15px; background:#e63946; color:#fff; font-family:inherit; font-size:13px; font-weight:800; cursor:pointer; box-shadow:0 8px 20px rgba(230,57,70,.24);"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i>Crear remate</button></sc-if> 6199 <sc-if value="{{ canLoadRosganLot }}" hint-placeholder-val="false"><button onClick="{{ goRosganLotLoad }}" style="display:inline-flex; align-items:center; gap:7px; border:none; border-radius:12px; padding:10px 15px; background:#0a5ca8; color:#fff; font-family:inherit; font-size:13px; font-weight:700; cursor:pointer; box-shadow:0 7px 18px rgba(10,92,168,.22);"><span style="font-size:16px;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></span>Cargar lote Rosgan</button></sc-if> 6200 </div> 6201 </div> 6202 </div> 6203 <sc-if value="{{ showRosganRemates }}" hint-placeholder-val="true"> 6204 <section style="margin-bottom:28px;"> 6205 <div style="display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:11px;"> 6206 <div><div style="font-size:11px; font-weight:850; letter-spacing:.12em; color:#0a5ca8;">PRÃXIMOS REMATES</div><h2 style="font-size:20px; margin:3px 0 0; color:#112033;">Remates activos</h2></div> 6207 <button onClick="{{ refreshRosganRemates }}" disabled="{{ rosganRematesLoading }}" title="Actualizar remates" style="width:40px; height:40px; display:grid; place-items:center; border-radius:12px; border:1px solid #dce7f2; background:#fff; cursor:pointer;"><img src="assets/icons/lucide/refresh-cw.svg" alt="" style="width:17px; height:17px;"></button> 6208 </div> 6209 <sc-if value="{{ rosganRematesInitialLoading }}" hint-placeholder-val="false"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:12px;"><sc-for list="{{ skeletons }}" as="k" hint-placeholder-count="3"><div style="height:142px; border-radius:18px; background:linear-gradient(90deg,#e8eef5 25%,#f8fafd 50%,#e8eef5 75%); background-size:800px 100%; animation:shimmer 1.3s linear infinite;"></div></sc-for></div></sc-if> 6210 <sc-if value="{{ rosganRematesHasError }}" hint-placeholder-val="false"><div style="padding:14px 16px; border-radius:14px; border:1px solid #f3c6c9; background:#fff4f4; color:#a72b35; font-size:13px;">{{ rosganRematesError }}</div></sc-if> 6211 <sc-if value="{{ hasRosganRemates }}" hint-placeholder-val="false"> 6212 <div class="rosgan-remate-admin-grid rg-active-remates"> 6213 <sc-for list="{{ rosganRemateCards }}" as="r" hint-placeholder-count="3"> 6214 <article onClick="{{ r.open }}" tabindex="0" role="button" style="background:#fff; border:1px solid #e2eaf3; border-radius:18px; padding:17px; box-shadow:0 10px 28px rgba(0,30,60,.07); min-width:0; cursor:pointer; transition:transform .2s ease, box-shadow .2s ease;"> 6215 <div style="display:flex; gap:8px; align-items:flex-start;"><span style="width:45px; height:45px; border-radius:13px; background:{{ r.logoBg }}; border:1px solid #e6edf5; display:grid; place-items:center; flex:none; overflow:hidden;"><sc-if value="{{ r.hasLogo }}" hint-placeholder-val="false"><img src="" data-dc-attr-src="{{ r.logo }}" alt="" style="width:38px; height:38px; object-fit:contain;"></sc-if><sc-if value="{{ r.noLogo }}" hint-placeholder-val="true"><strong style="color:#0a5ca8;">{{ r.initial }}</strong></sc-if></span><sc-if value="{{ r.showHeaderConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-header-cons-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;" title="{{ r.consignataria }}"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt="{{ r.consignataria }}"></span></sc-if><div style="min-width:0; flex:1;"><div style="font-size:10px; color:#16846e; font-weight:850; letter-spacing:.09em;">{{ r.status }}<sc-if value="{{ r.typeDetail }}" hint-placeholder-val="false"><span style="font-size:11px;font-weight:500;letter-spacing:normal;text-transform:none;"> · {{ r.typeDetail }}</span></sc-if></div><h3 style="font-size:15.5px; line-height:1.22; margin:3px 0; color:#112033; overflow-wrap:anywhere;">{{ r.title }}</h3><div style="font-size:12px; color:#65758a;">{{ r.date }}</div><sc-if value="{{ r.prebidsActive }}" hint-placeholder-val="false"><span class="prebid-active-badge"><span class="prebid-active-dot" aria-hidden="true"></span>Preofertas activas</span></sc-if></div></div> 6216 <div class="remate-card-facts" style="display:grid; gap:8px; margin:14px 0;"><sc-if value="{{ r.showFact }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">{{ r.factLabel }}</small><strong style="display:block; font-size:13px; color:#112033;">{{ r.factValue }}</strong></div></sc-if><sc-if value="{{ r.showConsignataria }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">CONSIGNATARIA</small><div class="remate-card-consignataria"><sc-if value="{{ r.hasConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-consignataria-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt=""></span></sc-if><strong style="display:block; font-size:12px; color:#112033;">{{ r.consignataria }}</strong></div></div></sc-if><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">UBICACIÃN</small><sc-if value="{{ r.hasVenue }}" hint-placeholder-val="false"><strong style="display:block;font-size:12px;color:#112033;margin-bottom:4px;">{{ r.venue }}</strong></sc-if><sc-if value="{{ !r.hasVenue }}"><strong style="display:block; font-size:12px; color:#112033;">{{ r.location }}</strong></sc-if></div></div> 6217 <sc-if value="{{ r.catalogAvailable }}" hint-placeholder-val="false"><button type="button" class="remate-list-catalog" onClick="{{ r.openCatalog }}"><img src="assets/icons/lucide/file-text.svg" alt="">Catálogo disponible</button></sc-if></article> 6218 </sc-for> 6219 </div> 6220 </sc-if> 6221 <sc-if value="{{ rosganRematesEmpty }}" hint-placeholder-val="false"><div style="padding:24px; text-align:center; border:1px dashed #cdd9e6; border-radius:16px; background:rgba(255,255,255,.7);"><strong style="display:block; color:#112033;">TodavÃa no hay remates activos</strong><span style="font-size:13px; color:#65758a;">Los próximos remates de Rosgan y cabañas aparecerán acá.</span></div></sc-if> 6222 </section> 6223 </sc-if> 6224 </sc-if> 6225 <sc-if value="{{ rosganRematePageVisible }}" hint-placeholder-val="false"> 6226 <button onClick="{{ closeRosganRemateForm }}" style="display:inline-flex; align-items:center; gap:7px; margin:22px 0 12px; padding:0; border:none; background:transparent; color:#0a5ca8; font:inherit; font-size:13px; font-weight:750; cursor:pointer;"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i>Volver a remates</button> 6227 <section style="background:#fff; border:1px solid #dfe8f2; border-radius:22px; padding:clamp(18px,3vw,28px); margin:0 0 30px; box-shadow:0 18px 50px rgba(0,35,70,.1); animation:pageIn 250ms ease both;"> 6228 <div style="display:flex; align-items:flex-start; justify-content:space-between; gap:14px; margin-bottom:22px;"><div><span style="font-size:10.5px; font-weight:850; letter-spacing:.12em; color:#e63946;">{{ rosganRemateFormEyebrow }}</span><h2 style="font-size:24px; margin:4px 0; color:#112033;">{{ rosganRemateFormTitle }}</h2><p style="font-size:13px; color:#65758a; margin:0;">Los campos con asterisco son obligatorios. El flyer se carga al guardar.</p></div><button onClick="{{ closeRosganRemateForm }}" title="Cerrar" style="width:38px; height:38px; border-radius:12px; border:1px solid #dfe7f0; background:#f7f9fc; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></div> 6229 <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,520px),1fr)); gap:22px; align-items:start;"> 6230 <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr)); gap:14px;"> 6231 <label style="grid-column:1/-1; display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">TÃTULO *</span><input value="" data-dc-attr-value="{{ rrfTitle }}" onInput="{{ setRrfTitle }}" placeholder="Ej. Remate Habitual Rosgan" style="width:100%; box-sizing:border-box; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit; outline:none;"></label> 6232 <label style="display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">FECHA *</span><input type="date" value="" data-dc-attr-value="{{ rrfDate }}" onInput="{{ setRrfDate }}" style="width:100%; box-sizing:border-box; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit;"></label> 6233 <label style="display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">HORA *</span><input type="time" value="" data-dc-attr-value="{{ rrfTime }}" onInput="{{ setRrfTime }}" style="width:100%; box-sizing:border-box; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit;"></label> 6234 <label style="display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">CABEZAS</span><input type="number" min="1" step="1" inputmode="numeric" value="" data-dc-attr-value="{{ rrfHeads }}" onInput="{{ setRrfHeads }}" placeholder="Automático: suma de los lotes" style="width:100%; box-sizing:border-box; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit;"><span style="font-size:11px;color:#60758c;">Opcional. VacÃo: suma de las cantidades de todos los lotes. Si indicás un valor, se mostrará ese.</span></label> 6235 <label style="display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">TIPO DE REMATE *</span><select value="" data-dc-attr-value="{{ rrfType }}" onChange="{{ setRrfType }}" disabled="{{ rrfTypeDisabled }}" style="width:100%; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit; background:#fff;"><option value="">{{ rrfTypePlaceholder }}</option><sc-for list="{{ rosganRemateTypeOptions }}" as="o" hint-placeholder-count="5"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select><sc-if value="{{ rrfTypesMessageVisible }}" hint-placeholder-val="false"><small style="color:#a32934;">{{ rrfTypesMessage }}</small></sc-if></label> 6236 <label style="display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">MODO DE CONSIGNATARIA *</span><select value="" data-dc-attr-value="{{ rrfMode }}" onChange="{{ setRrfMode }}" disabled="{{ rrfModeLocked }}" style="width:100%; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit; background:#fff;"><sc-for list="{{ rosganRemateModeOptions }}" as="o" hint-placeholder-count="2"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select><sc-if value="{{ rrfModeLocked }}" hint-placeholder-val="false"><small style="color:#65758a;">Este tipo admite únicamente modo Individual.</small></sc-if></label> 6237 <sc-if value="{{ rrfShowConsignataria }}" hint-placeholder-val="true"><label style="grid-column:1/-1; display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">CONSIGNATARIA *</span><select value="" data-dc-attr-value="{{ rrfConsignataria }}" onChange="{{ setRrfConsignataria }}" disabled="{{ rrfConsignatariaLocked }}" style="width:100%; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit; background:#fff;"><option value="">Seleccionar consignatariaâ¦</option><sc-for list="{{ rosganRemateConsignatarias }}" as="o" hint-placeholder-count="6"><option value="" data-dc-attr-value="{{ o.value }}">{{ o.label }}</option></sc-for></select><sc-if value="{{ rrfConsignatariaLocked }}" hint-placeholder-val="false"><small style="color:#65758a;">Asignada automáticamente según tu usuario.</small></sc-if></label></sc-if> 6238 <sc-if value="{{ rrfAllConsignatarias }}" hint-placeholder-val="false"><div style="grid-column:1/-1; padding:11px 13px; border:1px solid #c7e1d9; border-radius:12px; background:#f0faf7; color:#176b58; font-size:12px; font-weight:700;">El remate quedará disponible para todas las consignatarias.</div></sc-if> 6239 <label style="display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">PROVINCIA *</span><select value="" data-dc-attr-value="{{ rrfProvince }}" onChange="{{ setRrfProvince }}" style="width:100%; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit; background:#fff;"><option value="">Seleccionarâ¦</option><sc-for list="{{ rosganRemateProvinces }}" as="o" hint-placeholder-count="6"><option value="" data-dc-attr-value="{
6239{ o }}">{{ o }}</option></sc-for></select></label> 6240 <div style="position:relative;display:grid;gap:6px;"><label for="rrf-locality" style="display:flex;justify-content:space-between;font-size:11px;font-weight:800;color:#526174;">LOCALIDAD *<span style="display:inline-flex;gap:4px;align-items:center;font-weight:500;"><img src="assets/google-logo.webp" alt="" style="width:12px;height:12px;">Google</span></label><input id="rrf-locality" role="combobox" aria-autocomplete="list" aria-controls="rrf-place-options" aria-expanded="{{ rrfPlacesExpanded }}" aria-activedescendant="{{ rrfPlaceActiveId }}" value="" data-dc-attr-value="{{ rrfLocality }}" onInput="{{ setRrfLocality }}" onKeyDown="{{ rrfPlaceKey }}" onFocus="{{ rrfPlaceFocus }}" onBlur="{{ rrfPlaceBlur }}" disabled="{{ rrfLocalityDisabled }}" autocomplete="off" placeholder="{{ rrfLocalityPlaceholder }}" style="min-width:0;width:100%;box-sizing:border-box;border:1px solid #cedbea;border-radius:12px;padding:12px 13px;font:inherit;background:#fff;"><sc-if value="{{ rrfPlacesVisible }}"><div id="rrf-place-options" role="listbox" aria-label="Localidades sugeridas" style="position:absolute;z-index:45;left:0;right:0;top:100%;max-height:240px;overflow:auto;padding:5px;border:1px solid #cedbea;border-radius:12px;background:#fff;box-shadow:0 12px 28px rgba(0,30,60,.16);"><sc-for list="{{ rrfPlaces }}" as="place"><button type="button" role="option" id="{{ place.id }}" aria-selected="{{ place.selected }}" onPointerDown="{{ place.pickPointer }}" onClick="{{ place.pick }}" style="width:100%;padding:10px;border:0;border-radius:8px;background:{{ place.bg }};text-align:left;cursor:pointer;"><strong style="display:block;color:#123553;">{{ place.label }}</strong><small style="display:block;color:#60758c;margin-top:3px;">{{ place.address }} · {{ place.source }}</small></button></sc-for></div></sc-if><span role="status" style="font-size:11px;color:#60758c;">{{ rrfPlaceStatus }}</span></div> 6241 <label style="grid-column:1/-1; display:grid; gap:6px;"><span style="font-size:11px; font-weight:800; color:#526174;">LUGAR</span><input value="" data-dc-attr-value="{{ rrfPlace }}" onInput="{{ setRrfPlace }}" placeholder="Predio, sociedad rural o establecimiento" style="width:100%; box-sizing:border-box; border:1px solid #cedbea; border-radius:12px; padding:12px 13px; font:inherit;"></label> 6242 <div style="grid-column:1/-1;"><span style="font-size:11px; font-weight:800; color:#526174; display:block; margin-bottom:8px;">CONFIGURACIÃN OPERATIVA</span><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:8px;"><sc-for list="{{ rosganRemateToggles }}" as="t" hint-placeholder-count="7"><button onClick="{{ t.toggle }}" type="button" style="display:flex; align-items:center; gap:10px; padding:10px 11px; border-radius:11px; border:1px solid {{ t.border }}; background:{{ t.bg }}; color:#26384d; font:inherit; font-size:12px; font-weight:700; cursor:pointer; text-align:left;"><span style="width:34px; height:19px; padding:2px; box-sizing:border-box; background:{{ t.track }}; border-radius:20px; flex:none;"><span style="display:block; width:15px; height:15px; border-radius:50%; background:#fff; transform:translateX({{ t.knob }}); transition:transform .18s;"></span></span>{{ t.label }}</button></sc-for></div></div> 6243 </div> 6244 <aside style="border:1px solid #dce7f2; border-radius:17px; background:#f7fafd; padding:14px; align-self:start;"><div style="font-size:11px; font-weight:800; color:#526174; margin-bottom:9px;">FLYER DEL REMATE</div><label style="display:grid; place-items:center; min-height:230px; border:1px dashed #aac2d9; border-radius:14px; background:#fff; overflow:hidden; cursor:pointer; text-align:center; padding:12px; box-sizing:border-box;"><input type="file" accept="image/png,image/jpeg,image/webp" onChange="{{ setRosganRemateFlyer }}" style="display:none;"><sc-if value="{{ rrfHasFlyer }}" hint-placeholder-val="false"><img src="" data-dc-attr-src="{{ rrfFlyerPreview }}" alt="Previsualización del flyer" style="width:100%; height:230px; object-fit:contain;"></sc-if><sc-if value="{{ rrfNoFlyer }}" hint-placeholder-val="true"><span><img src="assets/icons/lucide/image-plus.svg" alt="" style="width:28px; height:28px; opacity:.65;"><strong style="display:block; color:#112033; margin-top:9px;">Elegir flyer</strong><small style="display:block; color:#7a8ca0; margin-top:4px;">PNG, JPG o WebP</small></span></sc-if></label><div style="font-size:11.5px; color:#65758a; margin-top:9px; line-height:1.4;">Podés crear el remate sin flyer y agregarlo luego desde Editar.</div></aside> 6245 </div> 6246 <sc-if value="{{ rosganRemateHasMessage }}" hint-placeholder-val="false"><div style="margin-top:16px; padding:12px 14px; border-radius:12px; border:1px solid {{ rosganRemateMessageBorder }}; background:{{ rosganRemateMessageBg }}; color:{{ rosganRemateMessageColor }}; font-size:13px;">{{ rosganRemateMessageText }}</div></sc-if> 6247 <div style="display:flex; justify-content:flex-end; gap:9px; margin-top:20px; padding-top:18px; border-top:1px solid #edf2f7;"><button onClick="{{ closeRosganRemateForm }}" disabled="{{ rosganRemateSaving }}" style="padding:11px 17px; border-radius:11px; border:1px solid #cedbea; background:#fff; font:inherit; font-weight:700; cursor:pointer;">Cancelar</button><button onClick="{{ saveRosganRemate }}" disabled="{{ rosganRemateSaving }}" style="padding:11px 19px; border-radius:11px; border:none; background:#0a5ca8; color:#fff; font:inherit; font-weight:800; cursor:pointer; box-shadow:0 8px 18px rgba(10,92,168,.2);">{{ rosganRemateSaveLabel }}</button></div> 6248 </section> 6249 </sc-if> 6250 <sc-if value="{{ hasScheduledRemates }}" hint-placeholder-val="false"> 6251 <section style="margin:26px 0 30px;"><div style="font-size:11px;font-weight:850;letter-spacing:.12em;color:#0a5ca8;">AGENDA DE REMATES</div><h2 style="font-size:20px;margin:4px 0 16px;color:#112033;">Próximos remates</h2><div class="rosgan-remate-admin-grid"> 6252 <sc-for list="{{ scheduledRemateCards }}" as="r" hint-placeholder-count="6"> 6253 <article onClick="{{ r.open }}" tabindex="0" role="button" data-reveal style="background:#fff; border:1px solid #e2eaf3; border-radius:18px; padding:17px; box-shadow:0 10px 28px rgba(0,30,60,.07); min-width:0; cursor:pointer; transition:transform .2s ease, box-shadow .2s ease;"> 6254 <div style="display:flex; gap:8px; align-items:flex-start;"><span style="width:45px; height:45px; border-radius:13px; background:{{ r.logoBg }}; border:1px solid {{ r.logoBorder }}; display:grid; place-items:center; flex:none; overflow:hidden;"><sc-if value="{{ r.hasLogo }}" hint-placeholder-val="false"><img src="" data-dc-attr-src="{{ r.logo }}" alt="" style="width:28px; height:28px; object-fit:contain;"></sc-if><sc-if value="{{ r.noLogo }}" hint-placeholder-val="true"><strong style="color:{{ r.color }};">{{ r.initial }}</strong></sc-if></span><sc-if value="{{ r.showHeaderConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-header-cons-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;" title="{{ r.consignataria }}"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt="{{ r.consignataria }}"></span></sc-if><div style="min-width:0; flex:1;"><div style="font-size:10px; color:{{ r.color }}; font-weight:850; letter-spacing:.09em;">{{ r.type }}<sc-if value="{{ r.typeDetail }}" hint-placeholder-val="false"><span style="font-size:11px;font-weight:500;letter-spacing:normal;text-transform:none;"> · {{ r.typeDetail }}</span></sc-if></div><h3 style="font-size:15.5px; line-height:1.22; margin:3px 0; color:#112033; overflow-wrap:anywhere;">{{ r.title }}</h3><div style="font-size:12px; color:#65758a;">{{ r.date }}</div><sc-if value="{{ r.prebidsActive }}" hint-placeholder-val="false"><span class="prebid-active-badge"><span class="prebid-active-dot" aria-hidden="true"></span>Preofertas activas</span></sc-if></div></div> 6255 <div class="remate-card-facts" style="display:grid; gap:8px; margin:14px 0 0;"><sc-if value="{{ r.showMeta }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">{{ r.metaLabel }}</small><strong style="display:block; font-size:12px; color:#112033;">{{ r.metaValue }}</strong></div></sc-if><sc-if value="{{ r.showConsignataria }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">CONSIGNATARIA</small><div class="remate-card-consignataria"><sc-if value="{{ r.hasConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-consignataria-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt=""></span></sc-if><strong style="display:block; font-size:12px; color:#112033;">{{ r.consignataria }}</strong></div></div></sc-if><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">UBICACIÃN</small><sc-if value="{{ r.hasVenue }}" hint-placeholder-val="false"><strong style="display:block;font-size:12px;color:#112033;margin-bottom:4px;">{{ r.venue }}</strong></sc-if><sc-if value="{{ !r.hasVenue }}"><strong style="display:block; font-size:12px; color:#112033;">{{ r.location }}</strong></sc-if></div></div> 6256 <sc-if value="{{ r.catalogAvailable }}" hint-placeholder-val="false"><button type="button" class="remate-list-catalog" onClick="{{ r.openCatalog }}"><img src="assets/icons/lucide/file-text.svg" alt="">Catálogo disponible</button></sc-if></article> 6257 </sc-for> 6258 </div> 6259 </section> 6260 </sc-if> 6261 <section style="margin:28px 0;" aria-label="Ãltimos remates"> 6262 <h2 style="font-size:20px;margin:0 0 16px;color:#112033;">Ãltimos remates</h2> 6263 <sc-if value="{{ latestFinishedLoading }}" hint-placeholder-val="false"><p style="color:#718196;">Cargando últimos rematesâ¦</p></sc-if> 6264 <sc-if value="{{ latestFinishedError }}" hint-placeholder-val="false"><p style="color:#a32934;">{{ latestFinishedError }} <button type="button" onClick="{{ retryLatestFinished }}">Reintentar</button></p></sc-if> 6265 <sc-if value="{{ hasLatestFinished }}" hint-placeholder-val="false"><div class="rosgan-remate-admin-grid"> 6266 <sc-for list="{{ latestFinishedCards }}" as="r" hint-placeholder-count="6"> 6267 <article onClick="{{ r.open }}" tabindex="0" role="button" data-reveal style="background:#fff; border:1px solid #e2eaf3; border-radius:18px; padding:17px; box-shadow:0 10px 28px rgba(0,30,60,.07); min-width:0; cursor:pointer; transition:transform .2s ease, box-shadow .2s ease;"> 6268 <div style="display:flex; gap:8px; align-items:flex-start;"><span style="width:45px; height:45px; border-radius:13px; background:{{ r.logoBg }}; border:1px solid {{ r.logoBorder }}; display:grid; place-items:center; flex:none; overflow:hidden;"><sc-if value="{{ r.hasLogo }}" hint-placeholder-val="false"><img src="" data-dc-attr-src="{{ r.logo }}" alt="" style="width:28px; height:28px; object-fit:contain;"></sc-if><sc-if value="{{ r.noLogo }}" hint-placeholder-val="true"><strong style="color:{{ r.color }};">{{ r.initial }}</strong></sc-if></span><sc-if value="{{ r.showHeaderConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-header-cons-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;" title="{{ r.consignataria }}"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt="{{ r.consignataria }}"></span></sc-if><div style="min-width:0; flex:1;"><div style="font-size:10px; color:{{ r.color }}; font-weight:850; letter-spacing:.09em;">{{ r.type }}<sc-if value="{{ r.typeDetail }}" hint-placeholder-val="false"><span style="font-size:11px;font-weight:500;letter-spacing:normal;text-transform:none;"> · {{ r.typeDetail }}</span></sc-if></div><h3 style="font-size:15.5px; line-height:1.22; margin:3px 0; color:#112033; overflow-wrap:anywhere;">{{ r.title }}</h3><div style="font-size:12px; color:#65758a;">{{ r.date }}</div></div></div> 6269 <div class="remate-card-facts" style="display:grid; gap:8px; margin:14px 0 0;"><sc-if value="{{ r.showMeta }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">{{ r.metaLabel }}</small><strong style="display:block; font-size:12px; color:#112033;">{{ r.metaValue }}</strong></div></sc-if><sc-if value="{{ r.showConsignataria }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">CONSIGNATARIA</small><div class="remate-card-consignataria"><sc-if value="{{ r.hasConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-consignataria-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt=""></span></sc-if><strong style="display:block; font-size:12px; color:#112033;">{{ r.consignataria }}</strong></div></div></sc-if><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">UBICACIÃN</small><sc-if value="{{ r.hasVenue }}" hint-placeholder-val="false"><strong style="display:block;font-size:12px;color:#112033;margin-bottom:4px;">{{ r.venue }}</strong></sc-if><sc-if value="{{ !r.hasVenue }}"><strong style="display:block; font-size:12px; color:#112033;">{{ r.location }}</strong></sc-if></div></div> 6270 <sc-if value="{{ r.catalogAvailable }}" hint-placeholder-val="false"><button type="button" class="remate-list-catalog" onClick="{{ r.openCatalog }}"><img src="assets/icons/lucide/file-text.svg" alt="">Catálogo disponible</button></sc-if></article> 6271 </sc-for> 6272 </div></sc-if> 6273 </section> 6274 <sc-if value="{{ showRemateArchiveToggle }}" hint-placeholder-val="false"><button class="remate-past-toggle" type="button" onClick="{{ toggleRemateArchive }}" aria-expanded="{{ remateArchiveOpen }}"><span class="remate-past-icon"><img src="assets/icons/lucide/file-text.svg" alt=""></span><span>{{ remateArchiveToggleLabel }}</span><img class="remate-past-chevron" src="assets/icons/lucide/chevron-down.svg" alt=""></button></sc-if> 6275 <sc-if value="{{ showRemateArchive }}" hint-placeholder-val="false"> 6276 <div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin:0 0 16px; padding-top:20px; border-top:1px solid #dfe8f1;"><div><div style="font-size:10.5px; letter-spacing:.12em; color:#7c8da2; font-weight:800;">ARCHIVO HISTÃRICO</div><h2 style="font-size:20px; margin:3px 0 0; color:#112033;">Remates anteriores</h2><p style="margin:4px 0 0; color:#718196; font-size:11.5px;">Historial unificado de Rosgan y cabañas</p></div><div style="display:flex; align-items:center; gap:8px;"><span style="font-size:12.5px; color:#65758a; font-weight:600;">Año</span><select value="" data-dc-attr-value="{{ remateYear }}" onChange="{{ onRemateYear }}" style="background:#fff; border:1px solid #e3eaf2; border-radius:12px; padding:10px 14px; font-size:14px; font-weight:700; font-family:inherit; color:#112033; outline:none;"><sc-for list="{{ remateYears }}" as="y" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ y }}">{{ y }}</option></sc-for></select></div></div> 6277 <sc-if value="{{ rematesLoadingVisible }}" hint-placeholder-val="{{ false }}"> 6278 <div aria-label="Cargando remates" style="animation:pageIn 260ms ease both;"> 6279 <div style="display:flex; gap:8px; margin-bottom:18px;">
6280 <span style="width:112px; height:36px; border-radius:100px; background:linear-gradient(90deg,#e8eef5 25%,#f7fafd 50%,#e8eef5 75%); background-size:500px 100%; animation:shimmer 1.3s linear infinite;"></span> 6281 <span style="width:96px; height:36px; border-radius:100px; background:linear-gradient(90deg,#e8eef5 25%,#f7fafd 50%,#e8eef5 75%); background-size:500px 100%; animation:shimmer 1.3s linear infinite;"></span> 6282 <span style="width:124px; height:36px; border-radius:100px; background:linear-gradient(90deg,#e8eef5 25%,#f7fafd 50%,#e8eef5 75%); background-size:500px 100%; animation:shimmer 1.3s linear infinite;"></span> 6283 </div> 6284 <div class="rosgan-remate-admin-grid"> 6285 <sc-for list="{{ skeletons }}" as="k" hint-placeholder-count="6"> 6286 <div style="height:142px; border-radius:18px; background:linear-gradient(90deg,#e8eef5 25%,#f8fafd 50%,#e8eef5 75%); background-size:800px 100%; animation:shimmer 1.3s linear infinite; box-shadow:0 8px 26px rgba(0,30,60,0.05);"></div> 6287 </sc-for> 6288 </div> 6289 </div> 6290 </sc-if> 6291 <sc-if value="{{ rematesReady }}" hint-placeholder-val="{{ true }}"> 6292 <!-- type filter chips --> 6293 <sc-if value="{{ remateFiltersVisible }}" hint-placeholder-val="{{ true }}"> 6294 <div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:18px;"> 6295 <sc-for list="{{ remateTypeChips }}" as="t" hint-placeholder-count="6"> 6296 <span onClick="{{ t.go }}" style="display:inline-flex; align-items:center; gap:8px; padding:9px 16px; border-radius:100px; font-size:13px; font-weight:600; cursor:pointer; border:1px solid {{ t.border }}; background:{{ t.bg }}; color:{{ t.color }}; transition:transform 160ms;" style-hover="transform:translateY(-2px);"><span style="width:9px; height:9px; border-radius:50%; background:{{ t.dot }};"></span>{{ t.label }}<strong style="font-variant-numeric:tabular-nums;">{{ t.count }}</strong></span> 6297 </sc-for> 6298 </div> 6299 </sc-if> 6300 <sc-if value="{{ hasRemates }}" hint-placeholder-val="{{ true }}"> 6301 <div class="rosgan-remate-admin-grid"> 6302 <sc-for list="{{ remateCards }}" as="r" hint-placeholder-count="6"> 6303 <article onClick="{{ r.open }}" tabindex="0" role="button" data-reveal style="background:#fff; border:1px solid #e2eaf3; border-radius:18px; padding:17px; box-shadow:0 10px 28px rgba(0,30,60,.07); min-width:0; cursor:pointer; transition:transform .2s ease, box-shadow .2s ease;"> 6304 <div style="display:flex; gap:8px; align-items:flex-start;"><span style="width:45px; height:45px; border-radius:13px; background:{{ r.logoBg }}; border:1px solid {{ r.logoBorder }}; display:grid; place-items:center; flex:none; overflow:hidden;"><sc-if value="{{ r.hasLogo }}" hint-placeholder-val="false"><img src="" data-dc-attr-src="{{ r.logo }}" alt="" style="width:28px; height:28px; object-fit:contain;"></sc-if><sc-if value="{{ r.noLogo }}" hint-placeholder-val="true"><strong style="color:{{ r.color }};">{{ r.initial }}</strong></sc-if></span><sc-if value="{{ r.showHeaderConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-header-cons-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;" title="{{ r.consignataria }}"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt="{{ r.consignataria }}"></span></sc-if><div style="min-width:0; flex:1;"><div style="font-size:10px; color:{{ r.color }}; font-weight:850; letter-spacing:.09em;">{{ r.type }}<sc-if value="{{ r.typeDetail }}" hint-placeholder-val="false"><span style="font-size:11px;font-weight:500;letter-spacing:normal;text-transform:none;"> · {{ r.typeDetail }}</span></sc-if></div><h3 style="font-size:15.5px; line-height:1.22; margin:3px 0; color:#112033; overflow-wrap:anywhere;">{{ r.title }}</h3><div style="font-size:12px; color:#65758a;">{{ r.date }}</div></div></div> 6305 <div class="remate-card-facts" style="display:grid; gap:8px; margin:14px 0 0;"><sc-if value="{{ r.showMeta }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">{{ r.metaLabel }}</small><strong style="display:block; font-size:12px; color:#112033;">{{ r.metaValue }}</strong></div></sc-if><sc-if value="{{ r.showConsignataria }}" hint-placeholder-val="false"><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">CONSIGNATARIA</small><div class="remate-card-consignataria"><sc-if value="{{ r.hasConsignatariaLogo }}" hint-placeholder-val="false"><span class="remate-card-consignataria-logo" style="background:{{ r.consignatariaLogoBg }};border-color:transparent;"><img src="" data-dc-attr-src="{{ r.consignatariaLogo }}" alt=""></span></sc-if><strong style="display:block; font-size:12px; color:#112033;">{{ r.consignataria }}</strong></div></div></sc-if><div style="padding:9px 10px; border-radius:11px; background:#f4f7fb; min-width:0;"><small style="display:block; font-size:9px; color:#7b8ca1; font-weight:800;">UBICACIÃN</small><sc-if value="{{ r.hasVenue }}" hint-placeholder-val="false"><strong style="display:block;font-size:12px;color:#112033;margin-bottom:4px;">{{ r.venue }}</strong></sc-if><sc-if value="{{ !r.hasVenue }}"><strong style="display:block; font-size:12px; color:#112033;">{{ r.location }}</strong></sc-if></div></div> 6306 <sc-if value="{{ r.catalogAvailable }}" hint-placeholder-val="false"><button type="button" class="remate-list-catalog" onClick="{{ r.openCatalog }}"><img src="assets/icons/lucide/file-text.svg" alt="">Catálogo disponible</button></sc-if></article> 6307 </sc-for> 6308 </div> 6309 </sc-if> 6310 <sc-if value="{{ remateArchiveHasMore }}" hint-placeholder-val="false"><button type="button" onClick="{{ moreRemateArchive }}" style="display:block;margin:20px auto;padding:12px 24px;border:1px solid #cbdced;border-radius:12px;background:#fff;color:#075ca8;font:700 14px inherit;cursor:pointer;">Ver más remates anteriores</button></sc-if> 6311 <sc-if value="{{ rematesEmpty }}" hint-placeholder-val="{{ false }}"> 6312 <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;"> 6313 <div style="font-size:16px; font-weight:700; color:#112033;">Sin remates para este filtro</div> 6314 <div style="font-size:13.5px; margin-top:4px;">Probá otro año o quitá el filtro de tipo.</div> 6315 </div> 6316 </sc-if> 6317 </sc-if> 6318 </sc-if> 6319 <div style="height:40px;"></div> 6320 </div> 6321 </sc-if> 6322 6323 <!-- ============ CANAL CONSIGNATARIA ============ --> 6324 <sc-if value="{{ isChannel }}" hint-placeholder-val="{{ false }}"> 6325 <div style="animation:pageIn 420ms cubic-bezier(0.22,0.72,0.18,1); min-height:100vh; background:#f4f7fb;"> 6326 <!-- HERO BRANDED --> 6327 <div style="position:relative; overflow:hidden; color:#fff; background:{{ chColor }};"> 6328 <sc-for list="{{ chHeroImages }}" as="img" hint-placeholder-count="1"> 6329 <div style="position:absolute; inset:0; background-image:url('{{ img.src }}'); background-size:cover; background-position:{{ img.position }}; opacity:{{ img.opacity }}; transition:opacity 1100ms ease; animation:kenburns 12s ease-in-out infinite alternate;"></div> 6330 </sc-for> 6331 <div style="position:absolute; inset:0; background:{{ chHeroOverlay }}; background-size:220% 220%; animation:gradientShift 14s ease infinite;"></div> 6332 <div style="position:absolute; width:420px; height:420px; border-radius:50%; top:-160px; right:-120px; background:radial-gradient(circle, {{ chGlow }}, transparent 68%); opacity:0.4; animation:gradientBlob 18s ease-in-out infinite; pointer-events:none;"></div> 6333 <div style="position:relative; max-width:1240px; margin:0 auto; padding:16px clamp(14px, 4vw, 36px) clamp(24px, 3.4vw, 34px);">
6334 <span onClick="{{ goHome }}" style="display:inline-flex; align-items:center; gap:6px; font-size:13px; color:rgba(255,255,255,0.82); font-weight:600; cursor:pointer;" style-hover="color:#fff;"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al inicio</span> 6335 <div style="display:flex; align-items:center; gap:18px; flex-wrap:wrap; margin-top:20px;"> 6336 <span style="width:88px; height:88px; border-radius:22px; background:#fff; display:grid; place-items:center; flex-shrink:0; box-shadow:0 18px 44px rgba(0,0,0,0.28); overflow:hidden; animation:spinIn 620ms cubic-bezier(0.22,0.72,0.18,1) both;"> 6337 <sc-if value="{{ chHasLogo }}" hint-placeholder-val="{{ false }}"><div style="width:100%; height:100%; background-image:url('{{ chLogo }}'); background-size:contain; background-position:center; background-repeat:no-repeat; padding:12px; box-sizing:border-box;"></div></sc-if> 6338 <sc-if value="{{ chNoLogo }}" hint-placeholder-val="{{ true }}"><span style="font-size:34px; font-weight:900; color:{{ chColor }};">{{ chInitial }}</span></sc-if> 6339 </span> 6340 <div style="flex:1; min-width:240px; animation:floatIn 560ms cubic-bezier(0.22,0.72,0.18,1) 80ms both;"> 6341 <div style="display:inline-flex; align-items:center; gap:7px; background:rgba(255,255,255,0.18); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.28); padding:5px 13px; border-radius:100px; font-size:11px; font-weight:800; letter-spacing:0.08em; margin-bottom:9px;"><span style="width:6px; height:6px; border-radius:50%; background:#fff;"></span>CANAL OFICIAL</div> 6342 <h1 style="font-size:clamp(26px, 4.2vw, 44px); font-weight:900; letter-spacing:-0.025em; margin:0; line-height:1.02; text-wrap:balance;">{{ chName }}</h1> 6343 <div style="font-size:14px; color:rgba(255,255,255,0.85); margin-top:8px;">Remates, transmisiones en vivo y grabaciones de la consignataria.</div> 6344 </div> 6345 </div> 6346 <div style="display:grid; grid-template-columns:repeat(3, 1fr); gap:12px; margin-top:24px; max-width:520px;"> 6347 <sc-for list="{{ chStats }}" as="st" hint-placeholder-count="3"> 6348 <div style="background:rgba(255,255,255,0.12); backdrop-filter:blur(10px); border:1px solid rgba(255,255,255,0.2); border-radius:15px; padding:14px 16px; animation:floatIn 560ms cubic-bezier(0.22,0.72,0.18,1) both;"><div style="font-size:clamp(22px, 3vw, 30px); font-weight:900; font-variant-numeric:tabular-nums; letter-spacing:-0.02em;">{{ st.value }}</div><div style="font-size:10.5px; color:rgba(255,255,255,0.78); letter-spacing:0.07em; margin-top:2px;">{{ st.label }}</div></div> 6349 </sc-for> 6350 </div> 6351 <sc-if value="{{ chHasHeroDots }}" hint-placeholder-val="{{ false }}"> 6352 <div style="display:flex; gap:7px; align-items:center; margin-top:20px;"> 6353 <sc-for list="{{ chHeroDots }}" as="d" hint-placeholder-count="5"> 6354 <span onClick="{{ d.go }}" style="height:6px; width:{{ d.w }}; border-radius:100px; background:{{ d.bg }}; cursor:pointer; transition:all 300ms;"></span> 6355 </sc-for> 6356 </div> 6357 </sc-if> 6358 </div> 6359 </div> 6360 6361 <div style="max-width:1240px; margin:0 auto; padding:clamp(20px, 3vw, 30px) clamp(14px, 4vw, 36px) 120px;"> 6362 <!-- loading --> 6363 <sc-if value="{{ chLoading }}" hint-placeholder-val="{{ false }}"> 6364 <div style="display:flex; align-items:center; gap:12px; color:#65758a; font-size:14px; padding:40px 0;"><span style="width:22px; height:22px; border:3px solid #dbe6f1; border-top-color:{{ chColor }}; border-radius:50%; animation:spin 800ms linear infinite;"></span>Cargando remates del canalâ¦</div> 6365 </sc-if> 6366 <!-- empty --> 6367 <sc-if value="{{ chEmpty }}" hint-placeholder-val="{{ false }}"> 6368 <div style="text-align:center; padding:64px 20px; background:#fff; border-radius:20px; box-shadow:0 10px 34px rgba(0,30,60,0.07);"> 6369 <div style="width:64px; height:64px; border-radius:18px; background:{{ chColor }}; opacity:0.14; margin:0 auto 16px;"></div> 6370 <div style="font-size:17px; font-weight:800; color:#112033;">TodavÃa no hay remates publicados</div> 6371 <div style="font-size:13.5px; color:#65758a; margin-top:6px;">Cuando esta consignataria transmita un remate, vas a verlo acá.</div> 6372 </div> 6373 </sc-if> 6374 6375 <!-- featured --> 6376 <sc-if value="{{ chHasFeatured }}" hint-placeholder-val="{{ false }}"> 6377 <div onClick="{{ chFeat.open }}" style="position:relative; border-radius:24px; overflow:hidden; cursor:pointer; box-shadow:0 20px 54px rgba(0,30,60,0.16); margin-bottom:26px; aspect-ratio:16/7; min-height:220px; animation:fadeScale 520ms cubic-bezier(0.22,0.72,0.18,1) both;" style-hover="box-shadow:0 30px 70px rgba(0,30,60,0.24);"> 6378 <div style="position:absolute; inset:0; background-image:url('{{ chFeat.thumb }}'); background-size:cover; background-position:{{ chFeat.thumbPosition }}; transition:transform 700ms cubic-bezier(0.22,0.72,0.18,1);"></div> 6379 <div style="position:absolute; inset:0; background:linear-gradient(105deg, rgba(0,20,40,0.9) 8%, rgba(0,20,40,0.45) 48%, transparent 82%);"></div> 6380 <div style="position:absolute; inset:0; padding:clamp(20px, 3vw, 34px); display:flex; flex-direction:column; justify-content:flex-end; gap:10px;"> 6381 <h2 style="color:#fff; font-size:clamp(20px, 3.2vw, 32px); font-weight:900; margin:0; line-height:1.08; letter-spacing:-0.02em; max-width:720px; text-shadow:0 2px 14px rgba(0,0,0,0.4);">{{ chFeat.title }}</h2> 6382 <div style="color:rgba(255,255,255,0.88); font-size:13.5px; font-weight:600;"><i class="dc-lucide dc-lucide-calendar" aria-hidden="true"></i> {{ chFeat.date }}<sc-if value="{{ chFeat.loc }}" hint-placeholder-val="{{ false }}"> · <i class="dc-lucide dc-lucide-map-pin" aria-hidden="true"></i> {{ chFeat.loc }}</sc-if></div> 6383 </div>
6384 <span style="position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:56px; height:56px; border-radius:50%; background:rgba(255,255,255,0.18); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.4); display:grid; place-items:center; color:#fff; font-size:20px; padding-left:4px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span> 6385 </div> 6386 </sc-if> 6387 6388 <!-- tabs --> 6389 <sc-if value="{{ chHasFeatured }}" hint-placeholder-val="{{ false }}"> 6390 <div style="display:flex; gap:9px; flex-wrap:wrap; margin-bottom:18px;"> 6391 <sc-for list="{{ chTabs }}" as="t" hint-placeholder-count="4"> 6392 <span onClick="{{ t.go }}" style="display:inline-flex; align-items:center; gap:8px; padding:9px 16px; border-radius:100px; font-size:13px; font-weight:700; cursor:pointer; background:{{ t.bg }}; color:{{ t.color }}; border:1px solid {{ t.border }}; transition:all 220ms;"> 6393 <sc-if value="{{ t.live }}" hint-placeholder-val="{{ false }}"><span style="width:7px; height:7px; border-radius:50%; background:currentColor;"></span></sc-if> 6394 {{ t.label }}<strong style="font-variant-numeric:tabular-nums; opacity:0.7;">{{ t.count }}</strong> 6395 </span> 6396 </sc-for> 6397 </div> 6398 </sc-if> 6399 6400 <!-- grid --> 6401 <sc-if value="{{ chHasCards }}" hint-placeholder-val="{{ false }}"> 6402 <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(clamp(240px, 30vw, 300px), 1fr)); gap:16px;"> 6403 <sc-for list="{{ chCards }}" as="m" hint-placeholder-count="6"> 6404 <div data-reveal onClick="{{ m.open }}" style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.09); cursor:pointer; transition:transform 240ms cubic-bezier(0.22,0.72,0.18,1), box-shadow 240ms;" style-hover="transform:translateY(-5px); box-shadow:0 22px 50px rgba(0,30,60,0.2);"> 6405 <div style="position:relative; aspect-ratio:16/9;"> 6406 <div style="position:absolute; inset:0; background-image:url('{{ m.thumb }}'); background-size:cover; background-position:{{ m.thumbPosition }};"></div> 6407 <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,20,40,0) 42%, rgba(0,20,40,0.78));"></div> 6408 <span style="position:absolute; inset:0; display:grid; place-items:center;"><span style="width:46px; height:46px; border-radius:50%; background:rgba(255,255,255,0.16); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.4); display:grid; place-items:center; color:#fff; font-size:16px; padding-left:3px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span></span> 6409 </div> 6410 <div style="padding:13px 15px;"> 6411 <strong style="font-size:15px; font-weight:800; color:#112033; line-height:1.2; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">{{ m.title }}</strong> 6412 <div style="font-size:12.5px; color:#65758a; margin-top:6px;">{{ m.date }}</div> 6413 </div> 6414 </div> 6415 </sc-for> 6416 </div> 6417 </sc-if> 6418 6419 <!-- videos del canal (YouTube asociado por remate) --> 6420 <sc-if value="{{ chHasVideos }}" hint-placeholder-val="{{ false }}"> 6421 <div style="margin-top:34px;"> 6422 <div style="display:flex; align-items:center; gap:9px; margin-bottom:16px;"> 6423 <span style="width:26px; height:20px; border-radius:5px; background:{{ chColor }}; display:grid; place-items:center; color:#fff; font-size:10px; padding-left:2px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span> 6424 <div style="font-size:11.5px; letter-spacing:0.14em; color:{{ chColor }}; font-weight:800;">PLAYLIST DEL CANAL</div> 6425 </div> 6426 <div style="display:grid; grid-auto-flow:column; grid-auto-columns:clamp(240px, 26vw, 320px); gap:16px; overflow-x:auto; padding-bottom:14px; scroll-snap-type:x mandatory; scrollbar-width:none;"> 6427 <sc-for list="{{ chVideos }}" as="v" hint-placeholder-count="4"> 6428 <a href="{{ v.href }}" target="_blank" rel="noopener" data-reveal style="scroll-snap-align:start; background:#fff; border-radius:16px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.09); color:#112033; display:block; transition:transform 220ms, box-shadow 220ms;" style-hover="transform:translateY(-4px); box-shadow:0 20px 46px rgba(0,30,60,0.18); color:#112033;"> 6429 <div style="aspect-ratio:16/9; background-image:url('{{ v.thumb }}'); background-size:cover; background-position:center; position:relative;"> 6430 <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,20,40,0) 55%, rgba(0,20,40,0.5));"></div>
6431 <span style="position:absolute; inset:0; display:grid; place-items:center;"><span style="width:44px; height:44px; border-radius:50%; background:rgba(0,29,61,0.72); display:grid; place-items:center; color:#fff; font-size:15px; padding-left:3px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span></span> 6432 </div> 6433 <div style="padding:12px 14px;"><strong style="font-size:14px; line-height:1.35; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">{{ v.title }}</strong><div style="font-size:12px; color:#8593a6; margin-top:5px;">{{ v.meta }}</div></div> 6434 </a> 6435 </sc-for> 6436 </div> 6437 </div> 6438 </sc-if> 6439 6440 <!-- videoteca por año --> 6441 <sc-if value="{{ chHasLibrary }}" hint-placeholder-val="{{ false }}"> 6442 <div style="margin-top:34px;"> 6443 <div style="display:flex; align-items:flex-end; justify-content:space-between; gap:16px; flex-wrap:wrap; margin-bottom:16px;"> 6444 <div> 6445 <div style="font-size:11.5px; letter-spacing:0.14em; color:{{ chColor }}; font-weight:800;">VIDEOTECA</div> 6446 <h2 style="font-size:clamp(19px, 2.4vw, 23px); font-weight:800; margin:2px 0 0; color:#112033;">Remates grabados por año</h2> 6447 </div> 6448 <span style="font-size:12.5px; color:#8593a6; font-weight:600;">{{ chLibCount }}</span> 6449 </div> 6450 <div style="display:flex; gap:9px; flex-wrap:wrap; margin-bottom:18px;"> 6451 <sc-for list="{{ chYearTabs }}" as="y" hint-placeholder-count="3"> 6452 <span onClick="{{ y.go }}" style="padding:8px 18px; border-radius:100px; font-size:13.5px; font-weight:{{ y.weight }}; cursor:pointer; background:{{ y.bg }}; color:{{ y.color }}; border:1px solid {{ y.border }}; font-variant-numeric:tabular-nums; transition:all 220ms;">{{ y.label }}</span> 6453 </sc-for> 6454 </div> 6455 <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(clamp(200px, 24vw, 250px), 1fr)); gap:14px;"> 6456 <sc-for list="{{ chLibrary }}" as="m" hint-placeholder-count="4"> 6457 <div data-reveal onClick="{{ m.open }}" style="background:#fff; border-radius:16px; overflow:hidden; box-shadow:0 6px 22px rgba(0,30,60,0.08); cursor:pointer; transition:transform 220ms, box-shadow 220ms;" style-hover="transform:translateY(-4px); box-shadow:0 18px 44px rgba(0,30,60,0.17);"> 6458 <div style="position:relative; aspect-ratio:16/9;"> 6459 <div style="position:absolute; inset:0; background-image:url('{{ m.thumb }}'); background-size:cover; background-position:{{ m.thumbPosition }};"></div> 6460 <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,20,40,0) 55%, rgba(0,20,40,0.62));"></div> 6461 <span style="position:absolute; inset:0; display:grid; place-items:center;"><span style="width:40px; height:40px; border-radius:50%; background:rgba(255,255,255,0.18); backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,0.4); display:grid; place-items:center; color:#fff; font-size:14px; padding-left:3px;"><i class="dc-lucide dc-lucide-play" aria-hidden="true"></i></span></span> 6462 </div> 6463 <div style="padding:11px 13px;"><strong style="font-size:13.5px; font-weight:800; color:#112033; line-height:1.25; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">{{ m.title }}</strong></div> 6464 </div> 6465 </sc-for> 6466 </div> 6467 </div> 6468 </sc-if> 6469 </div> 6470 </div> 6471 </sc-if> 6472 6473 <!-- ============ REMATE DETALLE ? CATÃLOGO ============ --> 6474 <sc-if value="{{ isRosganResults }}" hint-placeholder-val="{{ false }}"> 6475 <main style="max-width:1240px;margin:0 auto;padding:26px clamp(14px,4vw,36px) 60px;"> 6476 <a class="rosgan-page-action" href="{{ resultsBackHref }}">â Volver al remate</a> 6477 <rosgan-auction-results auction-key="{{ resultsPageKey }}"></rosgan-auction-results> 6478 </main> 6479 </sc-if> 6480 <sc-if value="{{ isRemateDetail }}" hint-placeholder-val="{{ false }}"> 6481 <div> 6482 <sc-if value="{{ rdCatalogPage }}" hint-placeholder-val="false"> 6483 <!-- HERO --> 6484 <div style="position:relative; overflow:hidden; background:linear-gradient(120deg, #001d3d 0%, #0a3e78 65%, {{ rdColor }} 140%); color:#fff;"> 6485 <div style="position:absolute; right:-60px; top:-60px; width:280px; height:280px; border-radius:50%; background:radial-gradient(circle, rgba(255,255,255,0.08), transparent 70%);"></div> 6486 <div style="position:relative; max-width:1240px; margin:0 auto; padding:18px clamp(14px, 4vw, 36px) clamp(20px, 3vw, 30px);">
6487 <span onClick="{{ goRemates }}" style="display:inline-flex; align-items:center; gap:6px; font-size:13px; color:#c8d8ea; font-weight:600; cursor:pointer;" style-hover="color:#fff;"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver a remates</span> 6488 <div style="display:flex; align-items:center; gap:16px; flex-wrap:wrap; margin-top:16px;"> 6489 <span style="width:62px; height:62px; border-radius:16px; background:{{ rdLogoBg }}; border:1px solid {{ rdLogoBorder }}; display:grid; place-items:center; flex-shrink:0; box-shadow:0 10px 30px rgba(0,0,0,0.25); overflow:hidden;"> 6490 <sc-if value="{{ rdHasLogo }}" hint-placeholder-val="{{ false }}"><div style="width:38px; height:38px; background-image:url('{{ rdLogo }}'); background-size:contain; background-position:center; background-repeat:no-repeat;"></div></sc-if> 6491 <sc-if value="{{ rdNoLogo }}" hint-placeholder-val="{{ true }}"><span style="font-size:24px; font-weight:800; color:{{ rdColor }};">{{ rdInitial }}</span></sc-if> 6492 </span> 6493 <div style="flex:1; min-width:220px;"> 6494 <span style="display:inline-block; font-size:10.5px; font-weight:800; letter-spacing:0.1em; background:{{ rdColor }}; padding:4px 11px; border-radius:100px; margin-bottom:6px;">{{ rdType }}</span> 6495 <h1 style="font-size:clamp(23px, 3.4vw, 34px); font-weight:800; letter-spacing:-0.02em; margin:0; line-height:1.08;">{{ rdTitle }}</h1> 6496 <div style="font-size:13.5px; color:#c8d8ea; margin-top:5px;"><i class="dc-lucide dc-lucide-calendar" aria-hidden="true"></i> {{ rdDate }} · <i class="dc-lucide dc-lucide-map-pin" aria-hidden="true"></i> {{ rdLocation }} · {{ rdConsignataria }}</div> 6497 </div> 6498 <div class="rd-hero-actions" style="display:flex; gap:8px; margin-left:auto; align-self:flex-start;"> 6499 <button type="button" onClick="{{ shareCurrentRemate }}" disabled="{{ !rdShareReady }}" style="display:inline-flex;align-items:center;gap:7px;min-height:35px;padding:9px 12px;border:1px solid rgba(255,255,255,.34);border-radius:11px;background:rgba(255,255,255,.12);color:#fff;font:750 12px inherit;cursor:pointer;"><img src="/assets/icons/lucide/share-2.svg" alt="" aria-hidden="true" width="15" height="15" style="filter:brightness(0) invert(1)">{{ rdShareLabel }}</button> 6500 <sc-if value="{{ rdCanAdminRemate }}" hint-placeholder-val="false"> 6501 <div style="display:contents;"> 6502 <button onClick="{{ editCurrentRosganRemate }}" style="display:inline-flex; align-items:center; gap:7px; border:1px solid rgba(255,255,255,.34); border-radius:11px; padding:9px 12px; background:rgba(255,255,255,.12); color:#fff; font:750 12px inherit; cursor:pointer;"><img src="assets/icons/lucide/pencil.svg" alt="" aria-hidden="true" width="15" height="15" style="display:block; width:15px; height:15px; filter:brightness(0) invert(1);">Editar remate</button> 6503 <button onClick="{{ deleteCurrentRosganRemate }}" disabled="{{ rdRemateDeleting }}" style="display:inline-flex; align-items:center; gap:7px; border:1px solid rgba(255,170,178,.42); border-radius:11px; padding:9px 12px; background:rgba(190,31,48,.24); color:#fff; font:750 12px inherit; cursor:pointer;"><img src="assets/icons/lucide/trash-2.svg" alt="" aria-hidden="true" width="15" height="15" style="display:block; width:15px; height:15px; filter:brightness(0) invert(1);">{{ rdDeleteRemateLabel }}</button> 6504 </div> 6505 </sc-if> 6506 </div> 6507 </div> 6508 <sc-if value="{{ rdHasCategoryHeads }}" hint-placeholder-val="false"> 6509 <div style="margin-top:14px;"> 6510 <div style="font-size:10.5px; font-weight:850; letter-spacing:.1em; color:#9fc0e2; margin-bottom:8px;">CABEZAS POR CATEGORÃA</div> 6511 <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(170px, 1fr)); gap:8px;"> 6512 <sc-for list="{{ rdCategoryHeads }}" as="category" hint-placeholder-count="6"> 6513 <button type="button" onClick="{{ category.pick }}" aria-pressed="{{ category.active }}" style="min-width:0; display:flex; align-items:center; justify-content:space-between; gap:10px; background:{{ category.bg }}; border:1px solid {{ category.border }}; border-radius:11px; padding:9px 11px; color:inherit; font:inherit; text-align:left; cursor:pointer;">
6514 <span style="min-width:0; font-size:10.5px; line-height:1.15; font-weight:750; color:#d9e7f5; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;" title="{{ category.label }}">{{ category.label }}</span> 6515 <strong style="flex:none; font-size:15px; font-weight:900; font-variant-numeric:tabular-nums;">{{ category.value }}</strong> 6516 </button> 6517 </sc-for> 6518 </div> 6519 </div> 6520 </sc-if> 6521 </div> 6522 </div> 6523 6524 <div style="max-width:1240px; margin:0 auto; padding:0 clamp(14px, 4vw, 36px);"> 6525 <a class="rosgan-page-action" href="{{ rdResultsHref }}" style="display:inline-flex;margin:20px 0 6px;"><img src="/assets/icons/lucide/chart-no-axes-combined.svg" alt="">Resultados</a> 6526 <!-- TOOLBAR --> 6527 <div class="rd-responsive-toolbar" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center; position:sticky; top:{{ poolBarTop }}; z-index:30; background:rgba(244,247,251,0.94); backdrop-filter:blur(12px); padding:14px 0; border-bottom:1px solid #e3eaf2;"> 6528 <div class="rd-search-field" style="flex:1;
6528 min-width:200px; display:flex; align-items:center; gap:10px; background:#fff; border:1px solid #e3eaf2; border-radius:12px; padding:11px 14px; box-shadow:0 4px 14px rgba(0,30,60,0.05);"> 6529 <span style="color:#8593a6; font-size:15px;"><i class="dc-lucide dc-lucide-search" aria-hidden="true"></i></span> 6530 <input value="" data-dc-attr-value="{{ rdQuery }}" onInput="{{ onRdQuery }}" placeholder="Buscar por lote, certificado, categorÃa, raza o localidadâ¦" style="border:none; outline:none; font-size:14px; font-family:inherit; width:100%; background:none; color:#112033;"> 6531 </div> 6532 <button onClick="{{ toggleRdFilters }}" aria-expanded="{{ rdFiltersOpen }}" aria-controls="remate-lot-filters" style="display:flex;align-items:center;gap:8px;border:1px solid #cbdff1;border-radius:100px;background:white;color:#003767;padding:12px 16px;font:700 13px inherit;cursor:pointer;"><img src="/assets/icons/lucide/sliders-horizontal.svg" width="16" height="16" alt="">{{ rdFilterButton }}</button> 6533 <sc-if value="{{ rdCatalogPublished }}"><button type="button" class="remate-list-catalog rd-catalog-cta" onClick="{{ openRdCatalog }}"><img src="assets/icons/lucide/file-text.svg" alt="" aria-hidden="true"><span>Catálogo</span></button></sc-if> 6534 <sc-if value="{{ rdCompanyCatalogAvailable }}"><button type="button" class="remate-list-catalog rd-catalog-cta rd-company-catalog" style="--company-color:{{ rdCompanyCatalogColor }}" onClick="{{ openRdCompanyCatalog }}"><img src="assets/icons/lucide/file-text.svg" alt=""><span>Catálogo por consignataria</span></button></sc-if> 6535 <sc-if value="{{ rdCanViewAuditor }}" hint-placeholder-val="false"> 6536 <button class="rosgan-auditor-entry" onClick="{{ openRdAuditor }}"><img src="/assets/icons/lucide/clipboard-list.svg" width="17" height="17" alt="" aria-hidden="true" style="display:block;flex-shrink:0;filter:brightness(0) invert(1);">{{ rdAuditorLabel }}</button> 6537 </sc-if> 6538 <sc-if value="{{ rdCanManageSalesOrder }}" hint-placeholder-val="false"> 6539 <button onClick="{{ openRdSalesOrder }}" style="display:inline-flex; align-items:center; justify-content:center; gap:8px; min-height:42px; border:1px solid #0a5ca8; border-radius:12px; padding:10px 14px; background:#0a5ca8; color:#fff; font:800 13px inherit; cursor:pointer; box-shadow:0 7px 18px rgba(10,92,168,.2);"><img src="assets/icons/lucide/list-ordered.svg" alt="" aria-hidden="true" width="16" height="16" style="filter:brightness(0) invert(1);">Orden de venta</button> 6540 </sc-if> 6541 <sc-if value="{{ rdShowOperations }}" hint-placeholder-val="false"> 6542 <div class="rosgan-ops-toolbar"> 6543 <sc-if value="{{ rdCanManageOperations }}" hint-placeholder-val="false"> 6544 <button onClick="{{ openRdCommercial }}"><img src="/assets/icons/lucide/badge-dollar-sign.svg" alt="" aria-hidden="true">Comercial</button> 6545 </sc-if> 6546 <sc-if value="{{ rdCanViewStats }}" hint-placeholder-val="false"> 6547 <button onClick="{{ openRdStats }}"><img src="/assets/icons/lucide/chart-no-axes-combined.svg" alt="">EstadÃsticas</button> 6548 </sc-if> 6549 <button onClick="{{ openRdPrices }}"><img src="/assets/icons/lucide/chart-no-axes-combined.svg" alt="" aria-hidden="true">Precios</button> 6550 <sc-if value="{{ rdCanManageOperations }}" hint-placeholder-val="false"> 6551 <button onClick="{{ openRdTv }}"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true">TV</button> 6552 <button onClick="{{ openRdTvConfig }}"><img src="/assets/icons/lucide/settings.svg" alt="" aria-hidden="true">Config. TV</button> 6553 </sc-if> 6554 </div> 6555 </sc-if> 6556 <div class="rd-view-switch" style="display:flex; background:#fff; border:1px solid #e3eaf2; border-radius:12px; padding:4px; gap:2px;"> 6557 <span onClick="{{ setGridView }}" title="Grilla" style="width:38px; height:34px; border-radius:9px; display:grid; place-items:center; cursor:pointer; font-size:15px; background:{{ gridViewBg }}; color:{{ gridViewColor }};"><i class="dc-lucide dc-lucide-grid" aria-hidden="true"></i></span>
6558 <span onClick="{{ setListView }}" title="Lista" style="width:38px; height:34px; border-radius:9px; display:grid; place-items:center; cursor:pointer; font-size:15px; background:{{ listViewBg }}; color:{{ listViewColor }};"><i class="dc-lucide dc-lucide-list" aria-hidden="true"></i></span> 6559 </div> 6560 </div> 6561 <sc-if value="{{ rdFiltersOpen }}"> 6562 <section id="remate-lot-filters" class="rd-filter-panel" aria-label="Filtros de lotes"> 6563 <div class="rd-filter-head"> 6564 <div class="rd-filter-title"><span><img src="/assets/icons/lucide/list-filter.svg" alt=""></span><strong>Filtrar lotes</strong><sc-if value="{{ rdHasActiveFilters }}"><span class="rd-filter-count">{{ rdActiveFilterCount }}</span></sc-if></div> 6565 <button type="button" class="rd-filter-clear" onClick="{{ clearRdFilters }}"><img src="/assets/icons/lucide/x.svg" alt="">Limpiar filtros</button> 6566 </div> 6567 <div class="rd-filter-grid"> 6568 <sc-for list="{{ rdFilterGroups }}" as="group"> 6569 <article class="rd-filter-group"> 6570 <header class="rd-filter-group-head"><span class="rd-filter-group-icon"><img data-dc-attr-src="{{ group.icon }}" alt=""></span><strong>{{ group.label }}</strong></header> 6571 <div class="rd-filter-options"> 6572 <sc-for list="{{ group.options }}" as="option"> 6573 <button type="button" class="rd-filter-chip" onClick="{{ option.pick }}" aria-pressed="{{ option.active }}"> 6574 <sc-if value="{{ option.hasLogo }}"><span class="rd-filter-chip-logo" style="background:{{ option.logoBg }};"><img data-dc-attr-src="{{ option.logo }}" alt=""></span></sc-if> 6575 <span>{{ option.label }}</span><span class="rd-filter-chip-count">{{ option.count }}</span> 6576 </button> 6577 </sc-for> 6578 </div> 6579 </article> 6580 </sc-for> 6581 </div> 6582 </section> 6583 </sc-if> 6584 <sc-if value="{{ rdLoading }}" hint-placeholder-val="{{ false }}"> 6585 <section class="rd-lots-loading" role="status" aria-live="polite" aria-label="Cargando lotes"> 6586 <div class="rd-lots-loading-heading"><span class="rd-lots-spinner" aria-hidden="true"></span><div><strong>Cargando lotesâ¦</strong><span>Estamos preparando el catálogo del remate.</span></div></div> 6587 <div class="rd-lots-skeletons" aria-hidden="true"><div></div><div></div><div></div><div></div></div> 6588 </section> 6589 </sc-if> 6590 <sc-if value="{{ rdLoaded }}" hint-placeholder-val="{{ true }}"> 6591 <div style="display:flex; justify-content:space-between; align-items:baseline; padding:16px 2px 14px;"> 6592 <span style="font-size:13.5px; color:#65758a;"><strong style="color:#112033; font-variant-numeric:tabular-nums;">{{ rdCount }}</strong> de {{ rdTotal }} lotes</span> 6593 <sc-if value="{{ rdSourceNote }}" hint-placeholder-val="{{ false }}"><span style="font-size:12px; color:#8593a6;">{{ rdSourceNote }}</span></sc-if> 6594 </div> 6595 </sc-if> 6596 6597 <!-- GRID VIEW --> 6598 <sc-if value="{{ rdShowGrid }}" hint-placeholder-val="{{ true }}"> 6599 <div class="rd-lot-grid" style="display:grid; grid-template-columns:repeat(auto-fill, minmax(240px, 1fr)); gap:16px;"> 6600 <sc-for list="{{ rdLots }}" as="l" hint-placeholder-count="6"> 6601 <div class="rd-lot-card" onClick="{{ l.open }}" onMouseEnter="{{ l.hoverIn }}" onMouseLeave="{{ l.hoverOut }}" data-reveal style="background:#fff; border-radius:18px; overflow:hidden; box-shadow:0 8px 26px rgba(0,30,60,0.08); cursor:pointer; display:flex; flex-direction:column; transition:transform 240ms cubic-bezier(0.22,0.72,0.18,1), box-shadow 240ms;" style-hover="transform:translateY(-6px); box-shadow:0 26px 60px rgba(0,30,60,0.18);"> 6602 <div class="rd-lot-card-media" style="position:relative; height:160px; overflow:hidden;"> 6603 <div data-lot-img style="position:absolute; inset:0; background-color:{{ l.thumbBg }}; background-image:url('{{ l.thumb }}'); background-size:{{ l.thumbSize }}; background-repeat:no-repeat; background-position:center; transition:transform 600ms cubic-bezier(0.22,0.72,0.18,1);"></div> 6604 <sc-if value="{{ l.thumbnailPending }}" hint-placeholder-val="false"><div role="status" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:10px;background:linear-gradient(110deg,#eaf0f6,#dbe6f0,#eaf0f6);color:#526b83;font-size:12px;"><span class="rd-lots-spinner" aria-hidden="true"></span>Preparando miniaturaâ¦</div></sc-if> 6605 <sc-if value="{{ l.videoPreview }}" hint-placeholder-val="false"><video data-lot-preview data-src="{{ l.videoPreview }}" muted loop playsinline preload="none" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;pointer-events:none;transition:opacity 240ms;background:#001d3d;"></video></sc-if> 6606 <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,29,61,0.02) 45%, rgba(0,29,61,0.68));"></div> 6607 <sc-if value="{{ l.showNumber }}" hint-placeholder-val="false"><span style="position:absolute; top:10px; left:10px; background:{{ rdColor }}; color:#fff; font-size:12px; font-weight:800; padding:5px 12px; border-radius:100px;">{{ l.number }}</span></sc-if>
6608 <span style="position:absolute; top:10px; right:10px; background:rgba(255,255,255,0.92); color:#112033; font-size:10.5px; font-weight:700; padding:4px 10px; border-radius:8px;">Cert. {{ l.cert }}</span> 6609 <div class="rd-lot-card-caption" style="position:absolute; bottom:9px; left:12px; right:58px; color:#fff;"><div style="font-size:15px; font-weight:800; line-height:1.15;">{{ l.categoria }}</div><sc-if value="{{ l.showRaza }}" hint-placeholder-val="true"><div style="font-size:11.5px; color:#c8d8ea;">{{ l.raza }}</div></sc-if></div> 6610 <span class="rd-lot-card-brand" title="{{ l.consignataria }}" aria-label="{{ l.consignataria }}" style="position:absolute;right:10px;bottom:10px;display:grid;place-items:center;width:38px;height:38px;box-sizing:border-box;padding:5px;overflow:hidden;border:1px solid rgba(255,255,255,.5);border-radius:11px;background:{{ l.consignatariaLogoBg }};box-shadow:0 5px 16px rgba(0,29,61,.25);"><sc-if value="{{ l.hasConsignatariaLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ l.consignatariaLogo }}" alt="{{ l.consignataria }}" style="display:block;width:26px;height:26px;max-width:26px;max-height:26px;object-fit:contain;object-position:center;"></sc-if><sc-if value="{{ l.noConsignatariaLogo }}" hint-placeholder-val="false"><strong style="color:#fff;font-size:15px;line-height:1;">{{ l.consignatariaInitial }}</strong></sc-if></span> 6611 </div> 6612 <div class="rd-lot-card-body" style="padding:13px 15px; display:grid; grid-template-columns:1fr 1fr; gap:9px;"> 6613 <sc-for list="{{ l.facts }}" as="fct" hint-placeholder-count="4"> 6614 <div class="rd-lot-card-fact"><div style="font-size:10px; color:#8593a6; letter-spacing:0.04em;">{{ fct.label }}</div><div style="font-size:14px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ fct.value }}</div></div> 6615 </sc-for> 6616 <button type="button" class="rd-share-lot" onClick="{{ l.shareLot }}" disabled="{{ !l.shareReady }}" style="grid-column:1/-1;min-height:36px;display:flex;align-items:center;justify-content:center;gap:7px;border:1px solid #c7d9ec;border-radius:10px;background:#fff;color:#004779;padding:7px 10px;font:700 11px inherit;cursor:pointer;"><img src="/assets/icons/lucide/share-2.svg" alt="" width="14" height="14">{{ l.shareLabel }}</button><sc-if value="{{ l.showAlert }}" hint-placeholder-val="true"><button type="button" onClick="{{ l.toggleAlert }}" disabled="{{ l.alertDisabled }}" aria-pressed="{{ l.alertAriaPressed }}" title="Te avisamos por la app y por WhatsApp, si lo tenés habilitado" style="grid-column:1/-1;min-height:37px;margin-top:3px;display:flex;align-items:center;justify-content:center;gap:7px;border:1px solid {{ l.alertBorder }};border-radius:10px;background:{{ l.alertBg }};color:{{ l.alertColor }};font:800 11.5px inherit;cursor:pointer;transition:background 160ms,border-color 160ms,transform 160ms;" style-hover="transform:translateY(-1px);"><img data-dc-attr-src="{{ l.alertIcon }}" alt="" aria-hidden="true" width="15" height="15" style="display:block;width:15px;height:15px;">{{ l.alertLabel }}</button></sc-if> 6617 </div> 6618 </div> 6619 </sc-for> 6620 </div> 6621 </sc-if> 6622 6623 <!-- LIST VIEW --> 6624 <sc-if value="{{ rdShowList }}" hint-placeholder-val="{{ false }}"> 6625 <div style="background:#fff; border-radius:18px; box-shadow:0 8px 26px rgba(0,30,60,0.07); overflow:hidden;"> 6626 <sc-for list="{{ rdLots }}" as="l" hint-placeholder-count="6"> 6627 <div class="rd-responsive-lot-row" onClick="{{ l.open }}" data-reveal style="display:flex; align-items:center; gap:14px; padding:12px 16px; border-bottom:1px solid #eef3f9; cursor:pointer; transition:background 160ms;" style-hover="background:#f6f9fc;"> 6628 <sc-if value="{{ l.showNumber }}" hint-placeholder-val="false"><span class="rd-row-number" style="width:44px; height:44px; border-radius:10px; background:{{ rdColor }}; color:#fff; display:grid; place-items:center; font-size:12px; font-weight:800; flex-shrink:0; text-align:center; line-height:1.05;">{{ l.numberShort }}</span></sc-if> 6629 <div class="rd-row-thumb" style="width:64px; height:48px; border-radius:10px; background-color:{{ l.thumbBg }}; background-image:url('{{ l.thumb }}'); background-size:{{ l.thumbSize }}; background-repeat:no-repeat; background-position:center; flex-shrink:0;"></div>
6630 <span class="rd-row-brand" title="{{ l.consignataria }}" aria-label="{{ l.consignataria }}" style="display:grid;place-items:center;width:34px;height:34px;box-sizing:border-box;padding:5px;border-radius:10px;background:{{ l.consignatariaLogoBg }};box-shadow:0 4px 12px rgba(0,29,61,.14);flex:0 0 34px;"><sc-if value="{{ l.hasConsignatariaLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ l.consignatariaLogo }}" alt="{{ l.consignataria }}" style="display:block;max-width:100%;max-height:100%;object-fit:contain;"></sc-if><sc-if value="{{ l.noConsignatariaLogo }}" hint-placeholder-val="false"><strong style="color:#fff;font-size:13px;line-height:1;">{{ l.consignatariaInitial }}</strong></sc-if></span> 6631 <div class="rd-row-copy" style="flex:1; min-width:0;"><div style="font-size:15px; font-weight:700; color:#112033;">{{ l.categoria }}</div><div style="font-size:12px; color:#65758a;">{{ l.listMeta }}</div></div> 6632 <button type="button" class="rd-share-lot" onClick="{{ l.shareLot }}" disabled="{{ !l.shareReady }}" style="grid-column:1/-1;min-height:36px;display:flex;align-items:center;justify-content:center;gap:7px;border:1px solid #c7d9ec;border-radius:10px;background:#fff;color:#004779;padding:7px 10px;font:700 11px inherit;cursor:pointer;"><img src="/assets/icons/lucide/share-2.svg" alt="" width="14" height="14">{{ l.shareLabel }}</button><sc-if value="{{ l.showAlert }}" hint-placeholder-val="true"><button class="rd-row-alert" type="button" onClick="{{ l.toggleAlert }}" disabled="{{ l.alertDisabled }}" aria-pressed="{{ l.alertAriaPressed }}" title="Te avisamos por la app y por WhatsApp, si lo tenés habilitado" style="min-height:36px;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid {{ l.alertBorder }};border-radius:10px;padding:0 11px;background:{{ l.alertBg }};color:{{ l.alertColor }};font:800 11px inherit;cursor:pointer;"><img data-dc-attr-src="{{ l.alertIcon }}" alt="" aria-hidden="true" width="14" height="14" style="display:block;width:14px;height:14px;">{{ l.alertLabel }}</button></sc-if> 6633 <sc-if value="{{ l.showCabezas }}" hint-placeholder-val="true"><div class="rd-row-heads" style="text-align:right; min-width:70px;"><div style="font-size:10px; color:#8593a6;">CABEZAS</div><div style="font-size:16px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ l.cabezas }}</div></div></sc-if> 6634 <sc-if value="{{ l.showPeso }}" hint-placeholder-val="true"><div class="rd-row-weight" style="text-align:right; min-width:80px;"><div style="font-size:10px; color:#8593a6;">PESO PROM.</div><div style="font-size:16px; font-weight:800; color:#112033; font-variant-numeric:tabular-nums;">{{ l.peso }}</div></div></sc-if> 6635 </div> 6636 </sc-for> 6637 </div> 6638 </sc-if> 6639 <sc-if value="{{ rdEmpty }}" hint-placeholder-val="{{ false }}"> 6640 <div style="background:#fff; border:1px dashed #cfdbe9; border-radius:18px; padding:44px 24px; text-align:center; color:#65758a;"> 6641 <div style="font-size:16px; font-weight:700; color:#112033;">{{ rdEmptyTitle }}</div> 6642 <div style="font-size:13.5px; margin-top:4px;">{{ rdEmptyText }}</div> 6643 </div> 6644 </sc-if> 6645 <div style="height:40px;"></div> 6646 </div> 6647 6648 </sc-if> 6649 <!-- FICHA DE LOTE ROSGAN / HISTÃRICO --> 6650 <sc-if value="{{ rdLotPage }}" hint-placeholder-val="false"> 6651 <div class="rg-lot-page" style="background:#f4f7fb;box-sizing:border-box;"> 6652 <main style="width:100%; max-width:1240px; min-width:0; box-sizing:border-box; margin:0 auto; padding:26px clamp(14px,4vw,36px) 100px;"> 6653 <button onClick="{{ closeLightbox }}" style="display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;color:#0a5ca8;font:700 13px inherit;padding:0;cursor:pointer;"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i> Volver al catálogo</button> 6654 <header style="display:flex;justify-content:space-between;align-items:flex-end;
6654gap:14px;flex-wrap:wrap;margin:10px 0 18px;"> 6655 <div><div style="display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-bottom:5px;"><sc-if value="{{ lbHasNumber }}"><span style="display:inline-flex;align-items:center;min-height:26px;padding:0 11px;border-radius:999px;background:{{ rdColor }};color:#fff;font-size:11px;font-weight:900;">{{ lbNumber }}</span></sc-if><span style="color:#718196;font-size:12px;font-weight:800;">CERT. {{ lbCert }}</span></div><h1 style="margin:0;color:#112033;font-size:clamp(26px,3.4vw,36px);line-height:1.08;">{{ lbCategoria }}</h1><div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:8px;"><span style="display:inline-flex;align-items:center;gap:6px;min-height:34px;padding:0 10px;border:1px solid #cfe0ef;border-radius:9px;background:#edf5fb;color:#174c75;font-size:12px;font-weight:800;"><img src="assets/icons/lucide/map-pin.svg" alt="" aria-hidden="true" width="13" height="13" style="display:block;width:13px;height:13px;">{{ lbLocation }}</span></div></div> 6656 <nav class="rg-lot-navigation" aria-label="Navegar entre lotes"> 6657 <button type="button" onClick="{{ shareCurrentLot }}" disabled="{{ !lbShareReady }}"><img src="/assets/icons/lucide/share-2.svg" alt="" aria-hidden="true">{{ lbShareLabel }}</button> 6658 <sc-if value="{{ lbHasPreviousLot }}" hint-placeholder-val="false"><button type="button" onClick="{{ lbPreviousLot }}"><img src="assets/icons/lucide/arrow-left.svg" alt="" aria-hidden="true">Anterior lote</button></sc-if> 6659 <sc-if value="{{ lbHasNextLot }}" hint-placeholder-val="false"><button type="button" onClick="{{ lbNextLot }}">Siguiente lote<img src="assets/icons/lucide/arrow-right.svg" alt="" aria-hidden="true"></button></sc-if> 6660 </nav> 6661 </header> 6662 <div style="display:grid;grid-template-columns:{{ rdLotDetailCols }};gap:20px;align-items:start;min-width:0;"> 6663 <div style="display:grid;gap:12px;min-width:0;"> 6664 <div class="dc-cabana-video-frame"><sc-if value="{{ lbPrimaryVideoUrl }}" hint-placeholder-val="false"><video class="dc-cabana-lot-player" data-remate-lot-player="true" data-src="{{ lbPrimaryVideoUrl }}" data-dc-attr-src="{{ lbPrimaryVideoUrl }}" data-dc-attr-poster="{{ lbThumb }}" onLoadedMetadata="{{ lbVideoMeta }}" onTimeUpdate="{{ lbVideoTimeUpdate }}" onVolumeChange="{{ lbVideoVolumeChange }}" onPause="{{ lbVideoPause }}" onPlay="{{ lbVideoPlay }}" onPlaying="{{ lbVideoPlay }}" onEnded="{{ lbVideoEnded }}" autoplay muted loop preload="auto" playsinline style="display:block;width:100%;height:100%;object-fit:cover;background:#001d3d;"></video></sc-if><sc-if value="{{ lbNoPrimaryVideo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ lbThumb }}" alt="{{ lbCategoria }}" style="display:block;width:100%;height:100%;object-fit:cover;"></sc-if><div class="rg-video-certifications"><sc-if value="{{ lbShowVisec }}" hint-placeholder-val="false"><span class="rg-video-certification rg-video-visec" title="Apto VISEC"><img src="/assets/icons/visec-mark.svg" alt=""><span>VISEC</span></span></sc-if><sc-if value="{{ lbShowChina }}" hint-placeholder-val="false"><span class="rg-video-certification rg-video-china" title="Apto China"><img src="/assets/icons/china-flag.svg" alt=""><span>Apto China</span></span></sc-if></div><sc-if value="{{ lbHasConsignatariaLogo }}" hint-placeholder-val="false"><span class="rg-video-cons-brand" style="background:{{ lbConsignatariaColor }};" title="{{ lbConsignataria }}"><img data-dc-attr-src="{{ lbConsignatariaLogo }}" alt="{{ lbConsignataria }}"></span></sc-if></div> 6665 <sc-if value="{{ lbPrimaryVideoUrl }}" hint-placeholder-val="false"><div class="dc-video-controls" aria-label="Controles del video"><button class="dc-video-control-button" onClick="{{ lbTogglePlay }}" title="{{ lbPlayLabel }}" aria-label="{{ lbPlayLabel }}"><img data-dc-attr-src="{{ lbPlayIcon }}" alt=""></button><input class="dc-video-slider" type="range" min="0" max="100" step="0.1" value="{{ lbVideoProgress }}" onInput="{{ lbSeekVideo }}" onChange="{{ lbSeekVideo }}" aria-label="Avance del video"><span class="dc-video-time">{{ lbVideoTime }}</span><button class="dc-video-control-button" onClick="{{ lbFullscreen }}" title="Pantalla completa" aria-label="Pantalla completa"><img src="assets/icons/lucide/maximize-2.svg" alt=""></button></div></sc-if> 6666 <sc-for list="{{ lbDetailGroups }}" as="group" hint-placeholder-count="3"><section style="background:#fff;border:1px solid #e3eaf2;border-radius:17px;padding:18px;box-shadow:0 10px 28px rgba(0,30,60,.05);"><small style="display:block;color:#8593a6;font-weight:900;letter-spacing:.11em;margin-bottom:11px;">{{ group.label }}</small><div class="rg-detail-list"><sc-for list="{{ group.cards }}" as="data" hint-placeholder-count="4"><div class="dc-rosgan-field-card rg-detail-row"><sc-if value="{{ data.isBreed }}" hint-placeholder-val="false"><sc-if value="{{ lbHasBreedIcons }}" hint-placeholder-val="false"><span style="display:inline-flex;align-items:center;gap:2px;"><sc-for list="{{ lbBreedIcons }}" as="breedIcon" hint-placeholder-count="2"><span style="position:relative;display:grid;place-items:center;width:27px;height:29px;flex:none;"><img data-dc-attr-src="{{ breedIcon.icon }}" alt="" aria-hidden="true" width="25" height="25" style="display:block;width:25px;height:25px;object-fit:contain;"><span style="position:absolute;right:-1px;bottom:0;display:flex;gap:1px;"><sc-if value="{{ breedIcon.showBlack }}" hint-placeholder-val="false"><i aria-hidden="true" style="display:block;width:7px;height:7px;border-radius:50%;background:#17191d;border:1px solid #fff;"></i></sc-if><sc-if value="{{ breedIcon.showRed }}" hint-placeholder-val="false"><i aria-hidden="true" style="display:block;width:7px;height:7px;border-radius:50%;background:#a84735;border:1px solid #fff;"></i></sc-if></span></span></sc-for></span></sc-if></sc-if><sc-if value="{{ !data.isBreed }}"><span class="rg-detail-icon" aria-hidden="true"><i style="background-position:{{ data.iconX }}% {{ data.iconY }}%;"></i></span></sc-if><span class="dc-rosgan-field-copy"><small>{{ data.label }}</small><strong>{{ data.value }}</strong></span></div></sc-for></div></section></sc-for> 6667 <sc-if value="{{ lbHasObservations }}" hint-placeholder-val="false"><section style="background:#fff;border:1px solid #e3eaf2;border-radius:17px;padding:18px;box-shadow:0 10px 28px rgba(0,30,60,.05);"><small class="dc-rosgan-observations-head"><span class="rg-detail-icon" aria-hidden="true"><i style="background-position:71.428571% 100%;"></i></span>OBSERVACIONES</small><p style="margin:9px 0 0;color:#3d4c5f;font-size:14px;line-height:1.55;white-space:pre-wrap;overflow-wrap:anywhere;">{{ lbObservations }}</p></section></sc-if> 6668 </div> 6669 <aside style="position:{{ rdLotAsidePosition }};top:88px;display:grid;gap:12px;min-width:0;width:100%;"> 6670 <section style="border-radius:18px;padding:21px;color:#fff;background:linear-gradient(145deg,#002e5f,{{ rdColor }});box-shadow:0 18px 48px rgba(0,30,60,.2);"><small style="font-weight:900;letter-spacing:.1em;opacity:.75;">{{ lbNumber }}</small><strong style="display:block;margin-top:5px;font-size:27px;line-height:1.08;">{{ lbCategoria }}</strong><span style="display:block;margin-top:7px;color:rgba(255,255,255,.76);font-size:12px;line-height:1.4;">{{ lbRaza }}</span></section> 6671 <section style="display:flex;align-items:center;gap:12px;min-width:0;color:#fff;background:{{ lbConsignatariaColor }};border-radius:15px;padding:14px 15px;box-shadow:0 10px 26px {{ lbConsignatariaShadow }};"><span class="dc-consignataria-logo-box" style="width:42px;height:42px;flex:0 0 42px;"><sc-if value="{{ lbHasConsignatariaLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ lbConsignatariaLogo }}" alt="{{ lbConsignataria }}"></sc-if><sc-if value="{{ lbNoConsignatariaLogo }}" hint-placeholder-val="false"><strong style="font-size:18px;">{{ lbConsignatariaInitial }}</strong></sc-if></span><span style="min-width:0;"><small style="display:block;color:rgba(255,255,255,.78);font-weight:800;letter-spacing:.07em;">CONSIGNATARIA</small><strong style="display:block;color:#fff;line-height:1.2;">{{ lbConsignataria }}</strong></span></section> 6672 <sc-if value="{{ lbHasRepresentative }}" hint-placeholder-val="false"><section class="dc-lot-contact-card"> 6673 <div class="dc-lot-contact-head"><span class="dc-lot-contact-avatar {{ lbRepresentativePhotoClass }}" style="background:{{ lbRepresentativeAvatarBg }};"><sc-if value="{{ lbRepresentativeHasAvatar }}"><img data-dc-attr-src="{{ lbRepresentativeAvatar }}" onError="{{ lbRepresentativeAvatarError }}" alt="" style="width:{{ lbRepresentativeAvatarSize }};height:{{ lbRepresentativeAvatarSize }};object-fit:{{ lbRepresentativeAvatarFit }};"></sc-if></span><span class="dc-lot-contact-copy"><small>REPRESENTANTE COMERCIAL</small><strong>{{ lbRepresentativeName }}</strong><sc-if value="{{ lbRepresentativeHasPhone }}"><span>{{ lbRepresentativePhone }}</span></sc-if></span></div> 6674 <sc-if value="{{ lbRepresentativeHasContact }}"><div class="dc-lot-contact-actions"><sc-if value="{{ lbRepresentativeHasWhatsapp }}"><a class="dc-contact-whatsapp" data-dc-attr-href="{{ lbRepresentativeWhatsappHref }}" target="_blank" rel="noopener noreferrer"><img src="assets/icons/brands/whatsapp.svg" alt="">WhatsApp</a></sc-if><sc-if value="{{ lbRepresentativeHasPhone }}"><a class="dc-contact-call" data-dc-attr-href="{{ lbRepresentativePhoneHref }}"><img src="assets/icons/lucide/phone.svg" alt="">Llamar</a></sc-if></div></sc-if> 6675 <sc-if value="{{ lbRepresentativeNoContact }}"><p style="margin:12px 0 0;color:#718196;font-size:12px;line-height:1.4;">Sin teléfono de contacto cargado.</p></sc-if> 6676 </section></sc-if> 6677 <section style="background:#fff;border:1px solid #e3eaf2;border-radius:15px;padding:15px;"><small style="display:block;color:#8593a6;font-weight:800;letter-spacing:.06em;">REMATE</small><strong style="display:block;margin-top:4px;color:#112033;line-height:1.25;">{{ rdTitle }}</strong><span style="display:block;margin-top:5px;color:#718196;font-size:11px;line-height:1.4;">{{ rdDate }}</span></section> 6678 </aside> 6679 </div> 6680 </main> 6681 </div> 6682 </sc-if> 6683 6684 <!-- LOT LIGHTBOX LEGACY --> 6685 <sc-if value="{{ rdLightbox }}" hint-placeholder-val="{{ false }}"> 6686 <div onClick="{{ closeLightbox }}" style="position:fixed; inset:0; z-index:80; background:rgba(0,20,45,0.6); backdrop-filter:blur(6px); display:grid; place-items:center; padding:20px; animation:pageIn 240ms both;"> 6687 <div onClick="{{ stopProp }}" style="background:#fff; border-radius:22px; overflow:hidden; max-width:760px; width:100%; max-height:90vh; overflow-y:auto; box-shadow:0 40px 100px rgba(0,20,45,0.4); animation:floatIn 320ms cubic-bezier(0.22,0.72,0.18,1) both;"> 6688 <div style="position:relative; aspect-ratio:16/10;"> 6689 <div style="position:absolute; inset:0; background-image:url('{{ lbThumb }}'); background-size:cover; background-position:center;"></div> 6690 <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,29,61,0.05) 50%, rgba(0,29,61,0.7));"></div>
6691 <span onClick="{{ closeLightbox }}" style="position:absolute; top:12px; right:12px; width:34px; height:34px; border-radius:50%; background:rgba(0,29,61,0.7); color:#fff; display:grid; place-items:center; cursor:pointer; font-size:16px;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></span> 6692 <span style="position:absolute; top:12px; left:12px; background:{{ rdColor }}; color:#fff; font-size:12px; font-weight:800; padding:6px 14px; border-radius:100px;">{{ lbNumber }}</span> 6693 <div style="position:absolute; bottom:14px; left:16px; right:16px; color:#fff;"><div style="font-size:22px; font-weight:800; line-height:1.1;">{{ lbCategoria }}</div><div style="font-size:13px; color:#c8d8ea; margin-top:2px;">Cert. {{ lbCert }} · {{ lbRaza }}</div></div> 6694 </div> 6695 <div style="padding:20px 22px;"> 6696 <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(120px, 1fr)); gap:10px;"> 6697 <sc-for list="{{ lbFacts }}" as="d" hint-placeholder-count="4"> 6698 <div style="background:#f6f9fc; border-radius:12px; padding:12px 14px;"><div style="font-size:10.5px; color:#8593a6; letter-spacing:0.04em;">{{ d.label }}</div><div style="font-size:16px; font-weight:800; color:#112033; margin-top:2px;">{{ d.value }}</div></div> 6699 </sc-for> 6700 </div> 6701 <sc-if value="{{ lbCanEditRosganLot }}" hint-placeholder-val="false"><button type="button" onClick="{{ editRosganLot }}" style="margin-top:16px;border:1px solid #b8d3ea;border-radius:11px;background:#edf6ff;color:#07518f;padding:11px 15px;font:800 13px inherit;cursor:pointer;"><i class="dc-lucide dc-lucide-pencil" aria-hidden="true"></i> Editar lote cargado</button></sc-if> 6702 <sc-if value="{{ lbRosganVideoManager }}" hint-placeholder-val="false"> 6703 <section style="margin-top:18px; padding-top:18px; border-top:1px solid #e5edf5;"> 6704 <div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; flex-wrap:wrap;"> 6705 <div><div style="font-size:10.5px; letter-spacing:.12em; color:#0a5ca8; font-weight:850;">VIDEOS DEL LOTE</div><h3 style="font-size:18px; color:#112033; margin:3px 0 0;">{{ lbVideoTitle }}</h3><p style="font-size:12px; color:#65758a; margin:4px 0 0;">Hasta 10 videos por vez · Máximo 1 GB cada uno.</p></div> 6706 <sc-if value="{{ lbCanManageVideos }}" hint-placeholder-val="false"><label style="display:inline-flex; align-items:center; gap:8px; border-radius:11px; padding:10px 14px; background:#0a5ca8; color:#fff; font-size:12.5px; font-weight:800; cursor:pointer; box-shadow:0 8px 18px rgba(10,92,168,.2);"><input type="file" multiple accept="video/*,.mp4,.mov,.m4v,.webm,.avi,.mpeg,.mpg" disabled="{{ lbVideoUploadBusy }}" onChange="{{ uploadRosganLotVideos }}" style="display:none;"><i class="dc-lucide dc-lucide-upload-cloud" aria-hidden="true"></i>Agregar videos</label><a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guÃa en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">â</span></a></sc-if> 6707 </div> 6708 <sc-if value="{{ lbVideoHasError }}" hint-placeholder-val="false"><div style="margin-top:12px; padding:10px 12px; border-radius:10px; background:#fff3f4; border:1px solid #efc6ca; color:#a32934; font-size:12.5px;">{{ lbVideoError }}</div></sc-if> 6709 <sc-if value="{{ lbVideoHasUploads }}" hint-placeholder-val="false"><div style="display:grid; gap:8px; margin-top:13px;"><sc-for list="{{ lbVideoUploads }}" as="u" hint-placeholder-count="2"><div style="border:1px solid #dce7f2; border-radius:11px; padding:10px 12px; background:#f8fbfe;"><div style="display:flex; justify-content:space-between; gap:12px; font-size:12px;"><strong style="color:#26384d; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ u.name }}</strong><span style="color:{{ u.color }}; flex:none;">{{ u.status }}</span></div><div style="height:5px; border-radius:10px; overflow:hidden; background:#e1eaf3; margin-top:8px;"><span style="display:block; width:{{ u.progress }}%; height:100%; border-radius:inherit; background:{{ u.bar }}; transition:width .2s ease;"></span></div><div style="font-size:10.5px; color:#7b8ca1; margin-top:5px;">{{ u.size }}</div><sc-if value="{{ u.hasError }}"><div style="font-size:11.5px;color:#b42332;margin-top:7px;">{{ u.error }}</div></sc-if><sc-if value="{{ u.canRetry }}"><button type="button" onClick="{{ u.retry }}" style="margin-top:8px;border:1px solid #bbd1e8;border-radius:9px;padding:8px 11px;background:#fff;color:#0a5ca8;font-weight:700;">{{ u.retryLabel }}</button></sc-if></div></sc-for></div></sc-if> 6710 <sc-if value="{{ lbCanManageVideos }}" hint-placeholder-val="false"><div style="margin-top:16px;"><button class="rg-filming-guide" type="button" onClick="{{ prepareFieldLot }}" style="margin:0;"><img src="/assets/icons/lucide/camera.svg" alt="" aria-hidden="true" style="width:18px;height:18px;">Modo filmación sin internet</button><p style="max-width:600px;margin:8px 0 0;color:#60758c;font-size:12px;line-height:1.5;">Preparalo con conexión antes de salir. Los videos quedan guardados en este dispositivo y se suben cuando vuelva internet.</p></div></sc-if> 6711 <sc-if value="{{ lbVideoLoading }}" hint-placeholder-val="false"><div style="display:flex; align-items:center; justify-content:center; gap:8px; padding:22px; color:#65758a; font-size:13px;"><i class="dc-lucide dc-lucide-loader-circle" aria-hidden="true"></i>Cargando videosâ¦</div></sc-if> 6712 <sc-if value="{{ lbVideoHasItems }}" hint-placeholder-val="false"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:11px; margin-top:14px;"><sc-for list="{{ lbVideos }}" as="v" hint-placeholder-count="2"><article style="border:1px solid #dce7f2; border-radius:13px; overflow:hidden; background:#fff;"><sc-if value="{{ v.hasUrl }}" hint-placeholder-val="true"><video controls preload="metadata" data-dc-attr-src="{{ v.url }}" style="width:100%; aspect-ratio:16/9; display:block; background:#061b32;"></video></sc-if><sc-if value="{{ v.noUrl }}" hint-placeholder-val="false"><div style="aspect-ratio:16/9; display:grid; place-items:center; background:#eef4fa; color:#0a5ca8;"><i class="dc-lucide dc-lucide-file-video" aria-hidden="true" style="font-size:28px;"></i></div></sc-if><div style="display:flex; align-items:center; gap:8px; padding:10px 11px;"><div style="min-width:0; flex:1;"><strong style="display:block; font-size:12px; color:#26384d; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ v.name }}</strong><span style="display:block; font-size:10.5px; color:#7b8ca1; margin-top:2px;">{{ v.size }}</span></div><sc-if value="{{ v.canDelete }}" hint-placeholder-val="false"><button onClick="{{ v.remove }}" disabled="{{ v.deleting }}" title="Eliminar video" style="width:32px; height:32px; border-radius:9px; border:1px solid #f0c8cc; background:#fff5f5; color:#ba2d3a; display:grid; place-items:center; cursor:pointer; flex:none;"><i class="dc-lucide dc-lucide-trash-2" aria-hidden="true"></i></button></sc-if></div></article></sc-for></div></sc-if> 6713 <sc-if value="{{ lbVideoEmpty }}" hint-placeholder-val="false"><div style="margin-top:14px; border:1px dashed #cbd9e7; border-radius:13px; padding:24px; text-align:center; color:#65758a; font-size:12.5px;"><i class="dc-lucide dc-lucide-video" aria-hidden="true" style="display:block; font-size:25px; color:#0a5ca8; margin-bottom:7px;"></i>Este lote todavÃa no tiene videos.</div></sc-if> 6714 </section> 6715 </sc-if> 6716 </div> 6717 </div> 6718 </div> 6719 </sc-if> 6720 </div> 6721 </sc-if> 6722 6723 <!-- CONFIGURACIÃN DE REMATE --> 6724 <sc-if value="{{ isRemateConfig }}" hint-placeholder-val="false"> 6725 <main style="max-width:1080px; margin:0 auto; padding:28px clamp(14px,4vw,36px) 100px; animation:pageIn 360ms both;"> 6726 <div style="display:flex; align-items:flex-end; justify-content:space-between; gap:18px; flex-wrap:wrap; margin-bottom:22px;"> 6727 <div><button onClick="{{ closeRemateConfig }}" style="display:inline-flex;align-items:center;gap:6px;border:0; background:transparent; color:#0a5ca8; font:700 13px inherit; padding:0; cursor:pointer;"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver a Cabañas</button><div style="font-size:11px; letter-spacing:.14em; color:#0a5ca8; font-weight:800; margin-top:14px;">ROSGAN - CABAÃAS</div><h1 style="font-size:clamp(26px,3.4vw,34px); margin:5px 0 3px; color:#112033;">{{ remateConfigTitle }}</h1><p style="margin:0; color:#65758a; font-size:13.5px;">{{ remateConfigSubtitle }}</p></div> 6728 <sc-if value="{{ rcDetailsMode }}"> <div style="display:flex; gap:9px; flex-wrap:wrap; justify-content:flex-end;"><button onClick="{{ resetRemateConfig }}" disabled="{{ remateConfigBusy }}" style="border:1px solid #dce5ef; border-radius:11px; padding:11px 16px; background:#fff; color:#3d4c5f; font:700 13px inherit; cursor:pointer; opacity:{{ remateConfigActionOpacity }};">Restablecer</button><button onClick="{{ saveRemateDraft }}" disabled="{{ remateConfigBusy }}" style="border:1px solid #b9cce0; border-radius:11px; padding:11px 16px; background:#edf5fc; color:#00457f; font:800 13px inherit; cursor:pointer; opacity:{{ remateConfigActionOpacity }};">{{ remateConfigDraftLabel }}</button><button onClick="{{ publishRemateConfig }}" disabled="{{ remateConfigBusy }}" style="border:0; border-radius:11px; padding:11px 18px; background:{{ remateConfigSaveBg }}; color:#fff; font:800 13px inherit; cursor:pointer; box-shadow:0 7px 18px rgba(230,57,70,.22); opacity:{{ remateConfigActionOpacity }};">{{ remateConfigPublishLabel }}</button></div></sc-if><sc-if value="{{ rcSchemaMode }}"><button type="button" onClick="{{ rcSaveSchema }}" disabled="{{ remateConfigBusy }}" style="border:0;border-radius:11px;padding:12px 18px;background:#0a5ca8;color:white;font:800 13px inherit;cursor:pointer">Guardar formulario de lotes</button></sc-if> 6729 </div> 6730 <sc-if value="{{ rcHasSavedRemate }}"><nav aria-label="Configuración del remate" style="display:flex;gap:10px;margin-bottom:20px;flex-wrap:wrap">
6730<button type="button" onClick="{{ rcShowDetails }}" aria-pressed="{{ rcDetailsMode }}" style="padding:11px 16px;border:1px solid #cbdced;border-radius:10px;background:{{ rcDetailsTabBg }};color:{{ rcDetailsTabColor }};font:700 13px inherit;cursor:pointer">Datos del remate</button><button type="button" onClick="{{ rcShowSchema }}" aria-pressed="{{ rcSchemaMode }}" style="padding:11px 16px;border:1px solid #cbdced;border-radius:10px;background:{{ rcSchemaTabBg }};color:{{ rcSchemaTabColor }};font:700 13px inherit;cursor:pointer">Formulario de lotes</button></nav></sc-if> 6731 <sc-if value="{{ rcDetailsMode }}"> 6732 <section class="cb-catalog-access-setting"><div><h2>Acceso al catálogo de lotes</h2><p>Activado: requiere iniciar sesión o registrarse para buscar y ver lotes. Desactivado: acceso público, con visitas de usuarios e invitados.</p><strong>{{ rcCatalogAccessLabel }}</strong></div><button type="button" role="switch" aria-label="Requerir registro para ver los lotes" aria-checked="{{ rcCatalogPrivate }}" onClick="{{ rcToggleCatalogPrivate }}" class="cb-catalog-access-switch" style="background:{{ rcCatalogSwitchBg }}"><span style="transform:translateX({{ rcCatalogSwitchOffset }});"></span></button></section> 6733 <sc-for list="{{ remateConfigSections }}" as="section" hint-placeholder-count="4"> 6734 <section style="background:#fff; border:1px solid #e7edf4; border-radius:18px; padding:21px 22px; box-shadow:0 8px 26px rgba(0,30,60,.06); margin-bottom:16px;"> 6735 <div style="font-size:10.5px; letter-spacing:.13em; color:#8593a6; font-weight:900;">{{ section.step }}</div><h2 style="font-size:17px; margin:3px 0 16px; color:#112033;">{{ section.title }}</h2> 6736 <div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:14px;"> 6737 <sc-for list="{{ section.fields }}" as="field" hint-placeholder-count="5"><label style="display:flex; flex-direction:column; gap:6px; font-size:12px; font-weight:700; color:#3d4c5f;"><span>{{ field.label }}</span><sc-if value="{{ field.isText }}" hint-placeholder-val="true"><input value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" placeholder="{{ field.placeholder }}" style="width:100%; border:1px solid #dce5ef; border-radius:11px; padding:11px 13px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isDate }}" hint-placeholder-val="false"><input type="date" value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" style="width:100%; border:1px solid #dce5ef; border-radius:11px; padding:10px 13px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isTime }}" hint-placeholder-val="false"><input type="time" value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" style="width:100%; border:1px solid #dce5ef; border-radius:11px; padding:10px 13px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isPlace }}" hint-placeholder-val="false"><div style="position:relative;display:grid;gap:6px;"><span style="font-size:11px"><span style="display:inline-flex;gap:4px;align-items:center;font-weight:500;"><img src="assets/google-logo.webp" alt="" style="width:12px;height:12px;">Google</span></span><input id="rc-locality" role="combobox" aria-autocomplete="list" aria-controls="rc-place-options" aria-expanded="{{ rcPlacesExpanded }}" aria-activedescendant="{{ rcPlaceActiveId }}" value="" data-dc-attr-value="{{ rcLocality }}" onInput="{{ setRcLocality }}" onKeyDown="{{ rcPlaceKey }}" onFocus="{{ rcPlaceFocus }}" onBlur="{{ rcPlaceBlur }}" disabled="{{ rcLocalityDisabled }}" autocomplete="off" placeholder="{{ rcLocalityPlaceholder }}" style="min-width:0;width:100%;box-sizing:border-box;border:1px solid #cedbea;border-radius:12px;padding:12px 13px;font:inherit;background:#fff;"><sc-if value="{{ rcPlacesVisible }}"><div id="rc-place-options" role="listbox" aria-label="Localidades sugeridas" style="position:absolute;z-index:45;left:0;right:0;top:100%;max-height:240px;overflow:auto;padding:5px;border:1px solid #cedbea;border-radius:12px;background:#fff;box-shadow:0 12px 28px rgba(0,30,60,.16);"><sc-for list="{{ rcPlaces }}" as="place"><button type="button" role="option" id="{{ place.id }}" aria-selected="{{ place.selected }}" onPointerDown="{{ place.pickPointer }}" onClick="{{ place.pick }}" style="width:100%;padding:10px;border:0;border-radius:8px;background:{{ place.bg }};text-align:left;cursor:pointer;"><strong style="display:block;color:#123553;">{{ place.label }}</strong><small style="display:block;color:#60758c;margin-top:3px;">{{ place.address }} · {{ place.source }}</small></button></sc-for></div></sc-if><span role="status" style="font-size:11px;color:#60758c;">{{ rcPlaceStatus }}</span></div></sc-if><sc-if value="{{ field.isSelect }}" hint-placeholder-val="false"><select value="" data-dc-attr-value="{{ field.value }}" onChange="{{ field.on }}" style="width:100%; border:1px solid #dce5ef; border-radius:11px; padding:11px 13px; outline:0; background:#fff; font:14px inherit;"><sc-for list="{{ field.options }}" as="option" hint-placeholder-count="5"><option value="" data-dc-attr-value="{{ option.value }}">{{ option.label }}</option></sc-for></select></sc-if><sc-if value="{{ field.isSwitch }}" hint-placeholder-val="false"><button type="button" onClick="{{ field.toggle }}" style="display:flex; align-items:center; justify-content:space-between; gap:12px; width:100%; border:1px solid #dce5ef; border-radius:11px; padding:9px 12px; background:#f8fafc; color:#3d4c5f; font:800 13px inherit; cursor:pointer;"><span style="width:44px; height:24px; border-radius:100px; background:{{ field.switchTrack }}; position:relative;"><span style="position:absolute; width:20px; height:20px; border-radius:50%; background:#fff; top:2px; left:2px; transform:translateX({{ field.switchKnob }}); transition:transform 180ms;"></span></span><span>{{ field.switchState }}</span></button></sc-if><sc-if value="{{ field.help }}" hint-placeholder-val="false"><small style="font-weight:500; color:#8593a6; line-height:1.35;">{{ field.help }}</small></sc-if></label></sc-for> 6738 </div> 6739 </section> 6740 </sc-for> 6741 <section style="background:#fff; border:1px solid #e7edf4; border-radius:18px; padding:21px 22px; box-shadow:0 8px 26px rgba(0,30,60,.06); margin-bottom:16px;"> 6742 <div style="font-size:10.5px; letter-spacing:.13em; color:#8593a6; font-weight:900;">05</div><h2 style="font-size:17px; margin:3px 0 4px; color:#112033;">Cabañas participantes</h2><p style="margin:0 0 18px; color:#65758a; font-size:12.5px;">El logo es opcional. Si lo cargás, debe ser PNG de hasta 2 MB.</p> 6743 <sc-for list="{{ cabanaParticipantGroups }}" as="group" hint-placeholder-count="2"><div style="margin-top:18px;"><div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:10px;"><div><strong style="display:block; color:#112033; font-size:14px;">{{ group.title }}</strong><small style="color:#8593a6;">{{ group.help }}</small></div><button onClick="{{ group.add }}" style="display:inline-flex; align-items:center; gap:6px; border:1px solid #cfe0f5; border-radius:9px; padding:8px 11px; background:#f3f8ff; color:#0a5ca8; font:800 12px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Agregar cabaña</button></div><div style="display:grid; gap:10px;"><sc-for list="{{ group.rows }}" as="row" hint-placeholder-count="2"><div style="display:grid; grid-template-columns:58px minmax(0,1fr) 38px; gap:10px; align-items:center; border:1px solid #e3eaf2; border-radius:13px; padding:10px; background:#fafcfe;"><div style="width:54px; height:54px; border-radius:11px; border:1px solid #dce5ef; background:#fff; display:grid; place-items:center; overflow:hidden;"><sc-if value="{{ row.hasLogo }}" hint-placeholder-val="false"><img data-dc-attr-src="{{ row.logo }}" alt="Logo de {{ row.name }}" style="width:100%; height:100%; object-fit:contain; padding:5px; box-sizing:border-box;"></sc-if><sc-if value="{{ row.noLogo }}" hint-placeholder-val="true"><span style="color:#9fb0c3; font-size:10px; font-weight:900;">PNG</span></sc-if></div><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:8px; min-width:0;"><input value="" data-dc-attr-value="{{ row.name }}" onInput="{{ row.onName }}" placeholder="Nombre de la cabaña" style="width:100%; border:1px solid #dce5ef; border-radius:10px; padding:10px 12px; outline:0; font:13px inherit;"><label style="display:flex; align-items:center; justify-content:center; min-height:40px; border:1px dashed #9fbad6; border-radius:10px; padding:8px 10px; color:#0a5ca8; background:#f3f8ff; font:800 11px inherit; cursor:pointer; text-align:center; overflow:hidden;"><span style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ row.fileLabel }}</span><input type="file" accept="image/png,.png" onChange="{{ row.onFile }}" style="display:none;"></label></div><button onClick="{{ row.remove }}" title="Quitar cabaña" style="width:36px; height:36px; border:1px solid #f0c5ca; border-radius:9px; background:#fff; color:#c62f3d; font:900 18px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></div></sc-for></div></div></sc-for> 6744 </section> 6745 <section style="background:#fff; border:1px solid #e7edf4; border-radius:18px; padding:21px 22px; box-shadow:0 8px 26px rgba(0,30,60,.06); margin-bottom:16px;"> 6746 <div style="font-size:10.5px; letter-spacing:.13em; color:#8593a6; font-weight:900;">06</div><h2 style="font-size:17px; margin:3px 0 3px; color:#112033;">Catálogo PDF</h2><p style="margin:0 0 14px; color:#65758a; font-size:12.5px;">Adjuntá el catálogo oficial del remate, hasta 30 MB.</p><div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap; border:1px dashed #9fbad6; border-radius:12px; padding:12px; background:#f8fbff;"><div style="width:42px; height:42px; display:grid; place-items:center; border-radius:9px; background:#e63946; color:#fff; font:900 10px inherit;">PDF</div><div style="flex:1 1 190px; min-width:0;"><strong style="display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#112033; font-size:13px;">{{ remateCatalogName }}</strong><small style="color:#8593a6;">{{ remateCatalogMeta }}</small></div><label style="border:1px solid #cfe0f5; border-radius:9px; padding:8px 11px; background:#fff; color:#0a5ca8; font:800 11px inherit; cursor:pointer;">{{ remateCatalogAction }}<input type="file" accept="application/pdf,.pdf" onChange="{{ onRemateCatalogFile }}" style="display:none;"></label><sc-if value="{{ hasRemateCatalog }}" hint-placeholder-val="false"><button onClick="{{ removeRemateCatalog }}" title="Quitar catálogo" style="width:34px; height:34px; border:1px solid #f0c5ca; border-radius:9px; background:#fff; color:#c62f3d; font:900 16px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></sc-if></div> 6747 </section> 6748 <p style="font-size:13px;color:#65758a;margin:0 0 18px">Guardá el remate para configurar sus categorÃas y campos desde «Formulario de lotes».</p> 6749 </sc-if><sc-if value="{{ rcSchemaMode }}"> 6750 <section style="background:#fff; border:1px solid #e7edf4; border-radius:18px; padding:21px 22px; box-shadow:0 8px 26px rgba(0,30,60,.06); margin-bottom:16px;"> 6751 <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:14px; flex-wrap:wrap; margin-bottom:17px;"><div><div style="font-size:10.5px; letter-spacing:.13em; color:#8593a6; font-weight:900;">07</div><h2 style="font-size:17px; margin:3px 0 3px; color:#112033;">Formulario</h2><p style="margin:0; color:#65758a; font-size:12.5px;">Creá los campos que se solicitarán para cada categorÃa del remate.</p></div><button onClick="{{ addRemateCategoryForm }}" style="display:inline-flex; align-items:center; gap:6px; border:1px solid #cfe0f5; border-radius:10px; padding:9px 12px; background:#f3f8ff; color:#0a5ca8; font:800 12px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Agregar categorÃa</button></div> 6752 <div style="display:grid; gap:12px;"> 6753 <sc-for list="{{ remateCategoryForms }}" as="category" hint-placeholder-count="2"> 6754 <article style="border:1px solid #dce5ef; border-radius:13px; background:#fff; overflow:hidden;"> 6755 <div style="display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:10px 11px; background:#f7f9fc; border-bottom:1px solid #e5ebf2;">
6756 <span style="width:24px; height:24px; display:grid; place-items:center; border-radius:7px; background:#0a5ca8; color:#fff; font:900 11px inherit;">{{ category.number }}</span> 6757 <input value="" data-dc-attr-value="{{ category.category }}" onInput="{{ category.onCategory }}" placeholder="Nombre de la categorÃa" aria-label="CategorÃa {{ category.number }}" style="flex:1 1 250px; min-width:0; border:0; border-bottom:1px solid #c9d7e6; padding:6px 2px; background:transparent; outline:0; color:#112033; font:800 13px inherit;"> 6758 <span style="white-space:nowrap; color:#1f7a63; font:900 10px inherit;">{{ category.visibleLabel }}</span> 6759 <button onClick="{{ category.remove }}" title="Quitar categorÃa" aria-label="Quitar categorÃa" style="width:28px; height:28px; border:0; border-radius:7px; background:transparent; color:#c62f3d; font:900 17px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button> 6760 </div> 6761 <div style="display:grid; gap:9px; padding:10px;"> 6762 <sc-for list="{{ category.forms }}" as="subform" hint-placeholder-count="2"> 6763 <section aria-label="{{ subform.dropLabel }}" style="border:1px solid #e3eaf2; border-radius:11px; background:#fbfcfe; overflow:hidden;"> 6764 <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; padding:8px 9px; border-bottom:1px solid #e8edf3; background:#fff;"> 6765 <button draggable="false" title="Arrastrar para ordenar subformularios" aria-label="{{ subform.dragLabel }}" style="width:27px; height:27px; border:0; border-radius:7px; background:#eef4fa; display:grid; place-items:center; cursor:grab; user-select:none; touch-action:none;"><img src="assets/icons/lucide/grip-vertical.svg" alt="" draggable="false" style="width:15px; height:15px; pointer-events:none;"></button> 6766 <span style="color:#8593a6; font:900 9px inherit; letter-spacing:.04em;">SUB {{ subform.number }}</span> 6767 <input value="" data-dc-attr-value="{{ subform.name }}" onInput="{{ subform.onName }}" placeholder="Nombre del subformulario" aria-label="Nombre del subformulario" style="flex:1 1 210px; min-width:0; border:0; border-bottom:1px solid #d6e0eb; padding:6px 2px; outline:0; color:#112033; font:800 12px inherit;"> 6768 <button onClick="{{ subform.toggleActive }}" title="Activar o desactivar subformulario" style="display:inline-flex; align-items:center; gap:6px; border:0; background:transparent; color:#526276; font:800 10px inherit; cursor:pointer;"><span style="width:34px; height:18px; border-radius:100px; background:{{ subform.activeTrack }}; position:relative;"><span style="position:absolute; width:14px; height:14px; border-radius:50%; background:#fff; top:2px; left:2px; transform:translateX({{ subform.activeKnob }}); transition:transform 180ms;"></span></span>{{ subform.activeLabel }}</button> 6769 <button onClick="{{ subform.remove }}" title="Quitar subformulario" aria-label="Quitar subformulario" style="width:27px; height:27px; border:0; border-radius:7px; background:#fff4f5; color:#c62f3d; font:900 15px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button> 6770 </div> 6771 <sc-if value="{{ isDesktop }}" hint-placeholder-val="true"> 6772 <div style="max-width:100%; overflow-x:auto;"> 6773 <div style="min-width:750px;"> 6774 <div style="display:grid; grid-template-columns:28px minmax(190px,1fr) 125px 96px 120px 30px; gap:8px; align-items:center; padding:6px 9px; color:#8593a6; font:850 8px inherit; letter-spacing:.04em; text-transform:uppercase;"><span></span><span>Campo</span><span>Tipo</span><span>Tarjeta</span><span>Identificador</span><span></span></div> 6775 <sc-for list="{{ subform.fields }}" as="field" hint-placeholder-count="3"> 6776 <div aria-label="{{ field.dropLabel }}" style="display:grid; grid-template-columns:28px minmax(190px,1fr) 125px 96px 120px 30px; gap:8px; align-items:center; padding:7px 9px; border-top:1px solid #edf1f5;"> 6777 <button draggable="false" title="Arrastrar para ordenar campos" aria-label="{{ field.dragLabel }}" style="width:27px; height:30px; border:0; border-radius:7px; background:#eef4fa; display:grid; place-items:center; cursor:grab; user-select:none; touch-action:none;"><img src="assets/icons/lucide/grip-vertical.svg" alt="" draggable="false" style="width:15px; height:15px; pointer-events:none;"></button> 6778 <input value="" data-dc-attr-value="{{ field.name }}" onInput="{{ field.onName }}" placeholder="Nombre del campo" aria-label="Campo {{ field.number }}" style="min-width:0; border:1px solid #dbe4ee; border-radius:8px; padding:8px 9px; outline:0; font:12px inherit;"> 6779 <select value="" data-dc-attr-value="{{ field.type }}" onChange="{{ field.onType }}" aria-label="Tipo de campo {{ field.number }}" style="min-width:0; border:1px solid #dbe4ee; border-radius:8px; padding:8px 9px; background:#fff; outline:0; font:12px inherit;"><sc-for list="{{ field.typeOptions }}" as="option" hint-placeholder-count="5"><option value="" data-dc-attr-value="{{ option.value }}">{{ option.label }}</option></sc-for></select> 6780 <button onClick="{{ field.toggleCard }}" title="Mostrar en la tarjeta del lote" style="display:inline-flex; align-items:center; justify-content:center; gap:6px; border:1px solid {{ field.cardBorder }}; border-radius:8px; padding:7px 6px; background:{{ field.cardTrack }}; color:{{ field.cardText }}; font:800 10px inherit; cursor:pointer; opacity:{{ field.cardOpacity }};"><img src="assets/icons/lucide/layout-panel-top.svg" alt="" style="width:14px; height:14px; filter:{{ field.cardIconFilter }};">Tarjeta</button> 6781 <div><button onClick="{{ field.toggleIdentifier }}" title="Usar como identificador principal de la categorÃa" style="display:inline-flex; align-items:center; justify-content:center; gap:6px; border:1px solid {{ field.identifierBorder }}; border-radius:8px; padding:7px 6px; background:{{ field.identifierTrack }}; color:{{ field.identifierText }}; font:800 10px inherit; cursor:pointer;"><img src="assets/icons/lucide/key-round.svg" alt="" style="width:14px; height:14px; filter:{{ field.identifierIconFilter }};">Principal</button><button type="button" role="checkbox" aria-checked="{{ field.characteristic }}" onClick="{{ field.toggleCharacteristic }}" style="margin-top:5px;padding:7px;border:1px solid #cadbea;border-radius:8px;background:#f2f7fc;font:11px inherit;cursor:pointer;">CaracterÃstico: {{ field.characteristicLabel }}</button></div> 6782 <button onClick="{{ field.remove }}" title="Quitar campo" aria-label="Quitar campo {{ field.number }}" style="width:28px; height:28px; border:0; border-radius:7px; background:#fff4f5; color:#c62f3d; font:900 15px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button> 6783 <sc-if value="{{ field.isSelection }}" hint-placeholder-val="false"><input value="" data-dc-attr-value="{{ field.optionsText }}" onInput="{{ field.onOptions }}" placeholder="Opciones separadas por coma" aria-label="Opciones del campo {{ field.number }}" style="grid-column:1 / -1; min-width:0; border:1px solid #dbe4ee; border-radius:8px; padding:8px 9px; outline:0; font:12px inherit;"></sc-if> 6784 </div> 6785 </sc-for> 6786 </div> 6787 </div> 6788 </sc-if> 6789 <sc-if value="{{ isMobile }}" hint-placeholder-val="false"> 6790 <div style="display:grid; gap:8px; padding:8px;"> 6791 <sc-for list="{{ subform.fields }}" as="field" hint-placeholder-count="3"> 6792 <article aria-label="{{ field.dropLabel }}" style="min-width:0; display:grid; grid-template-columns:32px minmax(0,1fr) 30px; gap:8px; align-items:start; border:1px solid #e4ebf3; border-radius:10px; padding:9px; background:#fff;"> 6793 <button draggable="false" title="Arrastrar para ordenar campos" aria-label="{{ field.dragLabel }}" style="grid-row:1 / span 3; width:30px; height:100%; min-height:72px; border:0; border-radius:8px; background:#eef4fa; display:grid; place-items:center; cursor:grab; user-select:none; touch-action:none;"><img src="assets/icons/lucide/grip-vertical.svg" alt="" draggable="false" style="width:16px; height:16px; pointer-events:none;"></button> 6794 <input value="" data-dc-attr-value="{{ field.name }}" onInput="{{ field.onName }}" placeholder="Nombre del campo" aria-label="Campo {{ field.number }}" style="width:100%; min-width:0; box-sizing:border-box; border:1px solid #dbe4ee; border-radius:8px; padding:9px; outline:0; font:12px inherit;"> 6795 <button onClick="{{ field.remove }}" title="Quitar campo" aria-label="Quitar campo {{ field.number }}" style="width:30px; height:34px; border:0; border-radius:8px; background:#fff4f5; color:#c62f3d; font:900 15px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button> 6796 <select value="" data-dc-attr-value="{{ field.type }}" onChange="{{ field.onType }}" aria-label="Tipo de campo {{ field.number }}" style="grid-column:2 / -1; width:100%; min-width:0; box-sizing:border-box; border:1px solid #dbe4ee; border-radius:8px; padding:9px; background:#fff; outline:0; font:12px inherit;"><sc-for list="{{ field.typeOptions }}" as="option" hint-placeholder-count="5"><option value="" data-dc-attr-value="{{ option.value }}">{{ option.label }}</option></sc-for></select> 6797 <div style="grid-column:2 / -1; min-width:0; display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px;"> 6798 <button onClick="{{ field.toggleCard }}" title="Mostrar en la tarjeta del lote" style="min-width:0; display:inline-flex; align-items:center; justify-content:center; gap:5px; border:1px solid {{ field.cardBorder }}; border-radius:8px; padding:8px 5px; background:{{ field.cardTrack }}; color:{{ field.cardText }}; font:800 10px inherit; cursor:pointer; opacity:{{ field.cardOpacity }};"><img src="assets/icons/lucide/layout-panel-top.svg" alt="" style="width:14px; height:14px; filter:{{ field.cardIconFilter }};">Tarjeta</button> 6799 <div><button onClick="{{ field.toggleIdentifier }}" title="Usar como identificador principal de la categorÃa" style="min-width:0; display:inline-flex; align-items:center; justify-content:center; gap:5px; border:1px solid {{ field.identifierBorder }}; border-radius:8px; padding:8px 5px; background:{{ field.identifierTrack }}; color:{{ field.identifierText }}; font:800 10px inherit; cursor:pointer;"><img src="assets/icons/lucide/key-round.svg" alt="" style="width:14px; height:14px; filter:{{ field.identifierIconFilter }};">Principal</button><button type="button" role="checkbox" aria-checked="{{ field.characteristic }}" onClick="{{ field.toggleCharacteristic }}" style="margin-top:5px;padding:7px;border:1px solid #cadbea;border-radius:8px;background:#f2f7fc;font:11px inherit;cursor:pointer;">CaracterÃstico: {{ field.characteristicLabel }}</button></div> 6800 </div> 6801 <sc-if value="{{ field.isSelection }}" hint-placeholder-val="false"><input value="" data-dc-attr-value="{{ field.optionsText }}" onInput="{{ field.onOptions }}" placeholder="Opciones separadas por coma" aria-label="Opciones del campo {{ field.number }}" style="grid-column:2 / -1; width:100%; min-width:0; box-sizing:border-box; border:1px solid #dbe4ee; border-radius:8px; padding:9px; outline:0; font:12px inherit;"></sc-if> 6802 </article> 6803 </sc-for> 6804 </div> 6805 </sc-if> 6806 <button onClick="{{ subform.addField }}" style="margin:7px 9px 9px; border:0; border-radius:8px; padding:7px 9px; background:#eef5fc; color:#0a5ca8; font:800 11px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Campo</button> 6807 </section> 6808 </sc-for> 6809 <button onClick="{{ category.addSubform }}" style="justify-self:start; border:1px dashed #9fbad6; border-radius:8px; padding:7px 10px; background:#f8fbff; color:#0a5ca8; font:800 11px inherit; cursor:pointer;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i> Agregar subformulario</button> 6810 </div> 6811 </article> 6812 </sc-for> 6813 </div> 6814 </section> 6815 </sc-if><sc-if value="{{ rcDetailsMode }}"> 6816 <section style="background:#fff; border:1px solid #e7edf4; border-radius:18px; padding:21px 22px; box-shadow:0 8px 26px rgba(0,30,60,.06); margin-bottom:16px;"><div style="font-size:10.5px; letter-spacing:.13em; color:#8593a6; font-weight:900;">08</div><h2 style="font-size:17px; margin:3px 0 16px; color:#112033;">Opciones de visualización y carga</h2><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(245px,1fr)); gap:10px;"><sc-for list="{{ remateConfigOptions }}" as="option" hint-placeholder-count="6"><button onClick="{{ option.toggle }}" style="display:flex; align-items:center; gap:12px; text-align:left; border:1px solid {{ option.border }}; border-radius:12px; padding:12px 14px; background:{{ option.bg }}; color:#112033; cursor:pointer; font:700 13px inherit;"><span style="width:38px; height:22px; border-radius:100px; background:{{ option.track }}; position:relative; flex:none;"><span style="position:absolute; width:18px; height:18px; border-radius:50%; background:#fff; top:2px; left:2px; transform:translateX({{ option.knob }}); transition:transform 180ms;"></span></span>{{ option.label }}</button></sc-for></div></section> 6817 <section class="cb-contract-section"><h2>Importes y aprobadores de preofertas</h2><p>Los importes por categorÃa son opcionales. Podés usar las bases e incrementos individuales de cada lote. Para definir importes por categorÃa, guardá primero las categorÃas nuevas.</p> 6818 <sc-for list="{{ rcPricing }}" as="price"><div class="cb-contract-grid"><strong>{{ price.label }}</strong><label>Base<input type="number" min="0" step="0.01" disabled="{{ price.disabled }}" data-dc-attr-value="{{ price.base }}" onInput="{{ price.onBase }}"></label><label>Incremento<input type="number" min="0.01" step="0.01" disabled="{{ price.disabled }}" data-dc-attr-value="{{ price.increment }}" onInput="{{ price.onIncrement }}"></label></div></sc-for> 6819 <fieldset disabled="{{ rcApproversDisabled }}"><legend>
6819Usuarios aprobadores</legend><p>Con aprobación automática no es necesario seleccionar aprobadores.</p><p role="status">{{ rcApproversError }}</p><div class="cb-contract-options"><sc-for list="{{ rcApprovers }}" as="user"><label><input type="checkbox" checked="{{ user.checked }}" onChange="{{ user.toggle }}">{{ user.label }}</label></sc-for></div></fieldset> 6820 </section> 6821 <section style="background:linear-gradient(180deg,#f3f8ff,#fff); border:1px solid #cfe0f5; border-radius:18px; padding:21px 22px; box-shadow:0 8px 26px rgba(10,92,168,.07);"><div style="display:flex; justify-content:space-between; align-items:center; gap:15px; flex-wrap:wrap;"><div><div style="font-size:10.5px; letter-spacing:.13em; color:#0a5ca8; font-weight:900;">PREOFERTA</div><h2 style="font-size:17px; margin:3px 0 2px; color:#112033;">Configuración de preoferta</h2><p style="margin:0; color:#65758a; font-size:12.5px;">Reglas y ventana para ofertas anticipadas.</p></div><button onClick="{{ togglePreoffer }}" style="display:flex; align-items:center; gap:9px; border:1px solid {{ preofferBorder }}; border-radius:100px; padding:9px 13px; background:{{ preofferBg }}; color:{{ preofferColor }}; font:800 12px inherit; cursor:pointer;"><span style="width:8px; height:8px; border-radius:50%; background:{{ preofferDot }};"></span>{{ preofferLabel }}</button></div><sc-if value="{{ preofferEnabled }}" hint-placeholder-val="false"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:14px; margin-top:18px;"><sc-for list="{{ preofferFields }}" as="field" hint-placeholder-count="5"><label style="display:flex; flex-direction:column; gap:6px; font-size:12px; font-weight:700; color:#3d4c5f;"><span>{{ field.label }}</span><sc-if value="{{ field.isDate }}" hint-placeholder-val="true"><input type="date" value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" style="width:100%; border:1px solid #cfe0f5; border-radius:11px; padding:10px 13px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isTime }}" hint-placeholder-val="false"><input type="time" value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" style="width:100%; border:1px solid #cfe0f5; border-radius:11px; padding:10px 13px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isSelect }}" hint-placeholder-val="false"><select value="" data-dc-attr-value="{{ field.value }}" onChange="{{ field.on }}" style="width:100%; border:1px solid #cfe0f5; border-radius:11px; padding:11px 13px; outline:0; background:#fff; font:14px inherit;"><sc-for list="{{ field.options }}" as="option" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ option.value }}">{{ option.label }}</option></sc-for></select></sc-if></label></sc-for></div></sc-if></section> 6822 </sc-if> 6823 </main> 6824 </sc-if> 6825 6826 <!-- REMATES DE CABAÃA --> 6827 <sc-if value="{{ isCabanas }}" hint-placeholder-val="false"> 6828 <main style="max-width:1240px; margin:0 auto; padding:28px clamp(14px,4vw,36px) 100px; animation:pageIn 360ms both;"> 6829 <div style="display:flex; justify-content:space-between; align-items:flex-end; gap:16px; flex-wrap:wrap; margin-bottom:20px;"> 6830 <div><div style="font-size:11px; letter-spacing:0.14em; color:#0a5ca8; font-weight:800;">ROSGAN - REMATES DE CABAÃA</div><h1 style="font-size:clamp(27px,3.4vw,38px); margin:5px 0 4px; color:#112033;">Remates de cabaña</h1><p style="margin:0; color:#65758a; font-size:14px; max-width:650px;">Remates de cabaña creados y administrados desde Rosgan.</p></div> 6831 <div style="display:flex;align-items:center;gap:9px;flex-wrap:wrap;"><sc-if value="{{ showCreateRemate }}" hint-placeholder-val="false"><button onClick="{{ createCabanaRemate }}" style="display:inline-flex; align-items:center; gap:7px; border:0; border-radius:11px; padding:11px 16px; background:#e63946; color:#fff; font:800 13px inherit; cursor:pointer; box-shadow:0 7px 18px rgba(230,57,70,.22);"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>Crear remate</button></sc-if></div> 6832 </div> 6833 <sc-if value="{{ cabanasLoading }}" hint-placeholder-val="false">
6834 <div role="status" aria-live="polite" style="display:flex; align-items:center; gap:10px; margin:2px 0 17px; color:#52657b; font-size:13px; font-weight:800;"> 6835 <span style="width:18px; height:18px; border:3px solid #d7e4f0; border-top-color:#0a5ca8; border-radius:50%; animation:spin .8s linear infinite;"></span> 6836 Cargando remates de cabaña⦠6837 </div> 6838 <div class="cabana-auction-list" aria-label="Cargando remates"><sc-for list="{{ cabanaSkeletonCards }}" as="skeleton" hint-placeholder-count="6"><div class="cabana-list-skeleton"></div></sc-for></div> 6839 </sc-if> 6840 <sc-if value="{{ cabanasHasError }}" hint-placeholder-val="false"><div style="padding:34px; border:1px solid #f0c5ca; border-radius:16px; background:#fff; color:#9d2732; text-align:center;">{{ cabanasError }}</div></sc-if> 6841 <sc-if value="{{ hasCabanas }}" hint-placeholder-val="true"> 6842 <div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:20px;"><sc-for list="{{ cabanaBreedChips }}" as="chip" hint-placeholder-count="5"><button onClick="{{ chip.go }}" style="border:1px solid #dce5ef; background:{{ chip.bg }}; color:{{ chip.color }}; border-radius:100px; padding:9px 15px; font:700 13px inherit; cursor:pointer;">{{ chip.label }} <span style="opacity:.7; margin-left:4px;">{{ chip.count }}</span></button></sc-for></div> 6843 <div class="cabana-auction-list" aria-label="Remates de cabaña ordenados por fecha"> 6844 <sc-for list="{{ cabanaCards }}" as="card" hint-placeholder-count="6"> 6845 <article class="cabana-list-row" onClick="{{ card.openRow }}"> 6846 <button class="cabana-list-thumb {{ card.listThumbClass }}" onClick="{{ card.open }}" aria-label="Abrir remate {{ card.title }}"><img data-dc-attr-src="{{ card.listThumb }}" alt="" loading="lazy" onError="{{ card.listThumbError }}"></button> 6847 <div class="cabana-list-identity"><span class="cabana-list-status" style="background:{{ card.badgeBg }};">{{ card.badge }}</span><sc-if value="{{ card.prebidsActive }}" hint-placeholder-val="false"><span class="prebid-active-badge"><span class="prebid-active-dot" aria-hidden="true"></span>Preofertas activas</span></sc-if><button class="cabana-list-title" onClick="{{ card.open }}">{{ card.title }}</button><sc-if value="{{ card.hasLogos }}"><div class="cabana-list-participants {{ card.participantRollClass }}"><div class="cabana-list-participant-window" tabindex="0" aria-label="Participantes"><div class="cabana-list-participant-track" style="animation-duration:{{ card.participantRollDuration }};"><span>{{ card.participantNames }}</span><sc-if value="{{ card.participantRoll }}"><span aria-hidden="true">{{ card.participantNames }}</span></sc-if></div></div><sc-if value="{{ card.participantRoll }}"><button class="cabana-list-roll-toggle" onClick="{{ card.toggleListRoll }}" aria-label="Pausar desplazamiento de participantes" aria-pressed="false">â ¡</button></sc-if></div></sc-if></div> 6848 <div class="cabana-list-date"><img src="assets/icons/lucide/calendar-days.svg" alt=""><div><strong>{{ card.dateTime }}</strong><small>{{ card.location }}</small></div></div> 6849 <div class="cabana-list-consignor"><span style="background:{{ card.consBg }};"><sc-if value="{{ card.hasConsIcon }}"><img data-dc-attr-src="{{ card.consIcon }}" alt="" style="filter:{{ card.consIconFilter }};"></sc-if></span><div><small>CONSIGNATARIA</small><strong>{{ card.consignataria }}</strong></div></div> 6850 <div class="cabana-list-actions"> 6851 <sc-if value="{{ card.canEdit }}"><button onClick="{{ card.edit }}" title="Editar remate" aria-label="Editar {{ card.title }}"><img src="assets/icons/lucide/pencil.svg" alt=""></button></sc-if> 6852 <sc-if value="{{ card.canManageSalesOrder }}"><button onClick="{{ card.salesOrder }}" title="Orden de ventas" aria-label="Ventas de {{ card.title }}"><img src="assets/icons/lucide/list-ordered.svg" alt=""></button></sc-if> 6853 <sc-if value="{{ card.canCreateLot }}"><button class="cabana-list-create-lot" onClick="{{ card.createLot }}" title="Cargar lote en este remate" aria-label="Cargar lote en {{ card.title }}"><img src="assets/icons/lucide/plus.svg" alt=""><span>Cargar lote</span></button></sc-if> 6854 </div> 6855 </article> 6856 </sc-for> 6857 </div> </sc-if> 6858 <sc-if value="{{ cabanasEmpty }}" hint-placeholder-val="false"><div style="padding:46px 24px; border:1px dashed #cbd8e7; border-radius:18px; background:#fff; text-align:center;"><strong style="display:block; color:#112033;">TodavÃa no creaste remates de cabaña</strong><span style="font-size:13px; color:#65758a;">Los remates que agregues desde Rosgan aparecerán acá.</span></div></sc-if> 6859 </main> 6860 </sc-if> 6861 6862 <!-- CREAR / EDITAR LOTE DE CABAÃA --> 6863 <sc-if value="{{ isCabanaLotCreate }}" hint-placeholder-val="false"> 6864 <main style="max-width:1120px; margin:0 auto; padding:28px clamp(14px,4vw,36px) 110px; animation:pageIn 320ms both;"> 6865 <button onClick="{{ closeCabanaLotCreate }}" style="display:inline-flex;align-items:center;gap:6px;border:0; background:transparent; color:#0a5ca8; font:800 13px inherit; padding:0; cursor:pointer;"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> {{ cabanaLotBackLabel }}</button> 6866 <div style="display:flex; justify-content:space-between; align-items:flex-end; gap:16px; flex-wrap:wrap; margin:14px 0 20px;"><div><div style="font-size:10.5px; letter-spacing:.14em; color:#0a5ca8; font-weight:900;">ROSGAN - CABAÃAS</div><h1 style="margin:5px 0 4px; color:#112033; font-size:clamp(27px,3.5vw,38px);">{{ cabanaLotPageTitle }}</h1><p style="margin:0; color:#65758a; font-size:14px;">{{ cabanaLotRemateTitle }}</p></div><span style="padding:7px 12px; border-radius:100px; background:#e8f2fc; color:#0a5ca8; font-size:11px; font-weight:900;">{{ cabanaLotModeLabel }}</span></div> 6867 <sc-if value="{{ cabanaLotCreateLoading }}"><section style="min-height:220px; display:grid; place-items:center; gap:10px; border:1px solid #e3eaf2; border-radius:17px; background:#fff; color:#65758a;"><span class="dc-spinner"></span><strong>Cargando lote...</strong></section></sc-if> 6868 <sc-if value="{{ cabanaLotNoSchema }}" hint-placeholder-val="false"><section style="padding:38px 24px; border:1px dashed #b8cadc; border-radius:17px; background:#fff; text-align:center;"><strong style="display:block; color:#112033;">Este remate todavÃa no tiene formularios configurados</strong><span style="display:block; margin-top:5px; color:#65758a; font-size:13px;">Agregá categorÃas y subformularios desde el creador del remate de cabaña.</span></section></sc-if> 6869 <sc-if value="{{ cabanaLotHasSchema }}" hint-placeholder-val="true"> 6870 <div style="display:grid; grid-template-columns:{{ cabanaLotLayout }}; gap:18px; align-items:start;"> 6871 <div style="display:grid; gap:15px;"> 6872 <section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px; box-shadow:0 8px 26px rgba(0,30,60,.06);"><small style="color:#8593a6; font-weight:900; letter-spacing:.12em;">CABAÃA</small><h2 style="margin:3px 0 5px; font-size:18px; color:#112033;">¿A qué cabaña pertenece?</h2><p style="margin:0 0 14px; color:#65758a; font-size:12px;">Elegà una de las cabañas participantes de este remate.</p><sc-if value="{{ cabanaLotHasCabanas }}" hint-placeholder-val="true"><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:9px;"><sc-for list="{{ cabanaLotCabanas }}" as="cabana" hint-placeholder-count="3"><button onClick="{{ cabana.select }}" type="button" style="min-width:0; display:grid; grid-template-columns:52px minmax(0,1fr) 20px; gap:10px; align-items:center; text-align:left; border:1px solid {{ cabana.border }}; border-radius:12px; padding:10px; background:{{ cabana.bg }}; box-shadow:{{ cabana.shadow }}; cursor:pointer;"><span style="width:52px; height:44px; display:grid; place-items:center; border-radius:9px; background:#fff; overflow:hidden;"><sc-if value="{{ cabana.hasLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ cabana.logo }}" alt="{{ cabana.name }}" style="display:block; max-width:44px; max-height:36px; object-fit:contain;"></sc-if><sc-if value="{{ cabana.noLogo }}" hint-placeholder-val="false"><b style="color:{{ cabana.brand }}; font-size:15px;">{{ cabana.initial }}</b></sc-if></span><span style="min-width:0;"><b style="display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#112033; font-size:12px;">{{ cabana.name }}</b><em style="display:block; margin-top:3px; color:{{ cabana.brand }}; font-style:normal; font-size:9px; font-weight:900; letter-spacing:.08em; text-transform:uppercase;">{{ cabana.typeLabel }}</em></span><span style="width:19px; height:19px; display:grid; place-items:center; border-radius:50%; border:2px solid {{ cabana.checkBorder }}; background:{{ cabana.checkBg }}; color:#fff; font-size:11px; font-weight:900;">{{ cabana.check }}</span></button></sc-for></div></sc-if><sc-if value="{{ cabanaLotNoCabanas }}" hint-placeholder-val="false"><div style="padding:12px; border:1px solid #f1c5c9; border-radius:10px; background:#fff4f5; color:#a72c37; font-size:12px; font-weight:800;">Este remate no tiene cabañas participantes cargadas.</div></sc-if></section> 6873 <section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px; box-shadow:0 8px 26px rgba(0,30,60,.06);"><small style="color:#8593a6; font-weight:900; letter-spacing:.12em;">CATEGORÃA</small><h2 style="margin:3px 0 13px; font-size:18px; color:#112033;">Tipo de lote</h2><div style="display:flex; gap:8px; flex-wrap:wrap;"><sc-for list="{{ cabanaLotCategories }}" as="category" hint-placeholder-count="3"><button onClick="{{ category.select }}" style="border:1px solid {{ category.border }}; border-radius:100px; padding:9px 14px; background:{{ category.bg }}; color:{{ category.color }}; font:800 12px inherit; cursor:pointer;">{{ category.label }}</button></sc-for></div></section> 6874 <sc-for list="{{ cabanaLotSubforms }}" as="subform" hint-placeholder-count="3"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px; box-shadow:0 8px 26px rgba(0,30,60,.05);"><div style="display:flex; align-items:center; gap:9px; margin-bottom:15px;"><span style="width:28px; height:28px; display:grid; place-items:center; border-radius:8px; background:#e8f2fc; color:#0a5ca8; font-size:11px; font-weight:900;">{{ subform.number }}</span><h2 style="margin:0; color:#112033; font-size:17px;">{{ subform.name }}</h2></div><div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:13px;"><sc-for list="{{ subform.fields }}" as="field" hint-placeholder-count="4"><label style="display:flex; flex-direction:column; gap:6px; color:#3d4c5f; font-size:12px; font-weight:800;"><span>{{ field.label }} <sc-if value="{{ field.isPrimary }}" hint-placeholder-val="false"><em style="font-style:normal; padding:3px 6px; border-radius:100px; background:#fff0d6; color:#9a6500; font-size:9px;">IDENTIFICADOR</em></sc-if></span><sc-if value="{{ field.isText }}" hint-placeholder-val="true"><input value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" placeholder="{{ field.placeholder }}" style="width:100%; border:1px solid #d6e1ed; border-radius:10px; padding:11px 12px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isNumber }}" hint-placeholder-val="false"><input type="number" value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" placeholder="0" style="width:100%; border:1px solid #d6e1ed; border-radius:10px; padding:11px 12px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isDate }}" hint-placeholder-val="false"><input type="date" value="" data-dc-attr-value="{{ field.value }}" onInput="{{ field.on }}" style="width:100%; border:1px solid #d6e1ed; border-radius:10px; padding:10px 12px; outline:0; font:14px inherit;"></sc-if><sc-if value="{{ field.isSelect }}" hint-placeholder-val="false"><select value="" data-dc-attr-value="{{ field.value }}" onChange="{{ field.on }}" style="width:100%; border:1px solid #d6e1ed; border-radius:10px; padding:11px 12px; outline:0; background:#fff; font:14px inherit;"><option value="">Seleccionar</option><sc-for list="{{ field.options }}" as="option" hint-placeholder-count="4"><option value="" data-dc-attr-value="{{ option }}">{{ option }}</option></sc-for></select></sc-if><sc-if value="{{ field.isBoolean }}" hint-placeholder-val="false"><button onClick="{{ field.toggle }}" type="button" style="display:flex; align-items:center; gap:9px; width:max-content; border:1px solid #d6e1ed; border-radius:100px; padding:7px 11px; background:#f8fafc; color:#3d4c5f; font:800 12px inherit; cursor:pointer;"><span style="width:34px; height:19px; border-radius:100px; background:{{ field.track }}; position:relative;"><span style="position:absolute; top:2px; left:2px; width:15px; height:15px; border-radius:50%; background:#fff; transform:translateX({{ field.knob }});"></span></span>{{ field.booleanLabel }}</button></sc-if></label></sc-for></div></section></sc-for> 6875 <section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px;"><label style="display:flex; flex-direction:column; gap:6px; color:#3d4c5f; font-size:12px; font-weight:800;">Observaciones<textarea value="" data-dc-attr-value="{{ cabanaLotObservations }}" onInput="{{ onCabanaLotObservations }}" rows="4" placeholder="Información adicional del lote" style="resize:vertical; width:100%; border:1px solid #d6e1ed; border-radius:10px; padding:11px 12px; outline:0; font:14px inherit;"></textarea></label></section> 6876 </div> 6877 <aside style="position:{{ cabanaLotMediaPosition }}; top:76px; display:grid; gap:15px;"> 6878 <section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:17px;"><div style="display:flex; justify-content:space-between; align-items:center;"><strong>Imágenes</strong><span style="font-size:11px; color:#8593a6;">{{ cabanaLotImageCount }}/8</span></div><label style="display:grid; place-items:center; gap:7px; min-height:88px; margin-top:12px; border:1px dashed #9fbad6; border-radius:12px; background:#f5f9fd; color:#0a5ca8; cursor:pointer;"><img src="assets/icons/lucide/image-plus.svg" alt="" style="width:22px; height:22px;"><b style="font-size:12px;">Agregar imágenes</b><input type="file" accept="image/jpeg,image/png,image/webp" multiple onChange="{{ onCabanaLotImages }}" style="display:none;"></label><sc-if value="{{ cabanaLotHasImages }}" hint-placeholder-val="false"><div style="display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:7px; margin-top:10px;"><sc-for list="{{ cabanaLotImages }}" as="media" hint-placeholder-count="3"><figure style="position:relative; margin:0; aspect-ratio:1; border-radius:9px; overflow:hidden; background:#edf2f7;"><img data-dc-attr-src="{{ media.url }}" alt="{{ media.name }}" style="width:100%; height:100%; object-fit:cover;"><button onClick="{{ media.remove }}" title="Quitar imagen" style="position:absolute; top:4px; right:4px; width:24px; height:24px; border:0; border-radius:50%; background:rgba(0,29,61,.82); color:#fff; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></figure></sc-for></div></sc-if></section> 6879 <section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:17px;"><div style="display:flex; justify-content:space-between; align-items:center;"><strong>Videos del lote</strong><span style="font-size:11px; color:#8593a6;">{{ cabanaLotVideoCount }}
6879/3</span></div><label style="display:grid; place-items:center; gap:7px; min-height:88px; margin-top:12px; border:1px dashed #9fbad6; border-radius:12px; background:#f5f9fd; color:#0a5ca8; cursor:pointer;"><img src="assets/icons/lucide/video.svg" alt="" style="width:22px; height:22px;"><b style="font-size:12px;">Agregar videos</b><input type="file" accept="video/*" multiple onChange="{{ onCabanaLotVideos }}" style="display:none;"></label><a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guÃa en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">â</span></a><sc-if value="{{ cabanaLotHasVideos }}" hint-placeholder-val="false"><div style="display:grid; gap:8px; margin-top:10px;"><sc-for list="{{ cabanaLotVideos }}" as="media" hint-placeholder-count="2"><div style="display:grid; grid-template-columns:90px minmax(0,1fr) 28px; gap:8px; align-items:center; padding:6px; border:1px solid #e3eaf2; border-radius:10px;"><video data-dc-attr-src="{{ media.url }}" muted preload="metadata" style="width:90px; aspect-ratio:16/9; object-fit:cover; border-radius:7px; background:#001d3d;"></video><span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:11px; font-weight:800;">{{ media.name }}</span><button onClick="{{ media.remove }}" title="Quitar video" style="width:26px; height:26px; border:0; border-radius:50%; background:#fff0f1; color:#c62f3d; cursor:pointer;"><i class="dc-lucide dc-lucide-x" aria-hidden="true"></i></button></div></sc-for></div></sc-if></section> 6880 <button onClick="{{ saveCabanaLotDraft }}" disabled="{{ cabanaLotCreateSaving }}" style="width:100%; border:0; border-radius:11px; padding:12px 16px; background:#e63946; color:#fff; font:900 13px inherit; cursor:{{ cabanaLotSaveCursor }}; opacity:{{ cabanaLotSaveOpacity }};">{{ cabanaLotSaveLabel }}</button> 6881 </aside> 6882 </div> 6883 </sc-if> 6884 </main> 6885 </sc-if> 6886 6887 <!-- DETALLE DE REMATE DE CABAÃA --> 6888 <sc-if value="{{ isCabanaPublicLoading }}" hint-placeholder-val="false"> 6889 <main style="min-height:100vh; display:grid; place-items:center; background:#f4f7fb; padding:28px;"> 6890 <section style="width:min(360px,100%); display:grid; gap:14px; justify-items:center; text-align:center; color:#65758a;"> 6891 <img src="/assets/rosgan-logo-blue.png" alt="Rosgan" style="width:150px; max-width:56vw; height:auto;"> 6892 <span aria-label="Preparando remate" style="width:30px; height:30px; border-radius:50%; border:3px solid #dbe6f1; border-top-color:#0a5ca8; animation:spin 700ms linear infinite;"></span> 6893 <strong style="color:#112033; font-size:16px;">Preparando remate</strong> 6894 </section> 6895 </main> 6896 </sc-if> 6897 <sc-if value="{{ cbAuditorOpen }}"><main class="ra-panel cb-auditor cb-auditor-page" aria-label="Panel auditor de cabañas"> 6898 <header class="ra-top"><button class="ra-back" onClick="{{ closeCbAuditor }}" aria-label="Volver al remate">â</button><div><div class="ra-brand">ROSGAN · GESTIÃN DE CABAÃAS</div><h1>Panel Auditor</h1><p>{{ cbAuditorName }}</p></div><span class="ra-private"><i></i>Acceso interno</span></header> 6899 <div class="ra-content"><div class="ra-summary"><span><strong>{{ cbAuditorCount }}</strong> lotes</span><span>Bases, incrementos y videos</span></div> 6900 <div class="ra-toolbar"><label class="ra-search"><input aria-label="Buscar lotes del auditor" placeholder="Buscar lote, RP, nombre o categorÃaâ¦" data-dc-attr-value="{{ cbAuditorQuery }}" onInput="{{ searchCbAuditor }}"></label><button class="ra-button" onClick="{{ refreshCbAuditor }}">â» Actualizar</button></div> 6901 <p style="margin:0;font-size:12px;">Dejá un importe vacÃo para heredar la categorÃa. Guardá cada fila al terminar.</p> 6902 <sc-if value="{{ cbAuditorLoading }}"><p role="status">Cargando lotesâ¦</p></sc-if><sc-if value="{{ cbAuditorError }}"><p class="ra-alert" role="alert">{{ cbAuditorError }}</p></sc-if> 6903 <div class="ra-list"><div class="cb-auditor-columns"><span>LOTE / RP / CATEGORÃA</span><span>BASE E INCREMENTO</span><span>EDICIÃN Y VIDEOS</span></div> 6904 <sc-for list="{{ cbAuditorRows }}" as="row"><article class="cb-auditor-row"> 6905 <div><strong>{{ row.title }}</strong><small>{{ row.subtitle }}</small><small>{{ row.effective }}</small></div> 6906 <form class="cb-auditor-pricing" onSubmit="{{ row.save }}"><label>Base ($)<input type="number" name="base" disabled="{{ row.saving }}" min="0" step="0.01" placeholder="CategorÃa" aria-label="Base del lote" data-dc-attr-value="{{ row.base }}" onInput="{{ row.setBase }}"></label><label>Incremento ($)<input type="number" name="increment" disabled="{{ row.saving }}" min="0.01" step="0.01" placeholder="CategorÃa" aria-label="Incremento del lote" data-dc-attr-value="{{ row.increment }}" onInput="{{ row.setIncrement }}"></label><button type="submit" disabled="{{ row.saving }}">{{ row.saveLabel }}</button></form> 6907 <div class="cb-auditor-action-cell"><div class="cb-auditor-actions"><button class="cb-auditor-edit" onClick="{{ row.edit }}" aria-label="Editar lote" title="Editar lote"><img src="assets/icons/lucide/pencil.svg" alt=""></button><sc-if value="{{ row.canUpload }}"><label class="cb-auditor-upload"><img src="assets/icons/lucide/upload.svg" alt="">Crudos<input type="file" accept="video/*" multiple data-no-filming-assistant="true" data-video-type="raw" aria-label="Subir videos crudos" onChange="{{ row.upload }}" disabled="{{ row.uploading }}"></label><sc-if value="{{ row.canFinal }}"><label class="cb-auditor-upload"><img src="assets/icons/lucide/upload.svg" alt="">Final<input type="file" accept="video/*" data-no-filming-assistant="true" data-video-type="final" aria-label="Subir un video final" onChange="{{ row.upload }}" disabled="{{ row.uploading }}"></label></sc-if></sc-if></div><small role="status">{{ row.status }}</small></div> 6908 </article></sc-for><sc-if value="{{ cbAuditorEmpty }}"><p>No hay lotes que coincidan con la búsqueda.</p></sc-if> 6909 </div></div></main></sc-if> 6910 6911 <sc-if value="{{ isCabanaDetail }}" hint-placeholder-val="false"> 6912 <main style="min-height:100vh; background:#f4f7fb;"> 6913 <section class="cb-detail-hero" style="--cb-hero-color:{{ cbColor }}; --cb-hero-text:{{ cbHeroText }}; --cb-hero-muted:{{ cbHeroMuted }}; --cb-hero-surface:{{ cbHeroSurface }}; --cb-hero-surface-strong:{{ cbHeroSurfaceStrong }}; --cb-hero-border:{{ cbHeroBorder }}; --cb-hero-icon-filter:{{ cbHeroIconFilter }};"> 6914 <div class="cb-detail-hero-shell"> 6915 <button class="cb-detail-back" onClick="{{ goCabanas }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver a remates de cabaña</button> 6916 <div class="cb-detail-hero-main"> 6917 <div class="cb-detail-copy">
6918 <span class="cb-detail-breed">{{ cbBreed }}</span> 6919 <h1 class="cb-detail-title">{{ cbTitle }}</h1> 6920 <div class="cb-detail-meta"> 6921 <span class="cb-detail-meta-item"><img src="assets/icons/lucide/calendar-days.svg" alt="">{{ cbDate }} · {{ cbTime }}</span> 6922 <span class="cb-detail-meta-item"><img src="assets/icons/lucide/map-pin.svg" alt="">{{ cbLocation }}</span> 6923 </div> 6924 </div> 6925 <sc-if value="{{ cbHasBranding }}" hint-placeholder-val="false"><div class="cb-detail-brand-panel {{ cbBrandPanelClass }}"> 6926 <sc-if value="{{ cbManyMainBrands }}" hint-placeholder-val="false"><div class="cb-detail-brand-heading">CABAÃAS PARTICIPANTES <span>{{ cbMainBrandCount }}</span></div></sc-if> 6927 <sc-if value="{{ cbFewMainBrands }}"><div class="cb-detail-main-logos" aria-label="Cabañas participantes"><sc-for list="{{ cbMainLogos }}" as="brand" hint-placeholder-count="2"><div class="cb-detail-main-brand" title="{{ brand.name }}"><sc-if value="{{ brand.hasLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ brand.logo }}" onError="{{ brand.onError }}" alt="{{ brand.name }}" style="--cb-logo-filter:{{ brand.filter }};"></sc-if><sc-if value="{{ brand.noLogo }}" hint-placeholder-val="false"><strong>{{ brand.name }}</strong></sc-if></div></sc-for></div></sc-if> 6928 <sc-if value="{{ cbManyMainBrands }}"><div class="cb-name-roll {{ cbNameRollPausedClass }}"> 6929 <div class="cb-name-roll-window" tabindex="0" role="region" aria-label="Cabañas participantes"><div class="cb-name-roll-track" style="--cb-roll-duration:{{ cbNameRollDuration }};"> 6930 <div class="cb-name-roll-group"><sc-for list="{{ cbMainLogos }}" as="brand"><div class="cb-name-roll-item"><strong>{{ brand.name }}</strong></div></sc-for></div> 6931 <div class="cb-name-roll-group cb-name-roll-copy" aria-hidden="true"><sc-for list="{{ cbMainLogos }}" as="brand"><div class="cb-name-roll-item"><strong>{{ brand.name }}</strong></div></sc-for></div> 6932 </div></div> 6933 <div class="cb-name-roll-footer"><span>Conocé las cabañas del remate</span><button type="button" onClick="{{ cbToggleNameRoll }}" aria-pressed="{{ cbNameRollPaused }}">{{ cbNameRollPauseLabel }}</button></div> 6934 </div></sc-if> 6935 <sc-if value="{{ cbManyGuestBrands }}"><div class="cb-name-roll {{ cbGuestRollPausedClass }}"> 6936 <div class="cb-detail-brand-heading">CABAÃAS INVITADAS <span>{{ cbGuestBrandCount }}</span></div> 6937 <div class="cb-name-roll-window" tabindex="0" role="region" aria-label="Cabañas invitadas"><div class="cb-name-roll-track" style="--cb-roll-duration:{{ cbGuestRollDuration }};"> 6938 <div class="cb-name-roll-group"><sc-for list="{{ cbGuestLogos }}" as="brand"><div class="cb-name-roll-item"><sc-if value="{{ brand.hasLogo }}"><img data-dc-attr-src="{{ brand.logo }}" onError="{{ brand.onError }}" alt="" style="width:64px;height:36px;flex:none;object-fit:contain;filter:{{ brand.filter }};"></sc-if><strong>{{ brand.name }}</strong></div></sc-for></div> 6939 <div class="cb-name-roll-group cb-name-roll-copy" aria-hidden="true"><sc-for list="{{ cbGuestLogos }}" as="brand"><div class="cb-name-roll-item"><sc-if value="{{ brand.hasLogo }}"><img data-dc-attr-src="{{ brand.logo }}" onError="{{ brand.onError }}" alt="" style="width:64px;height:36px;flex:none;object-fit:contain;filter:{{ brand.filter }};"></sc-if><strong>{{ brand.name }}</strong></div></sc-for></div> 6940 </div></div> 6941 <div class="cb-name-roll-footer"><span>Conocé las cabañas invitadas</span><button type="button" onClick="{{ cbToggleGuestRoll }}" aria-pressed="{{ cbGuestRollPaused }}">{{ cbGuestRollPauseLabel }}</button></div> 6942 </div></sc-if> 6943 <sc-if value="{{ cbFewGuestBrands }}" hint-placeholder-val="false"><div class="cb-detail-guests"><span>CABAÃAS INVITADAS</span><sc-for list="{{ cbGuestLogos }}" as="brand" hint-placeholder-count="3"><span class="cb-detail-guest-logo" title="{{ brand.name }}"><sc-if value="{{ brand.hasLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ brand.logo }}" onError="{{ brand.onError }}" alt="{{ brand.name }}" style="filter:{{ brand.filter }};"></sc-if><sc-if value="{{ brand.noLogo }}" hint-placeholder-val="false"><small>{{ brand.name }}</small></sc-if></span></sc-for></div></sc-if> 6944 </div></sc-if> 6945 </div> 6946 </div> 6947 </section> 6948 <section style="max-width:1240px; margin:0 auto; padding:22px clamp(14px,4vw,36px) 100px;"> 6949 <div style="display:flex; align-items:center; gap:13px; padding:15px 17px; border-radius:15px; color:#fff;
6949 background:{{ cbConsignatariaColor }}; box-shadow:0 10px 28px {{ cbConsignatariaShadow }}; margin-bottom:22px;"><span class="dc-consignataria-logo-box" style="width:42px; height:42px; flex:0 0 42px; font-weight:900;"><sc-if value="{{ cbHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ cbConsignatariaWhiteLogo }}" alt="{{ cbConsignataria }}"></sc-if><sc-if value="{{ cbNoConsignatariaWhiteLogo }}" hint-placeholder-val="false">{{ cbConsignatariaInitial }}</sc-if></span><div style="min-width:0;"><small style="display:block; color:rgba(255,255,255,.78); font-weight:800; letter-spacing:.08em;">CONSIGNATARIA</small><strong style="display:block; color:#fff; line-height:1.2;">{{ cbConsignataria }}</strong></div></div> 6950 <div class="dc-catalog-contact-row"><section class="dc-lot-contact-card"><div class="dc-lot-contact-head"><span class="dc-lot-contact-avatar" style="background:{{ cbConsignatariaColor }};"><sc-if value="{{ cbHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ cbConsignatariaWhiteLogo }}" alt="{{ cbConsignataria }}"></sc-if><sc-if value="{{ cbNoConsignatariaWhiteLogo }}" hint-placeholder-val="false"><strong>{{ cbConsignatariaInitial }}</strong></sc-if></span><span class="dc-lot-contact-copy"><small>CONTACTO DE LOS LOTES</small><strong>{{ cbContactName }}</strong><span>{{ cbContactPhone }}</span></span></div><div class="dc-lot-contact-actions"><a class="dc-contact-whatsapp" data-dc-attr-href="{{ cbContactWhatsappHref }}" target="_blank" rel="noopener noreferrer"><img src="assets/icons/brands/whatsapp.svg" alt="">Consultar por WhatsApp</a><a class="dc-contact-call" data-dc-attr-href="{{ cbContactPhoneHref }}"><img src="assets/icons/lucide/phone.svg" alt="">Llamar</a></div></section></div> 6951 <div class="cb-detail-actions" role="group" aria-label="Acciones del remate"> 6952 <sc-if value="{{ cbHasCatalogPdf }}" hint-placeholder-val="false"><a class="remate-list-catalog rd-catalog-cta" data-dc-attr-href="{{ cbCatalogPdfUrl }}" target="_blank" rel="noopener noreferrer"><img src="assets/icons/lucide/file-text.svg" alt="" aria-hidden="true"><span>Catálogo</span></a></sc-if> 6953 <sc-if value="{{ cbTermsVisible }}" hint-placeholder-val="false"><div class="cb-terms-entry"><button class="rosgan-page-action" type="button" onClick="{{ cbOpenTerms }}"><img src="assets/icons/lucide/file-text.svg" alt="">Condiciones comerciales</button></div></sc-if> 6954 <sc-if value="{{ cbPrebidSummaryAllowed }}" hint-placeholder-val="false"><nav class="cb-auction-subpages" aria-label="Resumen privado"><button class="rosgan-page-action" onClick="{{ cbOpenPrebidSummary }}"><img src="assets/icons/lucide/file-text.svg" alt="">Resumen de preofertas</button></nav></sc-if> 6955 <sc-if value="{{ cbShowAuctionSubpages }}" hint-placeholder-val="false"><nav class="cb-auction-subpages" aria-label="Secciones del remate"> 6956 <sc-if value="{{ cbCanAudit }}"><button class="rosgan-auditor-entry rosgan-page-action" onClick="{{ toggleCbAuditor }}"><img src="assets/icons/lucide/clipboard-list.svg" alt="">Panel auditor</button></sc-if> 6957 <sc-if value="{{ cbCanSeeStatsButton }}" hint-placeholder-val="false"><button class="rosgan-page-action" onClick="{{ openCbStats }}" title="Ver estadÃsticas del remate"><img src="assets/icons/lucide/chart-no-axes-combined.svg" alt="">EstadÃsticas</button></sc-if> 6958 <sc-if value="{{ cbCanManagePrivatePages }}" hint-placeholder-val="false"><button class="rosgan-page-action" onClick="{{ openCbCommercial }}" title="Abrir gestión comercial"><img src="assets/icons/lucide/table-2.svg" alt="">Comercial</button><button class="rosgan-page-action" onClick="{{ openCbPrices }}" title="Ver placa de precios"><img src="assets/icons/lucide/badge-dollar-sign.svg" alt="">Precios</button><button class="rosgan-page-action" onClick="{{ openCbTv }}" title="Abrir salida de TV"><img src="assets/icons/lucide/radio-tower.svg" alt="">TV</button><button class="rosgan-page-action" onClick="{{ openCbTvConfig }}" title="Configurar salida de TV"><img src="assets/icons/lucide/settings.svg" alt="">Config. TV</button></sc-if> 6959 </nav></sc-if> 6960 </div> 6961 <sc-if value="{{ cbCatalogUnlocked }}"> 6962 <div style="display:flex; align-items:end; justify-content:space-between; gap:14px; flex-wrap:wrap;"><div><small style="color:{{ cbColor }}; font-weight:900; letter-spacing:.12em;">CATÃLOGO</small><h2 style="margin:3px 0 0; color:#112033;">{{ cbCatalogTitle }}</h2></div><div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;"><sc-if value="{{ cbCanExportCatalog }}" hint-placeholder-val="false"><div class="dc-catalog-export-actions"><button class="dc-catalog-export-button is-excel" onClick="{{ downloadCbCatalogExcel }}" title="Descargar Excel del catalogo"><img src="assets/icons/lucide/file-spreadsheet.svg" alt="">Descargar Excel</button></div></sc-if><sc-if value="{{ cbCanSeeStatsButton }}" hint-placeholder-val="false"><button onClick="{{ openCbStats }}" title="Ver estadÃsticas del remate" style="display:inline-flex; align-items:center; gap:8px; border:1px solid #cbd8e7; border-radius:9px; padding:9px 12px; color:#12304d; background:#fff; font:800 12px inherit; cursor:pointer;"><img src="assets/icons/lucide/chart-no-axes-combined.svg" alt="" style="width:15px; height:15px;">EstadÃsticas</button></sc-if><sc-if value="{{ cbCanManageSalesOrder }}"><button onClick="{{ openCbSalesOrder }}" title="Ordenar los lotes del remate" style="display:inline-flex; align-items:center; gap:8px; border:1px solid #cbd8e7; border-radius:9px; padding:9px 12px; color:#12304d; background:#fff; font:800 12px inherit; cursor:pointer;"><img src="assets/icons/lucide/list-ordered.svg" alt="" style="width:15px; height:15px;">
6962Orden de ventas</button></sc-if><sc-if value="{{ cbCountsLoading }}"><span class="dc-skeleton" style="width:72px; height:16px; border-radius:99px;"></span></sc-if><sc-if value="{{ cbCountsReady }}"><span style="font-size:13px; color:#65758a;">Todos {{ cbTotal }}</span></sc-if></div></div> 6963 <div style="display:flex; gap:10px; flex-wrap:wrap; margin:14px 0 18px;"><label style="flex:1; min-width:220px; background:#fff; border:1px solid #dce5ef; border-radius:12px; padding:10px 13px;"><input value="" data-dc-attr-value="{{ cbQuery }}" onInput="{{ onCbQuery }}" placeholder="Buscar por RP, nombre o categorÃa" style="width:100%; border:0; outline:0; font:14px inherit;"></label></div> 6964 <sc-if value="{{ cbLoading }}" hint-placeholder-val="false"><div style="height:220px; border-radius:16px; background:linear-gradient(100deg,#e9eff6 20%,#f7f9fc 40%,#e9eff6 60%); background-size:220% 100%; animation:shimmer 1.5s infinite;"></div></sc-if> 6965 <sc-if value="{{ cbHasLots }}" hint-placeholder-val="true"> 6966 <div class="cb-catalog-grid" style="display:grid; grid-template-columns:repeat(auto-fill,minmax(245px,1fr)); gap:16px;"> 6967 <sc-for list="{{ cbLots }}" as="lot" hint-placeholder-count="8"> 6968 <article class="dc-cabana-catalog-card" onClick="{{ lot.open }}" style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; overflow:hidden; cursor:pointer; opacity:{{ lot.opacity }}; box-shadow:0 8px 24px rgba(0,30,60,.07);"> 6969 <div class="cb-catalog-card-cover" style="height:155px; position:relative; overflow:hidden; background:linear-gradient(145deg,#0a5ca8,#002e5f);"> 6970 <sc-if value="{{ lot.hasPreviousMediaLayer }}" hint-placeholder-val="false"><div class="dc-cabana-card-media" style="position:absolute; inset:0; opacity:{{ lot.previousMediaOpacity }};"> 6971 <sc-if value="{{ lot.previousHasMedia }}" hint-placeholder-val="false"><img data-dc-attr-src="{{ lot.previousMedia }}" alt="" style="width:100%; height:100%; object-fit:cover;"></sc-if> 6972 <sc-if value="{{ lot.previousHasBackgroundLogo }}" hint-placeholder-val="false"><img data-dc-attr-src="{{ lot.previousBackgroundLogo }}" alt="" style="position:absolute; inset:18px; width:calc(100% - 36px); height:calc(100% - 36px); object-fit:contain; opacity:.24; filter:{{ lot.previousBackgroundLogoFilter }};"></sc-if> 6973 </div></sc-if> 6974 <div class="dc-cabana-card-media" style="position:absolute; inset:0; opacity:{{ lot.mediaOpacity }};"> 6975 <sc-if value="{{ lot.hasMedia }}" hint-placeholder-val="false"><img class="dc-media-image" data-dc-attr-src="{{ lot.media }}" onLoad="{{ lot.mediaReady }}" onError="{{ lot.mediaError }}" alt="" style="width:100%; height:100%; object-fit:cover;"></sc-if> 6976 <sc-if value="{{ lot.hasVideoPreview }}" hint-placeholder-val="false"><video data-dc-attr-src="{{ lot.videoPreview }}" onCanPlay="{{ lot.mediaReady }}" onError="{{ lot.mediaError }}" muted autoplay loop playsinline preload="metadata" style="width:100%; height:100%; object-fit:cover;"></video></sc-if> 6977 <sc-if value="{{ lot.hasBackgroundLogo }}" hint-placeholder-val="false"><img data-dc-attr-src="{{ lot.backgroundLogo }}" alt="" style="position:absolute; inset:18px; width:calc(100% - 36px); height:calc(100% - 36px); object-fit:contain; opacity:.24; filter:{{ lot.backgroundLogoFilter }};"></sc-if> 6978 </div> 6979 <sc-if value="{{ lot.videoUploadBusy }}" hint-placeholder-val="false"><div class="dc-cabana-card-upload-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" data-dc-attr-aria-valuenow="{{ lot.videoUploadProgress }}" onClick="{{ lot.stopUploadClick }}"><span><b>{{ lot.videoUploadStage }}</b><strong>{{ lot.videoUploadProgress }}%</strong></span><div class="dc-cabana-upload-track"><span style="width:{{ lot.videoUploadProgress }}%;"></span></div><small>No cierres esta pantalla.</small></div></sc-if> 6980 <sc-if value="{{ lot.canUploadVideo }}" hint-placeholder-val="false"><label class="dc-cabana-card-upload" onClick="{{ lot.stopUploadClick }}"><img src="assets/icons/lucide/upload.svg" alt="">Subir video<input type="file" accept="video/*" multiple onChange="{{ lot.uploadVideo }}" style="display:none;"></label></sc-if> 6981 <div style="position:absolute; inset:0; background:linear-gradient(180deg,transparent,rgba(0,29,61,.78));"></div> 6982 <sc-if value="{{ lot.hasRp }}"><span class="dc-cabana-card-rp" data-dc-attr-title="{{ lot.rpTitle }}" style="position:absolute; top:10px; left:10px;
6982 background:{{ cbColor }}; color:#fff; border-radius:100px; padding:5px 10px; font-size:11px; font-weight:900;">{{ lot.rp }}</span></sc-if> 6983 <sc-if value="{{ lot.hasLotNumber }}" hint-placeholder-val="true"><span class="cb-catalog-card-number" style="position:absolute; top:10px; right:10px; background:rgba(0,24,49,.82); color:#fff; border:1px solid rgba(255,255,255,.48); border-radius:100px; padding:5px 10px; font-size:11px; font-weight:900; letter-spacing:.04em; backdrop-filter:blur(6px);">{{ lot.lotBadge }}</span></sc-if> 6984 <div style="position:absolute; left:12px; right:12px; bottom:10px; color:#fff;"><sc-if value="{{ lot.showName }}" hint-placeholder-val="false"><strong class="dc-cabana-card-title" style="display:block; font-size:16px;">{{ lot.name }}</strong></sc-if><span style="font-size:11px; opacity:.85;">{{ lot.category }} - {{ lot.breed }}</span></div> 6985 </div> 6986<sc-if value="{{ lot.canUploadVideo }}" hint-placeholder-val="false"><div style="padding:8px 13px;" onClick="{{ lot.stopUploadClick }}"><a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guÃa en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">â</span></a></div></sc-if> 6987 <sc-if value="{{ lot.hasCardMetaRow }}"><div style="display:flex; align-items:center; justify-content:space-between; gap:10px; padding:9px 13px 0;"><span style="display:flex; align-items:center; gap:8px; min-width:0;"><sc-if value="{{ lot.hasVariants }}"><small style="color:#65758a; font-size:10px; font-weight:800; letter-spacing:.06em;">{{ lot.variantPosition }}</small></sc-if><sc-if value="{{ lot.hasRankBadge }}" hint-placeholder-val="false"><span title="{{ lot.rankTitle }}" style="display:inline-flex; align-items:center; gap:5px; min-width:0; padding:4px 8px; border-radius:100px; background:{{ lot.rankBg }}; color:{{ lot.rankColor }}; border:1px solid {{ lot.rankBorder }}; font-size:10px; font-weight:950; letter-spacing:.03em; text-transform:uppercase;"><img data-dc-attr-src="{{ lot.rankIconUrl }}" alt="" style="width:12px; height:12px; flex:0 0 auto;">{{ lot.rankLabel }}</span></sc-if></span><sc-if value="{{ lot.hasVariants }}"><div style="display:flex; align-items:center; gap:5px;"><sc-for list="{{ lot.dots }}" as="dot"><button onClick="{{ dot.select }}" title="{{ dot.label }}" style="width:{{ dot.width }}; height:7px; padding:0; border:0; border-radius:999px; background:{{ dot.bg }}; cursor:pointer; transition:width .24s ease, background .24s ease;"></button></sc-for></div></sc-if></div></sc-if> 6988 <div class="cb-catalog-card-body" style="padding:13px;"><div class="cb-catalog-card-facts" style="display:grid; grid-template-columns:1fr 1fr; gap:9px;"><sc-for list="{{ lot.facts }}" as="fact" hint-placeholder-count="4"><div class="dc-cabana-card-fact"><small style="display:block; color:#8593a6;">{{ fact.label }}</small><strong data-dc-attr-title="{{ fact.title }}" style="font-size:13px;">{{ fact.value }}</strong></div></sc-for></div><sc-if value="{{ lot.showBase }}" hint-placeholder-val="false"><div class="cb-catalog-card-base" style="display:flex; justify-content:space-between; margin-top:12px; padding-top:10px; border-top:1px solid #edf2f7;"><small style="color:{{ lot.priceColor }}; font-weight:{{ lot.priceWeight }};">{{ lot.priceLabel }}</small><strong style="color:{{ lot.priceColor }};">{{ lot.base }}</strong></div></sc-if></div> 6989 </article> 6990 </sc-for> 6991 </div> 6992 </sc-if> 6993 <sc-if value="{{ cbLotsEmpty }}" hint-placeholder-val="false"><div style="padding:42px 22px; border:1px dashed #cbd8e7; border-radius:17px; background:#fff; text-align:center;"><strong style="display:block;">No hay lotes publicados para este remate</strong><span style="font-size:13px; color:#65758a;">El catálogo aparecerá cuando existan lotes reales asociados.</span></div></sc-if> 6994 </sc-if> 6995 </section> 6996 <div class="dc-catalog-floating-contact" aria-hidden="true"><span class="dc-catalog-floating-avatar" style="background:{{ cbC
6996onsignatariaColor }};"><sc-if value="{{ cbHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ cbConsignatariaWhiteLogo }}" alt="{{ cbConsignataria }}"></sc-if><sc-if value="{{ cbNoConsignatariaWhiteLogo }}" hint-placeholder-val="false"><strong>{{ cbConsignatariaInitial }}</strong></sc-if></span><span class="dc-catalog-floating-copy"><small>CONTACTO DE LOS LOTES</small><strong>{{ cbContactName }}</strong></span><a class="dc-contact-whatsapp" data-dc-attr-href="{{ cbContactWhatsappHref }}" target="_blank" rel="noopener noreferrer" aria-label="Consultar por WhatsApp"><img src="assets/icons/brands/whatsapp.svg" alt=""></a><a class="dc-contact-call" data-dc-attr-href="{{ cbContactPhoneHref }}" aria-label="Llamar"><img src="assets/icons/lucide/phone.svg" alt=""></a></div> 6997 </main> 6998 </sc-if> 6999 <sc-if value="{{ isCabanaPrebidPanel }}" hint-placeholder-val="false"> 7000 <main class="cb-prebid-page"> 7001 <section class="cb-prebid-page-shell"> 7002 <button class="cb-prebid-page-back" onClick="{{ closeCbPrebidPanel }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al remate</button> 7003 <header class="cb-prebid-page-title"> 7004 <div><small>GESTIÃN PRIVADA · CABAÃAS</small><h1>Preofertas de {{ cbTitle }}</h1><p>Seguimiento en tiempo real con identidad, lote y animales involucrados.</p></div> 7005 <span class="cb-prebid-page-live"><i class="cb-prebid-admin-live"></i>{{ cbPrebidPanelUpdated }}</span> 7006 </header> 7007 <section class="cb-prebid-admin-panel is-page"> 7008 <header class="cb-prebid-admin-head"> 7009 <div><small>PANEL DE CONTROL</small><h2>Actividad de preofertas</h2><p>Información privada visible sólo para perfiles autorizados.</p></div> 7010 <div class="cb-prebid-admin-head-actions"><button onClick="{{ refreshCbPrebidPanel }}" title="Actualizar ahora"><img src="assets/icons/lucide/refresh-cw.svg" alt=""></button></div> 7011 </header> 7012 <sc-if value="{{ cbPrebidPanelHasLots }}" hint-placeholder-val="true"><section class="cb-prebid-lot-picker"><div class="cb-prebid-lot-picker-head"><div><small>LOTES DEL REMATE</small><strong>Elegà un lote para ver sus preofertas</strong></div><span>La información se muestra por separado.</span></div><div class="cb-prebid-lot-picker-grid"><sc-for list="{{ cbPrebidPanelLots }}" as="lot" hint-placeholder-count="3"><button class="{{ lot.className }}" onClick="{{ lot.go }}"><span class="cb-prebid-lot-option-main"><strong>{{ lot.label }}</strong><small>{{ lot.meta }}</small></span><span class="cb-prebid-lot-option-side"><b>{{ lot.currentAmount }}</b><span>{{ lot.offerLabel }}</span></span></button></sc-for></div></section></sc-if> 7013 <sc-if value="{{ cbPrebidPanelHasSelectedLot }}" hint-placeholder-val="true"><div class="cb-prebid-selected-context">Mostrando únicamente <strong>{{ cbPrebidPanelSelectedLotLabel }}</strong></div></sc-if> 7014 <div class="cb-prebid-admin-metrics"><sc-for list="{{ cbPrebidPanelMetrics }}" as="metric" hint-placeholder-count="4"><article><small>{{ metric.label }}</small><strong>{{ metric.value }}</strong></article></sc-for></div> 7015 <div class="cb-prebid-admin-toolbar"><label><img src="assets/icons/lucide/search.svg" alt=""><input value="" data-dc-attr-value="{{ cbPrebidPanelQuery }}" onInput="{{ onCbPrebidPanelQuery }}" placeholder="Buscar email, nombre, CUIT, lote, RP, pintura..."></label><div><sc-for list="{{ cbPrebidPanelStatusTabs }}" as="tab" hint-placeholder-count="4"><button class="{{ tab.className }}" onClick="{{ tab.go }}">{{ tab.label }}</button></sc-for></div></div> 7016 <sc-if value="{{ cbPrebidPanelLoading }}" hint-placeholder-val="false"><div class="cb-prebid-admin-message"><span class="dc-spinner"></span> Cargando preofertas...</div></sc-if> 7017 <sc-if value="{{ cbPrebidPanelHasError }}" hint-placeholder-val="false"><div class="cb-prebid-admin-message is-error">{{ cbPrebidPanelError }}</div></sc-if> 7018 <sc-if value="{{ cbPrebidPanelHasRows }}" hint-placeholder-val="true"><div class="cb-prebid-admin-list"><sc-for list="{{ cbPrebidPanelRows }}" as="offer" hint-placeholder-count="5"><article class="cb-prebid-admin-row"><div class="cb-prebid-admin-offer"><span class="{{ offer.statusClass }}">{{ offer.status }}</span><strong>{{ offer.amount }}</strong><small>{{ offer.date }}</small><sc-if value="{{ offer.canDelete }}"><button disabled="{{ offer.deleteBusy }}" onClick="{{ offer.deleteOffer }}" style="padding:8px 12px; border:1px solid #efb7bf; border-radius:9px; color:#b51f35; background:#fff; cursor:pointer;">Eliminar preoferta</button></sc-if><sc-if value="{{ offer.canApprove }}"><div class="cb-contract-actions"><button disabled="{{ offer.busy }}" onClick="{{ offer.approve }}">Aprobar</button><button disabled="{{ offer.busy }}" onClick="{{ offer.reject }}">Rechazar</button></div></sc-if></div><div class="cb-prebid-admin-buyer"><small>OFERTANTE</small><strong>{{ offer.buyerName }}</strong><a data-dc-attr-href="mailto:{{ offer.buyerEmail }}">{{ offer.buyerEmail }}</a><span>{{ offer.buyerCuit }}</span><sc-if value="{{ offer.hasCommission }}" hint-placeholder-val="false"><em>{{ offer.commission }}</em></sc-if></div><div class="cb-prebid-admin-lot"><small>LOTE / CABAÃA</small><strong>{{ offer.lot }}</strong><span>{{ offer.lotMeta }}</span><sc-if value="{{ offer.hasAnimals }}" hint-placeholder-val="true"><div class="cb-prebid-admin-animals"><sc-for list="{{ offer.animals }}" as="animal" hint-placeholder-count="2"><span><b>{{ animal.cert }}</b>{{ animal.detail }}</span></sc-for></div></sc-if></div></article></sc-for></div></sc-if> 7019 <sc-if value="{{ cbPrebidPanelEmpty }}" hint-placeholder-val="false"><div class="cb-prebid-admin-message">Este lote todavÃa no tiene preofertas para los filtros seleccionados.</div></sc-if> 7020 </section> 7021 </section> 7022 </main> 7023 </sc-if> 7024 <sc-if value="{{ isRosganStats }}"><main class="rosgan-ops-page"><div class="rosgan-ops-shell"><button class="rosgan-ops-back" onClick="{{ rosOpsBack }}">â Volver al catálogo</button><rosgan-audience-dashboard remate-id="{{ rgAudienceId }}"></rosgan-audience-dashboard></div></main></sc-if> 7025 <sc-if value="{{ isRosganCommercial }}" hint-placeholder-val="false"> 7026 <main class="commercial-page rg-commercial-page"><div class="commercial-shell"> 7027 <button class="commercial-back" onClick="{{ rosOpsBack }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catálogo</button> 7028 <header class="commercial-hero"><div><span class="commercial-kicker">MESA COMERCIAL · ROSGAN</span><h1>Carga comercial</h1><p>{{ rosOpsTitle }} · valores por lote y certificado</p></div><div class="commercial-hero-actions"><sc-if value="{{ rosCanCloseRemate }}"><button type="button" class="commercial-prices-link" onClick="{{ rosToggleRemateClosed }}" disabled="{{ rosRemateClosingBusy }}">{{ rosRemateClosedLabel }}</button></sc-if> 7029 <button class="commercial-prices-link" onClick="{{ rosOpsOpenPrices }}"><img src="assets/icons/lucide/chart-no-axes-combined.svg" alt="">Placa de precios</button> 7030 <button class="commercial-prices-link" onClick="{{ rosOpsOpenTvConfig }}"><img src="assets/icons/lucide/settings.svg" alt="">Configurar TV</button> 7031 <button class="commercial-prices-link" onClick="{{ rosOpsOpenTv }}"><img src="assets/icons/lucide/video.svg" alt="">Abrir salida TV</button> 7032 <button class="commercial-prices-link" onClick="{{ rosExportAirtime }}" title="Descargar informe ejecutivo Excel de tiempos"><img src="assets/icons/lucide/file-spreadsheet.svg" alt="">Tiempos Excel</button> 7033 <button class="commercial-prices-link" onClick="{{ rosBillingExcel }}"><img src="assets/icons/lucide/file-spreadsheet.svg" alt="">Facturación Excel</button><button class="commercial-prices-link" onClick="{{ rosBillingPdf }}"><img src="assets/icons/lucide/file-text.svg" alt="">Facturación PDF</button><span class="commercial-autosave-note"><img src="assets/icons/lucide/save.svg" alt="">Autoguardado</span> 7034 </div></header> 7035 <section class="commercial-summary" aria-label="Resumen comercial"><sc-for list="{{ rosCommercialMetrics }}" as="metric"><article class="commercial-metric"><span class="commercial-metric-icon" style="background:{{ metric.bg }}"><img data-dc-attr-src="{{ metric.icon }}" alt=""></span><span><small>{{ metric.label }}</small><strong>{{ metric.value }}</strong></span></article></sc-for></section> 7036 <section class="rg-commercial-progress" aria-label="Distribución de estados del remate"><header><strong>Estado del remate</strong><span>{{ rosProgressTotal }}</span></header><div class="rg-progress-track" role="img" aria-label="{{ rosProgressDescription }}"><sc-for list="{{ rosProgressSegments }}" as="segment"><span class="rg-progress-segment" style="width:{{ segment.width }}%;background:{{ segment.color }}" title="{{ segment.label }}: {{ segment.percent }} · {{ segment.count }} lotes"></span></sc-for></div><div class="rg-progress-legend"><sc-for list="{{ rosProgressSegments }}" as="segment"><span><i style="background:{{ segment.color }}"></i>{{ segment.label }} <strong>{{ segment.percent }}</strong><small>({{ segment.count }})</small></span></sc-for></div></section> 7037 <section class="rg-airtime-panel" aria-label="Tiempos al aire"><header><strong>Cronómetro del remate</strong><span>{{ rosAirtimeStatus }}</span><button onClick="{{ rosAirtimeResetAll }}" class="rg-time-button">Reiniciar todos</button></header><div class="rg-airtime-metrics"><sc-for list="{{ rosAirtimeMetrics }}" as="metric"><div><small>{{ metric.label }}</small><strong>{{ metric.value }}</strong></div></sc-for></div><details><summary>Tiempos por consignataria</summary><div class="rg-airtime-groups"><sc-for list="{{ rosAirtimeGroups }}" as="group"><article><div class="rg-airtime-group-heading"><span class="rg-airtime-group-logo" style="background:{{ group.color }}"><sc-if value="{{ group.hasLogo }}"><img src="{{ group.logo }}" alt="" loading="lazy" decoding="async"></sc-if><sc-if value="{{ group.noLogo }}">{{ group.initial }}</sc-if></span><strong>{{ group.name }}</strong></div><span>Total <b>{{ group.totalLabel }}</b></span><span>Promedio <b>{{ group.averageLabel }}</b></span><small>{{ group.count }} lotes cronometrados</small></article></sc-for></div></details><p>Promedios sobre lotes con tiempo registrado. Fin estimado: promedio à lotes pendientes, sin paus
7037as. En pantalla múltiple, el tiempo se reparte por igual entre los lotes visibles.</p></section> 7038 <div class="rg-commercial-tools"><nav class="commercial-filters" aria-label="Filtrar por estado"><sc-for list="{{ rosCommercialFilters }}" as="filter"><button class="{{ filter.className }}" aria-pressed="{{ filter.active }}" onClick="{{ filter.select }}">{{ filter.label }}<b>{{ filter.count }}</b></button></sc-for></nav><label class="rg-commercial-search"><img src="assets/icons/lucide/search.svg" alt=""><input type="search" aria-label="Buscar lote comercial" data-dc-attr-value="{{ rosCommercialQuery }}" onInput="{{ rosCommercialSearch }}" placeholder="Lote, certificado o consignataria"></label></div> 7039 <div class="rg-commercial-caption"><strong>Carga de precios</strong><span>{{ rosCommercialResultCount }} de {{ rosOpsLotCount }} lotes</span></div> 7040 <section class="commercial-board" aria-label="Carga comercial por lote"> 7041 <div class="rg-commercial-head"><span>Orden</span><span>Lote / consignataria</span><span>Datos del lote</span><span>Estado</span><span>Precio</span></div> 7042 <sc-if value="{{ rosOpsLoading }}"><div class="commercial-empty"><span class="dc-spinner"></span> Cargando lotes...</div></sc-if> 7043 <sc-for list="{{ rosCommercialRows }}" as="row"><article key="{{ row.cert }}" class="commercial-row rg-commercial-row {{ row.commercialClass }}"> 7044 <div class="rg-commercial-order"><span class="commercial-order-badge">{{ row.order }}</span></div> 7045 <div class="rg-commercial-identity"><small>Cert. {{ row.cert }}</small><strong>{{ row.category }}</strong><span class="rg-commercial-breed">{{ row.breed }}</span><span class="rg-commercial-cons"><span class="commercial-cons-avatar" style="background:{{ row.consColor }}"><sc-if value="{{ row.hasConsLogo }}"><img data-dc-attr-src="{{ row.consLogo }}" alt=""></sc-if><sc-if value="{{ row.noConsLogo }}"><strong>{{ row.consInitial }}</strong></sc-if></span><span>{{ row.consignataria }}</span></span></div> 7046 <div class="rg-commercial-data"><strong>{{ row.dataSummary }}</strong><span>{{ row.locality }}</span><small>Plazo: {{ row.plazo }}</small><span class="rg-lot-interest" title="Personas que solicitaron el aviso de salida, incluidas las ya notificadas."><img src="/assets/icons/lucide/bell.svg" alt="" aria-hidden="true">{{ row.interestLabel }}</span><span class="rg-airtime-lot">{{ row.airtimeState }} · <b>{{ row.airtimeLabel }}</b><button class="rg-time-button" aria-label="Editar tiempo del lote {{ row.order }}" title="Editar tiempo" onClick="{{ row.editTime }}">â</button><button class="rg-time-button" aria-label="Reiniciar tiempo del lote {{ row.order }}" title="Reiniciar tiempo" onClick="{{ row.resetTime }}">âº</button></span><sc-if value="{{ row.editingTime }}"><div class="rg-time-editor"><input aria-label="Tiempo del lote {{ row.order }}" placeholder="HH:MM:SS" data-dc-attr-value="{{ row.timeDraft }}" onInput="{{ row.changeTime }}"><button class="rg-time-button" onClick="{{ row.saveTime }}">Guardar</button><button class="rg-time-button" onClick="{{ row.cancelTime }}">Cancelar</button></div></sc-if></div> 7047 <div class="rg-commercial-status"><span class="{{ row.statusClass }}">{{ row.statusLabel }}</span><div class="commercial-status"><button class="{{ row.pendingClass }}" aria-pressed="{{ row.isPending }}" disabled="{{ row.hasDivisions }}" onClick="{{ row.setPending }}">Pendiente</button><button class="{{ row.soldClass }}" aria-pressed="{{ row.isSold }}" disabled="{{ row.hasDivisions }}" onClick="{{ row.setSold }}">â Vendido</button><button class="{{ row.unsoldClass }}" aria-pressed="{{ row.isUnsold }}" disabled="{{ row.hasDivisions }}" onClick="{{ row.setUnsold }}">No vendido</button><button class="{{ row.liftedClass }}" aria-pressed="{{ row.isLifted }}" onClick="{{ row.setLifted }}">Levantado</button></div></div> 7048 <div class="commercial-price"><div class="commercial-mode"><button class="{{ row.kiloClass }}" disabled="{{ row.hasDivisions }}" onClick="{{ row.setKilo }}">$ / kg</button><button class="{{ row.headClass }}" disabled="{{ row.hasDivisions }}" onClick="{{ row.setHead }}">$ / cabeza</button></div><input type="text" disabled="{{ row.hasDivisions }}" aria-label="{{ row.priceLabel }}" data-dc-attr-value="{{ row.priceValue }}" onInput="{{ row.setPrice }}" onBlur="{{ row.commitPrice }}" onKeyDown="{{ row.commitPriceEnter }}" inputmode="decimal" placeholder="Ingresar precio"></div><label style="grid-column:1 / -1;display:flex;align-items:center;gap:10px;font-size:12px;color:#526b82;">Comprador (opcional)<input type="text" disabled="{{ row.hasDivisions }}" maxlength="200" aria-label="Comprador del lote {{ row.order }}" placeholder="Nombre o razón social" data-dc-attr-value="{{ row.buyerValue }}" onInput="{{ row.setBuyer }}" onBlur="{{ row.commitPrice }}" style="flex:1;min-width:0;padding:9px 11px;border:1px solid #cad8e6;border-radius:8px;font:inherit;"></label><button type="button" onClick="{{ row.openDivisions }}" style="grid-column:1 / -1;justify-self:end;border:1px solid #b9cddd;border-radius:8px;padding:9px 14px;background:#edf5fb;color:#073763;font:700 12px Arial;cursor:pointer">{{ row.divisionLabel }}</button> 7049 </article></sc-for> 7050 <sc-if value="{{ rosCommercialEmpty }}"><div class="commercial-empty">No hay lotes que coincidan con la búsqueda o el estado seleccionado.</div></sc-if> 7051 </section> 7052 </div></main> 7053 </sc-if> 7054 7055 <sc-if value="{{ isRosganPriceBoard }}" hint-placeholder-val="false"> 7056 <main class="price-board-page"><div class="price-board-shell"> 7057 <button class="commercial-back" onClick="{{ rosOpsBack }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catálogo</button> 7058<div class="price-board-toolbar"><sc-if value="{{ rosOpsCanAdmin }}" hint-placeholder-val="false"><button class="commercial-prices-link" onClick="{{ rosOpsOpenCommercial }}"><img src="/assets/icons/lucide/badge-dollar-sign.svg" alt="" aria-hidden="true">Carga comercial</button><button class="commercial-prices-link" onClick="{{ rosOpsOpenTvConfig }}"><img src="/assets/icons/lucide/settings.svg" alt="" aria-hidden="true">Configurar TV</button><button class="commercial-prices-link" onClick="{{ rosOpsOpenTv }}"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true">Abrir salida TV</button></sc-if><button class="commercial-prices-link price-board-story-export" onClick="{{ rosPriceExport }}" disabled="{{ rosPriceExportBusy }}" title="Descargar precios vendidos en imágenes PNG de 1080 à 1920"><img src="assets/icons/brands/instagram.svg" alt="">{{ rosPriceExportLabel }}</button><span class="price-board-updated">{{ rosPriceSource }}</span></div> 7059 <div class="price-board-view-switch" aria-label="Tipo de placa"><button onClick="{{ rosPriceCategoryView }}" aria-pressed="{{ rosPriceCategorySelected }}">Resultados por categorÃa</button><button onClick="{{ rosPriceWeightView }}" aria-pressed="{{ rosPriceWeightSelected }}">Por rango de kilaje</button></div> 7060 <p role="status">{{ rosPriceExportMessage }}</p><rosgan-price-board data-board="{{ rosPriceBoardData }}"></rosgan-price-board> 7061 7062 </div></main> 7063 </sc-if> 7064 7065 <sc-if value="{{ isRosganTvConfig }}" hint-placeholder-val="false"> 7066 <main class="rosgan-ops-page"> 7067 <div class="rosgan-ops-shell"> 7068 <button class="rosgan-ops-back" onClick="{{ rosOpsBack }}"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i>Volver al catálogo</button> 7069 <header class="rosgan-ops-hero"><div><small>CONTROL TV · ROSGAN</small><h1>Gráfica del remate</h1><p>{{ rosOpsTitle }} · los cambios se reflejan en la salida TV</p></div><div class="rosgan-ops-actions"><button onClick="{{ rosOpsOpenCommercial }}"><img src="/assets/icons/lucide/badge-dollar-sign.svg" alt="" aria-hidden="true">Comercial</button><button onClick="{{ rosOpsOpenPrices }}"><img src="/assets/icons/lucide/chart-no-axes-combined.svg" alt="" aria-hidden="true">Precios</button><button class="is-primary" onClick="{{ rosTvOpenOutput }}"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true">Abrir salida TV</button></div></header> 7070 <div class="rosgan-tv-config-grid"><section class="rosgan-tv-config-card"><h2>Lote al aire</h2><div class="rosgan-tv-selector"><button onClick="{{ rosTvPrev }}">Anterior</button><select value="" data-dc-attr-value="{{ rosTvSelectedCert }}" onChange="{{ rosTvSelect }}"><sc-for list="{{ rosTvOptions }}" as="option" hint-placeholder-count="8"><option value="" data-dc-attr-value="{{ option.value }}">{{ option.label }}</option></sc-for></select><button onClick="{{ rosTvNext }}">Siguiente</button></div><span class="rosgan-tv-url">{{ rosTvOutputUrl }}</span>
7070<p class="rosgan-tv-preview-note">El video del lote seleccionado se carga automáticamente, silenciado y en loop. La salida no requiere iniciar la reproducción manualmente.</p></section><section class="rosgan-tv-config-card"><h2>Elementos de pantalla</h2><div class="rosgan-tv-toggle-list"><button class="rosgan-tv-toggle {{ rosTvVideoClass }}" onClick="{{ rosTvToggleVideo }}" aria-pressed="{{ rosTvVideoVisible }}"><span>Videos y datos del lote</span><i></i></button><button class="rosgan-tv-toggle {{ rosTvVisibleClass }}" onClick="{{ rosTvToggleVisible }}"><span>Gráfica visible</span><i></i></button><button class="rosgan-tv-toggle {{ rosTvLogosClass }}" onClick="{{ rosTvToggleLogos }}"><span>Marcas superiores</span><i></i></button><button class="rosgan-tv-toggle {{ rosTvLowerClass }}" onClick="{{ rosTvToggleLower }}"><span>Zócalo del lote</span><i></i></button><button class="rosgan-tv-toggle {{ rosTvContactsClass }}" onClick="{{ rosTvToggleContacts }}"><span>Ticker de contactos</span><i></i></button></div></section></div> 7071 <section class="rosgan-tv-config-card" style="margin:16px 0"><h2>Conexión con vMix</h2><div style="display:flex;gap:12px;align-items:end;flex-wrap:wrap"><label class="ros-tv-edit-field" style="flex:1;min-width:220px">IP de vMix · puerto opcional<input aria-label="IP de vMix" data-dc-attr-value="{{ rosTvVmixHost }}" onInput="{{ rosTvVmixHostChange }}" placeholder="192.168.122.28:8088"></label><label class="ros-tv-edit-field" style="width:140px">Entrada Browser<input type="number" min="1" max="1000" aria-label="Entrada de vMix" data-dc-attr-value="{{ rosTvVmixInput }}" onInput="{{ rosTvVmixInputChange }}"></label><button class="ros-tv-save" onClick="{{ rosTvVmixSave }}" aria-label="Guardar conexión de vMix" title="Guardar conexión de vMix"><img src="assets/icons/lucide/save.svg" alt="" width="18" height="18"></button></div><p class="rosgan-tv-preview-note">Cronómetro y avisos usan el monitor local, que debe permanecer encendido en la red de vMix. Los cambios de IP y entrada se sincronizan desde rosgan.app. Puerto predeterminado: 8088. La entrada debe tener cargada la salida TV vinculada a este remate.</p><button class="rosgan-page-action" onClick="{{ rosTvVmixCopyUrl }}"><img src="/assets/icons/lucide/copy.svg" width="16" height="16" alt="">Copiar URL para vMix</button></section> 7072 <section class="rosgan-tv-config-card" style="margin-top:18px;"><h2>Zócalos de nombres</h2><p style="font-size:12px;color:#68839a;">Guardá nombres y cargos para presentarlos al aire. El zócalo de nombre reemplaza temporalmente los datos de los lotes.</p><div class="rtv-name-editor"><input aria-label="Nombre del zócalo" maxlength="100" placeholder="Nombre y apellido" data-dc-attr-value="{{ rosNameDraft }}" onInput="{{ rosNameChange }}"><input aria-label="Cargo del zócalo" maxlength="120" placeholder="Cargo (opcional)" data-dc-attr-value="{{ rosRoleDraft }}" onInput="{{ rosRoleChange }}"><button onClick="{{ rosNameSave }}">Guardar nombre</button></div><sc-for list="{{ rosNames }}" as="person"><div class="rtv-name-item"><div><strong>{{ person.name }}</strong><small>{{ person.role }}</small></div><button onClick="{{ person.toggle }}" aria-pressed="{{ person.active }}">{{ person.action }}</button><button onClick="{{ person.edit }}">Editar</button><button onClick="{{ person.remove }}" aria-label="Eliminar {{ person.name }}">â</button></div></sc-for></section> 7073 <section class="rosgan-tv-config-card" style="margin-top:18px;"><h2>Placas del remate</h2><p>La placa ocupa toda la salida. Al quitarla se recupera la gráfica anterior.</p><button class="rosgan-tv-toggle {{ rosWaitClass }}" onClick="{{ rosWaitToggle }}" aria-pressed="{{ rosWaitActive }}">Placa de espera<i></i></button><button class="rosgan-tv-toggle {{ rosEndClass }}" onClick="{{ rosEndToggle }}" aria-pressed="{{ rosEndActive }}">Fin del remate<i></i></button></section> 7074 <div class="rosgan-tv-workspace"> 7075 <section class="rosgan-tv-config-card ros-tv-catalog"><div class="ros-tv-catalog-heading"><h2>Todos los lotes · {{ rosOpsLotCount }}</h2><nav class="ros-tv-catalog-arrows" aria-label="Cambiar lote"><button onClick="{{ rosTvPrev }}" aria-label="Lote anterior" title="Lote anterior"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i></button><button onClick="{{ rosTvNext }}" aria-label="Lote siguiente" title="Lote siguiente"><i class="dc-lucide dc-lucide-arrow-right" aria-hidden="true"></i></button></nav></div> 7076 <input class="ros-tv-search" aria-label="Buscar lote" placeholder="Lote, certificado, categorÃa o consignataria" data-dc-attr-value="{{ rosTvQuery }}" onInput="{{ rosTvSearch }}"> 7077 <p class="rosgan-tv-preview-note">El seleccionado ocupa la pantalla 1. Usá 2, 3 o 4 para sumar otro lote; tocá el número activo para quitarlo.</p> 7078 <div class="ros-tv-multi-controls"><strong>{{ rosTvLayoutLabel }}</strong><sc-if value="{{ rosTvMulti }}
7078"><button onClick="{{ rosTvClearSlots }}">Volver a 1</button></sc-if></div> 7079 <div class="ros-tv-lot-list"><sc-for list="{{ rosTvLotList }}" as="lot"><div class="ros-tv-lot-row {{ lot.selectedClass }}"><button class="ros-tv-lot-main" disabled="{{ lot.disabled }}" onClick="{{ lot.select }}"> 7080 <span class="ros-tv-order">{{ lot.order }}</span><span class="ros-tv-lot-copy"><small>Cert. {{ lot.cert }} <sc-if value="{{ lot.sold }}"><span class="ros-tv-sold-check" role="img" aria-label="Vendido" title="Vendido"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="m7.5 12 3 3 6-6"/></svg></span></sc-if><sc-if value="{{ lot.disabled }}"><span class="ros-tv-lifted-label">Levantado</span></sc-if></small><strong>{{ lot.category }}</strong><span>{{ lot.consignataria }}</span></span> 7081 <span class="ros-tv-cons-mark" style="background:{{ lot.color }}"><img data-dc-attr-src="{{ lot.logo }}" alt="" loading="lazy"></span> 7082 </button><span class="ros-tv-slot-buttons"><sc-for list="{{ lot.slots }}" as="slot"><button class="{{ slot.className }}" aria-label="{{ slot.label }}" title="{{ slot.label }}" aria-pressed="{{ slot.active }}" disabled="{{ slot.disabled }}" onClick="{{ slot.select }}">{{ slot.number }}</button></sc-for></span></div></sc-for></div> 7083 </section> 7084 <section class="rosgan-tv-config-card"><div class="ros-tv-editor-heading"><h2>Editar lote · Cert. {{ rosTvSelectedCert }}</h2><button type="button" class="ros-tv-save" aria-label="Guardar cambios del lote" title="Guardar cambios y actualizar TV" aria-disabled="{{ rosTvSaveDisabled }}" onClick="{{ rosTvSave }}"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h12l4 4v12a2 2 0 0 1-2 2Z"/><path d="M7 3v6h10V3M7 21v-8h10v8"/></svg></button></div><p class="rosgan-tv-preview-note">Editá los datos y presioná guardar para actualizar el lote original y la salida de TV. El comitente no se puede modificar desde esta pantalla.</p><p role="status" class="ros-tv-save-status">{{ rosTvEditStatus }}</p> 7085 <div class="ros-tv-editor"><sc-for list="{{ rosTvEditorFields }}" as="field"><label key="{{ field.key }}" class="ros-tv-edit-field"><span>{{ field.label }}</span> 7086 <sc-if value="{{ field.isInput }}"><input data-dc-attr-type="{{ field.type }}" min="0" data-dc-attr-value="{{ field.value }}" onInput="{{ field.change }}"></sc-if> 7087 <sc-if value="{{ field.isTextarea }}"><textarea data-dc-attr-value="{{ field.value }}" onInput="{{ field.change }}" rows="3"></textarea></sc-if> 7088 <sc-if value="{{ field.isBoolean }}"><select data-dc-attr-value="{{ field.value }}" onChange="{{ field.change }}"><sc-if value="{{ field.nullableBoolean }}"><option value="">Sin dato</option></sc-if><option value="true">SÃ</option><option value="false">No</option></select></sc-if> 7089 <sc-if value="{{ field.isSelect }}"><select data-dc-attr-value="{{ field.value }}" onChange="{{ field.change }}"><sc-for list="{{ field.options }}" as="opt"><option data-dc-attr-value="{{ opt.value }}" data-dc-attr-label="{{ opt.label }}">{{ opt.label }}</option></sc-for></select></sc-if> 7090 <sc-if value="{{ field.isBreeds }}"><span class="ros-tv-breeds"><sc-for list="{{ field.breedOptions }}" as="breed"><button class="{{ breed.selectedClass }}" onClick="{{ breed.toggle }}">{{ breed.label }}</button></sc-for></span></sc-if> 7091 </label></sc-for></div> 7092 </section> 7093 </div> 7094 <section class="rosgan-tv-config-card ros-tv-contacts-section" aria-label="Contactos por consignataria"> 7095 <h2>Contactos del ticker por consignataria</h2> 7096 <p class="rosgan-tv-preview-note">Se guardan automáticamente y aparecen en el zócalo de los lotes de cada consignataria.</p> 7097 <div class="ros-tv-contact-grid"><sc-for list="{{ rosTvContactRows }}" as="contact"><label key="{{ contact.key }}" class="ros-tv-contact-card">
7098 <span class="ros-tv-contact-identity"><span class="ros-tv-contact-logo" style="background:{{ contact.color }}"><sc-if value="{{ contact.hasLogo }}"><img data-dc-attr-src="{{ contact.logo }}" alt=""></sc-if><sc-if value="{{ contact.noLogo }}">{{ contact.initial }}</sc-if></span><strong>{{ contact.name }}</strong></span> 7099 <span class="ros-tv-contact-caption">Teléfonos / contactos del ticker</span><input type="text" data-dc-attr-value="{{ contact.value }}" aria-label="{{ contact.label }}" onInput="{{ contact.change }}" placeholder="+54 9 ... | +54 9 ..."> 7100 </label></sc-for></div> 7101 </section> 7102 </div> 7103 </main> 7104 </sc-if> 7105 7106 <sc-if value="{{ isRosganTvLower }}" hint-placeholder-val="false"> 7107 <main class="rosgan-tv-output {{ rosTvOutputClass }}" aria-label="Salida de TV del remate"> 7108 <video data-rosgan-tv-multi-deck="1" muted loop playsinline preload="auto"></video> 7109 <video data-rosgan-tv-multi-deck="2" muted loop playsinline preload="auto"></video> 7110 <video data-rosgan-tv-multi-deck="3" muted loop playsinline preload="auto"></video> 7111 <video data-rosgan-tv-multi-deck="4" muted loop playsinline preload="auto"></video> 7112 <video data-rosgan-tv-multi-deck="5" muted loop playsinline preload="auto"></video> 7113 <video data-rosgan-tv-multi-deck="6" muted loop playsinline preload="auto"></video> 7114 <video data-rosgan-tv-multi-deck="7" muted loop playsinline preload="auto"></video> 7115 <video data-rosgan-tv-multi-deck="8" muted loop playsinline preload="auto"></video> 7116 <video data-rosgan-tv-deck="a" muted loop playsinline preload="auto" onEnded="{{ rosTvPlay }}"></video> 7117 <video data-rosgan-tv-deck="b" muted loop playsinline preload="auto" onEnded="{{ rosTvPlay }}"></video> 7118 <video data-rosgan-tv-deck="c" muted loop playsinline preload="auto" onEnded="{{ rosTvPlay }}"></video> 7119 <video data-rosgan-tv-deck="d" muted loop playsinline preload="auto" onEnded="{{ rosTvPlay }}"></video> 7120 <video data-rosgan-tv-deck="e" muted loop playsinline preload="auto" onEnded="{{ rosTvPlay }}"></video> 7121 <sc-if value="{{ rosPlateVisible }}"><section class="tv-waiting-screen rg-full-plate" aria-label="Placa del remate"><div class="tv-waiting-frame"><div class="tv-waiting-logos"><div class="tv-waiting-logo is-rosgan"><img src="/assets/rosgan-logo-blue.png" alt="Rosgan"></div><sc-for list="{{ rosPlateLogos }}" as="brand"><div class="tv-waiting-logo"><img data-dc-attr-src="{{ brand.logo }}" alt="{{ brand.name }}"></div></sc-for></div><sc-if value="{{ rosWaitActive }}"><div class="tv-waiting-body"><div class="tv-waiting-copy"><span class="tv-waiting-kicker">Transmisión oficial</span><h1 class="tv-waiting-title">{{ rosPlateTitle }}</h1><div class="tv-waiting-facts"><span class="tv-waiting-fact"><small>Fecha y hora</small><strong>{{ rosPlateDate }}</strong></span><span class="tv-waiting-fact"><small>Lugar</small><strong>{{ rosPlateLocation }}</strong></span></div></div><div class="tv-waiting-countdown"><rosgan-countdown start="{{ rosPlateStart }}"></rosgan-countdown></div></div></sc-if><sc-if value="{{ rosEndActive }}"><div class="tv-end-copy"><span class="tv-end-kicker">{{ rosPlateTitle }}</span><h1 class="tv-end-title">FIN DEL REMATE</h1><p class="tv-end-thanks">Gracias por acompañarnos</p></div></sc-if></div></section></sc-if> 7122 <section class="rtv-name-strap {{ rosNameStrapClass }}" aria-label="Presentación al aire" aria-hidden="{{ rosNameHidden }}"><div class="rtv-name-emblem"><img src="/assets/brand/rosgan-iso-blue.png" alt="Rosgan"></div><div class="rtv-name-copy"><strong class="rtv-name-title">{{ rosNameOnAirText }}</strong><sc-if value="{{ rosRoleOnAirText }}"><span class="rtv-name-subtitle">{{ rosRoleOnAirText }}</span></sc-if></div></section> 7123 <sc-if value="{{ rosTvShowSingleOverlay }}"> 7124 <sc-if value="{{ rosTvShowBrandBug }}"><div class="rtv-brand"><rosgan-brand-bug name="{{ rosTvConsignataria }}" iso="{{ rosTvConsLogo }}" full="{{ rosTvConsFullLogo }}" style="--brand:{{ rosTvConsColor }};"></rosgan-brand-bug></div></sc-if> 7125 <sc-if value="{{ rosTvShowLower }}"> 7126 <section class="rtv-lower" aria-label="Información del lote"> 7127 <sc-if value="{{ rosTvShowChina }}"><div class="rtv-china-badge"><img src="/assets/icons/china-flag.svg" alt="" aria-hidden="true"><span>Apto China</span></div></sc-if> 7128 <sc-if value="{{ rosTvShowVisec }}"><div class="rtv-china-badge rtv-visec-badge"><img src="/assets/icons/visec-mark.svg" alt="" aria-hidden="true"><span>Apto VISEC</span></div></sc-if> 7129 <sc-if value="{{ rosTvHasObservations }}"><div class="rtv-observations"><span class="rtv-observations-label">OBSERVACIONES</span><div class="rtv-observations-window"><div class="rtv-observations-track" style="--roll-duration:{{ rosTvObservationsDuration }}s;"><span class="rtv-observations-copy">{{ rosTvObservations }}</span><span class="rtv-observations-copy" aria-hidden="true">{{ rosTvObservations }}</span></div></div></div></sc-if> 7130 <div class="rtv-panel"> 7131 <div class="rtv-primary"> 7132 <sc-if value="{{ rosTvLotNumber }}"><div class="rtv-number"><small>LOTE</small><strong>{{ rosTvLotNumber }}</strong></div></sc-if> 7133 <div class="rtv-identity"><div class="rtv-identity-fixed"><div class="rtv-certificate">CERTIFICADO {{ rosTvCert }}</div><h1 class="rtv-title">{{ rosTvCategory }}</h1><sc-if value="{{ rosTvBreed }}"><div class="rtv-breed">{{ rosTvBreed }}</div></sc-if></div><sc-if value="{{ rosTvHasExtraFields }}"><div class="rtv-rotating"><div class="rtv-rotating-label"><span>{{ rosTvExtraLabel }}</span></div><div class="rtv-rotating-value">{{ rosTvExtraValue }}</div></div></sc-if></div> 7134 <div class="rtv-metrics"><sc-if value="{{ rosTvHasQuantity }}"><div class="rtv-metric"><small>CANTIDAD</small><strong class="rtv-quantity">{{ rosTvQuantity }}</strong><sc-if value="{{ rosTvQuantityFirst }}"><div class="rtv-quantity-breakdown"><span>{{ rosTvQuantityFirst }}</span><span>{{ rosTvQuantitySecond }}</span></div></sc-if></div></sc-if><sc-if value="{{ rosTvHasWeight }}"><div class="rtv-metric"><small>PESO PROMEDIO</small><strong>{{ rosTvWeight }}</strong></div></sc-if></div> 7135 </div> 7136 <div class="rtv-secondary"><sc-if value="{{ rosTvLocation }}"><div class="rtv-detail rtv-detail--location"><small>UBICACIÃN</small><sc-if value="{{ r.hasVenue }}" hint-placeholder-val="false"><strong style="display:block;font-size:12px;color:#112033;margin-bottom:4px;">{{ r.venue }}</strong></sc-if><strong>{{ rosTvLocation }}</strong></div></sc-if><sc-if value="{{ rosTvPayment }}"><div class="rtv-detail rtv-detail--payment"><small>PLAZO DE PAGO</small><strong>{{ rosTvPayment }}</strong></div></sc-if><sc-if value="{{ rosTvTick }}"><div class="rtv-detail"><small>GARRAPATA</small><strong>{{ rosTvTick }}</strong></div></sc-if><sc-if value="{{ rosTvPregnancy }}"><div class="rtv-detail"><small>PREÃEZ</small><strong>{{ rosTvPregnancy }}</strong></div></sc-if></div> 7137 </div> 7138 <sc-if value="{{ rosTvShowContacts }}"><div class="rtv-contacts"><span class="rtv-contacts-label">CONTACTO COMERCIAL</span><span class="rtv-contacts-copy">{{ rosTvContacts }}</span></div></sc-if> 7139 </section> 7140 </sc-if> 7141 </sc-if> 7142<sc-for list="{{ rosTvAirPanes }}" as="pane"><div class="ros-tv-air-pane" style="{{ pane.style }}"> <sc-if value="{{ pane.ShowBrandBug }}"><div class="rtv-brand"><rosgan-brand-bug name="{{ pane.Consignataria }}" iso="{{ pane.ConsLogo }}" full="{{ pane.ConsFullLogo }}" style="--brand:{{ pane.ConsColor }};"></rosgan-brand-bug></div></sc-if> 7143 <sc-if value="{{ pane.ShowLower }}"> 7144 <section class="rtv-lower" aria-label="Información del lote"> 7145 <sc-if value="{{ pane.ShowChina }}"><div class="rtv-china-badge"><img src="/assets/icons/china-flag.svg" alt="" aria-hidden="true"><span>Apto China</span></div></sc-if> 7146 <sc-if value="{{ pane.ShowVisec }}"><div class="rtv-china-badge rtv-visec-badge"><img src="/assets/icons/visec-mark.svg" alt="" aria-hidden="true"><span>Apto VISEC</span></div></sc-if> 7147 <sc-if value="{{ pane.HasObservations }}"><div class="rtv-observations"><span class="rtv-observations-label">OBSERVACIONES</span><div class="rtv-observations-window"><div class="rtv-observations-track" style="--roll-duration:{{ pane.ObservationsDuration }}s;"><span class="rtv-observations-copy">{{ pane.Observations }}</span><span class="rtv-observations-copy" aria-hidden="true">{{ pane.Observations }}</span></div></div></div></sc-if> 7148 <div class="rtv-panel"> 7149 <div class="rtv-primary"> 7150 <sc-if value="{{ pane.LotNumber }}"><div class="rtv-number"><small>LOTE</small><strong>{{ pane.LotNumber }}</strong></div></sc-if> 7151 <div class="rtv-identity"><div class="rtv-identity-fixed"><div class="rtv-certificate">CERTIFICADO {{ pane.Cert }}</div><h1 class="rtv-title">{{ pane.Category }}</h1><sc-if value="{{ pane.Breed }}"><div class="rtv-breed">{{ pane.Breed }}</div></sc-if></div><sc-if value="{{ pane.HasExtraFields }}"><div class="rtv-rotating"><div class="rtv-rotating-label"><span>{{ pane.ExtraLabel }}</span></div><div class="rtv-rotating-value">{{ pane.ExtraValue }}</div></div></sc-if></div> 7152 <div class="rtv-metrics"><sc-if value="{{ pane.HasQuantity }}"><div class="rtv-metric"><small>CANTIDAD</small><strong class="rtv-quantity">{{ pane.Quantity }}</strong><sc-if value="{{ pane.QuantityFirst }}"><div class="rtv-quantity-breakdown"><span>{{ pane.QuantityFirst }}</span><span>{{ pane.QuantitySecond }}</span></div></sc-if></div></sc-if><sc-if value="{{ pane.HasWeight }}"><div class="rtv-metric"><small>PESO PROMEDIO</small><strong>{{ pane.Weight }}</strong></div></sc-if></div> 7153 </div> 7154 <div class="rtv-secondary"><sc-if value="{{ pane.Location }}"><div class="rtv-detail rtv-detail--location"><small>UBICACIÃN</small><sc-if value="{{ r.hasVenue }}" hint-placeholder-val="false"><strong style="display:block;font-size:12px;color:#112033;margin-bottom:4px;">{{ r.venue }}</strong></sc-if><strong>{{ pane.Location }}</strong></div></sc-if><sc-if value="{{ pane.Payment }}"><div class="rtv-detail rtv-detail--payment"><small>PLAZO DE PAGO</small><strong>{{ pane.Payment }}</strong></div></sc-if><sc-if value="{{ pane.Tick }}"><div class="rtv-detail"><small>GARRAPATA</small><strong>{{ pane.Tick }}</strong></div></sc-if><sc-if value="{{ pane.Pregnancy }}"><div class="rtv-detail"><small>PREÃEZ</small><strong>{{ pane.Pregnancy }}</strong></div></sc-if></div> 7155 </div> 7156 <sc-if value="{{ pane.ShowContacts }}"><div class="rtv-contacts"><span class="rtv-contacts-label">CONTACTO COMERCIAL</span><span class="rtv-contacts-copy">{{ pane.Contacts }}</span></div></sc-if> 7157 </section> 7158 </sc-if> 7159</div></sc-for> 7160 </main> 7161 </sc-if> 7162 7163 <sc-if value="{{ isCabanaCommercial }}" hint-placeholder-val="false"> 7164 <main class="commercial-page"> 7165 <div class="commercial-shell"> 7166 <button class="commercial-back" onClick="{{ cbCommercialBack }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catalogo</button> 7167 <header class="commercial-hero"> 7168 <div>
7169 <span class="commercial-kicker">CARGA COMERCIAL</span> 7170 <h1>Precios del remate</h1> 7171 <p>{{ cbCommercialTitle }}. Carga y actualizacion de valores por RP.</p> 7172 </div> 7173 <div class="commercial-hero-actions"> 7174 <button class="commercial-prices-link" onClick="{{ cbCommercialOpenPrices }}"><img src="assets/icons/lucide/chart-no-axes-combined.svg" alt="">Ver placa de precios</button> 7175 <span class="commercial-autosave-note"><img src="assets/icons/lucide/save.svg" alt="">Autoguardado</span> 7176 </div> 7177 </header> 7178 <section class="cb-installment-settings"><div><strong>Preoferta por cuota</strong><p>{{ cbInstallmentSummary }}</p></div><button class="commercial-prices-link" onClick="{{ cbEditInstallments }}">Configurar cuotas</button></section> 7179 <section class="commercial-summary" aria-label="Resumen comercial"> 7180 <sc-for list="{{ cbCommercialMetrics }}" as="metric" hint-placeholder-count="4"> 7181 <article class="commercial-metric"> 7182 <span class="commercial-metric-icon" style="background:{{ metric.bg }};"><img data-dc-attr-src="{{ metric.iconUrl }}" alt=""></span> 7183 <span><small>{{ metric.label }}</small><strong>{{ metric.value }}</strong><em>{{ metric.pct }}</em></span> 7184 </article> 7185 </sc-for> 7186 </section> 7187 <nav class="commercial-filters" aria-label="Filtrar por estado"> 7188 <sc-for list="{{ cbCommercialFilters }}" as="filter" hint-placeholder-count="5"> 7189 <button class="{{ filter.className }}" onClick="{{ filter.set }}" style="background:{{ filter.bg }}; border-color:{{ filter.border }}; color:{{ filter.color }};">{{ filter.label }} <b style="background:{{ filter.pillBg }};">{{ filter.count }}</b></button> 7190 </sc-for> 7191 </nav> 7192 <div class="commercial-view-tabs" role="tablist" aria-label="Vista comercial"> 7193 <button class="{{ cbCommercialEntryTabClass }}" onClick="{{ cbCommercialShowEntry }}" role="tab" aria-selected="{{ cbCommercialIsEntry }}"><img src="assets/icons/lucide/pencil-line.svg" alt="">Carga de precios</button> 7194 <button class="{{ cbCommercialListTabClass }}" onClick="{{ cbCommercialShowList }}" role="tab" aria-selected="{{ cbCommercialIsList }}"><img src="assets/icons/lucide/table-2.svg" alt="">Listado y Excel</button> 7195 </div> 7196 <sc-if value="{{ cbCommercialIsEntry }}"> 7197 <section class="commercial-board" aria-label="Carga de precios por RP"> 7198 <div class="commercial-table-head"> 7199 <span>Orden</span><span>{{ cbCommercialGroupLabel }}</span><span>RP</span><span>N° / Pintura</span><span>Categoria</span><span>Consig.</span><span>Estado</span><span>Precio</span><span></span> 7200 </div> 7201 <sc-if value="{{ cbCommercialLoading }}"><div class="commercial-empty"><span class="dc-spinner"></span> Cargando RPs...</div></sc-if> 7202 <sc-if value="{{ cbCommercialHasRows }}"> 7203 <sc-for list="{{ cbCommercialRows }}" as="row" hint-placeholder-count="8"> 7204 <sc-if value="{{ row.showCategoryDivider }}"><div class="commercial-category-divider">{{ row.categoryDividerTitle }}</div></sc-if> 7205 <article class="commercial-row"> 7206 <div class="commercial-row-main"> 7207 <div><small class="commercial-row-muted">Orden</small><div class="commercial-order-badge">{{ row.order }}</div></div> 7208 <div><small class="commercial-row-muted">{{ row.groupLabel }}</small><div class="commercial-row-strong">{{ row.lotNumber }}</div></div> 7209 <div><small class="commercial-row-muted">RP</small><div class="commercial-row-strong">{{ row.rp }}</div></div> 7210 <div><small class="commercial-row-muted">N° / Pintura</small><div class="commercial-row-strong">{{ row.paintNumber }}</div></div> 7211 <div class="commercial-mobile-span"><small class="commercial-row-muted">Categoria</small><div class="commercial-category">{{ row.category }}</div></div> 7212 <div><span class="commercial-cons-avatar" style="background:{{ row.consColor }};"><sc-if value="{{ row.hasConsLogo }}"><img data-dc-attr-src="{{ row.consLogo }}" alt=""></sc-if><sc-if value="{{ row.noConsLogo }}"><strong>{{ row.consInitial }}</strong></sc-if></span></div> 7213 <div class="commercial-status commercial-mobile-span">
7214 <span class="commercial-status-pill" style="background:{{ row.statusBg }}; border-color:{{ row.statusBorder }}; color:{{ row.statusColor }};">{{ row.statusLabel }}</span> 7215 <sc-for list="{{ row.statusOptions }}" as="status" hint-placeholder-count="4"> 7216 <button class="{{ status.className }}" onClick="{{ status.set }}">{{ status.label }}</button> 7217 </sc-for> 7218 </div> 7219 <div class="commercial-price"><button class="cb-installment-edit" onClick="{{ row.editInstallments }}">{{ row.installmentLabel }}</button> 7220 <div class="commercial-mode"><button class="{{ row.kiloModeClass }}" onClick="{{ row.setKilo }}" disabled="{{ row.priceDisabled }}">$ / Kilo</button><button class="{{ row.cabezaModeClass }}" onClick="{{ row.setCabeza }}" disabled="{{ row.priceDisabled }}">{{ row.headPriceLabel }}</button></div> 7221 <input value="" data-dc-attr-value="{{ row.priceValue }}" onInput="{{ row.setPrice }}" onBlur="{{ row.commitPrice }}" onKeyDown="{{ row.commitPriceEnter }}" disabled="{{ row.priceDisabled }}" inputmode="numeric" placeholder="{{ row.pricePlaceholder }}"><small>{{ row.installmentTotal }}</small> 7222 </div> 7223 <button class="commercial-more-button" onClick="{{ row.toggleMore }}" title="Mas info" aria-label="Mas info"><img data-dc-attr-src="{{ row.moreIcon }}" alt=""></button> 7224 </div> 7225 <div class="{{ row.moreClassName }}"> 7226 <div class="commercial-status commercial-more-status"> 7227 <sc-for list="{{ row.statusOptions }}" as="status" hint-placeholder-count="4"> 7228 <button class="{{ status.className }}" onClick="{{ status.set }}">{{ status.label }}</button> 7229 </sc-for> 7230 </div> 7231 <div class="commercial-more-grid"> 7232 <label>Cantidad<input disabled="{{ row.hasDivisions }}" value="" data-dc-attr-value="{{ row.cantidad }}" onInput="{{ row.setCantidad }}" inputmode="numeric" placeholder="Cantidad"></label> 7233 <label>Peso prom.<input value="" data-dc-attr-value="{{ row.pesoProm }}" onInput="{{ row.setPesoProm }}" inputmode="decimal" placeholder="Kg"></label> 7234 <label>Plazo<input value="" data-dc-attr-value="{{ row.plazo }}" onInput="{{ row.setPlazo }}" placeholder="Ej: 30 dias"></label> 7235 <label>Comprador<input disabled="{{ row.hasDivisions }}" value="" data-dc-attr-value="{{ row.comprador }}" onInput="{{ row.setComprador }}" placeholder="Nombre o razon social"></label> 7236 </div> 7237 <button class="commercial-prices-link" onClick="{{ row.divideSale }}">{{ row.divisionLabel }}</button> 7238 </div> 7239 </article> 7240 </sc-for> 7241 </sc-if> 7242 <sc-if value="{{ cbCommercialEmpty }}"><div class="commercial-empty">No hay RPs para este filtro.</div></sc-if> 7243 </section> 7244 </sc-if> 7245 <sc-if value="{{ cbCommercialIsList }}"> 7246 <section class="commercial-report" aria-label="Listado de resultados del remate"> 7247 <header class="commercial-report-head"> 7248 <div><h2>Listado de resultados</h2><p>Lotes y reproductores del remate con identificación, estado y precio cargado.</p></div> 7249 <button class="commercial-export-excel" onClick="{{ cbCommercialExportExcel }}"><img src="assets/icons/lucide/file-spreadsheet.svg" alt="">Exportar Excel</button> 7250 </header> 7251 <sc-if value="{{ cbCommercialLoading }}"><div class="commercial-empty"><span class="dc-spinner"></span> Preparando listado...</div></sc-if> 7252 <sc-if value="{{ cbCommercialHasReportRows }}"> 7253 <div class="commercial-report-scroll"> 7254 <table class="commercial-report-table"> 7255 <thead><tr><th>Orden</th><th>{{ cbCommercialGroupLabel }}</th><th>RP</th><th>N° / Pintura</th><th>CategorÃa</th><th>Cabaña</th><th>Consignataria</th><th>Cantidad</th><th>Estado</th><th>
7255Modalidad</th><th style="text-align:right;">Precio</th><th style="text-align:right;">X / Total</th></tr></thead> 7256 <tbody> 7257 <sc-for list="{{ cbCommercialReportRows }}" as="row" hint-placeholder-count="8"> 7258 <tr> 7259 <td class="is-strong" data-label="Orden">{{ row.order }}</td><td data-dc-attr-data-label="{{ row.groupLabel }}">{{ row.lotNumber }}</td><td class="is-strong" data-label="RP">{{ row.rp }}</td><td data-label="N° / Pintura">{{ row.paintNumber }}</td><td data-label="CategorÃa">{{ row.category }}</td><td data-label="Cabaña">{{ row.cabana }}</td><td data-label="Consignataria">{{ row.consignataria }}</td><td class="is-number" data-label="Cantidad">{{ row.cantidad }}</td> 7260 <td data-label="Estado"><span class="commercial-report-status" style="background:{{ row.statusBg }}; border-color:{{ row.statusBorder }}; color:{{ row.statusColor }};">{{ row.statusLabel }}</span></td><td class="is-muted" data-label="Modalidad">{{ row.priceUnitLabel }}</td><td class="is-number is-strong" data-label="Precio">{{ row.priceDisplay }}</td><td class="is-number is-strong" data-label="X / Total">{{ row.totalDisplay }}</td> 7261 </tr> 7262 </sc-for> 7263 </tbody> 7264 </table> 7265 </div> 7266 <footer class="commercial-report-foot"><span>{{ cbCommercialReportCountLabel }}</span><span>El Excel incluye también peso promedio, plazo y comprador cuando están cargados.</span></footer> 7267 </sc-if> 7268 <sc-if value="{{ cbCommercialReportEmpty }}"><div class="commercial-empty">No hay lotes o RPs para mostrar.</div></sc-if> 7269 </section> 7270 </sc-if> 7271 </div> 7272 </main> 7273 </sc-if> 7274 <sc-if value="{{ isCabanaPriceBoard }}" hint-placeholder-val="false"> 7275 <main class="price-board-page"> 7276 <div class="price-board-shell"> 7277 <button class="commercial-back" onClick="{{ pbBackCatalog }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catalogo</button> 7278 <div class="price-board-toolbar"> 7279 <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;"><button class="commercial-prices-link price-board-story-export" onClick="{{ pbExportStory }}"><img src="assets/icons/brands/instagram.svg" alt="">{{ pbStoryLabel }}</button><div class="price-board-updated">{{ pbUpdatedLabel }}</div></div> 7280 </div> 7281 <rosgan-price-board data-board="{{ pbBoardData }}"></rosgan-price-board> 7282 </div> 7283 </main> 7284 </sc-if> 7285 <sc-if value="{{ isCabanaTvConfig }}" hint-placeholder-val="false"> 7286 <main class="tv-config-page"> 7287 <div class="tv-config-shell"> 7288 <button class="tv-config-back" onClick="{{ tvConfigBack }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catalogo</button> 7289 <header class="tv-config-hero"> 7290 <div> 7291 <small>Control TV</small> 7292 <h1>Zocalo del remate</h1> 7293 <p>Seleccion de lote y campos visibles. Los cambios se reflejan en tiempo real en la salida /tv.</p> 7294 </div> 7295 <div class="tv-config-live"> 7296 <span>{{ tvVisibleLabel }}</span> 7297 <button class="{{ tvVisibleSwitchClass }}" onClick="{{ tvToggleVisible }}" aria-label="Mostrar u ocultar toda la grafica"><span></span></button> 7298 </div> 7299 </header> 7300 <section class="tv-config-panel"> 7301 <div class="tv-config-toolbar"> 7302 <button onClick="{{ tvPrev }}">Anterior</button> 7303 <select value="" data-dc-attr-value="{{ tvSelectedKey }}" onChange="{{ tvSelectLot }}"> 7304 <sc-for list="{{ tvLotOptions }}" as="opt" hint-placeholder-count="8"> 7305 <option value="" data-dc-attr-value="{{ opt.key }}">{{ opt.label }}</option> 7306 </sc-for> 7307 </select> 7308 <button onClick="{{ tvNext }}">Siguiente</button> 7309 </div> 7310 <div class="tv-config-fields"> 7311 <div class="tv-config-side"> 7312 <article class="tv-config-card"> 7313 <div class="tv-config-card-head"> 7314 <h2>Elementos de pantalla</h2> 7315 </div> 7316 <div class="tv-config-screen-elements"> 7317 <sc-for list="{{ tvScreenElements }}" as="item" hint-placeholder-count="4"> 7318 <button class="{{ item.className }}" onClick="{{ item.toggle }}" disabled="{{ item.disabled }}" type="button"><span>{{ item.label }}</span><i><b></b></i></button> 7319 </sc-for> 7320 </div> 7321 <label class="tv-config-contacts">Teléfonos del ticker 7322 <textarea data-dc-attr-value="{{ tvContactsText }}" onChange="{{ tvContactsChange }}" maxlength="2000" placeholder="Nombre · teléfono | Nombre · teléfono" aria-label="Teléfonos del ticker"></textarea> 7323 <small>Separá los contactos con |. Se guardan automáticamente y se muestran en la salida TV.</small> 7324 </label> 7325 <sc-if value="{{ tvVideosConfigVisible }}" hint-placeholder-val="false"> 7326 <div class="tv-config-video-mode"> 7327 <sc-for list="{{ tvVideoModeOptions }}" as="mode" hint-placeholder-count="2"> 7328 <button class="{{ mode.className }}" onClick="{{ mode.select }}" type="button">{{ mode.label }}</button> 7329 </sc-for> 7330 </div> 7331 <sc-if value="{{ tvVideoTransportVisible }}" hint-placeholder-val="false"> 7332 <div class="tv-config-transport" aria-label="Control de reproduccion por RP"> 7333 <div class="tv-config-transport-head">
7334 <span><small>Reproduccion por RP</small><strong>{{ tvVideoTransportRp }}</strong></span> 7335 <em>{{ tvVideoPlaybackStatus }}</em> 7336 </div> 7337 <select value="" data-dc-attr-value="{{ tvVideoSelectedKey }}" onChange="{{ tvSelectVideoRp }}" disabled="{{ tvVideoTransportDisabled }}"> 7338 <sc-for list="{{ tvVideoRpOptions }}" as="video" hint-placeholder-count="4"> 7339 <option value="" data-dc-attr-value="{{ video.key }}">{{ video.label }}</option> 7340 </sc-for> 7341 </select> 7342 <div class="tv-config-transport-controls"> 7343 <button onClick="{{ tvPrevVideoRp }}" disabled="{{ tvVideoTransportDisabled }}" type="button">RP anterior</button> 7344 <button onClick="{{ tvRestartVideo }}" disabled="{{ tvVideoTransportDisabled }}" type="button">Reiniciar</button> 7345 <button class="is-primary" onClick="{{ tvToggleVideoPlayback }}" disabled="{{ tvVideoTransportDisabled }}" type="button">{{ tvVideoPlaybackAction }}</button> 7346 <button onClick="{{ tvNextVideoRp }}" disabled="{{ tvVideoTransportDisabled }}" type="button">RP siguiente</button> 7347 </div> 7348 </div> 7349 </sc-if> 7350 </sc-if> 7351 </article> 7352 <article class="tv-config-card"> 7353 <h2>{{ tvGroupListTitle }}</h2> 7354 <div class="tv-config-list"> 7355 <sc-for list="{{ tvLotRows }}" as="lot" hint-placeholder-count="8"> 7356 <button class="{{ lot.className }}" onClick="{{ lot.select }}" disabled="{{ lot.disabled }}"><strong>{{ lot.lotNumber }}</strong><span>{{ lot.category }}</span><small>{{ lot.count }} RP</small><i class="{{ lot.statusClass }}" aria-label="{{ lot.statusLabel }}"><sc-if value="{{ lot.statusIsLifted }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-minus" aria-hidden="true"></i></sc-if><sc-if value="{{ lot.statusIsResolved }}" hint-placeholder-val="{{ false }}"><i class="dc-lucide dc-lucide-check" aria-hidden="true"></i></sc-if></i></button> 7357 </sc-for> 7358 </div> 7359 </article> 7360 </div> 7361 <article class="tv-config-card"> 7362 <div class="tv-config-card-head"> 7363 <h2>Campos por categoria</h2> 7364 <div class="tv-config-mini-toggle"> 7365 <span>{{ tvDepsLabel }}</span> 7366 <button class="{{ tvDepsSwitchClass }}" onClick="{{ tvToggleDeps }}" aria-label="Activar DEPs y Precision"><span></span></button> 7367 </div> 7368 </div> 7369 <div class="tv-config-category-fields"> 7370 <sc-for list="{{ tvCategoryFieldGroups }}" as="group" hint-placeholder-count="4"> 7371 <section class="tv-config-category-card"> 7372 <h3>{{ group.label }}</h3> 7373 <div class="tv-config-subforms"> 7374 <sc-for list="{{ group.subforms }}" as="subform" hint-placeholder-count="3"> 7375 <div class="tv-config-subform"> 7376 <h4>{{ subform.label }}</h4> 7377 <div class="tv-config-checks"> 7378 <sc-for list="{{ subform.fields }}" as="field" hint-placeholder-count="5"> 7379 <label class="tv-config-check"><input type="checkbox" checked="{{ field.checked }}" onChange="{{ field.toggle }}">{{ field.label }}</label> 7380 </sc-for> 7381 </div> 7382 </div> 7383 </sc-for> 7384 </div> 7385 </section> 7386 </sc-for> 7387 </div> 7388 </article> 7389 </div> 7390 </section> 7391 </div> 7392 </main> 7393 </sc-if> 7394 <sc-if value="{{ isCabanaTvLower }}" hint-placeholder-val="false"> 7395 <main class="tv-lower-page"> 7396 <div class="tv-lower-stage"> 7397 <sc-if value="{{ tvShowControls }}"> 7398 <div class="tv-lower-control"> 7399 <button onClick="{{ tvPrev }}">Anterior</button> 7400 <select value="" data-dc-attr-value="{{ tvSelectedKey }}" onChange="{{ tvSelectLot }}"> 7401 <sc-for list="{{ tvLotOptions }}" as="opt" hint-placeholder-count="8"> 7402 <option value="" data-dc-attr-value="{{ opt.key }}">{{ opt.label }}</option> 7403 </sc-for> 7404 </select> 7405 <button onClick="{{ tvNext }}">Siguiente</button> 7406 </div> 7407 </sc-if> 7408 <sc-if value="{{ tvHasLot }}"> 7409 <sc-if value="{{ tvChannelBugVisible }}"> 7410 <div class="tv-channel-bug" style="--tv-cons-color:{{ tvConsColor }};
7410--tv-cabana-color:{{ tvCabanaColor }};--tv-cabana-logo-filter:{{ tvCabanaLogoFilter }};--tv-cabana-logo-background:{{ tvCabanaLogoBackground }};"> 7411 <sc-if value="{{ tvLogosVisible }}"> 7412 <div class="tv-bug-strip"> 7413 <div class="tv-bug-part tv-bug-rosgan"><img class="tv-bug-logo" data-dc-attr-src="{{ tvRosganLogo }}" alt="Rosgan"></div> 7414 <div class="tv-bug-part tv-bug-cons"><sc-if value="{{ tvHasConsLogo }}"><img class="tv-bug-logo" data-dc-attr-src="{{ tvConsLogo }}" alt=""></sc-if><sc-if value="{{ tvNoConsLogo }}"><strong>{{ tvConsInitial }}</strong></sc-if><span class="tv-bug-cons-name">{{ tvConsName }}</span></div> 7415 <div class="tv-bug-part tv-bug-cabana"><sc-if value="{{ tvHasLogo }}"><img class="tv-bug-logo" data-dc-attr-src="{{ tvLogo }}" alt="{{ tvCabanaName }}" onError="{{ tvLogoError }}"></sc-if><sc-if value="{{ tvNoLogo }}"><strong>{{ tvCabanaName }}</strong></sc-if></div> 7416 </div> 7417 </sc-if> 7418 <sc-if value="{{ tvLiveBugVisible }}"> 7419 <div class="tv-bug-live"><sc-if value="{{ tvClockVisible }}"><span class="tv-bug-clock"><img src="assets/icons/lucide/clock-3.svg" alt="" aria-hidden="true">{{ tvClockLabel }}</span></sc-if><sc-if value="{{ tvKickerVisible }}"><span class="tv-bug-live-card"><b>Vivo</b><span>{{ tvKicker }}</span></span></sc-if></div> 7420 </sc-if> 7421 </div> 7422 </sc-if> 7423 <sc-if value="{{ tvWeatherVisible }}"> 7424 <div class="tv-weather-bug"> 7425 <img class="tv-weather-icon" data-dc-attr-src="{{ tvWeatherIcon }}" alt=""> 7426 <span class="tv-weather-temp">{{ tvWeatherTemp }}</span> 7427 <span class="tv-weather-copy"><strong>{{ tvWeatherLabel }}</strong><small>{{ tvWeatherLocation }}</small></span> 7428 </div> 7429 </sc-if> 7430 <sc-if value="{{ tvWaitingVisible }}"> 7431 <section class="tv-waiting-screen tv-waiting-broadcast" style="--tv-wait-color:{{ tvConsColor }};" aria-label="Placa de espera del remate"> 7432 <span class="tv-waiting-orb is-one"></span> 7433 <span class="tv-waiting-orb is-two"></span> 7434 <div class="tv-waiting-frame"> 7435 <div class="tv-waiting-logos"> 7436 <div class="tv-waiting-logo is-rosgan"><img data-dc-attr-src="{{ tvRosganLogo }}" alt="Rosgan"></div> 7437 <div class="tv-waiting-logo" style="--tv-wait-logo-filter:{{ tvWaitingConsLogoFilter }};"> 7438 <sc-if value="{{ tvHasConsLogo }}"><img data-dc-attr-src="{{ tvConsLogo }}" alt="{{ tvConsName }}"></sc-if> 7439 <sc-if value="{{ tvNoConsLogo }}"><strong>{{ tvConsName }}</strong></sc-if> 7440 </div> 7441 <div class="tv-waiting-logo" style="--tv-wait-logo-filter:{{ tvCabanaLogoFilter }};"> 7442 <sc-if value="{{ tvHasLogo }}"><img data-dc-attr-src="{{ tvLogo }}" alt="{{ tvCabanaName }}" onError="{{ tvLogoError }}"></sc-if> 7443 <sc-if value="{{ tvNoLogo }}"><strong>{{ tvCabanaName }}</strong></sc-if> 7444 </div> 7445 </div> 7446 <div class="tv-waiting-body"> 7447 <div class="tv-waiting-copy"> 7448 <span class="tv-waiting-kicker">Transmisión oficial</span> 7449 <h1 class="tv-waiting-title">{{ tvWaitingTitle }}</h1> 7450 <div class="tv-waiting-facts"> 7451 <span class="tv-waiting-fact"><small>Fecha</small><strong>{{ tvWaitingDate }}</strong></span> 7452 <span class="tv-waiting-fact"><small>Hora</small><strong>{{ tvWaitingTime }}</strong></span> 7453 <span class="tv-waiting-fact is-location"><small>Localidad</small><strong>{{ tvWaitingLocation }}</strong></span> 7454 </div> 7455 </div> 7456 <div class="tv-waiting-countdown"> 7457 <span class="tv-waiting-countdown-label">{{ tvWaitingCountdownLabel }}</span> 7458 <sc-if value="{{ tvWaitingCountdownActive }}"> 7459 <div class="tv-waiting-count-grid"> 7460 <sc-for list="{{ tvWaitingCountdownParts }}" as="part" hint-placeholder-count="4">
7461 <span class="tv-waiting-count-part"><strong>{{ part.value }}</strong><small>{{ part.label }}</small></span> 7462 </sc-for> 7463 </div> 7464 </sc-if> 7465 <sc-if value="{{ tvWaitingMessageVisible }}"><strong class="tv-waiting-message">{{ tvWaitingMessage }}</strong></sc-if> 7466 <span class="tv-waiting-local-time">Hora Argentina <strong>{{ tvWaitingNow }}</strong></span> 7467 </div> 7468 </div> 7469 <footer class="tv-waiting-broadcast-footer"><span><i></i> Transmisión oficial · Rosgan</span></footer> 7470 </div> 7471 </section> 7472 </sc-if> 7473 <sc-if value="{{ tvEndVisible }}"> 7474 <section class="tv-waiting-screen tv-end-screen" style="--tv-wait-color:{{ tvConsColor }};" aria-label="Placa de fin del remate"> 7475 <span class="tv-waiting-orb is-one"></span> 7476 <span class="tv-waiting-orb is-two"></span> 7477 <div class="tv-waiting-frame tv-end-frame"> 7478 <div class="tv-waiting-logos tv-end-logos"> 7479 <div class="tv-waiting-logo is-rosgan"><img data-dc-attr-src="{{ tvRosganLogo }}" alt="Rosgan"></div> 7480 <div class="tv-waiting-logo" style="--tv-wait-logo-filter:{{ tvWaitingConsLogoFilter }};"> 7481 <sc-if value="{{ tvHasConsLogo }}"><img data-dc-attr-src="{{ tvConsLogo }}" alt="{{ tvConsName }}"></sc-if> 7482 <sc-if value="{{ tvNoConsLogo }}"><strong>{{ tvConsName }}</strong></sc-if> 7483 </div> 7484 <div class="tv-waiting-logo" style="--tv-wait-logo-filter:{{ tvCabanaLogoFilter }};"> 7485 <sc-if value="{{ tvHasLogo }}"><img data-dc-attr-src="{{ tvLogo }}" alt="{{ tvCabanaName }}" onError="{{ tvLogoError }}"></sc-if> 7486 <sc-if value="{{ tvNoLogo }}"><strong>{{ tvCabanaName }}</strong></sc-if> 7487 </div> 7488 </div> 7489 <div class="tv-end-copy"> 7490 <span class="tv-end-kicker">Transmisión oficial</span> 7491 <h1 class="tv-end-title">FIN DEL REMATE</h1> 7492 <p class="tv-end-thanks">Gracias por acompañarnos</p> 7493 </div> 7494 </div> 7495 </section> 7496 </sc-if> 7497 <sc-if value="{{ tvLowerThirdVisible }}"> 7498 <section class="{{ tvLowerClass }}"> 7499 <div class="tv-lower-topline"> 7500 <div class="tv-lower-title-line"><strong><span class="{{ tvCategoryRollClass }}">{{ tvCategory }}</span><span class="tv-title-count"><span class="tv-title-dot">·</span> Cantidad: <b class="{{ tvCountRollClass }}">{{ tvCount }}</b></span><sc-if value="{{ tvShowingDeps }}" hint-placeholder-val="false"><span class="tv-deps-pill">{{ tvDepsModeLabel }}</span></sc-if></strong></div> 7501 <div class="tv-lower-meta"></div> 7502 </div> 7503 <div class="tv-lower-lot-panel"> 7504 <div class="tv-lower-logo"><strong>{{ tvCabanaName }}</strong></div> 7505 <div class="tv-lower-lot"><span class="{{ tvLotRollClass }}">{{ tvLotLabel }}</span><sc-if value="{{ tvHasActiveRp }}" hint-placeholder-val="false"><small class="{{ tvActiveRpRollClass }}">{{ tvActiveRpLabel }}</small></sc-if></div> 7506 </div> 7507 <div class="{{ tvTableClass }}" style="--tv-cols:{{ tvGridCols }};"> 7508 <sc-if value="{{ tvShowTableHead }}" hint-placeholder-val="true"> 7509 <div class="tv-lower-row tv-lower-head"> 7510 <sc-for list="{{ tvColumns }}" as="col" hint-placeholder-count="6"> 7511 <span>{{ col.label }}</span> 7512 </sc-for> 7513 </div> 7514 </sc-if> 7515 <sc-for list="{{ tvRows }}" as="row" hint-placeholder-count="3"> 7516 <div class="{{ row.className }}"> 7517 <sc-for list="{{ row.cells }}" as="cell" hint-placeholder-count="6">
7518 <span class="{{ cell.className }}"><i class="{{ cell.rollClass }}">{{ cell.value }}</i></span> 7519 </sc-for> 7520 </div> 7521 </sc-for> 7522 </div> 7523 </section> 7524 </sc-if> 7525 </sc-if> 7526 <sc-if value="{{ tvShowContacts }}"><aside class="tv-phone-ticker" aria-label="Contacto comercial"><strong>CONTACTO COMERCIAL</strong><div class="tv-phone-ticker-window"><div class="tv-phone-ticker-track"><span>{{ tvContactsText }}</span><span aria-hidden="true">{{ tvContactsText }}</span></div></div></aside></sc-if> 7527 <sc-if value="{{ tvEmpty }}"><div class="tv-lower-empty">Cargando zocalo...</div></sc-if> 7528 </div> 7529 </main> 7530 </sc-if> 7531 <sc-if value="{{ isRosganSalesOrder }}" hint-placeholder-val="false"> 7532 <main class="sales-order-page"> 7533 <div class="sales-order-shell"> 7534 <button class="sales-order-back" onClick="{{ closeRgSalesOrder }}"><img src="assets/icons/lucide/arrow-left.svg" alt="" width="15" height="15"> Volver al remate</button> 7535 <header class="sales-order-head"> 7536 <div> 7537 <span class="sales-order-kicker">ADMINISTRACIÃN DEL REMATE</span> 7538 <h1>Orden de venta</h1> 7539 <p>{{ rgSalesOrderTitle }}</p> 7540 </div> 7541 <div class="sales-order-head-actions rg-order-save"> <button class="sales-order-action primary" onClick="{{ saveRgSalesOrder }}" style="opacity:{{ rgSalesOrderSaveOpacity }};"> 7542 <img src="assets/icons/lucide/save.svg" alt=""> 7543 {{ rgSalesOrderSaveLabel }} 7544 </button> 7545</div> 7546 </header> 7547 <div class="rg-order-tools" aria-label="Herramientas del orden de venta"> 7548 <section class="rg-order-tool-group"><h2>Excel de trabajo</h2><p>Descargá el preorden o importá el orden preparado.</p><div class="rg-order-tool-buttons"> <button class="sales-order-action" onClick="{{ exportRgPreorderExcel }}" disabled="{{ rgPreorderDisabled }}" title="Descargar preorden completo de la consignataria seleccionada, ordenado por categorÃa y peso"><img src="assets/icons/lucide/file-spreadsheet.svg" alt="">{{ rgPreorderLabel }}</button> 7549 <label class="sales-order-action sales-order-import {{ rgSalesOrderImportClass }}"> 7550 <img src="assets/icons/lucide/file-spreadsheet.svg" alt=""> 7551 {{ rgSalesOrderImportLabel }} 7552 <input type="file" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" onChange="{{ importRgSalesOrderExcel }}" aria-label="Cargar Excel con el orden de venta"> 7553 </label> 7554</div></section> 7555 <section class="rg-order-tool-group"><h2>Catálogo</h2><p>Controlá la publicación y prepará sus páginas.</p><div class="rg-order-tool-buttons"> <button class="sales-order-action" role="switch" aria-label="Publicar Catálogo" aria-checked="{{ rgCatalogPublished }}" aria-disabled="{{ rgCatalogBusy }}" onClick="{{ toggleRgCatalog }}" style="opacity:{{ rgCatalogOpacity }};"> 7556 <span style="width:34px;height:20px;border-radius:20px;background:{{ rgCatalogSwitchColor }};padding:3px;box-sizing:border-box;display:flex;align-items:center;"><span style="display:block;width:14px;height:14px;border-radius:50%;background:white;transform:translateX({{ rgCatalogSwitchOffset }}px);"></span></span> 7557 Publicar Catálogo 7558 </button> 7559</div><details class="rg-order-catalog-settings"><summary>Diseño del catálogo <span aria-hidden="true">â</span></summary><div class="rg-order-tool-buttons"> <sc-if value="{{ rgCanToggleLotLoading }}"><label class="sales-order-action sales-order-import"><img src="assets/icons/lucide/image.svg" alt="">Cargar tapa<input type="file" accept="image/jpeg,image/png" onChange="{{ uploadRgCatalogCover }}" aria-label="Cargar tapa del catálogo"></label></sc-if> 7560 <sc-if value="{{ rgCanToggleLotLoading }}">
7560<button class="sales-order-action" onClick="{{ configureRgCatalogNews }}">Páginas de noticias</button></sc-if> 7561 <sc-if value="{{ rgCanToggleLotLoading }}"><label class="sales-order-action sales-order-import"><img src="assets/icons/lucide/image.svg" alt="">Cargar contratapa<input type="file" accept="image/jpeg,image/png" onChange="{{ uploadRgCatalogBackCover }}" aria-label="Cargar contratapa del catálogo"></label></sc-if> 7562</div></details></section> 7563 <sc-if value="{{ rgCanToggleLotLoading }}"><section class="rg-order-tool-group"><h2>Carga de lotes</h2><p>Permità o cerrá el ingreso de nuevos lotes.</p><div class="rg-order-tool-buttons"> <sc-if value="{{ rgCanToggleLotLoading }}"><button type="button" class="sales-order-action lot-loading-switch" role="switch" aria-label="Habilitar carga de lotes" aria-checked="{{ rgLotLoadingEnabled }}" disabled="{{ rgLotLoadingBusy }}" onClick="{{ toggleRgLotLoading }}"><span class="lot-loading-track" aria-hidden="true"></span>{{ rgLotLoadingLabel }}</button></sc-if> 7564</div></section></sc-if> 7565 </div> 7566 <div class="sales-order-summary"><strong>{{ rgSalesOrderCount }}</strong> lotes. Usá las flechas o elegà una posición para definir el orden visible en el catálogo.</div> 7567 <sc-if value="{{ rgSalesOrderHasImport }}"><div class="sales-order-import-summary"><strong>{{ rgSalesOrderImportFile }}</strong><span>Inicio {{ rgSalesOrderImportStart }}</span><span>{{ rgSalesOrderImportGroups }} grupos</span><span>Fin estimado {{ rgSalesOrderImportEnd }}</span><small>Los horarios se recalculan con el promedio de cada alias.</small></div></sc-if> 7568 <div class="sales-order-filterbar"> 7569 <label class="sales-order-search"><img src="assets/icons/lucide/search.svg" alt=""><input type="search" placeholder="Buscar certificado, posición, categorÃa, localidad, razaâ¦" value="{{ rgSalesOrderQuery }}" onInput="{{ setRgSalesOrderQuery }}" aria-label="Buscar lotes del orden de venta"></label> 7570 <select value="{{ rgSalesOrderStatusFilter }}" onChange="{{ setRgSalesOrderStatusFilter }}" aria-label="Filtrar por estado"><sc-for list="{{ rgSalesOrderStatusOptions }}" as="option"><option value="{{ option.value }}">{{ option.label }}</option></sc-for></select> 7571 <select value="{{ rgSalesOrderCategoryFilter }}" onChange="{{ setRgSalesOrderCategoryFilter }}
7571" aria-label="Filtrar por categorÃa"><sc-for list="{{ rgSalesOrderCategoryOptions }}" as="option"><option value="{{ option.value }}">{{ option.label }}</option></sc-for></select> 7572 <select value="{{ rgSalesOrderConsignatariaFilter }}" onChange="{{ setRgSalesOrderConsignatariaFilter }}" aria-label="Filtrar por consignataria"><sc-for list="{{ rgSalesOrderConsignatariaOptions }}" as="option"><option value="{{ option.value }}">{{ option.label }}</option></sc-for></select> 7573 <select value="{{ rgSalesOrderRatingFilter }}" onChange="{{ setRgSalesOrderRatingFilter }}" aria-label="Filtrar por calificación"><option value="">Todas las calificaciones</option><option value="unrated">Sin calificar</option><option value="rated">Calificados</option></select> 7574 <div class="sales-order-filter-count sales-order-progress" role="status"><span><strong>{{ rgSalesOrderCount }}</strong> lotes totales</span><span class="sales-order-pending"><strong>{{ rgSalesOrderUnratedCount }}</strong> sin calificar</span><span><strong>{{ rgSalesOrderVisibleCount }}</strong> visibles con estos filtros</span></div> 7575 </div> 7576 <div class="sales-order-command" style="background:#fff;border:1px solid #d8e4f0;border-radius:12px;padding:16px;margin-bottom:16px"> 7577 <label for="rg-order-command" style="display:block;font-weight:800;margin-bottom:8px">Ordenar con una indicación</label> 7578 <div style="display:flex;gap:10px;flex-wrap:wrap"><input id="rg-order-command" type="text" class="sales-order-select" style="flex:1;min-width:220px;width:auto" placeholder="Quiero el lote 154 después del 4" onKeyDown="{{ rgOrderCommandEnter }}"><button class="sales-order-action" onClick="{{ rgOrderCommandApply }}">Aplicar</button></div> 7579 <p style="font-size:12px;color:#54718c;margin:8px 0 0">También: «lote 23 entre el 56 y el 57» o «lote 8 antes del 2». Usá los números de lote.</p> 7580 <div role="status" style="font-size:13px;color:#08745a;margin-top:8px">{{ rgOrderCommandMessage }}</div> 7581 </div> 7582 <sc-if value="{{ rgSalesOrderSaved }}"><div class="sales-order-message is-success">Orden guardado correctamente.</div></sc-if> 7583 <sc-if value="{{ rgSalesOrderHasError }}"><div class="sales-order-message is-error">{{ rgSalesOrderError }}</div></sc-if> 7584 <sc-if value="{{ rgSalesOrderLoading }}"><div class="sales-order-empty"><span class="dc-spinner"></span> Cargando lotes...</div></sc-if> 7585 <div class="sales-order-list"> 7586 <sc-for list="{{ rgSalesOrderRows }}" as="row" hint-placeholder-count="8"> 7587 <article class="sales-order-row"> 7588 <span class="sales-order-position">{{ row.position }}</span> 7589 <div class="sales-order-lot-main"> 7590 <div class="sales-order-copy"><strong>{{ row.category }}</strong><span class="sales-order-status {{ row.statusClass }}">{{ row.statusLabel }}</span><sc-if value="{{ row.isOutOfCatalog }}"><small>Fuera de catálogo</small></sc-if><small>Cert. {{ row.cert }}<sc-if value="{{ row.breed }}"> · {{ row.breed }}</sc-if></small></div> 7591 <div class="sales-order-facts"><sc-for list="{{ row.facts }}" as="fact"><span><small>{{ fact.label }}</small><strong>{{ fact.value }}</strong></span></sc-for></div> 7592 <sc-if value="{{ row.hasExcelSchedule }}"><div class="sales-order-excel-data"><span><small>GRUPO / ALIAS DEL EXCEL</small><strong>{{ row.excelAlias }}</strong></span><span><small>HORA ESTIMADA</small><strong>{{ row.estimatedTime }}</strong></span><span><small>PROMEDIO POR LOTE</small><strong>{{ row.averageTime }}</strong></span></div></sc-if> 7593 <div class="sales-order-lot-tools"> 7594 <span class="sales-order-consignataria"><span class="sales-order-consignataria-logo" style="background:{{ row.consignatariaLogoBg }};"><sc-if value="{{ row.hasConsignatariaLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ row.consignatariaLogo }}" alt="{{ row.consignataria }}"></sc-if><sc-if value="{{ row.noConsignatariaLogo }}" hint-placeholder-val="false"><strong style="color:#fff;font-size:14px;">{{ row.consignatariaInitial }}</strong></sc-if></span><span><small>CONSIGNATARIA</small><strong>{{ row.consignataria }}</strong></span></span> 7595 <div class="sales-order-rating" aria-label="{{ row.ratingLabel }}">
7596 <span class="sales-order-rating-label" style="color:{{ row.ratingColor }};">{{ row.ratingLabel }}</span> 7597 <span class="sales-order-stars"><sc-for list="{{ row.ratingStars }}" as="star"><button type="button" onClick="{{ star.pick }}" aria-label="{{ star.label }}" title="{{ star.label }}" style="color:{{ star.color }};">â </button></sc-for></span> 7598 </div> 7599 <sc-if value="{{ row.hasVideo }}" hint-placeholder-val="true"><button type="button" class="sales-order-video-button" onClick="{{ row.toggleVideo }}"><span class="sales-order-video-badge {{ row.videoBadgeClass }}">{{ row.videoLabel }}</span>{{ row.videoButtonLabel }}</button></sc-if> 7600 <sc-if value="{{ row.noVideo }}" hint-placeholder-val="false"><span class="sales-order-no-video">Sin video disponible</span></sc-if> 7601 </div> 7602 </div> 7603 <div class="sales-order-controls"> 7604 <button class="sales-order-control" onClick="{{ row.moveUp }}" disabled="{{ row.isFirst }}" title="Subir lote" aria-label="Subir lote"><img src="assets/icons/lucide/chevron-up.svg" alt=""></button> 7605 <button class="sales-order-control" onClick="{{ row.moveDown }}" disabled="{{ row.isLast }}" title="Bajar lote" aria-label="Bajar lote"><img src="assets/icons/lucide/chevron-down.svg" alt=""></button> 7606 <input class="sales-order-select" type="number" min="1" max="{{ row.maxPosition }}" data-dc-attr-value="{{ row.position }}" onChange="{{ row.setPosition }}" aria-label="Posición del certificado {{ row.cert }}"> 7607 </div> 7608 <sc-if value="{{ row.videoOpen }}" hint-placeholder-val="false"><div class="sales-order-video-panel"><div><strong>Video del lote</strong><span class="sales-order-video-badge {{ row.videoBadgeClass }}">{{ row.videoLabel }}</span><sc-if value="{{ row.videoIsRaw }}" hint-placeholder-val="false"><small>No hay video final; se muestra material crudo.</small></sc-if><a class="sales-order-video-direct" data-dc-attr-href="{{ row.videoUrl }}" target="_blank" rel="noopener">Abrir video directamente â</a></div><span class="sales-order-video-stage"><video id="sales-order-video-{{ row.cert }}" data-dc-attr-src="{{ row.videoUrl }}" data-dc-attr-poster="{{ row.videoPoster }}" onPlay="{{ row.videoDidPlay }}" onPlaying="{{ row.videoDidPlay }}" onPause="{{ row.videoDidPause }}" onEnded="{{ row.videoDidPause }}" onError="{{ row.videoDidError }}" controls playsinline preload="metadata"></video><button type="button" class="sales-order-video-play" onClick="{{ row.playVideo }}" aria-label="Reproducir video del lote {{ row.cert }}"><span aria-hidden="true">â¶</span> Reproducir</button></span></div></sc-if> 7609 </article> 7610 </sc-for> 7611 </div> 7612 <sc-if value="{{ rgSalesOrderNoResults }}"><div class="sales-order-empty">No hay lotes que coincidan con la búsqueda y los filtros.</div></sc-if> 7613 <sc-if value="{{ rgSalesOrderEmpty }}"><div class="sales-order-empty">Este remate todavÃa no tiene lotes para ordenar.</div></sc-if> 7614 </div> 7615 </main> 7616 </sc-if> 7617 <sc-if value="{{ isCabanaSalesOrder }}" hint-placeholder-val="false"> 7618 <main class="sales-order-page"> 7619 <div class="sales-order-shell"> 7620 <button class="sales-order-back" onClick="{{ closeCbSalesOrder }}"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catálogo</button> 7621 <header class="sales-order-head"> 7622 <div> 7623 <span class="sales-order-kicker">ADMINISTRACIÃN DEL REMATE</span> 7624 <h1>Orden de ventas</h1> 7625 <p>{{ cbSalesOrderAuctionTitle }}</p> 7626 </div> 7627 <div class="sales-order-toolbar"> 7628 <button class="sales-order-action" onClick="{{ toggleCbSalesOrderGrouped }}" style="border-color:{{ cbSalesGroupBorder }}; background:{{ cbSalesGroupBg }}; color:{{ cbSalesGroupColor }};"> 7629 <img src="assets/icons/lucide/layers-2.svg" alt=""> 7630 {{ cbSalesOrderGroupLabel }} 7631 </button> 7632 <button class="sales-order-action primary" onClick="{{ saveCbSalesOrder }}
7632" style="background:{{ cbColor }}; opacity:{{ cbSalesOrderSaveOpacity }};"> 7633 <img src="assets/icons/lucide/save.svg" alt=""> 7634 {{ cbSalesOrderSaveLabel }} 7635 </button> 7636 </div> 7637 </header> 7638 <div class="sales-order-summary"><strong>{{ cbSalesOrderCount }}</strong> lotes. Usá las flechas o elegà una posición para definir el orden visible en el catálogo.</div> 7639 <sc-if value="{{ cbSalesOrderSaved }}"><div class="sales-order-message is-success">Orden guardado correctamente.</div></sc-if> 7640 <sc-if value="{{ cbSalesOrderHasError }}"><div class="sales-order-message is-error">{{ cbSalesOrderError }}</div></sc-if> 7641 <sc-if value="{{ cbSalesOrderLoading }}"><div class="sales-order-empty"><span class="dc-spinner"></span> Cargando lotes...</div></sc-if> 7642 <sc-if value="{{ cbSalesOrderFlat }}"> 7643 <div class="sales-order-list" data-sales-flat> 7644 <sc-for list="{{ cbSalesOrderRows }}" as="row" hint-placeholder-count="8"> 7645 <article class="sales-order-row" data-sales-lot> 7646 <input class="sales-order-cert" type="hidden" value="{{ row.cert }}"> 7647 <span class="sales-order-position">{{ row.position }}</span> 7648 <div class="sales-order-copy"><strong>{{ row.title }}</strong><small>{{ row.category }} · {{ row.cabana }}</small></div> 7649 <div class="sales-order-controls"> 7650 <button class="sales-order-control" onClick="{{ row.moveUp }}" disabled="{{ row.isFirst }}" title="Subir lote" aria-label="Subir lote"><img src="assets/icons/lucide/chevron-up.svg" alt=""></button> 7651 <button class="sales-order-control" onClick="{{ row.moveDown }}" disabled="{{ row.isLast }}" title="Bajar lote" aria-label="Bajar lote"><img src="assets/icons/lucide/chevron-down.svg" alt=""></button> 7652 <select class="sales-order-select" value="{{ row.position }}" onChange="{{ row.setPosition }}" aria-label="Posición del lote"> 7653 <sc-for list="{{ row.positions }}" as="option"><option value="{{ option.value }}">{{ option.label }}</option></sc-for> 7654 </select> 7655 </div> 7656 </article> 7657 </sc-for> 7658 </div> 7659 </sc-if> 7660 <sc-if value="{{ cbSalesOrderGrouped }}"> 7661 <div class="sales-order-groups" data-sales-groups> 7662 <sc-for list="{{ cbSalesOrderGroups }}" as="group" hint-placeholder-count="3"> 7663 <section class="sales-order-category" data-sales-category> 7664 <input class="sales-order-category-key" type="hidden" value="{{ group.key }}"> 7665 <header class="sales-order-category-head"> 7666 <div><strong>{{ group.title }}</strong><small>{{ group.count }} lotes</small></div> 7667 <div class="sales-order-controls"> 7668 <button class="sales-order-control" onClick="{{ group.moveUp }}" disabled="{{ group.isFirst }}" title="Subir categorÃa" aria-label="Subir categorÃa"><img src="assets/icons/lucide/chevron-up.svg" alt=""></button> 7669 <button class="sales-order-control" onClick="{{ group.moveDown }}" disabled="{{ group.isLast }}" title="Bajar categorÃa" aria-label="Bajar categorÃa"><img src="assets/icons/lucide/chevron-down.svg" alt=""></button> 7670 <select class="sales-order-select" value="{{ group.position }}" onChange="{{ group.setPosition }}" aria-label="Posición de la categorÃa"> 7671 <sc-for list="{{ group.positions }}" as="option"><option value="{{ option.value }}">{{ option.label }}</option></sc-for> 7672 </select> 7673 </div> 7674 </header> 7675 <div class="sales-order-category-list" data-sales-category-list> 7676 <sc-for list="{{ group.lots }}" as="row" hint-placeholder-count="4"> 7677 <article class="sales-order-row" data-sales-lot> 7678 <input class="sales-order-cert" type="hidden" value="{{ row.cert }}">
7679 <span class="sales-order-position">{{ row.position }}</span> 7680 <div class="sales-order-copy"><strong>{{ row.title }}</strong><small>{{ row.cabana }}</small></div> 7681 <div class="sales-order-controls"> 7682 <button class="sales-order-control" onClick="{{ row.moveUp }}" disabled="{{ row.isFirst }}" title="Subir lote" aria-label="Subir lote"><img src="assets/icons/lucide/chevron-up.svg" alt=""></button> 7683 <button class="sales-order-control" onClick="{{ row.moveDown }}" disabled="{{ row.isLast }}" title="Bajar lote" aria-label="Bajar lote"><img src="assets/icons/lucide/chevron-down.svg" alt=""></button> 7684 <select class="sales-order-select" value="{{ row.groupPosition }}" onChange="{{ row.setPosition }}" aria-label="Posición del lote en la categorÃa"> 7685 <sc-for list="{{ row.positions }}" as="option"><option value="{{ option.value }}">{{ option.label }}</option></sc-for> 7686 </select> 7687 </div> 7688 </article> 7689 </sc-for> 7690 </div> 7691 </section> 7692 </sc-for> 7693 </div> 7694 </sc-if> 7695 <sc-if value="{{ cbSalesOrderEmpty }}"><div class="sales-order-empty">Este remate todavÃa no tiene lotes para ordenar.</div></sc-if> 7696 </div> 7697 </main> 7698 </sc-if> 7699 7700 <!-- FICHA DE REPRODUCTOR --> 7701 <sc-if value="{{ isCabanaLotDetail }}" hint-placeholder-val="false"> 7702 <main style="width:100%; max-width:1240px; min-width:0; box-sizing:border-box; overflow-x:hidden; margin:0 auto; padding:26px clamp(14px,4vw,36px) 100px; animation:pageIn 360ms both;"> 7703 <button onClick="{{ goCabanaBack }}" style="display:inline-flex;align-items:center;gap:6px;border:0; background:transparent; color:#0a5ca8; font:700 13px inherit; padding:0; cursor:pointer;"><span class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></span> Volver al catálogo</button> 7704 <div style="display:flex; justify-content:space-between; align-items:end; gap:12px; flex-wrap:wrap; margin:10px 0 18px;"><div><h1 style="margin:0; font-size:clamp(26px,3.4vw,36px); color:#112033;">{{ clTitle }}</h1><span style="font-size:13px; color:#65758a;">{{ clSubtitle }}</span></div><div class="dc-lot-social-actions"><sc-if value="{{ clCanEdit }}" hint-placeholder-val="false"><button class="dc-lot-social-button" onClick="{{ clEditPricing }}">Base e incremento</button><button class="dc-lot-social-button" onClick="{{ clEdit }}" title="Editar lote" aria-label="Editar lote"><img src="assets/icons/lucide/pencil.svg" alt="">Editar lote</button></sc-if><sc-if value="{{ clCanDelete }}" hint-placeholder-val="false"><button class="dc-lot-social-button" onClick="{{ clDelete }}" disabled="{{ clDeleting }}" title="Eliminar lote" aria-label="Eliminar lote" style="border-color:#f0c5ca; color:#b4232f; opacity:{{ clDeleteOpacity }}; cursor:{{ clDeleteCursor }};"><img src="assets/icons/lucide/trash-2.svg" alt="">{{ clDeleteLabel }}</button></sc-if><sc-if value="{{ clCanDeleteVideo }}" hint-placeholder-val="false"><button class="dc-lot-social-button" onClick="{{ clDeleteVideo }}" disabled="{{ clVideoDeleting }}" title="Eliminar el video seleccionado" aria-label="Eliminar el video seleccionado" style="border-color:#f0c5ca;color:#b4232f;"><img src="assets/icons/lucide/trash-2.svg" alt="">{{ clDeleteVideoLabel }}</button></sc-if><button class="dc-lot-social-button dc-lot-social-button--instagram" onClick="{{ clShareInstagramStory }}" title="Compartir en Instagram Stories" aria-label="Compartir en Instagram Stories"><img src="assets/icons/brands/instagram.svg" alt="">{{ clInstagramStoryLabel }}</button><button class="dc-lot-social-button dc-lot-social-button--whatsapp" onClick="{{ clShareWhatsappStatus }}" title="Compartir en WhatsApp Estado" aria-label="Compartir en WhatsApp Estado"><img src="assets/icons/brands/whatsapp.svg" alt="">{{ clWhatsappStatusLabel }}</button><button class="dc-lot-social-button" onClick="{{ clShare }}" title="Compartir este RP" aria-label="Compartir este RP"><img src="assets/icons/lucide/share-2.svg" alt="">Compartir <strong>{{ clShareCount }}</strong></button><sc-if value="{{ clShowStatus }}" hint-placeholder-val="false"><span style="padding:7px 14px; border-radius:100px; color:#fff; background:{{ clStatusBg }}; font-size:11px; font-weight:900;">{{ clStatus }}</span></sc-if></div></div> 7705 <sc-if value="{{ clHasRpVariants }}"><section style="margin:-4px 0 18px; padding:12px 14px; border:1px solid #dce6f0; border-radius:12px; background:#fff;"><small style="display:block; margin-bottom:8px; color:#728399; font-size:10px; font-weight:900; letter-spacing:.09em;">ANIMALES DEL LOTE</small><div style="display:flex; gap:9px; overflow-x:auto; padding:1px 1px 4px;"><sc-for list="{{ clRpVariants }}" as="variant"><button class="{{ variant.className }}" onClick="{{ variant.select }}" title="{{ variant.title }}" aria-label="{{ variant.title }}"><sc-if value="{{ variant.hasThumb }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ variant.thumb }}" alt="{{ variant.label }}" style="display:block; width:100%; height:100%; object-fit:cover;"></sc-if><sc-if value="{{ variant.noThumb }}" hint-placeholder-val="false"><span style="display:grid; place-items:center; width:100%; height:100%; color:#60758f; font-size:11px; font-weight:900;">{{ variant.shortLabel }}</span></sc-if><span class="dc-rp-variant-label">{{ variant.label }}</span><sc-if value="{{ variant.active }}" hint-placeholder-val="false"><span class="dc-rp-variant-check"><img src="assets/icons/lucide/check.svg" alt=""></span></sc-if></button></sc-for></div></section></sc-if> 7706 <div style="width:100%; min-width:0; display:grid; grid-template-columns:{{ cabanaDetailCols }}
7706; gap:20px; align-items:start;"> 7707 <div style="display:grid; gap:12px; min-width:0;"> 7708 <div class="dc-cabana-video-frame"><sc-if value="{{ clMediaLoading }}"><div class="dc-media-loader"><span class="dc-spinner"></span></div></sc-if><sc-if value="{{ clHasVideo }}" hint-placeholder-val="false"><video class="dc-cabana-lot-player" data-dc-attr-src="{{ clVideoUrl }}" data-dc-attr-poster="{{ clVideoPoster }}" onCanPlay="{{ clMediaLoaded }}" onLoadedMetadata="{{ clVideoMeta }}" onTimeUpdate="{{ clVideoTimeUpdate }}" onVolumeChange="{{ clVideoVolumeChange }}" onPause="{{ clVideoPause }}" onPlay="{{ clVideoPlay }}" onPlaying="{{ clVideoPlay }}" onEnded="{{ clVideoEnded }}" onError="{{ clMediaError }}" controls autoplay muted loop preload="auto" playsinline style="width:100%; height:100%; object-fit:contain;background:#000;"></video></sc-if><sc-if value="{{ clShowImage }}" hint-placeholder-val="false"><img class="dc-media-image" data-dc-attr-src="{{ clImage }}" onLoad="{{ clMediaLoaded }}" onError="{{ clMediaError }}" alt="{{ clName }}" style="width:100%; height:100%; object-fit:cover;"></sc-if><sc-if value="{{ clPlaybackError }}" hint-placeholder-val="false"><div class="dc-cabana-playback-error" role="alert" style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;padding:24px;text-align:center;background:rgba(0,24,45,.92);color:#fff;z-index:2;"><strong>No se pudo reproducir este archivo</strong><span>El material está cargado. Reintentá la reproducción.</span><button type="button" onClick="{{ clRetryMedia }}" style="padding:12px 22px;border:0;border-radius:12px;background:white;color:#003466;font:700 15px inherit;cursor:pointer;">Reintentar</button></div></sc-if><sc-if value="{{ clEmptyMedia }}" hint-placeholder-val="true"><div class="dc-cabana-empty-media"><span>Sin material audiovisual cargado</span><sc-if value="{{ clUploadMediaBusy }}" hint-placeholder-val="false"><div class="dc-cabana-upload-progress" role="progressbar" aria-label="Progreso de carga del video" aria-valuemin="0" aria-valuemax="100" data-dc-attr-aria-valuenow="{{ clUploadProgress }}"><div class="dc-cabana-upload-ring" style="--upload-progress:{{ clUploadProgress }}%;"><strong>{{ clUploadProgress }}%</strong></div><div class="dc-cabana-upload-copy"><strong>{{ clUploadStage }}</strong><div class="dc-cabana-upload-track"><span style="width:{{ clUploadProgress }}%;"></span></div><small>No cierres esta pantalla hasta finalizar.</small></div></div></sc-if><sc-if value="{{ clCanSelectUpload }}" hint-placeholder-val="false"><label class="dc-cabana-empty-upload"><img src="assets/icons/lucide/upload.svg" alt="">Subir video<input type="file" accept="video/*" multiple onChange="{{ clUploadMedia }}" style="display:none;"></label><a class="rg-filming-guide" href="/protocolofilmacion" target="_blank" rel="noopener" aria-label="Cómo filmar el lote: abrir guÃa en otra pestaña"><img src="/assets/icons/lucide/video.svg" alt="" aria-hidden="true"><span>Cómo filmar el lote</span><span aria-hidden="true">â</span></a></sc-if></div></sc-if></div> 7709 <sc-if value="{{ clHasVideo }}" hint-placeholder-val="false"><div class="dc-video-controls" aria-label="Controles del video"><button class="dc-video-control-button" onClick="{{ clTogglePlay }}" title="{{ clPlayLabel }}" aria-label="{{ clPlayLabel }}"><img data-dc-attr-src="{{ clPlayIcon }}" alt=""></button><input class="dc-video-slider" type="range" min="0" max="100" step="0.1" value="{{ clVideoProgress }}" onInput="{{ clSeekVideo }}" onChange="{{ clSeekVideo }}" aria-label="Avance del video"><span class="dc-video-time">{{ clVideoTime }}</span><button class="dc-video-control-button" onClick="{{ clToggleMute }}" title="{{ clMuteLabel }}" aria-label="{{ clMuteLabel }}"><img data-dc-attr-src="{{ clVolumeIcon }}" alt=""></button><input class="dc-video-slider dc-video-volume" type="range" min="0" max="1" step="0.05" value="{{ clVideoVolume }}" onInput="{{ clSetVolume }}" onChange="{{ clSetVolume }}" aria-label="Volumen"><button class="dc-video-control-button" onClick="{{ clFullscreen }}" title="Pantalla completa" aria-label="Pantalla completa"><img src="assets/icons/lucide/maximize-2.svg" alt=""></button></div></sc-if> 7710 <sc-if value="{{ clHasThumbs }}
7710" hint-placeholder-val="false"><div style="display:flex; gap:10px; overflow-x:auto; padding:2px 2px 4px;"><sc-for list="{{ clThumbs }}" as="thumb" hint-placeholder-count="3"><button onClick="{{ thumb.select }}" title="{{ thumb.label }}" style="position:relative; width:126px; aspect-ratio:16/9; flex:0 0 auto; border:2px solid {{ thumb.border }}; border-radius:11px; overflow:hidden; background:#edf2f7; padding:0; cursor:pointer; box-shadow:0 8px 20px rgba(0,30,60,.08);"><sc-if value="{{ thumb.hasThumb }}" hint-placeholder-val="false"><img class="dc-media-image" data-dc-attr-src="{{ thumb.thumb }}" onLoad="{{ thumb.mediaReady }}" onError="{{ thumb.mediaError }}" alt="{{ thumb.label }}" style="width:100%; height:100%; object-fit:cover;"></sc-if><span style="position:absolute; left:7px; bottom:7px; padding:3px 7px; border-radius:999px; background:rgba(0,29,61,.72); color:#fff; font-size:10px; font-weight:900; letter-spacing:.02em;">{{ thumb.icon }}</span></button></sc-for></div></sc-if> 7711 <sc-if value="{{ isMobile }}" hint-placeholder-val="{{ false }}"><aside style="display:grid; gap:12px; min-width:0; width:100%;"><sc-if value="{{ clShowBaseCard }}" hint-placeholder-val="false"><section style="border-radius:18px; padding:21px; color:#fff; background:{{ clPriceBackground }}; box-shadow:0 18px 48px rgba(0,30,60,.2);"><small style="font-weight:900; letter-spacing:.1em; opacity:.75;">{{ clPriceLabel }}</small><strong style="display:block; margin-top:5px; font-size:34px;">{{ clPriceAmount }}</strong><sc-if value="{{ clOwnWinning }}"><span style="display:inline-block;margin-top:10px;padding:5px 9px;border-radius:7px;background:rgba(255,255,255,.18);font-weight:800;font-size:13px;">{{ clOwnWinningLabel }}</span></sc-if><sc-if value="{{ clInstallmentsEnabled }}"><small style="display:block;margin-top:8px;">{{ clInstallmentPriceTotal }}</small></sc-if></section></sc-if><sc-if value="{{ clPrebidEnabled }}" hint-placeholder-val="false"><section class="dc-prebid-side-card {{ clPrebidCardClass }}"><div class="dc-prebid-side-head"><span class="dc-prebid-side-icon"><img src="assets/icons/lucide/gavel.svg" alt=""></span><span class="dc-prebid-side-copy"><small>PREOFERTAS</small></span></div><div class="dc-prebid-side-actions"><sc-if value="{{ clPrebidUpcoming }}" hint-placeholder-val="false"><div class="dc-prebid-start"><strong>Las preofertas comienzan</strong><span>{{ clPrebidStartLabel }}</span><prebid-start-countdown start="{{ clPrebidStart }}"></prebid-start-countdown></div></sc-if><sc-if value="{{ clPrebidClosed }}" hint-placeholder-val="false"><p class="dc-prebid-closed" role="status">Preofertas cerradas</p></sc-if><sc-if value="{{ clPrebidOpen }}" hint-placeholder-val="false"><button class="dc-prebid-side-button" onClick="{{ clOpenPrebid }}"><img src="assets/icons/lucide/badge-dollar-sign.svg" alt="">Preofertar</button></sc-if><button type="button" class="dc-prebid-history-button" onClick="{{ clViewPrebidHistory }}">Ver historial de preofertas <span aria-hidden="true">â</span></button></div></section></sc-if><sc-if value="{{ clHasCabanaLogo }}" hint-placeholder-val="false"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:15px; padding:16px;"><small style="display:block; color:#8593a6; margin-bottom:10px;">CABAÃA</small><img data-dc-attr-src="{{ clCabanaLogo }}" alt="{{ clCabanaName }}" style="display:block; width:100%; max-height:96px; object-fit:contain; object-position:left center;"></section></sc-if><sc-if value="{{ clShowCabanaNameCard }}" hint-placeholder-val="false"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:15px; padding:15px;"><small style="display:block; color:#8593a6;">CABAÃA</small><strong>{{ clCabanaName }}</strong></section></sc-if><section style="background:#fff; border:1px solid #e3eaf2; border-radius:15px; padding:15px;"><small style="display:block; color:#8593a6;">REMATE</small><strong>{{ clCabana }}</strong></section><section style="display:flex; align-items:center; gap:12px; min-width:0; color:#fff; background:{{ clConsignatariaColor }}; border-radius:15px; padding:14px 15px; box-shadow:0 10px 26px {{ clConsignatariaShadow }};"><span class="dc-consignataria-logo-box" style="width:42px; height:42px; flex:0 0 42px;"><sc-if value="{{ clHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ clConsignatariaWhiteLogo }}" alt=""></sc-if>
7711<sc-if value="{{ clNoConsignatariaWhiteLogo }}" hint-placeholder-val="false"><strong style="font-size:18px;">{{ clConsignatariaInitial }}</strong></sc-if></span><span style="min-width:0;"><small style="display:block; color:rgba(255,255,255,.78); font-weight:800; letter-spacing:.07em;">CONSIGNATARIA</small><strong style="display:block; color:#fff; line-height:1.2;">{{ clConsignataria }}</strong></span></section><section class="dc-lot-contact-card"><div class="dc-lot-contact-head"><span class="dc-lot-contact-avatar" style="background:{{ clConsignatariaColor }};"><sc-if value="{{ clHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ clConsignatariaWhiteLogo }}" alt="{{ clConsignataria }}"></sc-if><sc-if value="{{ clNoConsignatariaWhiteLogo }}" hint-placeholder-val="false"><strong>{{ clConsignatariaInitial }}</strong></sc-if></span><span class="dc-lot-contact-copy"><small>CONTACTO DEL LOTE</small><strong>{{ clContactName }}</strong><span>{{ clContactPhone }}</span></span></div><div class="dc-lot-contact-actions"><a class="dc-contact-whatsapp" data-dc-attr-href="{{ clContactWhatsappHref }}" target="_blank" rel="noopener noreferrer"><img src="assets/icons/brands/whatsapp.svg" alt="">Consultar por WhatsApp</a><a class="dc-contact-call" data-dc-attr-href="{{ clContactPhoneHref }}"><img src="assets/icons/lucide/phone.svg" alt="">Llamar</a></div></section></aside></sc-if> 7712 <sc-for list="{{ clPrimaryGroups }}" as="group" hint-placeholder-count="1"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px;"><small style="display:block; color:#8593a6; font-weight:900; letter-spacing:.11em; margin-bottom:11px;">{{ group.label }}</small><div class="rg-detail-list"><sc-for list="{{ group.cards }}" as="data" hint-placeholder-count="4"><div class="dc-rosgan-field-card rg-detail-row" style="grid-template-columns:minmax(0,1fr);"><span class="dc-rosgan-field-copy"><small>{{ data.label }}</small><strong>{{ data.value }}</strong></span></div></sc-for></div></section></sc-for> 7713 <sc-for list="{{ clTables }}" as="table" hint-placeholder-count="1"> 7714 <section style="min-width:0; background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px; box-shadow:0 10px 28px rgba(0,30,60,.05);"> 7715 <small style="display:block; color:#8593a6; font-weight:900; letter-spacing:.11em; margin-bottom:11px;">{{ table.title }}</small> 7716 <sc-if value="{{ isDesktop }}" hint-placeholder-val="{{ true }}"> 7717 <div style="max-width:100%; overflow-x:auto; border:1px solid #e5edf5; border-radius:14px;"> 7718 <table style="width:100%; min-width:720px; border-collapse:separate; border-spacing:0; font-size:13px;"> 7719 <thead><tr style="background:#f5f8fb;"><th style="position:sticky; left:0; z-index:1; background:#f5f8fb; color:#65758a; text-align:left; padding:12px 14px; font-size:11px; letter-spacing:.08em;">FORMULARIO</th><sc-for list="{{ table.columns }}" as="column" hint-placeholder-count="6"><th style="color:#65758a; text-align:left; padding:12px 14px; font-size:11px; letter-spacing:.08em; white-space:nowrap;">{{ column.label }}</th></sc-for></tr></thead> 7720 <tbody><sc-for list="{{ table.rows }}" as="row" hint-placeholder-count="2"><tr><th style="position:sticky; left:0; z-index:1; background:#fff; color:#0a5ca8; text-align:left; padding:13px 14px; border-top:1px solid #edf2f7; white-space:nowrap; font-size:13px;">{{ row.label }}</th><sc-for list="{{ row.cells }}" as="cell" hint-placeholder-count="6"><td style="padding:13px 14px; border-top:1px solid #edf2f7; color:#112033; font-weight:800; white-space:nowrap;">{{ cell.value }}</td></sc-for></tr></sc-for></tbody> 7721 </table> 7722 </div> 7723 </sc-if> 7724 <sc-if value="{{ isMobile }}" hint-placeholder-val="{{ false }}"> 7725 <div style="width:100%; max-width:100%; overflow:hidden; border:1px solid #e5edf5; border-radius:12px;"> 7726 <table style="width:100%; max-width:100%; table-layout:fixed; border-collapse:collapse;"> 7727 <thead><tr style="background:#f5f8fb;"><th style="width:48px; padding:8px 3px; color:#65758a; text-align:left;
7727 font-size:7px; line-height:1; letter-spacing:0;">FORM.</th><sc-for list="{{ table.columns }}" as="column" hint-placeholder-count="6"><th title="{{ column.label }}" style="padding:8px 1px; color:#65758a; text-align:center; font-size:7px; line-height:1; letter-spacing:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ column.label }}</th></sc-for></tr></thead> 7728 <tbody><sc-for list="{{ table.rows }}" as="row" hint-placeholder-count="2"><tr><th title="{{ row.label }}" style="padding:10px 3px; border-top:1px solid #edf2f7; color:#0a5ca8; text-align:left; font-size:9px; line-height:1.05; letter-spacing:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ row.mobileLabel }}</th><sc-for list="{{ row.cells }}" as="cell" hint-placeholder-count="6"><td title="{{ cell.label }}: {{ cell.value }}" style="padding:10px 1px; border-top:1px solid #edf2f7; color:#112033; text-align:center; font-size:9px; line-height:1.05; font-weight:800; letter-spacing:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ cell.value }}</td></sc-for></tr></sc-for></tbody> 7729 </table> 7730 </div> 7731 </sc-if> 7732 </section> 7733 </sc-for> 7734 <sc-for list="{{ clSecondaryGroups }}" as="group" hint-placeholder-count="2"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px;"><small style="display:block; color:#8593a6; font-weight:900; letter-spacing:.11em; margin-bottom:11px;">{{ group.label }}</small><div class="rg-detail-list"><sc-for list="{{ group.cards }}" as="data" hint-placeholder-count="4"><div class="dc-rosgan-field-card rg-detail-row" style="grid-template-columns:minmax(0,1fr);"><span class="dc-rosgan-field-copy"><small>{{ data.label }}</small><strong>{{ data.value }}</strong></span></div></sc-for></div></section></sc-for> 7735 <sc-if value="{{ clHasObservations }}" hint-placeholder-val="false"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:17px; padding:18px;"><small style="display:block; color:#8593a6; font-weight:900; letter-spacing:.11em;">OBSERVACIONES</small><p style="margin:9px 0 0; color:#3d4c5f; font-size:14px; line-height:1.55;">{{ clObservations }}</p></section></sc-if> 7736 </div> 7737 <sc-if value="{{ isDesktop }}" hint-placeholder-val="{{ true }}"><aside style="position:{{ cabanaSticky }}; top:76px; display:grid; gap:12px; min-width:0; width:100%;"><sc-if value="{{ clShowBaseCard }}" hint-placeholder-val="false"><section style="border-radius:18px; padding:21px; color:#fff; background:{{ clPriceBackground }}; box-shadow:0 18px 48px rgba(0,30,60,.2);"><small style="font-weight:900; letter-spacing:.1em; opacity:.75;">{{ clPriceLabel }}</small><strong style="display:block; margin-top:5px; font-size:34px;">{{ clPriceAmount }}</strong><sc-if value="{{ clOwnWinning }}"><span style="display:inline-block;margin-top:10px;padding:5px 9px;border-radius:7px;background:rgba(255,255,255,.18);font-weight:800;font-size:13px;">{{ clOwnWinningLabel }}</span></sc-if><sc-if value="{{ clInstallmentsEnabled }}"><small style="display:block;margin-top:8px;">{{ clInstallmentPriceTotal }}</small></sc-if></section></sc-if><sc-if value="{{ clPrebidEnabled }}" hint-placeholder-val="false"><section class="dc-prebid-side-card {{ clPrebidCardClass }}"><div class="dc-prebid-side-head"><span class="dc-prebid-side-icon"><img src="assets/icons/lucide/gavel.svg" alt=""></span><span class="dc-prebid-side-copy"><small>PREOFERTAS</small></span></div><div class="dc-prebid-side-actions"><sc-if value="{{ clPrebidUpcoming }}" hint-placeholder-val="false"><div class="dc-prebid-start"><strong>Las preofertas comienzan</strong><span>{{ clPrebidStartLabel }}</span><prebid-start-countdown start="{{ clPrebidStart }}"></prebid-start-countdown></div></sc-if><sc-if value="{{ clPrebidClosed }}" hint-placeholder-val="false"><p class="dc-prebid-closed" role="status">Preofertas cerradas</p></sc-if><sc-if value="{{ clPrebidOpen }}" hint-placeholder-val="false"><button class="dc-prebid-side-button" onClick="{{ clOpenPrebid }}"><img src="assets/icons/lucide/badge-dollar-sign.svg" alt="">Preofertar</button></sc-if><button type="button" class="dc-prebid-history-button" onClick="{{ clViewPrebidHistory }}">Ver historial de preofertas <span aria-hidden="true">â</span></button></div></section></sc-if><sc-if value="{{ clHasCabanaLogo }}" hint-placeholder-val="false"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:15px; padding:16px;"><small style="display:block; color:#8593a6; margin-bottom:10px;">CABAÃA</small><img data-dc-attr-src="{{ clCabanaLogo }}" alt="{{ clCabanaName }}" style="display:block; width:100%; max-height:96px; object-fit:contain; object-position:left center;"></section></sc-if><sc-if value="{{ clShowCabanaNameCard }}" hint-placeholder-val="false"><section style="background:#fff; border:1px solid #e3eaf2; border-radius:15px; padding:15px;"><small style="display:block; color:#8593a6;">CABAÃA</small><strong>{{ clCabanaName }}</strong></section></sc-if><section style="background:#fff; border:1px solid #e3eaf2; border-radius:15px; padding:15px;"><small style="display:block; color:#8593a6;">REMATE</small><strong>{{ clCabana }}</strong></section><section style="display:flex; align-items:center; gap:12px; min-width:0; color:#fff; background:{{ clConsignatariaColor }}; border-radius:15px; padding:14px 15px; box-shadow:0 10px 26px {{ clConsignatariaShadow }};"><span class="dc-consignataria-logo-box" style="width:42px; height:42px; flex:0 0 42px;"><sc-if value="{{ clHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ clConsignatariaWhiteLogo }}" alt=""></sc-if>
7737<sc-if value="{{ clNoConsignatariaWhiteLogo }}" hint-placeholder-val="false"><strong style="font-size:18px;">{{ clConsignatariaInitial }}</strong></sc-if></span><span style="min-width:0;"><small style="display:block; color:rgba(255,255,255,.78); font-weight:800; letter-spacing:.07em;">CONSIGNATARIA</small><strong style="display:block; color:#fff; line-height:1.2;">{{ clConsignataria }}</strong></span></section><section class="dc-lot-contact-card"><div class="dc-lot-contact-head"><span class="dc-lot-contact-avatar" style="background:{{ clConsignatariaColor }};"><sc-if value="{{ clHasConsignatariaWhiteLogo }}" hint-placeholder-val="true"><img data-dc-attr-src="{{ clConsignatariaWhiteLogo }}" alt="{{ clConsignataria }}"></sc-if><sc-if value="{{ clNoConsignatariaWhiteLogo }}" hint-placeholder-val="false"><strong>{{ clConsignatariaInitial }}</strong></sc-if></span><span class="dc-lot-contact-copy"><small>CONTACTO DEL LOTE</small><strong>{{ clContactName }}</strong><span>{{ clContactPhone }}</span></span></div><div class="dc-lot-contact-actions"><a class="dc-contact-whatsapp" data-dc-attr-href="{{ clContactWhatsappHref }}" target="_blank" rel="noopener noreferrer"><img src="assets/icons/brands/whatsapp.svg" alt="">Consultar por WhatsApp</a><a class="dc-contact-call" data-dc-attr-href="{{ clContactPhoneHref }}"><img src="assets/icons/lucide/phone.svg" alt="">Llamar</a></div></section></aside></sc-if> 7738 </div> 7739 <sc-if value="{{ clPrebidEnabled }}" hint-placeholder-val="false"> 7740 <section id="cabana-prebid-history" tabindex="-1" aria-label="Mis preofertas" style="scroll-margin-top:80px; margin-top:20px; overflow:hidden; border:1px solid #d9e5ef; border-radius:20px; background:#fff; box-shadow:0 16px 40px rgba(0,46,95,.08);"> 7741 <div style="padding:18px clamp(16px,3vw,25px) 23px;"> 7742 <small style="display:block; margin-bottom:12px; color:#60758a; font-size:10px; font-weight:900; letter-spacing:.12em;">MIS PREOFERTAS</small> 7743 <sc-if value="{{ clPrebidLoading }}" hint-placeholder-val="false"><div style="display:flex; align-items:center; justify-content:center; gap:9px; min-height:90px; color:#60758a; font-size:13px;"><span class="dc-spinner"></span>Cargando preofertas...</div></sc-if> 7744 <sc-if value="{{ clPrebidHasError }}" hint-placeholder-val="false"><div style="padding:13px 15px; border-radius:12px; background:#fff1f2; color:#a8212d; font-size:12px; font-weight:750;">{{ clPrebidError }}</div></sc-if> 7745 <sc-if value="{{ clPrebidHasHistory }}" hint-placeholder-val="false"><div style="display:grid; gap:8px;"><sc-for list="{{ clPrebidHistory }}" as="offer"><article style="display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:8px 12px; min-width:0; padding:12px 14px; border:1px solid #e5edf4; border-radius:13px; background:#fbfdff;"><div style="min-width:0;"><strong style="display:block; color:#102f50; font-size:16px;">{{ offer.amount }}</strong><small style="color:#7a8b9f; font-size:10px;">{{ offer.date }}</small></div><span style="justify-self:end; padding:6px 10px; border-radius:999px; background:{{ offer.statusBg }}; color:{{ offer.statusColor }}; font-size:10px; font-weight:900; white-space:nowrap;">{{ offer.status }}</span><sc-if value="{{ offer.hasCommission }}" hint-placeholder-val="false"><small style="grid-column:1/-1; display:block; color:#7a8b9f; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ offer.commission }}</small></sc-if><sc-if value="{{ offer.canDelete }}"><button disabled="{{ offer.deleteBusy }}" onClick="{{ offer.deleteOffer }}" style="padding:8px 12px; border:1px solid #efb7bf; border-radius:9px; color:#b51f35; background:#fff; cursor:pointer;">Eliminar preoferta</button></sc-if></article></sc-for></div></sc-if> 7746 <sc-if value="{{ clPrebidEmpty }}" hint-placeholder-val="false"><div style="display:flex; align-items:center; gap:12px; padding:14px; border:1px dashed #cad8e5; border-radius:14px; color:#60758a;"><span style="display:grid; place-items:center; width:38px; height:38px; flex:0 0 38px; border-radius:12px; background:#edf5fb;"><img src="assets/icons/lucide/circle-plus.svg" alt="" style="width:18px; height:18px;"></span><span><strong style="display:block; color:#27435f; font-size:13px;">TodavÃa no hiciste preofertas</strong><small>Acá vas a ver tus preofertas de este lote.</small></span></div></sc-if> 7747 </div> 7748 </section> 7749 </sc-if> 7750 </main> 7751 <sc-if value="{{ clPrebidSuccessOpen }}" hint-placeholder-val="false">
7752 <div class="prebid-success-layer" role="status" aria-live="assertive" aria-label="Preoferta confirmada"> 7753 <div class="prebid-success-flood" aria-hidden="true"></div> 7754 <div class="prebid-success-content"> 7755 <span class="prebid-success-check"><img src="assets/icons/lucide/check.svg" alt=""></span> 7756 <div class="prebid-success-copy"> 7757 <small>{{ clPrebidSuccessEyebrow }}</small> 7758 <h2>{{ clPrebidSuccessTitle }}</h2> 7759 <strong class="prebid-success-amount">{{ clPrebidSuccessAmount }}</strong> 7760 <p>{{ clPrebidSuccessMessage }}</p> 7761 </div> 7762 </div> 7763 </div> 7764 </sc-if> 7765 <sc-if value="{{ clPrebidModalOpen }}" hint-placeholder-val="false"> 7766 <div role="dialog" aria-modal="true" aria-label="Realizar preoferta" style="position:fixed; inset:0; z-index:100000; display:grid; place-items:center; padding:18px; background:rgba(0,28,59,.72); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px); overflow-y:auto;"> 7767 <section style="width:min(620px,100%); max-height:calc(100vh - 36px); overflow-y:auto; box-sizing:border-box; border-radius:22px; background:#fff; box-shadow:0 30px 90px rgba(0,20,45,.34);"> 7768 <header style="position:sticky; top:0; z-index:2; display:flex; align-items:flex-start; justify-content:space-between; gap:18px; padding:20px 22px 16px; border-bottom:1px solid #e5edf4; background:#fff;"><div><small style="display:block; color:#0a5ca8; font-size:10px; font-weight:900; letter-spacing:.12em;">PREOFERTA DE CABAÃA</small><h2 style="margin:4px 0 0; color:#102f50; font-size:24px; line-height:1.1;">Hacé tu preoferta</h2><p style="margin:6px 0 0; color:#718196; font-size:12px;">Actual: <strong style="color:#102f50;">{{ clPrebidCurrentAmount }}</strong> · incremento {{ clPrebidIncrement }}</p></div><button onClick="{{ clClosePrebid }}" title="Cerrar" aria-label="Cerrar" style="display:grid; place-items:center; width:38px; height:38px; flex:0 0 38px; border:1px solid #d9e4ee; border-radius:12px; background:#f7fafc; cursor:pointer;"><img src="assets/icons/lucide/x.svg" alt="" style="width:18px; height:18px;"></button></header> 7769 <div style="display:grid; gap:17px; padding:20px 22px 23px;"> 7770 <sc-if value="{{ clPrebidNeedsCuit }}"><label style="display:grid; gap:7px; color:#4e6278; font-size:11px; font-weight:900; letter-spacing:.04em;">CUIT / CUIL<input inputmode="numeric" autocomplete="off" maxlength="13" value="" data-dc-attr-value="{{ clPrebidCuit }}" onInput="{{ clSetPrebidCuit }}" placeholder="20421804460" style="height:48px; box-sizing:border-box; border:1px solid #cbd9e6; border-radius:12px; padding:0 13px; color:#102f50; font:750 15px inherit; outline:none;"></label></sc-if> 7771 <sc-if value="{{ clCuitCheckVisible }}"><div role="status" aria-live="polite" style="font-size:12px;color:{{ clCuitCheckColor }};line-height:1.5;">{{ clCuitCheckText }} <sc-if value="{{ clCuitCheckError }}"><button type="button" onClick="{{ clRetryCuit }}" style="margin-left:8px;border:1px solid #cbd9e6;border-radius:8px;padding:6px 10px;background:#fff;color:#0a5ca8;cursor:pointer;">Reintentar</button></sc-if></div></sc-if> 7772 <div style="display:grid; gap:7px;"><span style="color:#4e6278; font-size:11px; font-weight:900; letter-spacing:.04em;">{{ clPrebidAmountLabel }}</span><div style="display:grid; grid-template-columns:48px minmax(0,1fr) 48px; gap:8px;"><button onClick="{{ clPrebidMinus }}" aria-label="Restar un incremento" style="display:grid; place-items:center; border:1px solid #cbd9e6; border-radius:12px; background:#f4f8fb; cursor:pointer;"><img src="assets/icons/lucide/minus.svg" alt="" style="width:19px; height:19px;"></button><span style="position:relative; display:block; min-width:0;"><span aria-hidden="true" style="position:absolute; z-index:3; left:18px; top:50%; transform:translateY(-50%); color:#002e5f; font-family:inherit; font-size:20px; font-weight:900; font-variant-numeric:tabular-nums; line-height:1; pointer-events:none;">$</span><input aria-label="Monto de preoferta" type="text" inputmode="numeric" value="" data-dc-attr-value="{{ clPrebidAmount }}" onInput="{{ clSetPrebidAmount }}" style="width:100%; height:50px; min-width:0; box-sizing:border-box; border:2px solid #0a5ca8; border-radius:12px; padding:0 36px; color:#002e5f; text-align:center; font-family:inherit; font-size:20px; font-weight:900; font-variant-numeric:tabular-nums; line-height:1; outline:none;"><span aria-hidden="true" data-prebid-roll="true" style="position:absolute; z-index:2; inset:2px; display:grid; place-items:center; overflow:hidden; border-radius:10px; background:#fff; color:#002e5f; font-family:inherit; font-size:20px; font-weight:900; font-variant-numeric:tabular-nums; line-height:1; pointer-events:none; opacity:{{ clPrebidRollOpacity }}; transition:{{ clPrebidRollTransition }};">{{ clPrebidAmountRoll }}</span></span><button onClick="{{ clPrebidPlus }}" aria-label="Sumar un incremento" style="display:grid; place-items:center; border:1px solid #cbd9e6; border-radius:12px; background:#f4f8fb; cursor:pointer;"><img src="assets/icons/lucide/plus.svg" alt="" style="width:19px; height:19px;"></button></div><small style="color:#718196;">Monto sugerido: <strong>{{ clPrebidSuggestedAmount }}</strong></small><sc-if value="{{ clInstallmentsEnabled }}"><strong style="display:block;color:#0a5ca8;font-size:13px;">{{ clInstallmentOfferTotal }}</strong></sc-if></div> 7773 <div style="display:grid; gap:9px; padding:14px; border:1px solid #dde7f0; border-radius:14px; background:#f8fbfd;"><div style="display:flex; align-items:center; justify-content:space-between; gap:14px;"><span><strong style="display:block; color:#27435f; font-size:13px;">En comisión</strong><small style="color:#7a8b9f;">Activá esta opción si ofertás para otra persona.</small></span><button onClick="{{ clTogglePrebidCommission }}" role="switch" data-dc-attr-aria-checked="{{ clPrebidCommission }}" aria-label="Activar o desactivar oferta en comisión" style="position:relative; width:48px; height:27px; flex:0 0 48px; border:0; border-radius:999px; background:{{ clPrebidCommissionBg }}; cursor:pointer;"><span style="position:absolute; top:4px; left:{{ clPrebidCommissionLeft }}; width:19px; height:19px; border-radius:50%; background:#fff; box-shadow:0 2px 7px rgba(0,0,0,.2);"></span></button></div><sc-if value="{{ clPrebidCommission }}" hint-placeholder-val="false"><input value="" data-dc-attr-value="{{ clPrebidCommissionFor }}" onInput="{{ clSetPrebidCommissionFor }}
7773" placeholder="Nombre del comprador" style="height:44px; box-sizing:border-box; border:1px solid #cbd9e6; border-radius:11px; padding:0 12px; color:#102f50; font:700 13px inherit; outline:none;"></sc-if></div> 7774 <div style="display:grid; gap:9px;"><span><strong style="display:block; color:#27435f; font-size:13px;">Animales incluidos</strong><small style="color:#7a8b9f;">{{ clPrebidCertMode }}</small></span><sc-if value="{{ clPrebidHasCerts }}" hint-placeholder-val="false"><div style="display:flex; flex-wrap:wrap; gap:8px;"><sc-for list="{{ clPrebidCertOptions }}" as="cert"><button type="button" role="{{ cert.role }}" aria-checked="{{ cert.selected }}" onClick="{{ cert.toggle }}" style="display:inline-flex; align-items:center; gap:7px; min-height:38px; border:1px solid {{ cert.border }}; border-radius:999px; padding:0 12px; background:{{ cert.bg }}; color:{{ cert.color }}; font:850 12px inherit; cursor:{{ cert.cursor }};"><img data-dc-attr-src="{{ cert.icon }}" alt="" style="width:15px; height:15px;">{{ cert.label }}</button></sc-for></div></sc-if></div> 7775 <sc-if value="{{ clPrebidHasMessage }}" hint-placeholder-val="false"><div style="padding:12px 13px; border-radius:12px; background:{{ clPrebidMessageBg }}; color:{{ clPrebidMessageColor }}; font-size:12px; font-weight:800;">{{ clPrebidMessage }}</div></sc-if> 7776 <div style="display:flex; justify-content:flex-end; gap:9px; flex-wrap:wrap; padding-top:2px;"><button onClick="{{ clClosePrebid }}" style="min-height:46px; border:1px solid #cbd9e6; border-radius:12px; padding:0 16px; background:#fff; color:#40566d; font:850 13px inherit; cursor:pointer;">Cancelar</button><button onClick="{{ clSubmitPrebid }}" disabled="{{ clPrebidSubmitting }}" style="min-height:46px; display:inline-flex; align-items:center; justify-content:center; gap:8px; border:0; border-radius:12px; padding:0 19px; background:#0a5ca8; color:#fff; font:900 13px inherit; cursor:pointer; opacity:{{ clPrebidSubmitOpacity }}; box-shadow:0 10px 24px rgba(10,92,168,.24);"><img src="assets/icons/lucide/gavel.svg" alt="" style="width:17px; height:17px; filter:brightness(0) invert(1);">{{ clPrebidSubmitLabel }}</button></div> 7777 </div> 7778 </section> 7779 </div> 7780 </sc-if> 7781 </sc-if> 7782 7783 <sc-if value="{{ isCabanaStats }}" hint-placeholder-val="{{ false }}"> 7784 <style> 7785 .cb-stats-page{min-height:100vh;background:#f3f7fb;color:#112033;padding:28px 20px 110px} 7786 .cb-stats-wrap{width:min(1180px,100%);margin:0 auto} 7787 .cb-stats-back{display:inline-flex;align-items:center;gap:8px;margin:0 0 18px;padding:8px 0;border:0;background:transparent;color:#31526f;font:700 13px inherit;cursor:pointer} 7788 .cb-stats-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px} 7789 .cb-stats-toolbar .cb-stats-back{margin:0} 7790 .cb-stats-export-actions{display:flex;align-items:center;gap:9px;flex-wrap:wrap;justify-content:flex-end} 7791 .cb-stats-export-button{min-height:39px;display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1px solid #cad9e7;border-radius:9px;padding:0 13px;background:#fff;color:#173956;font:850 12px inherit;cursor:pointer;box-shadow:0 5px 14px rgba(0,38,77,.06);transition:transform .18s,border-color .18s,box-shadow .18s} 7792 .cb-stats-export-button:hover{transform:translateY(-1px);border-color:var(--cbs-accent);box-shadow:0 8px 20px rgba(0,38,77,.1)} 7793 .cb-stats-export-button.is-story{border-color:var(--cbs-dark);background:var(--cbs-dark);color:#fff} 7794 .cb-stats-export-button:disabled{cursor:wait;opacity:.58;transform:none;box-shadow:none} 7795 .cb-stats-export-button img{width:16px;height:16px}.cb-stats-export-button.is-story img{filter:brightness(0) invert(1)} 7796 .cb-stats-hero{position:relative;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) minmax(180px,320px);align-items:center;gap:30px;padding:34px 38px;border-radius:8px;background:linear-gradient(120deg,var(--cbs-dark),var(--cbs-accent));color:#fff;box-shadow:0 18px 44px rgba(0,38,77,.18)} 7797 .cb-stats-hero:after{content:"";position:absolute;inset:auto -5% -55% 35%;height:170%;border:1px solid rgba(255,255,255,.18);border-radius:50%;pointer-events:none} 7798 .cb-stats-eyebrow,.cb-stats-section-label{display:block;font-size:11px;font-weight:900;letter-spacing:.12em;text-transform:uppercase} 7799 .cb-stats-eyebrow{color:rgba(255,255,255,.72)} 7800 .cb-stats-hero h1{margin:7px 0 6px;font-size:clamp(30px,4vw,48px);line-height:1.02;letter-spacing:0} 7801 .cb-stats-hero p{max-width:680px;margin:0;color:rgba(255,255,255,.8);font-size:14px;line-height:1.55} 7802 .cb-stats-meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}
7803 .cb-stats-meta span{display:inline-flex;align-items:center;gap:7px;min-height:30px;padding:6px 10px;border:1px solid rgba(255,255,255,.22);border-radius:100px;background:rgba(255,255,255,.09);font-size:11px;font-weight:800} 7804 .cb-stats-logo{position:relative;z-index:1;display:grid;place-items:center;min-height:126px;padding:20px;border-radius:8px;background:rgba(255,255,255,.94)} 7805 .cb-stats-logo img{display:block;width:100%;max-height:92px;object-fit:contain} 7806 .cb-stats-kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:18px} 7807 .cb-stats-kpi{min-height:124px;padding:18px;border:1px solid #dfe8f1;border-radius:8px;background:#fff} 7808 .cb-stats-kpi small{display:block;min-height:28px;color:#71839a;font-size:10px;font-weight:900;letter-spacing:.08em;text-transform:uppercase} 7809 .cb-stats-kpi strong{display:block;margin-top:8px;color:#10253d;font-size:30px;line-height:1;letter-spacing:0} 7810 .cb-stats-kpi span{display:block;margin-top:9px;color:#7c8b9e;font-size:11px;line-height:1.35} 7811 .cb-stats-grid{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(280px,.8fr);gap:16px;margin-top:16px} 7812 .cb-stats-panel{min-width:0;padding:22px;border:1px solid #dfe8f1;border-radius:8px;background:#fff} 7813 .cb-stats-panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:22px} 7814 .cb-stats-section-label{color:var(--cbs-accent)} 7815 .cb-stats-panel h2{margin:4px 0 0;font-size:20px;line-height:1.2} 7816 .cb-stats-panel-head p{margin:4px 0 0;color:#7c8b9e;font-size:12px} 7817 .cb-stats-bar-list{display:grid;gap:17px} 7818 .cb-stats-bar-copy{display:flex;justify-content:space-between;gap:12px;margin-bottom:7px;font-size:12px} 7819 .cb-stats-bar-copy strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} 7820 .cb-stats-bar-copy span{flex:0 0 auto;color:#61748a;font-weight:800} 7821 .cb-stats-bar-track{height:7px;overflow:hidden;border-radius:100px;background:#e8eef5} 7822 .cb-stats-bar-fill{height:100%;border-radius:100px;background:linear-gradient(90deg,var(--cbs-dark),var(--cbs-accent))} 7823 .cb-stats-ranking{display:grid;gap:8px} 7824 .cb-stats-rank{display:grid;grid-template-columns:28px minmax(0,1fr) auto;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid #edf2f7} 7825 .cb-stats-rank:last-child{border-bottom:0} 7826 .cb-stats-rank i{display:grid;place-items:center;width:26px;height:26px;border-radius:50%;background:#edf4fa;color:var(--cbs-accent);font-style:normal;font-size:11px;font-weight:900} 7827 .cb-stats-rank strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px} 7828 .cb-stats-rank span{color:#667a91;font-size:11px;font-weight:800} 7829 .cb-stats-quality{display:grid;grid-template-columns:1.1fr 1fr 1fr;gap:0;margin-top:16px;border:1px solid #dfe8f1;border-radius:8px;background:#fff} 7830 .cb-stats-quality-intro,.cb-stats-quality-item{padding:20px} 7831 .cb-stats-quality-item{border-left:1px solid #e5ecf3} 7832 .cb-stats-quality h2{margin:5px 0 0;font-size:19px} 7833 .cb-stats-quality p{margin:6px 0 0;color:#74869a;font-size:12px;line-height:1.45} 7834 .cb-stats-quality-item strong{display:block;font-size:24px} 7835 .cb-stats-quality-item span{display:block;margin-top:7px;color:#74869a;font-size:11px;line-height:1.4} 7836 .cb-stats-empty{padding:24px 0;color:#7c8b9e;font-size:13px;text-align:center} 7837 .cb-stats-overview-list{display:grid;gap:10px;margin-top:16px} 7838 .cb-stats-overview-row{display:grid;grid-template-columns:minmax(220px,1.5fr) repeat(4,minmax(90px,.55fr)) auto;align-items:center;gap:12px;width:100%;padding:16px 18px;border:1px solid #dfe8f1;border-radius:10px;background:#fff;color:#112033;text-align:left;cursor:pointer} 7839 .cb-stats-overview-row:hover{border-color:var(--cbs-accent);box-shadow:0 10px 26px rgba(0,38,77,.08)} 7840 .cb-stats-overview-row strong,.cb-stats-overview-row span{display:block} 7841 .cb-stats-overview-row small{display:block;color:#71839a;font-size:10px;font-weight:900;letter-spacing:.06em;text-transform:uppercase} 7842 .cb-stats-overview-status{padding:6px 10px;border-radius:999px;background:#e8f7f1;color:#087653;font-size:10px;font-weight:900}.cb-stats-overview-status.is-error{background:#fff0f0;color:#b4232f} 7843 .cb-stats-activity{margin-top:16px} 7844 .cb-stats-rp-scroll{overflow-x:auto;margin-top:14px;border:1px solid #dfe8f1;border-radius:10px;background:#fff}
7845 .cb-stats-rp-table{min-width:1260px} 7846 .cb-stats-rp-head,.cb-stats-rp-row{display:grid;grid-template-columns:minmax(115px,.75fr) minmax(230px,1.55fr) repeat(6,minmax(82px,.55fr)) minmax(105px,.7fr) minmax(105px,.7fr);align-items:center;gap:12px;padding:12px 16px} 7847 .cb-stats-rp-head{position:sticky;top:0;z-index:1;background:#f0f5fa;color:#6b7e93;font-size:9px;font-weight:900;letter-spacing:.08em;text-transform:uppercase} 7848 .cb-stats-rp-row{border-top:1px solid #edf2f7;color:#18314b;font-size:12px} 7849 .cb-stats-rp-row:nth-child(odd){background:#fbfdff} 7850 .cb-stats-rp-row strong{font-size:13px;color:#10233b}.cb-stats-rp-row small{display:block;margin-top:3px;color:#71839a;font-size:10px;line-height:1.3} 7851 .cb-stats-rp-number{font-weight:850;font-variant-numeric:tabular-nums}.cb-stats-rp-time{color:var(--cbs-accent);font-weight:900;font-variant-numeric:tabular-nums} 7852 .cb-stats-context{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:14px;color:rgba(255,255,255,.78);font-size:11px;font-weight:800} 7853 .cb-stats-context span{display:inline-flex;align-items:center;min-height:27px;padding:0 9px;border:1px solid rgba(255,255,255,.18);border-radius:999px;background:rgba(255,255,255,.08)} 7854 .cb-stats-pulse{margin-top:18px;padding:22px;border:1px solid #dfe8f1;border-radius:12px;background:#fff} 7855 .cb-stats-pulse-head{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;margin-bottom:16px} 7856 .cb-stats-pulse-head h2{margin:4px 0 0;font-size:24px;line-height:1.08}.cb-stats-pulse-head p{max-width:620px;margin:5px 0 0;color:#71839a;font-size:12px;line-height:1.45} 7857 .cb-stats-period{flex:0 0 auto;color:#526a82;font-size:10px;font-weight:900;letter-spacing:.05em;text-transform:uppercase} 7858 .cb-stats-decision-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px} 7859 .cb-stats-decision{position:relative;min-width:0;min-height:148px;padding:17px;border:1px solid #dfe8f1;border-radius:10px;background:#f8fbfd} 7860 .cb-stats-decision small{display:block;min-height:25px;color:#65798f;font-size:9px;font-weight:900;letter-spacing:.09em;text-transform:uppercase} 7861 .cb-stats-decision strong{display:block;margin-top:5px;color:#10253d;font-size:32px;line-height:1;font-variant-numeric:tabular-nums}.cb-stats-decision strong em{font-size:15px;font-style:normal} 7862 .cb-stats-decision p{margin:9px 0 0;color:#74869a;font-size:10.5px;line-height:1.4} 7863 .cb-stats-decision-status{display:inline-flex;align-items:center;gap:6px;margin-top:10px;color:#536c83;font-size:9px;font-weight:900;text-transform:uppercase}.cb-stats-decision-status:before{content:"";width:7px;height:7px;border-radius:50%;background:#8da0b3} 7864 .cb-stats-decision.is-good .cb-stats-decision-status:before{background:#16a06f}.cb-stats-decision.is-watch .cb-stats-decision-status:before{background:#e69c24}.cb-stats-decision.is-alert .cb-stats-decision-status:before{background:#df4050} 7865 .cb-stats-commercial-grid{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(360px,.9fr);gap:14px;margin-top:14px} 7866 .cb-stats-diagnosis{display:grid;gap:9px;margin-top:14px} 7867 .cb-stats-insight{display:grid;grid-template-columns:34px minmax(0,1fr);gap:11px;align-items:start;padding:13px;border:1px solid #e2eaf2;border-radius:10px;background:#fbfdff} 7868 .cb-stats-insight i{display:grid;place-items:center;width:32px;height:32px;border-radius:9px;background:#eaf3fb;color:var(--cbs-accent);font-style:normal;font-size:13px;font-weight:950} 7869 .cb-stats-insight strong{display:block;color:#18314b;font-size:12px}.cb-stats-insight span{display:block;margin-top:3px;color:#71839a;font-size:10.5px;line-height:1.4} 7870 .cb-stats-trend{display:grid;gap:10px;margin-top:16px} 7871 .cb-stats-trend-row{display:grid;grid-template-columns:58px minmax(80px,1fr) 48px;align-items:center;gap:9px;color:#526a82;font-size:10px;font-weight:850} 7872 .cb-stats-trend-track{height:24px;overflow:hidden;border-radius:7px;background:#eef3f7}.cb-stats-trend-fill{display:flex;align-items:center;justify-content:flex-end;min-width:4px;height:100%;box-sizing:border-box;padding-right:7px;border-radius:7px;background:linear-gradient(90deg,var(--cbs-dark),var(--cbs-accent));color:#fff;font-size:9px} 7873 .cb-stats-commercial-scroll{overflow-x:auto;margin-top:14px;border:1px solid #dfe8f1;border-radius:10px;background:#fff} 7874 .cb-stats-category-table{min-width:780px}.cb-stats-category-head,.cb-stats-category-row{display:grid;grid-template-columns:minmax(240px,1.5fr) repeat(4,minmax(100px,.65fr));align-items:center;gap:12px;padding:12px 15px} 7875 .cb-stats-category-head{background:#f0f5fa;color:#6b7e93;font-size:9px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.cb-stats-category-row{border-top:1px solid #edf2f7;color:#18314b;font-size:11px}.cb-stats-category-row strong{font-size:12px}.cb-stats-category-row small{display:block;margin-top:3px;color:#71839a;font-size:9.5px} 7876 .cb-stats-opportunity-table{min-width:800px}.cb-stats-opportunity-head,.cb-stats-opportunity-row{display:grid;grid-template-columns:minmax(260px,1.55fr) repeat(4,minmax(100px,.6fr));align-items:center;gap:12px;padding:12px 15px} 7877 .cb-stats-opportunity-head{background:#f0f5fa;color:#6b7e93;font-size:9px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.cb-stats-opportunity-row{border-top:1px solid #edf2f7;color:#18314b;font-size:11px}.cb-stats-opportunity-row strong{font-size:12px}.cb-stats-opportunity-row small{display:block;margin-top:3px;color:#71839a;font-size:9.5px;line-height:1.3} 7878 .cb-stats-visitor-table{min-width:1060px}.cb-stats-visitor-head,.cb-stats-visitor-row{display:grid;grid-template-columns:minmax(170px,1fr) minmax(200px,1.1fr) minmax(150px,.85fr) minmax(300px,1.65fr) minmax(105px,.62fr) minmax(145px,.8fr);align-items:center;gap:12px;padding:12px 15px} 7879 .cb-stats-visitor-head{background:#f0f5fa;color:#6b7e93;font-size:9px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.cb-stats-visitor-row{border-top:1px solid #edf2f7;color:#18314b;font-size:11px}.cb-stats-visitor-row strong{font-size:12px}.cb-stats-visitor-row small{display:block;margin-top:3px;color:#71839a;font-size:9.5px;line-height:1.3}
7880 .cb-stats-pagination{display:flex;align-items:center;justify-content:flex-end;gap:9px;margin-top:12px}.cb-stats-pagination span{color:#65798f;font-size:11px;font-weight:800}.cb-stats-pagination button{border:1px solid #cedbe8;border-radius:9px;padding:7px 11px;background:#fff;color:#183b5e;font:800 11px inherit;cursor:pointer}.cb-stats-pagination button:disabled{opacity:.4;cursor:not-allowed} 7881 .cb-stats-note{display:flex;align-items:flex-start;gap:10px;margin-top:14px;padding:13px 15px;border:1px solid #dfe8f1;border-radius:10px;background:#f8fbfd;color:#65798f;font-size:10.5px;line-height:1.45}.cb-stats-note strong{color:#27435f} 7882 .cb-stats-skeleton{overflow:hidden;position:relative;background:#e7edf4} 7883 .cb-stats-skeleton:after{content:"";position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,rgba(255,255,255,.75),transparent);animation:cbStatsShimmer 1.25s infinite} 7884 .cb-stats-loading{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-top:18px} 7885 .cb-stats-loading span{height:124px;border-radius:8px} 7886 @keyframes cbStatsShimmer{100%{transform:translateX(100%)}} 7887 @media(max-width:900px){ 7888 .cb-stats-hero{grid-template-columns:1fr} 7889 .cb-stats-logo{max-width:360px} 7890 .cb-stats-kpis,.cb-stats-loading{grid-template-columns:repeat(2,minmax(0,1fr))} 7891 .cb-stats-grid{grid-template-columns:1fr} 7892 .cb-stats-decision-grid{grid-template-columns:repeat(2,minmax(0,1fr))} 7893 .cb-stats-commercial-grid{grid-template-columns:1fr} 7894 } 7895 @media(max-width:600px){ 7896 .cb-stats-page{padding:18px 14px 104px} 7897 .cb-stats-toolbar{align-items:stretch;flex-direction:column}.cb-stats-toolbar .cb-stats-back{align-self:flex-start}.cb-stats-export-actions{display:grid;grid-template-columns:1fr 1fr}.cb-stats-export-button{padding:0 10px} 7898 .cb-stats-hero{gap:20px;padding:25px 20px} 7899 .cb-stats-hero h1{font-size:30px} 7900 .cb-stats-logo{min-height:104px;padding:14px} 7901 .cb-stats-kpis,.cb-stats-loading{gap:8px} 7902 .cb-stats-kpi{min-height:112px;padding:15px} 7903 .cb-stats-kpi small{font-size:9px} 7904 .cb-stats-kpi strong{font-size:25px} 7905 .cb-stats-panel{padding:18px 15px} 7906 .cb-stats-panel-head{display:block} 7907 .cb-stats-quality{grid-template-columns:1fr} 7908 .cb-stats-quality-item{border-top:1px solid #e5ecf3;border-left:0} 7909 .cb-stats-overview-row{grid-template-columns:1fr 1fr;padding:15px}.cb-stats-overview-row>span:first-child,.cb-stats-overview-status{grid-column:1/-1} 7910 .cb-stats-pulse{padding:17px 14px}.cb-stats-pulse-head{display:block}.cb-stats-period{display:block;margin-top:9px}.cb-stats-decision-grid{gap:8px}.cb-stats-decision{min-height:138px;padding:14px}.cb-stats-decision strong{font-size:27px} 7911 } 7912 </style> 7913 <main class="cb-stats-page" style="--cbs-accent:{{ cbsAccent }};--cbs-dark:{{ cbsDark }};"> 7914 <sc-if value="{{ cbsIsOverview }}" hint-placeholder-val="false"> 7915 <div class="cb-stats-wrap"> 7916 <button class="cb-stats-back" onClick="{{ cbsBack }}" aria-label="Volver a cabañas"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i> Volver a cabañas</button> 7917 <section class="cb-stats-hero"><div><span class="cb-stats-eyebrow">Rosgan Cabañas · Control integral</span><h1>EstadÃsticas de cabañas</h1><p>Seguimiento real de cada remate. Cada evento queda aislado por su ID de remate para evitar cruces de información.</p><div class="cb-stats-meta"><span>Registro activo por remate</span><span>Consulta consolidada</span></div></div></section> 7918 <section class="cb-stats-kpis" aria-label="Resumen de todos los remates"><article class="cb-stats-kpi"><small>Remates controlados</small><strong>{{ cbsOverviewAuctionCount }}</strong><span>Remates de cabaña disponibles.</span></article><article class="cb-stats-kpi"><small>Señales registradas</small><strong>{{ cbsOverviewEventCount }}</strong><span>Eventos acumulados sin mezclar remates.</span></article><article class="cb-stats-kpi"><small>Vistas</small><strong>{{ cbsOverviewViewCount }}</strong><span>Vistas de lote y de RP.</span></article><article class="cb-stats-kpi"><small>Videos</small><strong>{{ cbsOverviewVideoCount }}</strong><span>Reproducciones efectivas.</span></article><article class="cb-stats-kpi"><small>Contactos</small><strong>{{ cbsOverviewContactCount }}</strong><span>WhatsApp y llamadas.</span></article></section> 7919 <sc-if value="{{ cbsOverviewLoading }}" hint-placeholder-val="true"><div class="cb-stats-loading"><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span></div></sc-if> 7920 <sc-if value="{{ cbsOverviewReady }}" hint-placeholder-val="false"><section class="cb-stats-panel cb-stats-activity"><div class="cb-stats-panel-head"><div><span class="cb-stats-section-label">Detalle por remate</span><h2>Actividad registrada</h2><p>Abrà un remate para consultar catálogo, ranking y métricas individuales.</p></div></div><sc-if value="{{ cbsOverviewHasRows }}" hint-placeholder-val="true"><div class="cb-stats-overview-list"><sc-for list="{{ cbsOverviewRows }}" as="row"><button class="cb-stats-overview-row" onClick="{{ row.open }}"><span><small>REMATE {{ row.id }}</small><strong>{{ row.title }}</strong></span><span><small>SEÃALES</small><strong>{{ row.events }}</strong></span><span><small>VISTAS</small><strong>{{ row.views }}</strong></span><span><small>VIDEOS</small><strong>{{ row.videos }}</strong></span><span><small>CONTACTOS</small><strong>{{ row.contacts }}</strong></span><em class="cb-stats-overview-status {{ row.statusClass }}">{{ row.status }}</em></button></sc-for></div></sc-if><sc-if value="{{ cbsOverviewEmpty }}" hint-placeholder-val="false"><div class="cb-stats-empty">No hay remates disponibles.</div></sc-if><sc-if value="{{ cbsOverviewHasError }}" hint-placeholder-val="false"><div class="cb-stats-empty">{{ cbsOverviewError }}</div></sc-if></section></sc-if> 7921 </div> 7922 </sc-if> 7923 <sc-if value="{{ cbsIsAuction }}" hint-placeholder-val="true"> 7924 <div class="cb-stats-wrap"> 7925 <div class="cb-stats-toolbar"> 7926 <button class="cb-stats-back" onClick="{{ cbsBack }}" aria-label="Volver al catálogo"><i class="dc-lucide dc-lucide-arrow-left" aria-hidden="true"></i> Volver al catálogo</button> 7927 <div class="cb-stats-export-actions"><button class="cb-stats-export-button" onClick="{{ cbsDownloadExcel }}" disabled="{{ cbsExportDisabled }}"><img src="assets/icons/lucide/file-spreadsheet.svg" alt="">{{ cbsExcelLabel }}</button><button class="cb-stats-export-button is-story" onClick="{{ cbsDownloadStory }}" disabled="{{ cbsExportDisabled }}"><img src="assets/icons/lucide/image-down.svg" alt="">{{ cbsStoryLabel }}</button></div> 7928 </div> 7929 <section class="cb-stats-hero"> 7930 <div>
7931 <span class="cb-stats-eyebrow">Análisis comercial · {{ cbsTitle }}</span> 7932 <h1>Centro de estadÃsticas</h1> 7933 <p>{{ cbsTitle }}</p> 7934 <div class="cb-stats-context"><span>{{ cbsRpCount }} RPs</span><span>{{ cbsLotCount }} lotes</span><span>{{ cbsCategoryCount }} categorÃas</span><span>Actualización cada 10 segundos</span></div> 7935 </div> 7936 <sc-if value="{{ cbsHasLogo }}" hint-placeholder-val="{{ true }}"><div class="cb-stats-logo"><img data-dc-attr-src="{{ cbsLogo }}" alt="Cabaña principal"></div></sc-if> 7937 </section> 7938 7939 <sc-if value="{{ cbsLoading }}" hint-placeholder-val="{{ false }}"> 7940 <div class="cb-stats-loading"><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span></div> 7941 </sc-if> 7942 7943 <sc-if value="{{ cbsReady }}" hint-placeholder-val="{{ true }}"> 7944 <section class="cb-stats-pulse" aria-label="Pulso comercial del remate"> 7945 <div class="cb-stats-pulse-head"><div><span class="cb-stats-section-label">Decisiones comerciales</span><h2>Pulso del interés comprador</h2><p>Indicadores relativos: muestran si el catálogo logra transformar atención en evaluación, difusión y consultas.</p></div><span class="cb-stats-period">Acumulado del remate</span></div> 7946 <sc-if value="{{ cbsAnalyticsLoading }}" hint-placeholder-val="false"><div class="cb-stats-loading"><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span><span class="cb-stats-skeleton"></span></div></sc-if> 7947 <sc-if value="{{ cbsAnalyticsReady }}" hint-placeholder-val="true"><div class="cb-stats-decision-grid"> 7948 <article class="cb-stats-decision {{ cbsContactHealthClass }}"><small>Conversión a consulta</small><strong>{{ cbsContactRate }}</strong><p>{{ cbsAnalyticsContactLabel }}
7948 sobre {{ cbsAnalyticsLotViews }} aperturas de lote.</p><span class="cb-stats-decision-status">{{ cbsContactHealth }}</span></article> 7949 <article class="cb-stats-decision {{ cbsDepthHealthClass }}"><small>Profundidad de evaluación</small><strong>{{ cbsEvaluationDepth }} <em>RPs</em></strong><p>Promedio de reproductores examinados por cada apertura de lote.</p><span class="cb-stats-decision-status">{{ cbsDepthHealth }}</span></article> 7950 <article class="cb-stats-decision {{ cbsVideoHealthClass }}"><small>Activación de video</small><strong>{{ cbsVideoRate }}</strong><p>{{ cbsAnalyticsVideoPlays }} reproducciones sobre {{ cbsAnalyticsRpViews }} vistas de RP.</p><span class="cb-stats-decision-status">{{ cbsVideoHealth }}</span></article> 7951 <article class="cb-stats-decision {{ cbsAttentionHealthClass }}"><small>Tiempo de evaluación</small><strong>{{ cbsAnalyticsAvgDuration }}</strong><p>Permanencia promedio efectiva al analizar un reproductor.</p><span class="cb-stats-decision-status">{{ cbsAttentionHealth }}</span></article> 7952 </div></sc-if> 7953 </section> 7954 7955 <div class="cb-stats-commercial-grid"> 7956 <section class="cb-stats-panel"> 7957 <div class="cb-stats-panel-head"><div><span class="cb-stats-section-label">Lectura ejecutiva</span><h2>Qué está diciendo la demanda</h2><p>Hallazgos calculados sobre el comportamiento observado.</p></div></div> 7958 <div class="cb-stats-diagnosis"><sc-for list="{{ cbsInsights }}" as="item" hint-placeholder-count="3"><article class="cb-stats-insight"><i>{{ item.index }}</i><span><strong>{{ item.title }}</strong><span>{{ item.detail }}</span></span></article></sc-for></div> 7959 </section> 7960 <section class="cb-stats-panel"> 7961 <div class="cb-stats-panel-head"><div><span class="cb-stats-section-label">Evolución</span><h2>Interés por dÃa</h2><p>Aperturas y evaluaciones; sirve para detectar cuándo empujar comunicación.</p></div></div> 7962 <sc-if value="{{ cbsHasTrend }}" hint-placeholder-val="true"><div class="cb-stats-trend"><sc-for list="{{ cbsTrend }}" as="item" hint-placeholder-count="6"><div class="cb-stats-trend-row"><span>{{ item.label }}</span><div class="cb-stats-trend-track"><div class="cb-stats-trend-fill" style="width:{{ item.width }};">{{ item.value }}</div></div><span>{{ item.intent }}</span></div></sc-for></div></sc-if> 7963 <sc-if value="{{ cbsNoTrend }}" hint-placeholder-val="false"><div class="cb-stats-empty">TodavÃa no hay una serie diaria suficiente.</div></sc-if> 7964 </section> 7965 </div> 7966 7967 <section class="cb-stats-panel cb-stats-activity"> 7968 <div class="cb-stats-panel-head"><div><span class="cb-stats-section-label">Demanda por segmento</span><h2>Rendimiento comercial por categorÃa</h2><p>Compara interés por RP, uso de video, señales de intención y tiempo de evaluación. No premia a una categorÃa solamente por tener más animales.</p></div></div> 7969 <sc-if value="{{ cbsHasCategoryPerformance }}" hint-placeholder-val="true"><div class="cb-stats-commercial-scroll"><div class="cb-stats-category-table"><div class="cb-stats-category-head"><span>CategorÃa</span><span>Vistas / RP</span><span>Video / vista</span><span>Intención</span><span>Tiempo prom.</span></div><sc-for list="{{ cbsCategoryPerformance }}" as="item" hint-placeholder-count="4"><article class="cb-stats-category-row"><span><strong>{{ item.label }}</strong><small>{{ item.published }} RPs publicados</small></span><span><strong>{{ item.viewsPerRp }}</strong><small>{{ item.views }} evaluaciones</small></span><span><strong>{{ item.videoRate }}</strong><small>{{ item.videos }} reproducciones</small></span><span><strong>{{ item.intent }}</strong><small>contactos + compartidos</small></span><span><strong>{{ item.avgTime }}</strong><small>por medición</small></span></article></sc-for></div></div></sc-if> 7970 <sc-if value="{{ cbsNoCategoryPerformance }}" hint-placeholder-val="false"><div class="cb-stats-empty">TodavÃa no hay actividad suficiente para comparar categorÃas.</div></sc-if> 7971 </section> 7972 7973 <section class="cb-stats-panel cb-stats-activity"> 7974 <div class="cb-stats-panel-head"><div><span class="cb-stats-section-label">Acción por reproductor</span><h2>Oportunidades prioritarias</h2><p>Los RPs con mayor atención, ordenados para decidir dónde reforzar difusión o impulsar una consulta.</p></div><strong>{{ cbsOpportunityCount }} analizados</strong></div> 7975 <sc-if value="{{ cbsHasOpportunities }}" hint-placeholder-val="true"><div class="cb-stats-commercial-scroll"><div class="cb-stats-opportunity-table"><div class="cb-stats-opportunity-head"><span>RP y lote</span><span>Vistas RP</span><span>Video / vista</span><span>Tiempo prom.</span><span>Intención</span></div><sc-for list="{{ cbsOpportunities }}" as="item" hint-placeholder-count="8"><article class="cb-stats-opportunity-row"><span><strong>{{ item.rp }} · {{ item.lot }}</strong><small>{{ item.category }}</small></span><span><strong>{{ item.rpViews }}</strong><small>evaluaciones</small></span><span><strong>{{ item.videoRate }}</strong><small>{{ item.videos }} plays</small></span><span><strong>{{ item.avgTime }}</strong><small>{{ item.samplesLabel }}</small></span><span><strong>{{ item.intent }}</strong><small>acciones</small></span></article></sc-for></div></div></sc-if> 7976 <sc-if value="{{ cbsNoOpportunities }}" hint-placeholder-val="false"><div class="cb-stats-empty">TodavÃa no hay actividad individual registrada por RP.</div></sc-if> 7977 </section> 7978 7979 <section class="cb-stats-panel cb-stats-activity"> 7980 <div class="cb-stats-panel-head"><div><span class="cb-stats-section-label">Audiencia identificada</span><h2>Visitantes del remate</h2><p>Nombre, correo y teléfono de usuarios registrados; quienes navegan sin iniciar sesión se muestran como Anónimo.</p></div><strong>{{ cbsVisitorCount }} visitantes</strong></div> 7981 <sc-if value="{{ cbsHasVisitors }}" hint-placeholder-val="true"><div class="cb-stats-commercial-scroll"><div class="cb-stats-visitor-table"><div class="cb-stats-visitor-head"><span>Nombre y apellido</span><span>Correo electrónico</span><span>Teléfono</span><span>Lotes vistos</span><span>Tiempo total</span><span>Ãltima visita</span></div><sc-for list="{{ cbsVisitors }}" as="visitor" hint-placeholder-count="6"><article class="cb-stats-visitor-row"><span><strong>{{ visitor.name }}</strong></span><span><strong>{{ visitor.email }}</strong></span><span><strong>{{ visitor.phone }}</strong></span><span><strong>{{ visitor.lots }}</strong><small>{{ visitor.detail }}</small></span><span><strong>{{ visitor.totalTime }}</strong><small>{{ visitor.activity }}</small></span><span><strong>{{ visitor.lastSeen }}</strong></span></article></sc-for></div></div><div class="cb-stats-pagination"><button onClick="{{ cbsVisitorPrevious }}" disabled="{{ cbsVisitorPreviousDisabled }}">Anterior</button><span>Página {{ cbsVisitorPage }} de {{ cbsVisitorPageCount }} · 25 por página</span><button onClick="{{ cbsVisitorNext }}" disabled="{{ cbsVisitorNextDisabled }}">Siguiente</button></div></sc-if> 7982 <sc-if value="{{ cbsNoVisitors }}" hint-placeholder-val="false"><div class="cb-stats-empty">TodavÃa no hay visitantes registrados para este remate.</div></sc-if> 7983 </section> 7984 7985 <div class="cb-stats-note"><strong>Cómo leerlo:</strong><span>âIntenciónâ suma contactos y compartidos. Cada visitante registrado se identifica mediante su sesión validada; las visitas sin inicio de sesión se conservan como Anónimo.</span></div> 7986 </sc-if> 7987 </div> 7988 </sc-if> 7989 </main> 7990 </sc-if> 7991 7992 <!-- MOBILE CARGAR LOTE FAB --> 7993 <sc-if value="{{ lotDestinationOpen }}" hint-placeholder-val="false"> 7994 <div class="lot-destination-backdrop" onKeyDown="{{ lotDestinationKey }}"> 7995 <section class="lot-destination-dialog" role="dialog" aria-modal="true" aria-label="Elegir dónde cargar el lote"> 7996 <div class="lot-destination-heading"><div><small>PUBLICà TU PRÃXIMO LOTE</small><h2>Elegà el destino</h2></div><button class="lot-destination-close" autofocus onClick="{{ closeLotDestination }}" aria-label="Cerrar selector">â</button></div> 7997 <p class="lot-destination-intro">Elegà uno de los destinos habilitados para tu cuenta.</p> 7998 <sc-if value="{{ lotDestinationRoslot }}"><button class="lot-destination-card destination-roslot" onClick="{{ chooseRoslotDestination }}"><span class="destination-roslot-icon"><img src="/assets/icons/lucide/tag.svg" alt=""></span><span class="destination-content"><strong>Roslot</strong><small>Publicá en el mercado digital de Rosgan</small></span><img class="destination-arrow" src="/assets/icons/lucide/arrow-up-right.svg" alt=""></button></sc-if> 7999 <sc-if value="{{ lotDestinationLoading }}"><p>Consultando remates habilitadosâ¦</p></sc-if> 8000 <sc-if value="{{ lotDestinationError }}"><p role="alert">{{ lotDestinationError }}</p><button onClick="{{ goLotDestination }}">Reintentar</button></sc-if> 8001 <sc-for list="{{ lotDestinationChoices }}" as="destination"><button class="lot-destination-card" onClick="{{ destination.choose }}"><span class="destination-content"><span class="destination-type">{{ destination.type }}</span><strong>{{ destination.title }}</strong><span class="destination-logos"><sc-for list="{{ destination.logos }}" as="brand"><span class="destination-logo" title="{{ brand.name }}" style="background:{{ brand.color }};"><img data-dc-attr-src="{{ brand.logo }}" alt="{{ brand.name }}"></span></sc-for></span></span><img class="destination-arrow" src="/assets/icons/lucide/arrow-up-right.svg" alt=""></button></sc-for> 8002 <sc-if value="{{ lotDestinationEmpty }}"><p>No tenés remates habilitados para cargar lotes en este momento.</p></sc-if> 8003 <p class="lot-destination-note">Solo ves los destinos disponibles para tu cuenta.</p> 8004 </section> 8005 </div> 8006 </sc-if> 8007 <sc-if value="{{ showLoadFab }}" hint-placeholder-val="{{ false }}"> 8008 <button class="app-mobile-load-fab" onClick="{{ goMobileLoad }}" style="position:fixed;
8008 right:18px; bottom:calc(72px + env(safe-area-inset-bottom)); z-index:55; display:inline-flex; align-items:center; gap:9px; background:#e63946; color:#fff; border:none; border-radius:100px; padding:14px 22px; font-size:14.5px; font-weight:800; font-family:inherit; cursor:pointer; letter-spacing:0.01em; box-shadow:0 12px 30px rgba(230,57,70,0.5); animation:bidPulse 2.4s infinite;"><span style="font-size:19px; line-height:1;"><i class="dc-lucide dc-lucide-plus" aria-hidden="true"></i></span>Cargar lote</button> 8009 </sc-if> 8010 8011 <!-- MOBILE BOTTOM NAV --> 8012 <sc-if value="{{ compactNavigation }}" hint-placeholder-val="{{ false }}"> 8013 <nav class="app-mobile-bottom-nav" aria-label="Navegación principal" style="position:fixed; bottom:0; left:0; right:0; background:rgba(255,255,255,0.94); backdrop-filter:blur(14px); border-top:1px solid #dfe7f0; display:flex; justify-content:space-around; padding:10px 0 max(14px, env(safe-area-inset-bottom)); z-index:50;"> 8014 <sc-for list="{{ mobileNavItems }}" as="n" hint-placeholder-count="5"> 8015 <button type="button" class="app-mobile-nav-item" onClick="{{ n.go }}" aria-current="{{ n.current }}" style="color:{{ n.mobColor }};"><span class="app-mobile-nav-icon" style="background:{{ n.iconBg }};"><svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M0 0" data-dc-svg-d="{{ n.mobilePath }}"></path></svg></span><span class="app-mobile-nav-label">{{ n.label }}</span></button> 8016 </sc-for> 8017 </nav> 8018 </sc-if> 8019 8020 <sc-if data-public-blocked-overlay value="{{ profilePhotoViewerOpen }}" hint-placeholder-val="{{ false }}"> 8021 <div style="position:fixed; inset:0; z-index:140; display:grid; place-items:center; padding:24px; background:rgba(0,14,32,0.86);
8021 backdrop-filter:blur(7px);"> 8022 <figure style="position:relative; width:min(880px, 94vw); height:min(82vh, 880px); margin:0; display:grid; place-items:center;"> 8023 <img data-dc-attr-src="{{ profilePhotoViewerUrl }}" alt="{{ profilePhotoViewerAlt }}" style="display:block; max-width:100%; max-height:100%; width:auto; height:auto; object-fit:contain; border-radius:8px; box-shadow:0 24px 80px rgba(0,0,0,0.46); background:#fff;"> 8024 <button onClick="{{ closeProfilePhotoViewer }}" title="Cerrar foto" aria-label="Cerrar foto" style="position:absolute; top:0; right:0; width:42px; height:42px; border:1px solid rgba(255,255,255,0.45); border-radius:50%; background:rgba(0,29,61,0.82); display:grid; place-items:center; cursor:pointer; box-shadow:0 8px 24px rgba(0,0,0,0.25);"> 8025 <img src="assets/icons/lucide/x.svg" alt="" style="width:20px; height:20px; filter:brightness(0) invert(1);"> 8026 </button> 8027 </figure> 8028 </div> 8029 </sc-if> 8030 8031 <!-- TOAST --> 8032 <sc-if value="{{ toast }}" hint-placeholder-val="{{ false }}"> 8033 <div style="position:fixed; bottom:{{ toastBottom }}; left:50%; transform:translateX(-50%); background:#001d3d; color:#fff; padding:12px 22px; border-radius:100px; font-size:13.5px; font-weight:600; box-shadow:0 14px 40px rgba(0,20,45,0.4); z-index:60; animation:floatIn 300ms both; white-space:nowrap; max-width:90vw; overflow:hidden; text-overflow:ellipsis;">{{ toast }}</div> 8034 </sc-if> 8035 <sc-if value="{{ showRoslotLiveToast }}" hint-placeholder-val="{{ false }}"> 8036 <button class="roslot-event-toast" onClick="{{ openRoslotLiveToast }}" aria-live="assertive" aria-label="{{ roslotLiveToastAria }}"> 8037 <span class="roslot-event-toast-icon"><img data-dc-attr-src="{{ roslotLiveToastIcon }}" alt=""></span> 8038 <span class="roslot-event-toast-copy"><strong>{{ roslotLiveToastTitle }}</strong><span>{{ roslotLiveToastBody }}</span></span> 8039 <span class="roslot-event-toast-arrow">âº</span> 8040 </button> 8041 </sc-if> 8042 </div> 8043</sc-if> 8044</x-dc>
8045<script src="/assets/rosgan-tv-editor.js"></script>
vendor: 1 bytes, line 8045
8045
8046<script src="/assets/rosgan-tv-health.js"></script>
vendor: 1 bytes, line 8046
8046
8047<script src="/assets/rosgan-tv-multiview.js"></script>
vendor: 1 bytes, line 8047
8047
8048<script src="/assets/rosgan-airtime.js"></script>
vendor: 1 bytes, line 8048
8048
8049<script src="/assets/rosgan-order-command.js"></script>
vendor: 1 bytes, line 8049
8049
8050<script src="/assets/rosgan-sales-order-import.js"></script>
vendor: 1 bytes, line 8050
8050
8051<script src="/assets/rosgan-preorder-excel.js?v=2"></script>
vendor: 1 bytes, line 8051
8051
8052<script src="/assets/rosgan-audience.js"></script>
vendor: 1 bytes, line 8052
8052
8053<script src="/assets/rosgan-price-input.js"></script>
vendor: 1 bytes, line 8053
8053
8054<script src="/assets/rosgan-billing.js"></script>
8054 8055<link rel="stylesheet" href="/assets/rosgan-auditor.css?v=upload-timeline-1">
8056<script src="/assets/rosgan-payment-term.js?v=slash-days-1"></script>
vendor: 1 bytes, line 8056
8056
8057<script src="/assets/rosgan-lot-pdf.js"></script>
vendor: 1 bytes, line 8057
8057
8058<script src="/assets/rosgan-auditor-excel.js"></script>
vendor: 1 bytes, line 8058
8058
8059<script src="/assets/catalogo/news-pages.js?v=1"></script>
8059<script src="/assets/catalog-cover.js?v=news-pages-1"></script>
vendor: 1 bytes, line 8059
8059
8060<script src="/assets/rosgan-auditor.js?v=auditor-filters-2"></script>
vendor: 1 bytes, line 8060
8060
8061<script src="/assets/legacy-comitentes.js"></script>
vendor: 1 bytes, line 8061
8061
8062<script src="/assets/comitente-merge.js"></script>
vendor: 1 bytes, line 8062
8062
8063<script src="/assets/dev-activity.js"></script>
vendor: 1 bytes, line 8063
8063
8064<script src="/assets/dev-whatsapp.js"></script>
vendor: 1 bytes, line 8064
8064
8065<script src="/assets/dev-activity-page.js"></script>
vendor: 1 bytes, line 8065
8065
8066<script src="/assets/comitente-alias.js"></script>
vendor: 1 bytes, line 8066
8066
8067<script src="/assets/home-channel.js"></script>
vendor: 1 bytes, line 8067
8067
8068<script src="/assets/home-spot-player.js"></script>
8068 8069<style> 8070.rg-est-status .dc-lucide-circle-check{--dc-icon:url('/assets/icons/lucide/circle-check.svg')} 8071.rg-est-status .dc-lucide-triangle-alert{--dc-icon:url('/assets/icons/lucide/triangle-alert.svg')} 8072</style> 8073<style> 8074.rg-est-picker{position:relative;min-width:0}.rg-est-trigger{width:100%;min-height:46px;border:1px solid #ccdae8;border-radius:12px;background:#fff;color:#102f50;padding:9px 12px;display:flex;align-items:center;gap:9px;text-align:left;font:13px inherit;cursor:pointer}.rg-est-trigger:disabled{background:#f5f8fc;color:#7b8ba0;cursor:default}.rg-est-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rg-est-status{width:28px;height:28px;flex:0 0 28px;display:grid;place-items:center;border-radius:8px}.rg-est-status .dc-lucide{width:18px;height:18px}.rg-est-menu{position:absolute;z-index:45;left:0;right:0;top:calc(100% + 6px);background:#fff;border:1px solid #d4e0ec;border-radius:14px;box-shadow:0 16px 34px #00264e26;padding:8px}.rg-est-search{width:100%;box-sizing:border-box;min-height:42px;border:1px solid #dbe5ef;border-radius:9px;padding:9px 11px;font:14px inherit;color:#123956;background:#f7faff}.rg-est-options{max-height:min(300px,42dvh);overflow:auto;margin-top:6px;overscroll-behavior:contain}.rg-est-option{width:100%;display:flex;align-items:center;gap:10px;border:0;border-radius:9px;background:#fff;text-align:left;min-height:54px;padding:9px 8px;color:#173753;cursor:pointer;font:13px inherit}.rg-est-option:hover,.rg-est-option:focus-visible,.rg-est-option[aria-selected=true]{background:#edf5fd}.rg-est-option-text{display:grid;gap:3px;min-width:0}.rg-est-option strong{font-size:12px;overflow-wrap:anywhere}.rg-est-option small{font-size:10px;color:#60758c;line-height:1.4}.rg-est-count{margin:7px 4px 2px;color:#718296;font-size:10px}.rg-est-more{width:100%;min-height:38px;border:0;border-radius:8px;background:#edf5fd;color:#075da8;cursor:pointer;font:12px inherit} 8075</style>
8076<script src="/assets/cabana-lot-identity.js"></script>
8076 8077<link rel="stylesheet" href="/assets/cabana-prebid.css">
8078<script src="/assets/cabana-prebid.js?v=active-badge-3"></script>
vendor: 1 bytes, line 8078
8078
8079<script src="/assets/prebid-start-countdown.js?v=1"></script>
8079 8080<link rel="stylesheet" href="/assets/prebid-active-badge.css?v=3"> 8081<link rel="stylesheet" href="/assets/cabana-commercial-terms.css?v=2">
8082<script src="/assets/cabana-commercial-terms.js?v=2"></script>
vendor: 1 bytes, line 8082
8082
8083<script src="/assets/cabana-installments.js"></script>
vendor: 1 bytes, line 8083
8083
8084<script src="/assets/cabana-prebid-report.js"></script>
8084 8085<link rel="stylesheet" href="/assets/cabana-prebid-report.css">
8086<script src="/assets/cabana-media-url.js"></script>
vendor: 1 bytes, line 8086
8086
8087<script src="/assets/rosgan-sale-divisions.js"></script>
vendor: 1 bytes, line 8087
8087
8088<script src="/assets/rosgan-operations-patch.js"></script>
vendor: 1 bytes, line 8088
8088
8089<script src="/assets/rosgan-weighted-prices.js"></script>
vendor: 1 bytes, line 8089
8089
8090<script src="/assets/price-board.js"></script>
vendor: 1 bytes, line 8090
8090
8091<script src="/assets/auction-share.js?v=quantity-weight-range-2"></script>
vendor: 1 bytes, line 8091
8091
8092<script src="/assets/rosgannet-price-results.js"></script>
vendor: 1 bytes, line 8092
8092
8093<script src="/assets/auction-results.js"></script>
vendor: 1 bytes, line 8093
8093
8094<script src="/assets/statistics/dashboard.js"></script>
vendor: 1 bytes, line 8094
8094
8095<script src="/assets/statistics/historical.js"></script>
vendor: 1 bytes, line 8095
8095
8096<script src="/assets/rosgan-sale-divisions-editor.js"></script>
vendor: 1 bytes, line 8096
8096
8097<script src="/assets/rosgan-commercial-progress.js"></script>
vendor: 1 bytes, line 8097
8097
8098<script src="/assets/feria-flyer.js"></script>
vendor: 1 bytes, line 8098
8098
8099<script src="/assets/feria-thumbnail.js"></script>
vendor: 1 bytes, line 8099
8099
8100<script src="/assets/feria-thumbnail-preview.js"></script>
vendor: 1 bytes, line 8100
8100
8101<script src="/assets/feria-devices.js"></script>
vendor: 1 bytes, line 8101
8101
8102<script type="text/x-dc" data-dc-script data-props="{"$preview": {"width": 1280, "height": 900}, "iniciarEnHome": {"editor": "boolean", "default": false, "tsType": "boolean", "section": "Comportamiento"}, "animaciones": {"editor": "boolean", "default": true, "tsType": "boolean", "section": "Comportamiento"}}"> 8103const ARGENTINA_PROVINCES = [ 8104 'Buenos Aires', 'Catamarca', 'Chaco', 'Chubut', 'Ciudad Autónoma de Buenos Aires', 8105 'Córdoba', 'Corrientes', 'Entre RÃos', 'Formosa', 'Jujuy', 'La Pampa', 'La Rioja', 8106 'Mendoza', 'Misiones', 'Neuquén', 'RÃo Negro', 'Salta', 'San Juan', 'San Luis', 8107 'Santa Cruz', 'Santa Fe', 'Santiago del Estero', 8108 'Tierra del Fuego, Antártida e Islas del Atlántico Sur', 'Tucumán' 8109]; 8110 8111class Component extends DCLogic { 8112 state = { 8113 publicAdvanceMode: window.__ROSGAN_PUBLIC_ADVANCE__ || '', 8114 route: 'login', authMode: 'signin', formName: '', formEmail: '', formPassword: '', formPhone: '+54', authPasswordVisible: false, 8115 accountAssistantOpen: false, accountAssistantMethod: '', accountAssistantStep: 0, accountAssistantMessage: '', 8116 authMessage: '', authReady: false, authWelcomeRequested: false, user: null, isMobile: window.innerWidth < 760, compactNavigation: window.innerWidth <= 1024, 8117 launchInterest: null, launchInterestLoading: false, launchInterestSaving: false, launchInterestMessage: '', 8118 launchInterestsAdmin: null, launchInterestsAdminTotal: 0, launchInterestsAdminLoading: false, launchInterestsAdminError: '', 8119 lotYieldCategories: null, lotYieldLoading: false, lotYieldError: '', lotYieldSavingId: '', lotYieldMessage: null, 8120 emailVerificationRequired: false, emailVerificationBusy: false, emailVerificationMessage: '', 8121 phoneVerificationRequired: false, phoneVerificationStep: 'phone', phoneVerificationNumber: '+54', phoneVerificationCode: '', phoneVerificationBusy: false, phoneVerificationMessage: '', 8122 ferias: null, consignatarias: null, videos: null, cafe: null, 8123 parallax: 0, statProgress: 0, toast: '', 8124 notificationsStatus: 'idle', notificationsMessage: '', notificationsOpen: false, 8125 whatsappNotificationsEnabled: false, whatsappNotificationsAvailable: false, whatsappNotificationsLoading: false, whatsappNotificationsSaving: false, whatsappNotificationsMessage: '', 8126 notificationsItems: null, notificationsLoading: false, notificationsError: '', 8127 notificationHistory: null, notificationHistoryLoading: false, notificationHistoryError: '', 8128 roslotMobilePanelOpen: false, roslotLiveToast: null, roslotLivePulseId: '', 8129 monthRemates: null, cafeVideos: null, heroSlide: 0, homeHeroSlide: 0, homeChannelManual: null, homeChannelNow: 0, homeSpots: [], homeSpotIndex: 0, homeSpotFailures: [], 8130 profileFerias: [], profileFeriasLoading: false, profileFeriasError: '', profileFeriasUid: '', 8131 profileRosganLots: [], profileRosganLoading: false, profileRosganError: '', profileRosganScopeUid: '', 8132 lots: null, lotsLoading: false, lotsReal: false, myLots: null, myLotsLoading: false, myLotsError: '', 8133 comitentes: null, comitentesLoading: false, comitentesError: '', comitenteForm: null, comitenteFormOpen: false, comitenteSaving: false, comitenteLookupLoading: false, comitenteFormMessage: null, 8134 establishments: null, establishmentsLoading: false, establishmentsError: '', estForm: null, estFormOpen: false, estSaving: false, estLookupLoading: false, estFormMessage: null, estDeleting: '', 8135 estPlaceLoading: false, estPlaceResults: [], estPlaceError: '', 8136 poolQuery: '', poolStatus: 'today', poolSort: 'recientes', poolView: 'grid', filtersOpen: false, marketClock: Date.now(), 8137 poolFilters: { category: [], destination: [], fattening: [], province: [], china: false }, 8138 roslotDailyReport: null, roslotDailyReportLoading: false, roslotDailyReportError: '', roslotDailyReportDate: '', roslotDailyReportFilter: 'all', roslotDailyReportSearch: '',
8139 roslotPresence: null, roslotPresenceLoading: false, roslotPresenceError: false, 8140 selectedCert: '', selectedVideo: 0, lotVideoPlaying: false, lotVideoProgress: 0, lotVideoDuration: 0, lotVideoVolume: 0, lotVideoMuted: true, videoThumbByUrl: {}, videoThumbFailedByUrl: {}, bidByCert: {}, bidsPlaced: {}, bidMessage: null, bidSubmitting: false, quickBidCert: '', lotClosingCert: '', closedRoslotLots: {}, roslotBidSuccessFx: null, 8141 lotBids: {}, lotBidsLoading: {}, myBids: null, myBidsLoading: false, myBidsError: '', pendingBids: null, pendingBidsLoading: false, pendingBidsError: '', bidActioning: '', 8142 viewsBump: {}, lotViewCounts: {}, lotViewLoading: {}, 8143 loadStep: 0, form: null, formMessage: null, publishing: false, vidProg: {}, 8144 rosganLotStep: 0, rosganLotForm: null, rosganLotRemates: null, rosganLotCategories: null, rosganLotBreeds: null, 8145 rosganLotOptions: null, rosganLotOptionsLoading: false, rosganLotOptionsError: '', 8146 rosganBreedSearch: '', rosganActiveBreedFamily: '', rosganBreedCrosses: {}, 8147 rosganLotConsignatarias: null, rosganLotComitentes: null, rosganLotPendingComitentes: [], rosganLotEstablishments: null, 8148 rosganLotLoading: false, rosganLotRelationsLoading: false, rosganLotPublishing: false, rosganLotMessage: null, rosganCreatedLot: null, rosganEditingLot: null, rosganConsignatariaMenuOpen: false, 8149 rosganComitenteFormOpen: false, rosganComitenteSaving: false, rosganComitenteLookupLoading: false, rosganComitenteMessage: null, 8150 rosganComitenteSearch: '', rosganComitenteMenuOpen: false, 8151 rosganComitenteForm: { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' }, 8152 rosganEstablishmentFormOpen: false, rosganEstablishmentSaving: false, rosganEstablishmentMessage: null, 8153 rosganEstablishmentForm: { nombre: '', provincia: '', localidad: '', renspa: '', latitud: '', longitud: '' }, 8154 rosganEstPlaceLoading: false, rosganEstPlaceResults: [], rosganEstPlaceError: '', 8155 editLotCert: '', editOriginalForm: null, editDeletedVideoIds: [], editLotLoading: false, 8156 profileTab: 'resumen', dashProgress: 0, 8157 buyerAuths: null, buyerAuthsLoading: false, buyerAuthsError: '', buyerAccess: null, buyerAccessLoading: false, buyerAccessError: '', sellerLabels: {}, buyerAccounts: null, buyerAccountsLoading: false, buyerAccountsError: '', buyerDirectorySearch: '', authSyncError: '', baForm: null, baFormOpen: false, baSaving: false, baFormMessage: null, baActioning: '', sellerClosedPage: 1, buyerClosedPage: 1, 8158 creditSummaryOpen: false, creditSummaryLoading: false, creditSummaryError: '', creditSummary: null, creditSummaryAuthorization: null, creditSummaryPerspective: 'buyer', 8159 creditSummaryCache: {}, creditSummaryLoadingIds: {}, creditSummaryErrors: {}, 8160 creditHistoryOpen: false, creditHistoryPerspective: 'buyer', creditHistoryAuthorization: null, 8161 republishLot: null, republishForm: null, republishSaving: false, republishMessage: null, 8162 profileFetchInfo: {}, apiUser: null, apiSessionError: '', userConsignataria: null, userConsignatarias: [], lotSellerConsignatarias: {}, 8163 profileEditorOpen: false, profileEditorSaving: false, profileEditorError: '', 8164 profileEditorForm: { firstName: '', lastName: '', phoneNumber: '' }, profilePhotoPreview: '', profilePhotoFailed: false, profilePhotoViewerOpen: false, 8165 profileCropOpen: false, profileCropSource: '', profileCropZoom: 1, profileCropX: 50, profileCropY: 50, 8166 adminUsersOpen: false, adminUsers: null, adminUsersLoading: false, adminUsersError: '', adminUsersSearch: '', adminUsersGroup: 'recent', adminUsersConsignataria: '', adminUsersPage: 1, adminCompanyMenuOpen: false, 8167 adminSelectedUser: null, adminSelectedRoles: [], adminSelectedConsignatariaIds: [], adminConsignatarias: [], adminUserRelationLoading: false, 8168 adminOriginalRoles: [], adminOriginalConsignatariaIds: [], adminOriginalRelationIsActive: false, 8169 adminUserSaving: false, adminUserMessage: null, 8170 mailingOpen: false, mailingLoading: false, mailingBusy: false, mailingError: '', mailingMessage: null, 8171 mailingStatus: null, mailingContacts: [], mailingContactTotal: 0, mailingLaunchInterestTotal: 0, mailingCampaigns: [], mailingSelectedId: '', mailingConsentConfirmed: false, 8172 mailingContactsState: 'idle', mailingCampaignsState: 'idle', mailingStatusState: 'idle', mailingLaunchState: 'idle', 8173 mailingReport: null, mailingReportLoading: false, mailingReportError: '', mailingReportExporting: false, 8174 mailingReportSearch: '', mailingReportStatusFilter: 'all', mailingReportLetter: 'all', mailingReportPage: 1, 8175 mailingEditorExpanded: false, mailingEditorSourceMode: false, mailingInsertDialog: '', mailingInsertUrl: '', mailingInsertText: '', mailingInsertImageSize: 'medium', mailingInsertImageLink: '', mailingImageUploading: false, mailingImageError: '', 8176 mailingDraft: { name: '', subject: '', previewText: '', html: '', text: '', audience: 'all', campaignType: 'standard' }, mailingTestEmail: '', 8177 remates: null, remateYears: null, rematesLoading: false, remateArchiveOpen: false, remateArchiveLimit: 12, remateYear: new Date().getFullYear(), remateType: '', carSlide: 0, 8178 rosganRemates: null, rosganRematesLoading: false, rosganRematesError: '', 8179 rematePanelPickerOpen: false, rematePanelPickerLoading: false, rematePanelPickerError: '', rematePanelScopeUid: '', rematePanelItems: [], rematePanelSelectedId: '', 8180 rosganRemateTypes: null, rosganRemateTypesLoading: false, rosganRemateTypesError: '', 8181 rosganRemateFormOpen: false, rosganRemateEditingId: '', rosganRemateForm: null, 8182 rosganRemateSaving: false, rosganRemateDeleting: '', rosganRemateMessage: null, 8183 rosganRemateFlyer: null, rosganRemateFlyerPreview: '', 8184 currentRemate: null, remateLots: null, remateLotsLoading: false, remateLotsReal: false, rdQuery: '', rdSort: 'orden', rdView: 'grid', rdLightbox: null, 8185 rosganOpsDrafts: {}, rosganOpsConfig: null, rosganSalesOrderRatings: {}, rosganSalesOrderSchedule: null, rosganOpsPulse: 0, rosganOpsLoading: false, rosganTvQuery: '', rosganOfficialPrices: {}, 8186 remateLotAlerts: {}, remateLotAlertsLoading: false, remateLotAlertBusy: '', 8187 rosganSalesOrderLots: [], rosganSalesOrderSaving: false, rosganSalesOrderImporting: false, rosganSalesOrderError: '', rosganSalesOrderSaved: false, rosganSalesOrderVideoCert: '', 8188 rosganSalesOrderQuery: '', rosganSalesOrderStatusFilter: '', rosganSalesOrderCategoryFilter: '', rosganSalesOrderConsignatariaFilter: '', rosganSalesOrderRatingFilter: '', rosganSalesOrderRatingSnapshot: null, 8189 rosganLotVideoState: {}, rosganLotVideoDeleting: '', 8190 cabanas: null, cabanasLoading: false, cabanasError: '', cabanasCollectionReady: false, cabanaCardMedia: {}, cabanaBreed: '', 8191 currentCabana: null, cabanaLots: null, cabanaLotsLoading: false, cbQuery: '', cbCat: 'todos', cabanaAnalyticsSummary: null, cabanaAnalyticsLoading: false, cabanaStatsOverview: false, cabanaStatsOverviewRows: [], cabanaStatsOverviewLoading: false, cabanaStatsOverviewError: '', cabanaPublicDeepLoading: false, cabanaBrandPreparing: false, cabanaCardVideoUploads: {},
8192 cabanaStatsExportBusy: '', cabanaStatsVisitorPage: 1, 8193 cabanaSalesOrderOpen: false, cabanaSalesOrderGrouped: false, cabanaSalesOrderLots: [], cabanaSalesOrderSaving: false, cabanaSalesOrderError: '', cabanaSalesOrderSaved: false, 8194 cabanaCommercialDrafts: {}, cabanaCommercialFilter: 'todos', cabanaCommercialView: 'carga', cabanaCommercialOpenRow: '', cabanaCommercialLoading: false, 8195 cabanaTvLotKey: '', cabanaTvPulse: 0, cabanaTvConfig: null, cabanaTvConfigPulse: 0, cabanaTvClock: 0, cabanaTvCycleStartedAt: 0, cabanaTvVideoIndex: 0, 8196 cabanaTvWeather: null, cabanaTvWeatherLocation: '', cabanaTvWeatherLoading: false, cabanaTvWeatherError: '', 8197 currentCabanaLot: null, currentCabanaLotVariants: [], cbLotVideo: 0, cbCarouselStep: 0, cbCarouselOffsets: {}, cbCarouselFading: false, cbCarouselFadingKey: '', cbCarouselCrossfadeActive: false, cbCarouselPreviousMedia: {}, cabanaLotVideosLoading: false, cabanaLotMediaReady: false, cabanaLotMediaFailed: false, cabanaLotVideoPlaying: false, cabanaLotVideoProgress: 0, cabanaLotVideoDuration: 0, cabanaLotVideoVolume: 0, cabanaLotVideoMuted: true, cabanaLotQuickVideoUploading: false, cabanaLotQuickVideoProgress: 0, cabanaLotQuickVideoStage: '', cabanaLotVideoDeleting: '', 8198 cabanaPrebids: [], cabanaPrebidMine: [], cabanaPrebidsLoading: false, cabanaPrebidsError: '', cabanaPrebidModalOpen: false, cabanaPrebidSubmitting: false, cabanaPrebidMessage: null, cabanaPrebidRolling: false, cabanaPrebidSuccessFx: null, 8199 cabanaPrebidPanelAllowed: false, cabanaPrebidPanelOpen: false, cabanaPrebidPanelLoading: false, cabanaPrebidPanelError: '', cabanaPrebidPanelData: null, cabanaPrebidPanelQuery: '', cabanaPrebidPanelStatus: 'all', cabanaPrebidPanelLot: '', 8200 cabanaPrebidForm: { cuitCuil: '', amount: '', selectedIdentifiers: [], enComision: false, enComisionPara: '' }, 8201 cabanaLotStoryBusy: false, 8202 cabanaPriceStoryBusy: false, 8203 cabanaSocialBumps: {}, 8204 cabanaLotCreateRemate: null, cabanaLotCreateForm: null, cabanaLotImages: [], cabanaLotVideosUpload: [], cabanaLotCreateSaving: false, cabanaLotCreateLoading: false, cabanaLotEditingCert: '', cabanaLotEditingSource: null, cabanaLotDeleting: '', 8205 remateConfigForm: null, remateConfigToggles: null, remateConfigSaved: false, remateConfigEditingId: '', remateConfigSaving: '', 8206 feriaTab: 'todos', feriaFeatured: null, feriaFormOpen: false, feriaSubmitting: false, feriaFormMessage: null, feriaCreated: [], 8207 feriaForm: { consignataria: '', provincia: '', localidad: '', nombre: '', fecha: '', hora: '', cabezas: '', youtube: '' }, 8208 feriaTitleAuto: true, feriaFavoriteId: '', feriaManualOpen:false, 8209 feriaPlaceLoading: false, feriaPlaceResults: [], feriaPlaceError: '', 8210 geoProvinces: [...ARGENTINA_PROVINCES], geoLocs: {}, geoLoadingProv: false, geoLoadingLoc: false, 8211 feriaPhase: 'idle', feriaResult: null, feriaError: '', 8212 selectedCons: null, consRemates: null, consRematesLoading: false, channelTab: 'todos', channelYear: null, chHeroSlide: 0, notiSlide: 0, mercIndex: 'piri', mercTipo: 'Invernada', mercHover: null, mercRange: 12, 8213 mavDate: new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 10), mavSummary: null, mavRanges: null, mavLoading: false, mavError: '', mavMetric: 'tasa_prom_pond', mavHover: null, 8214 }; 8215 8216 emptyRosganLotForm() { 8217 return { 8218 remateId: '', consignatariaId: '', certifiedBy: null, comitenteId: '', establishmentId: '', paymentTermDays: [], paymentTermReference: '', otherPaymentTermDay: '', otherPaymentTermReference: '', formType: '', categoryId: '', breedIds: [], 8219 quantity: '', maleQuantity: '', femaleQuantity: '', motherQuantity: '', calfQuantity: '', 8220 isTermino: false, isAptoChina: false, description: '', descriptionOptionValues: [], ageValue: '', ageUnit: 'meses', dentition: '', 8221 reproductiveStatus: '', destination: '', bodyCondition: '', animalClass: '', hasMioMio: null, 8222 tickStatus: '', health: '', nutrition: '', daysOfConfinement: '', averageWeight: '', maxWeight: '', minWeight: '', weighing: '', 8223 desbaste: '', hasLiquidBrand: false, isAptoVisec: false, service: '', pregnancy: '', calving: '', 8224 subCategory: '', conditionType: '', deliveryType: '', finishing: '', estimatedYield: '', 8225 }; 8226 } 8227 isTextEntryControl(element) { 8228 if (!element || element.nodeType !== 1) return false;
8229 const tag = String(element.tagName || '').toLowerCase(); 8230 if (tag === 'textarea' || element.isContentEditable) return true; 8231 if (tag !== 'input') return false; 8232 const type = String(element.type || 'text').toLowerCase(); 8233 return !['button', 'checkbox', 'color', 'file', 'hidden', 'image', 'radio', 'range', 'reset', 'submit'].includes(type); 8234 } 8235 captureTextEntryFocus() { 8236 if (this._formCompositionActive) return null; 8237 const element = document.activeElement; 8238 if (!this.isTextEntryControl(element)) return null; 8239 const templateId = element.getAttribute('data-dc-tpl') || ''; 8240 const matching = templateId ?Array.from(document.querySelectorAll('[data-dc-tpl="' + templateId + '"]')).filter((item) => this.isTextEntryControl(item)) : []; 8241 let selectionStart = null; let selectionEnd = null; let selectionDirection = 'none'; 8242 try { 8243 selectionStart = element.selectionStart; 8244 selectionEnd = element.selectionEnd; 8245 selectionDirection = element.selectionDirection || 'none'; 8246 } catch {} 8247 return { 8248 element, 8249 route: this.state.route, 8250 templateId, 8251 templateIndex: matching.indexOf(element), 8252 id: element.id || '', 8253 name: element.getAttribute('name') || '', 8254 type: String(element.type || '').toLowerCase(), 8255 placeholder: element.getAttribute('placeholder') || '', 8256 selectionStart, 8257 selectionEnd, 8258 selectionDirection, 8259 scrollLeft: element.scrollLeft || 0, 8260 scrollTop: element.scrollTop || 0, 8261 }; 8262 } 8263 restoreTextEntryFocus(snapshot) { 8264 if (!snapshot || snapshot.route !== this.state.route || this._formCompositionActive) return; 8265 const active = document.activeElement; 8266 if (active && active !== document.body && active !== snapshot.element && this.isTextEntryControl(active)) return; 8267 let target = snapshot.element?.isConnected ?snapshot.element : null; 8268 if (!target && snapshot.id) target = document.getElementById(snapshot.id); 8269 if (!target && snapshot.templateId) { 8270 const matching = Array.from(document.querySelectorAll('[data-dc-tpl="' + snapshot.templateId + '"]')).filter((item) => this.isTextEntryControl(item)); 8271 target = matching[Math.max(0, snapshot.templateIndex)] || null; 8272 } 8273 if (!target && snapshot.name) target = Array.from(document.querySelectorAll('input,textarea')).find((item) => item.getAttribute('name') === snapshot.name && String(item.type || '').toLowerCase() === snapshot.type) || null; 8274 if (!target && snapshot.placeholder) target = Array.from(document.querySelectorAll('input,textarea')).find((item) => item.getAttribute('placeholder') === snapshot.placeholder && String(item.type || '').toLowerCase() === snapshot.type) || null; 8275 if (!this.isTextEntryControl(target) || target.disabled) return; 8276 if (document.activeElement !== target) { 8277 try { target.focus({ preventScroll: true }); } catch { try { target.focus(); } catch {} } 8278 } 8279 if (snapshot.selectionStart != null && snapshot.selectionEnd != null && typeof target.setSelectionRange === 'function') { 8280 try { target.setSelectionRange(snapshot.selectionStart, snapshot.selectionEnd, snapshot.selectionDirection); } catch {} 8281 } 8282 target.scrollLeft = snapshot.scrollLeft; 8283 target.scrollTop = snapshot.scrollTop; 8284 } 8285 scheduleTextEntryRender(delay = 120) { 8286 if (this._textEntryRenderTimer) clearTimeout(this._textEntryRenderTimer); 8287 this._textEntryRenderTimer = setTimeout(() => { 8288 if (this._formCompositionActive) { this.scheduleTextEntryRender(delay); return; } 8289 this._textEntryRenderTimer = null; 8290 this.forceUpdate(); 8291 this.scheduleDomBindings(); 8292 }, delay); 8293 } 8294 setState(update, callback) { 8295 window.rosganAudienceApp=this; 8296 if (this._handlingTextInput && this.isTextEntryControl(document.activeElement)) { 8297 const patch = typeof update === 'function' ?update(this.state) : update; 8298 this.state = { ...this.state, ...(patch || {}) }; 8299 if (typeof callback === 'function') callback(); 8300 this.scheduleTextEntryRender(); 8301 return; 8302 } 8303 const snapshot = this.captureTextEntryFocus(); 8304 super.setState(update, () => { 8305 if (typeof callback === 'function') callback(); 8306 this.restoreTextEntryFocus(snapshot); 8307 window.RosganAudience?.sync(this); 8308 window.RosganNameFit?.sync(); 8309 document.documentElement.toggleAttribute('data-rosgan-video-hidden',false); 8310 if(this.state.route==='rosganTvLower'&&this.state.rosganOpsConfig?.videoVisible===false)document.documentElement.setAttribute('data-rosgan-video-hidden','true'); 8311 }); 8312 } 8313 forceUpdate(callback) { 8314 const snapshot = this.captureTextEntryFocus(); 8315 if (!this.__host) return; 8316 this.__host.forceUpdate(() => { 8317 if (typeof callback === 'function') callback(); 8318 this.restoreTextEntryFocus(snapshot); 8319 }); 8320 } 8321 rosganForm() { return this.state.rosganLotForm || this.emptyRosganLotForm(
8321); } 8322 bufferInputState(key, patch, delay = 90) { 8323 this.state = { ...this.state, ...(typeof patch === 'function' ?patch(this.state) : patch) }; 8324 this._inputRenderTimers = this._inputRenderTimers || {}; 8325 if (this._inputRenderTimers[key]) clearTimeout(this._inputRenderTimers[key]); 8326 this._inputRenderTimers[key] = setTimeout(() => { 8327 delete this._inputRenderTimers[key]; 8328 this.forceUpdate(); 8329 this.scheduleDomBindings(); 8330 }, delay); 8331 } 8332 setRosganLotField(key, value, buffered = false) { 8333 const patch = { rosganLotForm: { ...this.rosganForm(), [key]: value }, rosganLotMessage: null }; 8334 if (buffered) this.bufferInputState('rosgan-lot-' + key, patch); 8335 else this.setState(patch); 8336 } 8337 setRosganDescription(event) { 8338 const value = String(event.target?.value ?? event.currentTarget?.value ?? ''); 8339 // Save immediately, without re-rendering the textarea while typing. 8340 this.state = { ...this.state, rosganLotForm: { ...this.rosganForm(), description: value }, rosganLotMessage: null }; 8341 if (event.type === 'blur') this.forceUpdate(); 8342 } 8343 selectRosganLotType(value) { 8344 const formType = String(value || '').toLowerCase(); 8345 if (!['invernada', 'faena', 'vientres'].includes(formType)) return; 8346 const current = this.rosganForm(); 8347 const selectedCategory = this.rosganSelectedCategory(); 8348 const keepsCategory = String(selectedCategory?.formType || '').toLowerCase() === formType; 8349 this.setState({ 8350 rosganLotForm: { 8351 ...current, 8352 formType, 8353 categoryId: keepsCategory ?current.categoryId : '', 8354 quantity: keepsCategory ?current.quantity : '', 8355 maleQuantity: keepsCategory ?current.maleQuantity : '', 8356 femaleQuantity: keepsCategory ?current.femaleQuantity : '', 8357 motherQuantity: keepsCategory ?current.motherQuantity : '', 8358 calfQuantity: keepsCategory ?current.calfQuantity : '', 8359 }, 8360 rosganLotMessage: null, 8361 }); 8362 } 8363 selectRosganLotCategory(value) { 8364 const categoryId = String(value || ''); 8365 const category = (this.state.rosganLotCategories || []).find((item) => String(item.id) === categoryId); 8366 this.setState({ 8367 rosganLotForm: { ...this.rosganForm(), categoryId, formType: String(category?.formType || this.rosganForm().formType || '').toLowerCase() }, 8368 rosganLotMessage: null, 8369 }); 8370 } 8371 toggleRosganLotDescription(value) { 8372 const option = this.rosganDescriptionChoices().find(o => [o.value,o.label].some(v => this.consKey(v) === this.consKey(value))); 8373 if (!option) return; 8374 const selected = this.rosganForm().descriptionOptionValues; 8375 const values = Array.isArray(selected) ? selected : []; 8376 const matches = item => [option.value,option.label].some(v => this.consKey(v) === this.consKey(item)); 8377 this.setRosganLotField('descriptionOptionValues', values.some(matches) ? values.filter(item => !matches(item)) : [...values,option.value]); 8378 } 8379 toggleRosganBreed(id) { 8380 const selected = this.rosganForm().breedIds || []; 8381 const exists = selected.includes(id); 8382 const families = this.rosganBreedFamilies().filter((group) => group.key !== '__crosses__'); 8383 const selectedFamilies = families.filter((group) => group.items.some((item) => selected.includes(String(item.id)))); 8384 const targetFamily = families.find((group) => group.items.some((item) => String(item.id) === String(id))); 8385 const targetAlreadySelected = targetFamily && targetFamily.items.some((item) => selected.includes(String(item.id))); 8386 if (!exists && !targetAlreadySelected && selectedFamilies.length >= 3) { 8387 this.setState({ rosganLotMessage: { ok: false, text: 'Podés seleccionar hasta 3 razas.' } }); 8388 return; 8389 } 8390 this.setRosganLotField('breedIds', exists ?selected.filter((item) => item !== id) : [...selected, id]); 8391 } 8392 rosganBreedIconMap() { 8393 return { 8394 angus: 'assets/breeds/angus.png', hereford: 'assets/breeds/hereford.png', brahman: 'assets/breeds/brahman.png', 8395 braford: 'assets/breeds/braford.png', brangus: 'assets/breeds/brangus.png', charolais: 'assets/breeds/charolais.png', 8396 holandoargentino: 'assets/breeds/holando-argentino.png', jersey: 'assets/breeds/jersey.png', santagertrudis: 'assets/breeds/santa-gertrudis.png', 8397 bonsmara: 'assets/breeds/bonsmara.png', bovinocriollo: 'assets/breeds/bovino-criollo.png', criollos: 'assets/breeds/bovino-criollo.png', 8398 limousin: 'assets/breeds/limousin.png', pardosuizo: 'assets/breeds/pardo-suizo.png', piemontese: 'assets/breeds/piemontese.png', 8399 retinta: 'assets/breeds/retinta.png', murraygrey: 'assets/breeds/murray-grey.png', shorthorn: 'assets/breeds/shorthorn.png', tuli: 'assets/breeds/tuli.png', 8400 polledhereford: 'assets/breeds/hereford.png', brafordcuartinas: 'assets/breeds/braford.png', 8401 murrah: 'assets/breeds/murrah.png', bubilla: 'assets/breeds/bubilla.png', hampshiredown: 'assets/breeds/hampshire-down.png', 8402 kiwicross: 'assets/breeds/kiwicross.png', anguscruzajersey: 'assets/breeds/angus-cruza-jersey.png', limangus: 'assets/breeds/limangus.png', 8403 senangus: 'assets/breeds/senangus.png', seneford: 'assets/breeds/seneford.png', sanignacio: 'assets/breeds/san-ignacio.png', 8404 caretas: 'assets/breeds/caretas.png', cruzas: 'assets/breeds/cruzas.png', cruzasbritanicas: 'assets/breeds/cruzas-britanicas.png', 8405 cruzasindicas: 'assets/breeds/cruzas-indicas.png', cuartinas: 'assets/breeds/cuartinas.png', cuartinos: 'assets/breeds/cuartinos.png', 8406 mestizas: 'assets/breeds/mestizas.png', mestizos: 'assets/breeds/mestizas.png', mestizosas: 'assets/breeds/mestizas.png', 8407 pampas: 'assets/breeds/pampas.png', pampizada: 'assets/breeds/pampizada.png', negrosy: 'assets/breeds/cruzas.png', 8408 }; 8409 } 8410 rosganBreedIcon(name) { 8411 return this.rosganBreedIcons(name)[0]?.icon || ''; 8412 } 8413 rosganBreedIcons(name) { 8414 const source = String(name || '').trim(); 8415 if (!source) return []; 8416 const icons = this.rosganBreedIconMap(); 8417 const aliases = [...Object.keys(icons).filter((item) => item !== 'negrosy'), 'careta'] 8418 .sort((a, b) => b.length - a.length); 8419 const found = []; 8420 source.split(/[.,;|/+]+/).map((part) =>
8420 part.trim()).filter(Boolean).forEach((part) => { 8421 let remaining = this.consKey(part).split('ysuscruzas').join(''); 8422 if (!remaining) return; 8423 const showBlack = /negr/i.test(part); 8424 const showRed = /colorad/i.test(part); 8425 aliases.forEach((alias) => { 8426 if (!remaining.includes(alias)) return; 8427 const mapKey = alias === 'careta' ?'caretas' : alias; 8428 const icon = icons[mapKey]; 8429 if (icon) { 8430 const existing = found.find((item) => item.icon === icon); 8431 if (existing) { existing.showBlack = existing.showBlack || showBlack; existing.showRed = existing.showRed || showRed; } 8432 else found.push({ icon, showBlack, showRed }); 8433 } 8434 remaining = remaining.split(alias).join(''); 8435 }); 8436 }); 8437 return found; 8438 } 8439 rosganBreedFamilies() { 8440 const groups = new Map(); 8441 const family = (key, label) => { 8442 if (!groups.has(key)) groups.set(key, { key, label, base: null, black: null, red: null, both: null, items: [] }); 8443 return groups.get(key); 8444 }; 8445 (this.state.rosganLotBreeds || []).forEach((item) => { 8446 const name = String(item?.name || item?.nombre || '').trim(); 8447 if (!name) return; 8448 const id = String(item.id); 8449 if (/^y\s+sus\s+cruzas$/i.test(name)) { 8450 const group = family('__crosses__', 'Y sus cruzas'); 8451 group.base = { ...item, id, name: 'Y sus cruzas' }; 8452 group.items.push(group.base); 8453 return; 8454 } 8455 const bothMatch = name.match(/^(.*?)\s+(negros?|negras?)\s+y\s+(colorados?|coloradas?)$/i); 8456 const singleMatch = name.match(/^(.*?)\s+(negros?|negras?|colorados?|coloradas?)$/i); 8457 if (bothMatch?.[1]?.trim()) { 8458 const label = bothMatch[1].trim(); 8459 const group = family(this.consKey(label), label); 8460 group.both = { ...item, id, name }; 8461 group.items.push(group.both); 8462 return; 8463 } 8464 if (singleMatch?.[1]?.trim()) { 8465 const label = singleMatch[1].trim(); 8466 const group = family(this.consKey(label), label); 8467 const variant = { ...item, id, name }; 8468 if (/negr/i.test(singleMatch[2])) group.black = variant; 8469 else group.red = variant; 8470 group.items.push(variant); 8471 return; 8472 } 8473 const group = family(this.consKey(name), name); 8474 const base = { ...item, id, name }; 8475 group.base = base; 8476 group.items.push(base); 8477 }); 8478 return Array.from(groups.values()).map((group) => { 8479 const hasIndividualColors = !!(group.black || group.red); 8480 if (!hasIndividualColors && group.both && !group.base) { 8481 return { ...group, key: group.key + '__both', label: group.both.name, base: group.both, both: null, items: [group.both], supportsColors: false }; 8482 } 8483 return { ...group, supportsColors: hasIndividualColors }; 8484 }).sort((a, b) => { 8485 if (a.key === '__crosses__') return -1; 8486 if (b.key === '__crosses__') return 1; 8487 return a.label.localeCompare(b.label, 'es'); 8488 }); 8489 } 8490 selectRosganBreedFamily(key) { 8491 const group = this.rosganBreedFamilies().find((item) => item.key === key); 8492 if (!group) return; 8493 const selected = this.rosganForm().breedIds || []; 8494 const groupIds = group.items.map((item) => String(item.id)); 8495 const isSelected = groupIds.some((id) => selected.includes(id)); 8496 if (!group.supportsColors) { 8497 const id = String(group.base?.id || group.items[0]?.id || ''); 8498 if (id) this.toggleRosganBreed(id); 8499 return; 8500 } 8501 if (!isSelected) { 8502 const id = String(group.base?.id || group.both?.id || group.black?.id || group.red?.id || ''); 8503 if (!id) return; 8504 const selectedFamilies = this.rosganBreedFamilies().filter((candidate) => candidate.key !== '__crosses__' && candidate.items.some((item) => selected.includes(String(item.id)))); 8505 if (selectedFamilies.length >= 3) { this.setState({ rosganLotMessage: { ok: false, text: 'Podés seleccionar hasta 3 razas.' } }); return; } 8506 this.setState({ rosganLotForm: { ...this.rosganForm(), breedIds: [...selected, id] }, rosganActiveBreedFamily: key, rosganLotMessage: null }); 8507 return; 8508 } 8509 this.setState({ rosganActiveBreedFamily: key, rosganLotMessage: null }); 8510 } 8511 removeRosganBreedFamily(key) { 8512 const group = this.rosganBreedFamilies().find((item) => item.key === key); 8513 if (!group) return; 8514 const ids = new Set(group.items.map((item) => String(item.id))); 8515 const crossesGroup = this.rosganBreedFamilies().find((item) => item.key === '__crosses__');
8516 const crossesId = String(crossesGroup?.base?.id || crossesGroup?.items?.[0]?.id || ''); 8517 const nextCrosses = { ...(this.state.rosganBreedCrosses || {}) }; 8518 delete nextCrosses[key]; 8519 let nextBreedIds = (this.rosganForm().breedIds || []).filter((id) => !ids.has(String(id)) && (!crossesId || String(id) !== crossesId)); 8520 if (crossesId && Object.values(nextCrosses).some(Boolean)) nextBreedIds.push(crossesId); 8521 this.setState({ 8522 rosganLotForm: { ...this.rosganForm(), breedIds: nextBreedIds }, 8523 rosganBreedCrosses: nextCrosses, 8524 rosganActiveBreedFamily: this.state.rosganActiveBreedFamily === key ?'' : this.state.rosganActiveBreedFamily, 8525 rosganLotMessage: null, 8526 }); 8527 } 8528 toggleRosganBreedColor(key, color) { 8529 const group = this.rosganBreedFamilies().find((item) => item.key === key); 8530 if (!group?.supportsColors) return; 8531 const selected = this.rosganForm().breedIds || []; 8532 const groupIds = new Set(group.items.map((item) => String(item.id))); 8533 const outside = selected.filter((id) => !groupIds.has(String(id))); 8534 let black = !!group.both && selected.includes(String(group.both.id)) || !!group.black && selected.includes(String(group.black.id)); 8535 let red = !!group.both && selected.includes(String(group.both.id)) || !!group.red && selected.includes(String(group.red.id)); 8536 if (color === 'black') black = !black; 8537 if (color === 'red') red = !red; 8538 const next = []; 8539 if (black && red && group.both) next.push(String(group.both.id)); 8540 else { 8541 if (black && group.black) next.push(String(group.black.id)); 8542 if (red && group.red) next.push(String(group.red.id)); 8543 } 8544 if (!next.length && group.base) next.push(String(group.base.id)); 8545 this.setState({ rosganLotForm: { ...this.rosganForm(), breedIds: [...outside, ...next] }, rosganActiveBreedFamily: key, rosganLotMessage: null }); 8546 } 8547 toggleRosganBreedCrosses(key) { 8548 const group = this.rosganBreedFamilies().find((item) => item.key === key); 8549 const crossesGroup = this.rosganBreedFamilies().find((item) => item.key === '__crosses__'); 8550 const crossesId = String(crossesGroup?.base?.id || crossesGroup?.items?.[0]?.id || ''); 8551 if (!group || !crossesId) return; 8552 const nextCrosses = { ...(this.state.rosganBreedCrosses || {}), [key]: !this.state.rosganBreedCrosses?.[key] }; 8553 if (!nextCrosses[key]) delete nextCrosses[key]; 8554 const withoutCrosses = (this.rosganForm().breedIds || []).filter((id) => String(id) !== crossesId); 8555 const nextBreedIds = Object.values(nextCrosses).some(Boolean) ?[...withoutCrosses, crossesId] : withoutCrosses; 8556 this.setState({ rosganLotForm: { ...this.rosganForm(), breedIds: nextBreedIds }, rosganBreedCrosses: nextCrosses, rosganLotMessage: null }); 8557 } 8558 rosganArray(data) { 8559 if (Array.isArray(data)) return data; 8560 for (const key of ['data', 'items', 'results', 'remates', 'lots', 'videos', 'categories', 'breeds', 'consignatarias', 'comitentes', 'establishments']) { 8561 if (Array.isArray(data?.[key])) return data[key]; 8562 } 8563 if (data?.data && typeof data.data === 'object') return this.rosganArray(data.data); 8564 return []; 8565 } 8566 async loadRosganLotOptions(force = false) { 8567 if (this.state.rosganLotOptionsLoading || (!force && this.state.rosganLotOptions)) return; 8568 this.setState({ rosganLotOptionsLoading: true, rosganLotOptionsError: '' }); 8569 try { 8570 const response = await this.apiFetch('/api/rosgan-lots/options'); 8571 const options = response?.data && !Array.isArray(response.data) ?response.data : response; 8572 this.setState({ rosganLotOptions: options || {}, rosganLotOptionsLoading: false, rosganLotOptionsError: '' }); 8573 } catch (error) { 8574 this.setState({ rosganLotOptionsLoading: false, rosganLotOptionsError: error?.message || 'No se pudieron cargar las opciones.' }); 8575 } 8576 } 8577 rosganFieldOptions(...aliases) { 8578 const source = this.state.rosganLotOptions; 8579 if (!source) return []; 8580 const normalize = (value) => String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, ''); 8581 const targets = new Set(aliases.flatMap((alias) => [alias, alias + 's', alias + 'Options', alias + 'Values', alias + 'List']).map(normalize)); 8582 const containers = [source]; 8583 const visited = new Set(); 8584 let raw = null; 8585 while (containers.length && !raw) { 8586 const container = containers.shift(); 8587 if (!container || typeof container !== 'object' || visited.has(container)) continue; 8588 visited.add(container); 8589 if (Array.isArray(container)) { 8590 const field = container.find((item) => targets.has(normalize(item?.key || item?.field || item?.name || item?.
8590id))); 8591 if (field) { raw = field.options || field.values || field.items || field.data; break; } 8592 container.filter((item) => item && typeof item === 'object').forEach((item) => containers.push(item)); 8593 continue; 8594 } 8595 const key = Object.keys(container || {}).find((candidate) => targets.has(normalize(candidate))); 8596 if (key) { raw = container[key]; break; } 8597 Object.values(container).filter((value) => value && typeof value === 'object').forEach((value) => containers.push(value)); 8598 } 8599 if (!Array.isArray(raw)) raw = raw?.options || raw?.values || raw?.items || raw?.data || []; 8600 const seen = new Set(); 8601 return (Array.isArray(raw) ?raw : []).map((item) => { 8602 const value = item && typeof item === 'object' ?(item.value ?? item.id ?? item.code ?? item.name ?? item.label) : item; 8603 const label = item && typeof item === 'object' ?(item.label ?? item.displayName ?? item.name ?? item.description ?? value) : item; 8604 return { value: String(value ?? ''), label: String(label ?? value ?? '') }; 8605 }).filter((item) => item.value && !seen.has(item.value) && seen.add(item.value)); 8606 } 8607 rosganAssignedConsignatarias(consignatarias = this.state.rosganLotConsignatarias || [], relations = null) { 8608 if (this.canManageUsers(this.state.apiUser)) return null; 8609 const memberships = Array.isArray(relations) ?relations : [...(this.state.userConsignatarias || []), this.state.userConsignataria]; 8610 const refs = memberships 8611 .flatMap((item) => [item?.id, item?.slug, item?.ref, item?.name]).filter(Boolean); 8612 return consignatarias.filter((item) => refs.some((ref) => [item?.id, item?.slug, item?.name, item?.nombre] 8613 .some((value) => value && this.consKey(value) === this.consKey(ref)))); 8614 } 8615 rosganAssignedConsignataria(consignatarias = this.state.rosganLotConsignatarias || [], relations = null) { 8616 return this.rosganAssignedConsignatarias(consignatarias, relations)?.[0] || null; 8617 } 8618 rosganEligibleRemates(remates) { 8619 return (remates || []).filter((item) => item?.activo !== false && item?.cargaDeLotes !== false && item?.closeRemate !== true && !item?.cabanasConfig); 8620 } 8621 rosganUpcomingRemates(remates) { 8622 return (remates || []).filter((item) => item?.activo !== false && item?.closeRemate !== true && !item?.cabanasConfig); 8623 } 8624 homeFeaturedRemates(historical, current) { 8625 const active = this.rosganUpcomingRemates(current) 8626 .map((item) => this.normalizeRosganActiveRemate(item)); 8627 const seen = new Set(); 8628 return [...active, ...(historical || [])] 8629 .filter((item) => !this.isExcludedRemate(item) && this.homeAgendaIncludes(this.remateDate(item))) 8630 .filter((item) => { 8631 const key = item.__feriaAuction ?'feria:' + item.feriaId : this.rosganArchiveIdentity(item); 8632 if (seen.has(key)) return false; 8633 seen.add(key); 8634 return true; 8635 }) 8636 .sort((a, b) => this.remateDate(a) - this.remateDate(b)); 8637 } 8638 rosganArchiveIdentity(item) { 8639 const title = item?.nombre_remate || item?.titulo || ''; 8640 const year = this.remateDate(item)?.getFullYear() || ''; 8641 return `${this.consKey(title)}:${year}`; 8642 } 8643 rosganScheduledRemates(items, current) { 8644 const existing = new Set((current || []).map((item) => this.rosganArchiveIdentity(item))); 8645 const inactive = (current || []).filter((item) => (item.activo === false || item.cargaDeLotes === false) && !item.closeRemate && !item.cabanasConfig) 8646 .map((item) => ({ ...this.normalizeRosganActiveRemate(item), __rosganScheduled: true })); 8647 return [...inactive, ...(items || [])].filter((item) => { 8648 const typeAndName = this.consKey([item.nombre_tipo, item.tipo_remate, item.nombre_remate].filter(Boolean).join(' ')); 8649 return !this.isExcludedRemate(item) && !typeAndName.includes('cabana') && this.remateType(item)?.[0] !== 'Cabañas' 8650 && (item.__rosganScheduled || this.homeAgendaIncludes(this.remateDate(item))) && (item.__rosganScheduled || !existing.has(this.rosganArchiveIdentity(item))); 8651 }).sort((a, b) => this.remateDate(a) - this.remateDate(b)); 8652 } 8653 rosganArchiveRemates(historical, current, year) { 8654 const selectedYear = Number(year); 8655 const closed = (current || []) 8656 .filter((item) => item?.closeRemate === true && !item?.cabanasConfig && !this.isExcludedRemate(item)) 8657 .map((item) => ({ ...this.normalizeRosganActiveRemate(item), __rosganClosedArchive: true })) 8658 .filter((item) => !selectedYear || this.remateDate(item)?.getFullYear() === selectedYear); 8659 const replacements = new Set(closed.map((item) => this.rosganArchiveIdentity(item))); 8660 const legacy = (historical || []) 8661 .filter((item) => this.isRosganHistoricalRemate(item)) 8662 .filter((item) => !replacements.has(this.rosganArchiveIdentity(item))); 8663 return [...closed, ...legacy]; 8664 } 8665 emptyRosganRemateForm(item = null) { 8666 const sourceDate = item?.fechaHoraRemate ?new Date(item.fechaHoraRemate) : null; 8667 const validDate = sourceDate && !Number.isNaN(sourceDate.getTime()); 8668 const pad = (value) => String(value).padStart(2, '0'); 8669 const date = validDate ?`${sourceDate.getFullYear()}-${pad(sourceDate.getMonth() + 1)}-${pad(sourceDate.getDate())}` : ''; 8670 const time = validDate ?`${pad(sourceDate.getHours())}:${pad(sourceDate.getMinutes())}` : ''; 8671 return { 8672 titulo: item?.titulo || '', fecha: date, hora: time, cabezas: item?.cabezas ?String(item.cabezas) : '', 8673 tipoRemate: item?.tipoRemate || String(this.state.rosganRemateTypes?.[0]?.id || ''), modoConsignataria: item?.modoConsignataria || 'Individual', 8674 consignatariaId: String(item?.consignatariaId || ''), lugar: item?.lugar || '', provincia: item?.provincia || '', localidad: item?.localidad || '', 8675 canalRural: !!item?.canalRural, 8676 cargaDeLotes: item?.cargaDeLotes ?? false, closeRemate: !!item?.closeRemate, 8677 informaIndice: item?.informaIndice ?? false, mostrarComitentes: item?.mostrarComitentes ?? false, 8678 }; 8679 } 8680 normalizeRosganRemates(data) { 8681 if (Array.isArray(data)) return data; 8682 const root = data?.data ?? data; 8683 if (Array.isArray(root)) return root; 8684 for (const key of ['items', 'remates', 'results']) if (Array.isArray(root?.[key])) return root[key]; 8685 return root?.id ?[root] : []; 8686 } 8687 remateManualShareHeads(remate) { 8688 const heads = Number(remate?.cabezas); 8689 return Number.isSafeInteger(heads) && heads > 0 ? heads : 0; 8690 } 8691 rosganRemateHeads(remate, lots = []) { 8692 const manual = Number(remate?.cabezas); 8693 if (Number.isFinite(manual) && manual > 0) return manual; 8694 return lots.reduce((sum, lot) => { 8695 const source = { ...(lot?.data || lot?.lot || {}), ...lot }; 8696 const count = Number(source.totalQuantity ?? source.quantity ?? source.cantidad); 8697 const split = (Number(source.maleQuantity ?? source.cantidadMachos) || 0) + (Number(source.femaleQuantity ?? source.cantidadHembras) || 0) 8698 || (Number(source.motherQuantity ?? source.cantidadMadres) || 0) + (Number(source.calfQuantity ?? source.cantidadCrias) || 0); 8699 return sum + (Number.isFinite(count) && count > 0 ? count : Math.max(0, split)); 8700 }, 0); 8701 } 8702 async loadRosganAutomaticHeads(remates) { 8703 return Promise.all(remates.map(async (item) => { 8704 if (Number(item.cabezas) > 0) return item;
8705 try { 8706 const lots = this.rosganArray(await this.apiFetch('/api/remates/' + encodeURIComponent(item.id) + '/rosgan-lots')); 8707 return { ...item, __automaticHeads: this.rosganRemateHeads(item, lots) }; 8708 } catch { return { ...item, __automaticHeads: null }; } 8709 })); 8710 } 8711 normalizeRosganRemateTypes(data) { 8712 const root = data?.data ?? data; 8713 const items = Array.isArray(root) ?root : (root?.items || root?.tiposRemate || root?.results || []); 8714 return (Array.isArray(items) ?items : []) 8715 .filter((item) => item?.isActive === true) 8716 .map((item) => ({ 8717 ...item, 8718 id: String(item?.id || '').trim(), 8719 name: String(item?.name || item?.nombre || item?.id || '').trim(), 8720 permiteModoTodos: item?.permiteModoTodos === true, 8721 })) 8722 .filter((item) => item.id && item.name) 8723 .sort((a, b) => Number(a.order ?? Number.MAX_SAFE_INTEGER) - Number(b.order ?? Number.MAX_SAFE_INTEGER) || a.name.localeCompare(b.name, 'es')); 8724 } 8725 rosganRemateType(value, types = this.state.rosganRemateTypes || []) { 8726 const key = this.consKey(value); 8727 return types.find((item) => this.consKey(item.id) === key || this.consKey(item.name) === key) || null; 8728 } 8729 async loadRosganRemateTypes(force = false) { 8730 if (this.state.rosganRemateTypesLoading || (!force && this.state.rosganRemateTypes)) return; 8731 this.setState({ rosganRemateTypesLoading: true, rosganRemateTypesError: '' }); 8732 try { 8733 const types = this.normalizeRosganRemateTypes(await this.apiFetch('/api/tipos-remate')); 8734 if (!types.length) throw new Error('No hay tipos de remate activos disponibles.'); 8735 const form = this.state.rosganRemateForm; 8736 const selected = this.rosganRemateType(form?.tipoRemate, types) || types[0]; 8737 const nextForm = form ?{ 8738 ...form, 8739 tipoRemate: selected.id, 8740 modoConsignataria: selected.permiteModoTodos && form.modoConsignataria === 'Todos' ?'Todos' : 'Individual', 8741 consignatariaId: selected.permiteModoTodos && form.modoConsignataria === 'Todos' ?'' : form.consignatariaId, 8742 } : form; 8743 this.setState({ rosganRemateTypes: types, rosganRemateTypesLoading: false, rosganRemateTypesError: '', rosganRemateForm: nextForm }); 8744 } catch (error) { 8745 this.setState({ rosganRemateTypes: [], rosganRemateTypesLoading: false, rosganRemateTypesError: error?.message || 'No se pudieron cargar los tipos de remate.' }); 8746 } 8747 } 8748 async loadRosganRemateAdmin(force = false) { 8749 if (this._rosganRematesRequest) return this._rosganRematesRequest; 8750 if (!force && this.state.rosganRemates && this.state.rosganLotConsignatarias) return this.state.rosganRemates; 8751 const uid = String(this.state.user?.uid || this.state.apiUser?.uid || this.state.apiUser?.userId || ''); 8752 const cacheKey = 'rosgan:remates:v3:public'; 8753 const cached = !Array.isArray(this.state.rosganRemates) && cacheKey ?this.readCollectionCache(cacheKey) : null; 8754 const fallbackRemates = Array.isArray(cached?.remates) ?cached.remates : this.state.rosganRemates; 8755 const fallbackConsignatarias = Array.isArray(cached?.consignatarias) ?cached.consignatarias : this.state.rosganLotConsignatarias; 8756 const hadLiveConsignatarias = Array.isArray(this.state.rosganLotConsignatarias); 8757 this.setState({ 8758 rosganRemates: Array.isArray(fallbackRemates) ?fallbackRemates : this.state.rosganRemates, 8759 rosganLotConsignatarias: Array.isArray(fallbackConsignatarias) ?fallbackConsignatarias : this.state.rosganLotConsignatarias, 8760 rosganRematesLoading: true, 8761 rosganRematesError: '', 8762 }); 8763 const request = (async () => { 8764 try { 8765 const isAdmin = this.canManageUsers(this.state.apiUser); 8766 const canManage = this.canManageSellerData(this.state.apiUser); 8767 const knownRelations = [...(this.state.userConsignatarias || []), this.state.userConsignataria].filter(Boolean); 8768 const shouldRefreshConsignatarias = force || !hadLiveConsignatarias; 8769 const consignatariasRequest = shouldRefreshConsignatarias 8770 ?this.apiFetchWithFallback('/api/rosgan-consignatarias', '/api/consignatarias').catch(() => []) 8771 : Promise.resolve(null); 8772 const relationsRequest = canManage && !isAdmin && !knownRelations.length && uid 8773 ?this.fetchUserConsignataria(uid) 8774 : Promise.resolve(knownRelations); 8775 const publicRematesRequest = this.apiFetch('/api/Remates'); 8776 const [consignatariasRaw, resolvedRelations, publicRematesRaw] = await Promise.all([ 8777 consignatariasRequest, 8778 relationsRequest, 8779 publicRematesRequest, 8780 ]); 8781 const consignatariasSource = consignatariasRaw 8782 ?this.rosganArray(consignatariasRaw) 8783 : (this.state.rosganLotConsignatarias || fallbackConsignatarias || []); 8784 const consignatarias = consignatariasSource.filter((item) => item?.isActive !== false && item?.activo !== false); 8785 const relations = Array.isArray(resolvedRelations) && resolvedRelations.length ?resolvedRelations : knownRelations; 8786 const assigned = canManage ?this.rosganAssignedConsignataria(consignatarias, relations) : null; 8787 const remates = await this.loadRosganAutomaticHeads(this.normalizeRosganRemates(publicRematesRaw).filter((item) => !item?.cabanasConfig)); 8788 const currentForm = this.state.rosganRemateForm; 8789 const nextForm = currentForm && !currentForm.consignatariaId && assigned 8790 ?{ ...currentForm, consignatariaId: String(assigned.id) } 8791 : currentForm; 8792 this.writeCollectionCache(cacheKey, { remates, consignatarias }, 30 * 60 * 1000); 8793 this.setState({ 8794 rosganRemates: remates, rosganLotConsignatarias: consignatarias, rosganRematesLoading: false, 8795 rosganRematesError: '', rosganRemateForm: nextForm, 8796 }); 8797 this.loadRemateListCatalogs(remates); 8798 return remates; 8799 } catch (error) { 8800 this.setState({ 8801 rosganRemates: Array.isArray(this.state.rosganRemates) ?this.state.rosganRemates : (fallbackRemates || []), 8802 rosganRematesLoading: false, 8803 rosganRematesError: error?.message || 'No se pudieron cargar los remates Rosgan.', 8804 }); 8805 return null; 8806 } 8807 })(); 8808 this._rosganRematesRequest = request; 8809 try { return await request; } 8810 finally { if (this._rosganRematesRequest === request) this._rosganRematesRequest = null; } 8811 } 8812 chooseRemateCreationType() { 8813 if (!this.canManageUsers(this.state.apiUser)) return; 8814 if (document.getElementById('remate-create-choice')) return; 8815 const previousFocus = document.activeElement; 8816 const dialog = document.createElement('dialog'); 8817 dialog.id = 'remate-create-choice'; 8818 dialog.setAttribute('aria-labelledby', 'remate-create-choice-title'); 8819 dialog.style.cssText = 'width:min(520px,calc(100vw - 32px));box-sizing:border-box;padding:24px;border:1px solid #dce7f2;border-radius:20px;color:#112033;background:#fff;box-shadow:0 24px 80px #001d3b40;font-family:inherit;max-height:calc(100dvh - 32px);overflow:auto;'; 8820 dialog.innerHTML = ` 8821 <style>#remate-create-choice::backdrop{background:rgba(0,25,52,.5);backdrop-filter:blur(3px)}#remate-create-choice .remate-type-choice{display:flex;align-items:center;gap:14px;width:100%;padding:18px;margin-top:12px;text-align:left;font:inherit;color:#12304d;border:1px solid #dce7f2;border-radius:14px;background:#f7faff;cursor:pointer}#remate-create-choice .remate-type-choice:hover{background:#eaf3ff;border-color:#0a5ca8}#remate-create-choice button:focus-visible{outline:3px solid #0a5ca8;outline-offset:3px}#remate-create-choice .remate-type-choice img{width:26px;height:26px;flex:none}#remate-create-choice strong{display:block;font-size:16px}#remate-create-choice small{display:block;margin-top:5px;font-size:13px;line-height:1.5;color:#61758c}</style> 8822 <div style="display:flex;align-items:center;justify-content:space-between;gap:12px"><h2 id="remate-create-choice-title" style="margin:0;font-size:23px">Crear remate</h2><button type="button" data-close aria-label="Cerrar" style="border:1px solid #dce7f2;border-radius:50%;width:36px;height:36px;background:white;cursor:pointer;font-size:23px;color:#12304d">Ã</button></div> 8823 <p style="margin:10px 0 20px;color:#61758c;font-size:14px">Elegà qué tipo de remate querés crear.</p> 8824 <button type="button" class="remate-type-choice" data-kind="rosgan"><img src="/assets/icons/lucide/clipboard-list.svg" alt=""><span><strong>Invernada / CrÃa / Faena</strong><small>Crear un remate Rosgan.</small></span><span aria-hidden="true" style="margin-left:auto">â</span></button> 8825 <button type="button" class="remate-type-choice" data-kind="cabana"><img src="/assets/icons/lucide/warehouse.svg" alt=""><span><strong>Cabaña</strong><small>Crear un remate de cabaña.</small></span><span aria-hidden="true" style="margin-left:auto">â</span></button>`; 8826 dialog.querySelector('[data-close]').onclick = () => dialog.close(); 8827 dialog.addEventListener('click', event => { 8828 if (event.target === dialog) { 8829 const bounds = dialog.getBoundingClientRect(); 8830 if (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom) dialog.close(); 8831 } 8832 }); 8833 dialog.addEventListener('close', () => { 8834 dialog.remove(); 8835 if (previousFocus?.isConnected) previousFocus.focus(); 8836 }, { once: true }); 8837 dialog.querySelectorAll('[data-kind]').forEach(button => { 8838 button.onclick = () => {
8839 const kind = button.dataset.kind; 8840 dialog.close(); 8841 if (kind === 'cabana') this.openRemateConfig('Remate Cabañas'); 8842 else this.openRosganRemateForm(); 8843 }; 8844 }); 8845 document.body.appendChild(dialog); 8846 dialog.showModal(); 8847 dialog.querySelector('[data-kind]').focus(); 8848 } 8849 openRosganRemateForm(item = null, { navigate = true } = {}) { 8850 if (!this.canManageUsers(this.state.apiUser)) { this.showToast('Solo administradores pueden crear o editar remates.'); return; } 8851 this.fetchProvinces(); 8852 const assigned = this.rosganAssignedConsignataria(this.state.rosganLotConsignatarias || []); 8853 const form = this.emptyRosganRemateForm(item); 8854 if (!this.canManageUsers(this.state.apiUser) && assigned) form.consignatariaId = String(assigned.id); 8855 this.resetRosganRematePlaces(); 8856 if (this.state.rosganRemateFlyerPreview?.startsWith('blob:')) URL.revokeObjectURL(this.state.rosganRemateFlyerPreview); 8857 this.setState({ 8858 rosganRemateFormOpen: true, rosganRemateEditingId: String(item?.id || ''), rosganRemateForm: form, 8859 rosganRemateMessage: null, rosganRemateFlyer: null, rosganRemateFlyerPreview: item?.flyerUrl || '', 8860 }, () => { if (navigate) this.go('rosganRemateCreate'); }); 8861 this.loadRosganRemateTypes(true); 8862 if (!this.state.rosganLotConsignatarias) this.loadRosganRemateAdmin(true); 8863 } 8864 closeRosganRemateForm() { 8865 this.resetRosganRematePlaces(); 8866 if (this.state.rosganRemateSaving) return; 8867 if (this.state.rosganRemateFlyerPreview?.startsWith('blob:')) URL.revokeObjectURL(this.state.rosganRemateFlyerPreview); 8868 this.setState({ rosganRemateFormOpen: false, rosganRemateEditingId: '', rosganRemateForm: null, rosganRemateMessage: null, rosganRemateFlyer: null, rosganRemateFlyerPreview: '' }, () => this.go('remates')); 8869 } 8870 setRosganRemateField(key, value) { 8871 const form = { ...(this.state.rosganRemateForm || this.emptyRosganRemateForm()), [key]: value }; 8872 if (key === 'provincia') { form.localidad = ''; this.resetRosganRematePlaces(); } 8873 if (key === 'tipoRemate') { 8874 const type = this.rosganRemateType(value); 8875 if (!type?.permiteModoTodos) form.modoConsignataria = 'Individual'; 8876 } 8877 if (key === 'modoConsignataria' && value === 'Todos') form.consignatariaId = ''; 8878 this.setState({ rosganRemateForm: form, rosganRemateMessage: null }); 8879 } 8880 resetRosganRematePlaces() { 8881 clearTimeout(this._rrfPlaceTimer); clearTimeout(this._rrfPlaceBlur); 8882 this._rrfPlaceToken = (this._rrfPlaceToken || 0) + 1; 8883 this.setState({rrfPlaceResults:[],rrfPlaceLoading:false,rrfPlaceError:'',rrfPlaceOpen:false,rrfPlaceIndex:-1}); 8884 } 8885 onRosganRematePlaceInput(value) { 8886 this.resetRosganRematePlaces(); 8887 this.setRosganRemateField('localidad', value); 8888 const form = this.state.rosganRemateForm; 8889 if (!form?.provincia || String(value).trim().length < 3) return; 8890 const token = this._rrfPlaceToken; 8891 this._rrfPlaceTimer = setTimeout(() => this.searchRosganRematePlaces(value,form.provincia,token),350); 8892 } 8893 async searchRosganRematePlaces(value,province,token) { 8894 if (token !== this._rrfPlaceToken) return; 8895 this.setState({rrfPlaceLoading:true,rrfPlaceError:'',rrfPlaceOpen:true}); 8896 try { 8897 const response = await this.apiFetch('/api/places/search',{method:'POST',body:JSON.stringify({query:String(value).trim(),provincia:province})}); 8898 if (token !== this._rrfPlaceToken) return; 8899 const results = this.listFrom(response,['data','items','results']).slice(0,7); 8900 this.setState({rrfPlaceResults:results,rrfPlaceLoading:false,rrfPlaceError:results.length?'':'No encontramos esa localidad. Podés escribirla manualmente.'}); 8901 } catch { 8902 if (token !== this._rrfPlaceToken) return; 8903 this.setState({rrfPlaceResults:[],rrfPlaceLoading:false,rrfPlaceError:'No se pudo consultar la ubicación. Podés escribirla manualmente.'}); 8904 } 8905 } 8906 selectRosganRematePlace(item) { 8907 if (!item) return; 8908 const input = typeof document !== 'undefined' ?document.getElementById('rrf-locality') : null; 8909 input?.blur(); 8910 this.resetRosganRematePlaces(); 8911 this.setRosganRemateField('localidad',String(item.label || item.address || ''));
8912 if (input) input.value = String(item.label || item.address || ''); 8913 } 8914 rosganRematePlaceKey(event) { 8915 const items=this.state.rrfPlaceResults || []; 8916 if(event.key==='Escape'){event.preventDefault();this.resetRosganRematePlaces();return;} 8917 if(!items.length)return; 8918 if(['ArrowDown','ArrowUp'].includes(event.key)){ 8919 event.preventDefault();const direction=event.key==='ArrowDown'?1:-1; 8920 this.setState({rrfPlaceIndex:((this.state.rrfPlaceIndex ?? -1)+direction+items.length)%items.length,rrfPlaceOpen:true}); 8921 } else if(event.key==='Enter' && this.state.rrfPlaceIndex>=0){event.preventDefault();this.selectRosganRematePlace(items[this.state.rrfPlaceIndex]);} 8922 } 8923 resetCabanaRematePlaces() { 8924 clearTimeout(this._rcPlaceTimer); clearTimeout(this._rcPlaceBlur); 8925 this._rcPlaceToken = (this._rcPlaceToken || 0) + 1; 8926 this.setState({rcPlaceResults:[],rcPlaceLoading:false,rcPlaceError:'',rcPlaceOpen:false,rcPlaceIndex:-1}); 8927 } 8928 onCabanaRematePlaceInput(value) { 8929 this.resetCabanaRematePlaces(); 8930 this.setRemateConfigField('localidad', value); 8931 const form = this.state.remateConfigForm; 8932 if (!form?.provincia || String(value).trim().length < 3) return; 8933 const token = this._rcPlaceToken; 8934 this._rcPlaceTimer = setTimeout(() => this.searchCabanaRematePlaces(value,form.provincia,token),350); 8935 } 8936 async searchCabanaRematePlaces(value,province,token) { 8937 if (token !== this._rcPlaceToken) return; 8938 this.setState({rcPlaceLoading:true,rcPlaceError:'',rcPlaceOpen:true}); 8939 try { 8940 const response = await this.apiFetch('/api/places/search',{method:'POST',body:JSON.stringify({query:String(value).trim(),provincia:province})}); 8941 if (token !== this._rcPlaceToken) return; 8942 const results = this.listFrom(response,['data','items','results']).slice(0,7); 8943 this.setState({rcPlaceResults:results,rcPlaceLoading:false,rcPlaceError:results.length?'':'No encontramos esa localidad. Podés escribirla manualmente.'}); 8944 } catch { 8945 if (token !== this._rcPlaceToken) return; 8946 this.setState({rcPlaceResults:[],rcPlaceLoading:false,rcPlaceError:'No se pudo consultar la ubicación. Podés escribirla manualmente.'}); 8947 } 8948 } 8949 selectCabanaRematePlace(item) { 8950 if (!item) return; 8951 const input = typeof document !== 'undefined' ?document.getElementById('rc-locality') : null; 8952 input?.blur(); 8953 this.resetCabanaRematePlaces(); 8954 this.setRemateConfigField('localidad',String(item.label || item.address || '')); 8955 if (input) input.value = String(item.label || item.address || ''); 8956 } 8957 cabanaRematePlaceKey(event) { 8958 const items=this.state.rcPlaceResults || []; 8959 if(event.key==='Escape'){event.preventDefault();this.resetCabanaRematePlaces();return;} 8960 if(!items.length)return; 8961 if(['ArrowDown','ArrowUp'].includes(event.key)){ 8962 event.preventDefault();const direction=event.key==='ArrowDown'?1:-1; 8963 this.setState({rcPlaceIndex:((this.state.rcPlaceIndex ?? -1)+direction+items.length)%items.length,rcPlaceOpen:true}); 8964 } else if(event.key==='Enter' && this.state.rcPlaceIndex>=0){event.preventDefault();this.selectCabanaRematePlace(items[this.state.rcPlaceIndex]);} 8965 } 8966 toggleRosganRemateField(key) { 8967 const form = this.state.rosganRemateForm || this.emptyRosganRemateForm(); 8968 this.setRosganRemateField(key, !form[key]); 8969 } 8970 setRosganRemateFlyer(file) { 8971 if (this.state.rosganRemateFlyerPreview?.startsWith('blob:')) URL.revokeObjectURL(this.state.rosganRemateFlyerPreview); 8972 if (!file) { this.setState({ rosganRemateFlyer: null, rosganRemateFlyerPreview: '' }); return; } 8973 if (!String(file.type || '').startsWith('image/')) { this.setState({ rosganRemateMessage: { ok: false, text: 'El flyer debe ser una imagen.' } }); return; } 8974 this.setState({ rosganRemateFlyer: file, rosganRemateFlyerPreview: URL.createObjectURL(file), rosganRemateMessage: null }); 8975 } 8976 buildRosganRematePayload() { 8977 const form = this.state.rosganRemateForm || this.emptyRosganRemateForm(); 8978 const assigned = this.rosganAssignedConsignataria(this.state.rosganLotConsignatarias || []); 8979 const mode = form.modoConsignataria === 'Todos' ?'Todos' : 'Indiv
8979idual'; 8980 const consignatariaId = mode === 'Todos' ?null : (this.canManageUsers(this.state.apiUser) ?form.consignatariaId : (assigned?.id || '')); 8981 const localDate = form.fecha && form.hora ?new Date(`${form.fecha}T${form.hora}:00`) : null; 8982 return { 8983 titulo: String(form.titulo || '').trim(), 8984 fechaHoraRemate: localDate && !Number.isNaN(localDate.getTime()) ?localDate.toISOString() : '', 8985 cabezas: Number(form.cabezas), tipoRemate: String(form.tipoRemate || ''), modoConsignataria: mode, 8986 consignatariaId: consignatariaId ?String(consignatariaId) : null, lugar: String(form.lugar || '').trim(), provincia: form.provincia || '', localidad: form.localidad || '', 8987 canalRural: !!form.canalRural, cargaDeLotes: !!form.cargaDeLotes, 8988 closeRemate: !!form.closeRemate, informaIndice: !!form.informaIndice, mostrarComitentes: !!form.mostrarComitentes, 8989 }; 8990 } 8991 async setRemateLotLoading(item, enabled) { 8992 if (!this.canManageUsers(this.state.apiUser)) throw new Error('Solo Admin y SysAdmin pueden cambiar la carga de lotes.'); 8993 if (this.state.remateLotLoadingBusy) return null; 8994 const id = this.remateRouteId(item); 8995 this.setState({ remateLotLoadingBusy: true }); 8996 try { 8997 const response = await this.apiFetch('/api/remates/' + encodeURIComponent(id)); 8998 const fresh = response?.data || response; 8999 if (enabled && fresh.closeRemate === true) throw new Error('Para habilitar la carga de lotes, abrà el remate desde Editar remate.'); 9000 const { cargaDeDatos, filtroComitentes, ocultarEstablecimiento, ...currentFields } = fresh; 9001 const payload = { ...currentFields, cargaDeLotes: !!enabled, mostrarComitentes: fresh.mostrarComitentes ?? false }; 9002 await this.apiFetch('/api/remates/' + encodeURIComponent(id), { method: 'PUT', body: JSON.stringify(payload) }); 9003 const result = await this.apiFetch('/api/remates/' + encodeURIComponent(id)); 9004 const savedData = result?.data || result; 9005 const saved = { ...savedData, mostrarComitentes: savedData.mostrarComitentes ?? false }; 9006 if (saved.cargaDeLotes !== enabled) throw new Error('No se pudo confirmar el cambio. Actualizá el remate.'); 9007 Object.assign(item, saved); 9008 const current = this.state.currentRemate; 9009 this.setState({ ...(current && String(this.remateRouteId(current)) === String(id) ? { currentRemate: { ...current, ...saved } } : {}), 9010 rosganRemates: (this.state.rosganRemates || []).map(r => String(this.remateRouteId(r)) === String(id) ? { ...r, ...saved } : r) }); 9011 this.showToast(enabled ? 'Carga de lotes habilitada.' : 'Carga de lotes cerrada.'); 9012 return saved; 9013 } finally { this.setState({ remateLotLoadingBusy: false }); } 9014 } 9015 async setRosganRemateClosed(item, closed) { 9016 if (!this.canManageUsers(this.state.apiUser) || this.state.remateClosingBusy) return; 9017 const id = this.remateRouteId(item); 9018 this.setState({ remateClosingBusy: true }); 9019 try { 9020 const response = await this.apiFetch('/api/remates/' + encodeURIComponent(id)); 9021 const fresh = response?.data || response; 9022 const { cargaDeDatos, filtroComitentes, ocultarEstablecimiento, ...fields } = fresh; 9023 const payload = { ...fields, closeRemate: !!closed, mostrarComitentes: fresh.mostrarComitentes ?? false }; 9024 await this.apiFetch('/api/remates/' + encodeURIComponent(id), { method: 'PUT', body: JSON.stringify(payload) }); 9025 const result = await this.apiFetch('/api/remates/' + encodeURIComponent(id)); 9026 const saved = result?.data || result; 9027 if (saved.closeRemate !== closed) throw new Error('No se pudo confirmar el estado del remate.'); 9028 this.setState({ currentRemate: { ...this.state.currentRemate, ...saved }, rosganRemates: (this.state.rosganRemates || []).map(r => String(this.remateRouteId(r)) === String(id) ?{ ...r, ...saved } : r) }); 9029 this.showToast(closed ?'Remate cerrado.' : 'Remate abierto.'); 9030 } catch (error) { this.showToast(error.message || 'No se pudo cambiar el estado del remate.'); } 9031 finally { this.setState({ remateClosingBusy: false }); } 9032 } 9033 async saveRosganRemate() { 9034 if (this.state.rosganRemateSaving) return; 9035 if (!this.canManageUsers(this.state.apiUser)) { this.showToast('Solo administradores pueden guardar remates.'); return; } 9036 const payload = this.buildRosganRematePayload(); 9037 const selectedType = this.rosganRemateType(payload.tipoRemate); 9038 const required = [ 9039 [payload.titulo, 'Ingresá el tÃtulo del remate.'], [payload.fechaHoraRemate, 'Elegà la fecha y la hora del remate.'], 9040 [String((this.state.rosganRemateForm || {}).cabezas ?? '').trim() === '' || (Number.isSafeInteger(payload.cabezas) && payload.cabezas > 0), 'Indicá una cantidad de cabezas entera mayor a cero o dejá el campo vacÃo para calcularla automáticamente.'], [selectedType, 'Elegà un tipo de remate activo.'], 9041 [payload.modoConsignataria === 'Todos' || payload.consignatariaId, 'Elegà la consignataria.'], 9042 [payload.provincia, 'Elegà la provincia.'], [payload.localidad, 'Elegà la localidad.'], 9043 ]; 9044 if (payload.modoConsignataria === 'Todos' && !selectedType?.permiteModoTodos) { 9045 this.setState({ rosganRemateMessage: { ok: false, text: 'Este tipo de remate solo admite una consignataria individual.' } }); 9046 return; 9047 } 9048 const invalid = required.find(([valid]) => !valid); 9049 if (invalid) { this.setState({ rosganRemateMessage: { ok: false, text: invalid[1] } }); return; } 9050 const editingId = this.state.rosganRemateEditingId; 9051 this.setState({ rosganRemateSaving: true, rosganRemateMessage: null }); 9052 try { 9053 const response = await this.apiFetch(editingId ?'/api/remates/' + encodeURIComponent(editingId) : '/api/remates', { 9054 method: editingId ?'PUT' : 'POST', body: JSON.stringify(payload), 9055 }); 9056 const responseData = response?.data ?? response ?? {}; 9057 const root = { ...responseData, mostrarComitentes: responseData.mostrarComitentes ?? false }; 9058 const remateId = String(root?.id || root?.remateId || editingId || ''); 9059 if (remateId) this.setState({ rosganRemates: [{ ...payload, ...root, id: remateId }, ...(this.state.rosganRemates || []).filter(item => String(item.id) !== remateId)] }); 9060 if (this.state.rosganRemateFlyer) { 9061 if (!remateId) throw new Error('El remate se guardó, pero el API no devolvió su ID para cargar el flyer.'); 9062 const flyerBody = new FormData(); 9063 flyerBody.append('flyer', this.state.rosganRemateFlyer); 9064 await this.apiFetch('/api/remates/' + encodeURIComponent(remateId) + '/flyer', { method: 'POST', body: flyerBody }); 9065 } 9066 if (this.state.rosganRemateFlyerPreview?.startsWith('blob:')) URL.revokeObjectURL(this.state.rosganRemateFlyerPreview); 9067 this.setState({ 9068 rosganRemateSaving: false, rosganRemateFormOpen: false, rosganRemateEditingId: '', rosganRemateForm: null, 9069 rosganRemateFlyer: null, rosganRemateFlyerPreview: '', rosganRemateMessage: null, 9070 }, () => this.go('remates')); 9071 this.showToast(editingId ?'Remate actualizado correctamente.' : 'Remate creado correctamente.'); 9072 await this.loadRosganRemateAdmin(true); 9073 } catch (error) { 9074 const detail = error?.data?.errors ?Object.values(error.data.errors).flat().join(' ') : ''; 9075 this.setState({ rosganRemateSaving: false, rosganRemateMessage: { ok: false, text: detail || error?.message || 'No se pudo guardar el remate.' } }); 9076 } 9077 } 9078 async deleteRosganRemate(item) { 9079 if (!this.canManageUsers(this.state.apiUser)) { this.showToast('Solo administradores pueden eliminar remates.'); return; } 9080 const id = String(item?.id || ''); 9081 if (!id || this.state.rosganRemateDeleting || !window.confirm(`¿Eliminar el remate "${item?.titulo || 'Rosgan'}"? Esta acción no se puede deshacer.`)) return; 9082 this.setState({ rosganRemateDeleting: id }); 9083 try { 9084 await this.apiFetch('/api/Remates/' + encodeURIComponent(id), { method: 'DELETE' }); 9085 this.setState({ rosganRemateDeleting: '' }); 9086 this.showToast('Remate eliminado.'); 9087 if (this.state.currentRemate?.__rosganActive && this.remateRouteId(this.state.currentRemate) === id) this.go('remates'); 9088 await this.loadRosganRemateAdmin(true); 9089 } catch (error) { 9090 this.setState({ rosganRemateDeleting: '', rosganRematesError: error?.message || 'No se pudo eliminar el remate.' }); 9091 } 9092 } 9093 rosganConsignatariasForRemate(remateId = this.rosganForm().remateId) { 9094 if (!this.state.user?.uid || !this.state.apiUser || this.state.rosganLotLoading || this.state.rosganLotScopeUid !== this.state.user.uid) return []; 9095 const remate = (this.state.rosganLotRemates || []).find((item) => String(item.id) === String(remateId)); 9096 const all = this.state.rosganLotConsignatarias || []; 9097 const participants = Array.isArray(remate?.consignatariasParticipantes) ?remate.consignatariasParticipantes : []; 9098 let list = participants.length ?participants.map((participant) => { 9099 const id = participant.consignatariaId || participant.id; 9100 return all.find((item) => String(item.id) === String(id)) || { id, name: participant.consignatariaNombre || participant.name || id }; 9101 }) : (remate?.consignatariaId ?all.filter((item) => String(item.id) === String(remate.consignatariaId)) : []); 9102 const assigned = this.rosganAssignedConsignatarias(all); 9103 if (assigned) list = list.filter((item) => assigned.some((member) => this.consKey(item.id || item.name) === this.consKey(member.id || member.name))); 9104 return list; 9105 } 9106 async loadRosganLotCatalogs(force = false) { 9107 const uid = this.state.user?.uid; 9108 if (!uid || !this.state.apiUser) return; 9109 this.loadRosganLotOptions(force); 9110 if (this.state.rosganLotLoading && this._rosganCatalogUid === uid) return; 9111 if (!force && this.state.rosganLotScopeUid === uid && this.state.rosganLotRemates && this.state.rosganLotCategories) return; 9112 this._rosganCatalogUid = uid; 9113 const request = this._rosganCatalogRequest = (this._rosganCatalogRequest || 0) + 1; 9114 const currentRequest = () => request === this._rosganCatalogRequest && uid === this.state.user?.uid; 9115 this.setState({ rosganLotLoading: true, rosganLotScopeUid: '', rosganLotRelationsScope: '', rosganLotComitentes: null, rosganLotPendingComitentes: [], rosganLotEstablishments: null, rosganConsignatariaMenuOpen: false, rosganComitenteMenuOpen: false, rosganLotMessage: null }); 9116 try { 9117 const relations = this.canManageUsers(this.state.apiUser) ?[] : await this.fetchUserConsignataria(uid); 9118 if (!currentRequest()) return; 9119 const [rematesRaw, categoriesRaw, breedsRaw, consignatariasRaw] = await Promise.all([ 9120 this.apiFetch('/api/remates'), this.apiFetch('/api/rosgan-categories'), 9121 this.apiFetch('/api/rosganBreeds'), this.apiFetch('/api/rosgan-consignatarias'), 9122 ]); 9123 const remates = this.rosganEligibleRemates(this.rosganArray(rematesRaw)); 9124 const categories = this.rosganArray(categoriesRaw).filter((item) => item?.isActive !== false) 9125 .sort((a, b) => Number(a.order || 0) - Number(b.order || 0)); 9126 const breeds = this.rosganArray(breedsRaw).filter((item) => item?.isActive !== false) 9127 .sort((a, b) => Number(a.order || 0) - Number(b.order || 0)); 9128 if (!currentRequest()) return; 9129 let consignatarias = this.rosganArray(consignatariasRaw).filter((item) => item?.isActive !== false && item?.activo !== false); 9130 const current = this.rosganForm(); 9131 const assigned = this.rosganAssignedConsignatarias(consignatarias, relations || []); 9132 if (assigned) consignatarias = assigned; 9133 const next = { ...current }; 9134 const availableRemates = this.canManageUsers(this.state.apiUser) ?remates : remates.filter((item) => { 9135 const participants = Array.isArray(item.consignatariasParticipantes) && item.consignatariasParticipantes.length 9136 ?item.consignatariasParticipantes.map((entry) => entry.consignatariaId || entry.id)
9137 : [item.consignatariaId].filter(Boolean); 9138 return participants.some((id) => consignatarias.some((member) => String(member.id) === String(id))); 9139 }); 9140 if (!next.remateId && availableRemates.length === 1) next.remateId = String(availableRemates[0].id); 9141 if (!availableRemates.some((item) => String(item.id) === String(next.remateId))) next.remateId = ''; 9142 if (assigned?.length === 1) next.consignatariaId = String(assigned[0].id); 9143 if (!consignatarias.some(item => String(item.id) === String(next.consignatariaId))) next.consignatariaId = ''; 9144 if (next.consignatariaId !== current.consignatariaId) { 9145 next.certifiedBy = null; 9146 next.comitenteId = ''; next.establishmentId = ''; 9147 } 9148 this.setState({ 9149 rosganLotRemates: availableRemates, rosganLotCategories: categories, rosganLotBreeds: breeds, 9150 rosganLotConsignatarias: consignatarias, rosganLotForm: next, rosganLotLoading: false, rosganLotScopeUid: uid, rosganComitenteSearch: '', 9151 }, () => this.loadRosganLotRelations()); 9152 } catch (error) { 9153 if (!currentRequest()) return; 9154 this.setState({ rosganLotLoading: false, rosganLotMessage: { ok: false, text: error?.message || 'No se pudieron cargar los catálogos de Rosgan.' } }); 9155 } 9156 } 9157 rosganLotRelationScope() { 9158 const id = String(this.rosganForm().consignatariaId || ''); 9159 return id && this.rosganConsignatariasForRemate().some(item => String(item.id) === id) ?this.state.user.uid + ':' + id : ''; 9160 } 9161 async loadRosganCertifiers() { 9162 const id = String(this.rosganForm().consignatariaId || ''); 9163 const request = this._rosganCertifiersRequest = (this._rosganCertifiersRequest || 0) + 1; 9164 this.setState({ rosganCertifiers: [], rosganCertifiersLoading: !!id, rosganCertifiersError: '', rosganCertifierMenuOpen: false }); 9165 if (!id) return; 9166 const isCurrent = () => request === this._rosganCertifiersRequest && id === String(this.rosganForm().consignatariaId || ''); 9167 try { 9168 const result = await this.apiFetch('/api/consignatarias/' + encodeURIComponent(id) + '/certifiers'); 9169 if (!isCurrent()) return; 9170 this.setState({ rosganCertifiers: this.rosganArray(result), rosganCertifiersLoading: false }); 9171 } catch (error) { 9172 if (!isCurrent()) return; 9173 this.setState({ rosganCertifiersLoading: false, rosganCertifiersError: error?.message || 'No se pudieron obtener los responsables comerciales' }); 9174 } 9175 } 9176 rosganOperationComitenteId(item) { 9177 return String(item?.operationComitenteId ?? item?.canonicalComitenteId ?? item?.mergedIntoId ?? item?.id ?? ''); 9178 } 9179 rosganSelectedOperationComitenteId() { 9180 const f = this.rosganForm(); 9181 const item = [...(this.state.rosganLotComitentes || []), ...(this.state.rosganLotPendingComitentes || [])].find(c => String(c.id) === String(f.comitenteId) && (!c.consignatariaId || String(c.consignatariaId) === String(f.consignatariaId))); 9182 return this.rosganOperationComitenteId(item || { id: f.comitenteId }); 9183 } 9184 rosganCanonicalComitentes(comitentes = [], pending = []) { 9185 const canonicalId = item => this.rosganOperationComitenteId(item); 9186 const result = new Map(); 9187 const main = [...comitentes].sort((a, b) => Number(String(a.id) !== canonicalId(a)) - Number(String(b.id) !== canonicalId(b))); 9188 for (const item of [...main.map(item => ({ ...item, validated: true })), ...pending.filter(item => !item.mergedIntoId && !item.canonicalComitenteId)]) { 9189 const id = canonicalId(item); 9190 if (id && !result.has(id)) result.set(id, { ...item }); 9191 } 9192 return [...result.values()]; 9193 } 9194 rosganMergeEstablishments(consignatariaId, ...lists) { 9195 const result = new Map(); 9196 for (const list of lists) for (const item of this.rosganArray(list)) { 9197 const id = String(item.id || item.establishmentId || item.establecimientoId || ''); 9198 const owner = String(item.consignatariaId || consignatariaId); 9199 if (!id || owner !== String(consignatariaId)) continue; 9200 result.set(id, { ...item, id, consignatariaId: owner, comitenteId: String(item.comitenteHistoricoId || item.comitenteId || '') }); 9201 } 9202 return [...result.values()]; 9203 } 9204 async loadRosganLotRelations({ preserve = false } = {}) { 9205 this.loadRosganCertifiers(); 9206 const scope = this.rosganLotRelationScope(); 9207 const consignatariaId = String(this.rosganForm().consignatariaId || ''); 9208 const request = this._rosganRelationsRequest = (this._rosganRelationsRequest || 0) + 1; 9209 if (!preserve) this.setState({ rosganLotRelationsScope: '', rosganLotRelationsLoading: !!scope, rosganLotComitentes: null, rosganLotPendingComitentes: [], rosganLotEstablishments: null, rosganComitenteMenuOpen: false, rosganEstablishmentMenuOpen:false, rosganEstablishmentSearch:'' }); 9210 if (!consignatariaId || !scope) return; 9211 const currentRequest = () => request === this._rosganRelationsRequest && scope === this.rosganLotRelationScope(); 9212 try { 9213 const response = await this.apiFetch('/api/legacy-comitentes?mode=relations&consignatariaId='
9213+ encodeURIComponent(consignatariaId)); 9214 const relations = response?.data ?? response; 9215 if (!currentRequest()) return; 9216 this.setState({ 9217 rosganLotPendingComitentes: [], 9218 rosganComitenteLimit: 60, 9219 rosganLotComitentes: relations.comitentes ?? [], 9220 rosganLotEstablishments: [...(relations.establishments ?? []), ...(relations.establecimientosHistorico ?? [])], 9221 rosganLotRelationsLoading: false, rosganLotRelationsScope: scope, 9222 }); 9223 } catch (error) { 9224 if (!currentRequest()) return; 9225 if (preserve) { this.setState({ rosganLotRelationsLoading: false }); return; } 9226 this.setState({ rosganLotComitentes: [], rosganLotEstablishments: [], rosganLotRelationsLoading: false, rosganLotMessage: { ok: false, text: error?.message || 'No se pudieron cargar comitentes y establecimientos.' } }); 9227 } 9228 } 9229 rosganComitentesEndpoint(query = '') { 9230 // Catálogo compartido con Roslot. Para seleccionar relaciones consultar por consignatariaId; 9231 // el backend debe validar la pertenencia también al crear un lote Rosgan. 9232 return '/api/Comitentes' + String(query || ''); 9233 } 9234 async openComitenteMerge(fromLot = false) { 9235 if (!this.canManageComitentes(this.state.apiUser)) return; 9236 let consignatarias = fromLot ? this.rosganConsignatariasForRemate() : (this.state.userConsignatarias || []); 9237 if (!fromLot && this.canManageUsers(this.state.apiUser)) { 9238 try { consignatarias = this.rosganArray(await this.apiFetch('/api/rosgan-consignatarias')); } 9239 catch (error) { this.showToast(error.message || 'No se pudieron cargar las consignatarias.'); return; } 9240 } 9241 window.RosganComitenteMerge.open({ 9242 consignatarias, 9243 consignatariaId: fromLot ? this.rosganForm().consignatariaId : '', 9244 apiFetch: this.apiFetch.bind(this), 9245 onSuccess: async ({ consignatariaId, canonicalComitenteId, duplicateComitenteIds }) => { 9246 this.showToast('Comitentes fusionados correctamente.'); 9247 this._profileComitentesRequest = (this._profileComitentesRequest || 0) + 1; 9248 this._profileEstablishmentsRequest = (this._profileEstablishmentsRequest || 0) + 1; 9249 this._rosganRelationsRequest = (this._rosganRelationsRequest || 0) + 1; 9250 const form = this.rosganForm(); 9251 const sameScope = String(form.consignatariaId) === consignatariaId; 9252 const keepCanonical = sameScope && [canonicalComitenteId, ...duplicateComitenteIds].includes(String(form.comitenteId)); 9253 this.setState({ comitentes: null, comitentesScope: '', establishments: null, establishmentsLoading: false, 9254 rosganLotComitentes: null, rosganLotPendingComitentes: [], rosganLotEstablishments: null, rosganLotRelationsScope: '', 9255 ...(keepCanonical ? { rosganLotForm: { ...form, comitenteId: canonicalComitenteId }, rosganComitenteSearch: '' } : {}), 9256 }); 9257 // Refresh the exact relations endpoint even when the profile is the entry point. 9258 if (sameScope) await this.loadRosganLotRelations(); 9259 else { 9260 try { await this.apiFetch('/api/legacy-comitentes?mode=relations&consignatariaId=' + encodeURIComponent(consignatariaId), { cache: 'no-store' }); } 9261 catch { this.showToast('Fusión completada. No se pudo actualizar la lista; volvé a consultarla.'); } 9262 } 9263 if (!fromLot) await Promise.all([this.fetchComitentes(), this.fetchEstablishments()]); 9264 }, 9265 }); 9266 } 9267 roslotCuitLookupEndpoint(cuit) { 9268 return this.rosganComitentesEndpoint('/lookup-by-cuit?cuit=' + encodeURIComponent(String(cuit || '').replace(/\D/g, ''))); 9269 } 9270 onRosganRemateChange(remateId) { 9271 this._rosganComitenteLookupToken = (this._rosganComitenteLookupToken || 0) + 1; 9272 this._rosganComitenteLookupKey = ''; 9273 const allowed = this.rosganConsignatariasForRemate(remateId); 9274 const currentConsignataria = this.rosganForm().consignatariaId; 9275 const keepCurrent = allowed.some((item) => String(item.id) === String(currentConsignataria)); 9276 const consignatariaId = keepCurrent ?currentConsignataria : (allowed.length === 1 ?allowed[0].id : ''); 9277 this.setState({ 9278 rosganLotForm: { ...this.rosganForm(), remateId, consignatariaId: String(consignatariaId || ''), certifiedBy: keepCurrent ?this.rosganForm().certifiedBy : null, comitenteId: '', establishmentId: '' }, 9279 rosganLotComitentes: null, rosganLotPendingComitentes: [], rosganLotEstablishments: null, rosganLotMessage: null, rosganConsignatariaMenuOpen: false, 9280 rosganComitenteFormOpen: false, rosganComitenteMessage: null, rosganComitenteSearch: '', rosganComitenteMenuOpen: false, 9281 rosganComitenteLookupLoading: false,
9282 rosganComitenteForm: { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' }, 9283 }, () => this.loadRosganLotRelations()); 9284 } 9285 onRosganConsignatariaChange(consignatariaId) { 9286 if (!this.rosganConsignatariasForRemate().some(item => String(item.id) === String(consignatariaId))) return; 9287 this._rosganComitenteLookupToken = (this._rosganComitenteLookupToken || 0) + 1; 9288 this._rosganComitenteLookupKey = ''; 9289 this.setState({ 9290 rosganLotForm: { ...this.rosganForm(), consignatariaId, certifiedBy: null, comitenteId: '', establishmentId: '' }, 9291 rosganLotComitentes: null, rosganLotPendingComitentes: [], rosganLotEstablishments: null, rosganLotMessage: null, rosganConsignatariaMenuOpen: false, 9292 rosganComitenteFormOpen: false, rosganComitenteMessage: null, rosganComitenteSearch: '', rosganComitenteMenuOpen: false, 9293 rosganComitenteLookupLoading: false, 9294 rosganComitenteForm: { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' }, 9295 }, () => this.loadRosganLotRelations()); 9296 } 9297 toggleRosganComitenteForm() { 9298 if (!this.canManageComitentes(this.state.apiUser)) { 9299 this.setState({ rosganComitenteFormOpen: false, rosganLotMessage: { ok: false, text: 'No tenés permisos para gestionar comitentes.' } }); 9300 return; 9301 } 9302 this._rosganComitenteLookupToken = (this._rosganComitenteLookupToken || 0) + 1; 9303 this._rosganComitenteLookupKey = ''; 9304 this.setState({ 9305 rosganComitenteFormOpen: !this.state.rosganComitenteFormOpen, 9306 rosganComitenteLookupLoading: false, 9307 rosganComitenteMessage: null, 9308 rosganComitenteForm: { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' }, 9309 }); 9310 } 9311 setRosganComitenteField(key, value, buffered = false) { 9312 const next = { ...this.state.rosganComitenteForm, [key]: value }; 9313 if (key === 'province') { 9314 next.locality = ''; 9315 if (value) this.fetchLocalities(value); 9316 } 9317 const patch = { rosganComitenteForm: next, rosganComitenteMessage: null }; 9318 if (buffered) this.bufferInputState('rosgan-comitente-' + key, patch); 9319 else this.setState(patch); 9320 } 9321 onRosganComitenteCuit(value) { 9322 const cuit = String(value || '').replace(/\D/g, '').slice(0, 11); 9323 if (cuit.length < 11) { 9324 this._rosganComitenteLookupToken = (this._rosganComitenteLookupToken || 0) + 1; 9325 this._rosganComitenteLookupKey = ''; 9326 } 9327 this.setState({ 9328 rosganComitenteForm: { ...this.state.rosganComitenteForm, cuit, source: '' }, 9329 rosganComitenteLookupLoading: false, 9330 rosganComitenteMessage: null, 9331 }, () => { if (cuit.length === 11) this.lookupRosganComitente(cuit); }); 9332 } 9333 async fetchRosganComitenteLookup(cuit) { 9334 const consignatariaId = String(this.rosganForm().consignatariaId || ''); 9335 const scope = this.rosganLotRelationScope(); 9336 if (!consignatariaId || !scope) throw new Error('Primero seleccioná una consignataria habilitada.'); 9337 let response; 9338 try { 9339 response = await this.apiFetch(`/api/Comitentes/lookup?consignatariaId=${encodeURIComponent(consignatariaId)}&cuit=${encodeURIComponent(cuit)}`); 9340 } catch (error) { 9341 if (Number(error?.status) !== 404) throw error; 9342 } 9343 if (scope !== this.rosganLotRelationScope()) throw new Error('La consignataria cambió. Volvé a consultar el CUIT.'); 9344 const data = response?.data ?? response; 9345 const record = data?.comitente || data?.item || data; 9346 if (data?.found !== false && (record?.id || record?.comitenteId)) { 9347 if (String(record.consignatariaId || '') !== consignatariaId) throw new Error('El comitente recibido no pertenece a la consignataria seleccionada.'); 9348 const recordCuit = String(record.cuitCuil || record.cuitNormalized || record.cuit || '').replace(/\D/g, ''); 9349 if (recordCuit !== cuit) throw new Error('El comitente recibido no coincide con el CUIT consultado.'); 9350 return { data: { ...data, comitente: record, source: 'local', found: true } }; 9351 } 9352 return this.apiFetch(this.roslotCuitLookupEndpoint(cuit)); 9353 } 9354 async lookupRosganComitente(cuit) { 9355 if (!this.canManageComitentes(this.state.apiUser)) return; 9356 const normalized = String(cuit || '').replace(/\D/g, ''); 9357 if (normalized.length !== 11) return; 9358 const lookupKey = normalized; 9359 if (this._rosganComitenteLookupKey === lookupKey && this.state.rosganComitenteLookupLoading) return; 9360 this._rosganComitenteLookupKey = lookupKey; 9361 const token = (this._rosganComitenteLookupToken || 0) + 1; 9362 this._rosganComitenteLookupToken = token; 9363 this.setState({ rosganComitenteLookupLoading: true, rosganComitenteMessage: { kind: 'info', text: 'Consultando CUIT en ARCAâ¦' } }); 9364 try { 9365 const response = await this.fetchRosganComitenteLookup(normalized); 9366 if (token !== this._rosganComitenteLookupToken) return; 9367 const data = response?.data ?? response ?? {}; 9368 if (data?.error) throw new Error(String(data.error)); 9369 const record = data?.comitente || data?.item || data; 9370 const found = data?.found === true || (data?.found !== false && Boolean( 9371 record?.cuitCuil || record?.cuit || record?.cuitNormalized || record?.nombreFiscal || record?.fiscalName || record?.razonSocial || record?.alias || record?.name || record?.nombre 9372 )); 9373 if (!found) { 9374 this.setState({ 9375 rosganComitenteLookupLoading: false, 9376 rosganComitenteForm: { ...this.state.rosganComitenteForm, source: '' }, 9377 rosganComitenteMessage: { kind: 'error', text: 'ARCA no encontró este CUIT. Revisá los 11 dÃgitos.' }, 9378 }); 9379 return; 9380 } 9381 const source = String(data.source || record?.source || '').toLowerCase(); 9382 const current = this.state.rosganComitenteForm || {}; 9383 const provinceOptions = this.state.geoProvinces?.length ?this.state.geoProvinces : ARGENTINA_PROVINCES; 9384 const provinceRaw = String(record.province || record.provincia || '').trim(); 9385 const isCaba = ['caba', 'capitalfederal', 'ciudadautonomabuenosaires', 'ciudadautonomadebuenosaires'].includes(this.consKey(provinceRaw)); 9386 const province = isCaba ? 'Ciudad Autónoma de Buenos Aires' : (provinceOptions.find((item) => this.consKey(item) === this.consKey(provinceRaw)) || provinceRaw); 9387 // ARCA may omit localidad for CABA: province and city are the same jurisdiction. 9388 const locality = String(record.locality || record.localidad || '').trim() || (isCaba ? 'Ciudad Autónoma de Buenos Aires' : ''); 9389 const relationScope = this.rosganLotRelationScope(); 9390 const ownItems = relationScope && this.state.rosganLotRelationsScope === relationScope 9391 ? (this.state.rosganLotComitentes || []) : []; 9392 const localItem = ownItems.find((item) => { 9393 const itemCuit = String(item?.cuitCuil || item?.cuitNormalized || item?.cuit || '').replace(/\D/g, ''); 9394 return itemCuit === normalized; 9395 }); 9396 const existingId = String(localItem?.id || localItem?.comitenteId || ''); 9397 const fiscalName = String(record?.nombreFiscal || record?.fiscalName || record?.razonSocial || localItem?.nombreFiscal || localItem?.fiscalName || current.fiscalName || '').trim(); 9398 const name = fiscalName || String(record?.alias || record?.name || record?.nombre || localItem?.alias || localItem?.name || localItem?.nombre || current.name || '').trim(); 9399 if (province) this.fetchLocalities(province); 9400 if (localItem && existingId) { 9401 this.setState({ 9402 rosganLotForm: { ...this.rosganForm(), comitenteId: existingId, establishmentId: '' }, 9403 rosganComitenteSearch: localItem?.alias || record?.alias || localItem?.name || record?.name || name || fiscalName || normalized, 9404 rosganComitenteMenuOpen: false, 9405 rosganComitenteFormOpen: false, 9406 rosganComitenteLookupLoading: false, 9407 rosganComitenteMessage: null,
9408 rosganComitenteForm: { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' }, 9409 rosganLotMessage: { ok: true, text: 'Comitente existente encontrado y seleccionado.' }, 9410 }); 9411 return; 9412 } 9413 const verifiedSource = source || 'arca'; 9414 this.setState({ 9415 rosganComitenteLookupLoading: false, 9416 rosganComitenteForm: { ...current, cuit: normalized, name, fiscalName, province, locality, source: verifiedSource }, 9417 rosganComitenteMessage: { 9418 kind: name && province && locality ? 'success' : 'warning', 9419 text: name && province && locality 9420 ? (source === 'local' ? 'Comitente encontrado en los registros locales.' : 'Razón social y domicilio fiscal obtenidos de ARCA.') 9421 : 'La consulta terminó, pero no devolvió: ' + [!name && 'razón social', !province && 'provincia fiscal', !locality && 'localidad fiscal'].filter(Boolean).join(', ') + '. Revisá los datos fiscales antes de crear el comitente.', 9422 }, 9423 }); 9424 } catch (error) { 9425 if (token !== this._rosganComitenteLookupToken) return; 9426 const accessDenied = Number(error?.status) === 401 || Number(error?.status) === 403; 9427 const backendMessage = String(error?.data?.message || error?.message || '').trim(); 9428 this.setState({ 9429 rosganComitenteLookupLoading: false, 9430 rosganComitenteForm: { ...this.state.rosganComitenteForm, source: '' }, 9431 rosganComitenteMessage: accessDenied 9432 ?{ kind: 'error', text: backendMessage || 'No tenés permisos para gestionar comitentes.' } 9433 :{ kind: 'error', text: backendMessage || 'ARCA no respondió. Reintentá la consulta.' }, 9434 }); 9435 } 9436 } 9437 onRosganComitenteSearch(value) { 9438 this.bufferInputState('rosgan-comitente-search', { 9439 rosganComitenteSearch: value, 9440 rosganComitenteMenuOpen: true, 9441 rosganLotForm: { ...this.rosganForm(), comitenteId: '', establishmentId: '' }, 9442 }, 100); 9443 } 9444 selectRosganComitente(item) { 9445 const scope = this.rosganLotRelationScope(); 9446 if (!scope || scope !== this.state.rosganLotRelationsScope || this.state.rosganLotRelationsLoading) return; 9447 const id = String(item?.id || item?.comitenteId || ''); 9448 const name = item?.alias || item?.name || item?.nombre || item?.nombreFiscal || item?.fiscalName || item?.razonSocial || id; 9449 clearTimeout(this._rosganComitenteCloseTimer); 9450 // The renderer preserves focused input values while typing. A confirmed 9451 // selection must replace that query with the complete alias immediately. 9452 const input = document.getElementById('rg-comitente-search'); 9453 if (input) input.value = String(name || ''); 9454 this.setState({ 9455 rosganComitenteSearch: String(name || ''), 9456 rosganComitenteMenuOpen: false, 9457 rosganLotForm: { ...this.rosganForm(), comitenteId: id, establishmentId: '' }, 9458 rosganLotMessage: null, 9459 }); 9460 } 9461 openRosganComitenteMenu() { 9462 clearTimeout(this._rosganComitenteCloseTimer); 9463 if (!this.state.rosganComitenteMenuOpen) { 9464 // Do not replace the input during the browser's focus/typing sequence. 9465 this.bufferInputState('rosgan-comitente-search', { rosganComitenteMenuOpen: true }, 100); 9466 } 9467 } 9468 closeRosganComitenteMenu(event) { 9469 const picker = event?.currentTarget?.closest('[data-rg-comitente-picker]'); 9470 if (picker && event.relatedTarget && picker.contains(event.relatedTarget)) return; 9471 clearTimeout(this._rosganComitenteCloseTimer); 9472 this._rosganComitenteCloseTimer = setTimeout(() => { 9473 this._rosganComitenteCloseTimer = null; 9474 if (this.state.rosganComitenteMenuOpen) this.setState({ rosganComitenteMenuOpen: false }); 9475 }, 140); 9476 } 9477 async saveRosganComitente() { 9478 if (this.state.rosganComitenteSaving) return; 9479 if (this.state.rosganComitenteLookupLoading) return; 9480 const scope = this.rosganLotRelationScope(); 9481 const consignatariaId = String(this.rosganForm().consignatariaId || ''); 9482 if (!scope) return; 9483 if (!this.canManageComitentes(this.state.apiUser)) { 9484 this.setState({ rosganComitenteFormOpen: false, rosganLotMessage: { ok: false, text: 'No tenés permisos para gestionar comitentes.' } }); 9485 return; 9486 } 9487 const form = this.state.rosganComitenteForm || {}; 9488 if (!String(form.alias || '').trim()) { this.setState({ rosganComitenteMessage: { ok: false, text: 'Ingresá el al
9488ias del comitente.' } }); return; } 9489 const name = String(form.fiscalName || form.name || form.alias || '').trim(); 9490 const cuit = String(form.cuit || '').replace(/\D/g, ''); 9491 this.setState({ rosganComitenteSaving: true, rosganComitenteMessage: null }); 9492 try { 9493 const created = await this.apiFetch(form.source && cuit ? '/api/Comitentes' : '/api/legacy-comitentes', { 9494 method: 'POST', 9495 headers: { 'X-Rosgan-Catalog': 'true' }, 9496 body: JSON.stringify({ 9497 ...(!form.source || !cuit ? {action:'create_alias'} : {}), 9498 cuit, 9499 consignatariaId, 9500 name, 9501 alias: String(form.alias || '').trim(), 9502 fiscalName: String(form.fiscalName || '').trim(), 9503 province: String(form.province || '').trim(), 9504 locality: String(form.locality || '').trim(), 9505 }), 9506 }); 9507 const refreshed = [...(this.state.rosganLotComitentes || [])]; 9508 if (scope !== this.rosganLotRelationScope()) { this.setState({ rosganComitenteSaving: false }); return; } 9509 const createdId = String(created?.comitente?.id || created?.id || created?.comitenteId || created?.data?.comitente?.id || created?.data?.id || created?.data?.comitenteId || ''); 9510 const record = created?.comitente || created?.data?.comitente || created?.data || created; 9511 const match = record; 9512 const selectedId = createdId; 9513 if (!selectedId) throw new Error('La API no devolvió el ID del comitente creado. No vuelvas a crearlo; actualizá el catálogo.'); 9514 if (!refreshed.some(item => String(item.id || item.comitenteId) === selectedId)) { 9515 refreshed.push({ id: selectedId, consignatariaId, name, alias: String(form.alias || '').trim(), cuit, fiscalName: form.fiscalName, province: form.province, locality: form.locality, ...record, id: selectedId }); 9516 } 9517 this.setState({ 9518 rosganLotComitentes: refreshed, 9519 rosganLotRelationsScope: scope, 9520 rosganLotForm: { ...this.rosganForm(), comitenteId: selectedId, establishmentId: '' }, 9521 rosganComitenteFormOpen: false, 9522 rosganComitenteSaving: false, 9523 rosganComitenteForm: { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' }, 9524 rosganComitenteSearch: String(match?.alias || match?.name || match?.nombre || name), 9525 rosganComitenteMenuOpen: false, 9526 rosganComitenteMessage: null, 9527 rosganLotMessage: { ok: true, text: 'Comitente creado y seleccionado.' }, 9528 }); 9529 } catch (error) { 9530 const detail = error?.data?.errors && typeof error.data.errors === 'object' ?Object.values(error.data.errors).flat().filter(Boolean)[0] : ''; 9531 const message = String(detail || error?.message || 'No se pudo crear el comitente.').replace(/CUIT\s*\/\s*CUIL/gi, 'CUIT').replace(/\s+para esta consignataria\.?/gi, '.'); 9532 this.setState({ rosganComitenteSaving: false, rosganComitenteMessage: { ok: false, text: message } }); 9533 } 9534 } 9535 emptyRosganEstablishmentForm() { 9536 return { nombre: '', provincia: '', localidad: '', renspa: '', latitud: '', longitud: '' }; 9537 } 9538 toggleRosganEstablishmentForm() { 9539 if (!this.canManageComitentes(this.state.apiUser)) { 9540 this.setState({ rosganEstablishmentFormOpen: false, rosganLotMessage: { ok: false, text: 'No tenés permisos para gestionar establecimientos.' } }); 9541 return; 9542 } 9543 if (!this.rosganForm().comitenteId) { 9544 this.setState({ rosganLotMessage: { ok: false, text: 'Primero creá o seleccioná un comitente.' } }); 9545 return; 9546 } 9547 const opening = !this.state.rosganEstablishmentFormOpen; 9548 if (opening) this.fetchProvinces(); 9549 this.setState({ 9550 rosganEstablishmentFormOpen: opening, 9551 rosganEstablishmentForm: opening ?this.emptyRosganEstablishmentForm() : this.state.rosganEstablishmentForm, 9552 rosganEstablishmentMessage: null, rosganEstPlaceResults: [], rosganEstPlaceError: '', rosganEstPlaceLoading: false, 9553 }); 9554 } 9555 setRosganEstablishmentField(key, value) { 9556 const next = { ...(this.state.rosganEstablishmentForm || this.emptyRosganEstablishmentForm()), [key]: value }; 9557 if (key === 'provincia') { 9558 next.localidad = ''; next.latitud = ''; next.longitud = ''; 9559 } 9560 this.setState({ rosganEstablishmentForm: next, rosganEstablishmentMessage: null, ...(key === 'provincia' ?{ rosganEstPlaceResults: [], rosganEstPlaceError: '' } : {}) }); 9561 } 9562 onRosganEstPlaceInput(value) { 9563 const localidad = String(value || ''); 9564 const form = { ...(this.state.rosganEstablishmentForm || this.emptyRosganEstablishmentForm(
9564)), localidad, latitud: '', longitud: '' }; 9565 this.setState({ rosganEstablishmentForm: form, rosganEstablishmentMessage: null, rosganEstPlaceError: '', rosganEstPlaceResults: localidad.trim().length >= 3 ?this.state.rosganEstPlaceResults : [] }); 9566 clearTimeout(this._rosganEstPlaceTimer); 9567 if (!form.provincia || localidad.trim().length < 3) return; 9568 this._rosganEstPlaceTimer = setTimeout(() => this.searchRosganEstPlaces(localidad, form.provincia), 280); 9569 } 9570 async searchRosganEstPlaces(localidad, provincia) { 9571 const query = String(localidad || '').trim(); 9572 if (query.length < 3) return; 9573 const token = (this._rosganEstPlaceToken || 0) + 1; 9574 this._rosganEstPlaceToken = token; 9575 this.setState({ rosganEstPlaceLoading: true, rosganEstPlaceError: '' }); 9576 try { 9577 const response = await this.apiFetch('/api/places/search', { method: 'POST', body: JSON.stringify({ query, provincia: String(provincia || '').trim() }) }); 9578 if (token !== this._rosganEstPlaceToken) return; 9579 const results = Array.isArray(response?.data) ?response.data : this.listFrom(response, ['data', 'items', 'results']); 9580 this.setState({ rosganEstPlaceLoading: false, rosganEstPlaceResults: results.slice(0, 7), rosganEstPlaceError: results.length ?'' : 'No encontramos esa localidad. Podés escribirla manualmente.' }); 9581 } catch { 9582 if (token !== this._rosganEstPlaceToken) return; 9583 this.setState({ rosganEstPlaceLoading: false, rosganEstPlaceResults: [], rosganEstPlaceError: 'Google no respondió. Podés escribir la localidad manualmente.' }); 9584 } 9585 } 9586 selectRosganEstPlace(item) { 9587 if (!item) return; 9588 this.setState({ 9589 rosganEstablishmentForm: { ...(this.state.rosganEstablishmentForm || this.emptyRosganEstablishmentForm()), localidad: String(item.label || item.address || ''), latitud: item.lat != null ?String(item.lat) : '', longitud: item.lng != null ?String(item.lng) : '' }, 9590 rosganEstPlaceResults: [], rosganEstPlaceError: '', rosganEstablishmentMessage: null, 9591 }); 9592 } 9593 closeRosganEstPlaceResults() { 9594 setTimeout(() => this.setState({ rosganEstPlaceResults: [] }), 150); 9595 } 9596 selectRosganEstablishment(establishmentId) { 9597 if (!this.rosganForm().comitenteId) { this.showToast('Primero seleccioná un comitente.'); return; } 9598 this.setState({ rosganEstablishmentMenuOpen:false, rosganEstablishmentSearch:'' }); 9599 const item = (this.state.rosganLotEstablishments || []).find((entry) => String(entry.id || entry.establishmentId || entry.establecimientoId || '') === String(establishmentId || '')); 9600 if (!item) { this.setState({ rosganLotForm: { ...this.rosganForm(), establishmentId: '' } }); return; } 9601 if (item.isActive === false || String(item.comitenteId || '') !== this.rosganSelectedOperationComitenteId()) return; 9602 this.setState({ rosganLotForm: { ...this.rosganForm(), establishmentId:String(item.id) }, rosganLotMessage:null }); 9603 } 9604 async saveRosganEstablishment() { 9605 if (this.state.rosganEstablishmentSaving) return; 9606 const scope = this.rosganLotRelationScope(); 9607 if (!scope) return; 9608 const consignatariaId = String(this.rosganForm().consignatariaId || ''); 9609 const comitenteId = this.rosganSelectedOperationComitenteId(); 9610 const form = this.state.rosganEstablishmentForm || this.emptyRosganEstablishmentForm(); 9611 if (!comitenteId) { this.setState({ rosganEstablishmentMessage: { ok: false, text: 'Primero seleccioná un comitente.' } }); return; } 9612 if (!String(form.nombre || '').trim() || !String(form.provincia || '').trim() || !String(form.localidad || '').trim()) { 9613 this.setState({ rosganEstablishmentMessage: { ok: false, text: 'Completá nombre, provincia y localidad.' } }); return; 9614 } 9615 this.setState({ rosganEstablishmentSaving: true, rosganEstablishmentMessage: null }); 9616 let savedEstablishmentId = ''; 9617 try { 9618 const body = { 9619 consignatariaId, comitenteId, 9620 name: String(form.nombre).trim(), province: String(form.provincia).trim(), locality: String(form.localidad).trim(), 9621 renspa: String(form.renspa || '').trim() || null, location: null, 9622 latitude: this.numOrNull(form.latitud), longitude: this.numOrNull(form.longitud), 9623 }; 9624 const created = await this.apiFetch('/api/rosgan-establishments', { method: 'POST', body: JSON.stringify(body) }); 9625 if (scope !== this.rosganLotRelationScope()) { this.setState({ rosganEstablishmentSaving: false }); return; } 9626 const saved = created?.establishment || created?.establecimiento || created?.data?.establishment || created?.data?.establecimiento || created?.data || created; 9627 const createdId = String(saved?.id || created?.establishmentId || created?.establecimientoId || ''); 9628 const selectedId = createdId; 9629 if (!selectedId) throw new Error('La API no devolvió el ID del establecimiento creado. No vuelvas a crearlo; actualizá el catálogo.'); 9630 savedEstablishmentId = selectedId; 9631 const resolvedComitenteId = String(saved?.comitenteId || comitenteId); 9632 const establishments = [...(this.state.rosganLotEstablishments || []).filter(item => String(item.id) !== selectedId), { ...saved, id: selectedId, consignatariaId, comitenteId: resolvedComitenteId }]; 9633 this.setState({ 9634 rosganLotEstablishments: establishments, 9635 rosganLotComitentes: (this.state.rosganLotComitentes || []).map(item => String(item.id) === String(this.rosganForm().comitenteId) && String(item.consignatariaId) === consignatariaId ? { ...item, operationComitenteId: resolvedComitenteId } : item), 9636 rosganLotPendingComitentes: (this.state.rosganLotPendingComitentes || []).map(item => String(item.id) === String(this.rosganForm().comitenteId) && String(item.consignatariaId) === consignatariaId ? { ...item, operationComitenteId: resolvedComitenteId } : item), 9637 rosganLotForm: { ...this.rosganForm(), establishmentId: selectedId }, 9638 rosganComitenteSearch: '', rosganEstablishmentSearch: '', rosganEstablishmentMenuOpen: false, 9639 rosganLotRelationsScope: scope, 9640 rosganEstablishmentFormOpen: false, rosganEstablishmentSaving: false, 9641 rosganEstablishmentForm: this.emptyRosganEstablishmentForm(
9641), rosganEstablishmentMessage: null, 9642 rosganLotMessage: { ok: true, text: 'Establecimiento creado y seleccionado.' }, 9643 }); 9644 await this.loadRosganLotRelations({ preserve: true }); 9645 } catch (error) { 9646 const detail = error?.data?.errors && typeof error.data.errors === 'object' ?Object.values(error.data.errors).flat().filter(Boolean)[0] : ''; 9647 if (savedEstablishmentId) { 9648 this.setState({ rosganEstablishmentSaving:false, rosganEstablishmentFormOpen:false, rosganComitenteSearch:'', rosganLotMessage:{ ok:false, text:detail || error?.message || 'El establecimiento se creó, pero no se pudo actualizar la lista. No vuelvas a crearlo.' } }); 9649 return; 9650 } 9651 this.setState({ rosganEstablishmentSaving: false, rosganEstablishmentMessage: { ok: false, text: detail || error?.message || 'No se pudo crear el establecimiento.' } }); 9652 } 9653 } 9654 rosganSelectedCategory() { 9655 return (this.state.rosganLotCategories || []).find((item) => String(item.id) === String(this.rosganForm().categoryId)) || null; 9656 } 9657 rosganPositive(value) { const number = Number(value); return Number.isFinite(number) && number > 0 ?number : null; } 9658 rosganShowsConfinement() { 9659 const f = this.rosganForm(); 9660 if (String(this.rosganSelectedCategory()?.formType).toLowerCase() === 'faena') return false; 9661 const option = this.rosganFieldOptions('nutritions', 'nutrition', 'nutricion').find((o) => String(o.value) === String(f.nutrition)); 9662 return ['camporacion', 'camposupl', 'feedlot', 'racion', 'suplementados'].includes(this.consKey(option?.label || f.nutrition)); 9663 } 9664 rosganDescriptionChoices() { 9665 const category = this.rosganSelectedCategory(); 9666 if (String(category?.formType).toLowerCase() === 'faena') return []; 9667 const young = ['inv-terneros', 'inv-terneras', 'inv-terneros-as', 'inv-bubillos', 'inv-bubillas'].includes(category?.id); 9668 const cows = ['inv-vacas-inv', 'vientres-vacas-cria', 'vientres-vacas-cut-cria', 'vientres-vacas-gtia-prenez', 'vientres-vacas-cut-gtia-prenez'].includes(category?.id); 9669 const selected = this.rosganForm().breedIds || []; 9670 const hornedBreed = (this.state.rosganLotBreeds || []).filter(b => selected.includes(String(b.id))).some(b => /braford|hereford|brahman/.test(this.consKey(b.name || b.nombre))); 9671 return this.rosganFieldOptions('lotDescriptions', 'descriptionOptions', 'caracteristicas').filter(o => { 9672 const key = this.consKey(o.label); 9673 if (['alpiedelamadre', 'destetadosas'].includes(key)) return young; 9674 if (['mochosas', 'mochadosas', 'astadosas'].includes(key)) return hornedBreed; 9675 if (key === 'vacias') return cows; 9676 return false; 9677 }); 9678 } 9679 rosganFaenaSubCategoryChoices() { 9680 return this.rosganFieldOptions('faenaSubCategories', 'subCategories', 'subCategory', 'subcategoria'); 9681 } 9682 buildRosganLotPayload() { 9683 const f = this.rosganForm(); 9684 const weighingKey = String(f.weighing || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z]/g, ''); 9685 const weighing = ({ balanzapublica: 'Balanza Pública', enelcampo: 'en el Campo', balanzaferia: 'Balanza Feria' })[weighingKey] || null; 9686 const category = this.rosganSelectedCategory(); 9687 const formType = String(category?.formType || '').toLowerCase(); 9688 const choices = this.rosganDescriptionChoices(); 9689 const selectedDescriptions = (Array.isArray(f.descriptionOptionValues) ?f.descriptionOptionValues : []).map((item) => choices.find((o) => [o.value,o.label].some(v => this.consKey(v) === this.consKey(item)))?.label).filter(Boolean); 9690 const lotDescriptions = [...new Set(selectedDescriptions)]; 9691 const payload = { 9692 consignatariaId: f.consignatariaId, comitenteId: this.rosganSelectedOperationComitenteId(), establishmentId: f.establishmentId, 9693 categoryId: f.categoryId, breedIds: f.breedIds, ...window.RosganPaymentTerm.resolve(f), 9694 quantity: category?.quantityMode === 'total' ? this.rosganPositive(f.quantity) : null, 9695 maleQuantity: category?.quantityMode === 'male_female' && String(f.maleQuantity ?? '').trim() ? Number(f.maleQuantity) : null, 9696 femaleQuantity: category?.quantityMode === 'male_female' && String(f.femaleQuantity ?? '').trim() ? Number(f.femaleQuantity) : null, 9697 motherQuantity: category?.quantityMode === 'mothers_calves' ? this.rosganPositive(f.motherQuantity) : null, 9698 calfQuantity: category?.quantityMode === 'mothers_calves' ? this.rosganPositive(f.calfQuantity) : null, 9699 isTermino: !!category?.allowsTermino && ['inv-terneros', 'inv-terneras', 'inv-terneros-as'].includes(category?.id) && !!f.isTermino, 9700 isAptoChina: !!category?.allowsAptoChina && !!f.isAptoChina, 9701 lotDescriptions, 9702 observations: String(f.description || '').trim() || null, 9703 ageValue: category?.allowsAge ? this.rosganPositive(f.ageValue) : null, ageUnit: category?.allowsAge && this.rosganPositive(f.ageValue) ? f.ageUnit : null, 9704 dentition: category?.allowsDentition ? f.dentition || null : null, 9705 reproductiveStatus: category?.allowsReproductiveStatus ? f.reproductiveStatus || null : null, 9706 destination: category?.allowsDestination ? f.destination || null : null, 9707 bodyCondition: f.bodyCondition || null, animalClass: f.animalClass || null, hasMioMio: typeof f.hasMioMio === 'boolean' ? f.hasMioMio : null, 9708 tickStatus: f.tickStatus || null, health: f.health || null, nutrition: f.nutrition || null, 9709 daysOfConfinement: this.rosganShowsConfinement() ? this.rosganPositive(f.daysOfConfinement) : null, 9710 certifiedBy: String(f.certifiedBy || '').trim() || null, 9711 }; 9712 if (formType === 'invernada') payload.invernada = { 9713 averageWeight: this.rosganPositive(f.averageWeight), maxWeight: this.rosganPositive(f.maxWeight) ?? undefined, minWeight: this.rosganPositive(f.minWeight) ?? undefined, 9714 weighing, desbaste: f.desbaste || null, hasLiquidBrand: !!f.hasLiquidBrand, isAptoVisec: !!f.isAptoVisec, 9715 }; 9716 if (formType === 'vientres') payload.vientres = { service: f.service || null, pregnancy: f.pregnancy || null, calving: f.calving || null }; 9717 if (formType === 'faena') payload.faena = { 9718 subCategory: f.subCategory || null, averageWeight: this.rosganPositive(f.averageWeight), maxWeight: this.rosganPositive(f.maxWeight) ?? undefined, minWeight: this.rosganPositive(f.minWeight) ?? undefined, 9719 conditionType: f.conditionType || null, deliveryType: f.deliveryType || null, 9720 weighing, finishing: f.finishing || null, estimatedYield: String(f.estimatedYield ?? '').trim() ? Number(f.estimatedYield) : undefined, 9721 }; 9722 if (formType === 'faena') { 9723 ['bodyCondition', 'animalClass', 'hasMioMio', 'tickStatus', 'health', 'nutrition', 'daysOfConfinement'].forEach((key) => delete payload[key]); 9724 } 9725 return payload; 9726 } 9727 validateRosganLotDetails() { 9728 const f = this.rosganForm(); const category = this.rosganSelectedCategory(); 9729 const type = String(category?.formType || '').toLowerCase(); 9730 if (!category || category.isActive === false || !['invernada', 'vientres', 'faena'].includes(type)) return 'Seleccioná una categorÃa vigente.'; 9731 if (this.state.rosganLotOptionsLoading || !this.state.rosganLotOptions) return 'Esperá a que se carguen las opciones del formulario e intentá nuevamente.'; 9732 const quantities = category.quantityMode === 'male_female' ? [['maleQuantity', 'cantidad de machos'], ['femaleQuantity', 'cantidad de hembras']] : category.quantityMode === 'mothers_calves' ? [['motherQuantity', 'cantidad de madres'], ['calfQuantity', 'cantidad de crÃas']] : [['quantity', 'cantidad']]; 9733 const integerFields = [...quantities]; 9734 if (category.allowsAge) integerFields.push(['ageValue', 'edad']); 9735 if (type !== 'faena' && this.rosganShowsConfinement()) integerFields.push(['daysOfConfinement', 'dÃas de encierre']); 9736 if (['invernada', 'faena'].includes(type)) integerFields.push(['averageWeight', 'peso promedio'], ['minWeight', 'peso mÃnimo'], ['maxWeight', 'peso máximo']); 9737 for (const [key, label] of integerFields) {
9738 if (String(f[key] ?? '').trim() && (!Number.isInteger(Number(f[key])) || Number(f[key]) < 0 || Number(f[key]) > 2147483647)) return 'El campo ' + label + ' debe ser un número entero válido, sin decimales.'; 9739 } 9740 if (quantities.reduce((sum, [key]) => sum + (Number(f[key]) || 0), 0) > 2147483647) return 'La cantidad total supera el máximo permitido.'; 9741 const required = []; 9742 if (category?.allowsAge && !this.rosganPositive(f.ageValue)) return 'Completá la edad con un valor mayor que cero.'; 9743 if (type !== 'faena') { 9744 required.push(['tickStatus', 'garrapata'], ['bodyCondition', 'estado'], ['animalClass', 'clase'], ['health', 'sanidad'], ['nutrition', 'tratamiento nutricional']); 9745 if (typeof f.hasMioMio !== 'boolean') return 'Indicá Sà o No en MÃo-mÃo.'; 9746 } 9747 if (['invernada', 'faena'].includes(type)) { 9748 if (!this.rosganPositive(f.averageWeight)) return 'Completá el peso promedio con un valor mayor que cero.'; 9749 if (String(f.minWeight ?? '').trim() && (!this.rosganPositive(f.minWeight) || Number(f.minWeight) > Number(f.averageWeight))) return 'El peso mÃnimo debe ser mayor que cero y no superar el peso promedio.'; 9750 if (String(f.maxWeight ?? '').trim() && (!this.rosganPositive(f.maxWeight) || Number(f.maxWeight) < Number(f.averageWeight))) return 'El peso máximo debe ser mayor que cero y no ser menor al peso promedio.'; 9751 required.push(['weighing', 'pesaje']); 9752 } 9753 if (type === 'vientres' && !(category.allowsReproductiveStatus && ['VacÃas', 'Sin servicio'].includes(f.reproductiveStatus))) required.push(['service', 'servicio'], ['pregnancy', 'preñez'], ['calving', 'parición']); 9754 if (type === 'faena') required.push(['subCategory', 'subcategorÃa'], ['destination', 'destino'], ['conditionType', 'condición']); 9755 const missing = required.filter(([key]) => !String(f[key] ?? '').trim()).map(([, label]) => label); 9756 if (missing.length) return 'Completá los campos obligatorios: ' + missing.join(', ') + '.'; 9757 const checks = []; 9758 if (category.allowsAge) checks.push(['ageUnit', 'unidad de edad', 'ageUnits']); 9759 if (category.allowsDentition) checks.push(['dentition', 'dentición', 'dentitions']); 9760 if (category.allowsReproductiveStatus) checks.push(['reproductiveStatus', 'estado reproductivo', type === 'vientres' ? 'vientresReproductiveStatuses' : 'invernadaReproductiveStatuses']); 9761 if (category.allowsDestination) checks.push(['destination', 'destino', type === 'faena' ? 'faenaDestinations' : 'generalDestinations']); 9762 if (type !== 'faena') checks.push(['bodyCondition', 'estado', 'bodyConditions'], ['animalClass', 'clase', 'classes'], ['tickStatus', 'garrapata', 'tickStatuses'], ['health', 'sanidad', 'healthStatuses'], ['nutrition', 'tratamiento nutricional', 'nutritions']); 9763 if (['invernada', 'faena'].includes(type)) checks.push(['weighing', 'pesaje', 'weighings']); 9764 if (type === 'faena') checks.push(['conditionType', 'condición', 'faenaConditionTypes'], ['deliveryType', 'entrega', 'faenaDeliveryTypes'], ['finishing', 'terminación', 'faenaFinishings']); 9765 for (const [key, label, catalog] of checks) { 9766 if (!String(f[key] ?? '').trim()) continue; 9767 if (!this.rosganFieldOptions(catalog).some(o => String(o.label).toLowerCase() === String(f[key]).toLowerCase())) return 'Seleccioná un valor vigente para ' + label + '.'; 9768 } 9769 if (category.allowsAge && !String(f.ageUnit ?? '').trim()) return 'Seleccioná la unidad de edad.'; 9770 if (type === 'faena') { 9771 if (!this.rosganFaenaSubCategoryChoices().some(o => o.label === f.subCategory)) return 'Seleccioná una subcategorÃa de faena válida.'; 9772 if (String(f.estimatedYield ?? '').trim() && (!Number.isFinite(Number(f.estimatedYield)) || Number(f.estimatedYield) < 0 || Number(f.estimatedYield) > 100)) return 'El rendimiento estimado debe estar entre 0 y 100.'; 9773 } 9774 return ''; 9775 } 9776 validateRosganLot() { 9777 const f = this.rosganForm(); const category = this.rosganSelectedCategory(); 9778 if ((f.paymentTermDays || []).includes('other') && !String(f.otherPaymentTermDay || '').trim()) return 'Indicá los dÃas del plazo en Otro.'; 9779 if (f.paymentTermReference === 'other' && !String(f.otherPaymentTermReference || '').trim()) return 'Indicá la referencia del plazo en Otro.'; 9780 const payment = window.RosganPaymentTerm.resolve(f); 9781 if (payment.paymentTermReference && !payment.paymentTermDays.length) return 'Seleccioná un plazo de pago antes de indicar su referencia.'; 9782 if (!this.canManageRosganLots(this.state.apiUser)) return 'Tu cuenta no tiene permiso para guardar lotes Rosgan.'; 9783 if (!this.rosganConsignatariasForRemate(f.remateId).some((item) => String(item.id) === String(f.consignatariaId))) return 'La consignataria no está asignada a tu usuario o no participa en este remate.'; 9784 if (f.weighing && ['invernada', 'faena'].includes(String(category?.formType).toLowerCase()) && !this.buildRosganLotPayload()[String(category.formType).toLowerCase()]?.weighing) return 'Seleccioná un pesaje válido: Balanza Pública, en el Campo o Balanza Feria.'; 9785 if (!f.remateId || !f.consignatariaId || !f.comitenteId || !f.establishmentId || !f.categoryId) return 'Completá remate, consignataria, comitente, establecimiento y categorÃa.'; 9786 const establishment = (this.state.rosganLotEstablishments || []).find(item => String(item.id) === String(f.establishmentId)); 9787 if (!establishment || String(establishment.comitenteId) !== this.rosganSelectedOperationComitenteId()) return 'Seleccioná un establecimiento del alias elegido.'; 9788 if (!Array.isArray(f.breedIds) || !f.breedIds.length) return 'Seleccioná al menos una raza.'; 9789 if (f.breedIds.some(id => !(this.state.rosganLotBreeds || []).some(b => String(b.id) === String(id) && b.isActive !== false))) return 'Seleccioná razas vigentes del catálogo.'; 9790 const breeds = [...new Set(f.breedIds)]; 9791 const breedFamilies = this.rosganBreedFamilies().filter(group => group.key !== '__crosses__' && group.items.some(item => breeds.includes(String(item.id)))); 9792 if (!breedFamilies.length) return 'Seleccioná al menos una raza.'; 9793 if (breedFamilies.length > 3) return 'Podés seleccionar hasta 3 razas.'; 9794 const dairy = ['holando-argentino', 'jersey', 'kiwicross']; 9795 if (breeds.some(id => dairy.includes(id)) && breeds.some(id => !dairy.includes(id))) return 'Holando Argentino, KiwiCross y Jersey no pueden combinarse con otras razas.'; 9796 const mode = String(category?.quantityMode || 'total').toLowerCase(); 9797 if (mode === 'male_female' && (!String(f.maleQuantity ?? '').trim() || !String(f.femaleQuantity ?? '').trim() || Number(f.maleQuantity) < 0 || Number(f.femaleQuantity) < 0 || Number(f.maleQuantity) + Number(f.femaleQuantity) <= 0)) return 'Completá machos y hembras con cero o más; la suma debe ser mayor que cero.'; 9798 if (mode === 'mothers_calves' && (!this.rosganPositive(f.motherQuantity) || !this.rosganPositive(f.calfQuantity))) return 'Completá las cantidades de madres y crÃas.'; 9799 if (!['male_female', 'mothers_calves'].includes(mode) && !this.rosganPositive(f.quantity)) return 'La cantidad debe ser mayor que cero.'; 9800 return this.validateRosganLotDetails(); 9801 } 9802 rosganLotStepComplete(step) { 9803 const f = this.rosganForm(); const category = this.rosganSelectedCategory(); 9804 if (step === 0) { const farm = (this.state.rosganLotEstablishments || []).find(item => String(item.id) === String(f.establishmentId)); return !!(f.remateId && f.consignatariaId && f.comitenteId && farm && String(farm.comitenteId) === this.rosganSelectedOperationComitenteId()); } 9805 if (step === 1) { 9806 if (!f.categoryId || !f.breedIds.length) return false;
9807 const mode = String(category?.quantityMode || 'total').toLowerCase(); 9808 if (mode === 'male_female') return String(f.maleQuantity ?? '').trim() !== '' && String(f.femaleQuantity ?? '').trim() !== '' && Number.isInteger(Number(f.maleQuantity)) && Number.isInteger(Number(f.femaleQuantity)) && Number(f.maleQuantity) >= 0 && Number(f.femaleQuantity) >= 0 && Number(f.maleQuantity) + Number(f.femaleQuantity) > 0; 9809 if (mode === 'mothers_calves') return !!(this.rosganPositive(f.motherQuantity) && this.rosganPositive(f.calfQuantity)); 9810 return !!this.rosganPositive(f.quantity); 9811 } 9812 if (step === 2) return !!category && !this.validateRosganLotDetails(); 9813 return true; 9814 } 9815 rosganLotNext() { 9816 const step = Number(this.state.rosganLotStep || 0); 9817 if (step < 2) { 9818 if (!this.rosganLotStepComplete(step)) { 9819 const messages = ['Completá remate, consignataria, comitente y establecimiento.', 'Elegà categorÃa, raza y cantidades.']; 9820 this.setState({ rosganLotMessage: { ok: false, text: messages[step] } }); 9821 return; 9822 } 9823 this.setState({ rosganLotStep: step + 1, rosganLotMessage: null }); 9824 window.scrollTo({ top: 0, behavior: 'smooth' }); 9825 return; 9826 } 9827 if (step === 2) { this.publishRosganLot(); return; } 9828 if (step === 3) this.finishRosganLotLoad(); 9829 } 9830 resetRosganLotForm() { 9831 const current = this.rosganForm(); 9832 const keep = { remateId: current.remateId, consignatariaId: current.consignatariaId }; 9833 this.setState({ 9834 rosganLotStep: 0, rosganLotForm: { ...this.emptyRosganLotForm(), ...keep }, rosganCreatedLot: null, rosganEditingLot: null, 9835 rosganLotComitentes: keep.consignatariaId ?this.state.rosganLotComitentes : null, 9836 rosganLotEstablishments: keep.consignatariaId ?this.state.rosganLotEstablishments : null, 9837 rosganBreedSearch: '', rosganActiveBreedFamily: '', rosganBreedCrosses: {}, rosganLotMessage: null, 9838 }); 9839 } 9840 rosganLotSaveError(error) { 9841 const errors = error?.data?.errors; 9842 const entries = errors && typeof errors === 'object' ? Object.entries(errors) : []; 9843 const fields = { averageWeight: 'peso promedio', minWeight: 'peso mÃnimo', maxWeight: 'peso máximo', estimatedYield: 'rendimiento estimado', ageValue: 'edad', quantity: 'cantidad', maleQuantity: 'cantidad de machos', femaleQuantity: 'cantidad de hembras', motherQuantity: 'cantidad de madres', calfQuantity: 'cantidad de crÃas', daysOfConfinement: 'dÃas de encierre' }; 9844 const specific = entries.filter(([key]) => key.toLowerCase() !== 'request'); 9845 for (const [key, messages] of specific) { 9846 const field = key.split('.').pop(); 9847 if (fields[field]) return 'Revisá el campo ' + fields[field] + ': la API no aceptó su valor.'; 9848 const detail = (Array.isArray(messages) ? messages : [messages]).find(Boolean); 9849 if (detail) return String(detail); 9850 } 9851 if (entries.length) return 'La API no pudo interpretar los datos del lote. Revisá los campos e intentá nuevamente.'; 9852 return error?.message || 'No se pudo guardar el lote Rosgan.'; 9853 } 9854 async publishRosganLot() { 9855 if (this.state.rosganLotPublishing) return; 9856 const validation = this.validateRosganLot(); 9857 if (validation) { this.setState({ rosganLotMessage: { ok: false, text: validation } }); return; } 9858 const f = this.rosganForm(); 9859 const editing = this.state.rosganEditingLot; 9860 if (editing && (String(editing.remateId) !== String(f.remateId) || String(editing.consignatariaId) !== String(f.consignatariaId))) { 9861 this.setState({ rosganLotMessage: { ok: false, text: 'No se puede cambiar el remate ni la consignataria de un lote existente.' } }); 9862 return; 9863 } 9864 const requestBody = JSON.stringify(this.buildRosganLotPayload()); 9865 this.setState({ rosganLotPublishing: true, rosganLotMessage: null }); 9866 try { 9867 const pendingFarm = (this.state.rosganLotEstablishments || []).find(item => String(item.id) === String(f.establishmentId)); 9868 if (pendingFarm?.pendingComitenteId) await this.apiFetch('/api/legacy-comitentes', {method:'POST',body:JSON.stringify({action:'enable_for_lot',consignatariaId:f.consignatariaId,id:f.comitenteId,establishmentId:f.establishmentId})}); 9869 const endpoint = '/api/remates/' + encodeURIComponent(f.remateId) + '/rosgan-lots' + (editing ?'/' + encodeURIComponent(editing.lotCert) : ''); 9870 const created = await this.apiFetch(endpoint, { method: editing ?'PUT' : 'POST', body: requestBody }); 9871 const cert = editing?.lotCert || created?.lotCert || created?.cert || created?.data?.lotCert || created?.data?.cert || created?.id || created?.data?.id || ''; 9872 const createdLot = { remateId: f.remateId, lotCert: String(cert || ''), status: 'cargado', consignatariaId: f.consignatariaId, response: created }; 9873 if (!cert) { 9874 this.setState({ 9875 rosganLotStep: 3, 9876 rosganLotPublishing: false, 9877 rosganCreatedLot: createdLot, 9878 rosganLotMessage: { 9879 ok: false, 9880 text: 'El lote fue recibido, pero no pudimos completar su registro. No vuelvas a crearlo; contactá al equipo de auditorÃa para revisar el certificado.', 9881 }, 9882 }); 9883 return; 9884 } 9885 this.setState({ rosganLotStep: 3, rosganLotPublishing: false, rosganCreatedLot: createdLot, rosganEditingLot: null, rosganLotMessage: { ok: true, text: (editing ?'Lote actualizado' : 'Lote cargado. Quedó pendiente de control') + ' · Certificado ' + cert + '.' } }, () => { 9886 if (cert) this.fetchRosganLotVideos(f.remateId, cert, true); 9887 }); 9888 window.scrollTo({ top: 0, behavior: 'smooth' }); 9889 } catch (error) { 9890 this.setState({ rosganLotPublishing: false, rosganLotMessage: { ok: false, text: this.rosganLotSaveError(error) } }); 9891 } 9892 } 9893 async openRosganLotEditor(lot = this.state.rdLightbox, remate = this.state.currentRemate, options = {}) { 9894 const duplicate = options.duplicate === true;
9895 if (!lot || (!duplicate && !this.canEditRosganLot(lot))) return; 9896 const remateId = this.remateRouteId(remate); 9897 const lotCert = this.remateLotRouteId(lot); 9898 if (!remateId || !lotCert) return; 9899 try { 9900 const response = await this.apiFetch(duplicate 9901 ? '/api/auditor/remates/' + encodeURIComponent(remateId) + '/lotes/' + encodeURIComponent(lotCert) + '/duplicate' 9902 : '/api/rosgan-lots/' + encodeURIComponent(lotCert) + '/internal'); 9903 const source = { ...(response?.data?.lot || response?.lot || {}), ...(response?.data || response) }; 9904 if ((!duplicate && !this.canEditRosganLot(source)) || String(source.consignatariaId) !== String(lot.consignatariaId)) throw new Error('El lote ya no está disponible para edición.'); 9905 await this.loadRosganLotCatalogs(); 9906 const category = (this.state.rosganLotCategories || []).find((item) => String(item.id) === String(source.categoryId)); 9907 const form = { 9908 ...this.emptyRosganLotForm(), 9909 remateId: String(remateId), consignatariaId: String(source.consignatariaId), 9910 certifiedBy: source.certifiedBy || null, comitenteId: String(source.comitenteId || ''), establishmentId: String(source.establishmentId || ''), 9911 categoryId: String(source.categoryId || ''), formType: String(category?.formType || '').toLowerCase(), 9912 breedIds: (source.breedIds || source.breeds || []).map((entry) => String(entry?.id || entry?.breedId || entry)), 9913 ...window.RosganPaymentTerm.fromLot(source), quantity: source.quantity ?? '', maleQuantity: source.maleQuantity ?? '', femaleQuantity: source.femaleQuantity ?? '', 9914 motherQuantity: source.motherQuantity ?? '', calfQuantity: source.calfQuantity ?? '', 9915 isTermino: !!source.isTermino, isAptoChina: !!source.isAptoChina, 9916 description: source.observations || '', descriptionOptionValues: source.lotDescriptions || [], 9917 ageValue: source.ageValue ?? '', ageUnit: source.ageUnit || 'meses', dentition: source.dentition || '', 9918 reproductiveStatus: source.reproductiveStatus || '', destination: source.destination || '', bodyCondition: source.bodyCondition || '', 9919 animalClass: source.animalClass || '', hasMioMio: typeof source.hasMioMio === 'boolean' ? source.hasMioMio : null, tickStatus: source.tickStatus || '', 9920 health: source.health || '', nutrition: source.nutrition || '', daysOfConfinement: source.daysOfConfinement ?? '', 9921 averageWeight: source.invernada?.averageWeight ?? source.faena?.averageWeight ?? '', 9922 maxWeight: source.invernada?.maxWeight ?? source.faena?.maxWeight ?? '', minWeight: source.invernada?.minWeight ?? source.faena?.minWeight ?? '', 9923 weighing: source.invernada?.weighing || source.faena?.weighing || '', desbaste: source.invernada?.desbaste || '', 9924 hasLiquidBrand: !!source.invernada?.hasLiquidBrand, isAptoVisec: !!source.invernada?.isAptoVisec, 9925 service: source.vientres?.service || '', pregnancy: source.vientres?.pregnancy || '', calving: source.vientres?.calving || '', 9926 subCategory: source.faena?.subCategory || '', conditionType: source.faena?.conditionType || '', 9927 deliveryType: source.faena?.deliveryType || '', finishing: source.faena?.finishing || '', estimatedYield: source.faena?.estimatedYield ?? '', 9928 }; 9929 if (duplicate) { this._auditorPanel?.close(); this._auditorPanel = null; } 9930 this.setState({ rosganComitenteSearch: null, rosganEstablishmentSearch: '', rosganCertifierMenuOpen: false }); 9931 this.setState({ route: 'rosganLotLoad', rosganLotStep: 0, rosganLotForm: form, rosganCreatedLot: null, rosganEditingLot: duplicate ? null : { remateId, lotCert, consignatariaId: form.consignatariaId }, rosganLotMessage: duplicate ? { ok: true, text: 'Datos copiados del certificado ' + lotCert + '. Revisá y editá los datos; al guardar se creará un certificado nuevo en estado Cargado.' } : null }, () => this.loadRosganLotRelations()); 9932 this.syncAppLocation('rosganLotLoad'); 9933 window.scrollTo(0, 0); 9934 } catch (error) { this.showToast(error?.message || 'No se pudo abrir el lote para editar.'); } 9935 } 9936 finishRosganLotLoad() { 9937 const created = this.state.rosganCreatedLot; 9938 if (!created) return;
9939 const key = created.lotCert ?this.rosganLotVideoKey(created.remateId, created.lotCert) : ''; 9940 const uploads = key ?(this.state.rosganLotVideoState?.[key]?.uploads || []) : []; 9941 if (key && this.state.rosganLotVideoState?.[key]?.uploading) return; 9942 const f = this.rosganForm(); 9943 const keep = { remateId: f.remateId, consignatariaId: f.consignatariaId, comitenteId: f.comitenteId, establishmentId: f.establishmentId }; 9944 this.setState({ 9945 rosganLotStep: 0, rosganCreatedLot: null, rosganLotPublishing: false, 9946 rosganLotForm: { ...this.emptyRosganLotForm(), ...keep }, rosganBreedSearch: '', rosganActiveBreedFamily: '', rosganBreedCrosses: {}, 9947 rosganLotMessage: { ok: true, text: 'Lote Rosgan cargado para control' + (created.lotCert ?' · Certificado ' + created.lotCert : '') + '.' }, 9948 }); 9949 window.scrollTo({ top: 0, behavior: 'smooth' }); 9950 } 9951 INCREMENTS = [20, 50]; 9952 CATALOGS = { 9953 categories: [['macho-entero-joven','Macho Entero Joven (MEJ)','male'],['novillitos','Novillitos (NT)','male'],['novillitos-vaquillonas','Novillitos (NT) y Vaquillonas (VQ)','mixed'],['novillos','Novillos (NO)','male'],['toros','Toros (TO)','male'],['vacas','Vacas (VA)','female'],['vaquillonas','Vaquillonas (VQ)','female']], 9954 breeds: [['aberdeen-angus-negro','Angus Negro'],['aberdeen-angus-colorado','Angus Colorado'],['aberdeen-angus-negro-colorado','Angus Negro y Colorado'],['brangus-negro','Brangus Negro'],['brangus-colorado','Brangus Colorado'],['hereford','Hereford'],['braford','Braford'],['charolais','Charolais'],['holando-argentino','Holando Argentino'],['shorthorn','Shorthorn'],['limousin','Limousin'],['limangus','Limangus'],['careta','Careta'],['media-sangre','Media Sangre'],['bubalinos','Bubalinos'],['bonsmara','Bonsmara'],['jersey','Jersey'],['brahman','Brahman'],['san-ignacio','San Ignacio'],['galloway','Galloway']], 9955 fatteningTypes: [['a-campo','A campo'],['racion','Ración'],['feedlot','Feedlot']], 9956 destinations: [['consumo','Consumo'],['exportacion-hilton','Export. Hilton'],['exportacion-ue','Export. UE'],['cuota-481','Cuota 481']], 9957 saleConditions: [['la-carne-a-levantar','La carne a levantar'],['kg-vivo-a-levantar','Kg vivo a levantar']], 9958 weighingOptions: [['balanza-publica','Balanza pública'],['en-el-campo','En el campo'],['balanza-feria','Balanza feria']], 9959 termOptions: [['contado','Contado'],['7-dias','7 dÃas'],['14-dias','14 dÃas'],['21-dias','21 dÃas'],['30-dias','30 dÃas'],['otro','Otro']], 9960 subcategories: [['especial','Especial',['novillos','novillitos','vaquillonas','mej']],['gordo-a','Gordo/a',['novillos','novillitos','vaquillonas','mej']],['regular','Regular',['vacas','toros']],['cons-buena','Cons. Buena',['vacas','toros']],['cons-inferior','Cons. Inferior',['vacas','toros']]], 9961 }; 9962 WIZ_META = [ 9963 { title: 'CategorÃa', text: 'Datos, razas y cantidad' }, 9964 { title: 'Destino y pesos', text: 'Engorde, China y kilos' }, 9965 { title: 'Comercial', text: 'Precio, plazo y carga' }, 9966 { title: 'Videos', text: 'Material y portada' }, 9967 ]; 9968 emptyForm() { return { estId:'', estName:'', comitente:'', fecha:'', prov:'', loc:'', lat:'', lon:'', cat:'', subcat:'', cant:'', cantMachos:'', cantHembras:'', razas:[], engorde:'', diasEncierre:'', destino:'', china:'', pesoProm:'', pesoMin:'', pesoMax:'', cond:'', precio:'', listoParaCargar:false, dispo:'', desbaste:'', rinde:'', pesaje:'', plazo:'', plazoOtro:'', obs:'', videos:[null,null,null], videoPosters:[0,0,0], thumbIdx:0 }; } 9969 form() { return this.state.form || this.emptyForm(); } 9970 videoPoster(i) { 9971 const video = this.form().videos[i]; 9972 return video?.selectedFrame?.previewUrl || video?.frames?.[0]?.previewUrl || ''; 9973 } 9974 startVideoUpload(i) { 9975 if (this.state.vidProg[i] != null) return; 9976 this.openVideoPicker(i, false); 9977 } 9978 openVideoPicker(startIndex = 0, multiple = false) { 9979 const input = document.createElement('input'); 9980 input.type = 'file'; 9981 input.accept = 'video/*,.mp4,.mov,.webm,.m4v'; 9982 input.multiple = multiple; 9983 input.style.display = 'none'; 9984 input.addEventListener('change', async () => { 9985 const files = Array.from(input.files || []); 9986 input.remove(); 9987 if (files.length) await this.handleVideoFiles(files, startIndex); 9988 }, { once: true }); 9989 document.body.appendChild(input); 9990 input.click(); 9991 }
9992 captureVideoFrame(video, time) { 9993 return new Promise((resolve) => { 9994 const canvas = document.createElement('canvas'); 9995 const width = video.videoWidth || 1280; 9996 const height = video.videoHeight || 720; 9997 canvas.width = 640; 9998 canvas.height = Math.max(1, Math.round((height / width) * canvas.width)); 9999 const handleSeeked = () => { 10000 canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height); 10001 canvas.toBlob((blob) => resolve(blob ?{ blob, previewUrl: URL.createObjectURL(blob) } : null), 'image/jpeg', 0.86); 10002 }; 10003 video.addEventListener('seeked', handleSeeked, { once: true }); 10004 video.currentTime = Math.min(Math.max(time, 0), Math.max(video.duration - 0.1, 0)); 10005 }); 10006 } 10007 extractVideoFrames(objectUrl) { 10008 return new Promise((resolve, reject) => { 10009 const video = document.createElement('video'); 10010 video.preload = 'metadata'; 10011 video.muted = true; 10012 video.playsInline = true; 10013 video.src = objectUrl; 10014 video.addEventListener('loadedmetadata', async () => { 10015 try { 10016 const duration = Number.isFinite(video.duration) && video.duration > 0 ?video.duration : 3; 10017 const frames = []; 10018 for (const time of [duration * 0.2, duration * 0.5, duration * 0.8]) { 10019 const frame = await this.captureVideoFrame(video, time); 10020 if (frame) frames.push(frame); 10021 } 10022 video.removeAttribute('src'); 10023 video.load(); 10024 resolve(frames); 10025 } catch (error) { 10026 reject(error); 10027 } 10028 }, { once: true }); 10029 video.addEventListener('error', () => reject(new Error('No se pudieron leer los frames del video.')), { once: true }); 10030 }); 10031 } 10032 async handleVideoFile(i, file) { 10033 if (!file) return; 10034 const isVideo = file.type?.startsWith('video/') || /\.(mp4|mov|webm|m4v)$/i.test(file.name || ''); 10035 if (!isVideo) throw new Error('Seleccioná un archivo de video MP4, MOV o WebM.'); 10036 const previous = this.form().videos[i]; 10037 const hadVideos = this.form().videos.some(Boolean); 10038 if (previous?.objectUrl) URL.revokeObjectURL(previous.objectUrl); 10039 (previous?.frames || []).forEach((frame) => frame?.previewUrl && URL.revokeObjectURL(frame.previewUrl)); 10040 const objectUrl = URL.createObjectURL(file); 10041 const videos = [...this.form().videos]; 10042 videos[i] = { file, fileName: file.name, contentType: file.type || 'application/octet-stream', fileSize: file.size, objectUrl, frames: [], selectedFrame: null }; 10043 this.setState({ 10044 form: { ...this.form(), videos, thumbIdx: hadVideos ?this.form().thumbIdx : i }, 10045 vidProg: { ...this.state.vidProg, [i]: 12 }, 10046 formMessage: { text: 'Generando portadas del video ' + (i + 1) + '...', ok: true }, 10047 }); 10048 try { 10049 const frames = await this.extractVideoFrames(objectUrl); 10050 if (!frames.length) throw new Error('No se pudieron generar portadas para el video.'); 10051 const currentVideos = [...this.form().videos]; 10052 currentVideos[i] = { ...currentVideos[i], frames, selectedFrame: frames[Math.floor(frames.length / 2)] || frames[0] }; 10053 const prog = { ...this.state.vidProg }; delete prog[i]; 10054 this.setState({ form: { ...this.form(), videos: currentVideos }, vidProg: prog, formMessage: null }); 10055 } catch (error) { 10056 this.removeVideo(i); 10057 this.setState({ formMessage: { text: error.message || 'No se pudo procesar el video.', ok: false } }); 10058 } 10059 } 10060 async handleVideoFiles(files, startIndex = 0) { 10061 const selected = files.filter((file) => file?.type?.startsWith('video/') || /\.(mp4|mov|webm|m4v)$/i.test(file?.name || '')).slice(0, 3); 10062 if (!selected.length) { 10063 this.setState({ formMessage: { text: 'Seleccioná archivos de video MP4, MOV o WebM.', ok: false } }); 10064 return; 10065 } 10066 const videos = this.form().videos; 10067 const available = videos.map((video, index) => ({ video, index })) 10068 .filter(({ video, index }) => index >= startIndex && !video) 10069 .map(({ index }) => index); 10070 const targetSlots = available.length ?available : (startIndex < 3 ?[startIndex] : []); 10071 if (files.length > Math.min(targetSlots.length, 3)) { 10072 this.setState({ formMessage: { text: 'Solo se pueden cargar hasta 3 videos por lote.', ok: false } }); 10073 } 10074 for (let index = 0; index < Math.min(selected.length, targetSlots.length, 3); index += 1) {
10075 await this.handleVideoFile(targetSlots[index], selected[index]); 10076 } 10077 } 10078 removeVideo(i) { 10079 const removed = this.form().videos[i]; 10080 if (removed?.objectUrl) URL.revokeObjectURL(removed.objectUrl); 10081 (removed?.frames || []).forEach((frame) => frame?.previewUrl && URL.revokeObjectURL(frame.previewUrl)); 10082 const prog = { ...this.state.vidProg }; delete prog[i]; 10083 const nv = [...this.form().videos]; nv[i] = null; 10084 const thumbIdx = this.form().thumbIdx === i ?(nv.findIndex(Boolean) >= 0 ?nv.findIndex(Boolean) : 0) : this.form().thumbIdx; 10085 const deletedIds = removed?.existing && removed?.id 10086 ? Array.from(new Set([...(this.state.editDeletedVideoIds || []), String(removed.id)])) 10087 : (this.state.editDeletedVideoIds || []); 10088 this.setState({ vidProg: prog, form: { ...this.form(), videos: nv, thumbIdx }, editDeletedVideoIds: deletedIds, formMessage: null }); 10089 } 10090 setVideoPoster(i, p) { 10091 const videos = [...this.form().videos]; 10092 if (!videos[i]?.frames?.[p]) return; 10093 videos[i] = { ...videos[i], selectedFrame: videos[i].frames[p] }; 10094 this.setF('videos', videos); 10095 } 10096 uploadAllVideos() { this.openVideoPicker(0, true); } 10097 filmAssistedVideo() { window.RosganFilmingAssistant?.open(file => this.handleVideoFiles([file], 0)); } 10098 setF(key, val, buffered = false) { 10099 const form = { ...this.form(), [key]: val }; 10100 if (key === 'listoParaCargar' && val === true) form.dispo = ''; 10101 if (key === 'engorde' && (val === 'racion' || val === 'feedlot') && form.destino === 'exportacion-hilton') { 10102 form.destino = ''; 10103 } 10104 const patch = { form, formMessage: null }; 10105 if (buffered) this.bufferInputState('lot-form-' + key, patch); 10106 else this.setState(patch); 10107 } 10108 loadDateOffset(isoDate, days) { 10109 const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(isoDate || '')); 10110 if (!match) return ''; 10111 const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))); 10112 if (date.toISOString().slice(0, 10) !== isoDate) return ''; 10113 date.setUTCDate(date.getUTCDate() + days); 10114 return date.toISOString().slice(0, 10); 10115 } 10116 loadAvailabilityError(fechaSubasta, listoParaCargar, fechaDisponibleCarga) { 10117 const min = this.loadDateOffset(fechaSubasta, 1); 10118 if (!min) return 'Elegà una fecha de subasta válida.'; 10119 if (listoParaCargar === true) return ''; 10120 const max = this.loadDateOffset(fechaSubasta, 7); 10121 if (!this.loadDateOffset(fechaDisponibleCarga, 0)) return 'Elegà la fecha disponible de carga.'; 10122 if (fechaDisponibleCarga < min || fechaDisponibleCarga > max) return 'La carga debe estar disponible entre 1 y 7 dÃas corridos después de la subasta.'; 10123 return ''; 10124 } 10125 toggleRaza(id) { 10126 const f = this.form(); const has = f.razas.includes(id); 10127 if (!has && f.razas.length >= 3) return; 10128 this.setF('razas', has ?f.razas.filter((r) => r !== id) : [...f.razas, id]); 10129 } 10130 catName(id) { return (this.CATALOGS.categories.find((c) => c[0] === id) || [null, ''])[1]; } 10131 catYieldPct(id) { 10132 const raw = (this.CATALOGS.categories.find((c) => String(c[0]) === String(id)) || [null, '', '', null])[3]; 10133 const value = Number(raw); 10134 if (!Number.isFinite(value) || value <= 0) return ''; 10135 return String(Math.round(value * 10000) / 100); 10136 } 10137 subcatsFor(catId) { return (this.CATALOGS.subcategories || []).filter((s) => !catId || (s[2] || []).includes(catId)); } 10138 subcatName(id) { return (this.CATALOGS.subcategories.find((s) => s[0] === id) || [null, ''])[1]; } 10139 async loadCatalog() { 10140 try { 10141 const d = await this.apiFetch('/api/Catalogs/lot-form'); 10142 const srt = (a) => (a || []).slice().sort((x, y) => (x.sortOrder || 0) - (y.sortOrder || 0)); 10143 const c = {}; 10144 if (d.categories?.length) c.categories = srt(d.categories).map((x) => [x.id, x.name, x.sexType, x.meatYieldPercentage ?? null]); 10145 if (d.breeds?.length) c.breeds = srt(d.breeds).map((x) => [x.id, x.name]); 10146 if (d.feedingTypes?.length) c.fatteningTypes = srt(d.feedingTypes).map((x) => [x.id, x.name]); 10147 if (d.destinations?.length) c.destinations = srt(d.destinations).map((x) => [x.id, x.name]); 10148 if (d.conditions?.length) c.saleConditions = srt(d.conditions).map((x) => [x.id, x.name]); 10149 if (d.weighingTypes?.length) c.weighingOptions = srt(d.weighingTypes).map((x) => [x.id, x.name]); 10150 if (d.paymentTerms?.length) c.termOptions = srt(d.paymentTerms).map((x) => [x.id, x.name]); 10151 if (d.subcategories?.length) c.subcategories = srt(d.subcategories).map((x) => [x.id, x.name, x.categoryIds || []]); 10152 this.CATALOGS = { ...this.CATALOGS, ...c }; 10153 this.forceUpdate(); 10154 } catch (e) { console.warn('catalog', e); } 10155 } 10156 async loadLotYieldCategories(force = false, user = this.state.apiUser) { 10157 if (!this.isSysadmin(user) || this.state.lotYieldLoading) return; 10158 if (!force && Array.isArray(this.state.lotYieldCategories)) return; 10159 this.setState({ lotYieldLoading: true, lotYieldError: '', lotYieldMessage: null }); 10160 try { 10161 const data = await this.apiFetch('/api/catalogs/lot-categories'); 10162 const source = Array.isArray(data) ?data : Array.isArray(data?.data) ?data.data : Array.isArray(data?.items) ?data.items : []; 10163 const categories = source 10164 .slice() 10165 .sort((a, b) => (Number(a.sortOrder) || 0) - (Number(b.sortOrder) || 0)) 10166 .map((category) => { 10167 const decimal = Number(category.meatYieldPercentage); 10168 return { 10169 ...category, 10170 uiPercent: Number.isFinite(decimal) && decimal >
10170 0 ?String(Math.round(decimal * 10000) / 100) : '', 10171 }; 10172 }); 10173 this.setState({ lotYieldCategories: categories, lotYieldLoading: false, lotYieldError: '' }); 10174 } catch (error) { 10175 this.setState({ lotYieldLoading: false, lotYieldError: error?.data?.detail || error?.data?.message || error?.message || 'No pudimos cargar los rendimientos por categorÃa.' }); 10176 } 10177 } 10178 setLotYieldPercent(categoryId, rawValue) { 10179 const normalized = String(rawValue ?? '').replace(',', '.').replace(/[^\d.]/g, ''); 10180 const firstDot = normalized.indexOf('.'); 10181 const value = firstDot < 0 ?normalized : normalized.slice(0, firstDot + 1) + normalized.slice(firstDot + 1).replace(/\./g, ''); 10182 this.setState({ 10183 lotYieldCategories: (this.state.lotYieldCategories || []).map((category) => String(category.id) === String(categoryId) ?{ ...category, uiPercent: value } : category), 10184 lotYieldMessage: null, 10185 }); 10186 } 10187 async saveLotYieldCategory(categoryId) { 10188 if (!this.isSysadmin() || this.state.lotYieldSavingId) return; 10189 const current = (this.state.lotYieldCategories || []).find((category) => String(category.id) === String(categoryId)); 10190 if (!current) return; 10191 const uiPercent = Number(String(current.uiPercent || '').replace(',', '.')); 10192 if (!Number.isFinite(uiPercent) || uiPercent < 1 || uiPercent > 100) { 10193 this.setState({ lotYieldMessage: { ok: false, text: 'Ingresá un rendimiento entre 1% y 100%.' } }); 10194 return; 10195 } 10196 this.setState({ lotYieldSavingId: String(categoryId), lotYieldMessage: null }); 10197 try { 10198 const response = await this.apiFetch('/api/catalogs/lot-categories/' + encodeURIComponent(categoryId) + '/meat-yield', { 10199 method: 'PATCH', 10200 body: JSON.stringify({ meatYieldPercentage: uiPercent / 100 }), 10201 }); 10202 const updated = response?.category || response?.data?.category || { ...current, meatYieldPercentage: uiPercent / 100 }; 10203 const decimal = Number(updated.meatYieldPercentage); 10204 const nextUiPercent = Number.isFinite(decimal) ?String(Math.round(decimal * 10000) / 100) : String(uiPercent); 10205 this.CATALOGS = { 10206 ...this.CATALOGS, 10207 categories: (this.CATALOGS.categories || []).map((item) => String(item[0]) === String(categoryId) ?[item[0], updated.name || item[1], updated.sexType || item[2], Number.isFinite(decimal) ?decimal : uiPercent / 100] : item), 10208 }; 10209 this.setState({ 10210 lotYieldCategories: (this.state.lotYieldCategories || []).map((category) => String(category.id) === String(categoryId) ?{ ...category, ...updated, uiPercent: nextUiPercent } : category), 10211 lotYieldSavingId: '', 10212 lotYieldMessage: { ok: true, text: response?.message || 'Rendimiento actualizado correctamente.' }, 10213 }); 10214 } catch (error) { 10215 this.setState({ lotYieldSavingId: '', lotYieldMessage: { ok: false, text: error?.data?.detail || error?.data?.message || error?.message || 'No pudimos guardar el rendimiento.' } }); 10216 } 10217 } 10218 catSex(id) { return (this.CATALOGS.categories.find((c) => c[0] === id) || [null, '', ''])[2]; } 10219 isMixedCat(id) { return this.catSex(id) === 'mixed'; } 10220 labelOf(cat, id) { return (this.CATALOGS[cat].find((c) => c[0] === id) || [null, ''])[1]; } 10221 numOrNull(v) { const s = String(v || '').trim(); if (s === '') return null; const n = Number(s); return Number.isFinite(n) ?n : null; } 10222 clampPct(v, max) { const n = this.numOrNull(v); if (n === null) return null; return Math.max(0, Math.min(max, n)); } 10223 totalHeads(f) { 10224 if (this.isMixedCat(f.cat)) { 10225 const t = this.numOrNull(f.cant); 10226 if (t) return t; 10227 return (this.numOrNull(f.cantMachos) || 0) + (this.numOrNull(f.cantHembras) || 0); 10228 } 10229 return this.numOrNull(f.cant) || 0; 10230 } 10231 estComplete(f) { 10232 return !!f.estId; 10233 } 10234 loadStepComplete(i) { 10235 const f = this.form(); 10236 if (i === 0) { 10237 const razasOk = f.razas.length >= 1 && f.razas.length <= 3; 10238 const cantOk = this.isMixedCat(f.cat) 10239 ? ((this.numOrNull(f.cantMachos) || 0) + (this.numOrNull(f.cantHembras) || 0)) > 0 10240 : (this.numOrNull(f.cant) || 0) > 0; 10241 return !!(this.estComplete(f) && f.cat && f.fecha && razasOk && cantOk); 10242 } 10243 if (i === 1) return !!(f.engorde && f.destino && f.china !== '' && this.numOrNull(f.pesoProm) !== null); 10244 if (i === 2) return !!(this.numOrNull(f.precio) !== null && f.cond && f.pesaje && f.plazo && (f.plazo !== 'otro' || String(f.plazoOtro || '').trim()) && !this.loadAvailabilityError(f.fecha, f.listoParaCargar, f.dispo)); 10245 if (i === 3) return f.videos.some(Boolean); 10246 return false;
10247 } 10248 formCompleteness() { 10249 const f = this.form(); 10250 const keys = ['estName', 'prov', 'loc', 'fecha', 'cat', 'engorde', 'destino', 'china', 'pesoProm', 'cond', 'precio', 'pesaje', 'plazo']; 10251 let done = keys.filter((k) => String(f[k] || '').length).length; 10252 if (this.estComplete(f)) done += 1; 10253 if (f.razas.length) done += 1; 10254 if (this.totalHeads(f) > 0) done += 1; 10255 if (f.videos.some(Boolean)) done += 1; 10256 return Math.round((done / (keys.length + 4)) * 100); 10257 } 10258 loadNext() { 10259 const step = this.state.loadStep; 10260 if (step < 3) { 10261 if (step === 2) { 10262 const f = this.form(); 10263 const availabilityError = this.loadAvailabilityError(f.fecha, f.listoParaCargar, f.dispo); 10264 if (availabilityError) { this.setState({ formMessage: { text: availabilityError, ok: false } }); return; } 10265 } 10266 if (!this.loadStepComplete(step)) { this.setState({ formMessage: { text: 'Completá los campos obligatorios de esta etapa.', ok: false } }); return; } 10267 this.setState({ loadStep: step + 1, formMessage: null }); return; 10268 } 10269 this.publishLot(); 10270 } 10271 buildLotPayload(f) { 10272 const mixed = this.isMixedCat(f.cat); 10273 const engordeCampo = f.engorde === 'a-campo'; 10274 const p = { 10275 fechaSubasta: f.fecha, 10276 categoriaId: f.cat, 10277 subcategoriaId: f.subcat || f.subcategoriaId || '', 10278 razaIds: f.razas, 10279 cantidadTotal: mixed ?null : this.totalHeads(f), 10280 engordeId: f.engorde, 10281 destinoId: f.destino, 10282 aptoChina: f.china === 'true', 10283 pesoPromedio: this.numOrNull(f.pesoProm), 10284 pesoMinimo: this.numOrNull(f.pesoMin), 10285 pesoMaximo: this.numOrNull(f.pesoMax), 10286 listoParaCargar: f.listoParaCargar === true, 10287 fechaDisponibleCarga: f.listoParaCargar === true ?null : f.dispo, 10288 observaciones: f.obs, 10289 condicionId: f.cond, 10290 precioSugerido: this.numOrNull(f.precio), 10291 pesajeId: f.pesaje, 10292 desbaste: this.clampPct(f.desbaste, 100), 10293 rendimientoEstimado: this.clampPct(f.rinde, 100), 10294 plazoId: f.plazo, 10295 plazoOtro: f.plazo === 'otro' ?(String(f.plazoOtro || '').trim() || null) : null, 10296 establecimientoId: f.estId || null, 10297 }; 10298 if (mixed) { 10299 p.cantidadMachos = this.numOrNull(f.cantMachos) || 0; 10300 p.cantidadHembras = this.numOrNull(f.cantHembras) || 0; 10301 } 10302 if (!engordeCampo) p.diasEncierre = this.numOrNull(f.diasEncierre) || 0; 10303 return p; 10304 } 10305 blobToDataUrl(blob) { 10306 return new Promise((resolve, reject) => { 10307 const reader = new FileReader(); 10308 reader.onload = () => resolve(reader.result || ''); 10309 reader.onerror = () => reject(new Error('No se pudo preparar la portada del video.')); 10310 reader.readAsDataURL(blob); 10311 }); 10312 } 10313 createdLotId(data) { 10314 return data?.cert || data?.lot?.cert || data?.id || data?.lot?.id 10315 || data?.lotId || data?.lot?.lotId || data?.certificate || data?.lot?.certificate || ''; 10316 } 10317 async uploadSignedFile(uploadUrl, file, onProgress) { 10318 try { 10319 await new Promise((resolve, reject) => { 10320 const xhr = new XMLHttpRequest(); 10321 xhr.open('PUT', uploadUrl); 10322 xhr.withCredentials = false; 10323 xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream'); 10324 xhr.upload.onprogress = (event) => { 10325 if (event.lengthComputable && onProgress) onProgress(Math.round((event.loaded / event.total) * 100)); 10326 }; 10327 xhr.onload = () => xhr.status >= 200 && xhr.status < 300 10328 ? resolve() 10329 : reject(new Error('No se pudo subir el archivo (HTTP ' + xhr.status + ').')); 10330 xhr.onerror = () => reject(Object.assign(new Error('No se pudo conectar con el almacenamiento de videos.'), { networkError: true })); 10331 xhr.send(file); 10332 }); 10333 } catch (error) { 10334 if (!error?.networkError || !['localhost', '127.0.0.1'].includes(window.location.hostname)) throw error; 10335 await this.uploadSignedFileViaLocalProxy(uploadUrl, file, onProgress); 10336 } 10337 } 10338 async uploadSignedFileViaLocalProxy(uploadUrl, file, onProgress) { 10339 const token = await (await this.ensureFirebase()).auth().currentUser?.getIdToken(); 10340 if (!token) throw new Error('La sesión venció. Volvé a ingresar antes de subir el video.'); 10341 return new Promise((resolve, reject) => { 10342 const xhr = new XMLHttpRequest(); 10343 xhr.open('POST', '/api/signed-upload'); 10344 xhr.setRequestHeader('Authorization', 'Bearer ' + token); 10345 xhr.setRequestHeader('X-Upload-Url', uploadUrl); 10346 xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream'); 10347 xhr.upload.onprogress = (event) => { 10348 if (event.lengthComputable && onProgress) onProgress(Math.round((event.loaded / event.total) * 100)); 10349 }; 10350 xhr.onload = () => { 10351 if (xhr.status >= 200 && xhr.status < 300) { resolve(); return; } 10352 let message = ''; 10353 try { message = JSON.parse(xhr.responseText || '{}')?.message || ''; } catch {} 10354 reject(new Error(message || 'No se pudo subir el archivo mediante el servidor local (HTTP ' + xhr.status + ').')); 10355 }; 10356 xhr.onerror = () => reject(new Error('Tampoco se pudo conectar con el servidor local de carga.')); 10357 xhr.send(file); 10358 }); 10359 } 10360 async apiFetchWithFallback(primary, fallback, opts) { 10361 try { 10362 return await this.apiFetch(primary, opts); 10363 } catch (error) { 10364 if (![404, 405].includes(error?.status)) throw error; 10365 return this.apiFetch(fallback, opts); 10366 } 10367 } 10368 async uploadVideoAsset(lotId, video, slotIndex, completedSteps, totalSteps) { 10369 const uploadData = await this.apiFetch('/api/lots/' + encodeURIComponent(lotId) + '/videos/upload-url', { 10370 method: 'POST', 10371 body: JSON.stringify({ 10372 fileName: video.fileName, 10373 contentType: video.file?.type || 'application/octet-stream', 10374 sizeBytes: video.fileSize, 10375 }), 10376 }); 10377 const uploadUrl = uploadData?.uploadUrl || uploadData?.signedUrl; 10378 if (!uploadUrl) throw new Error('La API no devolvió una URL para subir el video ' + (slotIndex + 1) + '.'); 10379 await this.uploadSignedFile(uploadUrl, video.file, (progress) => { 10380 this.setState({ 10381 vidProg: { ...this.state.vidProg, [slotIndex]: progress }, 10382 formMessage: { text: 'Subiendo video ' + (completedSteps + 1) + ' de ' + totalSteps + ' · ' + progress + '%', ok: true }, 10383 }); 10384 }); 10385 const poster = video.selectedFrame?.blob 10386 ? await this.blobToDataUrl(video.selectedFrame.blob) 10387 : (video.selectedFrame?.previewUrl || ''); 10388 await this.apiFetchWithFallback( 10389 '/api/lots/' + encodeURIComponent(lotId) + '/videos/metadata', 10390 '/api/lots/' + encodeURIComponent(lotId) + '/videos/complete', 10391 { 10392 method: 'POST', 10393 body: JSON.stringify({ 10394 fileKey: uploadData.fileKey || uploadData.key, 10395 url: uploadData.publicUrl || uploadData.url, 10396 fileName: video.fileName, 10397 contentType: video.file?.type || 'application/octet-stream', 10398 sizeBytes: video.fileSize, 10399 thumbnailUrl: poster, 10400 posterUrl: poster, 10401 }), 10402 }, 10403 ); 10404 const progress = { ...this.state.vidProg }; 10405 delete progress[slotIndex]; 10406 this.setState({ vidProg: progress }); 10407 } 10408 async uploadCoverAsset(lotId, selectedFrame, completedSteps, totalSteps) { 10409 if (!selectedFrame?.blob) return; 10410 const coverFile = new File([selectedFrame.blob], 'lot-' + lotId + '-cover.jpg', { type: 'image/jpeg' }); 10411 const uploadData = await this.apiFetch('/api/lots/' + encodeURIComponent(lotId) + '/cover-image/upload-url', { 10412 method: 'POST', 10413 body: JSON.stringify({ fileName: coverFile.name, contentType: coverFile.type, sizeBytes: coverFile.size }), 10414 }); 10415 const uploadUrl = uploadData?.uploadUrl || uploadData?.signedUrl; 10416 if (!uploadUrl) throw new Error('La API no devolvió una URL para subir la portada.'); 10417 await this.uploadSignedFile(uploadUrl, coverFile, (progress) => { 10418 this.setState({ formMessage: { text: 'Subiendo portada ' + (completedSteps + 1) + ' de ' + totalSteps + ' · ' + progress + '%', ok: true } }); 10419 }); 10420 await this.apiFetchWithFallback( 10421 '/api/lots/' + encodeURIComponent(lotId) + '/cover-image/metadata', 10422 '/api/lots/' + encodeURIComponent(lotId) + '/cover-image/complete', 10423 { 10424 method: 'POST', 10425 body: JSON.stringify({ 10426 fileKey: uploadData.fileKey || uploadData.key, 10427 url: uploadData.publicUrl || uploadData.url, 10428 fileName: coverFile.name, 10429 contentType: coverFile.type, 10430 sizeBytes: coverFile.size, 10431 }), 10432 }, 10433 ); 10434 } 10435 async rollbackCreatedLot(lotId) { 10436 if (!lotId) return false;
10437 try { 10438 await this.apiFetch('/api/lots/' + encodeURIComponent(lotId), { method: 'DELETE' }); 10439 return true; 10440 } catch (error) { 10441 console.warn('No se pudo revertir el lote incompleto', lotId, error); 10442 return false; 10443 } 10444 } 10445 releaseVideoPreviews(videos) { 10446 (videos || []).forEach((video) => { 10447 if (video?.objectUrl) URL.revokeObjectURL(video.objectUrl); 10448 (video?.frames || []).forEach((frame) => frame?.previewUrl && URL.revokeObjectURL(frame.previewUrl)); 10449 }); 10450 } 10451 lotDetailToForm(lot, videoData) { 10452 const base = this.emptyForm(); 10453 const breedIds = (lot.razaIds || lot.breedIds || lot.razas || []) 10454 .map((breed) => String(breed?.id || breed)) 10455 .filter(Boolean) 10456 .slice(0, 3); 10457 const remoteVideos = (videoData || []).slice(0, 3).map((video) => { 10458 const url = video.url || video.publicUrl || video.downloadUrl || video.videoUrl || ''; 10459 const poster = video.thumbnailUrl || video.posterUrl || video.thumbnail || lot.coverImage?.url || ''; 10460 const frame = poster ?{ previewUrl: poster, remote: true } : null; 10461 return { 10462 id: String(video.id || video.videoId || video._id || video.fileKey || ''), 10463 existing: true, 10464 url, 10465 fileName: video.fileName || video.name || 'Video publicado', 10466 contentType: video.contentType || 'video/mp4', 10467 fileSize: Number(video.sizeBytes || video.fileSize || 0), 10468 frames: frame ?[frame] : [], 10469 selectedFrame: frame, 10470 }; 10471 }); 10472 while (remoteVideos.length < 3) remoteVideos.push(null); 10473 const coverUrl = lot.coverImage?.url || lot.thumbnailUrl || ''; 10474 const coverIndex = Math.max(0, remoteVideos.findIndex((video) => 10475 video && [video.selectedFrame?.previewUrl, video.url].includes(coverUrl))); 10476 return { 10477 ...base, 10478 estId: String(lot.establecimientoId || ''), 10479 estName: lot.establecimientoNombre || '', 10480 comitente: lot.establecimientoComitente || '', 10481 fecha: String(lot.fechaSubasta || '').slice(0, 10), 10482 prov: lot.establecimientoProvincia || lot.provincia || '', 10483 loc: lot.establecimientoLocalidad || lot.localidad || '', 10484 lat: lot.establecimientoLatitud != null ?String(lot.establecimientoLatitud) : '', 10485 lon: lot.establecimientoLongitud != null ?String(lot.establecimientoLongitud) : '', 10486 cat: lot.categoriaId || '', 10487 subcat: lot.subcategoriaId || '', 10488 cant: lot.cantidadTotal != null ?String(lot.cantidadTotal) : '', 10489 cantMachos: lot.cantidadMachos != null ?String(lot.cantidadMachos) : '', 10490 cantHembras: lot.cantidadHembras != null ?String(lot.cantidadHembras) : '', 10491 razas: breedIds, 10492 engorde: lot.engordeId || '', 10493 diasEncierre: lot.diasEncierre != null ?String(lot.diasEncierre) : '', 10494 destino: lot.destinoId || '', 10495 china: lot.aptoChina === true || lot.aptoChina === 'true' ?'true' : 'false', 10496 pesoProm: lot.pesoPromedio != null ?String(lot.pesoPromedio) : '', 10497 pesoMin: lot.pesoMinimo != null ?String(lot.pesoMinimo) : '', 10498 pesoMax: lot.pesoMaximo != null ?String(lot.pesoMaximo) : '', 10499 cond: lot.condicionId || '', 10500 precio: lot.precioSugerido != null ?String(lot.precioSugerido) : '', 10501 listoParaCargar: lot.listoParaCargar === true, 10502 dispo: String(lot.fechaDisponibleCarga || '').slice(0, 10), 10503 desbaste: lot.desbaste != null ?String(lot.desbaste) : '', 10504 rinde: lot.rendimientoEstimado != null ?String(lot.rendimientoEstimado) : '', 10505 pesaje: lot.pesajeId || '', 10506 plazo: lot.plazoId || '', 10507 plazoOtro: lot.plazoOtro || '', 10508 obs: lot.observaciones || '', 10509 videos: remoteVideos, 10510 thumbIdx: coverIndex, 10511 }; 10512 } 10513 async startEditLot(mappedLot) { 10514 if (!mappedLot || ['sold', 'closed_no_bids'].includes(mappedLot.auctionStatus) || mappedLot.status === 'Vendido' || mappedLot.status === 'Caducado') return; 10515 const cert = String(mappedLot.cert || ''); 10516 if (!cert || this.state.editLotLoading) return; 10517 this.setState({ editLotLoading: true }); 10518 this.showToast('Cargando datos del lote...'); 10519 try { 10520 const [detailData, videosData] = await Promise.all([ 10521 this.apiFetch('/api/lots/' + encodeURIComponent(cert)), 10522 this.apiFetch('/api/lots/' + encodeURIComponent(cert) + '/videos').cat
10522ch(() => []), 10523 ]); 10524 const detail = detailData?.lot || detailData?.data || detailData || mappedLot.raw || mappedLot; 10525 const videos = Array.isArray(videosData) 10526 ? videosData 10527 : (videosData?.items || videosData?.videos || videosData?.data || []); 10528 const resolvedVideos = Array.isArray(videos) && videos.length 10529 ? videos 10530 : (detail.videos || detail.videoUploads || []); 10531 const editForm = this.lotDetailToForm(detail, Array.isArray(resolvedVideos) ?resolvedVideos : []); 10532 this.setState({ 10533 route: 'load', 10534 loadStep: 0, 10535 form: editForm, 10536 formMessage: null, 10537 editLotCert: cert, 10538 editOriginalForm: editForm, 10539 editDeletedVideoIds: [], 10540 editLotLoading: false, 10541 vidProg: {}, 10542 }); 10543 window.scrollTo(0, 0); 10544 } catch (error) { 10545 this.setState({ editLotLoading: false }); 10546 this.showToast(error.message || 'No se pudo abrir el lote para editar.'); 10547 } 10548 } 10549 async openLotDestination() { 10550 this.setState({lotDestinationOpen:true,lotDestinationLoading:true,lotDestinationError:'',lotDestinationItems:[]}); 10551 try { 10552 const uid=this.state.apiUser?.uid || this.state.user?.uid; 10553 const [raw]=await Promise.all([this.apiFetch('/api/Remates'),uid&&!this.canManageUsers()?this.fetchUserConsignataria(uid):Promise.resolve()]); 10554 this.setState({lotDestinationItems:this.rosganArray(raw),lotDestinationLoading:false}); 10555 } catch(error) {this.setState({lotDestinationLoading:false,lotDestinationError:error.message||'No se pudieron consultar tus remates.'});} 10556 } 10557 lotDestinationLogos(item) { 10558 const all=[...(this.state.rosganLotConsignatarias||[]),...(this.state.consignatarias||[])]; 10559 const participants=item.consignatariasParticipantes?.length?item.consignatariasParticipantes:[{consignatariaId:item.consignatariaId,consignatariaNombre:item.consignatariaNombre||item.nombre_consignatario||'Rosgan'}]; 10560 const seen=new Set(); 10561 return participants.map(participant=>{ 10562 const ref=participant.consignatariaId||participant.id||participant.consignatariaNombre||'Rosgan'; 10563 const found=all.find(c=>[c.id,c.slug,c.name,c.nombre].some(v=>v&&this.consKey(v)===this.consKey(ref))); 10564 const name=found?.name||found?.nombre||participant.consignatariaNombre||participant.name||ref; 10565 const white=this.consWhiteLogo(ref);return {name,logo:white||this.consLogo(ref)||'/assets/brand/rosgan-iso-blue.png',color:white?this.consBrandColor(ref):'#fff'}; 10566 }).filter(brand=>{if(seen.has(brand.name))return false;seen.add(brand.name);return true;}); 10567 } 10568 lotDestinationAllowed(item) { 10569 if(!this.canManageRosganLots() || item.activo===false || item.cargaDeLotes===false || item.closeRemate===true || item.load?.lotes===false || String(item.cargaLotes||'').toLowerCase()==='cerrada')return false; 10570 if(item.cabanasConfig && !this.canManageSellerData())return false; 10571 if(this.canManageUsers())return true; 10572 if(item.cabanasConfig)return this.canEditCabanaRemate(item); 10573 const memberships=[...(this.state.userConsignatarias||[]),this.state.userConsignataria].filter(Boolean); 10574 const keys=new Set(memberships.flatMap(c=>[c.id,c.slug,c.ref,c.name]).filter(Boolean).map(v=>this.consKey(v))); 10575 const participants=item.consignatariasParticipantes||[]; 10576 return participants.some(c=>[c.consignatariaId,c.id,c.consignatariaNombre,c.name].some(v=>v&&keys.has(this.consKey(v)))) || [item.consignatariaId,item.consignatariaNombre].some(v=>v&&keys.has(this.consKey(v))); 10577 } 10578 async chooseLotDestination(item) { 10579 if(!this.lotDestinationAllowed(item))return; 10580 this.setState({lotDestinationOpen:false}); 10581 if(item.cabanasConfig){try {const remate=await this.fetchCabanaById(String(item.id),{force:true});if(remate)this.openCabanaLotCreate(remate);}catch(error){this.showToast(error.message||'No se pudo abrir el remate.');}return;} 10582 await this.loadRosganLotCatalogs(); 10583 this.onRosganRemateChange(String(item.id)); 10584 this.go('rosganLotLoad'); 10585 } 10586 beginNewLot() { 10587 if (!this.canAccessRoslot()) return; 10588 this.releaseVideoPreviews(this.form().videos); 10589 this.setState({ 10590 route: 'load', 10591 loadStep: 0, 10592 form: this.emptyForm(), 10593 formMessage: null, 10594 editLotCert: '', 10595 editOriginalForm: null, 10596 editDeletedVideoIds: [], 10597 vidProg: {}, 10598 }); 10599 window.scrollTo(0, 0); 10600 } 10601 resetLotWizard() { 10602 this.releaseVideoPreviews(this.form().videos); 10603 this.setState({ form: this.state.editLotCert && this.state.editOriginalForm ?this.state.editOriginalForm : this.emptyForm(), loadStep: 0, formMessage: null, editDeletedVideoIds: [], vidProg: {} }); 10604 } 10605 closeLotWizard() { 10606 this.releaseVideoPreviews(this.form().videos); 10607 this.setState({ route: this.state.editLotCert ?'profile' : 'pool', form: this.emptyForm(), loadStep: 0, formMessage: null, editLotCert: '', editOriginalForm: null, editDeletedVideoIds: [], vidProg: {} }); 10608 window.scrollTo(0, 0); 10609 } 10610 async deleteEditedVideos(lotId) { 10611 for (const videoId of (this.state.editDeletedVideoIds || [])) { 10612 await this.apiFetch('/api/lots/' + encodeURIComponent(lotId) + '/videos/' + encodeURIComponent(videoId), { method: 'DELETE' }); 10613 } 10614 } 10615 async publishLot() { 10616 if (this.state.publishing) return; 10617 const f = this.form(); 10618 const availabilityError = this.loadAvailabilityError(f.fecha, f.listoParaCargar, f.dispo); 10619 if (availabilityError) { this.setState({ loadStep: 2, formMessage: { text: availabilityError, ok: false } }); return; } 10620 for (let i = 0; i <= 3; i++) { 10621 if (!this.loadStepComplete(i)) { this.setState({ loadStep: i, formMessage: { text: 'Faltan campos obligatorios en la etapa ' + (i + 1) + '.', ok: false } }); return; } 10622 } 10623 this.setState({ publishing: true, formMessage: { text: 'Publicando lote...', ok: true } }); 10624 let token = ''; 10625 try { token = await (await this.ensureFirebase()).auth().currentUser?.getIdToken(); } catch {} 10626 if (!token) { 10627 this.setState({ publishing: false, formMessage: { text: 'Tenés que iniciar sesión para publicar el lote.', ok: false } }); 10628 return; 10629 } 10630 const payload = this.buildLotPayload(f); 10631 const videoUploads = f.videos.filter((video) => video?.file); 10632 const coverVideo = f.videos[f.thumbIdx] || videoUploads[0]; 10633 const selectedCover = coverVideo?.selectedFrame || coverVideo?.frames?.[0] || null; 10634 const totalUploadSteps = videoUploads.length + (selectedCover ?1 : 0);
10635 let createdLotId = ''; 10636 let createdLotNeedsRollback = false; 10637 try { 10638 const editing = !!this.state.editLotCert; 10639 const created = await this.apiFetch( 10640 editing ?'/api/Lots/' + encodeURIComponent(this.state.editLotCert) : '/api/Lots', 10641 { method: editing ?'PUT' : 'POST', body: JSON.stringify(payload) }, 10642 ); 10643 createdLotId = editing ?this.state.editLotCert : this.createdLotId(created); 10644 if (!createdLotId && totalUploadSteps) { 10645 throw new Error('El lote se creó, pero la API no devolvió el certificado para subir los videos.'); 10646 } 10647 createdLotNeedsRollback = !editing && totalUploadSteps > 0; 10648 for (let index = 0; index < videoUploads.length; index += 1) { 10649 const slotIndex = f.videos.indexOf(videoUploads[index]); 10650 await this.uploadVideoAsset(createdLotId, videoUploads[index], slotIndex, index, totalUploadSteps); 10651 } 10652 if (selectedCover) { 10653 await this.uploadCoverAsset(createdLotId, selectedCover, videoUploads.length, totalUploadSteps); 10654 } 10655 if (editing && this.state.editDeletedVideoIds.length) { 10656 this.setState({ formMessage: { text: 'Actualizando videos del lote...', ok: true } }); 10657 await this.deleteEditedVideos(createdLotId); 10658 } 10659 createdLotNeedsRollback = false; 10660 this.releaseVideoPreviews(f.videos); 10661 this.setState({ publishing: false, formMessage: { text: editing ?'Lote actualizado correctamente.' : 'Lote publicado. Ya aparece en el pool.', ok: true }, editDeletedVideoIds: [] }); 10662 this.fetchLots(); 10663 this.fetchMyLots(); 10664 setTimeout(() => this.setState({ route: editing ?'profile' : 'pool', form: this.emptyForm(), loadStep: 0, formMessage: null, editLotCert: '', editOriginalForm: null }), 1400); 10665 } catch (e) { 10666 let rollbackText = ''; 10667 if (createdLotNeedsRollback && createdLotId) { 10668 const reverted = await this.rollbackCreatedLot(createdLotId); 10669 rollbackText = reverted 10670 ? ' El lote incompleto fue eliminado.' 10671 : ' Revisá Mis lotes: no se pudo eliminar automáticamente la publicación incompleta.'; 10672 } 10673 this.setState({ 10674 publishing: false, 10675 vidProg: {}, 10676 formMessage: { text: (e.message || 'No se pudo publicar. Verificá tu sesión e intentá de nuevo.') + rollbackText, ok: false }, 10677 }); 10678 } 10679 } 10680 syncCatalogFloatingContact() { 10681 const floating = document.querySelector('.dc-catalog-floating-contact'); 10682 if (!floating) return; 10683 const card = document.querySelector('.dc-catalog-contact-row .dc-lot-contact-card'); 10684 const shouldShow = this.state.route === 'cabanaDetail' && this.state.isMobile && !!card && card.getBoundingClientRect().bottom < 74; 10685 floating.classList.toggle('is-visible', shouldShow); 10686 floating.setAttribute('aria-hidden', shouldShow ? 'false' : 'true'); 10687 } 10688 lacassinaLocationState() { 10689 const path = window.location.pathname.replace(/\/+$/, '') || '/'; 10690 const isStats = path === '/lacassina/estadisticas'; 10691 const isCommercial = path === '/lacassina/comercial'; 10692 const isPrices = path === '/lacassina/precios'; 10693 const isTv = path === '/lacassina/tv'; 10694 const isTvConfig = path === '/lacassina/tv/config'; 10695 const isStandalone = isStats || isCommercial || isPrices || isTv || isTvConfig; 10696 const lotMatch = isStandalone ?null : path.match(/^\/lacassina\/([^/]+)\/([^/]+)$/i); 10697 return { 10698 path: lotMatch ?'/lacassina' : path, 10699 route: isStats ?'lacassina-stats' : (isCommercial ?'lacassina-commercial' : (isPrices ?'lacassina-prices' : (isTv ?'lacassina-tv' : (isTvConfig ?'lacassina-tv-config' : 'lacassina')))), 10700 lotNumber: lotMatch ?decodeURIComponent(lotMatch[1]) : '', 10701 rp: lotMatch ?decodeURIComponent(lotMatch[2]) : '', 10702 isLot: !!lotMatch, 10703 isStats, 10704 isCommercial, 10705 isPrices, 10706 isTv, 10707 isTvConfig, 10708 }; 10709 } 10710 setLacassinaPublicLocation(info) { 10711 window.__ROSGAN_PUBLIC_PATH__ = info.path; 10712 window.__ROSGAN_PUBLIC_ROUTE__ = info.route; 10713 window.__ROSGAN_PUBLIC_LOT_NUMBER__ = info.lotNumber || ''; 10714 window.__ROSGAN_PUBLIC_RP__ = info.rp || ''; 10715 document.documentElement.dataset.publicRoute = info.route; 10716 } 10717 handleLacassinaPopState() { 10718 if (this.state.publicAdvanceMode !== 'lacassina') return; 10719 const info = this.lacassinaLocationState(); 10720 this.setLacassinaPublicLocation(info); 10721 this.flushCabanaLotViewTimer('browser_navigation'); 10722 if (info.isStats) { 10723 this.setState({ route: 'cabanaStats', cabanaPublicDeepLoading: false, currentCabanaLot: null, currentCabanaLotVariants: [] }); 10724 return; 10725 } 10726 if (info.isCommercial) { 10727 this.setState({ route: 'cabanaCommercial', cabanaPublicDeepLoading: false, currentCabanaLot: null, currentCabanaLotVariants: [], cabanaCommercialLoading: true }, () => { 10728 window.scrollTo(0, 0); 10729 if (this.state.currentCabana) this.fetchCabanaCommercial(this.state.currentCabana); 10730 }); 10731 return; 10732 } 10733 if (info.isPrices) { 10734 this.setState({ route: 'cabanaPriceBoard', cabanaPublicDeepLoading: false, currentCabanaLot: null, currentCabanaLotVariants: [], cabanaCommercialDrafts: this.loadCabanaCommercialDrafts(this.state.currentCabana) }, () => { 10735 window.scrollTo(0, 0); 10736 if (this.state.currentCabana) this.fetchCabanaCommercial(this.state.currentCabana, { silent: true }); 10737 }); 10738 return; 10739 } 10740 if (info.isTv || info.isTvConfig) { 10741 this.setState({ route: info.isTvConfig ?'cabanaTvConfig' : 'cabanaTvLower', cabanaPublicDeepLoading: false, currentCabanaLot: null, currentCabanaLotVariants: [], cabanaLotsLoading: true }, () => { 10742 window.scrollTo(0, 0); 10743 if (this.state.currentCabana) this.fetchCabanaLots(this.state.currentCabana); 10744 }); 10745 return; 10746 } 10747 if (!info.isLot) { 10748 this.setState({ route: 'cabanaDetail', cabanaPublicDeepLoading: false, currentCabanaLot: null, currentCabanaLotVariants: [], cbLotVideo: 0, c
10748abanaLotVideosLoading: false, cabanaLotMediaReady: false, cabanaLotMediaFailed: false }, () => window.scrollTo(0, 0)); 10749 return; 10750 } 10751 const openTarget = (lots) => { 10752 const opened = this.openCabanaLotByRp(info.rp, lots, info.lotNumber); 10753 if (!opened) this.setState({ route: 'cabanaDetail', cabanaPublicDeepLoading: false, currentCabanaLot: null, currentCabanaLotVariants: [] }); 10754 }; 10755 this.setState({ cabanaPublicDeepLoading: true }, () => { 10756 if (Array.isArray(this.state.cabanaLots) && this.state.cabanaLots.length) { 10757 openTarget(this.state.cabanaLots); 10758 } else if (this.state.currentCabana) { 10759 this.fetchCabanaLots(this.state.currentCabana).then(openTarget).catch(() => this.setState({ cabanaPublicDeepLoading: false })); 10760 } else { 10761 this.fetchCabanaById('2', { force: true }).then((remate) => { 10762 if (!remate) throw new Error('No se encontro el remate de La Cassina.'); 10763 this.openCabana(remate, { focusRp: info.rp, focusLotNumber: info.lotNumber }); 10764 }).catch(() => this.setState({ cabanaPublicDeepLoading: false })); 10765 } 10766 }); 10767 } 10768 phoneNumberE164(value) { 10769 let phone = String(value || '').trim().replace(/[\s().-]/g, ''); 10770 if (phone.startsWith('00')) phone = '+' + phone.slice(2); 10771 return /^\+[1-9]\d{7,14}$/.test(phone) ?phone : ''; 10772 } 10773 maskedPhoneNumber(value) { 10774 const phone = String(value || ''); 10775 if (phone.length < 7) return phone; 10776 return phone.slice(0, 4) + ' â¢â¢â¢â¢ ' + phone.slice(-4); 10777 } 10778 clearPhoneRecaptcha() { 10779 try { this._phoneRecaptchaVerifier?.clear?.(); } catch {} 10780 this._phoneRecaptchaVerifier = null; 10781 this._phoneRecaptchaWidgetId = null; 10782 } 10783 async savePhoneAndContinue() { 10784 if (this.state.phoneVerificationBusy) return; 10785 const phoneNumber = this.phoneNumberE164(this.state.phoneVerificationNumber); 10786 if (!phoneNumber) { this.setState({ phoneVerificationMessage: 'Ingresá tu teléfono con código de paÃs, por ejemplo +54 9 341 000 0000.' }); return; } 10787 this.setState({ phoneVerificationBusy: true, phoneVerificationMessage: '' }); 10788 try { 10789 const user = (await this.ensureFirebase()).auth().currentUser; 10790 if (!user) throw new Error('La sesión venció. Volvé a ingresar.'); 10791 const token = await user.getIdToken(); 10792 const response = await fetch('/api/auth/sync-user', { 10793 method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, 10794 body: JSON.stringify({ phoneNumber }), 10795 }); 10796 const result = await response.json(); 10797 if (!response.ok || !this.phoneNumberE164(result?.session?.phoneNumber)) throw new Error(result?.message || 'No se pudo guardar el teléfono. Intentá nuevamente.'); 10798 if ((await this.ensureFirebase()).auth().currentUser?.uid !== user.uid) throw new Error('La sesión cambió. Volvé a ingresar.'); 10799 const access = await this.authVerificationAccess(user); 10800 await this.activateAuthenticatedUser(user, access.apiUser); 10801 this.setState({ phoneVerificationRequired: false, phoneVerificationBusy: false, phoneVerificationMessage: '' }); 10802 } catch (error) { 10803 this.setState({ phoneVerificationBusy: false, phoneVerificationMessage: error?.message || 'No se pudo guardar el teléfono.' }); 10804 } 10805 } 10806 changePhoneNumber() { 10807 if (this.state.phoneVerificationBusy) return; 10808 this._phoneConfirmationResult = null; 10809 this._phoneVerificationId = null; 10810 this.clearPhoneRecaptcha(); 10811 this.setState({ phoneVerificationStep: 'phone', phoneVerificationCode: '', phoneVerificationMessage: '' }); 10812 } 10813 async logoutFromPhoneVerification() { 10814 if (this.state.phoneVerificationBusy) return; 10815 this.clearWelcomeAfterSignIn(); 10816 this.clearCachedApiUser(); 10817 this._phoneConfirmationResult = null; 10818 this._phoneVerificationId = null; 10819 this.clearPhoneRecaptcha(); 10820 try { sessionStorage.removeItem('rosgan:lastRoute'); } catch {} 10821 await this.signOutSession(); 10822 this.setState({ user: null, apiUser: null, routeLoading: false, userConsignataria: null, userConsignatarias: [], myLots: null, myBids: null, profileFetchInfo: {}, route: 'login', authReady: true, authMode: 'signin', phoneVerificationRequired: false, phoneVerificationStep: 'phone', phoneVerificationCode: '', phoneVerificationBusy: false, phoneVerificationMessage: '' }); 10823 } 10824 isPasswordAccount(user) { 10825 return !!user && Array.isArray(user.providerData) && user.providerData.some((provider) => provider?.providerId === 'password'); 10826 } 10827 requiresEmailVerification(user) { 10828 return false;
10829 } 10830 authActionSettings(action) { 10831 const target = new URL(window.location.origin + '/'); 10832 target.searchParams.set('authAction', action); 10833 return { url: target.toString(), handleCodeInApp: false }; 10834 } 10835 async sendRegistrationVerification(user) { 10836 if (!user || !this.requiresEmailVerification(user)) return; 10837 const response = await this.authOperationWithTimeout( 10838 this.apiFetch('/api/mailing/auth/verification-email', { method: 'POST', body: '{}' }), 10839 30000, 10840 'El servicio de correo no confirmó el envÃo. Tocá âReenviar correoâ para intentarlo nuevamente.' 10841 ); 10842 const result = response?.data || response; 10843 if (result?.alreadyVerified === true) return result; 10844 if (result?.accepted !== true || result?.provider !== 'mailgun') { 10845 throw new Error('El servicio de correo no confirmó el envÃo. Tocá âReenviar correoâ para intentarlo nuevamente.'); 10846 } 10847 return result; 10848 } 10849 authOperationWithTimeout(promise, timeoutMs, message) { 10850 let timer = null; 10851 return Promise.race([ 10852 Promise.resolve(promise).finally(() => { if (timer) clearTimeout(timer); }), 10853 new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), timeoutMs); }), 10854 ]); 10855 } 10856 async checkEmailVerification(options = {}) { 10857 if (this.state.emailVerificationBusy) return; 10858 const silent = options?.silent === true; 10859 this.setState({ emailVerificationBusy: true, emailVerificationMessage: silent ?this.state.emailVerificationMessage : '' }); 10860 try { 10861 const firebase = await this.ensureFirebase(); 10862 const user = firebase.auth().currentUser; 10863 if (!user) throw new Error('La sesión venció. Volvé a ingresar.'); 10864 await this.authOperationWithTimeout(user.reload(), 8000, 'Firebase demoró demasiado en comprobar el correo. Probá nuevamente.'); 10865 if (sessionEpoch !== (this._sessionEpoch || 0) || !firebase.auth().currentUser) return; 10866 const currentUser = firebase.auth().currentUser; 10867 if (!currentUser.emailVerified) { 10868 this.setState({ emailVerificationBusy: false, emailVerificationMessage: silent ?this.state.emailVerificationMessage : 'TodavÃa no figura confirmado. Abrà el enlace del correo y volvé a intentar.' }); 10869 return; 10870 }
10871 await currentUser.getIdToken(true).catch(() => null); 10872 this.setState({ emailVerificationMessage: 'Correo verificado. Ingresandoâ¦' }); 10873 await this.handleAuthenticatedUser(currentUser, { skipReload: true }); 10874 } catch (error) { 10875 this.setState({ emailVerificationBusy: false, emailVerificationMessage: error?.message || 'No pudimos comprobar el correo. Probá nuevamente.' }); 10876 } 10877 } 10878 async resendEmailVerification() { 10879 if (this.state.emailVerificationBusy) return; 10880 this.setState({ emailVerificationBusy: true, emailVerificationMessage: '' }); 10881 try { 10882 const firebase = await this.ensureFirebase(); 10883 const user = firebase.auth().currentUser; 10884 if (!user) throw new Error('La sesión venció. Volvé a ingresar.'); 10885 if (user.emailVerified) { 10886 this.setState({ emailVerificationRequired: false, emailVerificationBusy: false, emailVerificationMessage: '' }); 10887 await this.handleAuthenticatedUser(firebase.auth().currentUser || user, { skipReload: true }); 10888 return; 10889 } 10890 await this.sendRegistrationVerification(user); 10891 this.setState({ emailVerificationBusy: false, emailVerificationMessage: 'Correo aceptado para entrega. Revisá Recibidos y también Spam o Correo no deseado.' }); 10892 } catch (error) { 10893 this.setState({ emailVerificationBusy: false, emailVerificationMessage: this.authError(error) }); 10894 } 10895 } 10896 async logoutFromEmailVerification() { 10897 if (this.state.emailVerificationBusy) return; 10898 this.clearWelcomeAfterSignIn(); 10899 this.clearCachedApiUser(); 10900 try { sessionStorage.removeItem('rosgan:lastRoute'); } catch {} 10901 await this.signOutSession(); 10902 this.setState({ user: null, route: 'login', authReady: true, authMode: 'signin', emailVerificationRequired: false, emailVerificationBusy: false, emailVerificationMessage: '', phoneVerificationRequired: false }); 10903 } 10904 requestWelcomeAfterSignIn() { 10905 this._welcomeStartedAt = Date.now(); 10906 try { 10907 sessionStorage.setItem('rosgan:welcomeAfterSignIn', '1'); 10908 sessionStorage.setItem('rosgan:welcomeStartedAt', String(this._welcomeStartedAt)); 10909 } catch {} 10910 this.setState({ authWelcomeRequested: true }); 10911 } 10912 consumeWelcomeAfterSignIn() { 10913 let requested = !!this.state.authWelcomeRequested; 10914 try { 10915 requested = requested || sessionStorage.getItem('rosgan:welcomeAfterSignIn') === '1'; 10916 const storedStart = Number(sessionStorage.getItem('rosgan:welcomeStartedAt')); 10917 if (!this._welcomeStartedAt && Number.isFinite(storedStart) && storedStart > 0) this._welcomeStartedAt = storedStart; 10918 sessionStorage.removeItem('rosgan:welcomeAfterSignIn'); 10919 sessionStorage.removeItem('rosgan:welcomeStartedAt'); 10920 } catch {} 10921 return requested; 10922 } 10923 clearWelcomeAfterSignIn() { 10924 this._welcomeStartedAt = null; 10925 try { 10926 sessionStorage.removeItem('rosgan:welcomeAfterSignIn'); 10927 sessionStorage.removeItem('rosgan:welcomeStartedAt'); 10928 } catch {} 10929 if (this.state.authWelcomeRequested) this.setState({ authWelcomeRequested: false }); 10930 } 10931 hasPendingWelcome() { 10932 if (this.state.authWelcomeRequested) return true; 10933 try { return sessionStorage.getItem('rosgan:welcomeAfterSignIn') === '1'; } catch { return false; } 10934 } 10935 readCachedApiUser(user) { 10936 if (!user?.uid) return null; 10937 try { 10938 const cached = JSON.parse(sessionStorage.getItem('rosgan:authenticatedApiUser') || 'null'); 10939 if (!cached || cached.uid !== user.uid || Number(cached.expiresAt || 0) <= Date.now()) return null; 10940 const apiUser = this.normalizeApiSessionUser(cached.apiUser); 10941 return apiUser && this.apiRoleIds(apiUser).length ?apiUser : null; 10942 } catch { return null; } 10943 } 10944 cacheApiUser(user, apiUser) { 10945 if (!user?.uid || !apiUser || !this.apiRoleIds(apiUser).length) return; 10946 try { 10947 sessionStorage.setItem('rosgan:authenticatedApiUser', JSON.stringify({ 10948 uid: user.uid, 10949 expiresAt: Date.now() + 6 * 60 * 60 * 1000, 10950 apiUser, 10951 })); 10952 } catch {} 10953 } 10954 clearSessionIdentity() { 10955 this._sessionEpoch = (this._sessionEpoch || 0) + 1; 10956 this._pathRequest = (this._pathRequest || 0) + 1; 10957 this.clearCachedApiUser(); 10958 this._userConsignatariaPromise = null; 10959 this._userConsignatariaUid = null; 10960 this.setState({ user: null, apiUser: null, apiSessionError: '', userConsignataria: null, 10961 userConsignatarias: [], myLots: null, myBids: null, profileFetchInfo: {}, 10962 profileRosganLots: [], profilePhotoFailed: false, profilePhotoViewerOpen: false, 10963 notificationsItems: null, notificationsOpen: false }); 10964 } 10965 async signOutSession() { 10966 this._signingOut = true; 10967 this.clearSessionIdentity(); 10968 try { await (await this.ensureFirebase()).auth().signOut(); } 10969 catch { this.setState({ authMessage: 'No pudimos cerrar la sesión. Volvé a intentar salir.' }); } 10970 finally { this._signingOut = false; } 10971 } 10972 clearCachedApiUser() { 10973 try { sessionStorage.removeItem('rosgan:authenticatedApiUser'); } catch {} 10974 } 10975 readCollectionCache(key) { 10976 if (!key) return null; 10977 try { 10978 const cached = JSON.parse(sessionStorage.getItem(key) || 'null'); 10979 if (!cached || Number(cached.expiresAt || 0) <= Date.now()) return null; 10980 return cached.value ?? null; 10981 } catch { return null; } 10982 } 10983 writeCollectionCache(key, value, ttlMs = 30 * 60 * 1000) { 10984 if (!key) return; 10985 try { 10986 const serialized = JSON.stringify({ expiresAt: Date.now() + ttlMs, value }, (_key, item) => { 10987 if (typeof item === 'string' && item.startsWith('data:') && item.length > 10000) return ''; 10988 return item; 10989 }); 10990 sessionStorage.setItem(key, serialized); 10991 } catch {} 10992 } 10993 runViewTransition(update) { 10994 const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; 10995 if (reduceMotion) return Promise.resolve().then(update); 10996 if (!document.startViewTransition) { 10997 return new Promise((resolve, reject) => { 10998 this.setState({ authTransitioning: true }, () => { 10999 setTimeout(() => { 11000 Promise.resolve().then(update).then((result) => { 11001 setTimeout(() => this.setState({ authTransitioning: false }, () => resolve(result)), 340); 11002 }).catch((error) => { 11003 this.setState({ authTransitioning: false }, () => reject(error)); 11004 }); 11005 }, 220); 11006 }); 11007 }); 11008 } 11009 let transition; 11010 try { 11011 transition = document.startViewTransition(() => Promise.resolve().then(update)); 11012 } catch { 11013 return Promise.resolve().then(update); 11014 } 11015 transition.finished.catch(() => {}); 11016 return transition.updateCallbackDone; 11017 } 11018 transitionState(nextState) { 11019 return this.runViewTransition(() => new Promise((resolve) => this.setState(nextState, resolve))); 11020 } 11021 async finishWelcome(targetRoute = 'home', initialPath = '') { 11022 this._welcomeTimer = null; 11023 if (this._welcomeBindTimer) clearTimeout(this._welcomeBindTimer); 11024 this._welcomeBindTimer = null; 11025 this._welcomeStartedAt = null; 11026 if (this._welcomeLottiePlayer && this._welcomeLottieComplete) { 11027 this._welcomeLottiePlayer.removeEventListener('complete', this._welcomeLottieComplete); 11028 } 11029 if (this._welcomeDotLottie && this._welcomeLottieComplete) { 11030 this._welcomeDotLottie.removeEventListener('complete', this._welcomeLottieComplete); 11031 } 11032 this._welcomeLottiePlayer = null; 11033 this._welcomeDotLottie = null; 11034 this._welcomeLottieComplete = null; 11035 this._welcomeAnimationComplete = false;
11036 this._welcomeFinishWhenReady = null; 11037 if (this.state.route !== 'welcome') return; 11038 const roslotLotTarget = this.canAccessRoslot() ?this.roslotLotPathTarget(initialPath) : null; 11039 const deepAppPath = roslotLotTarget || this.rematePathTarget(initialPath) || this.cabanaPathTarget(initialPath); 11040 if (deepAppPath) { 11041 const initialUrl = initialPath + (this._initialSearch || ''); 11042 await this.runViewTransition(() => new Promise((resolve) => { 11043 try { window.history.replaceState({}, '', initialUrl); } catch {} 11044 this.setState({ routeLoading: true }, () => { 11045 this.navigateFromPath(initialPath); 11046 requestAnimationFrame(resolve); 11047 }); 11048 })); 11049 return; 11050 } 11051 const nextRoute = targetRoute || 'home'; 11052 await this.runViewTransition(() => new Promise((resolve) => { 11053 this.syncAppLocation(nextRoute, { replace: true }); 11054 this.go(nextRoute, { syncPath: false }); 11055 requestAnimationFrame(resolve); 11056 })); 11057 } 11058 async activateAuthenticatedUser(user, bootstrapApiUser = null) { 11059 const sessionEpoch = this._sessionEpoch || 0; 11060 const enteringFromLogin = this.state.authReady && this.state.route === 'login'; 11061 const welcomeRequested = this.consumeWelcomeAfterSignIn(); 11062 const requestedRoute = this._initialPathRoute || ''; 11063 const initialPath = this._initialPathname || window.location.pathname; 11064 const initialUrl = initialPath + (this._initialSearch || ''); 11065 const preserveReadyView = !!bootstrapApiUser && this.state.authReady && !this.state.routeLoading && this.state.route !== 'login' && !welcomeRequested; 11066 const authenticatedState = { 11067 user, 11068 publicAdvanceMode: this.state.publicAdvanceMode === 'cabanas' ? '' : this.state.publicAdvanceMode, 11069 authReady: preserveReadyView, 11070 authBusy: false, 11071 authMessage: '', 11072 emailVerificationRequired: false, 11073 emailVerificationBusy: false, 11074 emailVerificationMessage: '', 11075 phoneVerificationRequired: false, 11076 phoneVerificationBusy: false, 11077 phoneVerificationMessage: '', 11078 phoneVerificationNumber: user.phoneNumber || this.state.phoneVerificationNumber, 11079 }; 11080 // authVerificationAccess already synchronized the user and returned the API 11081 // session. Avoid repeating both remote calls after every Google sign-in. 11082 const minimumConnectingTime = welcomeRequested 11083 ? new Promise((resolve) => setTimeout(resolve, 900)) 11084 : Promise.resolve(); 11085 if (enteringFromLogin) await this.transitionState(authenticatedState); 11086 else await new Promise((resolve) => this.setState(authenticatedState, resolve)); 11087 11088 const validationPromise = bootstrapApiUser 11089 ? Promise.resolve([null, bootstrapApiUser]) 11090 : Promise.all([ 11091 this.syncAuthUser(user), 11092 this.fetchApiSession(), 11093 ]); 11094 const [, initialApiUser] = await validationPromise; 11095 if (sessionEpoch !== (this._sessionEpoch || 0) || this.state.user?.uid !== user.uid) return; 11096 await minimumConnectingTime; 11097 if (sessionEpoch !== (this._sessionEpoch || 0) || this.state.user?.uid !== user.uid) return; 11098 const apiUser = initialApiUser || bootstrapApiUser || await this.fetchApiSession(); 11099 if (sessionEpoch !== (this._sessionEpoch || 0) || this.state.user?.uid !== user.uid) return; 11100 if (apiUser) { 11101 this.cacheApiUser(user, apiUser); 11102 if (apiUser !== this.state.apiUser) this.setState({ apiUser }); 11103 } 11104 const catalogReturn = this.cabanaPathTarget(initialPath); 11105 const returningToCatalog = ['cabanaDetail', 'cabanaLotDetail'].includes(requestedRoute) && ['detail', 'lot'].includes(catalogReturn?.action) && !user.isAnonymous; 11106 const canAccess = this.canAccessApp(apiUser) || returningToCatalog; 11107 const requestedRouteAllowed = !requestedRoute || this.canAccessRoute(requestedRoute, apiUser); 11108 const targetRoute = canAccess ?(requestedRouteAllowed ?(requestedRoute || 'home') : 'home') : 'home'; 11109 const showWelcome = canAccess && welcomeRequested; 11110 const route = showWelcome ?'welcome' : targetRoute; 11111 const roslotDeepPath = this.roslotLotPathTarget(initialPath); 11112 const remateDeepPath = this.rematePathTarget(initialPath); 11113 const cabanaDeepPath = this.cabanaPathTarget(initialPath); 11114 const deepRoute = roslotDeepPath ?'pool' : (remateDeepPath ?'remates' : (cabanaDeepPath ?'cabanas' : '')); 11115 const deepAppPath = canAccess && this.canAccessRoute(deepRoute, apiUser) && (roslotDeepPath || remateDeepPath || cabanaDeepPath); 11116 try { 11117 const welcomePath = requestedRoute ?(deepAppPath ?initialUrl : this.appPathForRoute(targetRoute)) : '/'; 11118 const nextUrl = canAccess ?(showWelcome ?welcomePath : (deepAppPath ?initialUrl : this.appPathForRoute(targetRoute))) : '/'; 11119 if (!preserveReadyView) window.history.replaceState({}, '', nextUrl); 11120 } catch {} 11121 const readyState = { 11122 route: preserveReadyView ? this.state.route : route, 11123 routeLoading: preserveReadyView ? !!this.state.routeLoading : !showWelcome && !!deepAppPath, 11124 authReady: true, 11125 authWelcomeRequested: false, 11126 mailingOpen: false, 11127 adminUsersOpen: false, 11128 }; 11129 if (route === 'welcome' || preserveReadyView) await new Promise((resolve) => this.setState(readyState, resolve)); 11130 else await this.transitionState(readyState); 11131// Resolve deep links as soon as authentication is ready. Profile metrics and 11132 // secondary catalogs can keep loading in parallel without leaving the route blank. 11133 if (!showWelcome && deepAppPath && !preserveReadyView) this.navigateFromPath(initialPath); 11134 if (showWelcome) { 11135 requestAnimationFrame(() => { 11136 const player = document.querySelector('.app-welcome-lottie'); 11137 let completed = false;
11138 let bindAttempts = 0; 11139 const continueToApp = () => { 11140 if (completed || this.state.route !== 'welcome') return; 11141 completed = true; 11142 if (this._welcomeTimer) clearTimeout(this._welcomeTimer); 11143 if (this._welcomeBindTimer) clearTimeout(this._welcomeBindTimer); 11144 this.finishWelcome(targetRoute, initialPath); 11145 }; 11146 const bindDotLottie = () => { 11147 const dotLottie = player?.dotLottie || null; 11148 if (!dotLottie && bindAttempts < 20 && this.state.route === 'welcome') { 11149 bindAttempts += 1; 11150 this._welcomeBindTimer = setTimeout(bindDotLottie, 50); 11151 return; 11152 } 11153 this._welcomeDotLottie = dotLottie; 11154 dotLottie?.addEventListener('complete', continueToApp); 11155 const lottieAlreadyFinished = !!dotLottie 11156 && dotLottie.isLoaded 11157 && !dotLottie.isPlaying 11158 && dotLottie.totalFrames > 0 11159 && dotLottie.currentFrame >= dotLottie.totalFrames - 2; 11160 if (lottieAlreadyFinished) continueToApp(); 11161 }; 11162 this._welcomeLottiePlayer = player; 11163 this._welcomeLottieComplete = continueToApp; 11164 player?.addEventListener('complete', continueToApp, { once: true }); 11165 bindDotLottie(); 11166 this._welcomeTimer = setTimeout(continueToApp, 2850); 11167 }); 11168 } 11169 if (targetRoute === 'cabanas' && !this.state.cabanasCollectionReady && !this.state.cabanasLoading) { 11170 await this.fetchCabanas(true); 11171 } 11172 if (targetRoute === 'rosganLotLoad') { 11173 this.loadRosganLotCatalogs(); 11174 } 11175 this.loadCatalog(); 11176 this.fetchData(); 11177 this.fetchLots(); 11178 this.initializeNotifications(); 11179 this.fetchNotifications(false); 11180 this.fetchWhatsAppNotificationPreference(true); 11181 if (targetRoute !== 'cabanas' && !this.state.cabanas && !this.state.cabanasLoading) { 11182 const warmCabanaCards = () => this.fetchCabanas(); 11183 if (typeof window.requestIdleCallback === 'function') window.requestIdleCallback(warmCabanaCards, { timeout: 1200 }); 11184 else setTimeout(warmCabanaCards, 200); 11185 } 11186 if (targetRoute !== 'home') await this.refreshProfileData(); 11187 if (!showWelcome && this.state.route === 'profile') { 11188 this.animateDash(); 11189 } 11190 } 11191 async fetchLaunchInterest() { 11192 if (!this.state.user || this.state.launchInterestLoading) return; 11193 this.setState({ launchInterestLoading: true, launchInterestMessage: '' }); 11194 try { 11195 const data = await this.apiFetch('/api/launch-interests?mine=1'); 11196 this.setState({ launchInterest: data?.interest || data?.data?.interest || null, launchInterestLoading: false }); 11197 } catch (error) { 11198 this.setState({ launchInterestLoading: false, launchInterestMessage: error?.message || 'No pudimos consultar tu aviso.' }); 11199 } 11200 } 11201 async toggleLaunchInterest() { 11202 if (!this.state.user || this.state.launchInterestSaving) return; 11203 const subscribed = this.state.launchInterest?.status === 'subscribed'; 11204 this.setState({ launchInterestSaving: true, launchInterestMessage: '' }); 11205 try { 11206 const data = await this.apiFetch('/api/launch-interests', subscribed 11207 ? { method: 'DELETE' } 11208 : { method: 'POST', body: JSON.stringify({ consent: true, emailOptIn: true, whatsappOptIn: true }) }); 11209 const interest = data?.interest || data?.data?.interest || { status: subscribed ?'unsubscribed' : 'subscribed' }; 11210 this.setState({ 11211 launchInterest: interest, 11212 launchInterestSaving: false, 11213 launchInterestMessage: subscribed 11214 ? 'Listo. Dejamos de enviarte este aviso.' 11215 : '¡Listo! Te avisaremos cuando Rosgan esté disponible.', 11216 }); 11217 } catch (error) { 11218 this.setState({ launchInterestSaving: false, launchInterestMessage: error?.message || 'No pudimos guardar tu elección.' }); 11219 } 11220 } 11221 async fetchLaunchInterestsAdmin() { 11222 if (!this.canManageUsers() || this.state.launchInterestsAdminLoading) return; 11223 this.setState({ launchInterestsAdminLoading: true, launchInterestsAdminError: '' }); 11224 try { 11225 const data = await this.apiFetch('/api/launch-interests'); 11226 const interests = Array.isArray(data?.interests) ?data.interests : (Array.isArray(data?.data?.interests) ?data.data.interests : []); 11227 this.setState({ 11228 launchInterestsAdmin: interests, 11229 launchInterestsAdminTotal: Number(data?.total ?? data?.data?.total) || interests.length, 11230 launchInterestsAdminLoading: false, 11231 }); 11232 } catch (error) {
11233 this.setState({ launchInterestsAdminLoading: false, launchInterestsAdminError: error?.message || 'No pudimos cargar los interesados.' }); 11234 } 11235 } 11236 exportLaunchInterests() { 11237 const interests = Array.isArray(this.state.launchInterestsAdmin) ?this.state.launchInterestsAdmin : []; 11238 if (!interests.length) return; 11239 const cell = (value) => '"' + String(value ?? '').replace(/"/g, '""') + '"'; 11240 const rows = [['Nombre', 'Email', 'Teléfono', 'Email', 'WhatsApp', 'Fecha de alta']] 11241 .concat(interests.map((item) => [ 11242 item.displayName || '', item.email || '', item.phoneNumber || '', item.emailOptIn ?'SÃ' : 'No', 11243 item.whatsappOptIn ?'SÃ' : 'No', item.updatedAt || item.createdAt || '', 11244 ])) 11245 .map((row) => row.map(cell).join(',')); 11246 this.downloadCabanaBlob(new Blob(['\ufeff', rows.join('\r\n')], { type: 'text/csv;charset=utf-8' }), 'interesados-lanzamiento-rosgan.csv'); 11247 } 11248 async authVerificationAccess(user) { 11249 if (!user) return { bypass: false, apiUser: null }; 11250 const cachedApiUser = this.readCachedApiUser(user); 11251 let tokenClaims = null; 11252 try { 11253 const [token, tokenResult] = await Promise.all([ 11254 user.getIdToken(), 11255 user.getIdTokenResult?.().catch(() => null), 11256 ]); 11257 tokenClaims = tokenResult?.claims || null; 11258 const response = await fetch('/api/auth/sync-user', { 11259 method: 'POST', 11260 headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, 11261 body: JSON.stringify({ 11262 displayName: user.displayName || '', 11263 email: user.email || '', 11264 phoneNumber: user.phoneNumber || '', 11265 phoneVerified: Boolean(user.phoneNumber), 11266 }), 11267 }); 11268 const data = await response.json().catch(() => null); 11269 if (!response.ok || !data) { 11270 const claimsRoles = this.userRoleIds(tokenClaims); 11271 const claimsUser = claimsRoles.length ?{ 11272 uid: user.uid, 11273 email: user.email || '', 11274 roles: claimsRoles, 11275 roleId: claimsRoles, 11276 role: ['sysadmin', 'admin', 'dev', 'seller', 'buyer'].find((role) => claimsRoles.includes(role)) || 'user', 11277 } : null; 11278 const fallback = cachedApiUser || claimsUser; 11279 return { bypass: this.userRoleIds(fallback).includes('dev'), apiUser: fallback }; 11280 } 11281 const session = data.session || data.data?.session || data.data || data; 11282 const external = this.normalizeApiSessionUser(session.apiSession || data.apiSession || session); 11283 const stored = session.user && typeof session.user === 'object' ?session.user : null; 11284 const roles = [...new Set([ 11285 ...this.userRoleIds(data), 11286 ...this.userRoleIds(session), 11287 ...this.userRoleIds(stored), 11288 ...this.userRoleIds(external), 11289 ...this.userRoleIds(tokenClaims), 11290 ])]; 11291 const primaryRole = ['sysadmin', 'admin', 'seller', 'cabanero', 'buyer', 'dev'].find((role) => roles.includes(role)) 11292 || external?.role || session.role || stored?.role || 'user'; 11293 const apiUser = { 11294 ...(stored || {}), 11295 ...(external || {}), 11296 phoneNumber: session.phoneNumber || stored?.phoneNumber || user.phoneNumber || '', 11297 uid: external?.uid || stored?.uid || session.uid || user.uid, 11298 email: external?.email || stored?.email || session.email || user.email || '', 11299 roles, 11300 roleId: roles, 11301 role: primaryRole, 11302 }; 11303 this.cacheApiUser(user, apiUser); 11304 return { bypass: session.bypassVerification === true || roles.includes('dev'), apiUser }; 11305 } catch (error) { 11306 console.warn('auth verification access', error); 11307 const claimsRoles = this.userRoleIds(tokenClaims); 11308 const claimsUser = claimsRoles.length ?{ 11309 uid: user.uid, 11310 email: user.email || '', 11311 roles: claimsRoles, 11312 roleId: claimsRoles, 11313 role: ['sysadmin', 'admin', 'dev', 'seller', 'buyer'].find((role) => claimsRoles.includes(role)) || 'user', 11314 } : null; 11315 const fallback = cachedApiUser || claimsUser; 11316 return { bypass: this.userRoleIds(fallback).includes('dev'), apiUser: fallback }; 11317 } 11318 } 11319 async handleAuthenticatedUser(user, options = {}) { 11320 if (this._signingOut || !user?.uid) return; 11321 const epoch = this._sessionEpoch || 0;
11322 const key = user.uid + ':' + epoch; 11323 if (this._authHandling?.key === key) return this._authHandling.promise; 11324 if (this.state.user?.uid === user.uid && this.state.authReady && this.state.apiUser && !this.state.phoneVerificationRequired && !options.skipReload) { 11325 this.setState({ user }); 11326 return; 11327 } 11328 if (this.state.user && this.state.user.uid !== user.uid) this.clearSessionIdentity(); 11329 const promise = this.restoreAuthenticatedUser(user, options); 11330 this._authHandling = { key, promise }; 11331 try { return await promise; } 11332 finally { if (this._authHandling?.promise === promise) this._authHandling = null; } 11333 } 11334 async restoreAuthenticatedUser(user, options = {}) { 11335 const sessionEpoch = this._sessionEpoch || 0; 11336 // Una recarga no debe bloquear la interfaz mientras se repiten las mismas 11337 // consultas remotas. Restauramos el rol ya validado para este UID y luego 11338 // refrescamos todos los datos con los endpoints autenticados habituales. 11339 const cachedApiUser = this.readCachedApiUser(user); 11340 if (cachedApiUser && (this.userRoleIds(cachedApiUser).includes('dev') || this.phoneNumberE164(user.phoneNumber || cachedApiUser.phoneNumber))) { 11341 const canAccess = this.canAccessApp(cachedApiUser); 11342 const targetRoute = canAccess ?(this._initialPathRoute || this._initialRoute || 'home') : 'home'; 11343 await new Promise((resolve) => this.setState({ 11344 user, 11345 apiUser: cachedApiUser, 11346 route: targetRoute, 11347 routeLoading: canAccess && !!(this.rematePathTarget(this._initialPathname) || this.cabanaPathTarget(this._initialPathname) || this.roslotLotPathTarget(this._initialPathname)), 11348 authReady: true, 11349 authBusy: false, 11350 authMessage: '', 11351 emailVerificationRequired: false, 11352 phoneVerificationRequired: false, 11353 }, resolve)); 11354 if (this._signingOut || sessionEpoch !== (this._sessionEpoch || 0)) return; 11355 await this.activateAuthenticatedUser(user, cachedApiUser); 11356 return; 11357 } 11358 // Once Firebase hands back the Google session, move off the login form 11359 // immediately while role and verification checks continue in parallel. 11360 if (this.state.authWelcomeRequested && this.state.authReady) { 11361 await new Promise((resolve) => this.setState({ 11362 user, 11363 authReady: false, 11364 authBusy: true, 11365 authMessage: '', 11366 }, resolve)); 11367 } 11368 if (!options?.skipReload) { 11369 try { await this.authOperationWithTimeout(user.reload(), 8000, ''); } catch {} 11370 } 11371 const firebase = await this.ensureFirebase(); 11372 const currentUser = firebase.auth().currentUser; 11373 if (this._signingOut || sessionEpoch !== (this._sessionEpoch || 0) || currentUser?.uid !== user.uid) return; 11374 const verificationAccess = await this.authVerificationAccess(currentUser); 11375 if (sessionEpoch !== (this._sessionEpoch || 0) || firebase.auth().currentUser?.uid !== currentUser.uid) return; 11376 if (!verificationAccess.bypass && !this.phoneNumberE164(currentUser.phoneNumber || verificationAccess.apiUser?.phoneNumber)) { 11377 const prefilled = this.phoneNumberE164(this.state.formPhone) || this.phoneNumberE164(this.state.phoneVerificationNumber) || '+54'; 11378 this.setState({ 11379 user: currentUser, 11380 route: this.state.route === 'login' ?(this._initialRoute || 'profile') : this.state.route, 11381 authReady: true, 11382 authBusy: false, 11383 authMessage: '', 11384 emailVerificationRequired: false, 11385 emailVerificationBusy: false, 11386 emailVerificationMessage: '', 11387 phoneVerificationRequired: true, 11388 phoneVerificationStep: 'phone', 11389 phoneVerificationNumber: prefilled, 11390 phoneVerificationCode: '', 11391 phoneVerificationBusy: false, 11392 phoneVerificationMessage: '', 11393 }); 11394 return; 11395 } 11396 await this.activateAuthenticatedUser(currentUser, verificationAccess.apiUser); 11397 } 11398 async componentDidMount() { 11399 window.rosganAudienceApp = this; 11400 const configureFieldMode = () => { const uploadBridge = { 11401 user: () => this.state.user && this.state.apiUser ? { uid: this.state.user.uid } : null, 11402 request: async (uid, path, options = {}) => { 11403 const token = await this.authToken(); 11404 if (!token || this.state.user?.uid !== uid || !this.state.apiUser) throw Object.assign(new Error('Iniciá sesión con la cuenta que filmó.'), { status: 401 }); 11405 const response = await fetch(this.API + path, { ...options, headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token } }); 11406 if (!response.ok) { 11407 const data = await response.json().catch(() => null); 11408 const detail = data?.message || data?.detail || (typeof data?.error === 'string' ? data.error : ''); 11409 throw Object.assign(new Error('No se pudo confirmar la operación.'), { status: response.status, detail: typeof detail === 'string' ? detail : '' }); 11410 } 11411 const text = await response.text(); 11412 return text.trim() ? JSON.parse(text) : null; 11413 }, 11414 }; window.RosganField.configure(uploadBridge); window.RosganVideoQueue?.configure(uploadBridge); }; 11415 if (window.RosganField) configureFieldMode(); 11416 else window.addEventListener('rosgan-field-ready', configureFieldMode, { once: true }); 11417 this._onTextInputCapture = (event) => { 11418 if (!this.isTextEntryControl(event.target)) return; 11419 this._handlingTextInput = true; 11420 queueMicrotask(() => { this._handlingTextInput = false; }); 11421 }; 11422 this._onFormCompositionStart = (event) => { if (this.isTextEntryControl(event.target)) this._formCompositionActive = true; }; 11423 this._onFormCompositionEnd = (event) => { 11424 if (!this.isTextEntryControl(event.target)) return; 11425 this._formCompositionActive = false;
11426 this.scheduleTextEntryRender(40); 11427 }; 11428 document.addEventListener('input', this._onTextInputCapture, true); 11429 document.addEventListener('compositionstart', this._onFormCompositionStart, true); 11430 document.addEventListener('compositionend', this._onFormCompositionEnd, true); 11431 const publicMode = this.state.publicAdvanceMode; 11432 const legacyAppPath = this.legacyRoslotHashPath(window.location.hash) || this.legacyCabanaHashPath(window.location.hash); 11433 this._initialPathname = legacyAppPath || window.location.pathname; 11434 this._initialSearch = legacyAppPath ?'' : window.location.search; 11435 this._initialPathRoute = publicMode ?'' : this.routeFromPath(this._initialPathname); 11436 this._initialRoute = window.__ROSGAN_LOCAL_MAILING_ENTRY__ ?'mailingAdmin' : ((this.props.iniciarEnHome || false) ?'home' : this.restoreRoute()); 11437 if (publicMode === 'lacassina') { 11438 const wantsStats = window.__ROSGAN_PUBLIC_ROUTE__ === 'lacassina-stats' || window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/estadisticas'; 11439 const wantsCommercial = window.__ROSGAN_PUBLIC_ROUTE__ === 'lacassina-commercial' || window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/comercial'; 11440 const wantsPrices = window.__ROSGAN_PUBLIC_ROUTE__ === 'lacassina-prices' || window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/precios'; 11441 const wantsTv = window.__ROSGAN_PUBLIC_ROUTE__ === 'lacassina-tv' || window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/tv'; 11442 const wantsTvConfig = window.__ROSGAN_PUBLIC_ROUTE__ === 'lacassina-tv-config' || window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/tv/config'; 11443 const params = new URLSearchParams(window.location.search || ''); 11444 const focusRp = window.__ROSGAN_PUBLIC_RP__ || params.get('rp') || ''; 11445 const focusLotNumber = window.__ROSGAN_PUBLIC_LOT_NUMBER__ || params.get('lote') || ''; 11446 this.releaseProfileCrop(); 11447 this.setState({ 11448 route: wantsStats ? 'cabanaStats' : (wantsCommercial ? 'cabanaCommercial' : (wantsPrices ? 'cabanaPriceBoard' : (wantsTvConfig ? 'cabanaTvConfig' : (wantsTv ? 'cabanaTvLower' : (focusRp ? 'cabanaDetail' : 'cabanas'))))), 11449 authReady: true, 11450 user: null, 11451 profileEditorOpen: false, 11452 profilePhotoViewerOpen: false, 11453 profileCropOpen: false, 11454 profileCropSource: '', 11455 adminUsersOpen: false, 11456 notificationsOpen: false, 11457 cabanasLoading: true, 11458 cabanaLotsLoading: wantsStats || wantsCommercial || wantsPrices || wantsTv || wantsTvConfig, 11459 cabanaPublicDeepLoading: !!focusRp 11460 }, async () => { 11461 try { 11462 const remate = await this.fetchCabanaById('2', { force: true }); 11463 if (!remate) throw new Error('No se encontró el remate de La Cassina.'); 11464 if (wantsStats) this.openCabanaStats(remate); 11465 else if (wantsCommercial) this.openCabanaCommercial(remate); 11466 else if (wantsPrices) this.openCabanaPriceBoard(remate); 11467 else if (wantsTvConfig) this.openCabanaTvConfig(remate); 11468 else if (wantsTv) this.openCabanaTvLower(remate); 11469 else this.openCabana(remate, { focusRp, focusLotNumber }); 11470 } catch (error) { 11471 this.setState({ cabanasError: error?.message || 'No se pudo cargar el adelanto de La Cassina.', cabanasLoading: false, cabanaLotsLoading: false }); 11472 } 11473 }); 11474 } else { 11475 if (this._authBootstrapTimer) clearTimeout(this._authBootstrapTimer); 11476 this._authBootstrapTimer = setTimeout(() => { 11477 this._authBootstrapTimer = null; 11478 if (this.state.authReady) return; 11479 if (this.state.user || window.firebase?.auth?.().currentUser) return; 11480 this.setState({ 11481 route: 'login', 11482 authReady: true, 11483 authBusy: false, 11484 authMessage: 'La conexión demoró más de lo esperado. Ya podés intentar ingresar.', 11485 emailVerificationRequired: false, 11486 phoneVerificationRequired: false, 11487 }); 11488 }, 12000); 11489 try { 11490 const firebase = await this.ensureFirebase(); 11491 const auth = firebase.auth(); 11492 this._authUnsubscribe = auth.onAuthStateChanged(async (u) => { 11493 if (this._authBootstrapTimer) clearTimeout(this._authBootstrapTimer); 11494 this._authBootstrapTimer = null; 11495 if (u) { 11496 if (this._signingOut || auth.currentUser?.uid !== u.uid) return; 11497 try { await this.handleAuthenticatedUser(u); } 11498 catch { this.setState({ authBusy: false, authReady: true, authMessage: 'No pudimos cargar tu sesión. Revisá la conexión y recargá la página.' }); } 11499 } else { 11500 // Ignore a stale empty callback if Firebase already recovered the 11501 // durable user while this callback was queued during page startup. 11502 if (auth.currentUser) { 11503 await this.handleAuthenticatedUser(auth.currentUser); 11504 return; 11505 } 11506 this.clearSessionIdentity(); 11507 this.setState({ routeLoading: false }); 11508 this.clearCachedApiUser(); 11509 this._phoneConfirmationResult = null; 11510 this._phoneVerificationId = null; 11511 this.clearPhoneRecaptcha(); 11512 if (this.activatePublicCatalogGuest(window.location.pathname)) return; 11513 this.setState({ user: null, route: 'login', authReady: true, authBusy: false, emailVerificationRequired: false, emailVerificationBusy: false, emailVerificationMessage: '', phoneVerificationRequired: false, phoneVerificationBusy: false, phoneVerificationStep: 'phone', phoneVerificationCode: '', phoneVerificationMessage: '', notificationsItems: null, notificationsOpen: false }); 11514 } 11515 }); 11516 auth.getRedirectResult().then(async (r) => { 11517 if (r && r.user) { 11518 await this.persistFirebaseUser(firebase, r.user); 11519 this.setState({ authBusy: false, authMessage: '' }); 11520 } 11521 }).catch((e) => { const m = this.googleErr(e); if (e?.code) this.setState({ authBusy: false, authMessage: m }); }); 11522 } catch (e) { 11523 if (this._authBootstrapTimer) clearTimeout(this._authBootstrapTimer); 11524 this._authBootstrapTimer = null; 11525 console.warn('firebase init', e); 11526 if (this.state.user || window.firebase?.auth?.().currentUser) return; 11527 this.setState({ route: 'login', authReady: true, authBusy: false, authMessage: 'No pudimos restaurar tu sesión. Probá nuevamente.' }); 11528 } 11529 } 11530 this._onResize = () => { 11531 const isMobile = window.innerWidth < 760; 11532 const compactNavigation = window.innerWidth <= 1024; 11533 if (isMobile !== this.state.isMobile || compactNavigation !== this.state.compactNavigation) this.setState({ isMobile, compactNavigation }, () => this.syncCatalogFloatingContact()); 11534 else this.syncCatalogFloatingContact(); 11535 }; 11536 window.addEventListener('resize', this._onResize); 11537 this._onRoslotTouchStart = (event) => { 11538 if (!this.state.isMobile || this.state.route !== 'pool' || event.touches?.length !== 1) return; 11539 const target = event.target; 11540 if (target?.closest?.('input, textarea, select, [data-drag-scroll]')) return; 11541 const touch = event.touches[0]; 11542 this._roslotSwipeStart = { x: touch.clientX, y: touch.clientY, at: Date.now() }; 11543 }; 11544 this._onRoslotTouchEnd = (event) => { 11545 const start = this._roslotSwipeStart; 11546 this._roslotSwipeStart = null; 11547 if (!start || !this.state.isMobile || this.state.route !== 'pool' || event.changedTouches?.length !== 1) return; 11548 const touch = event.changedTouches[0]; 11549 const dx = touch.clientX - start.x; 11550 const dy = touch.clientY - start.y; 11551 const horizontal = Math.abs(dx) >= 58 && Math.abs(dx) > Math.abs(dy) * 1.25; 11552 const quickEnough = Date.now() - start.at < 1400; 11553 if (!horizontal || !quickEnough) return; 11554 if (!this.state.roslotMobilePanelOpen && dx < 0) this.openRoslotMobilePanel(); 11555 else if (this.state.roslotMobilePanelOpen && dx > 0) this.closeRoslotMobilePanel(); 11556 }; 11557 document.addEventListener('touchstart', this._onRoslotTouchStart, { passive: true }); 11558 document.addEventListener('touchend', this._onRoslotTouchEnd, { passive: true }); 11559 this._onHashChange = () => this.navigateFromHash(); 11560 window.addEventListener('hashchange', this._onHashChange); 11561 this._onPopState = () => { 11562 if (this.state.publicAdvanceMode === 'lacassina') this.handleLacassinaPopState(); 11563 else this.navigateFromPath(); 11564 }; 11565 window.addEventListener('popstate', this._onPopState); 11566 this._onEmailVerificationFocus = () => { 11567 if (this.state.emailVerificationRequired && !this.state.emailVerificationBusy) this.checkEmailVerification({ silent: true }); 11568 }; 11569 window.addEventListener('focus', this._onEmailVerificationFocus); 11570 this._onCabanaCommercialStorage = (event) => { 11571 if (this.state.route !== 'cabanaPriceBoard') return; 11572 if (event?.key && event.key !== this.cabanaCommercialStorageKey(this.state.currentCabana)) return; 11573 this.refreshCabanaPriceBoardDrafts(); 11574 }; 11575 window.addEventListener('storage', this._onCabanaCommercialStorage); 11576 this._onCabanaTvStorage = () => this.syncCabanaTvExternalState(); 11577 window.addEventListener('storage', this._onCabanaTvStorage); 11578 window.addEventListener('rosgan:tv-config', this._onCabanaTvStorage); 11579 this._onRosganOpsStorage = (event) => { 11580 if (event?.key === this.rosganOpsStorageKey(this.state.currentRemate) + ':lot-preview') { 11581 this.receiveRosganTvPreview(event.newValue); 11582 return; 11583 } 11584 if (event?.key && event.key !== this.rosganOpsStorageKey(this.state.currentRemate)) return; 11585 this.syncRosganOpsExternalState(); 11586 }; 11587 window.addEventListener('storage', this._onRosganOpsStorage); 11588 window.addEventListener('rosgan:remate-operations', this._onRosganOpsStorage); 11589 this._rosganAirtimeTimer = setInterval(() => this.refreshRosganAirtime(), 1000); 11590 this._rosganOpsTimer = setInterval(() => { 11591 this.refreshRosganLotInterest(); 11592 if (this.state.route !== 'rosganTvLower') { this.resetRosganTvDecks(); this.resetRosganTvMultiview(); } 11593 if (!['rosganCommercial', 'rosganPriceBoard', 'rosganTvLower', 'rosganTvConfig'].includes(this.state.route)) return; 11594 this.syncRosganOpsExternalState(); 11595 this.fetchRosganOpsRemoteState({ silent: true }); 11596 if (this.state.route === 'rosganTvLower') this.ensureRosganOpsSelectedVideo(); 11597 }, 1200); 11598 this._rosganTvFieldsTimer = setInterval(() => { 11599 if (this.state.route === 'rosganTvLower' && this._rosganTvDecks?.onAirCert) { 11600 this.setState({ rosganTvFieldStep: (this.state.rosganTvFieldStep || 0) + 1 }); 11601 } 11602 }, 6000); 11603 this._cabanaPriceBoardTimer = setInterval(() => { 11604 if (this.state.route === 'cabanaPriceBoard') this.refreshCabanaPriceBoardDrafts(); 11605 }, 3000); 11606 this._cabanaStatsRefreshTimer = setInterval(() => { 11607 if (this.state.route === 'cabanaStats' && !this.state.cabanaStatsOverview && this.state.currentCabana && !this.state.cabanaAnalyticsLoading) { 11608 this.fetchCabanaAnalyticsSummary(this.state.currentCabana); 11609 } 11610 }, 10000); 11611 this._cabanaPrebidRealtimeTimer = setInterval(() => this.refreshCabanaPrebidRealtime(), 4000); 11612 this._prebidStartHandler = () => this.setState({ prebidBadgeTick: Date.now() }); 11613 document.addEventListener('prebid-started', this._prebidStartHandler); 11614 this._prebidBadgeTimer = setInterval(() => { 11615 if (document.hidden || !['home', 'remates', 'cabanas', 'cabanaLotDetail'].includes(this.state.route)) return; 11616 const signature = [...(this.state.cabanas || []), this.state.currentCabana].map(item => window.CabanaPrebid.isOpen(item)).join(','); 11617 if (signature !== this._prebidBadgeSignature) { this._prebidBadgeSignature = signature; this.setState({ prebidBadgeTick: Date.now() }); } 11618 }, 15000); 11619 this._creditRealtimeTimer = setInterval(() => this.refreshCreditRealtime(), 4500); 11620 this._homeChannelFetchedAt = Date.now(); 11621 this._homeChannelTimer = setInterval(() => this.refreshHomeChannel(), 30000); 11622 this._adminVersionTimer = setInterval(() => { 11623 if (!document.hidden && this.state.adminUsersOpen && !this.state.adminUserSaving) this.fetchAdminUsers(true); 11624 }, 60000); 11625 this._roslotPresenceTimer = setInterval(() => this.refreshRoslotPresence(), 60000); 11626 this._onScroll = () => { 11627 if (this.state.route === 'home') { 11628 const y = Math.min(window.scrollY, 600); 11629 const hero = document.querySelector('[data-hero-img]'); 11630 if (hero) hero.style.transform = 'translateY(' + Math.round(y * 0.35) + 'px)'; 11631 } 11632 this.syncCatalogFloatingContact(); 11633 }; 11634 window.addEventListener('scroll', this._onScroll, { passive: true }); 11635 this._cabanaAnalyticsClickHandler = (event) => { 11636 const target = event.target?.closest?.('.dc-contact-whatsapp,.dc-contact-call'); 11637 if (!target || this.state.route !== 'cabanaLotDetail') return; 11638 const whatsapp = target.classList.contains('dc-contact-whatsapp'); 11639 const contact = this.cabanaContactData(this.state.currentCabana); 11640 this.trackCabanaAnalytics(whatsapp ?'contact_whatsapp_click' : 'contact_call_click', this.state.currentCabanaLot, { 11641 metadata: { action: whatsapp ?'contact_whatsapp' : 'contact_call', contactName: contact.name }, 11642 }); 11643 }; 11644 document.addEventListener('click', this._cabanaAnalyticsClickHandler, true); 11645 this._onVisibilityChange = () => { 11646 if (document.hidden) this.flushCabanaLotViewTimer('tab_hidden'); 11647 if (publicMode) return; 11648 if (!document.hidden) { 11649 if (this.state.emailVerificationRequired && !this.state.emailVerificationBusy) this.checkEmailVerification({ silent: true }); 11650 if (this.state.user) this.refreshProfileData(true); 11651 this.syncAuctionStatuses(); 11652 this.syncLiveBids(); 11653 this.fetchNotifications(true); 11654 if (this.state.route === 'notifications') this.fetchNotificationHistory(true); 11655 this.refreshCabanaPrebidRealtime(); 11656 this.refreshCreditRealtime({ refreshAuthorizations: true }); 11657 this.refreshRoslotPresence(); 11658 } 11659 }; 11660 document.addEventListener('visibilitychange', this._onVisibilityChange); 11661 this._onSessionFocus = () => { if (!document.hidden && this.state.user) this.refreshProfileData(true); }; 11662 window.addEventListener('focus', this._onSessionFocus); 11663 if (!publicMode) { 11664 this._marketClockTimer = setInterval(() => { 11665 if (['pool', 'lot'].includes(this.state.route) && !document.hidden) this.updateRoslotMarketClockDom(); 11666 }, 1000); 11667 this._auctionStatusTimer = setInterval(() => { 11668 if (this.isRealtimeRoute()) this.syncAuctionStatuses(); 11669 }, 4000); 11670 this._liveBidTimer = setInterval(() => { 11671 if (this.isRealtimeRoute()) this.syncLiveBids(); 11672 }, 3000); 11673 this._notificationsTimer = setInterval(() => { 11674 if (document.hidden) return; 11675 const active = this.state.notificationsOpen || ['notifications', 'pool'].includes(this.state.route); 11676 if (!active && Date.now() - (this._lastNotificationsFetchAt || 0) < 30000) return; 11677 this.fetchNotifications(true); 11678 if (['notifications', 'pool'].includes(this.state.route)) this.fetchNotificationHistory(true); 11679 }, 5000); 11680 } 11681 this._cabanaCarouselTimer = setInterval(() => { 11682 if (this.state.route === 'cabanaDetail' && Array.isArray(this.state.cabanaLots) && this.state.cabanaLots.length > 1) { 11683 this.startCabanaCarouselCrossfade('*', (previous) => ({ 11684 cbCarouselStep: Number(previous.cbCarouselStep || 0) + 1, 11685 })); 11686 } 11687 }, 5000); 11688 this._cabanaTvSyncTimer = setInterval(() => { 11689 if (this.state.route !== 'cabanaTvLower' && this.state.route !== 'cabanaTvConfig') return; 11690 this.syncCabanaTvExternalState(); 11691 this.fetchCabanaTvRemoteConfig({ silent: true }); 11692 if (this.state.route === 'cabanaTvLower' || this.state.route === 'cabanaTvConfig') { 11693 const now = Date.now(); 11694 if (!this._lastCabanaTvCommercialFetchAt || now - this._lastCabanaTvCommercialFetchAt > 2500) { 11695 this._lastCabanaTvCommercialFetchAt = now; 11696 if (this.state.currentCabana) this.refreshCabanaTvCommercial(); 11697 } 11698 } 11699 if (this.state.route === 'cabanaTvLower') { 11700 const now = Date.now(); 11701 if (Math.floor(Number(this.state.cabanaTvClock || 0) / 1000) !== Math.
11701floor(now / 1000)) this.setState({ cabanaTvClock: now }); 11702 if (!this._lastCabanaTvWeatherCheckAt || now - this._lastCabanaTvWeatherCheckAt > 10 * 60 * 1000) { 11703 this._lastCabanaTvWeatherCheckAt = now; 11704 this.fetchCabanaTvWeather(this.state.currentCabana); 11705 } 11706 } 11707 }, 900); 11708 if (!publicMode) { this.bindRemateConfigSorting(); } 11709 this.scheduleDomBindings(); 11710 } 11711 componentWillUnmount() { 11712 clearInterval(this._adminVersionTimer); 11713 clearInterval(this._homeChannelTimer); 11714 document.removeEventListener('input', this._onTextInputCapture, true); 11715 document.removeEventListener('compositionstart', this._onFormCompositionStart, true); 11716 document.removeEventListener('compositionend', this._onFormCompositionEnd, true); 11717 if (this._textEntryRenderTimer) clearTimeout(this._textEntryRenderTimer); 11718 Object.values(this._inputRenderTimers || {}).forEach((timer) => clearTimeout(timer)); 11719 this.flushCabanaLotViewTimer('unmount'); 11720 if (this._authUnsubscribe) { 11721 try { this._authUnsubscribe(); } catch {} 11722 this._authUnsubscribe = null; 11723 } 11724 window.removeEventListener('resize', this._onResize); 11725 document.removeEventListener('touchstart', this._onRoslotTouchStart); 11726 document.removeEventListener('touchend', this._onRoslotTouchEnd); 11727 window.removeEventListener('hashchange', this._onHashChange); 11728 window.removeEventListener('popstate', this._onPopState); 11729 window.removeEventListener('focus', this._onEmailVerificationFocus); 11730 window.removeEventListener('storage', this._onCabanaCommercialStorage); 11731 window.removeEventListener('storage', this._onCabanaTvStorage); 11732 window.removeEventListener('rosgan:tv-config', this._onCabanaTvStorage); 11733 window.removeEventListener('scroll', this._onScroll); 11734 document.removeEventListener('click', this._cabanaAnalyticsClickHandler, true); 11735 document.removeEventListener('visibilitychange', this._onVisibilityChange); 11736 window.removeEventListener('focus', this._onSessionFocus); 11737 document.removeEventListener('pointerdown', this._onRemateSortPointerDown); 11738 window.removeEventListener('pointermove', this._onRemateSortPointerMove); 11739 window.removeEventListener('pointerup', this._onRemateSortPointerUp); 11740 window.removeEventListener('pointercancel', this._onRemateSortPointerUp); 11741 this.destroyCabanaSalesOrderSorting(); 11742 if (this._auctionStatusTimer) clearInterval(this._auctionStatusTimer); 11743 if (this._marketClockTimer) clearInterval(this._marketClockTimer); 11744 if (this._liveBidTimer) clearInterval(this._liveBidTimer); 11745 if (this._notificationsTimer) clearInterval(this._notificationsTimer); 11746 if (this._roslotLiveToastTimer) clearTimeout(this._roslotLiveToastTimer); 11747 if (this._roslotLivePulseTimer) clearTimeout(this._roslotLivePulseTimer); 11748 if (this._roslotBidSuccessTimer) clearTimeout(this._roslotBidSuccessTimer); 11749 if (this._cabanaCarouselTimer) clearInterval(this._cabanaCarouselTimer); 11750 if (this._cabanaTvSyncTimer) clearInterval(this._cabanaTvSyncTimer); 11751 if (this._rosganAirtimeTimer) clearInterval(this._rosganAirtimeTimer); 11752 if (this._rosganOpsTimer) clearInterval(this._rosganOpsTimer); 11753 if (this._rosganTvFieldsTimer) clearInterval(this._rosganTvFieldsTimer); 11754 this.resetRosganTvDecks(); 11755 this.resetRosganTvMultiview(); 11756 if (this._rosganOpsRemoteSaveTimer) clearTimeout(this._rosganOpsRemoteSaveTimer); 11757 if (this._onRosganOpsStorage) { 11758 window.removeEventListener('storage', this._onRosganOpsStorage); 11759 window.removeEventListener('rosgan:remate-operations', this._onRosganOpsStorage); 11760 } 11761 if (this._cabanaTvRemoteSaveTimer) clearTimeout(this._cabanaTvRemoteSaveTimer); 11762 if (this._cabanaPriceBoardTimer) clearInterval(this._cabanaPriceBoardTimer); 11763 if (this._cabanaStatsRefreshTimer) clearInterval(this._cabanaStatsRefreshTimer); 11764 if (this._cabanaPrebidRealtimeTimer) clearInterval(this._cabanaPrebidRealtimeTimer); 11765 if (this._prebidBadgeTimer) clearInterval(this._prebidBadgeTimer); 11766 document.removeEventListener('prebid-started', this._prebidStartHandler); 11767 if (this._creditRealtimeTimer) clearInterval(this._creditRealtimeTimer); 11768 if (this._roslotPresenceTimer) clearInterval(this._roslotPresenceTimer); 11769 if (this._cabanaCommercialPatchTimers) Object.values(this._cabanaCommercialPatchTimers).forEac
11769h((timer) => clearTimeout(timer)); 11770 if (this._cabanaLotViewHeartbeat) clearInterval(this._cabanaLotViewHeartbeat); 11771 if (this._cabanaCarouselFadeTimer) clearTimeout(this._cabanaCarouselFadeTimer); 11772 if (this._cabanaCarouselRevealTimer) clearTimeout(this._cabanaCarouselRevealTimer); 11773 if (this._welcomeTimer) clearTimeout(this._welcomeTimer); 11774 if (this._authBootstrapTimer) clearTimeout(this._authBootstrapTimer); 11775 if (this._welcomeLottiePlayer && this._welcomeLottieComplete) this._welcomeLottiePlayer.removeEventListener('complete', this._welcomeLottieComplete); 11776 if (this._welcomeDotLottie && this._welcomeLottieComplete) this._welcomeDotLottie.removeEventListener('complete', this._welcomeLottieComplete); 11777 if (this._viewT) clearTimeout(this._viewT); 11778 if (this._lotViewPoll) clearInterval(this._lotViewPoll); 11779 if (this._mapPlayT) clearInterval(this._mapPlayT); 11780 if (this._domBindingsFrame) cancelAnimationFrame(this._domBindingsFrame); 11781 if (this._dashRAF) cancelAnimationFrame(this._dashRAF); 11782 if (this._routeWorkTimer) clearTimeout(this._routeWorkTimer); 11783 if (this._revealFallback) clearTimeout(this._revealFallback); 11784 Object.values(this._inputRenderTimers || {}).forEach((timer) => clearTimeout(timer)); 11785 this._inputRenderTimers = {}; 11786 this._phoneConfirmationResult = null; 11787 this._phoneVerificationId = null; 11788 this.clearPhoneRecaptcha(); 11789 this.releaseProfileCrop(); 11790 if (this.state.profilePhotoPreview) URL.revokeObjectURL(this.state.profilePhotoPreview); 11791 } 11792 componentDidUpdate(prevProps, prevState) { 11793 const routeChanged = !!prevState && prevState.route !== this.state.route; 11794 this.syncMailingRoute(prevState); 11795 if (prevState?.user?.uid !== this.state.user?.uid) { 11796 if (this.state.user?.uid) this.refreshRoslotPresence(); 11797 else this.setState({ roslotPresence: null, roslotPresenceLoading: false, roslotPresenceError: false }); 11798 } 11799 if (prevState && prevState.route === 'lot' && this.state.route !== 'lot') { 11800 if (this._viewT) clearTimeout(this._viewT); 11801 if (this._lotViewPoll) clearInterval(this._lotViewPoll); 11802 this._viewT = null; 11803 this._lotViewPoll = null; 11804 } 11805 if (routeChanged) { 11806 if (this.state.route === 'pool') this.refreshRoslotPresence(); 11807 if (this.state.route === 'profile') { 11808 this.animateDash(); 11809 this.refreshCreditRealtime({ refreshAuthorizations: true }); 11810 } 11811 this.scheduleRouteWork(this.state.route); 11812 } 11813 if (this.state.route === 'roslotSummary' 11814 && this.state.authReady 11815 && this.state.user 11816 && !this.state.roslotDailyReport 11817 && !this.state.roslotDailyReportLoading 11818 && (routeChanged || prevState?.authReady !== this.state.authReady || prevState?.user?.uid !== this.state.user?.uid || prevState?.apiUser !== this.state.apiUser)) { 11819 this.fetchRoslotDailyReport(); 11820 } 11821 if (this.state.route === 'lot' && ( 11822 routeChanged 11823 || prevState?.user?.uid !== this.state.user?.uid 11824 || prevState?.userConsignataria !== this.state.userConsignataria 11825 )) this.fetchLotSellerConsignataria(this.currentLot()); 11826 const activeInput = document.activeElement; 11827 const activeTag = String(activeInput?.tagName || '').toLowerCase(); 11828 const activeType = String(activeInput?.type || 'text').toLowerCase(); 11829 const typing = activeTag === 'textarea' || (activeTag === 'input' && !['checkbox', 'radio', 'range', 'file', 'date', 'time'].includes(activeType)); 11830 if (!typing || routeChanged) this.scheduleDomBindings(); 11831 const vp = document.querySelector('[data-lot-player]'); 11832 if (vp && vp.dataset.src) { 11833 const source = vp.dataset.src; 11834 if (vp.getAttribute('src') !== source) { 11835 vp.removeAttribute('poster'); 11836 vp.src = source; 11837 vp.defaultMuted = true; 11838 vp.muted = true; 11839 try { vp.volume = 0; } catch (_) {} 11840 vp.load(); 11841 vp.play().catch(() => {}); 11842 } 11843 const currentLot = this.currentLot(); 11844 const videoCount = currentLot?.videos?.length || 0; 11845 vp.loop = videoCount <= 1; 11846 this.hydrateVideoThumbnails(currentLot?.videos || []); 11847 } 11848 const rematePlayer = document.querySelector('video[data-remate-lot-player="true"]'); 11849 if (rematePlayer && rematePlayer.dataset.src) { 11850 const source = rematePlayer.dataset.src; 11851 const sourceChanged = rematePlayer.getAttribute('src') !== source; 11852 const needsAutoplayInit = rematePlayer.dataset.autoplaySource !== source; 11853 rematePlayer.autoplay = true; 11854 rematePlayer.loop = true;
11855 rematePlayer.playsInline = true; 11856 if (needsAutoplayInit) { 11857 rematePlayer.dataset.autoplaySource = source; 11858 rematePlayer.defaultMuted = true; 11859 rematePlayer.muted = true; 11860 try { rematePlayer.volume = 0; } catch (_) {} 11861 if (sourceChanged) { 11862 rematePlayer.src = source; 11863 rematePlayer.load(); 11864 } 11865 rematePlayer.play().catch(() => {}); 11866 } 11867 } 11868 if (prevState && ( 11869 prevState.route !== this.state.route || 11870 prevState.cabanaSalesOrderGrouped !== this.state.cabanaSalesOrderGrouped || 11871 prevState.cabanaSalesOrderLots !== this.state.cabanaSalesOrderLots 11872 )) { 11873 this.destroyCabanaSalesOrderSorting(); 11874 } 11875 } 11876 scheduleDomBindings() { 11877 if (this._domBindingsFrame) return; 11878 this._domBindingsFrame = requestAnimationFrame(() => { 11879 this._domBindingsFrame = null; 11880 window.RosganNameFit?.sync(); 11881 this.bindReveals(); 11882 if (this.state.route === 'home') this.bindDragScroll(); 11883 if (this.state.route === 'profile') { 11884 this.relocateSellerBuyerPanel(); 11885 this.bindProfileImageErrors(); 11886 this.drawProfileCrop(); 11887 } 11888 if (['cabanaLotDetail', 'cabanaDetail'].includes(this.state.route)) this.syncCatalogFloatingContact(); 11889 }); 11890 } 11891 isRealtimeRoute(route = this.state.route) { 11892 return ['home', 'pool', 'lot', 'profile'].includes(route); 11893 } 11894 bindProfileImageErrors() { 11895 if (this.state.profilePhotoFailed || !this.profilePhotoUrl()) return; 11896 document.querySelectorAll('img[data-profile-avatar="true"]').forEach((image) => { 11897 if (image.dataset.profileErrorBound === 'true') return; 11898 image.dataset.profileErrorBound = 'true'; 11899 const fail = () => { 11900 if (!this.state.profilePhotoFailed) this.markProfilePhotoFailed(); 11901 }; 11902 image.addEventListener('error', fail, { once: true }); 11903 if (image.complete && image.naturalWidth === 0) fail(); 11904 }); 11905 } 11906 drawProfileCrop() { 11907 if (!this.state.profileCropOpen || !this._profileCropImage) return; 11908 const canvas = document.querySelector('canvas[data-profile-crop-canvas="true"]'); 11909 if (!canvas) return; 11910 const image = this._profileCropImage; 11911 const context = canvas.getContext('2d'); 11912 if (!context || !image.naturalWidth || !image.naturalHeight) return; 11913 const size = canvas.width || 512; 11914 const zoom = Math.max(1, Number(this.state.profileCropZoom) || 1); 11915 const scale = Math.max(size / image.naturalWidth, size / image.naturalHeight) * zoom; 11916 const width = image.naturalWidth * scale; 11917 const height = image.naturalHeight * scale; 11918 const x = -(width - size) * Math.max(0, Math.min(100, Number(this.state.profileCropX) || 0)) / 100; 11919 const y = -(height - size) * Math.max(0, Math.min(100, Number(this.state.profileCropY) || 0)) / 100; 11920 context.clearRect(0, 0, size, size); 11921 context.drawImage(image, x, y, width, height); 11922 } 11923 relocateSellerBuyerPanel() { 11924 const buyers = document.getElementById('seller-buyers-panel'); 11925 const analytics = document.getElementById('profile-analytics-grid'); 11926 if (!buyers || !analytics || buyers.parentNode !== analytics.parentNode || buyers.nextElementSibling === analytics) return; 11927 analytics.insertAdjacentElement('beforebegin', buyers); 11928 } 11929 bindDragScroll() { 11930 document.querySelectorAll('[data-drag-scroll]:not([data-drag-bound])').forEach((el) => { 11931 el.dataset.dragBound = '1'; 11932 let down = false, startX = 0, startLeft = 0, moved = 0; 11933 el.addEventListener('pointerdown', (e) => { down = true; moved = 0; startX = e.clientX; startLeft = el.scrollLeft; el.style.cursor = 'grabbing'; el.setPointerCapture?.(e.pointerId); this._carPause = true; }); 11934 el.addEventListener('pointermove', (e) => { if (!down) return; const dx = e.clientX - startX; moved = Math.abs(dx); el.scrollLeft = startLeft - dx; }); 11935 const up = () => { down = false; el.style.cursor = 'grab'; setTimeout(() => { this._carPause = false; }, 4000); }; 11936 el.addEventListener('pointerup', up); el.addEventListener('pointercancel', up); el.addEventListener('pointerleave', up); 11937 el.addEventListener('click', (e) => { if (moved > 6) { e.preventDefault(); e.stopPropagation(); } }, true); 11938 el.addEventListener('mouseenter', () => { this._carHover = true; }); 11939 el.addEventListener('mouseleave', () => { this._carHover = false; }); 11940 el.addEventListener('scroll', () => { 11941 const card = el.querySelector('[data-drag-item]'); const step = card ?card.offsetWidth + 16 : 300; 11942 const real = this._monthReal || 1; 11943 const idx = ((Math.round(el.scrollLeft / step) % real) + real) % real; 11944 if (idx !== this.state.carSlide) this.setState({ carSlide: idx }); 11945 if (this._monthLoop) { 11946 clearTimeout(this._wrapT); 11947 this._wrapT = setTimeout(() => { 11948 if (!this._carCentered) return; 11949 const setW = step * real; 11950 if (setW <= 0) return; 11951 if (el.scrollLeft >= setW * 2) el.scrollLeft -= setW; 11952 else if (el.scrollLeft < setW * 0.5) el.scrollLeft += setW; 11953 }, 140); 11954 } 11955 }, { passive: true }); 11956 el.querySelectorAll('[data-drag-item]').forEach((it) => it.addEventListener('dragstart', (e) => e.preventDefault())); 11957 if (this._monthLoop) this.centerCarousel(el); 11958 this.startCarouselAuto(); 11959 }); 11960 } 11961 centerCarousel(el, tries) { 11962 tries = tries || 0; 11963 const real = this._monthReal || 1; 11964 const card = el.querySelector('[data-drag-item]'); 11965 const step = card ?card.offsetWidth + 16 : 300; 11966 const target = step * real; 11967 // esperar a que el layout refleje las 3 copias antes de centrar 11968 if (el.scrollWidth < step * real * 2.5 && tries < 30) { requestAnimationFrame(() => this.centerCarousel(el, tries + 1)); return; } 11969 el.scrollLeft = target; 11970 this._carCentered = true; 11971 } 11972 startCarouselAuto() { 11973 if (this._carTimer || !(this.props.animaciones || true)) return; 11974 this._carTimer = setInterval(() => { 11975 if (this.state.route !== 'home' || this._carPause || this._carHover) return; 11976 const el = document.querySelector('[data-drag-scroll]'); 11977 if (!el) return; 11978 const card = el.querySelector('[data-drag-item]'); 11979 const step = card ?card.offsetWidth + 16 : 300; 11980 if (!this._monthLoop) { 11981 const maxLeft = el.scrollWidth - el.clientWidth - 4; 11982 if (el.scrollLeft >= maxLeft) { el.scrollTo({ left: 0, behavior: 'smooth' }); return; } 11983 } 11984 el.scrollBy({ left: step, behavior: 'smooth' }); 11985 }, 5000); 11986 } 11987 revealEl(el) { el.style.opacity = '1'; el.style.transform = 'translateY(0)'; } 11988 bindReveals() { 11989 if (!(this.props.animaciones || true)) { document.querySelectorAll('[data-reveal]').forEach((el) => { el.dataset.bound = '1'; this.revealEl(el); }); return; } 11990 if (!this._io) this._io = new IntersectionObserver((entries) => {
11991 entries.forEach((en) => { if (en.isIntersecting) { this.revealEl(en.target); this._io.unobserve(en.target); } }); 11992 }, { root: null, rootMargin: '0px 0px -5% 0px', threshold: 0 }); 11993 const fresh = [...document.querySelectorAll('[data-reveal]:not([data-bound])')]; 11994 fresh.forEach((el, i) => { 11995 el.dataset.bound = '1'; 11996 el.style.opacity = '0'; el.style.transform = 'translateY(12px)'; 11997 el.style.transition = 'opacity 280ms cubic-bezier(0.22,0.72,0.18,1), transform 280ms cubic-bezier(0.22,0.72,0.18,1)'; 11998 el.style.transitionDelay = this.state.isMobile ?'0ms' : ((i % 5) * 24 + 'ms'); 11999 const r = el.getBoundingClientRect(); 12000 if (r.top < (window.innerHeight || 800) && r.bottom > 0) { requestAnimationFrame(() => requestAnimationFrame(() => this.revealEl(el))); } 12001 else this._io.observe(el); 12002 }); 12003 if (fresh.length) { clearTimeout(this._revealFallback); this._revealFallback = setTimeout(() => document.querySelectorAll('[data-reveal][data-bound]').forEach((el) => this.revealEl(el)), 360); } 12004 } 12005 animateStats() { 12006 if (this.state.statProgress !== 1) this.setState({ statProgress: 1 }); 12007 } 12008 animateDash() { 12009 if (this._dashRAF) cancelAnimationFrame(this._dashRAF); 12010 this._dashRAF = null; 12011 if (this.state.dashProgress !== 1) this.setState({ dashProgress: 1 }); 12012 } 12013 seededSeries(seed, n, lo, hi) { 12014 const out = []; let x = seed * 9301 + 49297; 12015 for (let i = 0; i < n; i++) { x = (x * 9301 + 49297) % 233280; out.push(lo + (x / 233280) * (hi - lo)); } 12016 return out; 12017 } 12018 linePath(vals, w, h, pad) { 12019 const max = Math.max(...vals), min = Math.min(...vals), rng = (max - min) || 1, n = vals.length; 12020 return vals.map((v, i) => { const x = pad + i * ((w - 2 * pad) / (n - 1)); const y = h - pad - ((v - min) / rng) * (h - 2 * pad); return (i ?'L' : 'M') + x.toFixed(1) + ' ' + y.toFixed(1); }).join(' '); 12021 } 12022 areaPath(vals, w, h, pad) { return this.linePath(vals, w, h, pad) + ' L ' + (w - pad).toFixed(1) + ' ' + (h - pad
12022) + ' L ' + pad + ' ' + (h - pad) + ' Z'; } 12023 async fetchMavData(force = false) { 12024 const date = this.state.mavDate; 12025 if (!date || (this.state.mavLoading && !force)) return; 12026 this.setState({ mavLoading: true, mavError: '' }); 12027 try { 12028 const query = '?fecha=' + encodeURIComponent(date); 12029 const [summaryResponse, rangesResponse] = await Promise.all([ 12030 fetch('/api/mav/resumen-diario' + query), 12031 fetch('/api/mav/segregacion-rangos-segmentos' + query), 12032 ]); 12033 if (!summaryResponse.ok || !rangesResponse.ok) throw new Error('El proveedor no devolvió información para la fecha seleccionada.'); 12034 const [summary, ranges] = await Promise.all([summaryResponse.json(), rangesResponse.json()]); 12035 const rangeRows = Array.isArray(ranges?.resultados) ?ranges.resultados : []; 12036 if (!summary || !rangeRows.length) throw new Error('No hay operaciones informadas para la fecha seleccionada.'); 12037 this.setState({ mavSummary: summary, mavRanges: rangeRows, mavLoading: false, mavError: '' }); 12038 } catch (error) { 12039 this.setState({ mavSummary: null, mavRanges: null, mavLoading: false, mavError: error.message || 'No se pudieron obtener los datos del MAV.' }); 12040 } 12041 } 12042 mavData() { 12043 const s = this.state; 12044 const summary = s.mavSummary || {}; 12045 const rows = Object.values(summary).reduce((all, value) => Array.isArray(value) ?all.concat(value) : all, []); 12046 const number = (value) => Number(value) || 0; 12047 const total = rows.reduce((sum, row) => sum + number(row.monto_total_negociado), 0); 12048 const previousTotal = rows.reduce((sum, row) => sum + number(row.monto_total_negociado_dia_anterior), 0); 12049 const weighted = (field) => total ?rows.reduce((sum, row) => sum + number(row.monto_total_negociado) * number(row[field]), 0) / total : 0; 12050 const amountDelta = previousTotal ?((total - previousTotal) / previousTotal) * 100 : 0; 12051 const money = (value) => { 12052 const n = number(value); 12053 if (n >= 1e9) return '$' + (n / 1e9).toLocaleString('es-AR', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + ' mil M'; 12054 if (n >= 1e6) return '$' + (n / 1e6).toLocaleString('es-AR', { maximumFractionDigits: 1 }) + ' M'; 12055 return '$' + Math.round(n).toLocaleString('es-AR'); 12056 }; 12057 const rate = (value) => number(value).toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + '%'; 12058 const deltaText = amountDelta ?Math.abs(amountDelta).toLocaleString('es-AR', { maximumFractionDigits: 1 }) + '% vs. rueda anterior' : ''; 12059 const ranges = ['0-30', '31-60', '61-90', '91-120', '121-180', '181-365']; 12060 const metric = s.mavMetric || 'tasa_prom_pond'; 12061 const segmentDefs = [ 12062 { key: 'avalado', label: 'Avalado', color: '#075ca8' }, 12063 { key: 'garantizado', label: 'Garantizado', color: '#16856f' }, 12064 { key: 'no garantizado', label: 'No garantizado', color: '#e84655' }, 12065 ]; 12066 const sourceRows = Array.isArray(s.mavRanges) ?s.mavRanges : []; 12067 const rawSeries = segmentDefs.map((definition) => { 12068 const source = sourceRows.find((row) => (row.segmento || '').toLowerCase() === definition.key); 12069 const byRange = new Map((source?.datos || []).map((item) => [item.rango, item])); 12070 const records = ranges.map((rangeName) => byRange.get(rangeName) || null); 12071 return { ...definition, records, values: records.map((record) => record ?number(record[metric]) : null) }; 12072 }); 12073 const allValues = rawSeries.flatMap((series) => series.values.filter((value) => value !== null)); 12074 const W = 940, H = 286, left = 58, right = 46, top = 24, bottom = 42; 12075 const rawMin = allValues.length ?Math.min(...allValues) : 0; 12076 const rawMax = allValues.length ?Math.max(...allValues) : 1; 12077 const padding = Math.max((rawMax - rawMin) * 0.14, 1); 12078 const min = Math.max(0, rawMin - padding), max = rawMax + padding, span = (max - min) || 1; 12079 const x = (index) => left + index * ((W - left - right) / (ranges.length - 1)); 12080 const y = (value) => top + ((max - value) / span) * (H - top - bottom); 12081 const mavSeries = rawSeries.map((series) => { 12082 const points = series.values.map((value, index) => { 12083 if (value === null) return null; 12084 const record = series.records[index] || {}; 12085 const point = { x: x(index), y: y(value), range: ranges[index] + ' dÃas', value: rate(value), segment: series.label, color: series.color }; 12086 const hover = { 12087 x: point.x, y: point.y, range: point.range, value: point.value, segment: point.segment, color: point.color, 12088 tna: rate(record.tasa_prom_pond), tea: rate(record.tasa_tea_pond), tem: rate(record.tasa_tem_pond), 12089 }; 12090 point.aria = point.segment + ', ' + point.range + ', ' + point.value; 12091 point.show = () => this.setState({ mavHover: hover }); 12092 point.toggle = () => this.setState({ mavHover: s.mavHover?.segment === hover.segment && s.mavHover?.range === hover.range ?null : hover }); 12093 return point; 12094 }
12094).filter(Boolean); 12095 return { ...series, points, path: points.map((point, index) => (index ?'L' : 'M') + point.x.toFixed(1) + ' ' + point.y.toFixed(1)).join(' ') }; 12096 }); 12097 const dateObj = s.mavDate ?new Date(s.mavDate + 'T12:00:00') : null; 12098 const hover = s.mavHover; 12099 const tooltipX = hover ?Math.max(8, Math.min(W - 192, hover.x > W * 0.72 ?hover.x - 194 : hover.x + 12)) : 0; 12100 const tooltipY = hover ?Math.max(8, Math.min(H - 62, hover.y - 62)) : 0; 12101 const tooltipLeft = hover ?Math.max(17, Math.min(83, (hover.x / W) * 100)) : 50; 12102 const tooltipTop = hover ?(hover.y / H) * 100 : 50; 12103 return { 12104 mavDate: s.mavDate, 12105 mavToday: new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 10), 12106 mavDateSet: (event) => this.setState({ mavDate: event.target.value, mavHover: null }, () => this.fetchMavData(true)), 12107 mavRetry: () => this.fetchMavData(true), 12108 mavLoading: !!s.mavLoading, 12109 mavHasError: !s.mavLoading && !!s.mavError, 12110 mavError: s.mavError, 12111 mavReady: !s.mavLoading && !s.mavError && !!s.mavSummary && !!s.mavRanges, 12112 mavSkeletons: [{}, {}, {}, {}], 12113 mavKpis: [ 12114 { label: 'Volumen negociado', value: money(total), delta: deltaText, deltaUp: amountDelta > 0, deltaDown: amountDelta < 0, deltaColor: amountDelta >= 0 ?'#16856f' : '#e84655' }, 12115 { label: 'TNA promedio', value: rate(weighted('tasa_prom_pond')), delta: 'Ponderada por volumen', deltaColor: '#74859a' }, 12116 { label: 'TEA promedio', value: rate(weighted('tasa_tea_ponderada')), delta: 'Tasa efectiva anual', deltaColor: '#74859a' }, 12117 { label: 'TEM promedio', value: rate(weighted('tasa_tem_ponderada')), delta: 'Tasa efectiva mensual', deltaColor: '#74859a' }, 12118 ], 12119 mavMetricTabs: [ 12120 { key: 'tasa_prom_pond', label: 'TNA' }, 12121 { key: 'tasa_tea_pond', label: 'TEA' }, 12122 { key: 'tasa_tem_pond', label: 'TEM' }, 12123 ].map((item) => { 12124 const active = metric === item.key; 12125 return { label: item.label, pick: () => this.setState({ mavMetric: item.key, mavHover: null }), bg: active ?'#003b5c' : '#fff', color: active ?'#fff' : '#52657a', border: active ?'#003b5c' : '#d8e3ed' }; 12126 }), 12127 mavW: W, mavH: H, mavSeries, 12128 mavXTicks: ranges.map((label, index) => ({ x: x(index), label: label + ' dÃas' })), 12129 mavYTicks: [0, 0.25, 0.5, 0.75, 1].map((step) => { const value = min + (max - min) * step; return { y: y(value), label: rate(value) }; }), 12130 mavDateLabel: dateObj ?dateObj.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' }) : '', 12131 mavTooltipVisible: !!hover, 12132 mavTooltipHide: () => this.setState({ mavHover: null }), 12133 mavTooltipX: tooltipX, mavTooltipY: tooltipY, 12134 mavTooltipDotX: tooltipX + 13, mavTooltipDotY: tooltipY + 18, 12135 mavTooltipTextX: tooltipX + 23, mavTooltipTitleY: tooltipY + 21, mavTooltipValueY: tooltipY + 40, 12136 mavTooltipColor: hover?.color || '#075ca8', 12137 mavTooltipTitle: hover ?hover.segment + ' · ' + hover.range : '', 12138 mavTooltipValue: hover ?({ tasa_prom_pond: 'TNA', tasa_tea_pond: 'TEA', tasa_tem_pond: 'TEM' }[metric] || 'Tasa') + ' ' + hover.value : '', 12139 mavTooltipLeft: tooltipLeft, 12140 mavTooltipTop: tooltipTop, 12141 mavTooltipTransform: hover && hover.y < 105 ?'translate(-50%, 16px)' : 'translate(-50%, calc(-100% - 14px))', 12142 mavTooltipRates: hover ? [ 12143 { label: 'TNA', value: hover.tna }, 12144 { label: 'TEA', value: hover.tea }, 12145 { label: 'TEM', value: hover.tem }, 12146 ] : [], 12147 }; 12148 } 12149 mercadoData() { 12150 const arr = this.state.indices || []; 12151 const s = this.state; 12152 const defs = [ 12153 { key: 'piri', label: 'PIRI', dec: 2, unit: '$/kg', compact: false, desc: 'Invernada' }, 12154 { key: 'pirc', label: 'PIRC', dec: 0, unit: '$/vientre', compact: true, desc: 'CrÃa' }, 12155 { key: 'indice_ternero', label: 'Ãndice Ternero', dec: 2, unit: '$/kg', compact: false, desc: 'Ternero' }, 12156 ]; 12157 const active = defs.find((d) => d.key === s.mercIndex) || defs[0]; 12158 const fullSeries = this.indexSeries(active.key); 12159 const range = [6,12,24].includes(s.mercRange) ? s.mercRange : 12; 12160 const series = range >= 999 ?fullSeries : fullSeries.slice(-range); 12161 const vals = series.map((p) => p.v); 12162 const W = 920, H = 300, PAD = 46;
12163 const max = Math.max(...vals, 1), min = Math.min(...vals, 0); 12164 12165 const n = vals.length; 12166 const px = (i) => PAD + (n <= 1 ?0 : (i / (n - 1)) * (W - 2 * PAD)); 12167 const py = (v) => { const rng = (max - min) || 1; return H - PAD - ((v - min) / rng) * (H - 2 * PAD); }; 12168 const pts = series.map((p, i) => ({ x: px(i), y: py(p.v), left: (px(i)/W*100)+'%', top: (py(p.v)/H*100)+'%', focus: event => event.currentTarget.focus(), v: p.v, label: p.label, vFmt: active.compact ?this.fmtCompact(p.v) : this.fmtMoney(p.v, active.dec) })); 12169 const line = pts.map((p,i)=>(i?'L':'M')+p.x+' '+p.y).join(' '); 12170 const area = n > 1 ? line+' L'+pts[n-1].x+' '+(H-PAD)+' L'+pts[0].x+' '+(H-PAD)+' Z' : ''; 12171 const last = series[n - 1], first = series[0]; 12172 const cur = last ?last.v : 0, prev = n > 1 ?series[n - 2].v : 0; 12173 const pct = prev ?((cur - prev) / prev) * 100 : 0; 12174 const yoy = first ?((cur - first.v) / first.v) * 100 : 0; 12175 const avg = vals.length ?vals.reduce((a, b) => a + b, 0) / vals.length : 0; 12176 const gEvery = Math.ceil(n / 8) || 1; 12177 const xLabels = series.map((p, i) => ({ x: px(i), left: (px(i)/W*100)+'%', label: p.label, show: i % gEvery === 0 || i === n - 1 })); 12178 const yTicks = [0, 0.25, 0.5, 0.75, 1].map((t) => { const v = min + t * (max - min); return { y: py(v), top: (py(v)/H*100)+'%', label: active.compact ?this.fmtCompact(v) : this.fmtMoney(v, 0) }; }); 12179 const latest = arr[0]; 12180 const tipos = latest?.tipos || []; 12181 const tipoObj = tipos.find((t) => t.titulo === s.mercTipo) || tipos[0]; 12182 const cats = (tipoObj?.categorias || []).filter((c) => (c.razas || []).some((r) => r.precio > 0) || c.precio > 0); 12183 const razaSet = []; 12184 cats.forEach((c) => (c.razas || []).forEach((r) => { if (!razaSet.includes(r.titulo)) razaSet.push(r.titulo); })); 12185 const allPrices = []; 12186 cats.forEach((c) => (c.razas || []).forEach((r) => { if (r.precio > 0) allPrices.push(r.precio); })); 12187 const hMax = Math.max(...allPrices, 1), hMin = Math.min(...allPrices, 0); 12188 const heatColor = (v) => { if (!v) return 'rgba(148,163,184,0.10)'; const t = (v - hMin) / ((hMax - hMin) || 1); const l = 92 - t * 52; return 'hsl(155 62% ' + l + '%)'; }; 12189 const heatText = (v) => { if (!v) return '#b6c2d1'; const t = (v - hMin) / ((hMax - hMin) || 1); return t > 0.55 ?'#fff' : '#0d3a2a'; }; 12190 const heatRows = cats.map((c) => ({ 12191 cat: c.titulo, 12192 cells: razaSet.map((rz) => { const r = (c.razas || []).find((x) => x.titulo === rz); const v = r ?r.precio : 0; return { v, bg: heatColor(v), color: heatText(v), fmt: v ?(active.compact ?this.fmtCompact(v) : '$' + Math.round(v).toLocaleString('es-AR')) : 'â' }; }), 12193 promFmt: c.precio ?(active.compact ?this.fmtCompact(c.precio) : '$' + Math.round(c.precio).toLocaleString('es-AR')) : 'â', 12194 })); 12195 return { 12196 mercTabs: defs.map((d) => ({ label: d.label, desc: d.desc, active: d.key === active.key, pick: () => this.setState({ mercIndex: d.key }), bg: d.key === active.key ?'#002e5f' : '#fff', color: d.key === active.key ?'#fff' : '#3d4c5f', border: d.key === active.key ?'#002e5f' : '#dde7f2' })), 12197 mercActive: active.label, mercUnit: active.unit, 12198 mercHasData: n > 1, mercW: W, mercH: H, mercLine: line, mercArea: area, 12199 mercPts: pts, mercXLabels: xLabels, mercYTicks: yTicks, 12200 mercCur: active.compact ?this.fmtCompact(cur) : this.fmtMoney(cur, active.dec), 12201 mercRollEl: this.rollEl(active.compact ?this.fmtCompact(cur) : this.fmtMoney(cur, active.dec)), 12202 mercDrawKey: active.key + '-' + range + '-' + n, 12203 mercPct: (pct > 0 ?'+' : '') + pct.toFixed(1) + '%', mercPctColor: pct >= 0 ?'#1fbf75' : '#ff5a6a', 12204 mercPctArrow: pct >= 0 ?'M6 14l6-6 6 6' : 'M6 10l6 6 6-6', 12205 mercYoy: (yoy > 0 ?'+' : '') + yoy.toFixed(1) + '%', mercYoyColor: yoy >= 0 ?'#1fbf75' : '#ff5a6a', 12206 mercMax: active.compact ?this.fmtCompact(max) : this.fmtMoney(max, active.dec), 12207 mercMin: active.compact ?this.fmtCompact(min) : this.fmtMoney(min, active.dec), 12208 mercAvg: active.compact ?this.fmtCompact(avg) : this.fmtMoney(avg, active.dec), 12209 mercPeriod: series.length ?(first.label + ' · ' + last.label) : '', 12210 mercMonthLabel: latest ?this.MES_ES[(latest.mes_remate || 1) - 1] + ' ' + latest.anio_remate : '', 12211 mercTipoTabs: tipos.map((t) => ({ label: t.titulo, active: t.titulo === (tipoObj?.titulo), pick: () => this.setState({ mercTipo: t.titulo }), bg: t.titulo === (tipoObj?.titulo) ?'#1f7a63' : '#fff', color: t.titulo === (tipoObj?.titulo) ?'#fff' : '#3d4c5f', border: t.titulo === (tipoObj?.titulo) ?'#1f7a63' : '#dde7f2' })), 12212 mercRazas: razaSet, heatRows, 12213 mercRangeTabs: [{ n: 6, l: '6 meses' }, { n: 12, l: '12 meses' }, { n: 24, l: '24 meses' }].map((o) => { const on = (s.mercRange || 12) === o.n; return { label: o.l, pick: () => this.setState({ mercRange: o.n }), chipBg: on ?'#2fe08d' : 'rgba(255,255,255,0.09)', chipColor: on ?'#04213f' : '#c8d8ea' }; }), 12214 mercNoData: !arr.length, 12215 }; 12216 } 12217 normProv(v) { 12218 const s = (v || '').toString().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z ]/g, ' ').replace(/\s+/g, ' ').trim(); 12219 if (!s) return ''; 12220 if ((s.includes('ciudad') && s.includes('buenos aires')) || s === 'caba' || s === 'cap' || s === 'capital' || s.includes('capital federal') || s.includes('distrito federal')) return 'caba'; 12221 if (s.includes('tierra del fuego') || s === 'tf') return 'tierra del fuego'; 12222 const codes = { 12223 ba: 'buenos aires', cat: 'catamarca', cba: 'cordoba', cor: 'cordoba', cord: 'cordoba', cts: 'corrientes', ctes: 'corrientes', corr: 'corrientes', 12224 cho: 'chaco', chc: 'chaco', chu: 'chubut', chb: 'chubut', chub: 'chubut', er: 'entre rios', ers: 'entre rios', f: 'formosa', for: 'formosa', fsa: 'formosa', 12225 juj: 'jujuy', juy: 'jujuy', jy: 'jujuy', lp: 'la pampa', lr: 'la rioja', mza: 'mend
12225oza', men: 'mendoza', mdz: 'mendoza', mis: 'misiones', mnes: 'misiones', 12226 nqn: 'neuquen', neu: 'neuquen', rn: 'rio negro', rng: 'rio negro', sal: 'salta', sj: 'san juan', sl: 'san luis', sc: 'santa cruz', scz: 'santa cruz', 12227 sf: 'santa fe', sfe: 'santa fe', se: 'santiago del estero', sde: 'santiago del estero', sgo: 'santiago del estero', tuc: 'tucuman', tm: 'tucuman', 12228 }; 12229 if (codes[s]) return codes[s]; 12230 return s; 12231 } 12232 buildGeo(geo) { 12233 const polys = []; let minX = 1e18, minY = 1e18, maxX = -1e18, maxY = -1e18; 12234 geo.features.forEach((f) => { 12235 const g = f.geometry; if (!g) return; 12236 const mp = g.type === 'Polygon' ?[g.coordinates] : (g.type === 'MultiPolygon' ?g.coordinates : []); 12237 mp.forEach((poly) => poly.forEach((ring) => ring.forEach((pt) => { const x = pt[0], y = pt[1]; if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; }))); 12238 polys.push({ name: (f.properties && f.properties.name) || '', mp }); 12239 }); 12240 const W = 560, H = 760, P = 12; 12241 const k = Math.min((W - 2 * P) / ((maxX - minX) || 1), (H - 2 * P) / ((maxY - minY) || 1)); 12242 const ox = (W - (maxX - minX) * k) / 2, oy = (H - (maxY - minY) * k) / 2; 12243 const X = (x) => ox + (x - minX) * k, Y = (y) => H - oy - (y - minY) * k; 12244 this._geoBox = { minX, minY, maxX, maxY, k, ox, oy, W, H }; 12245 this._geoT = (geo['hc-transform'] && geo['hc-transform']['default']) || null; 12246 this._geoProvs = polys.filter((p) => p.name).map((p) => { 12247 let d = ''; 12248 p.mp.forEach((poly) => poly.forEach((ring) => { 12249 if (ring.length < 3) return; 12250 d += 'M' + ring.map((pt) => X(pt[0]).toFixed(1) + ' ' + Y(pt[1]).toFixed(1)).join('L') + 'Z'; 12251 })); 12252 return { key: this.normProv(p.name), name: p.name, d }; 12253 }); 12254 } 12255 buildAgg(list) { 12256 const agg = {}, catCount = {}, pts = {}, monthsByYear = {}; 12257 const bump = (y, c, p, pk, pc, heads, loc, cons) => { 12258 const a = ((agg[y] = agg[y] || {})[c] = agg[y][c] || {}); 12259 const cell = (a[p] = a[p] || { sk: 0, nk: 0, sc: 0, nc: 0, lots: 0, heads: 0, locs: {}, cons: {} }); 12260 if (pk > 0) { cell.sk += pk; cell.nk++; } 12261 if (pc > 0) { cell.sc += pc; cell.nc++; } 12262 cell.lots++; cell.heads += heads; 12263 if (loc) { const L = (cell.locs[loc] = cell.locs[loc] || { sk: 0, nk: 0, sc: 0, nc: 0, lots: 0 }); if (pk > 0) { L.sk += pk; L.nk++; } if (pc > 0) { L.sc += pc; L.nc++; } L.lots++; } 12264 if (cons) { const C = (cell.cons[cons] = cell.cons[cons] || { sk: 0, nk: 0, sc: 0, nc: 0, lots: 0, heads: 0 }); if (pk > 0) { C.sk += pk; C.nk++; } if (pc > 0) { C.sc += pc; C.nc++; } C.lots++; C.heads += heads; } 12265 }; 12266 (list || []).forEach((r) => { 12267 const d = new Date(r.fecha_remate || r.fecha || ''); 12268 const y = isNaN(d) ?null : d.getFullYear(); if (!y || y < 2000) return; 12269 const m = d.getMonth() + 1; 12270 const ym = y * 100 + m; 12271 const p = this.normProv(r.nombre_provincia || r.provincia); if (!p) return; 12272 const cat = ((r.nombre_categoria || r.categoria || '') + '').trim() || 'Sin categorÃa'; 12273 const pk = +(r.precio_kilo_conciliado || r.precio_kilo || r.precio_kilo_remate || 0) || 0; 12274 const pc = +(r.precio_cabeza_conciliado || r.precio_cabeza || r.precio_cabeza_remate || 0) || 0; 12275 if (!pk && !pc) return; 12276 const heads = +(r.cantidad || 0) || 0; 12277 const loc = ((r.nombre_localidad || r.localidad || '') + '').trim(); 12278 const cons = ((r.consignataria || r.nombre_cuenta || r.nombre_consignatario || '') + '').trim(); 12279 catCount[cat] = (catCount[cat] || 0) + 1; 12280 (monthsByYear[y] = monthsByYear[y] || {})[m] = true; 12281 bump(y, cat, p, pk, pc, heads, loc, cons); bump(y, '__all__', p, pk, pc, heads, loc, cons); 12282 bump(ym, cat, p, pk, pc, heads, loc, cons); bump(ym, '__all__', p, pk, pc, heads, loc, cons); 12283 let lat = +r.latitud, lng = +r.longitud; 12284 if ((!lat || !lng) && typeof r.latlong === 'string' && r.latlong.includes(',')) { const ll = r.latlong.split(','); lat = +ll[0]; lng = +ll[1]; } 12285 if (lat < -20 && lat > -56 && lng < -52 && lng > -76) { 12286 [y, ym].forEach((per) => [cat, '__all__'].forEach((c3) => { 12287 const P2 = ((pts[per] = pts[per] || {})[c3] = pts[per][c3] || {});
12288 const kk = p + '|' + (loc || 's/l'); 12289 const o = (P2[kk] = P2[kk] || { name: loc || 'Sin localidad', prov: p, latS: 0, lngS: 0, n: 0, sk: 0, nk: 0, sc: 0, nc: 0, lots: 0 }); 12290 o.latS += lat; o.lngS += lng; o.n++; if (pk > 0) { o.sk += pk; o.nk++; } if (pc > 0) { o.sc += pc; o.nc++; } o.lots++; 12291 })); 12292 } 12293 }); 12294 this._mapPts = pts; 12295 this._mapAgg = agg; 12296 this._mapYears = Object.keys(agg).map(Number).filter((k) => k < 10000).sort((a, b) => a - b); 12297 this._monthsByYear = {}; 12298 Object.keys(monthsByYear).forEach((y2) => { this._monthsByYear[y2] = Object.keys(monthsByYear[y2]).map(Number).sort((a, b) => a - b); }); 12299 this._mapCats = Object.keys(catCount).sort((a, b) => catCount[b] - catCount[a]).slice(0, 8); 12300 } 12301 heatColor(t) { 12302 t = Math.max(0, Math.min(1, t)); 12303 const stops = [[38, 128, 196], [96, 178, 128], [240, 168, 48], [225, 45, 58]]; 12304 const f = t * (stops.length - 1); const i = Math.min(stops.length - 2, Math.floor(f)); const lt = f - i; 12305 const a = stops[i], b = stops[i + 1]; 12306 return 'rgb(' + Math.round(a[0] + (b[0] - a[0]) * lt) + ',' + Math.round(a[1] + (b[1] - a[1]) * lt) + ',' + Math.round(a[2] + (b[2] - a[2]) * lt) + ')'; 12307 } 12308 bestMapYear() { 12309 const agg = this._mapAgg || {}; let best = null, bestLots = -1; 12310 Object.keys(agg).forEach((y) => { const cells = agg[y]['__all__'] || {}; let lots = 0; Object.values(cells).forEach((c) => { lots += c.lots || 0; }); if (lots > bestLots) { bestLots = lots; best = +y; } }); 12311 return best; 12312 } 12313 async fetchMapa() { 12314 const url = '/api/preciosporlote'; 12315 let geo = await this.unwrap('https://cdn.jsdelivr.net/npm/@highcharts/map-collection@2/countries/ar/ar-all.geo.json'); 12316 if (!geo || !geo.features) geo = await this.unwrap('https://code.highcharts.com/mapdata/countries/ar/ar-all.geo.json'); 12317 if (geo && geo.features) this.buildGeo(geo); 12318 // The range endpoint is slow, so pull year-by-year (priced years first) and 12319 // render progressively as each batch lands. 12320 const y = new Date().getFullYear(); 12321 const years = [y, y - 1, y - 2, y - 3, y - 4]; 12322 this._mapTargetYears = years.slice().sort((a, b) => a - b); 12323 this._rawRows = []; 12324 let anyReal = false; 12325 this.setState({ mapFetching: true }); 12326 for (let i = 0; i < years.length; i++) { 12327 const yr = years[i]; 12328 let offset = 0; 12329 for (let pg = 0; pg < 5; pg++) { 12330 let d = null; 12331 try { d = await this.unwrap(url + '?from=' + yr + '-01-01&to=' + yr + '-12-31&limit=2500&offset=' + offset); } catch (e) { console.warn('precios ' + yr, e); } 12332 const items = d && Array.isArray(d.items) ?d.items : (Array.isArray(d) ?d : null); 12333 if (!items || !items.length) break; 12334 for (let k = 0; k < items.length; k++) this._rawRows.push(items[k]); 12335 this.buildAgg(this._rawRows); 12336 this.calibrateProj(); 12337 if (this._mapYears && this._mapYears.length) { 12338 anyReal = true; 12339 const patch = { mapReady: true, mapDemo: false }; 12340 if (!this._userPickedYear && !this._mapYearInit) { patch.mapYear = this._mapYears[this._mapYears.length - 1]; this._mapYearInit = true; } 12341 this.setState(patch); 12342 } 12343 if (d.hasMore === false) break; 12344 offset = d.nextOffset != null ?d.nextOffset : offset + items.length; 12345 } 12346 } 12347 if (!anyReal) this.setState({ mapReady: !!this._geoProvs, mapDemo: false }); 12348 this.setState({ mapFetching: false }); 12349 } 12350 toggleMapPlay() { 12351 this._userPickedYear = true; 12352 if (this._mapPlayT) { clearInterval(this._mapPlayT); this._mapPlayT = null; this.forceUpdate(); return; } 12353 const year = this.state.mapYear; 12354 const months = (this._monthsByYear || {})[year] || []; 12355 if (months.length >= 2) { 12356 let i = months.indexOf(this.state.mapMonth); 12357 const step = () => { i = (i + 1) % months.length; this.setState({ mapMonth: months[i] }); }; 12358 this._mapPlayT = setInterval(step, 1300); 12359 step(); 12360 return; 12361 } 12362 const years = this._mapYears || []; if (years.length < 2) return; 12363 const step = () => { const i = years.indexOf(this.state.mapYear); this.setState({ mapYear: years[(i + 1) % years.length], mapMonth: null }); }; 12364 this._mapPlayT = setInterval(step, 1500); 12365 step(); 12366 } 12367 mapTipMove(e, name, txt) { 12368 const svg = e.currentTarget.ownerSVGElement; if (!svg || !svg.parentElement) return; 12369 const host = svg.parentElement.getBoundingClientRect(); 12370 this.setState({ mapTip: { x: e.clientX - host.left, y: e.clientY - host.top, name, txt } }); 12371 } 12372 calibrateProj() { 12373 this._projOk = false; this._hasPts = false;
12374 if (!this._geoBox || !this._mapPts) return; 12375 const sample = []; 12376 Object.values(this._mapPts).forEach((byCat) => { const all = byCat['__all__'] || {}; Object.values(all).forEach((o) => { if (sample.length < 300) sample.push({ lat: o.latS / o.n, lng: o.lngS / o.n }); }); }); 12377 if (!sample.length) return; 12378 this._hasPts = true; 12379 const b = this._geoBox, mx = (b.maxX - b.minX) * 0.03, my = (b.maxY - b.minY) * 0.03; 12380 const inB = (c) => c && isFinite(c[0]) && isFinite(c[1]) && c[0] >= b.minX - mx && c[0] <= b.maxX + mx && c[1] >= b.minY - my && c[1] <= b.maxY + my; 12381 let a = 0, f = 0; 12382 sample.forEach((p2) => { if (inB(this.projRaw(p2.lat, p2.lng, false))) a++; if (inB(this.projRaw(p2.lat, p2.lng, true))) f++; }); 12383 this._projFlip = f > a; 12384 this._projOk = Math.max(a, f) >= sample.length * 0.5; 12385 } 12386 projRaw(lat, lng, flip) { 12387 const t = this._geoT; 12388 try { 12389 if (t && t.crs && window.proj4) { 12390 const p = window.proj4(t.crs).forward([lng, lat]); 12391 const res = (t.scale || 1) * (t.jsonres || 1); 12392 const x = (p[0] - (t.xoffset || 0)) * res + (t.jsonmarginX || 0); 12393 const y = flip ?((t.yoffset || 0) - p[1]) * res - (t.jsonmarginY || 0) : (p[1] - (t.yoffset || 0)) * res + (t.jsonmarginY || 0); 12394 return [x, y]; 12395 } 12396 } catch (e) {} 12397 const b = this._geoBox; 12398 return [b.minX + ((lng + 73.6) / 20.1) * (b.maxX - b.minX), b.minY + ((lat + 55.1) / 33.4) * (b.maxY - b.minY)]; 12399 } 12400 geoSvgXY(x, y) { const b = this._geoBox; return [b.ox + (x - b.minX) * b.k, b.H - b.oy - (y - b.minY) * b.k]; } 12401 mapaData() { 12402 const s = this.state, agg = this._mapAgg, provs = this._geoProvs; 12403 if (!s.mapReady || !agg || !provs) return { mapReady: false, mapLoading: true, mapFetching: true, mapDemo: !!s.mapDemo, mapYearLabel: '', mapMetricLabel: 'promedio $/kg', mapMetricTabs: [], mapCatTabs: [] }; 12404 const years = this._mapYears; 12405 const year = years.includes(s.mapYear) ?s.mapYear : years[years.length - 1]; 12406 const cat = s.mapCat || '__all__'; 12407 const metric = s.mapMetric || 'kg'; 12408 const monthsY = (this._monthsByYear || {})[year] || [];
12409 const month = s.mapMonth || null; 12410 const useMonth = !!(month && monthsY.includes(month)); 12411 const periodKey = useMonth ?year * 100 + month : year; 12412 const byProv = (agg[periodKey] || {})[cat] || {}; 12413 const val = (c) => !c ?0 : (metric === 'kg' ?(c.nk ?c.sk / c.nk : 0) : (c.nc ?c.sc / c.nc : 0)); 12414 const fmtV = (v) => metric === 'kg' ?this.fmtMoney(v, 0) : this.fmtCompact(v); 12415 const present = provs.map((p) => ({ p, c: byProv[p.key], v: val(byProv[p.key]) })).filter((x) => x.v > 0); 12416 const vMin = present.length ?Math.min.apply(null, present.map((x) => x.v)) : 0; 12417 const vMax = present.length ?Math.max.apply(null, present.map((x) => x.v)) : 0; 12418 const tt = (v) => vMax > vMin ?(v - vMin) / (vMax - vMin) : 0.5; 12419 const col = (v) => v ?'hsl(211 64% ' + (90 - tt(v) * 61).toFixed(1) + '%)' : '#edf2f8'; 12420 const sel = s.mapSel; 12421 const unit = metric === 'kg' ?' $/kg' : ' $/cab'; 12422 const view = (s.mapView === 'loc' && this._projOk) ?'loc' : 'prov'; 12423 const ptsMap = ((this._mapPts || {})[periodKey] || {})[cat] || {}; 12424 const yearPts = ((this._mapPts || {})[year] || {})[cat] || {}; 12425 const dotsRaw = view === 'loc' ? Object.values(ptsMap).map((o) => { 12426 const v = metric === 'kg' ?(o.nk ?o.sk / o.nk : 0) : (o.nc ?o.sc / o.nc : 0); 12427 const raw = this.projRaw(o.latS / o.n, o.lngS / o.n, this._projFlip); 12428 const c2 = this.geoSvgXY(raw[0], raw[1]); 12429 return { o, v, cx: c2[0], cy: c2[1] }; 12430 }).filter((d2) => d2.v > 0 && d2.cx >= 0 && d2.cx <= 560 && d2.cy >= 0 && d2.cy <= 760) : []; 12431 const heatUnion = view === 'loc' ? Object.keys(yearPts).map((kk) => { 12432 const base = yearPts[kk]; 12433 const raw = this.projRaw(base.latS / base.n, base.lngS / base.n, this._projFlip); 12434 const c2 = this.geoSvgXY(raw[0], raw[1]); 12435 const cell = ptsMap[kk]; 12436 const v = cell ?(metric === 'kg' ?(cell.nk ?cell.sk / cell.nk : 0) : (cell.nc ?cell.sc / cell.nc : 0)) : 0; 12437 return { kk, cx: c2[0], cy: c2[1], v, lots: cell ?cell.lots : 0, name: base.name, prov: base.prov }; 12438 }).filter((d2) => d2.cx >= 0 && d2.cx <= 560 && d2.cy >= 0 && d2.cy <= 760).sort((a2, b2) => a2.kk < b2.kk ?-1 : 1) : []; 12439 const dMin = dotsRaw.length ?Math.min.apply(null, dotsRaw.map((d2) => d2.v)) : 0; 12440 const dMax = dotsRaw.length ?Math.max.apply(null, dotsRaw.map((d2) => d2.v)) : 0; 12441 const dT = (v) => dMax > dMin ?(v - dMin) / (dMax - dMin) : 0.5; 12442 const dCol = (v) => 'hsl(211 70% ' + (78 - dT(v) * 50).toFixed(1) + '%)'; 12443 const mapProvs = provs.map((p) => { 12444 const c = byProv[p.key], v = val(c); 12445 const isSel = sel === p.key; 12446 return { 12447 d: p.d, fill: view === 'loc' ?'#eef2f7' : col(v), stroke: isSel ?'#e63946' : '#fff', sw: isSel ?'2' : '0.8', 12448 click: () => this.setState({ mapSel: isSel ?null : p.key }), 12449 move: (e) => this.mapTipMove(e, p.name, v ?fmtV(v) + unit + ' · ' + (c ?c.lots : 0) + ' lotes' : 'Sin datos en el perÃodo'), 12450 leave: () => this.setState({ mapTip: null }), 12451 }; 12452 }); 12453 const ranked = present.slice().sort((a, b) => b.v - a.v); 12454 const totLots = present.reduce((a, x) => a + (x.c.lots || 0), 0); 12455 const selEntry = sel ?provs.find((p) => p.key === sel) : null; 12456 const selCell = sel ?byProv[sel] : null; 12457 const selVal = val(selCell); 12458 const locs = selCell ?Object.entries(selCell.locs).map(([n, L]) => ({ n, v: metric === 'kg' ?(L.nk ?L.sk / L.nk : 0) : (L.nc ?L.sc / L.nc : 0) })).filter((l) => l.v > 0).sort((a, b) => b.v - a.v).slice(0, 4) : []; 12459 const locMax = locs.length ?Math.max.apply(null, locs.map((l) => l.v)) : 1; 12460 const consList = selCell && selCell.cons ?Object.entries(selCell.cons).map(([n, C]) => ({ n, lots: C.lots, v: metric === 'kg' ?(C.nk ?C.sk / C.nk : 0) : (C.nc ?C.sc / C.nc : 0) })).filter((c3) => c3.v > 0).sort((a, b) => b.lots - a.lots).slice(0, 5) : []; 12461 const consMax = consList.length ?Math.max.apply(null, consList.map((c3) => c3.v)) : 1; 12462 const catLabel = cat === '__all__' ?'Todas las categorÃas' : cat; 12463 const tip = s.mapTip; 12464 return { 12465 mapReady: true, mapLoading: false, mapDemo: !!s.mapDemo, mapFetching: !!s.mapFetching, 12466 mapYearLabel: '' + year,
12467 mapLotsLabel: totLots ?totLots.toLocaleString('es-AR') + ' lotes · ' + catLabel : 'Sin datos para el perÃodo', 12468 mapMetricLabel: metric === 'kg' ?'promedio $/kg' : 'promedio $/cabeza', 12469 mapMetricUnit: metric === 'kg' ?'$/kg promedio' : '$/cabeza promedio', 12470 mapMetricTabs: [{ k: 'kg', l: '$ / kg' }, { k: 'cab', l: '$ / cabeza' }].map((m) => { const on = metric === m.k; return { label: m.l, pick: () => this.setState({ mapMetric: m.k }), bg: on ?'#002e5f' : '#fff', color: on ?'#fff' : '#3d4c5f', border: on ?'#002e5f' : '#dde7f2' }; }), 12471 mapCatTabs: [{ k: '__all__', l: 'Todas' }].concat((this._mapCats || []).map((c2) => ({ k: c2, l: c2 }))).map((o) => { const on = cat === o.k; return { label: o.l, pick: () => this.setState({ mapCat: o.k }), bg: on ?'#0a5ca8' : '#fff', color: on ?'#fff' : '#3d4c5f', border: on ?'#0a5ca8' : '#dde7f2' }; }), 12472 mapYearTabs: [...new Set([].concat(this._mapYears || [], this._mapTargetYears || []))].sort((a2, b2) => a2 - b2).map((y2) => { const loaded = (this._mapYears || []).includes(y2); const loading = !loaded && !!s.mapFetching; const on = y2 === year; return { label: '' + y2, loading, loaded, pick: () => { if (!loaded) return; this._userPickedYear = true; if (this._mapPlayT) { clearInterval(this._mapPlayT); this._mapPlayT = null; } this.setState({ mapYear: y2, mapMonth: null }); }, bg: on ?'#002e5f' : '#f2f6fb', color: on ?'#fff' : (loaded ?'#3d4c5f' : '#a9b6c6'), border: on ?'#002e5f' : '#e3ecf5' }; }), 12473 mapHasMonths: view !== 'prov' ?monthsY.length >= 1 : monthsY.length >= 2, 12474 mapMonthMax: monthsY.length, 12475 mapMonthIndex: useMonth ?monthsY.indexOf(month) + 1 : 0, 12476 mapMonthLabel: useMonth ?(this.MES_ES[month - 1].toUpperCase() + ' ' + year) : ('AÃO COMPLETO ' + year), 12477 mapMonthSet: (e) => { const v = +e.target.value; this._userPickedYear = true; if (this._mapPlayT) { clearInterval(this._mapPlayT); this._mapPlayT = null; } this.setState({ mapMonth: v === 0 ?null : monthsY[v - 1] }); }, 12478 mapPlayIcon: this._mapPlayT ?'M7 5h4v14H7zM13 5h4v14h-4z' : 'M8 5l12 7-12 7z', 12479 mapPlayToggle: () => this.toggleMapPlay(), 12480 mapProvs, 12481 mapLegendMin: view === 'loc' ?(dotsRaw.length ?fmtV(dMin) : 'â') : (present.length ?fmtV(vMin) : 'â'), 12482 mapLegendMax: view === 'loc' ?(dotsRaw.length ?fmtV(dMax) : 'â') : (present.length ?fmtV(vMax) : 'â'), 12483 mapHasPts: !!(this._projOk && this._hasPts), 12484 mapViewTabs: [{ k: 'prov', l: 'Provincias' }, { k: 'loc', l: 'Localidades' }].map((m2) => { const on = view === m2.k; return { label: m2.l, pick: () => this.setState({ mapView: m2.k }), bg: on ?'#002e5f' : 'transparent', color: on ?'#fff' : '#3d4c5f' }; }), 12485 mapHeatView: view === 'loc', mapProvView: view !== 'loc', 12486 mapClip: view === 'loc' ?provs.map((p) => ({ d: p.d })) : [], 12487 mapHeat: heatUnion.map((d2) => { 12488 const on = d2.v > 0; 12489 return { 12490 key: d2.kk, cx: d2.cx.toFixed(1), cy: d2.cy.toFixed(1), 12491 r: on ?Math.min(62, 22 + Math.sqrt(d2.lots) * 3.6).toFixed(1) : '0', 12492 fill: on ?this.heatColor(dT(d2.v)) : 'rgb(38,128,196)', op: on ?'0.62' : '0', 12493 }; 12494 }), 12495 mapDots: heatUnion.filter((d2) => d2.v > 0).sort((a2, b2) => b2.lots - a2.lots).map((d2) => { 12496 const provName = (provs.find((pp) => pp.key === d2.prov) || {}).name || d2.prov; 12497 return { 12498 key: d2.kk, cx: d2.cx.toFixed(1), cy: d2.cy.toFixed(1), r: Math.max(7, Math.min(14, 5 + Math.sqrt(d2.lots) * 1.1)).toFixed(1), 12499 click: () => this.setState({ mapSel: d2.prov }), 12500 move: (e) => this.mapTipMove(e, d2.name + ' · ' + provName, fmtV(d2.v) + unit + ' · ' + d2.lots + ' lotes'), 12501 leave: () => this.setState({ mapTip: null }), 12502 }; 12503 }), 12504 mapTipShow: !!tip, mapTipX: tip ?tip.x + 'px' : '0px', mapTipY: tip ?tip.y + 'px' : '0px', mapTipName: tip ?tip.name : '', mapTipTxt: tip ?tip.txt : '', 12505 mapHasSel: !!(sel && selCell), mapNoSel: !(sel && selCell), 12506 mapSelKey: (sel || '') + year + metric, 12507 mapSelName: selEntry ?selEntry.name : '', mapClearSel: () => this.setState({ mapSel: null }), 12508 mapSelValRoll: this.rollEl(selVal ?fmtV(selVal) : 'â'), 12509 mapSelStats: selCell ? [
12510 { label: 'LOTES', val: selCell.lots.toLocaleString('es-AR') }, 12511 { label: 'CABEZAS', val: selCell.heads.toLocaleString('es-AR') }, 12512 { label: '$/KG PROM', val: selCell.nk ?this.fmtMoney(selCell.sk / selCell.nk, 0) : 'â' }, 12513 { label: '$/CAB PROM', val: selCell.nc ?this.fmtCompact(selCell.sc / selCell.nc) : 'â' }, 12514 ] : [], 12515 mapSelHasLocs: locs.length > 0, 12516 mapSelLocs: locs.map((l) => ({ name: l.n, val: fmtV(l.v), pct: Math.round((l.v / locMax) * 100) + '%' })), 12517 mapSelHasCons: consList.length > 0, 12518 mapSelCons: consList.map((c3) => ({ name: c3.n, val: fmtV(c3.v), lots: c3.lots + (c3.lots === 1 ?' lote' : ' lotes'), pct: Math.round((c3.v / consMax) * 100) + '%' })), 12519 mapCatLabel: catLabel, 12520 mapRanking: ranked.slice(0, 6).map((x, i) => ({ pos: i + 1, name: x.p.name, val: fmtV(x.v), pct: Math.round(tt(x.v) * 88 + 12) + '%', barBg: i === 0 ?'linear-gradient(90deg, #0a5ca8, #002e5f)' : '#9db9d6', pick: () => this.setState({ mapSel: x.p.key }) })), 12521 }; 12522 } 12523 async unwrap(url, opts) { 12524 try { 12525 const normalizedUrl = String(url || '').replace(/\s+\|\|\s+/, '?'); 12526 const r = await fetch(normalizedUrl, opts); 12527 if (!r.ok) throw new Error(r.status); 12528 const j = await r.json(); 12529 return (j && typeof j === 'object' && 'data' in j) ?j.data : j; 12530 } catch { return null; } 12531 } 12532 scrollHomeChannels(direction) { 12533 const list = document.querySelector('.home-channel-programs'); 12534 list?.scrollBy({left:direction*Math.max(180,list.clientWidth*.8),behavior:'smooth'}); 12535 } 12536 homeChannelItems() { 12537 return [...(this.state.monthRemates || []), ...(this.state.rosganRemates || []), ...(this.state.cabanas || []), ...(this.state.videos || []).filter(v => v.scheduledStartTime || v.actualStartTime)]; 12538 } 12539 selectHomeChannel(item, event) { 12540 if (event && (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)) return; 12541 const selected = window.RosganHomeChannel.normalizeSpot(item) || window.RosganHomeChannel.normalize(item); 12542 event?.preventDefault(); 12543 if (!selected) { this.showToast('Esta transmisión todavÃa no tiene video asignado.'); return; } 12544 this.setState({ homeChannelManual: selected }, () => document.getElementById('home-channel-player')?.scrollIntoView({ behavior: 'smooth', block: 'center' })); 12545 } 12546 homeChannelView() { 12547 const api = window.RosganHomeChannel; 12548 const now = this.state.homeChannelNow || Date.now(); 12549 const items = this.homeChannelItems(); 12550 const spots = (this.state.homeSpots || []).filter(spot => !(this.state.homeSpotFailures || []).includes(spot.id)); 12551 const selected = api.select({ items, cafes: this.state.cafeVideos || [], spots, spotIndex: this.state.homeSpotIndex || 0, manual: this.state.homeChannelManual, now }); 12552 const seen = new Set(); 12553 const today = [...items, ...(this.state.cafeVideos || [])].map(item => ({item, video:api.normalize(item)})).filter(({video}) => { 12554 if (!video || video.day !== api.clock(now).day || seen.has(video.id)) return false; 12555 seen.add(video.id); return true; 12556 }); 12557 return { 12558 hcIsSpot: !!selected?.spot, hcHasYoutube: !!selected && !selected.spot, hcSpotUrl: selected?.videoUrl || '', 12559 hcSpotLoop: spots.length === 1 ? 'true' : 'false', 12560 hcSpotEnded: () => { const currentIndex = spots.findIndex(spot => api.normalizeSpot(spot)?.id === selected?.id); const nextIndex = (currentIndex + 1) % Math.max(1,spots.length); this.setState({homeSpotIndex:nextIndex, ...(this.state.homeChannelManual?.spot ? {homeChannelManual:api.normalizeSpot(spots[nextIndex])} : {})}); }, 12561 hcSpotError: () => { if (selected?.spot) this.setState({homeSpotFailures:[...(this.state.homeSpotFailures || []), selected.id.slice(5)]}); }, 12562 hcHasVideo: !!selected, hcEmpty: !selected, hcPlayerKey:selected?.spot ? selected.id + ':' + (this.state.homeSpotIndex || 0) : selected?.id || '', hcEmbed:selected?.embed || '', hcTitle:selected?.title || 'La programación de hoy', hcHref:selected?.href || '', 12563 hcManual:!!this.state.homeChannelManual, hcAutomatic:() => this.setState({homeChannelManual:null, homeChannelNow:Date.now()}), 12564 hcLiveClass:selected?.live ? 'is-live' : '', hcBadge:selected?.spot ? 'INSTITUCIONAL' : selected?.live ? 'â V
12564IVO' : selected?.ended ? 'GRABADO' : selected ? 'PROGRAMADO' : 'ROSGAN', 12565 hcReason:selected?.reason || 'No hay transmisiones programadas.', hcEmptyTitle:this.state.monthRemates===null ? 'Cargando programaciónâ¦' : 'Sin programación disponible', 12566 hcHasSpots:spots.length>0, hcSpots:spots.map(spot=>({title:spot.title || 'Spot institucional',selected:api.normalizeSpot(spot)?.id===selected?.id,select:()=>this.selectHomeChannel(spot)})), 12567 hcHasToday:today.length>0, hcToday:today.map(({item,video})=>({title:video.title,label:video.cafe ? (video.start && api.clock(video.start).minute >= 720 ? 'Café de la Tarde' : 'Café Ganadero') : (item.nombre_cuenta || item.consignatariaNombre || video.title),thumb:video.thumb,selected:video.id===selected?.id,time:video.hasTime ? new Intl.DateTimeFormat('es-AR',{timeZone:'America/Argentina/Buenos_Aires',hour:'2-digit',minute:'2-digit',hourCycle:'h23'}).format(new Date(video.start))+' h' : 'Horario a confirmar',select:()=>this.selectHomeChannel(item)})), hcPreviousChannels:()=>
12567this.scrollHomeChannels(-1), hcNextChannels:()=>this.scrollHomeChannels(1), 12568 }; 12569 } 12570 async refreshHomeChannel() { 12571 if (this.state.route !== 'home') return; 12572 this.setState({ homeChannelNow: Date.now() }); 12573 if (this._homeDataRequest || this._homeChannelRefreshing || Date.now()-(this._homeChannelFetchedAt || 0)<300000) return; 12574 this._homeChannelRefreshing = true; 12575 this._homeChannelFetchedAt = Date.now(); 12576 try { 12577 const [channel,cafe,spots] = await Promise.all([this.unwrap('/api/youtube/channel'),this.unwrap('/api/youtube/cafe-ganadero'),window.RosganHomeChannel.fetchSpots().catch(() => null)]); 12578 const patch = {}; 12579 if (spots) patch.homeSpots = spots; 12580 if (channel) patch.videos = [...(channel.live || []),...(channel.upcoming || []),...(channel.recent || [])]; 12581 if (cafe) patch.cafeVideos = cafe.cafeGanadero?.videos || cafe.videos || []; 12582 this.setState(patch); 12583 await this.fetchMonthRemates(); 12584 } finally { this._homeChannelRefreshing = false; } 12585 } 12586 async fetchData() { 12587 if (this._homeDataRequest) return this._homeDataRequest; 12588 // Publish each section as it arrives; public content must not wait for 12589 // authentication, the private Feria list, or a slow YouTube response. 12590 const sessionEpoch = this._sessionEpoch || 0; 12591 const publish = patch => { 12592 if (sessionEpoch === (this._sessionEpoch || 0)) this.setState(patch); 12593 }; 12594 let channelCafe = null; 12595 const jobs = [ 12596 this.fetchMonthRemates(), this.loadRosganRemateAdmin(), 12597 this.fetchNews(), this.fetchIndices(), 12598 window.RosganHomeChannel.fetchSpots().then(spots => publish({homeSpots:spots})).catch(() => {}), 12599 this.unwrap('/api/consignatarias').then(cons => { 12600 const items = Array.isArray(cons) ? cons : (cons?.items || []); 12601 publish({ consignatarias: Array.isArray(items) ? items : [] }); 12602 }), 12603 this.unwrap('/api/youtube/channel').then(channel => { 12604 channelCafe = channel?.cafeGanadero; 12605 publish({ videos: [...(channel?.live || []), ...(channel?.upcoming || []), ...(channel?.recent || [])], 12606 ...(channelCafe ? { cafe: channelCafe, cafeVideos: channelCafe.videos || [] } : {}) }); 12607 }), 12608 this.unwrap('/api/youtube/cafe-ganadero').then(cafe => { 12609 const value = channelCafe || cafe?.cafeGanadero || cafe || {}; 12610 publish({ cafe: value, cafeVideos: value.videos || [] }); 12611 }), 12612 (async () => { 12613 const token = await this.authToken(); 12614 const ferias = token ? await this.unwrap('/api/ferias', { headers: { Accept: 'application/json', Authorization: 'Bearer ' + token } }) : null; 12615 const items = Array.isArray(ferias) ? ferias : (ferias?.items || ferias?.ferias || []); 12616 publish({ ferias: Array.isArray(items) ? items : [] }); 12617 })(), 12618 ]; 12619 this._homeChannelFetchedAt = Date.now(); 12620 this._homeDataRequest = Promise.allSettled(jobs); 12621 try { await this._homeDataRequest; } 12622 finally { this._homeDataRequest = null; } 12623 } 12624 async fetchNews() { 12625 const nb = 'https://us-central1-studio-4500177996-48a75.cloudfunctions.net'; 12626 const [lote, noti] = await Promise.all([ 12627 this.unwrap(nb + '/getLoteNoticias'), 12628 this.unwrap(nb + '/getNoticiasConHero'), 12629 ]); 12630 this.setState({ 12631 loteNoticias: Array.isArray(lote) ?lote : (Array.isArray(lote?.items) ?lote.items : []), 12632 noticias: Array.isArray(noti) ?noti : (Array.isArray(noti?.items) ?noti.items : []), 12633 }); 12634 } 12635 async fetchIndices() { 12636 const response = await this.apiFetch('/api/market-indices'); 12637 const idx = response?.data; 12638 if (!idx || !Array.isArray(idx) || !idx.length) { 12639 this.setState({ indices: [] }); 12640 return; 12641 } 12642 const sorted = idx.slice().sort((a, b) => new Date(b.fecha_remate) - new Date(a.fecha_remate)); 12643 this.setState({ indices: sorted }); 12644 } 12645 MES_ES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic']; 12646 DigitRollComp() { 12647 if (this._Roll) return this._Roll; 12648 const R = React; 12649 this._Roll = class extends R.Component { 12650 render() { 12651 const p = this.props; 12652 const chars = String(p.text == null ?'' : p.text).split(''); 12653 return R.createElement('span', { style: { display: 'inline-flex', alignItems: 'flex-end', lineHeight: 1 } }, 12654 chars.map((ch, i) => { 12655 if (/[0-9]/.test(ch)) { 12656 const d = parseInt(ch, 10); 12657 const reel = []; 12658 for (let k = 0; k <= 9; k++) reel.push(R.createElement('span', { key: k, style: { display: 'block', height: '1em', lineHeight: '1em' } }, String(k))); 12659 return R.createElement('span', { key: i, style: { display: 'inline-block', height: '1em', lineHeight: '1em', overflow: 'hidden', verticalAlign: 'bottom' } }, 12660 R.createElement('span', { style: { display: 'block', transform: 'translateY(-' + d + 'em)', transition: 'transform 850ms cubic-bezier(0.16,1.02,0.3,1)', transitionDelay: (i * 55) + 'ms' } }, reel)); 12661 } 12662 return R.createElement('span', { key: i, style: { display: 'inline-block' } }, ch); 12663 })); 12664 } 12665 }; 12666 return this._Roll; 12667 } 12668 rollEl(text) { return React.createElement(this.DigitRollComp(), { text }); } 12669 fmtMoney(n, dec) { const v = Number(n) || 0;
12669 return '$' + v.toLocaleString('es-AR', { minimumFractionDigits: dec || 0, maximumFractionDigits: dec || 0 }); } 12670 fmtCompact(n) { const v = Number(n) || 0; if (v >= 1e6) return '$' + (v / 1e6).toFixed(2) + 'M'; if (v >= 1e3) return '$' + (v / 1e3).toFixed(0) + 'k'; return '$' + Math.round(v); } 12671 indexSeries(key) { return (this.state.indices || []).slice().reverse().map((d) => ({ v: Number(d[key]) || 0, label: this.MES_ES[(d.mes_remate || 1) - 1] + ' ' + String(d.anio_remate).slice(2), fecha: d.fecha_remate })).filter((p) => p.v > 0); } 12672 prevNonZero(key, fromIdx) { const arr = this.state.indices || []; for (let i = fromIdx + 1; i < arr.length; i++) { if (Number(arr[i][key]) > 0) return Number(arr[i][key]); } return null; } 12673 indexCards() { 12674 const arr = this.state.indices || []; 12675 if (!arr.length) return []; 12676 const defs = [ 12677 { key: 'piri', label: 'PIRI', sub: 'Precio Ãndice Rosgan Invernada', dec: 2, unit: '/kg' }, 12678 { key: 'pirc', label: 'PIRC', sub: 'Precio Ãndice Rosgan CrÃa', dec: 0, unit: '/vientre', compact: true }, 12679 { key: 'indice_ternero', label: 'Ãndice Ternero', sub: 'Promedio ternero invernada', dec: 2, unit: '/kg' }, 12680 ]; 12681 return defs.map((d) => { 12682 let ci = 0; while (ci < arr.length && !(Number(arr[ci][d.key]) > 0)) ci++; 12683 const cur = Number(arr[ci]?.[d.key]) || 0; 12684 const prev = this.prevNonZero(d.key, ci); 12685 const pct = prev ?((cur - prev) / prev) * 100 : 0; 12686 const dir = pct > 0.05 ?'up' : pct < -0.05 ?'down' : 'flat'; 12687 return { 12688 ...d, cur, prev, pct, dir, 12689 curFmt: d.compact ?this.fmtCompact(cur) : this.fmtMoney(cur, d.dec), 12690 rollEl: this.rollEl(d.compact ?this.fmtCompact(cur) : this.fmtMoney(cur, d.dec)), 12691 pctFmt: (pct > 0 ?'+' : '') + pct.toFixed(1) + '%', 12692 color: dir === 'up' ?'#1fbf75' : dir === 'down' ?'#ff5a6a' : '#c8d8ea', 12693 arrow: dir === 'up' ?'M6 14l6-6 6 6' : dir === 'down' ?'M6 10l6 6 6-6' : 'M6 12h12', 12694 mes: this.MES_ES[(arr[ci]?.mes_remate || 1) - 1] + ' ' + arr[ci]?.anio_remate, 12695 }; 12696 }); 12697 } 12698 async fetchAgendaMonth(month) { 12699 const [historical, ferias] = await Promise.all([ 12700 this.unwrap('/api/remateshistorico?scope=month&year=' + month.slice(0,4) + '&month=' + month.slice(5)), 12701 this.unwrap('/api/ferias/agenda?month=' + encodeURIComponent(month)), 12702 ]); 12703 const seen = new Set(); 12704 const items = [...(ferias?.items || []), ...(historical?.items || [])].filter(item => { 12705 const video = item.youtubeVideoId || item.youtubeApiId; 12706 const key = video ?'youtube:' + video : this.consKey(item.nombre_remate || '') + '|' + String(item.fecha_remate || '').slice(0,10) + '|' + this.consKey(item.nombre_cuenta || ''); 12707 if (seen.has(key)) return false; 12708 seen.add(key); return true; 12709 }); 12710 return {items}; 12711 } 12712 async fetchMonthRemates() { 12713 const start = new Date(this.homeAgendaDay(new Date())); 12714 const end = new Date(start.getTime() + 30 * 86400000); 12715 const months = new Set(); 12716 for (let date = new Date(start); date <= end; date.setUTCDate(date.getUTCDate() + 1)) months.add(date.toISOString().slice(0, 7)); 12717 const results = await Promise.allSettled([...months].map((month) => this.fetchAgendaMonth(month))); 12718 const seen = new Set(); 12719 const items = results.flatMap((result) => result.status === 'fulfilled' && Array.isArray(result.value?.items) ?result.value.items : []).filter((item) => { 12720 const key = String(item.id || item.id_remate || [item.nombre_remate, item.fecha_remate, item.nombre_cuenta].join('|')); 12721 if (seen.has(key)) return false; 12722 seen.add(key); return true; 12723 }); 12724 this.setState({ monthRemates: items }); 12725 this.loadRemateListCatalogs(items); 12726 if (items.length) this.startHeroAuto(); 12727 } 12728 startHeroAuto() { 12729 if (!(this.props.animaciones || true)) return; 12730 clearInterval(this._heroTimer); 12731 this._heroTimer = setInterval(() => { 12732 const n = this._heroCount || 1; 12733 if (this.state.route === 'home' && n > 0 && !this._heroGesture && Date.now() > (this._heroPauseUntil || 0)) this.selectHomeHero((this.state.heroSlide + 1) % n); 12734 const hn = (this.homeHeroImages || []).length; 12735 if (this.state.route === 'home' && hn > 1) this.setState((s) => ({ homeHeroSlide: (s.homeHeroSlide + 1) % hn })); 12736 const nn = this._notiCount || 0; 12737 if (this.state.route === 'home' && nn > 1) this.setState((s) => ({ notiSlide: (s.notiSlide + 1) % nn })); 12738 }, 10000); 12739 } 12740 moveHomeHero(step) { 12741 const count = this._heroCount || 1; 12742 this._heroPauseUntil = Date.now() + 10000; 12743 this.selectHomeHero(((this.state.heroSlide + step) % count + count) % count); 12744 } 12745 rosganBanner(ordinal = 0) { 12746 return Math.abs(ordinal) % 2 ?{ src:'assets/home/rosgan-remate-bolsa.webp', position:'center 45%' } : { src:'assets/home/rosgan-remate-microfono.webp', position:'center 58%' }; 12747 } 12748 rosganAnniversaryImage(item) {
12749 return String(item?.id || item?.id_remate || '') === '5' 12750 && this.consKey(item?.nombre_remate || item?.titulo) === 'granremateaniversario' 12751 ?'assets/home/gran-remate-aniversario-ecu-2026.webp' : ''; 12752 } 12753 selectHomeHero(index) { 12754 const rosgan = this._homeHeroRosganIndices || []; 12755 this.setState(s => { 12756 let visit = Number.isInteger(s.homeRosganBannerIndex) ?s.homeRosganBannerIndex : (rosgan.includes(s.heroSlide) ?0:-1); 12757 if (rosgan.includes(index) && (index !== s.heroSlide || this._heroCount === 1)) visit++; 12758 return { heroSlide:index, homeRosganBannerIndex:visit }; 12759 }); 12760 } 12761 startHomeHeroGesture(event) { 12762 if (event.isPrimary === false || (event.pointerType === 'mouse' && event.button !== 0) || (this._heroCount || 0) < 2) return; 12763 if (event.target.closest('a,button,input,select,.home-hero-actions,.home-hero-indices,.home-hero-dots')) return; 12764 this._heroGesture = { id: event.pointerId, x: event.clientX, y: event.clientY }; 12765 event.currentTarget.setPointerCapture(event.pointerId); 12766 } 12767 endHomeHeroGesture(event, cancelled = false) { 12768 const start = this._heroGesture; 12769 this._heroGesture = null; 12770 if (!start || start.id !== event.pointerId) return; 12771 if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId); 12772 const dx = event.clientX - start.x, dy = event.clientY - start.y; 12773 if (!cancelled && Math.abs(dx) >= 45 && Math.abs(dx) > Math.abs(dy) * 1.4) this.moveHomeHero(dx < 0 ?1 : -1); 12774 } 12775 histHeroThumb(it) { 12776 const anniversaryImage = this.rosganAnniversaryImage(it); 12777 if (anniversaryImage) return anniversaryImage; 12778 const branded = it.bannerUrl || it.backgroundUrl || this.consBackground(this.remateConsignatariaRef(it)); 12779 if (branded) return branded; 12780 const d = it.youtube?.thumbnailUrl || it.thumbnailUrl; 12781 if (d) return d; 12782 const id = it.youtubeApiId || it.youtubeVideoId || it.youtube?.id; 12783 if (id) return 'https://i.ytimg.com/vi/' + id + '/maxresdefault.jpg'; 12784 return ''; 12785 } 12786 histThumb(it) { return this.histHeroThumb(it) || 'assets/feria.png'; } 12787 async loadHomeCalendar(month) { 12788 const today = new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Argentina/Buenos_Aires', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date());
12789 month = month || this.state.homeCalendarMonth || today.slice(0, 7); 12790 this.setState({ homeCalendarMonth: month, homeCalendarDay: '', homeCalendarPage: 0 }); 12791 this._calendarRequest = month; 12792 if (this.state.homeCalendarCache?.[month]) { this.setState({ homeCalendarLoading: false, homeCalendarError: '' }); return; } 12793 this.setState({ homeCalendarLoading: true, homeCalendarError: '' }); 12794 try { 12795 const data = await this.fetchAgendaMonth(month); 12796 if (!Array.isArray(data?.items)) throw new Error('Invalid agenda'); 12797 this.setState(s => ({ homeCalendarCache: { ...s.homeCalendarCache, [month]: data.items } })); 12798 if (this._calendarRequest === month) this.setState({ homeCalendarLoading: false }); 12799 } catch { 12800 if (this._calendarRequest === month) this.setState({ homeCalendarLoading: false, homeCalendarError: 'No pudimos completar la agenda. Volvé a intentar.' }); 12801 } 12802 } 12803 homeCalendarView() { 12804 const s = this.state, tz = 'America/Argentina/Buenos_Aires'; 12805 const dayOf = date => date && !isNaN(date) ?new Intl.DateTimeFormat('en-CA', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' }).format(date) : '';
12806 const today = dayOf(new Date()), month = s.homeCalendarMonth || today.slice(0, 7); 12807 const [year, mon] = month.split('-').map(Number); 12808 const monthDate = new Date(Date.UTC(year, mon - 1, 1, 12)); 12809 const selected = s.homeCalendarDay || ''; 12810 const candidates = [ 12811 ...(s.cabanas || []).filter(it => !this.isExcludedCabana(it)).map(it => ({ it, kind: 'Cabañas', date: this.cabanaAuctionStartAt(it), open: () => this.openCabana(it) })), 12812 ...(s.rosganRemates || []).filter(it => !this.isExcludedRemate(it)).map(it => ({ it, kind: 'Rosgan', date: this.remateDate(it), open: () => this.openRemate(it) })), 12813 ...(s.homeCalendarCache?.[month] || s.monthRemates || []).filter(it => !this.isExcludedRemate(it)).map(it => ({ it, kind: this.remateType(it)[0], date: this.remateDate(it), open: () => this.openRemate(it) })), 12814 ]; 12815 const seen = new Set(); 12816 const events = candidates.map(({ it, kind, date, open }) => { 12817 const raw = it.fecha_remate || it.fecha || it.date || it.schedule?.fecha || ''; 12818 const day = /^\d{4}-\d{2}-\d{2}$/.test(raw) ?raw : dayOf(date); 12819 const title = it.nombre_remate || it.titulo || it.title || it.nombre || 'Remate'; 12820 const cons = this.remateConsignatariaRef(it) || 'Rosgan'; 12821 const isRosgan = this.consKey(cons) === 'rosgan'; 12822 const whiteLogo = isRosgan ?'' : this.consWhiteLogo(cons); 12823 const consLogo = isRosgan ?'assets/rosgan-logo-blue.png' : whiteLogo || this.consLogo(cons); 12824 const place = it.ubicacion_remate || it.lugar || it.location || it.ubicacion; 12825 const clock = it.hora_remate || it.hora || it.schedule?.hora; 12826 return { day, title, kind: /cabaña|cabana/i.test(title)?'Cabañas':kind, open, key: this.consKey(title) + '|' + day, 12827 time: clock ?String(clock).slice(0, 5) + ' h' : date && !/^\d{4}-\d{2}-\d{2}$/.test(raw) ?date.toLocaleTimeString('es-AR', { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12:false }) + ' h' : 'Horario a confirmar', 12828 location: typeof place === 'string' ?place : [place?.lugar || place?.nombre || place?.name || place?.address, place?.localidad || place?.locality, place?.provinciaNombre || place?.provincia || place?.province].filter(v=>typeof v==='string'&&v).join(', ') || 'Ubicación a confirmar', 12829 cons: this.consRecord(cons)?.name || this.consRecord(cons)?.nombre || cons, 12830 consLogo, hasConsLogo:!!consLogo, noConsLogo:!consLogo, consLogoBg:whiteLogo?this.consBrandColor(cons):isRosgan||consLogo?'#fff':'#002e5f', consInitial:String(cons).trim().slice(0,1).toUpperCase(), 12831 number: day.slice(8), dateLabel: day ?new Date(day + 'T12:00:00Z').toLocaleDateString('es-AR', { timeZone: tz, weekday: 'short', day: 'numeric', month: 'short' }) : '', 12832 }; 12833 }).filter(e => { if (!e.day.startsWith(month) || seen.has(e.key)) return false; seen.add(e.key); return true; }).sort((a,b) => a.day.localeCompare(b.day) || a.time.localeCompare(b.time)); 12834 const days = [], offset = (monthDate.getUTCDay() + 6) % 7; 12835 const total = new Date(Date.UTC(year, mon, 0)).getUTCDate(); 12836 for (let i=0; i<Math.ceil((offset+total)/7)*7; i++) { 12837 const number=i-offset+1, valid=number>0&&number<=total; 12838 const key=valid?month+'-'+String(number).padStart(2,'0'):''; 12839 const count=valid?events.filter(e=>e.day===key).length:0; 12840 days.push({ number: valid?number:'', disabled:!valid, label:valid?`${number} de ${monthDate.toLocaleDateString('es-AR',{month:'long',timeZone:tz})}: ${count} remates`:'Fuera del mes', 12841 css: 'agenda-day'+(!valid?' is-blank':'')+(key===today?' is-today':'')+(key===selected?' is-selected':'')+(count?' has-events':''), 12842 count, hasEvents:count>0, pressed:valid&&selected===key?'true':'false', pick:()=>valid&&this.setState({homeCalendarDay:selected===key?'':key,homeCalendarPage:0}) }); 12843 } 12844 const move = delta => this.loadHomeCalendar(new Date(Date.UTC(year,mon-1+delta,1,12)).toISOString().slice(0,7)); 12845 const visible = events.filter(e=>!selected||e.day===selected); 12846 const pageSize = s.isMobile ?1:3; 12847 const pages = Math.ceil(visible.length / pageSize);
12848 const page = Math.min(Math.max(0, Number(s.homeCalendarPage) || 0), Math.max(0, pages - 1)); 12849 return { calendarMonthLabel: monthDate.toLocaleDateString('es-AR',{month:'long',year:'numeric',timeZone:tz}), calendarDays:days, 12850 calendarWeekdays:['Lun','Mar','Mié','Jue','Vie','Sáb','Dom'].map(label=>({label})), calendarEvents:visible.slice(page*pageSize,page*pageSize+pageSize), 12851 calendarEventDays:days.filter(d=>d.hasEvents), calendarMonthClass:'agenda-month'+(s.homeCalendarExpanded?' is-expanded':''), 12852 calendarExpanded:s.homeCalendarExpanded?'true':'false',calendarToggleLabel:s.homeCalendarExpanded?'Ocultar calendario':'Ver calendario',calendarToggle:()=>this.setState({homeCalendarExpanded:!s.homeCalendarExpanded}), 12853 calendarHasPages:pages>1, calendarPageLabel:s.isMobile?`${page+1} de ${visible.length} remates`:`${page*3+1}â${Math.min(page*3+3,visible.length)} de ${visible.length} remates`, 12854 calendarPageFirst:page===0, calendarPageLast:page>=pages-1, 12855 calendarPagePrev:()=>this.setState({homeCalendarPage:Math.max(0,page-1)}), 12856 calendarPageNext:()=>this.setState({homeCalendarPage:Math.max(0,pages-1,0)>page?page+1:page}), 12857 calendarCount:events.length+' remates este mes', calendarListTitle:selected?new Date(selected+'T12:00:00Z').toLocaleDateString('es-AR',{weekday:'long',day:'numeric',month:'long',timeZone:tz}):'Todos los remates del mes', 12858 calendarEmpty:!visible.length&&!s.homeCalendarLoading, calendarLoading:!!s.homeCalendarLoading, calendarError:s.homeCalendarError||'', calendarHasError:!!s.homeCalendarError, 12859 calendarPrev:()=>move(-1), calendarNext:()=>move(1), calendarToday:()=>this.loadHomeCalendar(today.slice(0,7)), calendarAll:()=>this.setState({homeCalendarDay:'',homeCalendarPage:0}), calendarRetry:()=>this.loadHomeCalendar(month) }; 12860 } 12861 youtubeOriginalThumb(it) { 12862 const direct = it?.youtube?.thumbnailUrl || it?.youtube?.thumbnail || it?.thumbnailUrl || it?.thumbnail; 12863 if (direct) return direct; 12864 const id = it?.youtubeApiId || it?.youtubeVideoId || it?.youtube?.id || this.ytId(it?.url_streaming || it?.youtubeUrl || ''); 12865 return id ?'https://i.ytimg.com/vi/' + id + '/maxresdefault.jpg' : 'assets/feria.png'; 12866 } 12867 histUrl(it) { const id = it.youtubeApiId || it.youtubeVideoId || it.youtubeStreamConfig?.broadcastId; return it.url_streaming || it.urlStream || it.youtubeStreamConfig?.watchUrl || (id ?'https://www.youtube.com/watch?v=' + id : 'https://www.youtube.com/@rosgantv'); } 12868 histMode(it) { const d = this.remateDate(it); if (!d) return 'recent'; const now = new Date(); if (d <= now && now <= new Date(d.getTime() + 4 * 3600e3)) return 'live'; return d > now ?'upcoming' : 'recent'; } 12869 homeAgendaDay(value) { 12870 const date = new Date(value); 12871 if (!value || Number.isNaN(date.getTime())) return ''; 12872 return new Intl.DateTimeFormat('en-CA', { timeZone: 'America/Argentina/Buenos_Aires', year: 'numeric', month: '2-digit', day: '2-digit' }).format(date); 12873 } 12874 homeAgendaIncludes(date, now = new Date()) { 12875 const day = this.homeAgendaDay(date); 12876 const start = this.homeAgendaDay(now); 12877 const end = new Date(new Date(start).getTime() + 30 * 86400000).toISOString().slice(0, 10); 12878 return !!day && day >= start && day <= end; 12879 } 12880 async fetchLots() { 12881 this.setState({ lotsLoading: true }); 12882 try { 12883 const data = await this.apiFetch('/api/Lots?limit=100'); 12884 const raw = this.listFrom(data, ['data', 'items', 'lots']); 12885 const lots = raw.map((l, i) => this.mapLot(l, i)); 12886 const selectedExisting = (this.state.lots || []).find((lot) => lot.cert === this.state.selectedCert); 12887 if (selectedExisting && !lots.some((lot) => lot.cert === selectedExisting.cert)) lots.push(selectedExisting); 12888 this.setState({ lots, lotsReal: true, lotsLoading: false }, () => { 12889 if (this.state.route === 'lot' && this.state.selectedCert) { 12890 this.fetchLotBids(this.state.selectedCert); 12891 this.fetchLotSellerConsignataria(this.currentLot()); 12892 } 12893 }); 12894 } catch { 12895 const selectedExisting = (this.state.lots || []).find((lot) => lot.cert === this.state.selectedCert); 12896 this.setState({ lots: selectedExisting ?[selectedExisting] : [], lotsReal: !!selectedExisting, lotsLoading: false }); 12897 } 12898 } 12899 async fetchRoslotLotByCert(cert) { 12900 const targetCert = String(cert || '').trim(); 12901 if (!targetCert || this._roslotLotFetchCert === targetCert) return; 12902 this._roslotLotFetchCert = targetCert; 12903 try { 12904 const data = await this.apiFetch('/api/Lots/' + encodeURIComponent(targetCert)); 12905 const root = data?.data ?? data?.lot ?? data?.item ?? data; 12906 const raw = root?.lot ?? root?.item ?? root; 12907 if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return; 12908 const mapped = this.mapLot(raw, 0); 12909 if (mapped.cert !== targetCert) return; 12910 this.setState((previous) => ({ 12911 lots: [mapped, ...(previous.lots || []).filter((lot) => lot.cert !== targetCert)], 12912 lotsReal: true, 12913 }), () => { 12914 if (this.state.route === 'lot' && this.state.selectedCert === targetCert) { 12915 this.fetchLotBids(targetCert); 12916 this.fetchLotSellerConsignataria(mapped); 12917 } 12918 }); 12919 } catch (error) { 12920 if (![404, 410].includes(Number(error?.status))) console.warn('[Roslot lot deep link failed]', targetCert, error?.message || error); 12921 } finally { 12922 if (this._roslotLotFetchCert === targetCert) this._roslotLotFetchCert = ''; 12923 } 12924 } 12925 async syncAuctionStatuses() { 12926 if (this._auctionStatusSyncing || document.hidden) return; 12927 this._auctionStatusSyncing = true; 12928 try { 12929 const data = await this.apiFetch('/api/Lots?limit=100'); 12930 const raw = this.listFrom(data, ['data', 'items', 'lots']); 12931 const mappedLots = raw.map((lot, index) => this.mapLot(lot, index)); 12932 const patch = {};
12933 const lotsSignature = JSON.stringify(mappedLots); 12934 if (lotsSignature !== this._auctionLotsSignature) { 12935 this._auctionLotsSignature = lotsSignature; 12936 patch.lots = mappedLots; 12937 patch.lotsReal = true; 12938 } 12939 if (this.state.user && this.canManageSellerData(this.state.apiUser)) { 12940 try { 12941 const mineData = await this.apiFetch('/api/Lots/mine?limit=100'); 12942 const mineRaw = this.listFrom(mineData, ['data', 'items', 'lots']); 12943 const mappedMine = mineRaw.map((lot, index) => this.mapLot(lot, index)); 12944 const mineSignature = JSON.stringify(mappedMine); 12945 if (mineSignature !== this._auctionMineSignature) { 12946 this._auctionMineSignature = mineSignature; 12947 patch.myLots = mappedMine; 12948 } 12949 } catch {} 12950 } 12951 if (Object.keys(patch).length) this.setState(patch); 12952 } catch {} 12953 finally { this._auctionStatusSyncing = false; } 12954 } 12955 async syncLiveBids() { 12956 const canBuy = this.canUseBuyerData(this.state.apiUser); 12957 const canSell = this.canManageSellerData(this.state.apiUser); 12958 if (this._liveBidSyncing || document.hidden || !this.state.user || (!canBuy && !canSell)) return; 12959 this._liveBidSyncing = true; 12960 try { 12961 let myBids = this.state.myBids || []; 12962 if (canBuy) { 12963 const bidsData = await this.apiFetch('/api/Bids/mine?limit=50'); 12964 myBids = this.ownBidsOnly(this.listFrom(bidsData, ['items', 'bids', 'data']).map((bid) => this.normBid(bid)).filter(Boolean)); 12965 } 12966 const currentLots = this.state.lots || []; 12967 const openCerts = new Set(currentLots.filter((lot) => lot.auctionStatus === 'open').map((lot) => lot.cert)); 12968 const buyerCerts = myBids 12969 .filter((bid) => openCerts.has(bid.cert) && !/(reject|rechaz|declin|lost)/i.test(bid.status)) 12970 .map((bid) => String(bid.cert || '')) 12971 .filter(Boolean); 12972 const sellerCerts = canSell 12973 ? (this.state.myLots || []).filter((lot) => ['open', 'sold'].includes(lot.auctionStatus)).map((lot) => String(lot.cert || '')).filter(Boolean) 12974 : []; 12975 const activeCerts = [...new Set([...buyerCerts, ...sellerCerts])].slice(0, 24); 12976 const bidResults = await Promise.all(activeCerts.map(async (cert) => { 12977 try { 12978 const lotBidsData = await this.apiFetch('/api/Lots/bids/' + encodeURIComponent(cert)); 12979 const bids = this.listFrom(lotBidsData, ['items', 'bids', 'data']).map((bid) => this.normBid(bid, cert)).filter(Boolean).sort((a, b) => b.amount - a.amount); 12980 return [cert, bids]; 12981 } catch { return null; } 12982 })); 12983 let pendingBids = this.state.pendingBids || []; 12984 if (canSell) { 12985 try { 12986 const pendingData = await this.apiFetch('/api/Bids/pending-approvals?limit=50'); 12987 pendingBids = this.listFrom(pendingData, ['items', 'bids', 'data']).map((bid) => this.normBid(bid)).filter(Boolean); 12988 } catch {} 12989 } 12990 const nextLotBids = { ...(this.state.lotBids || {}), ...Object.fromEntries(bidResults.filter(Boolean)) }; 12991 const liveSignature = JSON.stringify([canBuy ?myBids : null, canSell ?pendingBids : null, nextLotBids]); 12992 if (liveSignature !== this._liveBidSignature) { 12993 this._liveBidSignature = liveSignature; 12994 this.setState({ ...(canBuy ?{ myBids } : {}), ...(canSell ?{ pendingBids } : {}), lotBids: nextLotBids }); 12995 } 12996 const now = Date.now(); 12997 if (!this._lastCreditSyncAt || now - this._lastCreditSyncAt > 30000) { 12998 this._lastCreditSyncAt = now; 12999 if (this.canManageBuyerAccounts(this.state.apiUser)) this.fetchBuyerAuths(); 13000 } 13001 } catch {} 13002 finally { this._liveBidSyncing = false; } 13003 } 13004 listFrom(data, keys = []) { 13005 if (Array.isArray(data)) return data; 13006 for (const key of keys) { 13007 const value = data?.[key]; 13008 if (Array.isArray(value)) return value; 13009 if (value && typeof value === 'object') { 13010 for (const nested of ['items', 'data', 'lots', 'bids', 'establishments', 'authorizations', 'results', 'value']) { 13011 if (Array.isArray(value[nested])) return value[nested]; 13012 } 13013 } 13014 } 13015 if (Array.isArray(data?.data)) return data.data; 13016 if (Array.isArray(data?.items)) return data.items; 13017 if (data?.data && typeof data.data === 'object') return this.listFrom(data.data, keys); 13018 return []; 13019 } 13020 async fetchMyLots() { 13021 if (this.state.myLotsLoading) return; 13022 const token = await this.authToken(); 13023 if (!token) { 13024 this.setState({ myLots: null, myLotsLoading: false, myLotsError: '' }); 13025 return; 13026 } 13027 this.setState({ myLotsLoading: true, myLotsError: '' }); 13028 try { 13029 const data = await this.apiFetch('/api/Lots/mine?limit=100'); 13030 const raw = this.listFrom(data, ['data', 'items', 'lots']); 13031 let lots = raw.map((l, i) => this.mapLot(l, i)); 13032 if (!lots.length && this.state.user?.email) { 13033 const allData = await this.apiFetch('/api/Lots?limit=100'); 13034 const email = String(this.state.user.email).toLowerCase(); 13035 lots = this.listFrom(allData, ['data', 'items', 'lots']) 13036 .map((l, i) => this.mapLot(l, i)) 13037 .filter((l) => String(l.sellerEmail || '').toLowerCase() === email); 13038 } 13039 this.setProfileFetchInfo('lotes', lots.length, ''); 13040 this.setState({ myLots: lots, myLotsLoading: false, myLotsError: '' }); 13041 } catch (e) { 13042 this.setProfileFetchInfo('lotes', null, e?.message || 'Error'); 13043 this.setState({ myLots: null, myLotsLoading: false, myLotsError: e?.message || 'No se pudieron cargar tus lotes.' }); 13044 } 13045 } 13046 setProfileFetchInfo(key, count, error) { 13047 this.setState({ profileFetchInfo: { ...(this.state.profileFetchInfo || {}), [key]: { count, error: error || '', at: new Date().toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) } } }); 13048 } 13049 async refreshProfileData(force = false) { 13050 const now = Date.now(); 13051 if (!force && this._lastProfileRefreshAt && now - this._lastProfileRefreshAt < 30000) return; 13052 if (this._profileRefreshPromise) return this._profileRefreshPromise; 13053 this._profileRefreshPromise = this.performProfileRefresh(); 13054 try { 13055 await this._profileRefreshPromise; 13056 this._lastProfileRefreshAt = Date.now(); 13057 } finally { 13058 this._profileRefreshPromise = null; 13059 } 13060 } 13061 async performProfileRefresh() { 13062 const token = await this.authToken(); 13063 if (!token) return; 13064 const apiUser = await this.fetchApiSession(); 13065 if (!apiUser) return; 13066 const rosganProfile = this.isRosganProfile(apiUser); 13067 const canSell = !rosganProfile && this.canManageSellerData(apiUser); 13068 const canBuy = !rosganProfile && this.canUseBuyerData(apiUser); 13069 if (this.canManageRosganLots(apiUser) && this.state.user?.uid) await this.fetchUserConsignataria(this.state.user.uid); 13070 else if (this.state.userConsignataria || this.state.userConsignatarias?.length) this.setState({ userConsignataria: null, userConsignatarias: [] }); 13071 if (canSell && !this.state.rosganRemates && !this.state.rosganRematesLoading) this.loadRosganRemateAdmin(); 13072 if (!canSell && this.state.route === 'load') this.setState({ route: 'profile' }); 13073 const jobs = []; 13074 if (rosganProfile) jobs.push(this.fetchProfileRosganLots(), this.loadRosganRemateAdmin()); 13075 if (rosganProfile || this.canManageUsers(apiUser)) jobs.push(this.fetchProfileFerias()); 13076 if (canSell) jobs.push(this.fetchMyLots(), this.fetchPendingApprovals()); 13077 else this.setState({ myLots: [], myLotsLoading: false, myLotsError: '', pendingBids: [], pendingBidsLoading: false, pendingBidsError: '' }); 13078 if (rosganProfile || canSell || this.isCertifierOnly(apiUser)) jobs.push(this.fetchComitentes(), this.fetchEstablishments()); 13079 else this.setState({ comitentes: [], comitentesLoading: false, comitentesError: '', establishments: [], establishmentsLoading: false, establishmentsError: '' }); 13080 if (canBuy) jobs.push(this.fetchMyBids()); 13081 else this.setState({ myBids: [], myBidsLoading: false, myBidsError: '' }); 13082 this.setState({ buyerAccess: [], buyerAccessLoading: false, buyerAccessError: '' }); 13083 if (!rosganProfile && this.canManageBuyerAccounts(apiUser)) { 13084 jobs.push(this.fetchBuyerAccounts(), this.fetchBuyerAuths()); 13085 } else { 13086 this.setState({ 13087 buyerAccounts: [], buyerAccountsLoading: false, buyerAccountsError: '', 13088 buyerAuths: [], buyerAuthsLoading: false, buyerAuthsError: '', 13089 creditSummaryCache: {}, creditSummaryLoadingIds: {}, creditSummaryErrors: {}, 13090 creditSummaryOpen: false, creditSummaryLoading: false, creditSummaryError: '', 13091 creditSummary: null, creditSummaryAuthorization: null, 13092 creditHistoryOpen: false, creditHistoryAuthorization: null, 13093 }); 13094 } 13095 if (this.canManageUsers(apiUser) && this.state.adminUsersOpen) jobs.push(this.fetchAdminUsers()); 13096 else if (!this.canManageUsers(apiUser) && (this.state.adminUsersOpen || this.state.adminUsers)) this.setState({ adminUsersOpen: false, adminUsers: null, adminSelectedUser: null }); 13097 if (this.canManageUsers(apiUser)) jobs.push(this.fetchLaunchInterestsAdmin()); 13098 else if (this.state.launchInterestsAdmin) this.setState({ launchInterestsAdmin: null, launchInterestsAdminTotal: 0, launchInterestsAdminError: '' }); 13099 const allowedTabs = [...(rosganProfile ?['resumen', 'comitentes', 'establecimientos'] : []), ...(canSell ?['resumen', 'aprobar'] : []), ...(canSell || this.isCertifierOnly(apiUser) ?['comitentes', 'establecimientos'] : []), ...(canBuy ?['ofertas'] : [])]; 13100 if (allowedTabs.length && !allowedTabs.includes(this.state.profileTab)) this.setState({ profileTab: allowedTabs[0] }); 13101 await Promise.allSettled(jobs); 13102 if (canBuy || canSell) await this.syncLiveBids(); 13103 }
13104 normalizeApiSessionUser(data) { 13105 if (!data || typeof data !== 'object') return null; 13106 const sources = [ 13107 data, 13108 data.data, 13109 data.session, 13110 data.data?.session, 13111 data.apiSession, 13112 data.data?.apiSession, 13113 data.session?.apiSession, 13114 data.data?.session?.apiSession, 13115 data.user, 13116 data.data?.user, 13117 data.session?.user, 13118 data.data?.session?.user, 13119 data.apiSession?.user, 13120 data.session?.apiSession?.user, 13121 ].filter((value) => value && typeof value === 'object' && !Array.isArray(value)); 13122 if (!sources.length) return null; 13123 const merged = Object.assign({}, ...sources); 13124 const roles = [...new Set(sources.flatMap((source) => this.userRoleIds(source)))]; 13125 if (!roles.length) return merged; 13126 return { 13127 ...merged, 13128 roles, 13129 roleId: roles, 13130 role: ['sysadmin', 'admin', 'dev', 'seller', 'buyer'].find((role) => roles.includes(role)) || merged.role || 'user', 13131 }; 13132 } 13133 async fetchApiSession() { 13134 const uid = this.state.user?.uid; 13135 const sessionEpoch = this._sessionEpoch || 0; 13136 if (!uid) return null; 13137 const current = () => this.state.user?.uid === uid && sessionEpoch === (this._sessionEpoch || 0); 13138 try { 13139 // The external session omits local permissions such as devAccess. 13140 // sync-user returns both the external roles and the current stored grants. 13141 const data = await this.apiFetch('/api/auth/sync-user', { method: 'POST', body: '{}' }); 13142 let apiUser = this.normalizeApiSessionUser(data); 13143 if (!this.apiRoleIds(apiUser).length && ['localhost', '127.0.0.1'].includes(window.location.hostname)) { 13144 try { 13145 const localData = await this.apiFetch('/api/me'); 13146 apiUser = this.normalizeApiSessionUser(localData) || apiUser; 13147 } catch {} 13148 } 13149 if (!current()) return null; 13150 if (apiUser && this.state.apiUser) { 13151 const retainedCuit = this.apiUserCuit(this.state.apiUser); 13152 apiUser = { 13153 ...this.state.apiUser, 13154 ...apiUser, 13155 ...(retainedCuit && !this.apiUserCuit(apiUser) ?{ cuitCuil: this.cuitInputDisplay(retainedCuit) } : {}), 13156 }; 13157 } 13158 this.setState({ apiUser, apiSessionError: '', profilePhotoFailed: false }, () => { 13159 if (this.state.route === 'rosganRemateCreate' && !this.canManageUsers(apiUser)) { 13160 this.syncAppLocation('remates', { replace: true }); 13161 this.go('remates', { syncPath: false }); 13162 } else if (['remates', 'rosganRemateCreate'].includes(this.state.route) && this.canManageSellerData(apiUser)) { 13163 this.scheduleRouteWork(this.state.route); 13164 } 13165 }); 13166 this.cacheApiUser(this.state.user, apiUser); 13167 return apiUser; 13168 } catch (e) { 13169 if (!current()) return null; 13170 const retained = this.canAccessApp(this.state.apiUser) ?this.state.apiUser : null; 13171 this.setState({ apiUser: retained, apiSessionError: e?.message || 'No se pudo validar la sesion con Rosgan API.' }); 13172 return retained; 13173 } 13174 } 13175 profileApiData() { 13176 const user = this.state.user ? (this.state.apiUser || {}) : {}; 13177 return user.profile && typeof user.profile === 'object' ?{ ...user, ...user.profile } : user; 13178 } 13179 profilePhotoUrl(user = this.profileApiData()) { 13180 const candidates = [user?.profilePhotoUrl, user?.photoUrl, user?.photoURL, user?.avatarUrl, user?.profilePhoto, user?.photo]; 13181 const value = candidates.find((candidate) => typeof candidate === 'string' && candidate.trim()); 13182 return value ?value.trim() : ''; 13183 } 13184 commercialProfilePhotoUrl(user) { 13185 const candidates = [user?.profilePhotoUrl, user?.photoUrl, user?.photoURL, user?.avatarUrl, user?.profilePhoto, user?.photo]; 13186 return candidates.find(value => { 13187 if (typeof value !== 'string' || !value.trim()) return false; 13188 try { 13189 const host = new URL(value.trim(), 'https://rosgan.app').hostname.toLowerCase(); 13190 return !['googleusercontent.com', 'ggpht.com', 'google.com'].some(domain => host === domain || host.endsWith('.' + domain)); 13191 } catch { return false; } 13192 })?.trim() || ''; 13193 } 13194 commercialAvatar(user, consignatariaRef, photoFailed = false) { 13195 const photo = photoFailed ? '' : this.commercialProfilePhotoUrl(user?.profile ? { ...user, ...user.profile } : user); 13196 const whiteLogo = this.consWhiteLogo(consignatariaRef);
13197 const avatar = photo || whiteLogo || this.consLogo(consignatariaRef); 13198 return { avatar, hasAvatar: !!avatar, noAvatar: !avatar, photo: !!photo, 13199 bg: photo ? '#fff' : whiteLogo ? this.consBrandColor(consignatariaRef) : '#edf3f8', 13200 size: photo ? '100%' : '22px', fit: photo ? 'cover' : 'contain' }; 13201 } 13202 async fetchRosganRepresentative(lot) { 13203 if (!lot?.certifier?.uid || !this.state.currentRemate?.__rosganActive) return; 13204 const key = this.remateRouteId(this.state.currentRemate) + ':' + this.remateLotRouteId(lot); 13205 try { 13206 const result = await this.apiFetch('/api/rosgan-representative/' + key.split(':').map(encodeURIComponent).join('/')); 13207 this.setState({ rosganRepresentativeContacts: { ...this.state.rosganRepresentativeContacts, [key]: result.contact || null } }); 13208 } catch { /* Keep the contact projection already supplied by the lot. */ } 13209 } 13210 rosganLotRepresentative(lot) { 13211 const key = this.remateRouteId(this.state.currentRemate) + ':' + this.remateLotRouteId(lot || {}); 13212 const resolved = this.state.rosganRepresentativeContacts?.[key]; 13213 const contact = resolved?.uid === lot?.certifier?.uid ? resolved : lot?.certifier; 13214 const selected = lot && Object.hasOwn(lot, 'certifiedBy') ? lot.certifiedBy : contact?.uid; 13215 if (!selected || !contact?.uid || String(selected) !== String(contact.uid)) return null; 13216 const name = String(contact.name || contact.displayName || '').trim(); 13217 if (!name) return null; 13218 const phone = String(contact.phone || contact.phoneNumber || contact.whatsappPhone || '').trim(); 13219 const whatsapp = String(contact.whatsappPhone || phone).replace(/\D/g, ''); 13220 const dial = phone.replace(/[^\d+]/g, ''); 13221 return { ...contact, name, phone, hasPhone: /\d/.test(dial), hasWhatsapp: !!whatsapp, 13222 phoneHref: /\d/.test(dial) ? 'tel:' + dial : '', 13223 whatsappHref: whatsapp ? 'https://wa.me/' + whatsapp + '?text=' + encodeURIComponent('Hola, quisiera consultar por el lote ' + (lot.lotNumber || lot.lotCert || lot.cert || '') + ' de Rosgan.') : '', 13224 noContact: !/\d/.test(dial) && !whatsapp }; 13225 } 13226 openProfilePhotoViewer(event) { 13227 event?.stopPropagation?.(); 13228 event?.preventDefault?.(); 13229 const profilePhoto = !this.state.profilePhotoFailed ?this.profilePhotoUrl() : ''; 13230 const consignatariaRef = this.state.userConsignataria?.id || this.state.userConsignataria?.slug || this.state.userConsignataria?.ref || this.state.userConsignataria?.name || ''; 13231 if (!profilePhoto && !this.consLogo(consignatariaRef)) return; 13232 this.setState({ profilePhotoViewerOpen: true }); 13233 } 13234 closeProfilePhotoViewer() { this.setState({ profilePhotoViewerOpen: false }); } 13235 markProfilePhotoFailed() { this.setState({ profilePhotoFailed: true, profilePhotoViewerOpen: false }); } 13236 async openProfileEditor() { 13237 const profile = this.profileApiData(); 13238 const displayParts = String(this.state.user?.displayName || '').trim().split(/\s+/).filter(Boolean); 13239 this._profilePhotoFile = null; 13240 this.releaseProfileCrop(); 13241 if (this.state.profilePhotoPreview) URL.revokeObjectURL(this.state.profilePhotoPreview); 13242 this.setState({ 13243 profileEditorOpen: true, 13244 profileEditorSaving: false, 13245 profileEditorError: '', 13246 profilePhotoPreview: '', 13247 profileCropOpen: false, 13248 profileCropSource: '', 13249 profileCropZoom: 1, 13250 profileCropX: 50, 13251 profileCropY: 50, 13252 profileEditorForm: { 13253 cuitCuil: this.apiUserCuit(profile) || '', 13254 firstName: profile.firstName || profile.nombre || displayParts[0] || '', 13255 lastName: profile.lastName || profile.apellido || displayParts.slice(1).join(' '), 13256 phoneNumber: this.state.user?.phoneNumber || profile.phoneNumber || profile.phone || profile.telefono || '', 13257 }, 13258 }); 13259 const uid=this.state.user?.uid; 13260 try {const response=await this.cabanaApiFetch('/api/users/me');const me=response?.data ?? response;if(this.state.user?.uid === uid && this.state.profileEditorOpen) this.setState({cabanaPrebidProfile:me,profileEditorForm:{...this.state.profileEditorForm,cuitCuil:me.cuitCuil || ''}});} 13261 catch(error){if(this.state.user?.uid === uid && this.state.profileEditorOpen)this.setState({profileEditorError:'No se pudo consultar el CUIT/CUIL: '+(error?.data?.detail || error.message)});} 13262 13263 } 13264 closeProfileEditor() { 13265 if (this.state.profileEditorSaving) return; 13266 this.releaseProfileCrop(); 13267 if (this.state.profilePhotoPreview) URL.revokeObjectURL(this.state.profilePhotoPreview); 13268 this._profilePhotoFile = null; 13269 this.setState({ profileEditorOpen: false, profileEditorError: '', profilePhotoPreview: '', profileCropOpen: false, profileCropSource: '' }); 13270 } 13271 setProfileEditorField(field, value) { 13272 this.setState({ profileEditorForm: { ...(this.state.profileEditorForm || {}), [field]: value }, profileEditorError: '' }); 13273 } 13274 selectProfilePhoto(file) { 13275 if (!file) return; 13276 if (!/^image\/(png|jpe || g|webp)$/i.test(file.type || '')) { 13277 this.setState({ profileEditorError: 'La foto debe ser PNG, JPG o WebP.' }); 13278 return; 13279 } 13280 if (file.size > 5 * 1024 * 1024) { 13281 this.setState({ profileEditorError: 'La foto no puede superar 5 MB.' }); 13282 return; 13283 } 13284 this.releaseProfileCrop(); 13285 const source = URL.createObjectURL(file); 13286 const image = new Image(); 13287 this._profileCropFile = file; 13288 image.onload = () => { 13289 this._profileCropImage = image; 13290 this.setState({ 13291 profileCropOpen: true, 13292 profileCropSource: source, 13293 profileCropZoom: 1, 13294 profileCropX: 50, 13295 profileCropY: 50, 13296 profileEditorError: '', 13297 }, () => requestAnimationFrame(() => this.drawProfileCrop())); 13298 }; 13299 image.onerror = () => { 13300 URL.revokeObjectURL(source); 13301 this._profileCropFile = null; 13302 this._profileCropImage = null; 13303 this.setState({ profileEditorError: 'No pudimos abrir esa imagen.' }); 13304 }; 13305 image.src = source; 13306 } 13307 releaseProfileCrop() { 13308 if (this.state?.profileCropSource) URL.revokeObjectURL(this.state.profileCropSource); 13309 this._profileCropFile = null; 13310 this._profileCropImage = null; 13311 } 13312 cancelProfileCrop() { 13313 this.releaseProfileCrop(); 13314 this.setState({ profileCropOpen: false, profileCropSource: '', profileCropZoom: 1, profileCropX: 50, profileCropY: 50 }); 13315 } 13316 setProfileCrop(field, value) { 13317 const numericValue = Number(value); 13318 this.setState({ [field]: Number.isFinite(numericValue) ?numericValue : 0 }, () => this.drawProfileCrop()); 13319 } 13320 confirmProfileCrop() { 13321 const canvas = document.querySelector('canvas[data-profile-crop-canvas="true"]'); 13322 if (!canvas || !this._profileCropImage) { 13323 this.setState({ profileEditorError: 'No pudimos preparar el recorte.' }); 13324 return; 13325 } 13326 this.drawProfileCrop(); 13327 canvas.toBlob((blob) => { 13328 if (!blob) { 13329 this.setState({ profileEditorError: 'No pudimos preparar el recorte.' }); 13330 return; 13331 } 13332 const croppedFile = new File([blob], 'foto-perfil.jpg', { type: 'image/jpeg', lastModified: Date.now() }); 13333 if (this.state.profilePhotoPreview) URL.revokeObjectURL(this.state.profilePhotoPreview); 13334 const preview = URL.createObjectURL(croppedFile); 13335 this.releaseProfileCrop(); 13336 this._profilePhotoFile = croppedFile; 13337 this.setState({ 13338 profilePhotoPreview: preview, 13339 profileCropOpen: false, 13340 profileCropSource: '', 13341 profileEditorError: '', 13342 }); 13343 }, 'image/jpeg', 0.92); 13344 } 13345 async saveProfileEditor() { 13346 const uid = this.state.apiUser?.uid || this.state.apiUser?.id || this.state.user?.uid; 13347 if (!uid) { this.setState({ profileEditorError: 'No pudimos identificar tu usuario.' }); return; } 13348 const form = this.state.profileEditorForm || {}; 13349 const firstName = String(form.firstName || '').trim(); 13350 const lastName = String(form.lastName || '').trim(); 13351 const phoneNumber = String(this.state.user?.phoneNumber || form.phoneNumber || '').trim(); 13352 if (form.cuitCuil && this.cuitInputRaw(form.cuitCuil).length !== 11) { this.setState({profileEditorError:'Ingresá un CUIT/CUIL de 11 dÃgitos.'}); return; } 13353 if (!firstName || !lastName) { this.setState({ profileEditorError: 'Completá n
13353ombre y apellido.' }); return; } 13354 this.setState({ profileEditorSaving: true, profileEditorError: '' }); 13355 try { 13356 await this.apiFetch('/api/Users/' + encodeURIComponent(uid) + '/profile', { 13357 method: 'PUT', 13358 body: JSON.stringify({ firstName, lastName, phoneNumber }), 13359 }); 13360 if (this._profilePhotoFile) { 13361 const photoBody = new FormData(); 13362 photoBody.append('Photo', this._profilePhotoFile, this._profilePhotoFile.name); 13363 await this.apiFetch('/api/Users/' + encodeURIComponent(uid) + '/profile-photo', { method: 'POST', body: photoBody }); 13364 } 13365 const firebaseUser = (await this.ensureFirebase()).auth().currentUser; 13366 if (firebaseUser?.updateProfile) await firebaseUser.updateProfile({ displayName: [firstName, lastName].filter(Boolean).join(' ') }).catch(() => {}); 13367 await this.fetchApiSession(); 13368 if (form.cuitCuil && this.cuitInputRaw(form.cuitCuil) !== this.cuitInputRaw(this.state.cabanaPrebidProfile?.cuitCuil)) await this.persistCabanaPrebidCuit(form.cuitCuil); 13369 if (this.state.profilePhotoPreview) URL.revokeObjectURL(this.state.profilePhotoPreview); 13370 this._profilePhotoFile = null; 13371 this.setState({ profileEditorOpen: false, profileEditorSaving: false, profileEditorError: '', profilePhotoPreview: '', profilePhotoFailed: false }); 13372 this.showToast('Perfil actualizado.'); 13373 } catch (error) { 13374 this.setState({ profileEditorSaving: false, profileEditorError: error?.message || 'No se pudo actualizar el perfil.' }); 13375 } 13376 } 13377 isBasicUserProfile(user = this.state.apiUser) { 13378 return this.apiRoleIds(user).every(role => ['user', 'usuario'].includes(role)); 13379 } 13380 isRosganProfile(user = this.state.apiUser) { 13381 return this.apiRoleIds(user).some((role) => ['consignatario', 'certificador'].includes(role)); 13382 } 13383 canViewProfileFerias() { return this.isRosganProfile() || this.canManageUsers(this.state.apiUser); } 13384 async fetchProfileFerias() { 13385 const uid = this.state.user?.uid; 13386 if (!uid || !this.canViewProfileFerias()) return; 13387 const request = {}; this._profileFeriasRequest = request; 13388 this.setState({ profileFerias: [], profileFeriasLoading: true, profileFeriasError: '', profileFeriasUid: uid }); 13389 const active = () => this._profileFeriasRequest === request && this.state.user?.uid === uid && this.canViewProfileFerias(); 13390 try { 13391 const data = await this.apiFetch('/api/ferias'); 13392 if (!active()) return; 13393 this.setState({ profileFerias: this.listFrom(data, ['data','items','ferias']), profileFeriasLoading: false }); 13394 } catch (error) { 13395 if (active()) this.setState({ profileFeriasLoading: false, profileFeriasError: 'No se pudieron cargar los remates feria. Reintentá.' }); 13396 } 13397 } 13398 profileFeriaRows() { 13399 if (!this.canViewProfileFerias() || this.state.profileFeriasUid !== this.state.user?.uid) return []; 13400 const refs = new Set((this.state.userConsignatarias || []).flatMap(c => [c.id,c.slug,c.name,c.nombre,this.consName(c)]).filter(Boolean).map(v => this.consKey(v))); 13401 const today = this.localIsoDate(); 13402 return (this.state.profileFerias || []).filter(f => !f.deleted && !f.cancelled && String(f.fecha || '').slice(0,10) >= today && !['cancelled','completed','complete','finished','finalizado'].includes(String(f.status || '').toLowerCase()) && !['complete','completed'].includes(String(f.youtubeStatus || '').toLowerCase()) && (this.canManageUsers(this.state.apiUser) || [f.consignatariaId,f.consignataria].some(v => v && refs.has(this.consKey(v))))) 13403 .sort((a,b) => { const ad=String(a.fecha||''),bd=String(b.fecha||''); const ap=ad>=today,bp=bd>=today; return Number(bp)-Number(ap) || (ap?ad.localeCompare(bd):bd.localeCompare(ad)) || String(a.hora||'').localeCompare(String(b.hora||'')); }) 13404 .map(f => { const date = f.fecha ? new Date(f.fecha+'T'+(f.hora||'12:00').slice(0,5)+':00-03:00') : null; 13405 const url = /^https:\/\/(?:www\.)?(?:youtube\.com\/watch\?|youtu\.be\/)/i.test(f.youtubeUrl || '') ? f.youtubeUrl : ''; 13406 return { id:f.id, title:f.nombreRemate || 'Remate feria', company:f.consignataria || '', location:[f.localidad,f.provincia].filter(
13406Boolean).join(', '), 13407 date:date && !isNaN(date) ? date.toLocaleDateString('es-AR',{day:'numeric',month:'long',year:'numeric'})+' · '+(f.hora||'').slice(0,5)+' h' : 'Fecha a confirmar', 13408 copyTransmission: async () => { try { await navigator.clipboard.writeText(url); this.showToast('URL de transmisión copiada'); } catch { this.showToast('No se pudo copiar el enlace. Intentá nuevamente.'); } }, 13409 downloadFlyer: async (event) => { 13410 const button=event?.currentTarget;if(button){button.disabled=true;button.querySelector('span').textContent='Generando flyerâ¦';} 13411 try {const fullLogo=this.consFullHeroLogo(f.consignataria);const {blob,data}=await window.RosganFeriaFlyer.render(f,{color:this.consBrandColor(f.consignataria),logo:fullLogo||this.consLogo(f.consignataria),mono:!!fullLogo});const objectUrl=URL.createObjectURL(blob),a=document.createElement('a');a.href=objectUrl;a.download='feria-'+data.title.normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^a-z0-9]+/gi,'-').toLowerCase()+'.png';document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(objectUrl),60000);} 13412 catch(error){this.showToast(error.message||'No se pudo descargar el flyer.');} 13413 finally {if(button){button.disabled=false;button.querySelector('span').textContent='Descargar flyer';}} 13414 }, 13415 canControlYoutube: this.canManageUsers(), controlYoutube: () => window.RosganFeriaYoutube.open({feria:f,apiFetch:(path,opts)=>this.apiFetch(path,opts)}), 13416 youtubeMessage: f.youtubeError || 'Transmisión pendiente', reviewTransmission: () => { this.go('feria'); this.waitFeriaYoutube(f); }, deleteFeria: () => this.deleteSavedFeria(f), editFeria: () => this.editSavedFeria(f), 13417 status:f.fecha < today ? 'Finalizado' : f.fecha === today ? 'Hoy' : 'Próximo', hasLink:!!url, href:url, logo:this.consLogo(f.consignataria), hasLogo:!!this.consLogo(f.consignataria) }; 13418 }); 13419 } 13420 profileUpcomingRemates(remates, now = Date.now()) { 13421 return (remates || []).filter((item) => { 13422 const date = this.remateDate(item); 13423 return item.activo !== false && item.closeRemate !== true && !item.cabanasConfig && date && date.getTime() >= now; 13424 }); 13425 } 13426 async fetchProfileRosganLots() { 13427 const uid = this.state.user?.uid; 13428 if (!uid || !this.isRosganProfile()) return; 13429 const request = {}; 13430 this._profileRosganRequest = request; 13431 this.setState({ profileRosganLots: [], profileRosganLoading: true, profileRosganError: '', profileRosganScopeUid: uid }); 13432 const active = () => this._profileRosganRequest === request && this.state.user?.uid === uid && this.isRosganProfile(); 13433 try { 13434 const remates = this.profileUpcomingRemates(this.normalizeRosganRemates(await this.apiFetch('/api/Remates'))).filter((item) => 13435 [...(item.consignatariasParticipantes || []).map((entry) => entry.consignatariaId || entry.id), item.consignatariaId].some((id) => 13436 (this.state.userConsignatarias || []).some((member) => String(member.id) === String(id)))); 13437 const ids = new Set((this.state.userConsignatarias || []).map((item) => String(item.id))); 13438 if (!ids.size) { if (active()) this.setState({ profileRosganLoading: false }); return; } 13439 const results = []; 13440 // Limit simultaneous authenticated requests; the API enforces private lot access. 13441 for (let offset = 0; offset < remates.length; offset += 4) { 13442 const batch = await Promise.allSettled(remates.slice(offset, offset + 4).map(async (remate) => { 13443 const id = this.remateRouteId(remate); 13444 const response = await this.apiFetch('/api/remates/' + encodeURIComponent(id) + '/rosgan-lots/all'); 13445 return this.rosganArray(response).map((row) => ({ ...(row.data || row.lot || {}), ...row })) 13446 .filter((lot) => ids.has(String(lot.consignatariaId))) 13447 .map((lot) => ({ ...lot, profileRemate: remate })); 13448 })); 13449 if (!active()) return; 13450 results.push(...batch); 13451 } 13452 const lots = results.filter((r) => r.status === 'fulfilled').flatMap((r) => r.value); 13453 this.setState({ profileRosganLots: lots, profileRosganLoading: false, 13454 profileRosganError: results.some((r) => r.status === 'rejected') ? 'No se pudieron consultar todos los remates. Reintentá para completar la lista.' : '' }); 13455 } catch (error) { 13456 if (active()) this.setState({ profileRosganLoading: false, profileRosganError: 'No se pudieron cargar los lotes de tu consignataria. Reintentá.' }); 13457 } 13458 } 13459 profileRosganRows() { 13460 const uid = String(this.state.user?.uid || ''); 13461 const own = (lot) =>
13461 !!uid && String(lot.createdByUid || lot.operatorUid || lot.creatorUid || lot.createdByUserId || '') === uid; 13462 if (!uid || this.state.profileRosganScopeUid !== uid) return []; 13463 return [...(this.state.profileRosganLots || [])].sort((a, b) => Number(own(b)) - Number(own(a)) || 13464 (Date.parse(b.createdAt || '') || 0) - (Date.parse(a.createdAt || '') || 0) || 13465 String(a.lotCert || a.id).localeCompare(String(b.lotCert || b.id), 'es', { numeric: true })) 13466 .map((source) => { 13467 const lot = this.normalizeRosganActiveLot(source, 0, source.profileRemate); 13468 return { cert: lot.cert, category: lot.nombre_categoria, heads: lot.cantidad, 13469 remate: source.profileRemate.nombre || source.profileRemate.name || source.profileRemate.titulo || 'Remate ' + this.remateRouteId(source.profileRemate), 13470 location: [source.locality, source.province].filter(Boolean).join(', ') || 'Sin localidad', 13471 status: this.rosganLotWorkflowStatus(source), own: own(source), 13472 href: this.rematePath(source.profileRemate, lot), 13473 canEdit: this.canEditRosganLot(source), edit: () => this.openRosganLotEditor(lot, source.profileRemate) }; 13474 }); 13475 } 13476 async fetchUserConsignataria(uid) { 13477 if (!uid) { this.setState({ userConsignataria: null, userConsignatarias: [] }); return null; } 13478 if (this._userConsignatariaPromise && this._userConsignatariaUid === uid) return this._userConsignatariaPromise; 13479 this._userConsignatariaUid = uid; 13480 const epoch = this._sessionEpoch || 0; 13481 const current = () => this.state.user?.uid === uid && epoch === (this._sessionEpoch || 0); 13482 const request = (async () => { 13483 try { 13484 const data = await this.apiFetch('/api/consignataria-users/' + encodeURIComponent(uid)); 13485 const root = data?.data || data; 13486 const relations = Array.isArray(root) ?root : (Array.isArray(root?.items) ?root.items : [root?.item || root?.consignatariaUser || root?.membership || root]); 13487 const resolved = relations.filter((relation) => relation?.isActive === true).flatMap((relation) => { 13488 const consignataria = relation?.consignataria && typeof relation.consignataria === 'object' ?relation.consignataria : {}; 13489 const ids = Array.isArray(relation?.consignatariaIds) ?relation.consignatariaIds : []; 13490 const refs = [ 13491 ...ids, 13492 relation?.consignatariaId, 13493 relation?.consignatariaSlug, 13494 relation?.consignatariaName, 13495 consignataria.id, 13496 consignataria.slug, 13497 consignataria.name, 13498 consignataria.nombre, 13499 typeof relation?.consignataria === 'string' ?relation.consignataria : '', 13500 ].filter(Boolean); 13501 return refs.map((ref) => ({ id: String(ref), slug: String(ref), name: String(ref), ref: String(ref) })); 13502 }).filter((item, index, items) => items.findIndex((candidate) => this.consKey(candidate.ref) === this.consKey(item.ref)) === index); 13503 if (current()) this.setState({ userConsignataria: resolved[0] || null, userConsignatarias: resolved }); 13504 return resolved; 13505 } catch (error) { 13506 if (current() && [401, 403].includes(error?.status)) this.setState({ userConsignataria: null, userConsignatarias: [] }); 13507 return null; 13508 } 13509 })(); 13510 this._userConsignatariaPromise = request; 13511 try { return await request; } 13512 finally { if (this._userConsignatariaPromise === request) this._userConsignatariaPromise = null; } 13513 } 13514 consignatariaRelationRefs(data) { 13515 const root = data?.data || data; 13516 const relations = Array.isArray(root) ?root : (Array.isArray(root?.items) ?root.items : [root?.item || root?.consignatariaUser || root?.membership || root]); 13517 return relations.filter((relation) => relation?.isActive === true).flatMap((relation) => { 13518 const consignataria = relation?.consignataria && typeof relation.consignataria === 'object' ?relation.consignataria : {}; 13519 const ids = Array.isArray(relation?.consignatariaIds) ?relation.consignatariaIds : []; 13520 const refs = [ 13521 ...ids, 13522 relation?.consignatariaId, 13523 relation?.consignatariaSlug, 13524 relation?.consignatariaName, 13525 consignataria.id, 13526 consignataria.slug, 13527 consignataria.name, 13528 consignataria.nombre, 13529 typeof relation?.consignataria === 'string' ?relation.consignataria : '',
13530 ].filter(Boolean); 13531 return refs.map((ref) => ({ id: String(ref), slug: String(ref), name: String(ref), ref: String(ref) })); 13532 }).filter((item, index, items) => items.findIndex((candidate) => this.consKey(candidate.ref) === this.consKey(item.ref)) === index); 13533 } 13534 async fetchLotSellerConsignataria(lot) { 13535 const sellerUid = String(lot?.sellerUid || '').trim(); 13536 if (!sellerUid || lot?.consignatariaId || lot?.consignatariaName) return null; 13537 const isCurrentSeller = sellerUid === String(this.state.user?.uid || ''); 13538 if (Object.prototype.hasOwnProperty.call(this.state.lotSellerConsignatarias || {}, sellerUid)) { 13539 const cached = this.state.lotSellerConsignatarias[sellerUid]; 13540 if (cached || !isCurrentSeller) return cached || null; 13541 } 13542 this._lotSellerConsignatariaPromises ||= {}; 13543 if (this._lotSellerConsignatariaPromises[sellerUid]) return this._lotSellerConsignatariaPromises[sellerUid]; 13544 const request = (async () => { 13545 let resolved = []; 13546 if (isCurrentSeller) { 13547 resolved = [...(this.state.userConsignatarias || []), this.state.userConsignataria].filter(Boolean); 13548 if (!resolved.length) resolved = (await this.fetchUserConsignataria(sellerUid)) || []; 13549 } else { 13550 try { 13551 resolved = this.consignatariaRelationRefs(await this.apiFetch('/api/consignataria-users/' + encodeURIComponent(sellerUid))); 13552 } catch (error) { 13553 if (![403, 404].includes(Number(error?.status))) console.warn('[Roslot publisher relation failed]', sellerUid, error?.message || error); 13554 } 13555 } 13556 const relation = resolved[0] || null; 13557 this.setState({ lotSellerConsignatarias: { ...(this.state.lotSellerConsignatarias || {}), [sellerUid]: relation || false } }); 13558 return relation; 13559 })(); 13560 this._lotSellerConsignatariaPromises[sellerUid] = request; 13561 try { return await request; } 13562 finally { delete this._lotSellerConsignatariaPromises[sellerUid]; } 13563 } 13564 apiRoleIds(user = this.state.apiUser) { 13565 return this.userRoleIds(user); 13566 } 13567 canManageBuyerAccounts(user = this.state.apiUser) { 13568 return this.canManageSellerData(user); 13569 } 13570 canAccessApp(user = this.state.apiUser) { return true; } 13571 canCreateCabanaRemate(user = this.state.apiUser) { 13572 return !!this.state.user && this.apiRoleIds(user).some(role => ['admin', 'sysadmin'].includes(role)); 13573 } 13574 canAccessRoslot(user = this.state.apiUser) { 13575 return !!this.state.user && this.apiRoleIds(user).some((role) => ['admin', 'sysadmin'].includes(role)); 13576 } 13577 canAccessFeria(user = this.state.apiUser) { 13578 return !!this.state.user && this.apiRoleIds(user).some((role) => ['admin', 'sysadmin', 'consignatario', 'certificador'].includes(role)); 13579 } 13580 canAccessStatistics(user = this.state.apiUser) { 13581 return !!this.state.user && this.apiRoleIds(user).some((role) => ['admin', 'sysadmin'].includes(role)); 13582 } 13583 canAccessRoute(route, user = this.state.apiUser) { 13584 const target = String(route || ''); 13585 if (target === 'rosganLotLoad') return this.canManageRosganLots(user); 13586 if (['pool', 'lot', 'load', 'roslotSummary'].includes(target)) return this.canAccessRoslot(user); 13587 if (target === 'feria') return this.canAccessFeria(user); 13588 if (target === 'mercado') return this.canAccessStatistics(user); 13589 if (target === 'cabanaStats') return this.canSeeCabanaStatsNavigation(user); 13590 if (['home', 'remates', 'remateDetail', 'rosganResults', 'cabanas', 'cabanaDetail', 'cabanaLotDetail'].includes(target)) return true; 13591 return !!this.state.user; 13592 } 13593 isSysadmin(user = this.state.apiUser) { return this.apiRoleIds(user).includes('sysadmin'); } 13594 canManageUsers(user = this.state.apiUser) { return this.apiRoleIds(user).some((role) => ['admin', 'sysadmin'].includes(role)); } 13595 canManageSellerData(user = this.state.apiUser) { return this.apiRoleIds(user).some((role) => ['seller', 'vendedor', 'consignatario', 'admin', 'sysadmin'].includes(role)); } 13596 canManageRosganLots(user = this.state.apiUser) { return !!this.state.user && this.apiRoleIds(user).some((role) => ['seller', 'consignatario', 'certificador', 'admin', 'sysadmin'].includes(role)); } 13597 isCertifierOnly(user = this.state.apiUser) { return this.apiRoleIds(user).includes('certificador') && !this.canManageSellerData(user); } 13598 canManageComitentes(user = this.state.apiUser) { return this.apiRoleIds(user).some((role) => ['seller', 'vendedor', 'consignatario', 'admin', 'sysadmin'].includes(role)); } 13599 canUseBuyerData(user = this.state.apiUser) { return this.apiRoleIds(user).some((role) => ['buyer', 'comprador', 'consignatario', 'sysadmin'].includes(role)); } 13600 canManageCabanaPrivatePages(user = this.state.apiUser) { 13601 return !!this.state.user && this.apiRoleIds(user).some((role) => ['admin', 'sysadmin', 'consignatario', 'seller', 'cabanero'].includes(role)); 13602 } 13603 canSeeCabanaStatsNavigation(user = this.state.apiUser) { 13604 return !!this.state.user && this.apiRoleIds(user).some((role) => ['admin', 'sysadmin', 'consignatario'].includes(role)); 13605 } 13606 canSeeCabanaStats(item = this.state.currentCabana, user = this.state.apiUser) { 13607 return this.canExportCabanaCatalog(item, user); 13608 } 13609 canExportCabanaCatalog(item = this.state.currentCabana, user = this.state.apiUser) { 13610 if (!this.state.user || !item) return false;
13611 const roles = this.apiRoleIds(user); 13612 if (roles.some((role) => ['admin', 'sysadmin'].includes(role))) return true; 13613 if (!roles.includes('consignatario')) return false; 13614 13615 return this.belongsToCabanaConsignataria(item); 13616 } 13617 belongsToCabanaConsignataria(item) { 13618 const membershipRefs = [...(this.state.userConsignatarias || []), this.state.userConsignataria] 13619 .filter(Boolean) 13620 .flatMap((relation) => [relation?.id, relation?.consignatariaId, relation?.slug, relation?.ref, relation?.name, relation?.nombre]); 13621 const consignataria = item?.consignataria && typeof item.consignataria === 'object' ?item.consignataria : {}; 13622 const participants = Array.isArray(item?.consignatariasParticipantes) ?item.consignatariasParticipantes : []; 13623 const auctionRefs = [ 13624 item?.consignatariaId, item?.consignatariaSlug, item?.consignatariaNombre, item?.consignatariaName, 13625 item?.nombre_consignatario, item?.nombre_cuenta, typeof item?.consignataria === 'string' ?item.consignataria : '', 13626 consignataria?.id, consignataria?.consignatariaId, consignataria?.slug, consignataria?.name, consignataria?.nombre, 13627 ...participants.flatMap((participant) => [ 13628 participant?.consignatariaId, participant?.id, participant?.slug, 13629 participant?.consignatariaNombre, participant?.consignatariaName, participant?.name, participant?.nombre, 13630 ]), 13631 ]; 13632 const membershipKeys = new Set(membershipRefs.filter(Boolean).map((value) => this.consKey(value)).filter(Boolean)); 13633 const auctionKeys = new Set(auctionRefs.filter(Boolean).map((value) => this.consKey(value)).filter(Boolean)); 13634 const enrichFromCatalog = (keys) => { 13635 (this.state.consignatarias || []).forEach((record) => { 13636 const refs = [record?.id, record?.consignatariaId, record?.slug, record?.name, record?.nombre] 13637 .filter(Boolean).map((value) => this.consKey(value)).filter(Boolean); 13638 if (refs.some((key) => keys.has(key))) refs.forEach((key) => keys.add(key)); 13639 }); 13640 }; 13641 enrichFromCatalog(membershipKeys); 13642 enrichFromCatalog(auctionKeys); 13643 return [...membershipKeys].some((key) => auctionKeys.has(key)); 13644 } 13645 denyCabanaPrivatePage(item = null) { 13646 const target = item ?this.normalizeCabanaAuctionRecord(item) : null; 13647 try { window.history.replaceState({}, '', target ?this.cabanaPath(target) : '/cabanas'); } catch {} 13648 this.setState({ 13649 route: target ?'cabanaDetail' : 'cabanas', 13650 currentCabana: target || null, 13651 cabanaStatsOverview: false, 13652 cabanaSalesOrderOpen: false, 13653 }); 13654 if (!target && !Array.isArray(this.state.cabanas)) this.fetchCabanas(); 13655 this.showToast('Esta sección requiere permisos de gestión de cabañas.'); 13656 window.scrollTo(0, 0); 13657 return false; 13658 } 13659 userRoleIds(user) { 13660 const aliases = { 13661 comprador: 'buyer', 13662 vendedor: 'seller', 13663 consignataria: 'consignatario', 13664 cabana: 'cabanero', 13665 'cabaña': 'cabanero', 13666 'cabañero': 'cabanero', 13667 administrador: 'admin', 13668 administrator: 'admin', 13669 'administrador del sistema': 'sysadmin', 13670 'administrador de sistema': 'sysadmin', 13671 'system administrator': 'sysadmin', 13672 'system admin': 'sysadmin', 13673 superadmin: 'sysadmin', 13674 developer: 'dev', 13675 desarrollador: 'dev', 13676 desarrollo: 'dev', 13677 invitado: 'guest', 13678 visitante: 'guest', 13679 }; 13680 const raw = [ 13681 user?.roleIds, user?.roleId, user?.roles, user?.role, 13682 user?.profile?.roleId, user?.profile?.roles, user?.profile?.role, 13683 user?.userData?.roleId, user?.userData?.roles, user?.userData?.role, 13684 user?.claims?.roleId, user?.claims?.roles, user?.claims?.role, 13685 user?.customClaims?.roleId, user?.customClaims?.roles, user?.customClaims?.role, 13686 user?.session?.roleId, user?.session?.roles, user?.session?.role, 13687 user?.apiSession?.roleId, user?.apiSession?.roles, user?.apiSession?.role, 13688 user?.apiSession?.user?.roleId, user?.apiSession?.user?.roles, user?.apiSession?.user?.role, 13689 user?.devAccess === true || user?.userData?.devAccess === true ?'dev' : null, 13690 ]; 13691 const unpack = (value) => { 13692 if (Array.isArray(value)) return value.flatMap(unpack); 13693 if (value && typeof value === 'object') return [value.id, value.roleIds, value.roleId, value.name, value.value, value.roles].flatMap(unpack); 13694 return value == null ?[] : [value]; 13695 }; 13696 return [...new Set(raw.flatMap(unpack) 13697 .map((role) => String(role || '').trim().toLowerCase()) 13698 .map((role) => aliases[role] || role) 13699 .filter(Boolean))]; 13700 } 13701 adminRegistrationTime(user) { 13702 const raw = user?.createdAt || user?.registeredAt || user?.createdOn || user?.creationTime || user?.metadata?.creationTime; 13703 const seconds = raw?.seconds ?? raw?._seconds; 13704 const time = seconds != null ? Number(seconds) * 1000 : (typeof raw === 'number' ? raw : Date.parse(raw || '')); 13705 return Number.isFinite(time) && time > 0 ? time : null; 13706 } 13707 adminUserDirectory() { 13708 const users = this.state.adminUsers || []; 13709 const defs = [ 13710 ['auction', 'Consignatarios y certificadores', u => u.roles.some(r => ['consignatario', 'certificador'].includes(r))], 13711 ['roslot', 'Roslot · compradores y vendedores', u => u.roles.some(r => ['buyer', 'seller'].includes(r))], 13712 ['admin', 'Admin', u => u.roles.includes('admin')], 13713 ['sysadmin', 'SysAdmin', u => u.roles.includes('sysadmin')], 13714 ['other', 'Otros', u => !u.roles.some(r => ['consignatario', 'certificador', 'buyer', 'seller', 'admin', 'sysadmin'].includes(r))], 13715 ['all', 'Todos los usuarios', () => true], 13716 ]; 13717 const group = this.state.adminUsersGroup || 'recent'; 13718 const consId = this.state.adminUsersConsignataria || ''; 13719 const consMap = new Map((this.state.adminConsignatarias || []).map(c =>
13719 [String(c.id), c.name || c.nombre || String(c.id)])); 13720 const consNames = u => (u.consignatariaIds || []).map(id => consMap.get(String(id)) || 'Consignataria no disponible').join(' · '); 13721 const normalize = v => String(v || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); 13722 const search = normalize(this.state.adminUsersSearch).trim(); 13723 const recent = users.filter(u => u.registeredTime).sort((a,b) => b.registeredTime - a.registeredTime || a.uid.localeCompare(b.uid)).slice(0,50); 13724 const base = group === 'recent' ? recent : users.filter((defs.find(d => d[0] === group) || defs[0])[2]); 13725 const consCounts = new Map(); 13726 const staff = users.filter(u => u.roles.some(r => ['consignatario','certificador'].includes(r))); 13727 for (const user of staff) for (const id of new Set((user.consignatariaIds || []).map(String))) consCounts.set(id, (consCounts.get(id) || 0) + 1); 13728 const unassigned = staff.filter(u => !(u.consignatariaIds || []).length).length; 13729 const companies = [...consMap].map(([id,label]) => ({id,label,count:consCounts.get(id) || 0})).sort((a,b) => a.label.localeCompare(b.label,'es')); 13730 let matches = base.filter(u => (!consId || (consId === '__none__' ? !(u.consignatariaIds || []).length : (u.consignatariaIds || []).map(String).includes(consId))) && (!search || normalize(u.displayName + ' ' + u.email + ' ' + consNames(u)).includes(search))); 13731 if (group !== 'recent') matches = [...matches].sort((a,b) => a.displayName.localeCompare(b.displayName,'es') || a.uid.localeCompare(b.uid)); 13732 if (group === 'all' && ['newest','oldest'].includes(this.state.adminUsersSort)) matches.sort((a,b) => !a.registeredTime ? (!b.registeredTime ? a.uid.localeCompare(b.uid) : 1) : !b.registeredTime ? -1 : (this.state.adminUsersSort === 'oldest' ? a.registeredTime - b.registeredTime : b.registeredTime - a.registeredTime) || a.uid.localeCompare(b.uid)); 13733 const total = matches.length, pageSize = group === 'recent' ? 10 : 20, pages = Math.max(1,Math.ceil(total / pageSize)); 13734 const page = Math.min(pages,Math.max(1,Number(this.state.adminUsersPage) || 1)); 13735 const rows = matches.slice((page-1)*pageSize,page*pageSize).map(u => ({...u, consignatariaSummary:consNames(u), registrationLabel:u.registeredTime ? 'Alta: ' + new Date(u.registeredTime).toLocaleDateString('es-AR') : 'Fecha de alta no disponible'})); 13736 return {rows,total,page,pages,start:total ? (page-1)*pageSize+1 : 0,end:Math.min(page*pageSize,total),missingDates:users.filter(u => !u.registeredTime).length, 13737 companies:[{id:'',label:'Todas las consignatarias',count:staff.length},...companies,{id:'__none__',label:'Sin consignataria asignada',count:unassigned}], 13738 filters:[...defs.map(([id,label,test]) => ({id,label,count:users.filter(test).length})),{id:'recent',label:'Ãltimos 50 registrados',count:recent.length}]}; 13739 } 13740 normAdminUser(user) { 13741 if (!user) return null; 13742 const uid = String(user.uid || user.userId || user.id || ''); 13743 const email = String(user.email || '').trim(); 13744 if (!uid && !email) return null; 13745 const firstName = String(user.firstName || user.nombre || '').trim(); 13746 const lastName = String(user.lastName || user.apellido || '').trim(); 13747 const displayName = String(user.displayName || user.name || [firstName, lastName].filter(Boolean).join(' ') || email.split('@')[0] || 'Usuario').trim(); 13748 return { 13749 source: user, uid, email, displayName, appDevices: user.appDevices || [], registeredTime: this.adminRegistrationTime(user), 13750 photoUrl: user.photoUrl || user.photoURL || user.profilePhotoUrl || '', 13751 roles: Array.isArray(user.roleIds) ?this.userRoleIds({ roleIds: user.roleIds }) : this.userRoleIds(user), 13752 consignatariaIds: [...new Set((Array.isArray(user.consignatariaIds) ?user.consignatariaIds : []).map(String).filter(Boolean))], 13753 consignatariaUserIsActive: user.consignatariaUserIsActive === true, 13754 }; 13755 } 13756 async hydrateAdminUserAssignments(users, actorUid) { 13757 const cache = this._adminAssignmentCache || (this._adminAssignmentCache = new Map()); 13758 const result = []; 13759 // Only staff have these memberships. Read sequentially and reuse successful 13760 // responses; stop on rate limiting rather than starting a request per row. 13761 for (const user of users) { 13762 if (this.state.user?.uid !== actorUid || !this.canManageUsers()) return []; 13763 if (!user.roles.some(role => ['consignatario','certificador','cabanero'].includes(role))) { result.push(user); continue; } 13764 // The directory response already includes authoritative memberships, 13765 // including an empty array for staff without assignments. 13766 if (Array.isArray(user.source?.consignatariaIds)) { 13767 const ids = user.source.consignatariaUserIsActive === false ? [] : [...new Set(user.source.consignatariaIds.map(String).filter(
13767Boolean))]; 13768 cache.set(user.uid, {at:Date.now(),ids}); 13769 result.push({...user,consignatariaIds:ids,consignatariaUserIsActive:user.source.consignatariaUserIsActive === true || ids.length > 0}); 13770 continue; 13771 } 13772 let entry = cache.get(user.uid); 13773 if (!entry || Date.now() - entry.at >= 300000) { 13774 let response; 13775 try { response = await this.apiFetch('/api/consignataria-users/' + encodeURIComponent(user.uid)); } 13776 catch (error) { if (error?.status !== 404) throw error; response = []; } 13777 const root = response?.data || response; 13778 const relations = Array.isArray(root) ? root : (Array.isArray(root?.items) ? root.items : [root?.item || root?.consignatariaUser || root?.membership || root]); 13779 const active = relations.filter(relation => relation && relation.isActive !== false); 13780 const ids = [...new Set(active.flatMap(relation => Array.isArray(relation.consignatariaIds) && relation.consignatariaIds.length ? relation.consignatariaIds : [relation.consignatariaId]).filter(id => id != null && String(id).trim()).map(String))]; 13781 entry = {at:Date.now(),ids}; cache.set(user.uid,entry); 13782 } 13783 result.push({...user,consignatariaIds:entry.ids,consignatariaUserIsActive:entry.ids.length > 0}); 13784 } 13785 return result; 13786 } 13787 async fetchAdminUsers(force = false) { 13788 if (!this.canManageUsers() || this.state.adminUsersLoading) return; 13789 const uid = this.state.user?.uid || ''; 13790 const now = Date.now(); 13791 if (this._adminUsersRequestUid !== uid) { 13792 this._adminUsersRequestUid = uid; 13793 this._adminAssignmentCache = new Map(); 13794 this._adminUsersLoadedAt = 0; 13795 this._adminUsersRetryAt = 0; 13796 this._adminUsersRateLimits = 0; 13797 } 13798 if (now < (this._adminUsersRetryAt || 0)) { 13799 const seconds = Math.ceil((this._adminUsersRetryAt - now) / 1000); 13800 this.setState({adminUsersError:'El servicio está limitando las consultas. Podés volver a actualizar en ' + seconds + ' segundos.'}); 13801 return; 13802 } 13803 if (!force && Array.isArray(this.state.adminUsers) && now - (this._adminUsersLoadedAt || 0) < 300000) return; 13804 if (force && now - (this._adminUsersLoadedAt || 0) < 15000) return; 13805 this.setState({ adminUsersLoading: true, adminUsersError: '' }); 13806 try { 13807 const directory = await this.apiFetch('/api/admin-directory' + (force ? '?refresh=1' : '')); 13808 const raw = this.listFrom(directory, ['data','items','users']); 13809 const devAccess = {uids:raw.filter(user=>(user.roleIds || []).includes('dev')).map(user=>user.uid)}; 13810 const consignatariasResponse = directory.consignatarias || []; 13811 if (this.state.user?.uid !== uid || !this.canManageUsers()) return; 13812 const devUids = new Set(Array.isArray(devAccess?.uids) ?devAccess.uids.map(String) : []); 13813 let users = raw.map((user) => { 13814 const account = this.normAdminUser(user); 13815 if (!account || !devUids.has(account.uid)) return account; 13816 return { ...account, roles: [...new Set([...(account.roles || []), 'dev'])] }; 13817 }).filter(Boolean) 13818 .filter((user) => user.uid !== this.state.user?.uid) 13819 .sort((a, b) => a.displayName.localeCompare(b.displayName, 'es')); 13820 users = await this.hydrateAdminUserAssignments(users, uid); 13821 if (this.state.user?.uid !== uid || !this.canManageUsers()) return; 13822 const consignatarias = this.listFrom(consignatariasResponse, ['data', 'items', 'consignatarias']) 13823 .filter((item) => item?.id && item.isActive !== false); 13824 this._adminUsersLoadedAt = Date.now(); 13825 this._adminUsersRateLimits = 0; 13826 this._adminUsersRetryAt = 0; 13827 this.setState({ adminUsers: users, adminCurrentVersion: directory.currentVersion || '', adminVersionsAvailable: directory.versionsAvailable !== false, adminConsignatarias: consignatarias, adminUsersLoading: false, adminUsersError: directory.stale ? 'Mostrando la última lista disponible mientras se recupera la actualización.' : '' }); 13828 } catch (error) { 13829 if (this.state.user?.uid !== uid || !this.canManageUsers()) return; 13830 let message = error?.message || 'No se pudieron cargar los usuarios.'; 13831 if (error?.status === 429) { 13832 this._adminUsersRateLimits = (this._adminUsersRateLimits || 0) + 1; 13833 const wait = Math.max(Number(error.retryAfterMs) || 0, Math.min(300000,60000 * 2 ** Math.min(this._adminUsersRateLimits-1,3))); 13834 this._adminUsersRetryAt = Date.now() + wait; 13835 message = 'El servicio está limitando las consultas. Esperá ' + Math.ceil(wait / 1000) + ' segundos antes de actualizar.'; 13836 } 13837 if (this.state.adminUsers?.length) message += ' Se mantiene la última lista cargada.'; 13838 this.setState({ adminUsersLoading: false, adminUsersError: message }); 13839 } 13840 } 13841 toggleAdminUsers() { 13842 if (!this.canManageUsers()) return; 13843 const open = !this.state.adminUsersOpen; 13844 this.setState({ adminUsersOpen: open, mailingOpen: open ?false : this.state.mailingOpen, adminUserMessage: null }); 13845 if (open) this.fetchAdminUsers(); 13846 } 13847 setAdminUsersSearch(value) { this.setState({ adminUsersSearch: value || '', adminUsersPage: 1 }); } 13848 async selectAdminUser(account) { 13849 if (!account?.uid || this.state.adminUserSaving || !this.canManageUsers()) return; 13850 if (this.state.adminSelectedUser?.uid === account.uid && this.state.adminUserRelationLoading) return; 13851 const actorUid = this.state.user?.uid; 13852 const cached = this._adminUsersRequestUid === actorUid ? this._adminAssignmentCache?.get(account.uid) : null; 13853 const fresh = cached && Date.now() - cached.at < 300000; 13854 const ids = [...(account.consignatariaIds || [])]; 13855 this.setState({ 13856 adminSelectedUser: account, 13857 adminSelectedRoles: [...(account.roles || [])], 13858 adminSelectedConsignatariaIds: ids, 13859 adminOriginalRoles: [...(account.roles || [])], 13860 adminOriginalConsignatariaIds: ids, 13861 adminOriginalRelationIsActive: account.consignatariaUserIsActive === true, 13862 adminUserRelationLoading: !fresh, 13863 adminUserMessage: null, 13864 }); 13865 if (fresh) { this.setState({adminSelectedConsignatariaIds:[...cached.ids],adminOriginalConsignatariaIds:[...cached.ids],adminOriginalRelationIsActive:cached.ids.length > 0}); return; } 13866 try { 13867 const response = await this.apiFetch('/api/consignataria-users/' + encodeURIComponent(account.uid)); 13868 const root = response?.data || response; 13869 const relations = Array.isArray(root) ?root : (Array.isArray(root?.items) ?root.items : [root?.item || root?.consignatariaUser || root?.membership || root]); 13870 const assignedIds = [...new Set(relations.flatMap((relation) => 13871 Array.isArray(relation?.consignatariaIds) && relation.consignatariaIds.length 13872 ?relation.consignatariaIds : [relation?.consignatariaId] 13873 ).map(String).filter((id) => id && id !== 'undefined'))]; 13874 if (this.state.user?.uid !== actorUid || !this.canManageUsers()) return; 13875 (this._adminAssignmentCache || (this._adminAssignmentCache = new Map())).set(account.uid,{at:Date.now(),ids:assignedIds}); 13876 const isActive = relations.some((relation) => relation?.isActive === true); 13877 if (this.state.user?.uid === actorUid && this.state.adminSelectedUser?.uid === account.uid) this.setState({ 13878 adminSelectedConsignatariaIds: assignedIds, 13879 adminOriginalConsignatariaIds: assignedIds, 13880 adminOriginalRelationIsActive: isActive, 13881 adminUserRelationLoading: false, 13882 }); 13883 } catch (error) { 13884 if (this.state.user?.uid === actorUid && this.state.adminSelectedUser?.uid === account.uid) this.setState({ 13885 adminUserRelationLoading: false, 13886 ...(error?.status !== 404 ?{ adminUserMessage: { ok: false, text: 'No se pudieron consultar las asignaciones: ' + (error?.message || 'Error de conexión.') } } : {}), 13887 }); 13888 } 13889 } 13890 toggleAdminRole(roleId) { 13891 if (this.state.adminUserSaving || this.state.adminUserRelationLoading) return; 13892 const id = String(roleId || '').toLowerCase(); 13893 const selected = [...(this.state.adminSelectedRoles || [])]; 13894 const next = id === 'user' 13895 ?(selected.includes('user') ?[] : ['user']) 13896 : [...selected.filter((role) => role !== 'user' && role !== id), ...(selected.includes(id) ?[] : [id])]; 13897 this.setState({ adminSelectedRoles: next, adminUserMessage: null }); 13898 } 13899 selectAdminConsignataria(id) { 13900 if (this.state.adminUserSaving || this.state.adminUserRelationLoading) return;
13901 const value = String(id || ''); 13902 if (!value) return; 13903 const selected = this.state.adminSelectedConsignatariaIds || []; 13904 const next = selected.includes(value) ?selected.filter((item) => item !== value) : [...selected, value]; 13905 this.setState({ adminSelectedConsignatariaIds: next, adminUserMessage: null }); 13906 } 13907 async adminAccessWrite(path, options) { 13908 if (!this.state.user?.uid || !this.state.adminSelectedUser?.uid || !this.canManageUsers()) throw new Error('Iniciá sesión con permisos de administración.'); 13909 if (!['PUT','DELETE'].includes(options?.method)) throw new Error('Operación de permisos no válida.'); 13910 return this.apiFetch(path, options); 13911 } 13912 async saveAdminUserAccess() { 13913 const account = this.state.adminSelectedUser; 13914 if (!account?.uid || this.state.adminUserSaving || this.state.adminUserRelationLoading || !this.canManageUsers()) return; 13915 const roles = [...new Set((this.state.adminSelectedRoles || []).map((role) => String(role).trim().toLowerCase()).filter(Boolean))]; 13916 if (!roles.length) { 13917 this.setState({ adminUserMessage: { ok: false, text: 'Seleccioná al menos un rol.' } }); 13918 return; 13919 } 13920 const needsConsignataria = roles.some((role) => ['consignatario', 'certificador'].includes(role)); 13921 const consignatariaIds = [...new Set((this.state.adminSelectedConsignatariaIds || []).map(String).filter(Boolean))]; 13922 const activeIds = new Set((this.state.adminConsignatarias || []).filter((item) => item.isActive !== false).map((item) => String(item.id))); 13923 if (needsConsignataria && (!consignatariaIds.length || consignatariaIds.some((id) => !activeIds.has(id)))) { 13924 this.setState({ adminUserMessage: { ok: false, text: 'Elegà al menos una consignataria activa de la lista.' } }); 13925 return; 13926 } 13927 const originalRoles = [...new Set((this.state.adminOriginalRoles || []).map((role) => String(role).trim().toLowerCase()).filter(Boolean))]; 13928 const rolesChanged = roles.slice().sort().join('|') !== originalRoles.slice().sort().join('|'); 13929 const backendRoles = roles.filter((role) => role !== 'dev'); 13930 const originalBackendRoles = originalRoles.filter((role) => role !== 'dev'); 13931 const backendRolesChanged = backendRoles.slice().sort().join('|') !== originalBackendRoles.slice().sort().join('|'); 13932 const devChanged = roles.includes('dev') !== originalRoles.includes('dev'); 13933 const originalConsignatariaIds = [...new Set((this.state.adminOriginalConsignatariaIds || []).map(String).filter(Boolean))]; 13934 const consignatariaChanged = (needsConsignataria ?consignatariaIds : []).slice().sort().join('|') !== originalConsignatariaIds.slice().sort().join('|') 13935 || (needsConsignataria && !this.state.adminOriginalRelationIsActive); 13936 if (!rolesChanged && !consignatariaChanged) { 13937 this.setState({ adminUserMessage: { ok: true, text: 'No hay cambios para guardar.' } }); 13938 return; 13939 } 13940 const checkpoint = patch => { 13941 const updated = {...this.state.adminSelectedUser,...patch}; 13942 this.setState({adminSelectedUser:updated,adminUsers:(this.state.adminUsers || []).map(user=>user.uid===account.uid?updated:user), 13943 ...(patch.roles ? {adminOriginalRoles:patch.roles} : {}), 13944 ...(patch.consignatariaIds ? {adminOriginalConsignatariaIds:patch.consignatariaIds,adminOriginalRelationIsActive:patch.consignatariaUserIsActive} : {})}); 13945 }; 13946 this.setState({ adminUserSaving: true, adminUserRetrying:false, adminUserMessage: null }); 13947 try { 13948 if (backendRolesChanged && backendRoles.length) { 13949 await this.adminAccessWrite('/api/Users/' + encodeURIComponent(account.uid) + '/role', { method: 'PUT', body: JSON.stringify({ roleId: backendRoles }) }); 13950 checkpoint({roles:[...backendRoles,...(originalRoles.includes('dev')?['dev']:[])]}); 13951 } 13952 if (devChanged) { 13953 await this.adminAccessWrite('/api/dev-access/' + encodeURIComponent(account.uid), { 13954 method: 'PUT', 13955 body: JSON.stringify({ enabled: roles.includes('dev') }), 13956 }); 13957 checkpoint({roles}); 13958 } 13959 if (consignatariaChanged) { 13960 try { 13961 await this.adminAccessWrite('/api/consignataria-users/' + encodeURIComponent(account.uid), needsConsignataria 13962 ?{ method: 'PUT', body: JSON.stringify({ consignatariaIds, isActive: true }) } 13963 : { method: 'DELETE' }); 13964 } catch (relationError) { 13965 if (!(relationError?.status === 404 && !needsConsignataria)) throw relationError; 13966 } 13967 } 13968 if (consignatariaChanged) checkpoint({consignatariaIds:needsConsignataria?consignatariaIds:[],consignatariaUserIsActive:needsConsignataria}); 13969 const consignataria = (this.state.adminConsignatarias || []).find((item) => String(item.id) === consignatariaIds[0]); 13970 let notificationWarning = '';
13971 try { 13972 await this.apiFetch('/api/notifications/admin-role-change', { 13973 method: 'POST', 13974 body: JSON.stringify({ 13975 recipientUid: account.uid, 13976 roles, 13977 previousRoles: originalRoles, 13978 consignatariaId: needsConsignataria ?consignatariaIds[0] : '', 13979 consignatariaIds: needsConsignataria ?consignatariaIds : [], 13980 consignatariaName: needsConsignataria ?String(consignataria?.name || consignataria?.nombre || consignatariaIds[0]) : '', 13981 eventId: `roles-${account.uid}-${Date.now()}`, 13982 }), 13983 }); 13984 } catch (notificationError) { 13985 notificationWarning = ' No se pudo enviar la notificacion.'; 13986 console.warn('role change notification', notificationError); 13987 } 13988 if (account.uid === this.state.user?.uid) { await (await this.ensureFirebase()).auth().currentUser?.getIdToken(true); await this.refreshProfileData(true); } 13989 (this._adminAssignmentCache || (this._adminAssignmentCache = new Map())).set(account.uid,{at:Date.now(),ids:needsConsignataria?consignatariaIds:[]}); 13990 const updated = { ...account, roles, consignatariaIds: needsConsignataria ?consignatariaIds : [], consignatariaUserIsActive: needsConsignataria }; 13991 this.setState({ 13992 adminUsers: (this.state.adminUsers || []).map((user) => user.uid === account.uid ?updated : user), 13993 adminSelectedUser: updated, 13994 adminOriginalRoles: roles, 13995 adminOriginalConsignatariaIds: updated.consignatariaIds, 13996 adminOriginalRelationIsActive: updated.consignatariaUserIsActive, 13997 adminUserSaving: false, adminUserRetrying:false, 13998 adminUserMessage: { ok: true, text: 'Roles y consignataria actualizados.' + notificationWarning }, 13999 }); 14000 } catch (error) { 14001 const statusHint = error?.status === 401 ?'Sesión vencida (401). ' : error?.status === 403 ?'Sin permisos (403). ' : ''; 14002 this.setState({ adminUserSaving: false, adminUserRetrying:false, adminUserMessage: { ok: false, text: statusHint + (error?.message || 'No se pudieron guardar los permisos.') } }); 14003 } 14004 } 14005 syncMailingRoute(prevState) { 14006 // StreamableLogic's host calls componentDidUpdate with props only, not prevState. 14007 // Keep our own snapshot and advance it BEFORE setState to avoid reset loops. 14008 prevState = this._mailingRouteSnapshot; 14009 this._mailingRouteSnapshot = { route: this.state.route, authReady: this.state.authReady, user: { uid: this.state.user?.uid }, apiUser: this.state.apiUser }; 14010 const identityChanged = prevState?.user?.uid !== this.state.user?.uid; 14011 if (identityChanged) { 14012 this._mailingFetchRevision = (this._mailingFetchRevision || 0) + 1; 14013 this._mailingFetchOwner = null; 14014 this._lastMailingFetchAt = 0; 14015 this.setState({ mailingLoading: false, mailingContactsState: 'idle', mailingCampaignsState: 'idle', mailingStatusState: 'idle', mailingLaunchState: 'idle', mailingCampaigns: [], mailingContacts: [], mailingContactTotal: 0, mailingLaunchInterestTotal: 0, mailingStatus: null, mailingError: '' }); 14016 } 14017 if (this.state.route === 'mailingAdmin' && this.state.authReady && this.state.user?.uid && this.canManageUsers() 14018 && (prevState?.route !== this.state.route || prevState?.authReady !== this.state.authReady || identityChanged || prevState?.apiUser !== this.state.apiUser)) { 14019 this.fetchMailing(); 14020 } 14021 } 14022 mailingRoot(payload) { return payload?.data || payload || {}; } 14023 async fetchMailing(force = false) { 14024 if (!this.canManageUsers()) return; 14025 const owner = String(this.state.user?.uid || this.state.apiUser?.uid || ''); 14026 if (this._mailingFetchOwner === owner && this.state.mailingLoading) return; 14027 if (!force && this._mailingFetchOwner === owner && this._lastMailingFetchAt && Date.now() - this._lastMailingFetchAt < 30000) { 14028 this.ensureMailingEditorObserver(); 14029 return; 14030 } 14031 const changedOwner = this._mailingFetchOwner !== owner; 14032 this._mailingFetchOwner = owner; 14033 const revision = this._mailingFetchRevision = (this._mailingFetchRevision || 0) + 1; 14034 const current = () => revision === this._mailingFetchRevision && owner === String(this.state.user?.uid || this.state.apiUser?.uid || '') && this.canManageUsers(); 14035 this.ensureMailingEditorObserver(); 14036 this.setState({ mailingLoading: true, mailingError: '', mailingContactsState: 'loading', mailingCampaignsState: 'loading', mailingStatusState: 'loading', mailingLaunchState: 'loading', 14037 ...(changedOwner ? { mailingCampaigns: [], mailingStatus: null, mailingContactTotal: 0, mailingLaunchInterestTotal: 0 } : {}) }); 14038 const errors = []; 14039 const load = async (url, stateKey, label, map) => {
14040 try { 14041 const data = this.mailingRoot(await this.apiFetch(url)); 14042 if (current()) this.setState({ ...map(data), [stateKey]: 'ready' }); 14043 } catch (error) { 14044 errors.push(label + ': ' + (error?.message || 'No se pudo consultar.')); 14045 if (current()) this.setState({ [stateKey]: 'error', mailingError: errors.join(' ') }); 14046 } 14047 }; 14048 await Promise.all([ 14049 load('/api/mailing/contacts?summary=1', 'mailingContactsState', 'Contactos', data => ({ mailingContactTotal: Number(data.total) || 0 })), 14050 load('/api/mailing/campaigns', 'mailingCampaignsState', 'Campañas', data => ({ mailingCampaigns: Array.isArray(data.campaigns) ? data.campaigns : [] })), 14051 load('/api/mailing/status', 'mailingStatusState', 'Mailgun', data => ({ mailingStatus: data })), 14052 load('/api/launch-interests', 'mailingLaunchState', 'Interesados', data => ({ mailingLaunchInterestTotal: Number(data.total) || 0 })), 14053 ]); 14054 if (current()) { 14055 this.setState({ mailingLoading: false }); 14056 this._lastMailingFetchAt = errors.length ? 0 : Date.now(); 14057 } 14058 } 14059 toggleMailing() { 14060 if (!this.canManageUsers()) return; 14061 const leaving = this.state.route === 'mailingAdmin'; 14062 document.documentElement.classList.remove('mailing-editor-is-expanded'); 14063 this.syncAppLocation(leaving ?'profile' : 'mailingAdmin'); 14064 this.setState({ 14065 route: leaving ?'profile' : 'mailingAdmin', 14066 mailingOpen: !leaving, 14067 adminUsersOpen: false, 14068 mailingMessage: null, 14069 mailingEditorExpanded: false, 14070 mailingInsertDialog: '', 14071 }, () => { 14072 window.scrollTo(0, 0); 14073 if (leaving) this.refreshProfileData(); 14074 else this.fetchMailing(); 14075 }); 14076 } 14077 toggleMailingConsent() { 14078 if (!this.canManageUsers() || this.state.mailingBusy) return; 14079 this.setState({ mailingConsentConfirmed: !this.state.mailingConsentConfirmed, mailingMessage: null }); 14080 } 14081 newMailingCampaign() { 14082 this.setState({ 14083 mailingSelectedId: '', 14084 mailingDraft: { name: '', subject: '', previewText: '', html: '', text: '', audience: 'all', campaignType: 'standard' }, 14085 mailingReport: null, 14086 mailingReportError: '', 14087 mailingReportSearch: '', 14088 mailingReportStatusFilter: 'all', 14089 mailingReportLetter: 'all', 14090 mailingReportPage: 1, 14091 mailingMessage: null, 14092 }, () => this.syncMailingEditor('')); 14093 } 14094 launchMailingTemplate() { 14095 const html = ` 14096<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="width:100%;margin:0;background:#eef4f8;font-family:Arial,Helvetica,sans-serif;color:#102f4e;"> 14097 <tr><td align="center" style="padding:28px 12px;"> 14098 <table role="presentation" width="680" cellspacing="0" cellpadding="0" border="0" style="width:100%;max-width:680px;background:#ffffff;border-radius:24px;overflow:hidden;box-shadow:0 16px 48px rgba(0,35,70,.13);"> 14099 <tr><td style="height:7px;background:linear-gradient(90deg,#003b71 0%,#0f73a9 54%,#26b7bc 100%);font-size:0;line-height:0;"> </td></tr> 14100 <tr><td align="center" style="padding:34px 42px 26px;background:#ffffff;"> 14101 <img src="https://rosgan.app/assets/rosgan-logo-blue.png" width="190" alt="Rosgan" style="display:block;width:190px;max-width:62%;height:auto;border:0;"> 14102 </td></tr> 14103 <tr><td style="padding:0 26px;"> 14104 <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background:#003b71;border-radius:22px;overflow:hidden;"> 14105 <tr><td align="center" style="padding:50px 42px 46px;background:linear-gradient(135deg,#00315f 0%,#075d98 58%,#1da8b5 100%);"> 14106 <div style="font-size:12px;line-height:1.4;letter-spacing:3px;text-transform:uppercase;color:#9debf0;font-weight:800;">Rosgan.app ya está en línea</div> 14107 <h1 style="margin:15px 0 16px;font-size:42px;line-height:1.08;color:#ffffff;font-weight:850;letter-spacing:-1.2px;">Todo el mercado ganadero, en un solo lugar.</h1> 14108 <p style="max-width:520px;margin:0 auto;font-size:18px;line-height:1.55;color:#dceef7;">Hoy presentamos la nueva plataforma de Rosgan: una experiencia digital más simple, cercana y potente para conectar personas, oportunidades y negocios.</p> 14109 </td></tr> 14110 </table> 14111 </td></tr> 14112 <tr><td style="padding:38px 42px 10px;"> 14113 <p style="margin:0 0 12px;font-size:17px;line-height:1.6;color:#294b68;">Te anotaste para enterarte antes que nadie. <strong style="color:#003b71;">Ese momento llegó: rosgan.app ya está disponible.</strong></p> 14114 <p style="margin:0;font-size:17px;line-height:1.6;color:#294b68;">Creamos un espacio donde el mercado ganadero se encuentra, se informa y hace negocios:</p> 14115 </td></tr> 14116 <tr><td style="padding:18px 42px 8px;"> 14117 <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"> 14118 <tr> 14119 <td width="33.33%" valign="top" style="padding:6px;"> 14120 <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="height:100%;background:#f0f7fb;border:1px solid #d8e8f1;border-radius:16px;"><tr><td valign="top" style="padding:22px 18px;"> 14121 <div style="width:38px;height:38px;line-height:38px;text-align:center;border-radius:12px;background:#dcedf8;color:#075d98;font-size:20px;font-weight:900;">01</div> 14122 <h2 style="margin:16px 0 8px;font-size:17px;line-height:1.25;color:#003b71;">Remates y cabañas</h2> 14123 <p style="margin:0;font-size:13px;line-height:1.55;color:#587087;">Catálogos, lotes y transmisiones, más cerca y mejor organizados.</p> 14124 </td></tr></table> 14125 </td> 14126 <td width="33.33%" valign="top" style="padding:6px;"> 14127 <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="height:100%;background:#eef9f7;border:1px solid #d2eae4;border-radius:16px;"><tr><td valign="top" style="padding:22px 18px;"> 14128 <div style="width:38px;height:38px;line-height:38px;text-align:center;border-radius:12px;background:#d8f0eb;color:#128268;font-size:20px;font-weight:900;">
1412802</div> 14129 <h2 style="margin:16px 0 8px;font-size:17px;line-height:1.25;color:#003b71;">Roslot</h2> 14130 <p style="margin:0;font-size:13px;line-height:1.55;color:#587087;">Una nueva forma de descubrir oportunidades y participar del mercado.</p> 14131 </td></tr></table> 14132 </td> 14133 <td width="33.33%" valign="top" style="padding:6px;"> 14134 <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="height:100%;background:#fff5f4;border:1px solid #f2dedb;border-radius:16px;"><tr><td valign="top" style="padding:22px 18px;"> 14135 <div style="width:38px;height:38px;line-height:38px;text-align:center;border-radius:12px;background:#f8e2df;color:#d64149;font-size:20px;font-weight:900;">03</div> 14136 <h2 style="margin:16px 0 8px;font-size:17px;line-height:1.25;color:#003b71;">Datos y conexión</h2> 14137 <p style="margin:0;font-size:13px;line-height:1.55;color:#587087;">Información para decidir mejor y vínculos que hacen crecer al sector.</p> 14138 </td></tr></table> 14139 </td> 14140 </tr> 14141 </table> 14142 </td></tr> 14143 <tr><td align="center" style="padding:34px 42px 42px;"> 14144 <p style="margin:0 0 20px;font-size:16px;line-height:1.55;color:#294b68;">Hoy comienza una nueva etapa. Gracias por ser parte desde el comienzo.</p> 14145 <a href="https://rosgan.app" style="display:inline-block;padding:16px 30px;border-radius:12px;background:#e63746;color:#ffffff;text-decoration:none;font-size:16px;line-height:1;font-weight:850;box-shadow:0 8px 20px rgba(230,55,70,.24);">Ingresar a rosgan.app</a> 14146 </td></tr> 14147 <tr><td align="center" style="padding:23px 30px;background:#002b53;color:#bad0e2;font-size:12px;line-height:1.6;"> 14148 <strong style="display:block;color:#ffffff;font-size:14px;letter-spacing:.4px;">ROSGAN</strong> 14149 El mercado ganadero, ahora conectado. 14150 </td></tr> 14151 </table> 14152 </td></tr> 14153</table>`; 14154 return { 14155 name: 'Lanzamiento rosgan.app', 14156 subject: 'Rosgan.app ya está disponible: todo el mercado ganadero en un solo lugar', 14157 previewText: 'Conocé la nueva plataforma de Rosgan. El mercado ganadero, ahora conectado.', 14158 html, 14159 text: 'Rosgan.app ya está disponible.\n\nTodo el mercado ganadero, en un solo lugar. Hoy presentamos la nueva plataforma de Rosgan: una experiencia digital más simple, cercana y potente para conectar personas, oportunidades y negocios.\n\nTe anotaste para enterarte antes que nadie. Ese momento llegó.\n\nRemates y cabañas, Roslot, información y conexiones para decidir mejor.\n\nIngresá ahora: https://rosgan.app', 14160 audience: 'launch_interest', 14161 campaignType: 'launch', 14162 }; 14163 } 14164 newLaunchMailingCampaign() { 14165 const draft = this.launchMailingTemplate(); 14166 this.setState({ 14167 mailingSelectedId: '', 14168 mailingDraft: draft, 14169 mailingReport: null, 14170 mailingReportError: '', 14171 mailingReportSearch: '', 14172 mailingReportStatusFilter: 'all', 14173 mailingReportLetter: 'all', 14174 mailingReportPage: 1, 14175 mailingMessage: { ok: true, text: 'Plantilla de lanzamiento lista. Revisala y guardá la campaña cuando quieras.' }, 14176 }, () => this.syncMailingEditor(draft.html)); 14177 } 14178 selectMailingCampaign(campaign) { 14179 if (!campaign) return; 14180 this.setState({ 14181 mailingSelectedId: campaign.id || '', 14182 mailingDraft: { 14183 name: campaign.name || '', subject: campaign.subject || '', previewText: campaign.previewText || '', 14184 html: campaign.html || '', text: campaign.text || '', audience: campaign.campaignType === 'launch' ?'launch_interest' : 'all', campaignType: campaign.campaignType === 'launch' ?'launch' : 'standard', 14185 }, 14186 mailingReport: null, 14187 mailingReportError: '', 14188 mailingReportSearch: '', 14189 mailingReportStatusFilter: 'all', 14190 mailingReportLetter: 'all', 14191 mailingReportPage: 1, 14192 mailingMessage: null, 14193 }, () => { 14194 this.syncMailingEditor(campaign.html || ''); 14195 this.fetchMailingCampaignReport(campaign.id); 14196 }); 14197 } 14198 setMailingDraftField(field, value) { 14199 if (!['name', 'subject', 'previewText', 'html', 'text'].includes(field)) return; 14200 this.setState({ mailingDraft: { ...(this.state.mailingDraft || {}), [field]: value || '' }, mailingMessage: null }); 14201 } 14202 mailingSelectedCampaign() { 14203 return (this.state.mailingCampaigns || []).find((campaign) => campaign.id === this.state.mailingSelectedId) || null; 14204 } 14205 mailingEditorElement() { return document.querySelector('[data-mailing-rich-editor="true"]'); } 14206 ensureMailingEditorObserver() { 14207 if (this._mailingEditorObserver || typeof MutationObserver === 'undefined') return; 14208 this._mailingEditorObserver = new MutationObserver(() => { 14209 const editor = this.mailingEditorElement(); 14210 if (!editor || editor.dataset.mailingHydrated === 'true') return; 14211 editor.dataset.mailingHydrated = 'true'; 14212 editor.innerHTML = this.mailingSanitizeHtml(this.state.mailingDraft?.html || ''); 14213 }); 14214 this._mailingEditorObserver.observe(document.body, { childList: true, subtree: true }); 14215 } 14216 mailingSanitizeHtml(value) { 14217 const template = document.createElement('template'); 14218 template.innerHTML = String(value || ''); 14219 template.content.querySelectorAll('script,iframe,object,embed,form,input,textarea,select,option,meta,link,base').forEach((node) => node.remove()); 14220 template.content.querySelectorAll('*').forEach((node) => {
14221 [...node.attributes].forEach((attribute) => { 14222 const name = attribute.name.toLowerCase(); 14223 const raw = String(attribute.value || '').trim(); 14224 if (name.startsWith('on')) node.removeAttribute(attribute.name); 14225 if ((name === 'href' || name === 'src') && !/^(https?:|mailto:|tel:|#|\/)/i.test(raw) && !(name === 'src' && /^data:image\/(?:png|jpe?g|gif|webp);base64,/i.test(raw))) node.removeAttribute(attribute.name); 14226 if (name === 'style' && /(?:javascript:|expression\s*\(|url\s*\(|behavior\s*:|-moz-binding)/i.test(raw)) node.removeAttribute(attribute.name); 14227 }); 14228 }); 14229 return template.innerHTML; 14230 } 14231 syncMailingEditor(html) { 14232 const value = this.mailingSanitizeHtml(html == null ?this.state.mailingDraft?.html : html); 14233 requestAnimationFrame(() => { 14234 const editor = this.mailingEditorElement(); 14235 if (editor) { 14236 editor.dataset.mailingHydrated = 'true'; 14237 if (editor.innerHTML !== value) editor.innerHTML = value; 14238 } 14239 }); 14240 } 14241 captureMailingEditorHtml() { 14242 if (this.state.mailingEditorSourceMode) return this.mailingSanitizeHtml(this.state.mailingDraft?.html || ''); 14243 const editor = this.mailingEditorElement(); 14244 const html = this.mailingSanitizeHtml(editor?.innerHTML || this.state.mailingDraft?.html || ''); 14245 this.state.mailingDraft = { ...(this.state.mailingDraft || {}), html }; 14246 return html; 14247 } 14248 onMailingVisualInput(event) { 14249 if (this.mailingSelectedCampaign()?.status !== 'draft' && this.mailingSelectedCampaign()) return; 14250 const html = this.mailingSanitizeHtml(event?.currentTarget?.innerHTML || event?.target?.innerHTML || ''); 14251 this.state.mailingDraft = { ...(this.state.mailingDraft || {}), html }; 14252 this.saveMailingEditorSelection(); 14253 } 14254 saveMailingEditorSelection() { 14255 const editor = this.mailingEditorElement(); 14256 const selection = window.getSelection?.(); 14257 if (!editor || !selection?.rangeCount) return; 14258 const range = selection.getRangeAt(0); 14259 if (editor.contains(range.commonAncestorContainer)) this._mailingEditorRange = range.cloneRange(); 14260 } 14261 restoreMailingEditorSelection() { 14262 const editor = this.mailingEditorElement(); 14263 if (!editor) return; 14264 editor.focus(); 14265 if (!this._mailingEditorRange) return; 14266 const selection = window.getSelection?.(); 14267 selection?.removeAllRanges(); 14268 selection?.addRange(this._mailingEditorRange); 14269 } 14270 execMailingEditorCommand(event, command, value = null) { 14271 event?.preventDefault?.(); 14272 const campaign = this.mailingSelectedCampaign(); 14273 if (this.state.mailingEditorSourceMode || (campaign && campaign.status !== 'draft')) return; 14274 this.restoreMailingEditorSelection(); 14275 document.execCommand(command, false, value); 14276 this.captureMailingEditorHtml(); 14277 this.saveMailingEditorSelection(); 14278 } 14279 insertMailingEditorHtml(html) { 14280 if (!html) return; 14281 this.restoreMailingEditorSelection(); 14282 document.execCommand('insertHTML', false, this.mailingSanitizeHtml(html)); 14283 this.captureMailingEditorHtml(); 14284 this.saveMailingEditorSelection(); 14285 } 14286 toggleMailingEditorSource() { 14287 if (this.state.mailingEditorSourceMode) { 14288 const html = this.mailingSanitizeHtml(this.state.mailingDraft?.html || ''); 14289 this.setState({ mailingEditorSourceMode: false, mailingDraft: { ...(this.state.mailingDraft || {}), html } }, () => this.syncMailingEditor(html)); 14290 return; 14291 } 14292 const html = this.captureMailingEditorHtml(); 14293 this.setState({ mailingEditorSourceMode: true, mailingDraft: { ...(this.state.mailingDraft || {}), html } }); 14294 } 14295 toggleMailingEditorExpanded() { 14296 const html = this.captureMailingEditorHtml(); 14297 const expanded = !this.state.mailingEditorExpanded; 14298 document.documentElement.classList.toggle('mailing-editor-is-expanded', expanded); 14299 this.setState({ mailingEditorExpanded: expanded }, () => this.syncMailingEditor(html)); 14300 } 14301 mailingEscapeHtml(value) { 14302 return String(value || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); 14303 } 14304 mailingNormalizeUrl(value) { 14305 let url = String(value || '').trim(); 14306 if (!url) return ''; 14307 if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) url = 'https://' + url; 14308 return /^(https?:|mailto:|tel:)/i.test(url) ?url : ''; 14309 } 14310 openMailingInsertDialog(type) { 14311 this.saveMailingEditorSelection(); 14312 const selected = String(window.getSelection?.()?.toString?.() || '').trim(); 14313 this.setState({ mailingInsertDialog: type, mailingInsertUrl: '', mailingInsertText: selected, mailingInsertImageSize: 'medium', mailingInsertImageLink: '', mailingImageError: '' }); 14314 } 14315 closeMailingInsertDialog() { 14316 this.setState({ mailingInsertDialog: '', mailingInsertUrl: '', mailingInsertText: '', mailingInsertImageSize: 'medium', mailingInsertImageLink: '', mailingImageError: '' }); 14317 } 14318 async uploadMailingEditorImage(file) { 14319 if (!file || this.state.mailingImageUploading) return; 14320 if (!/^image\/(jpeg|png|webp|gif)$/i.test(file.type || '') || file.size > 8 * 1024 * 1024) { 14321 this.setState({ mailingImageError: 'Usá una imagen JPG, PNG, WEBP o GIF de hasta 8 MB.' }); 14322 return; 14323 } 14324 this.setState({ mailingImageUploading: true, mailingImageError: '' }); 14325 try { 14326 const dataUrl = await new Promise((resolve, reject) => { 14327 const reader = new FileReader(); 14328 reader.onload = () => resolve(String(reader.result || '')); 14329 reader.onerror = () => reject(new Error('No se pudo leer la imagen.'));
14330 reader.readAsDataURL(file); 14331 }); 14332 const raw = await this.apiFetch('/api/mailing/assets/images', { 14333 method: 'POST', 14334 body: JSON.stringify({ fileName: file.name, contentType: file.type, dataBase64: dataUrl.split(',')[1] || '' }), 14335 }); 14336 const image = this.mailingRoot(raw); 14337 this.setState({ mailingImageUploading: false, mailingInsertUrl: image.url || '', mailingInsertText: this.state.mailingInsertText || file.name.replace(/\.[^.]+$/, ''), mailingImageError: '' }); 14338 } catch (error) { 14339 this.setState({ mailingImageUploading: false, mailingImageError: error?.message || 'No se pudo subir la imagen.' }); 14340 } 14341 } 14342 confirmMailingInsert() { 14343 const type = this.state.mailingInsertDialog; 14344 const url = this.mailingNormalizeUrl(this.state.mailingInsertUrl); 14345 const text = String(this.state.mailingInsertText || '').trim(); 14346 if (!type || !url) return; 14347 if (type === 'image') { 14348 if (!/^https?:/i.test(url)) return; 14349 const rawLink = String(this.state.mailingInsertImageLink || '').trim(); 14350 const redirectUrl = rawLink ?this.mailingNormalizeUrl(rawLink) : ''; 14351 if (rawLink && !/^https?:/i.test(redirectUrl)) return; 14352 const maxWidths = { small: '320px', medium: '480px', large: '640px', full: '100%' }; 14353 const maxWidth = maxWidths[this.state.mailingInsertImageSize] || maxWidths.medium; 14354 const image = '<img src="' + this.mailingEscapeHtml(url) + '" alt="' + this.mailingEscapeHtml(text || 'Imagen') + '" style="display:block;width:100%;max-width:100%;height:auto;border:0;border-radius:8px">'; 14355 const content = redirectUrl ?'<a href="' + this.mailingEscapeHtml(redirectUrl) + '" target="_blank" rel="noopener noreferrer" style="display:inline-block;width:100%;max-width:' + maxWidth + ';text-decoration:none">' + image + '</a>' : '<span style="display:inline-block;width:100%;max-width:' + maxWidth + '">' + image + '</span>'; 14356 this.insertMailingEditorHtml('<div style="text-align:center;margin:18px 0">' + content + '</div>'); 14357 } else if (type === 'button') { 14358 this.insertMailingEditorHtml('<div style="text-align:center;margin:22px 0"><a href="' + this.mailingEscapeHtml(url) + '" style="display:inline-block;padding:13px 22px;border-radius:8px;background:#075ca8;color:#ffffff;text-decoration:none;font-family:Arial,sans-serif;font-size:15px;font-weight:700">' + this.mailingEscapeHtml(text || 'Ver más') + '</a></div>'); 14359 } else { 14360 this.insertMailingEditorHtml('<a href="' + this.mailingEscapeHtml(url) + '" style="color:#075ca8;text-decoration:underline">' + this.mailingEscapeHtml(text || url) + '</a>'); 14361 } 14362 this.closeMailingInsertDialog(); 14363 } 14364 previewMailingEditor() { 14365 const body = this.captureMailingEditorHtml(); 14366 const popup = window.open('', '_blank'); 14367 if (!popup) { this.showToast('El navegador bloqueó la vista previa.'); return; } 14368 try { popup.opener = null; } catch {} 14369 const signature = '<div style="margin-top:28px;padding-top:18px;border-top:1px solid #dce6ef;color:#233b53;font:13px/1.6 Arial,sans-serif"><strong>Mercado Ganadero S.A.</strong><br><strong>Prensa y Comunicación</strong><br>Paraguay 777, Piso 11. S2000AWV.<br>Rosario. Santa Fe. República Argentina.<br>Celular: +54 9 341 2142232<br>Tel: (54 - 341) 2858558 interno 2968<br>E-mail: [email protected]<br>www.rosgan.com.ar</div>'; 14370 popup.document.open();
14371 popup.document.write('<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Vista previa del correo</title><style>body{margin:0;background:#eef3f7;font-family:Arial,sans-serif;color:#172b3f}.mail{max-width:720px;margin:28px auto;background:#fff;padding:34px;box-sizing:border-box;box-shadow:0 12px 36px rgba(0,30,60,.12)}img{max-width:100%;height:auto}@media(max-width:760px){.mail{margin:0;padding:20px}}</style></head><body><main class="mail">' + this.mailingSanitizeHtml(body) + signature + '</main></body></html>'); 14372 popup.document.close(); 14373 } 14374 async fetchMailingCampaignReport(campaignId) { 14375 const id = String(campaignId || this.state.mailingSelectedId || '').trim(); 14376 if (!id || !this.canManageUsers()) return; 14377 this.setState({ mailingReportLoading: true, mailingReportError: '' }); 14378 try { 14379 const raw = await this.apiFetch('/api/mailing/campaigns/' + encodeURIComponent(id) + '/report?limit=500'); 14380 const report = this.mailingRoot(raw); 14381 const deliveries = Array.isArray(report.deliveries) ?report.deliveries.slice() : []; 14382 let cursor = String(report.nextCursor || ''); 14383 for (let page = 0; cursor && page < 200; page++) { 14384 if (this.state.mailingSelectedId !== id) return; 14385 const nextRaw = await this.apiFetch('/api/mailing/campaigns/' + encodeURIComponent(id) + '/report?limit=500&summary=0&cursor=' + encodeURIComponent(cursor)); 14386 const next = this.mailingRoot(nextRaw); 14387 deliveries.push(...(Array.isArray(next.deliveries) ?next.deliveries : [])); 14388 cursor = String(next.nextCursor || ''); 14389 } 14390 report.deliveries = deliveries; 14391 report.nextCursor = cursor; 14392 if (this.state.mailingSelectedId !== id) return; 14393 this.setState({ mailingReport: report, mailingReportLoading: false, mailingReportError: '', mailingReportPage: 1 }); 14394 } catch (error) { 14395 if (this.state.mailingSelectedId !== id) return; 14396 this.setState({ mailingReportLoading: false, mailingReportError: error?.message || 'No se pudo cargar el reporte.' }); 14397 } 14398 } 14399 mailingReportDate(value) { 14400 const date = value ?new Date(value) : null; 14401 if (!date || Number.isNaN(date.getTime())) return 'â'; 14402 return date.toLocaleString('es-AR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' }); 14403 } 14404 async exportMailingCampaignReport() { 14405 const campaign = this.mailingSelectedCampaign(); 14406 if (!campaign?.id || this.state.mailingReportExporting) return; 14407 this.setState({ mailingReportExporting: true, mailingReportError: '' }); 14408 try { 14409 const deliveries = []; 14410 let cursor = ''; 14411 for (let page = 0; page < 200; page++) { 14412 const path = '/api/mailing/campaigns/' + encodeURIComponent(campaign.id) + '/report?limit=500' + (cursor ?'&cursor=' + encodeURIComponent(cursor) : ''); 14413 const report = this.mailingRoot(await this.apiFetch(path)); 14414 deliveries.push(...(Array.isArray(report.deliveries) ?report.deliveries : [])); 14415 cursor = String(report.nextCursor || ''); 14416 if (!cursor) break; 14417 } 14418 const columns = [ 14419 ['Email', 'email'], ['Nombre', 'name'], ['Tipo', 'kind'], ['Estado', 'status'], ['Enviado', 'sentAt'], 14420 ['Entregado', 'deliveredAt'], ['Primera apertura', 'openedAt'], ['Ãltima apertura', 'lastOpenedAt'], 14421 ['Aperturas', 'openCount'], ['Primer clic', 'clickedAt'], ['Ãltimo clic', 'lastClickedAt'], 14422 ['Clics', 'clickCount'], ['Rebote', 'failedAt'], ['Baja', 'unsubscribedAt'], ['Queja', 'complainedAt'], ['Detalle', 'error'], 14423 ]; 14424 const cell = (value) => { 14425 let text = String(value == null ?'' : value).replace(/\r?\n/g, ' '); 14426 if (/^[=+\-@]/.test(text)) text = "'" + text; 14427 return '"' + text.replace(/"/g, '""') + '"'; 14428 }; 14429 const rows = [columns.map(([label]) => cell(label)).join(';')]; 14430 deliveries.forEach((delivery) => rows.push(columns.map(([, key]) => cell(delivery[key])).join(';'))); 14431 const base = String(campaign.name || campaign.subject || 'campana').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase() || 'campana'; 14432 this.downloadCabanaBlob(new Blob(['\ufeff', rows.join('\r\n')], { type: 'text/csv;charset=utf-8' }), 'reporte-mailing-' + base + '.csv'); 14433 this.setState({ mailingReportExporting: false }); 14434 } catch (error) { 14435 this.setState({ mailingReportExporting: false, mailingReportError: error?.message || 'No se pudo exportar el reporte.' }); 14436 } 14437 } 14438 async saveMailingDraft() { 14439 if (!this.canManageUsers() || this.state.mailingBusy) return; 14440 const capturedHtml = this.captureMailingEditorHtml(); 14441 this.state.mailingDraft = { ...(this.state.mailingDraft || {}), html: capturedHtml }; 14442 const draft = this.state.mailingDraft || {}; 14443 if (!String(draft.subject || '').trim() || !String(draft.html || '').trim()) { 14444 this.setState({ mailingMessage: { ok: false, text: 'Completá el asunto y el contenido HTML.' } }); 14445 return; 14446 } 14447 const selected = this.mailingSelectedCampaign(); 14448 if (selected && selected.status !== 'draft') return; 14449 this.setState({ mailingBusy: true, mailingMessage: null }); 14450 try { 14451 const raw = await this.apiFetch(selected ?'/api/mailing/campaigns/' + encodeURIComponent(selected.id) : '/api/mailing/campaigns', { 14452 method: selected ?'PUT' : 'POST', 14453 body: JSON.stringify(draft), 14454 }); 14455 const campaign = this.mailingRoot(raw); 14456 const campaigns = selected 14457 ?(this.state.mailingCampaigns || []).map((item) =>
14457 item.id === selected.id ?campaign : item) 14458 : [campaign, ...(this.state.mailingCampaigns || [])]; 14459 this.setState({ mailingBusy: false, mailingCampaigns: campaigns, mailingSelectedId: campaign.id || '', mailingDraft: { name: campaign.name || '', subject: campaign.subject || '', previewText: campaign.previewText || '', html: campaign.html || '', text: campaign.text || '', audience: campaign.campaignType === 'launch' ?'launch_interest' : 'all', campaignType: campaign.campaignType === 'launch' ?'launch' : 'standard' }, mailingMessage: { ok: true, text: 'Campaña guardada.' } }, () => this.syncMailingEditor(campaign.html || '')); 14460 } catch (error) { 14461 this.setState({ mailingBusy: false, mailingMessage: { ok: false, text: error?.message || 'No se pudo guardar la campaña.' } }); 14462 } 14463 } 14464 async sendMailingTest() { 14465 const campaign = this.mailingSelectedCampaign(); 14466 if (!campaign?.id || this.state.mailingBusy) return; 14467 const email = String(this.state.mailingTestEmail || this.state.user?.email || '').trim(); 14468 if (!/^\S+@\S+\.\S+$/.test(email)) { 14469 this.setState({ mailingMessage: { ok: false, text: 'Ingresá un email de prueba válido.' } }); 14470 return; 14471 } 14472 this.setState({ mailingBusy: true, mailingMessage: null }); 14473 try { 14474 const raw = await this.apiFetch('/api/mailing/campaigns/' + encodeURIComponent(campaign.id) + '/test', { method: 'POST', body: JSON.stringify({ email }) }); 14475 const data = this.mailingRoot(raw); 14476 const updatedCampaign = data.campaign || { ...campaign, testC
14476ount: (Number(campaign.testCount) || 0) + 1, lastTestAt: new Date().toISOString(), lastTestEmail: email }; 14477 const campaigns = (this.state.mailingCampaigns || []).map((item) => item.id === campaign.id ?updatedCampaign : item); 14478 this.setState({ mailingBusy: false, mailingCampaigns: campaigns, mailingMessage: { ok: true, text: 'Prueba aceptada por Mailgun para ' + email + '. El tracking se actualizará automáticamente.' } }, () => this.fetchMailingCampaignReport(campaign.id)); 14479 } catch (error) { 14480 this.setState({ mailingBusy: false, mailingMessage: { ok: false, text: error?.message || 'No se pudo enviar la prueba.' } }); 14481 } 14482 } 14483 async deleteMailingCampaign(campaign) { 14484 if (!campaign?.id || this.state.mailingBusy || ['queued', 'sending'].includes(campaign.status)) return; 14485 if (!window.confirm('¿Eliminar la campaña "' + (campaign.name || campaign.subject || 'Sin nombre') + '" y todo su reporte? Esta acción no se puede deshacer.')) return; 14486 this.setState({ mailingBusy: true, mailingMessage: null }); 14487 try { 14488 await this.apiFetch('/api/mailing/campaigns/' + encodeURIComponent(campaign.id), { method: 'DELETE' }); 14489 const campaigns = (this.state.mailingCampaigns || []).filter((item) => item.id !== campaign.id); 14490 const selected = this.state.mailingSelectedId === campaign.id; 14491 this.setState({ 14492 mailingBusy: false, 14493 mailingCampaigns: campaigns, 14494 mailingSelectedId: selected ?'' : this.state.mailingSelectedId, 14495 mailingDraft: selected ?{ name: '', subject: '', previewText: '', html: '', text: '', audience: 'all', campaignType: 'standard' } : this.state.mailingDraft, 14496 mailingReport: selected ?null : this.state.mailingReport, 14497 mailingReportError: selected ?'' : this.state.mailingReportError, 14498 mailingMessage: { ok: true, text: 'Campaña eliminada.' }, 14499 }, () => { if (selected) this.syncMailingEditor(''); }); 14500 } catch (error) { 14501 this.setState({ mailingBusy: false, mailingMessage: { ok: false, text: error?.message || 'No se pudo eliminar la campaña.' } }); 14502 } 14503 } 14504 async queueMailingCampaign() { 14505 const campaign = this.mailingSelectedCampaign(); 14506 const resumable = ['draft', 'failed', 'cancelled'].includes(campaign?.status || ''); 14507 if (!campaign?.id || !resumable || this.state.mailingBusy) return; 14508 const action = campaign.status === 'draft' ?'Enviar' : 'Reanudar'; 14509 const audienceLabel = campaign.audience === 'launch_interest' 14510 ?'las personas anotadas para conocer el lanzamiento' 14511 : 'todos los contactos suscriptos'; 14512 if (!window.confirm('¿' + action + ' "' + campaign.name + '"? Se incluirán ' + audienceLabel + ' que sigan vigentes y todavÃa no tengan un envÃo registrado en esta campaña.')) return; 14513 this.setState({ mailingBusy: true, mailingMessage: null }); 14514 try { 14515 const raw = await this.apiFetch('/api/mailing/campaigns/' + encodeURIComponent(campaign.id) + '/queue', { method: 'POST', body: '{}' }); 14516 const response = this.mailingRoot(raw); 14517 const queuedTotal = Number(response.total) || 0; 14518 const remaining = Number(response.remaining) || 0; 14519 const campaigns = (this.state.mailingCampaigns || []).map((item) => item.id === campaign.id ?{ ...item, status: 'queued', total: queuedTotal } : item); 14520 this.setState({ mailingBusy: false, mailingCampaigns: campaigns, mailingMessage: { ok: true, text: 'EnvÃo iniciado para ' + remaining.toLocaleString('es-AR') + ' contactos pendientes, sin repetir los ya registrados.' } }); 14521 } catch (error) { 14522 this.setState({ mailingBusy: false, mailingMessage: { ok: false, text: error?.message || 'No se pudo programar la campaña.' } }); 14523 } 14524 } 14525 parseMailingCsv(text) { 14526 const lines = String(text || '').replace(/^\uFEFF/, '').split(/\r?\n/).filter((line) => line.trim()); 14527 if (!lines.length) return []; 14528 const separator = (lines[0].match(/;/g) || []).length > (lines[0].match(/,/g) || []).length ?';' : ','; 14529 const parseLine = (line) => { 14530 const cells = []; let current = ''; let quoted = false; 14531 for (let i = 0; i < line.length; i++) { 14532 const char = line[i]; 14533 if (char === '"' && quoted && line[i + 1] === '"') { current += '"'; i++; } 14534 else if (char === '"') quoted = !quoted; 14535 else if (char === separator && !quoted) { cells.push(current.trim()); current = ''; } 14536 else current += char; 14537 } 14538 cells.push(current.trim()); 14539 return cells; 14540 }; 14541 const headers = parseLine(lines[0]).map((header) => header.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim()); 14542 const emailIndex = headers.findIndex((header) => ['email', 'mail', 'correo', 'correo electronico'].includes(header)); 14543 if (emailIndex < 0) throw new Error('El CSV debe tener una columna email.'); 14544 const nameIndex = headers.findIndex((header) => ['nombre', 'name', 'nombre completo'].includes(header)); 14545 const sourceIndex = headers.findIndex((header) => ['origen', 'source'].includes(header)); 14546 const statusIndex = headers.findIndex((header) => ['estado', 'status'].includes(header)); 14547 const validationIndex = headers.findIndex((header) => ['validacion', 'validation status', 'validation_status', 'estado validacion', 'estado_validacion'].includes(header)); 14548 const validationReasonIndex = headers.findIndex((header) => ['motivo validacion', 'motivo_validacion', 'validation reason', 'validation_reason'].includes(header)); 14549 const bounceCountIndex = headers.findIndex((header) => ['rebotes', 'bounce count', 'bounce_count'].includes(header)); 14550 const tagsIndex = headers.findIndex((header) => ['etiquetas', 'tags'].includes(header)); 14551 return lines.slice(1).map(parseLine).map((cells) => ({ 14552 email: cells[emailIndex] || '', name: nameIndex >= 0 ?cells[nameIndex] || '' : '', 14553 source: sourceIndex >= 0 ?cells[sourceIndex] || '' : 'importación CSV', 14554 status: statusIndex >= 0 ?cells[statusIndex] || '' : 'subscribed', 14555 validationStatus: validationIndex >= 0 ?cells[validationIndex] || '' : 'validated', 14556 validationReason: validationReasonIndex >= 0 ?cells[validationReasonIndex] || '' : '', 14557 bounceCount: bounceCountIndex >= 0 ?Number(cells[bounceCountIndex]) || 0 : 0, 14558 tags: tagsIndex >= 0 ?cells[tagsIndex].split('|').map((tag) =>
14558 tag.trim()).filter(Boolean) : [], 14559 })).filter((contact) => contact.email); 14560 } 14561 async importMailingCsvFile(file) { 14562 if (!file || this.state.mailingBusy) return; 14563 if (!this.state.mailingConsentConfirmed) { 14564 this.setState({ mailingMessage: { ok: false, text: 'Confirmá el consentimiento de la base antes de importarla.' } }); 14565 return; 14566 } 14567 this.setState({ mailingBusy: true, mailingMessage: null }); 14568 try { 14569 const contacts = this.parseMailingCsv(await file.text()); 14570 let imported = 0; let rejected = 0; 14571 for (let offset = 0; offset < contacts.length; offset += 1000) { 14572 const raw = await this.apiFetch('/api/mailing/contacts/import', { method: 'POST', body: JSON.stringify({ contacts: contacts.slice(offset, offset + 1000), source: file.name, consentConfirmed: true }) }); 14573 const data = this.mailingRoot(raw); imported += Number(data.imported) || 0; rejected += Number(data.rejected) || 0; 14574 } 14575 this.setState({ mailingBusy: false, mailingMessage: { ok: true, text: imported + ' contactos importados' + (rejected ?' · ' + rejected + ' rechazados.' : '.') } }); 14576 await this.fetchMailing(true); 14577 } catch (error) { 14578 this.setState({ mailingBusy: false, mailingMessage: { ok: false, text: error?.message || 'No se pudo importar el CSV.' } }); 14579 } 14580 } 14581 async fetchAllPages(path, keys) { 14582 const all = []; 14583 let cursor = ''; 14584 for (let page = 0; page < 100; page++) { 14585 const separator = path.includes('?') ? '&' : '?'; 14586 const url = path + separator + 'limit=100' + (cursor ?'&cursor=' + encodeURIComponent(cursor) : ''); 14587 const data = await this.apiFetch(url); 14588 all.push(...this.listFrom(data, keys)); 14589 cursor = String(data?.nextCursor || data?.data?.nextCursor || ''); 14590 if (!cursor) break; 14591 } 14592 return all; 14593 } 14594 appPathForRoute(route) { 14595 const paths = { 14596 home: '/inicio', 14597 pool: '/roslot', 14598 roslotSummary: '/roslot/resumen-diario', 14599 load: '/roslot/cargar-lote', 14600 rosganLotLoad: '/cargar-lote-rosgan', 14601 remates: '/remates', 14602 rosganRemateCreate: '/remates/crear', 14603 cabanas: '/cabanas', 14604 feria: '/feria', 14605 feriaConfig: '/feria/config', 14606 mercado: '/estadisticas', 14607 profile: '/perfil', 14608 devActivity: '/dev/actividad', 14609 mailingAdmin: '/mailing', 14610 notifications: '/notificaciones', 14611 }; 14612 return paths[route] || ''; 14613 } 14614 routeFromPath(pathname = window.location.pathname) { 14615 let decoded = String(pathname || '/'); 14616 try { decoded = decodeURIComponent(decoded); } catch {} 14617 const path = decoded.replace(/\/+$/, '') || '/'; 14618 const normalized = path.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); 14619 const parts = normalized.split('/').filter(Boolean); 14620 if (!parts.length || parts[0] === 'lacassina') return ''; 14621 if (parts[0] === 'feria' && parts[1] === 'config') return 'feriaConfig'; 14622 if (parts[0] === 'roslot' && parts[1] === 'resumen-diario') return 'roslotSummary'; 14623 if (parts[0] === 'roslot' && parts[1] === 'cargar-lote') return 'load'; 14624 if (parts[0] === 'roslot' && parts[1] === 'lotes' && parts[2]) return 'pool'; 14625 if (parts[0] === 'cargar-lote-rosgan') return 'rosganLotLoad'; 14626 if (parts[0] === 'remates' && parts[1] === 'crear') return 'rosganRemateCreate'; 14627 if (parts[0] === 'remates' && parts[1] && parts[2] === 'auditor') return 'rosganAuditor'; 14628 if (parts[0] === 'remates' && parts[1] && parts[2] === 'resultados') return 'rosganResults'; 14629 if (parts[0] === 'remates' && parts[1] && parts[2] === 'estadisticas') return 'rosganStats'; 14630 if (parts[0] === 'remates' && parts[1] && parts[2] === 'comercial') return 'rosganCommercial'; 14631 if (parts[0] === 'remates' && parts[1] && parts[2] === 'precios') return 'rosganPriceBoard'; 14632 if (parts[0] === 'remates' && parts[1] && parts[2] === 'tv' && parts[3] === 'config') return 'rosganTvConfig'; 14633 if (parts[0] === 'remates' && parts[1] && parts[2] === 'tv') return 'rosganTvLower'; 14634 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'auditor') return 'cabanaAuditor'; 14635 if (parts[0] === 'cabanas' && parts[1] === 'crear') return 'remateConfig'; 14636 if (parts[0] === 'cabanas' && parts[1] === 'estadisticas' && !parts[2]) return 'cabanaStats'; 14637 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'estadisticas') return 'cabanaStats'; 14638 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'comercial') return 'cabanaCommercial'; 14639 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'precios') return 'cabanaPriceBoard'; 14640 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'tv' && parts[3] === 'config') return 'cabanaTvConfig'; 14641 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'tv') return 'cabanaTvLower'; 14642 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'preofertas') return 'cabanaPrebidPanel'; 14643 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'orden-ventas') return 'cabanaSalesOrder'; 14644 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'editar') return 'remateConfig'; 14645 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'crear-lote') return 'cabanaLotCreate'; 14646 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'lotes' && parts[3] && parts[4] === 'editar') return 'cabanaLotCreate'; 14647 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'lotes' && parts[3]) return 'cabanaLotDetail'; 14648 if (parts[0] === 'cabanas' && parts[1]) return 'cabanaDetail'; 14649 if (parts[0] === 'dev' && parts[1] === 'actividad') return 'devActivity'; 14650 if (parts[0] === 'perfil' && parts[1] === 'mailing') return 'mailingAdmin'; 14651 const routes = { 14652 inicio: 'home', home: 'home', 14653 roslot: 'pool', pool: 'pool', 14654 remate: 'remates', remates: 'remates', 14655 cabana: 'cabanas', cabanas: 'cabanas', 14656 feria: 'feria', ferias: 'feria', 14657 mercado: 'mercado', mercados: 'mercado', estadisticas: 'mercado', 14658 perfil: 'profile', profile: 'profile', 14659 mailing: 'mailingAdmin', 'mailing-admin': 'mailingAdmin', 14660 notificaciones: 'notifications', notifications: 'notifications', 14661 'cargar-lote': 'load', load: 'load', 'cargar-lote-rosgan': 'rosganLotLoad', 14662 }; 14663 return routes[parts[0]] || ''; 14664 } 14665 isPublicCabanaPath(pathname = window.location.pathname) { 14666 let decoded = String(pathname || '/'); 14667 try { decoded = decodeURIComponent(decoded); } catch {} 14668 const path = decoded.replace(/\/+$/, '') || '/'; 14669 const normalized = path.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); 14670 if (normalized === '/cabanas' || normalized === '/cabanas/estadisticas') return true; 14671 const target = this.cabanaPathTarget(path); 14672 return !!target && ['detail', 'lot', 'stats'].includes(target.action); 14673 } 14674 isPublicRematePath(pathname = window.location.pathname) { 14675 let decoded = String(pathname || '/'); 14676 try { decoded = decodeURIComponent(decoded); } catch {} 14677 const path = decoded.replace(/\/+$/, '') || '/'; 14678 const normalized = path.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); 14679 if (normalized === '/remates') return true; 14680 const target = this.rematePathTarget(path); 14681 return !!target && ['detail', 'lot', 'tv', 'results'].includes(target.action); 14682 } 14683 isPublicCatalogPath(pathname = window.location.pathname) { 14684 return this.isPublicRematePath(pathname) || this.isPublicCabanaPath(pathname); 14685 } 14686 activatePublicCatalogGuest(pathname = window.location.pathname) { 14687 if (this.state.user || window.firebase?.auth?.().currentUser) return false;
14688 if (!this.isPublicCatalogPath(pathname)) return false; 14689 const isRemates = this.isPublicRematePath(pathname); 14690 const route = this.routeFromPath(pathname) || (isRemates ?'remates' : 'cabanas'); 14691 this.setState({ 14692 user: null, 14693 apiUser: null, 14694 userConsignataria: null, userConsignatarias: [], 14695 publicAdvanceMode: '', 14696 route, 14697 routeLoading: !!(this.rematePathTarget(pathname) || this.cabanaPathTarget(pathname)), 14698 authReady: true, 14699 authBusy: false, 14700 authMessage: '', 14701 emailVerificationRequired: false, 14702 phoneVerificationRequired: false, 14703 notificationsItems: null, 14704 notificationsOpen: false, 14705 }, () => { 14706 if (isRemates) { 14707 if (this.state.monthRemates === null) this.fetchMonthRemates(); 14708 this.fetchLatestFinishedRemates(); 14709 if (!this.state.cabanasCollectionReady && !this.state.cabanasLoading) this.fetchCabanas(); 14710 this.loadRosganRemateAdmin(); 14711 if (this.rematePathTarget(pathname)) this.navigateFromPath(pathname); 14712 } else if (route === 'cabanas') this.fetchCabanas(); 14713 else this.navigateFromPath(pathname); 14714 }); 14715 return true; 14716 } 14717 remateListPlace(item) { 14718 const city = [item.localidad || item.location?.localidad, item.provincia || item.location?.provinciaNombre].filter(Boolean).join(', '); 14719 const venue = String(item.lugar || item.location?.lugar || item.ubicacion_remate || '').trim(); 14720 const location = city || venue || 'Ubicación a confirmar'; 14721 return { location, venue: city && this.consKey(venue) !== this.consKey(city) ? venue : '', hasVenue: !!city && !!venue && this.consKey(venue) !== this.consKey(city) }; 14722 } 14723 rematePathTarget(pathname = window.location.pathname) { 14724 let decoded = String(pathname || ''); 14725 try { decoded = decodeURIComponent(decoded); } catch {} 14726 const parts = decoded.replace(/\/+$/, '').split('/').filter(Boolean); 14727 if (String(parts[0] || '').toLowerCase() !== 'remates' || !parts[1] || String(parts[1]).toLowerCase() === 'crear') return null; 14728 const third = String(parts[2] || ''); 14729 const fourth = String(parts[3] || ''); 14730 const section = third.toLowerCase(); 14731 const action = section === 'auditor' ?'auditor' 14732 : section === 'orden-ventas' ?'salesOrder' 14733 : section === 'comercial' ?'commercial' 14734 : section === 'resultados' ?'results' 14735 : section === 'estadisticas' ?'stats' 14736 : section === 'precios' ?'prices' 14737 : section === 'tv' && fourth.toLowerCase() === 'config' ?'tvConfig' 14738 : section === 'tv' ?'tv' 14739 : third ?'lot' : 'detail'; 14740 return { remateId: String(parts[1]), lotId: action === 'lot' ?third : '', action, salesOrder: action === 'salesOrder' }; 14741 } 14742 roslotLotPathTarget(pathname = window.location.pathname) { 14743 let decoded = String(pathname || ''); 14744 try { decoded = decodeURIComponent(decoded); } catch {} 14745 const parts = decoded.replace(/\/+$/, '').split('/').filter(Boolean); 14746 if (String(parts[0] || '').toLowerCase() !== 'roslot' || String(parts[1] || '').toLowerCase() !== 'lotes' || !parts[2]) return null; 14747 return { cert: String(parts.slice(2).join('/')).trim() }; 14748 } 14749 cabanaPathTarget(pathname = window.location.pathname) { 14750 let decoded = String(pathname || ''); 14751 try { decoded = decodeURIComponent(decoded); } catch {} 14752 const rawParts = decoded.replace(/\/+$/, '').split('/').filter(Boolean); 14753 const parts = rawParts.map((part) => String(part).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '')); 14754 if (parts[0] !== 'cabanas') return null; 14755 if (parts[1] === 'crear') return { action: 'create', auctionId: '', lotCert: '' }; 14756 if (parts[1] === 'estadisticas' && !parts[2]) return { action: 'statsIndex', auctionId: '', lotCert: '' }; 14757 if (!parts[1]) return null; 14758 const sectionActions = { 14759 auditor: 'auditor', estadisticas: 'stats', comercial: 'commercial', precios: 'prices', 14760 preofertas: 'prebids', 'orden-ventas': 'salesOrder', editar: 'edit', 'crear-lote': 'createLot', 14761 }; 14762 const action = parts[2] === 'tv' ?(parts[3] === 'config' ?'tvConfig' : 'tv') 14763 : (parts[2] === 'lotes' && parts[3] ?(parts[4] === 'editar' ?'editLot' : 'lot') 14764 : (sectionActions[parts[2]] || 'detail')); 14765 return { action, auctionId: String(rawParts[1] || ''), lotCert: ['lot', 'editLot'].includes(action) ?String(rawParts[3] || '') : '' }; 14766 } 14767 legacyCabanaHashPath(hash = window.location.hash) { 14768 const raw = String(hash || '').replace(/^#\/?/, '').split('?')[0]; 14769 let decoded = raw; 14770 try { decoded = decodeURIComponent(raw); } catch {}
14771 const parts = decoded.split('/').filter(Boolean); 14772 const root = String(parts[0] || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); 14773 if (root === 'cabanas') { 14774 if (!parts[1]) return '/cabanas'; 14775 const base = '/cabanas/' + encodeURIComponent(parts[1]); 14776 if (String(parts[2] || '').toLowerCase() === 'lote' && parts[3]) return base + '/lotes/' + encodeURIComponent(parts.slice(3).join('/')); 14777 if (String(parts[2] || '').toLowerCase() === 'orden-ventas') return base + '/orden-ventas'; 14778 return base; 14779 } 14780 if (root === 'crear-remate') return parts[1] ?'/cabanas/' + encodeURIComponent(parts.slice(1).join('/')) + '/editar' : '/cabanas/crear'; 14781 if (root === 'crear-lote-cabana' && parts[1]) return '/cabanas/' + encodeURIComponent(parts.slice(1).join('/')) + '/crear-lote'; 14782 return ''; 14783 } 14784 legacyRoslotHashPath(hash = window.location.hash) { 14785 const raw = String(hash || '').replace(/^#\/?/, '').split('?')[0]; 14786 let decoded = raw; 14787 try { decoded = decodeURIComponent(raw); } catch {} 14788 const parts = decoded.split('/').filter(Boolean); 14789 if (String(parts[0] || '').toLowerCase() !== 'lot' || !parts[1]) return ''; 14790 return this.roslotLotPath(parts.slice(1).join('/')); 14791 } 14792 roslotLotPath(cert) { 14793 const value = String(cert || '').trim(); 14794 return value ?'/roslot/lotes/' + encodeURIComponent(value) : '/roslot'; 14795 } 14796 syncRoslotLotLocation(cert, { replace = false } = {}) { 14797 if (this.state.publicAdvanceMode) return; 14798 const path = this.roslotLotPath(cert); 14799 let currentPath = window.location.pathname; 14800 try { currentPath = decodeURIComponent(currentPath); } catch {} 14801 let targetPath = path; 14802 try { targetPath = decodeURIComponent(path); } catch {} 14803 if (currentPath.replace(/\/+$/, '') === targetPath.replace(/\/+$/, '') && !window.location.search && !window.location.hash) return; 14804 try { this.writeAppHistory(path, { replace }); } catch {} 14805 } 14806 cabanaPath(item = this.state.currentCabana, suffix = '') { 14807 const auctionId = this.cabanaRemateId(item); 14808 if (!auctionId) return '/cabanas'; 14809 return '/cabanas/' + encodeURIComponent(auctionId) + (suffix ?'/' + suffix : ''); 14810 } 14811 syncCabanaLocation(item = this.state.currentCabana, suffix = '', { replace = false } = {}) { 14812 if (this.state.publicAdvanceMode === 'lacassina') return; 14813 const path = this.cabanaPath(item, suffix); 14814 const normalizedSuffix = String(suffix || '').replace(/^\/+|\/+$/g, '').toLowerCase(); 14815 const publicRoute = normalizedSuffix === 'tv' ?'cabana-tv' : (normalizedSuffix === 'tv/config' ?'cabana-tv-config' : 'cabanas-public'); 14816 window.__ROSGAN_PUBLIC_PATH__ = path; 14817 window.__ROSGAN_PUBLIC_ROUTE__ = publicRoute; 14818 document.documentElement.dataset.publicRoute = publicRoute;
14819 let currentPath = window.location.pathname; 14820 try { currentPath = decodeURIComponent(currentPath); } catch {} 14821 let targetPath = path; 14822 try { targetPath = decodeURIComponent(path); } catch {} 14823 if (currentPath.replace(/\/+$/, '') === targetPath.replace(/\/+$/, '') && !window.location.search && !window.location.hash) return; 14824 try { this.writeAppHistory(path, { replace }); } catch {} 14825 } 14826 remateRouteId(item) { 14827 return String(item?.id_remate || item?.sourceId || item?.id || '').trim(); 14828 } 14829 remateLotRouteId(item) { 14830 return String(item?.id_certificado || item?.nro_certificado || item?.lotCert || item?.id || item?.orden_final || item?.orden_venta || '').trim(); 14831 } 14832 rematePath(item, lot = null) { 14833 const remateId = this.remateRouteId(item); 14834 if (!remateId) return '/remates'; 14835 const lotId = lot ?this.remateLotRouteId(lot) : ''; 14836 return '/remates/' + encodeURIComponent(remateId) + (lotId ?'/' + encodeURIComponent(lotId) : ''); 14837 } 14838 remateSectionPath(item, suffix = '') { 14839 const remateId = this.remateRouteId(item); 14840 return remateId ?'/remates/' + encodeURIComponent(remateId) + (suffix ?'/' + suffix : '') : '/remates'; 14841 } 14842 syncRemateSectionLocation(item, suffix = '', { replace = false } = {}) { 14843 if (this.state.publicAdvanceMode) return; 14844 try { this.writeAppHistory(this.remateSectionPath(item, suffix), { replace }); } catch {} 14845 } 14846 syncRemateLocation(item, lot = null, { replace = false } = {}) { 14847 if (this.state.publicAdvanceMode) return; 14848 const path = this.rematePath(item, lot); 14849 try { this.writeAppHistory(path, { replace }); } catch {} 14850 } 14851 syncAppLocation(route, { replace = false } = {}) { 14852 if (this.state.publicAdvanceMode === 'lacassina') return; 14853 const path = this.appPathForRoute(route); 14854 if (!path) return; 14855 let currentPath = window.location.pathname; 14856 try { currentPath = decodeURIComponent(currentPath); } catch {} 14857 if (currentPath === path && !window.location.search && !window.location.hash) return; 14858 try { this.writeAppHistory(path, { replace }); } catch {} 14859 } 14860 writeAppHistory(path, { replace = false } = {}) { 14861 const target = new URL(path, window.location.origin); 14862 if (target.href === window.location.href) return; 14863 window.history[replace ? 'replaceState' : 'pushState']({}, '', target.pathname + target.search + target.hash); 14864 } 14865 async navigateFromPath(pathname = window.location.pathname) { 14866 const request = this._pathRequest = (this._pathRequest || 0) + 1; 14867 const deep = this.rematePathTarget(pathname) || this.cabanaPathTarget(pathname) || this.roslotLotPathTarget(pathname); 14868 if (deep) this.setState({ routeLoading: true }); 14869 try { await this.resolveAppPath(pathname); } 14870 catch (error) { if (request === this._pathRequest) this.showToast(error?.message || 'No se pudo abrir la página.'); } 14871 finally { if (request === this._pathRequest) this.setState({ routeLoading: false }); } 14872 } 14873 async resolveAppPath(pathname = window.location.pathname) { 14874 if (this.state.publicAdvanceMode === 'lacassina') return; 14875 if (!this.state.user && !this.isPublicCatalogPath(pathname)) { 14876 this.setState({ route: 'login', routeLoading: false, apiUser: null, authReady: true }); 14877 return; 14878 } 14879 const roslotTarget = this.roslotLotPathTarget(pathname); 14880 if (roslotTarget) { 14881 if (!this.canAccessRoslot()) { 14882 this.syncAppLocation('home', { replace: true }); 14883 this.go('home', { syncPath: false }); 14884 this.showToast('Por ahora Roslot está disponible solo para administradores.'); 14885 return; 14886 } 14887 if (this.state.route !== 'lot' || String(this.state.selectedCert) !== roslotTarget.cert) { 14888 return this.openLot(roslotTarget.cert, false, { syncPath: false }); 14889 } 14890 return; 14891 } 14892 const legacyCabanaHash = String(window.location.hash || ''); 14893 if (/^#\/(?:cabanas(?:\/|$)|crear-remate\/|crear-lote-cabana\/)/i.test(legacyCabanaHash)) { 14894 this.navigateFromHash(legacyCabanaHash); 14895 return; 14896 } 14897 const cabanaTarget = this.cabanaPathTarget(pathname); 14898 if (cabanaTarget) { 14899 const requestedPrebidLot = cabanaTarget.action === 'prebids' ?String(new URLSearchParams(window.location.search || '').get('lote') || '').trim() : ''; 14900 if (cabanaTarget.action === 'create') { 14901 if (this.state.route !== 'remateConfig' || this.state.remateConfigEditingId) this.openRemateConfig('Remate Cabañas', false); 14902 return; 14903 } 14904 if (cabanaTarget.action === 'statsIndex') { 14905 return this.openCabanaStatsIndex(); 14906 } 14907 const requestedPath = window.location.pathname; 14908 const openTarget = (item) => { 14909 if (window.location.pathname !== requestedPath) return; 14910 if (!item) return; 14911 if (cabanaTarget.action === 'auditor') return this.openCabanaAuditor(item); 14912 else if (cabanaTarget.action === 'stats') return this.openCabanaStats(item); 14913 else if (cabanaTarget.action === 'commercial') return this.openCabanaCommercial(item); 14914 else if (cabanaTarget.action === 'prices') return this.openCabanaPriceBoard(item); 14915 else if (cabanaTarget.action === 'tv') return this.openCabanaTvLower(item); 14916 else if (cabanaTarget.action === 'tvConfig') return this.openCabanaTvConfig(item); 14917 else if (cabanaTarget.action === 'prebids') return this.openCabanaPrebidPanelPage(item, false, requestedPrebidLot); 14918 else if (cabanaTarget.action === 'salesOrder') return this.openCabanaSalesOrderPage(item, null, false); 14919 else if (cabanaTarget.action === 'edit') return this.openCabanaRemateEdit(item, false); 14920 else if (cabanaTarget.action === 'createLot') return this.openCabanaLotCreate(item, false); 14921 else if (cabanaTarget.action === 'editLot') return this.openCabanaLotEdit(item, cabanaTarget.lotCert, false); 14922 else if (cabanaTarget.action === 'lot') return this.openCabana(item, { focusLotCert: cabanaTarget.lotCert, syncPath: fal
14922se }); 14923 else return this.openCabana(item, { syncPath: false }); 14924 }; 14925 const item = (this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === cabanaTarget.auctionId); 14926 if (item) return openTarget(item); 14927 else return this.fetchCabanaById(cabanaTarget.auctionId, { force: true }).then(openTarget); 14928 } 14929 const remateTarget = this.rematePathTarget(pathname); 14930 if (remateTarget) { 14931 if (remateTarget.action === 'results') { 14932 const source = new URLSearchParams(window.location.search).get('origen') === 'historico' ? 'historico' : 'actual'; 14933 this.setState({route:'rosganResults', resultsPageKey:source+':'+remateTarget.remateId, resultsBackHref:'/remates/'+encodeURIComponent(remateTarget.remateId)}); 14934 return; 14935 } 14936 if (remateTarget.action === 'auditor') { 14937 return this.openRosganAuditorById(remateTarget.remateId); 14938 } 14939 if (remateTarget.salesOrder) { 14940 const active = (this.state.rosganRemates || []).find((candidate) => this.remateRouteId(candidate) === remateTarget.remateId); 14941 if (active) return this.openRosganSalesOrderPage(active, null, false, true); 14942 else return this.openRosganSalesOrderById(remateTarget.remateId); 14943 } 14944 if (['commercial', 'prices', 'stats', 'tv', 'tvConfig'].includes(remateTarget.action)) { 14945 const active = (this.state.rosganRemates || []).find((candidate) => this.remateRouteId(candidate) === remateTarget.remateId); 14946 if (active) return this.openRosganOperations(active, remateTarget.action, { syncPath: false }); 14947 else return this.openRosganOperationsById(remateTarget.remateId, remateTarget.action); 14948 } 14949 const currentRemateId = this.remateRouteId(this.state.currentRemate); 14950 if (this.state.route !== 'remateDetail' || currentRemateId !== remateTarget.remateId) { 14951 const active = (this.state.rosganRemates || []).find((candidate) => this.remateRouteId(candidate) === remateTarget.remateId); 14952 if (active) return this.openRosganActiveRemate(active, { lotId: remateTarget.lotId, syncPath: false }); 14953 else return this.openRosganActiveRemateById(remateTarget.remateId, remateTarget.lotId); 14954 } else if (remateTarget.lotId) { 14955 const lot = (this.state.remateLots || []).find((candidate) => this.remateLotRouteId(candidate) === remateTarget.lotId); 14956 if (lot && this.remateLotRouteId(this.state.rdLightbox) !== remateTarget.lotId) this.openRemateLot(lot, { syncPath: false }); 14957 } else if (this.state.rdLightbox) { 14958 this.setState({ rdLightbox: null }); 14959 } 14960 return; 14961 } 14962 const cleanPath = String(pathname || '/').replace(/\/+$/, '') || '/'; 14963 const route = this.routeFromPath(pathname) || (cleanPath === '/' ?'profile' : ''); 14964 const canonicalPath = this.appPathForRoute(route); 14965 if (canonicalPath && cleanPath !== canonicalPath) this.syncAppLocation(route, { replace: true }); 14966 if (route && route !== this.state.route) this.go(route, { syncPath: false }); 14967 } 14968 routeFromHash() { 14969 const raw = String(window.location.hash || '').replace(/^#\/?/, '').split('?')[0].split('/')[0]; 14970 const map = { home: 'home', pool: 'pool', profile: 'profile', 'mailing-admin': 'mailingAdmin', notifications: 'notifications', remates: 'remates', cabanas: 'cabanas', 'crear-remate': 'remateConfig', 'crear-lote-cabana': 'cabanaLotCreate', 'cargar-lote-rosgan': 'rosganLotLoad', feria: 'feria', mercado: 'mercado', load: 'load', 'load-lot': 'load' }; 14971 return map[raw] || ''; 14972 } 14973 navigateFromHash(hash = window.location.hash) { 14974 if (!this.state.user && !this.state.publicAdvanceMode) return; 14975 const path = String(hash || '').replace(/^#\/?/, '').split('?')[0]; 14976 const parts = path.split('/').filter(Boolean); 14977 if (parts[0] === 'lot' && parts[1]) { 14978 if (!this.canAccessRoslot()) { 14979 this.syncAppLocation('home', { replace: true }); 14980 this.go('home', { syncPath: false }); 14981 this.showToast('Por ahora Roslot está disponible solo para administradores.'); 14982 return; 14983 } 14984 const cert = decodeURIComponent(parts.slice(1).join('/'));
14985 const nextPath = this.roslotLotPath(cert); 14986 try { window.history.replaceState({}, '', nextPath); } catch {} 14987 if (this.state.route !== 'lot' || this.state.selectedCert !== cert) this.openLot(cert, false, { syncPath: false }); 14988 return; 14989 } 14990 if (parts[0] === 'crear-lote-cabana' && parts[1]) { 14991 const id = decodeURIComponent(parts.slice(1).join('/')); 14992 const item = (this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === id); 14993 if (item && (this.state.route !== 'cabanaLotCreate' || this.cabanaRemateId(this.state.cabanaLotCreateRemate) !== id)) this.openCabanaLotCreate(item); 14994 else if (!this.state.cabanas && !this.state.cabanasLoading) this.fetchCabanas().then(() => this.navigateFromHash(hash)); 14995 else if (!this.state.cabanasLoading) this.fetchCabanaById(id).then((remate) => remate && this.openCabanaLotCreate(remate)).catch(() => {}); 14996 return; 14997 } 14998 if (parts[0] === 'crear-remate' && parts[1]) { 14999 const id = decodeURIComponent(parts.slice(1).join('/')); 15000 const item = (this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === id); 15001 if (item && (this.state.route !== 'remateConfig' || this.state.remateConfigEditingId !== id)) this.openCabanaRemateEdit(item); 15002 else if (!this.state.cabanas && !this.state.cabanasLoading) this.fetchCabanas().then(() => this.navigateFromHash(hash)); 15003 if (!this.state.cabanasLoading) this.fetchCabanaById(id, { force: true }).then((remate) => remate && this.openCabanaRemateEdit(remate)).catch(() => {}); 15004 return; 15005 } 15006 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'orden-ventas') { 15007 const id = decodeURIComponent(parts[1]); 15008 const item = (this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === id); 15009 if (item) { 15010 if (this.state.route !== 'cabanaSalesOrder' || this.cabanaRemateId(this.state.currentCabana) !== id) { 15011 this.openCabanaSalesOrderPage(item); 15012 } 15013 } else if (!this.state.cabanasLoading) { 15014 this.fetchCabanaById(id, { force: true }) 15015 .then((remate) => remate && this.openCabanaSalesOrderPage(remate)) 15016 .catch(() => {}); 15017 } 15018 return; 15019 } 15020 if (parts[0] === 'cabanas' && parts[1] && parts[2] === 'lote' && parts[3]) { 15021 const id = decodeURIComponent(parts[1]); 15022 const lotCert = decodeURIComponent(parts.slice(3).join('/')); 15023 const item = (this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === id); 15024 if (item) this.openCabana(item, { focusLotCert: lotCert }); 15025 else if (!this.state.cabanasLoading) { 15026 this.fetchCabanaById(id, { force: true }) 15027 .then((remate) => remate && this.openCabana(remate, { focusLotCert: lotCert })) 15028 .catch(() => {}); 15029 } 15030 return; 15031 } 15032 if (parts[0] === 'cabanas' && parts[1]) { 15033 const id = decodeURIComponent(parts.slice(1).join('/')); 15034 const item = (this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === id); 15035 if (item && (this.state.route !== 'cabanaDetail' || this.cabanaRemateId(this.state.currentCabana) !== id)) this.openCabana(item); 15036 else if (!this.state.cabanas && !this.state.cabanasLoading) this.fetchCabanas().then(() => this.navigateFromHash(hash)); 15037 if (!this.state.cabanasLoading) this.fetchCabanaById(id, { force: true }).then((remate) => remate && this.openCabana(remate)).catch(() => {}); 15038 return; 15039 } 15040 const route = this.routeFromHash(); 15041 if (route && route !== this.state.route) this.go(route); 15042 } 15043 restoreRoute() { 15044 const allowed = ['home', 'pool', 'load', 'rosganLotLoad', 'feria', 'feriaConfig', 'profile', 'mailingAdmin', 'devActivity', 'notifications', 'remates', 'rosganRemateCreate', 'rosganSalesOrder', 'rosganResults', 'rosganStats', 'rosganCommercial', 'rosganPriceBoard', 'rosganTvLower', 'rosganTvConfig', 'cabanas', 'cabanaDetail', 'cabanaAuditor', 'cabanaPrebidPanel', 'cabanaStats', 'cabanaCommercial', 'cabanaPriceBoard', 'cabanaTvLower', 'cabanaTvConfig', 'remateConfig', 'cabanaLotDetail', 'cabanaLotCreate', 'cabanaSalesOrder', 'channel', 'mercado']; 15045 const route = this.routeFromHash() || this.routeFromPath() || sessionStorage.getItem('rosgan:lastRoute') || 'home'; 15046 return allowed.includes(route) ?route : 'home'; 15047 } 15048 rememberRoute(route) { 15049 const allowed = ['home', 'pool', 'load', 'rosganLotLoad', 'feria', 'feriaConfig', 'profile', 'mailingAdmin', 'devActivity', 'notifications', 'remates', 'rosganRemateCreate', 'rosganSalesOrder', 'rosganResults', 'rosganStats', 'rosganCommercial', 'rosganPriceBoard', 'rosganTvLower', 'rosganTvConfig', 'cabanas', 'cabanaDetail', 'cabanaAuditor', 'cabanaPrebidPanel', 'cabanaStats', 'cabanaCommercial', 'cabanaPriceBoard', 'cabanaTvLower', 'cabanaTvConfig', 'remateConfig', 'cabanaLotDetail', 'cabanaLotCreate', 'cabanaSalesOrder', 'channel', 'mercado']; 15050 if (!allowed.includes(route)) return; 15051 try { sessionStorage.setItem('rosgan:lastRoute', route); } catch {} 15052 } 15053 // ---- Comitentes y establecimientos ---- 15054 editComitenteAlias(id) { 15055 if (!this.canManageComitentes(this.state.apiUser)) return; 15056 const item = [...(this.state.rosganLotComitentes || []), ...(this.state.comitentes || [])].find(c =>
15056 String(c.id) === String(id)); 15057 window.RosganComitenteAlias.open({ id, scoped: !!item?.consignatariaId, apiFetch: this.apiFetch.bind(this), onSaved: async () => { 15058 await this.fetchComitentes(); 15059 if (this.rosganForm().consignatariaId) { 15060 await this.loadRosganLotRelations(); 15061 const selected = (this.state.rosganLotComitentes || []).find(c => String(c.id) === String(this.rosganForm().comitenteId)); 15062 if (selected) this.setState({ rosganComitenteSearch: selected.alias || selected.name || selected.fiscalName || '' }); 15063 } 15064 }}); 15065 } 15066 emptyComitenteForm() { return { alias: '', cuitCuil: '', nombreFiscal: '', provincia: '', localidad: '', source: '' }; } 15067 normComitente(item) { 15068 if (!item) return null; 15069 return { 15070 id: String(item.id || item.comitenteId || item._id || ''), 15071 consignatariaId: String(item.consignatariaId || ''), 15072 cuitCuil: String(item.cuitCuil || item.cuitNormalized || item.cuit || ''), 15073 rawAlias: item.alias || '', 15074 alias: item.alias || item.name || item.nombre || item.nombreFiscal || item.fiscalName || 'Comitente', 15075 nombreFiscal: item.nombreFiscal || item.fiscalName || item.razonSocial || '', 15076 provincia: item.provincia || item.province || '', localidad: item.localidad || item.locality || '', 15077 }; 15078 } 15079 profileComitentesScope() { 15080 const uid = this.state.user?.uid; 15081 if (!uid) return ''; 15082 const ids = [...new Set((this.state.userConsignatarias || []).map((item) => String(item.id || '')).filter(Boolean))].sort(); 15083 return this.isRosganProfile() ? (ids.length ? uid + ':' + ids.join('|') : '') : uid + ':mine'; 15084 } 15085 visibleProfileComitentes() { 15086 if (this.state.comitentesScope !== this.profileComitentesScope() || !this.profileComitentesScope()) return []; 15087 const assigned = new Set((this.state.userConsignatarias || []).map((item) => String(item.id))); 15088 return (this.state.comitentes || []).filter((item) => !this.isRosganProfile() || assigned.has(String(item.consignatariaId))); 15089 } 15090 profileComitenteDirectory() { 15091 const all = this.visibleProfileComitentes(); 15092 const normalize = value => String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim(); 15093 const query = normalize(this.state.profileComitenteQuery); 15094 const province = this.state.profileComitenteProvince || ''; 15095 const locality = this.state.profileComitenteLocality || ''; 15096 const options = values => [...new Set(values.filter(Boolean))].sort((a,b) => a.localeCompare(b,'es',{sensitivity:'base'})); 15097 const filtered = all.filter(item => (!province || item.provincia === province) && (!locality || item.localidad === locality) && 15098 (!query || normalize([item.alias, item.nombreFiscal, item.cuitCuil].join(' ')).includes(query) || 15099 (/^[\d\s-]+$/.test(query) && String(item.cuitCuil || '').replace(/\D/g,'').includes(query.replace(/\D/g,''))))) 15100 .sort((a,b) => (a.alias || a.nombreFiscal || '').localeCompare(b.alias || b.nombreFiscal || '', 'es', {sensitivity:'base'})); 15101 const pages = Math.max(1, Math.ceil(filtered.length / 25)); 15102 const page = Math.max(1, Math.min(pages, this.state.profileComitentePage || 1)); 15103 return { total: all.length, count: filtered.length, page, pages, rows: filtered.slice((page-1)*25,page*25), 15104 provinces: options(all.map(item => item.provincia)), 15105 localities: options(all.filter(item => !province || item.provincia === province).map(item => item.localidad)) }; 15106 } 15107 async fetchComitentes() { 15108 const scope = this.profileComitentesScope(); 15109 const request = this._profileComitentesRequest = (this._profileComitentesRequest || 0) + 1; 15110 this.setState({ comitentes: [], comitentesScope: '', comitentesLoading: !!scope, comitentesError: '' }); 15111 if (!scope) return; 15112 const current = () => request === this._profileComitentesRequest && scope === this.profileComitentesScope(); 15113 try { 15114 let items; 15115 if (this.isRosganProfile()) { 15116 const ids = [...new Set((this.state.userConsignatarias || []).map((item) => String(item.id || '')).filter(Boolean))]; 15117 const results = await Promise.all(ids.map(async (id) => { 15118 const data = await this.apiFetch('/api/Comitentes?consignatariaId='
15118+ encodeURIComponent(id)); 15119 return this.listFrom(data, ['items', 'data', 'comitentes']).filter((item) => String(item.consignatariaId || '') === id); 15120 })); 15121 items = results.flat(); 15122 } else { 15123 items = this.listFrom(await this.apiFetch('/api/Comitentes/mine'), ['items', 'data', 'comitentes']); 15124 } 15125 if (!current()) return; 15126 const unique = new Map(items.map((item) => this.normComitente(item)).filter((item) => item?.id).map((item) => [item.consignatariaId + ':' + item.id, item])); 15127 this.setState({ comitentes: [...unique.values()], comitentesScope: scope, comitentesLoading: false }); 15128 } catch (error) { 15129 if (!current()) return; 15130 this.setState({ comitentes: [], comitentesScope: '', comitentesLoading: false, comitentesError: error?.message || 'No se pudieron cargar tus comitentes.' }); 15131 } 15132 } 15133 setComitenteForm(key, value) { 15134 const next = { ...(this.state.comitenteForm || this.emptyComitenteForm()), [key]: value }; 15135 if (key === 'cuitCuil') { next.cuitCuil = String(value || '').replace(/\D/g, '').slice(0, 11); next.source = ''; } 15136 this.setState({ comitenteForm: next, comitenteFormMessage: null }); 15137 } 15138 onComitenteCuit(value) { 15139 const cuitCuil = String(value || '').replace(/\D/g, '').slice(0, 11); 15140 if (cuitCuil.length < 11) { 15141 this._comitenteLookupToken = (this._comitenteLookupToken || 0) + 1; 15142 this._comitenteLookupKey = ''; 15143 } 15144 this.setState({ 15145 comitenteForm: { ...(this.state.comitenteForm || this.emptyComitenteForm()), cuitCuil, nombreFiscal: '', provincia: '', localidad: '', source: '' }, 15146 comitenteLookupLoading: false, comitenteFormMessage: null, 15147 }, () => { if (cuitCuil.length === 11) this.lookupComitenteCuit(cuitCuil); }); 15148 } 15149 async lookupComitenteCuit(cuit) { 15150 const normalized = String(cuit || '').replace(/\D/g, ''); 15151 if (normalized.length !== 11) return; 15152 if (this._comitenteLookupKey === normalized && this.state.comitenteLookupLoading) return; 15153 this._comitenteLookupKey = normalized; 15154 const token = (this._comitenteLookupToken || 0) + 1; 15155 this._comitenteLookupToken = token; 15156 this.setState({ comitenteLookupLoading: true, comitenteFormMessage: { ok: true, text: 'Consultando datos fiscales en ARCAâ¦' } }); 15157 try { 15158 const response = await this.apiFetch(this.roslotCuitLookupEndpoint(normalized)); 15159 if (token !== this._comitenteLookupToken) return; 15160 const data = response?.data ?? response ?? {}; 15161 if (data?.error) throw new Error(String(data.error)); 15162 const record = data?.comitente || data?.item || data; 15163 const existingId = String(record?.id || record?.comitenteId || ''); 15164 const source = String(data?.source || record?.source || '').toLowerCase(); 15165 const localItem = (this.state.comitentes || []).find((item) => String(item.id) === existingId || String(item.cuitCuil || '').replace(/\D/g, '') === normalized); 15166 if ((source === 'local' || localItem) && (existingId || localItem?.id)) { 15167 const selectedId = String(existingId || localItem.id); 15168 this.setState({ 15169 comitenteLookupLoading: false, comitenteFormOpen: false, comitenteForm: this.emptyComitenteForm(), 15170 estForm: { ...(this.state.estForm || this.emptyEstForm()), comitenteId: selectedId }, 15171 comitenteFormMessage: null, estFormMessage: { ok: true, text: 'El comitente ya existÃa y quedó seleccionado.' }, 15172 }); 15173 return; 15174 } 15175 const found = data?.found === true || (data?.found !== false && Boolean(record?.nombreFiscal || record?.fiscalName || record?.razonSocial || record?.name || record?.nombre)); 15176 if (!found) throw new Error('ARCA no encontró este CUIT. Revisá los 11 dÃgitos.'); 15177 const nombreFiscal = String(record?.nombreFiscal || record?.fiscalName || record?.razonSocial || record?.name || record?.nombre || '').trim(); 15178 const provincia = String(record?.provincia || record?.province || '').trim(); 15179 const localidad = String(record?.localidad || record?.locality || '').trim(); 15180 this.setState({ 15181 comitenteLookupLoading: false, 15182 comitenteForm: { alias: this.state.comitenteForm?.alias || '', cuitCuil: normalized, nombreFiscal, provincia, localidad, source: source || 'arca' }, 15183 comitenteFormMessage: { ok: true, text: 'Datos fiscales obtenidos de ARCA.' }, 15184 }); 15185 } catch (error) { 15186 if (token !== this._comitenteLookupToken) return; 15187 this.setState({ comitenteLookupLoading: false, comitenteFormMessage: { ok: false, text: error?.message || 'No se pudo consultar ARCA.' } }); 15188 } 15189 } 15190 async saveComitente() { 15191 if (this.state.comitenteSaving) return; 15192 const form = this.state.comitenteForm || this.emptyComitenteForm(); 15193 if (!String(form.alias || '').trim()) { this.setState({comitenteFormMessage:{ok:false,text:'Ingresá el al
15193ias del comitente.'}}); return; } 15194 this.setState({ comitenteSaving: true, comitenteFormMessage: null }); 15195 try { 15196 const cuit = String(form.cuitCuil || '').replace(/\D/g, ''); 15197 const razonSocial = String(form.nombreFiscal || '').trim(); 15198 const provincia = String(form.provincia || '').trim(); 15199 const localidad = String(form.localidad || '').trim(); 15200 const created = await this.apiFetch(form.source && cuit ? '/api/Comitentes' : '/api/legacy-comitentes', { method: 'POST', body: JSON.stringify({ 15201 ...(!form.source || !cuit ? {action:'create_alias'} : {}), 15202 cuitCuil: cuit, 15203 alias: String(form.alias || '').trim(), 15204 nombreFiscal: razonSocial, 15205 provincia, 15206 localidad, 15207 }) }); 15208 const createdId = String(created?.comitente?.id || created?.id || created?.data?.comitente?.id || created?.data?.id || ''); 15209 this.setState({ comitentes: null, comitenteSaving: false, comitenteFormOpen: false, comitenteForm: this.emptyComitenteForm(), comitenteFormMessage: null, 15210 estForm: createdId ?{ ...(this.state.estForm || this.emptyEstForm()), comitenteId: createdId } : this.state.estForm }); 15211 await this.fetchComitentes(); 15212 if (!createdId) { 15213 const match = (this.state.comitentes || []).find((item) => String(item.cuitCuil || '').replace(/\D/g, '') === String(form.cuitCuil)); 15214 if (match?.id) this.setState({ estForm: { ...(this.state.estForm || this.emptyEstForm()), comitenteId: match.id } }); 15215 } 15216 this.showToast('Comitente creado correctamente.'); 15217 } catch (error) { 15218 const message = String(error?.message || 'No se pudo crear el comitente.').replace(/CUIT\s*\/\s*CUIL/gi, 'CUIT').replace(/\s+para esta consignataria\.?/gi, '.'); 15219 if (/ya existe un comitente con ese CUIT/i.test(message)) { 15220 await this.fetchComitentes(); 15221 const match = (this.state.comitentes || []).find((item) => String(item.cuitCuil || '').replace(/\D/g, '') === String(form.cuitCuil || '').replace(/\D/g, '')); 15222 if (match?.id) { 15223 this.setState({ 15224 comitenteSaving: false, comitenteFormOpen: false, comitenteForm: this.emptyComitenteForm(), comitenteFormMessage: null, 15225 estForm: { ...(this.state.estForm || this.emptyEstForm()), comitenteId: match.id }, 15226 }); 15227 this.showToast('El comitente ya existÃa y quedó seleccionado.'); 15228 return; 15229 } 15230 } 15231 this.setState({ comitenteSaving: false, comitenteFormMessage: { ok: false, text: message } }); 15232 } 15233 } 15234 emptyEstForm() { return { nombre: '', provincia: '', localidad: '', comitenteId: '', renspa: '', latitud: '', longitud: '' }; } 15235 normEst(e) { 15236 if (!e) return null; 15237 return { 15238 id: String(e.id || e.establishmentId || e.establecimientoId || e._id || ''), 15239 nombre: e.nombre || e.name || 'Establecimiento', 15240 provincia: e.provincia || e.province || '', 15241 localidad: e.localidad || e.locality || '', 15242 comitenteId: String(e.comitenteId || e.comitente?.id || ''), 15243 comitenteAlias: e.comitenteAlias || e.comitente?.alias || e.comitente?.name || '', 15244 comitenteCuitCuil: e.comitenteCuitCuil || e.comitente?.cuitCuil || e.comitente?.cuit || '', 15245 comitenteNombreFiscal: e.comitenteNombreFiscal || e.comitente?.nombreFiscal || e.comitente?.fiscalName || '', 15246 comitente: e.comitenteAlias || e.comitenteNombreFiscal || e.comitente?.alias || e.comitente?.nombreFiscal || e.comitente || e.principal || '', 15247 renspa: e.renspa || null, 15248 latitud: e.latitud || e.latitude || e.lat || null, 15249 longitud: e.longitud || e.longitude || e.lng || e.lon || null, 15250 }; 15251 } 15252 async fetchEstablishments() { 15253 if (this.state.establishmentsLoading) return; 15254 const request = this._profileEstablishmentsRequest = (this._profileEstablishmentsRequest || 0) + 1; 15255 this.setState({ establishmentsLoading: true, establishmentsError: '' }); 15256 try { 15257 const certifierView = this.isCertifierOnly(this.state.apiUser); 15258 const data = await this.apiFetch(certifierView ?'/api/rosgan-establishments' : '/api/Establishments/mine?limit=50'); 15259 if (request !== this._profileEstablishmentsRequest) return; 15260 const assigned = new Set((this.state.userConsignatarias || []).map((item) => String(item.id))); 15261 const arr = this.listFrom(data, ['items', 'data', 'establishments']).filter((item) => !certifierView || assigned.has(String(item.consignatariaId))); 15262 this.setProfileFetchInfo('establecimientos', arr.length, ''); 15263 this.setState({ establishments: arr.map((e) => this.normEst(e)).filter((e) => e && e.id), establishmentsLoading: false, establishmentsError: '' }); 15264 } catch (e) { if (request !== this._profileEstablishmentsRequest) return; this.setProfileFetchInfo('establecimientos', null, e?.message || 'Error'); this.setState({ establishments: [], establishmentsLoading: false, establishmentsError: e?.message || 'No se pudieron cargar tus establecimientos.' }); } 15265 } 15266 async createEstablishment(data) { 15267 const body = { 15268 nombre: data.nombre || '', provincia: data.provincia || '', localidad: data.localidad || '', 15269 comitenteId: data.comitenteId || '', renspa: String(data.renspa || '').trim() || null, 15270 latitud: this.numOrNull(data.latitud), longitud: this.numOrNull(data.longitud), 15271 }; 15272 const res = await this.apiFetch('/api/Establishments', { method: 'POST', body: JSON.stringify(body) }); 15273 const created = res?.establishment || res?.establecimiento || res?.data?.establishment || res?.data?.establecimiento || res?.data || res || {}; 15274 return this.normEst({ ...body, ...created }); 15275 } 15276 async saveEstablishment(forLot) { 15277 if (this.state.estSaving) return; 15278 if (!this.canManageComitentes(this.state.apiUser)) { 15279 this.setState({ estFormOpen: false, estFormMessage: { text: 'No tenés permisos para gestionar comitentes.', ok: false } }); return; 15280 } 15281 const ef = this.state.estForm || this.emptyEstForm(); 15282 if (!String(ef.comitenteId || '').trim() || !String(ef.nombre).trim() || !String(ef.provincia).trim() || !String(ef.localidad).trim()) { 15283 this.setState({ estFormMessage: { text: 'Seleccioná un comitente y completá n
15283ombre, provincia y localidad.', ok: false } }); return; 15284 } 15285 this.setState({ estSaving: true, estFormMessage: { text: 'Guardando establecimientoâ¦', ok: true } }); 15286 try { 15287 const created = await this.createEstablishment(ef); 15288 await this.fetchEstablishments(); 15289 const saved = (this.state.establishments || []).find((item) => item.id === created?.id) || created; 15290 const patch = { estSaving: false, estFormOpen: false, estForm: this.emptyEstForm(), estFormMessage: null }; 15291 if (forLot && saved && saved.id) { 15292 patch.form = { ...this.form(), estId: saved.id, estName: saved.nombre, comitente: saved.comitente || '', prov: saved.provincia, loc: saved.localidad, lat: saved.latitud != null ?String(saved.latitud) : '', lon: saved.longitud != null ?String(saved.longitud) : '' }; 15293 } 15294 this.setState(patch); 15295 this.showToast('Establecimiento "' + (created?.nombre || ef.nombre) + '" guardado.'); 15296 } catch { 15297 this.setState({ estSaving: false, estFormMessage: { text: 'No se pudo guardar. Verificá tu sesión.', ok: false } }); 15298 } 15299 } 15300 async deleteEstablishment(id) { 15301 if (!id || this.state.estDeleting) return; 15302 this.setState({ estDeleting: id }); 15303 try { 15304 await this.apiFetch('/api/Establishments/' + encodeURIComponent(id), { method: 'DELETE' }); 15305 this.setState({ establishments: (this.state.establishments || []).filter((e) => e.id !== id), estDeleting: '' }); 15306 this.showToast('Establecimiento eliminado.'); 15307 } catch { this.setState({ estDeleting: '' }); this.showToast('No se pudo eliminar.'); } 15308 } 15309 setEstForm(key, val) { 15310 const ef = { ...(this.state.estForm || this.emptyEstForm()), [key]: val }; 15311 if (key === 'provincia') { ef.localidad = ''; ef.latitud = ''; ef.longitud = ''; } 15312 this.setState({ estForm: ef, estFormMessage: null, ...(key === 'provincia' ?{ estPlaceResults: [], estPlaceError: '' } : {}) }); 15313 } 15314 onEstPlaceInput(value) { 15315 const localidad = String(value || ''); 15316 const form = { ...(this.state.estForm || this.emptyEstForm()), localidad, latitud: '', longitud: '' }; 15317 this.setState({ estForm: form, estFormMessage: null, estPlaceError: '', estPlaceResults: localidad.trim().length >= 3 ?this.state.estPlaceResults : [] }); 15318 clearTimeout(this._estPlaceTimer); 15319 if (!form.provincia || localidad.trim().length < 3) return; 15320 this._estPlaceTimer = setTimeout(() => this.searchEstPlaces(localidad, form.provincia), 280); 15321 } 15322 async searchEstPlaces(localidad, provincia) { 15323 const query = String(localidad || '').trim(); 15324 if (query.length < 3) return; 15325 const token = (this._estPlaceToken || 0) + 1; 15326 this._estPlaceToken = token; 15327 this.setState({ estPlaceLoading: true, estPlaceError: '' }); 15328 try { 15329 const response = await this.apiFetch('/api/places/search', { method: 'POST', body: JSON.stringify({ query, provincia: String(provincia || '').trim() }) }); 15330 if (token !== this._estPlaceToken) return; 15331 const results = Array.isArray(response?.data) ?response.data : this.listFrom(response, ['data', 'items', 'results']); 15332 this.setState({ estPlaceLoading: false, estPlaceResults: results.slice(0, 7), estPlaceError: results.length ?'' : 'No encontramos esa localidad. Podés escribirla manualmente.' }); 15333 } catch (error) { 15334 if (token !== this._estPlaceToken) return; 15335 this.setState({ estPlaceLoading: false, estPlaceResults: [], estPlaceError: 'Google no respondió. Podés escribir la localidad manualmente.' }); 15336 } 15337 } 15338 selectEstPlace(item) { 15339 if (!item) return; 15340 this.setState({ 15341 estForm: { ...(this.state.estForm || this.emptyEstForm()), localidad: String(item.label || item.address || ''), latitud: item.lat != null ?String(item.lat) : '', longitud: item.lng != null ?String(item.lng) : '' }, 15342 estPlaceResults: [], estPlaceError: '', estFormMessage: null, 15343 }); 15344 } 15345 closeEstPlaceResults() { 15346 setTimeout(() => this.setState({ estPlaceResults: [] }), 150); 15347 } 15348 establishmentConsignatariaId() { 15349 const memberships = [...(this.state.userConsignatarias || []), this.state.userConsignataria].filter(
15349Boolean); 15350 const refs = memberships.flatMap((item) => [item?.id, item?.slug, item?.ref, item?.name]).filter(Boolean); 15351 const catalogs = [...(this.state.rosganLotConsignatarias || []), ...(this.state.consignatarias || [])]; 15352 const match = catalogs.find((item) => refs.some((ref) => [item?.id, item?.slug, item?.name, item?.nombre] 15353 .some((value) => value && this.consKey(value) === this.consKey(ref)))); 15354 return String(match?.id || match?.consignatariaId || refs[0] || ''); 15355 } 15356 async resolveEstablishmentConsignatariaId() { 15357 let id = this.establishmentConsignatariaId(); 15358 if (this.state.rosganLotConsignatarias?.length) return id; 15359 try { 15360 const raw = await this.apiFetch('/api/rosgan-consignatarias'); 15361 const items = this.rosganArray(raw).filter((item) => item?.isActive !== false && item?.activo !== false); 15362 await new Promise((resolve) => this.setState({ rosganLotConsignatarias: items }, resolve)); 15363 id = this.establishmentConsignatariaId(); 15364 } catch {} 15365 return id; 15366 } 15367 onEstCuit(value) { 15368 const cuit = String(value || '').replace(/\D/g, '').slice(0, 11); 15369 if (cuit.length < 11) { 15370 this._estLookupToken = (this._estLookupToken || 0) + 1; 15371 this._estLookupKey = ''; 15372 } 15373 this.setState({ 15374 estForm: { ...(this.state.estForm || this.emptyEstForm()), cuit, source: '' }, 15375 estLookupLoading: false, 15376 estFormMessage: null, 15377 }, () => { if (cuit.length === 11) this.lookupEstComitente(cuit); }); 15378 } 15379 async lookupEstComitente(cuit) { 15380 if (!this.canManageComitentes(this.state.apiUser)) return; 15381 const normalized = String(cuit || '').replace(/\D/g, ''); 15382 if (normalized.length !== 11) return; 15383 const lookupKey = normalized; 15384 if (this._estLookupKey === lookupKey && this.state.estLookupLoading) return; 15385 this._estLookupKey = lookupKey; 15386 const token = (this._estLookupToken || 0) + 1; 15387 this._estLookupToken = token; 15388 this.setState({ estLookupLoading: true, estFormMessage: { text: 'Consultando CUIT en ARCAâ¦', ok: true } }); 15389 try { 15390 const response = await this.apiFetch(this.roslotCuitLookupEndpoint(normalized)); 15391 if (token !== this._estLookupToken) return; 15392 const data = response?.data ?? response ?? {}; 15393 if (data?.error) throw new Error(String(data.error)); 15394 const record = data?.comitente || data?.item || data; 15395 const found = data?.found === true || (data?.found !== false && Boolean( 15396 record?.cuit || record?.cuitNormalized || record?.fiscalName || record?.razonSocial || record?.name || record?.nombre 15397 )); 15398 if (!found) { 15399 this.setState({ estLookupLoading: false, estFormMessage: { text: 'ARCA no encontró este CUIT. Revisá los 11 dÃgitos.', ok: false } }); 15400 return; 15401 } 15402 const current = this.state.estForm || this.emptyEstForm(); 15403 const provinceRaw = String(record.province || record.provincia || '').trim(); 15404 const provinceOptions = this.state.geoProvinces?.length ?this.state.geoProvinces : ARGENTINA_PROVINCES; 15405 const province = provinceOptions.find((item) => this.consKey(item) === this.consKey(provinceRaw)) || provinceRaw; 15406 const locality = String(record.locality || record.localidad || '').trim(); 15407 const fiscalName = String(record.fiscalName || record.razonSocial || '').trim(); 15408 const visibleName = String(record.alias || record.name || record.nombre || fiscalName || current.comitente || '').trim(); 15409 const verifiedSource = String(data.source || record.source || 'arca'); 15410 if (province) this.fetchLocalities(province); 15411 this.setState({ 15412 estLookupLoading: false, 15413 estForm: { ...current, cuit: normalized, comitente: visibleName, fiscalName, provincia: province || current.provincia, localidad: locality || current.localidad, source: verifiedSource }, 15414 estFormMessage: { text: verifiedSource.toLowerCase() === 'local' ?'Comitente encontrado en los registros locales.' : 'Datos fiscales obtenidos de ARCA.', ok: true }, 15415 }); 15416 } catch (error) { 15417 if (token !== this._estLookupToken) return; 15418 const detail = String(error?.data?.message || error?.message || '').trim(); 15419 const permission = Number(error?.status) === 403; 15420 this.setState({ 15421 estLookupLoading: false, 15422 estForm: { ...(this.state.estForm || this.emptyEstForm()), source: '' }, 15423 estFormMessage: { text: permission ?'El backend no autorizó a este rol para consultar ARCA.' : (detail || 'ARCA no respondió. Reintentá la consulta.'), ok: false }, 15424 }); 15425 } 15426 } 15427 localIsoDate() { 15428 const now = new Date(); 15429 const pad = (value) => String(value).padStart(2, '0'); 15430 return now.getFullYear() + '-' + pad(now.getMonth() + 1) + '-' + pad(now.getDate()); 15431 } 15432 emptyBaForm() { return { buyerEmail: '', buyerUid: '', creditStartDate: this.localIsoDate(), weeklyCreditAmount: '' }; } 15433 formatBaDate(s) { const d = new Date(s + 'T00:00:00'); if (isNaN(d)) return s; return d.toLocaleDateString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric' }); } 15434 formatBaCurrency(value) { const amount = Number(value); return Number.isFinite(amount) ?'$' + Math.round(amount).toLocaleStr
15434ing('es-AR') : '$0'; } 15435 calculateCreditEndDate(startDate) { 15436 const date = new Date(String(startDate || '') + 'T00:00:00'); 15437 if (isNaN(date)) return ''; 15438 date.setDate(date.getDate() + 6); 15439 const pad = (value) => String(value).padStart(2, '0'); 15440 return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()); 15441 } 15442 normBuyerAuth(a) { 15443 if (!a) return null; 15444 const source = a.authorization || a.buyerAuthorization || a; 15445 const seller = source.seller || a.seller || source.owner || a.owner || source.authorizer || a.authorizer || source.grantedBy || a.grantedBy || source.createdBy || a.createdBy || source.consignataria || a.consignataria || {}; 15446 const sellerFirstName = String(source.sellerFirstName || a.sellerFirstName || seller.firstName || seller.nombre || '').trim(); 15447 const sellerLastName = String(source.sellerLastName || a.sellerLastName || seller.lastName || seller.apellido || '').trim(); 15448 const sellerFullName = [sellerFirstName, sellerLastName].filter(Boolean).join(' '); 15449 const finite = (...values) => { 15450 const value = values.find((candidate) => candidate !== null && candidate !== undefined && candidate !== '' && Number.isFinite(Number(candidate))); 15451 return value === undefined ?null : Number(value); 15452 }; 15453 return { 15454 id: source.id || source.authorizationId || source._id || source.docId || source.buyerUid || source.uid || '', 15455 summaryId: source.authorizationId || source.id || source._id || source.docId || '', 15456 buyerUid: source.buyerUid || source.buyerId || source.userUid || source.uid || '', 15457 buyerEmail: source.buyerEmail || source.email || source.buyerName || '', 15458 buyerName: String(source.buyerName || source.buyerDisplayName || source.displayName || source.name || source.nombre || ''), 15459 creditStartDate: String(source.creditStartDate || '').slice(0, 10), 15460 creditEndDate: String(source.creditEndDate || '').slice(0, 10), 15461 weeklyCreditAmount: finite(source.weeklyCreditAmount) || 0, 15462 sellerUid: String(source.sellerUid || a.sellerUid || source.sellerId || a.sellerId || source.consignatariaUid || a.consignatariaUid || source.ownerUid || a.ownerUid || source.authorizerUid || a.authorizerUid || source.grantedByUid || a.grantedByUid || source.createdByUid || a.createdByUid || seller.uid || seller.id || ''), 15463 sellerEmail: String(source.sellerEmail || a.sellerEmail || source.consignatariaEmail || a.consignatariaEmail || source.ownerEmail || a.ownerEmail || source.authorizerEmail || a.authorizerEmail || source.grantedByEmail || a.grantedByEmail || source.createdByEmail || a.createdByEmail || seller.email || ''), 15464 sellerName: String(source.sellerName || a.sellerName || source.sellerDisplayName || a.sellerDisplayName || source.sellerFullName || a.sellerFullName || source.consignatariaName || a.consignatariaName || source.ownerName || a.ownerName || source.authorizerName || a.authorizerName || source.grantedByName || a.grantedByName || source.createdByName || a.createdByName || seller.displayName || seller.fullName || seller.name || sellerFullName || seller.nombreCompleto || (typeof source.consignataria === 'string' ?source.consignataria : '') || ''), 15465 isActive: source.isActive !== false && source.active !== false && source.activo !== false, 15466 }; 15467 } 15468 normalizeCreditMovement(movement, index) { 15469 const source = movement?.movement || movement || {}; 15470 const finite = (...values) => { 15471 const value = values.find((candidate) => candidate !== null && candidate !== undefined && candidate !== '' && Number.isFinite(Number(candidate))); 15472 return value === undefined ?0 : Number(value); 15473 }; 15474 const rawStatus = String(source.status || source.state || source.movementStatus || source.result || '').trim().toLowerCase(); 15475 const status = rawStatus === 'won' ?'won' : rawStatus === 'winning' ?'winning' : rawStatus; 15476 return { 15477 id: source.id || source.movementId || source.bidId || ('credit-movement-' + index), 15478 date: source.date || source.createdAt || source.occurredAt || source.updatedAt || source.timestamp || '', 15479 lot: source.lotName || source.lotLabel || source.lotTitle || source.category || source.categoria || (source.lotNumber != null ?'Lote ' + source.lotNumber : '') || source.lotCert || source.cert || source.lotId || 'Lote', 15480 status, 15481 bidAmount: finite(source.bidAmount, source.amount, source.offeredAmount, source.offerAmount, source.currentAmount), 15482 consumedAmount: finite(source.consumedCreditAmount, source.creditConsumed, source.creditAmount, source.usedCreditAmount, source.usedAmount), 15483 }; 15484 } 15485 normalizeCreditSummary(data, authorization) { 15486 const envelope = data?.creditSummary || data?.summary || data?.result || data?.data || data || {}; 15487 const source = Array.isArray(envelope) ?{} : envelope; 15488 const finiteOrNull = (...values) => { 15489 const value = values.find((candidate) => candidate !== null && candidate !== undefined && candidate !== '' && Number.isFinite(Number(candidate))); 15490 return value === undefined ?null : Number(value); 15491 }; 15492 const movementSource = [source.movements, source.items, source.history, source.creditMovements, data?.movements, data?.items].find(Array.isArray) || []; 15493 const movements = movementSource.map((movement, index) => this.normalizeCreditMovement(movement, index)); 15494 const weeklyCreditAmount = finiteOrNull(source.weeklyCreditAmount, source.authorizedCreditAmount, source.creditAmount, authorization?.weeklyCreditAmount) || 0; 15495 const usedFromMovements = movements.reduce((sum, movement) => sum + (Number(movement.consumedAmount) || 0), 0); 15496 const usedCreditAmount = finiteOrNull(source.usedCreditAmount, source.usedAmount, source.creditUsed, source.consumedCreditAmount); 15497 const normalizedUsed = usedCreditAmount === null ?usedFromMovements : usedCreditAmount; 15498 const availableCreditAmount = finiteOrNull(source.availableCreditAmount, source.availableAmou
15498nt, source.creditAvailable, source.remainingCreditAmount); 15499 return { 15500 weeklyCreditAmount, 15501 usedCreditAmount: Math.max(0, normalizedUsed || 0), 15502 availableCreditAmount: Math.max(0, availableCreditAmount === null ?weeklyCreditAmount - (normalizedUsed || 0) : availableCreditAmount), 15503 movements, 15504 }; 15505 } 15506 creditAuthorizationKey(authorization) { 15507 const normalized = this.normBuyerAuth(authorization) || authorization || {}; 15508 return String(normalized.summaryId || normalized.authorizationId || normalized.id || '').trim(); 15509 } 15510 creditAuthorizationIsCurrent(authorization) { 15511 const normalized = this.normBuyerAuth(authorization) || authorization || {}; 15512 if (normalized.isActive === false) return false; 15513 const today = this.localIsoDate(); 15514 const start = String(normalized.creditStartDate || '').slice(0, 10); 15515 const end = String(normalized.creditEndDate || this.calculateCreditEndDate(start) || '').slice(0, 10); 15516 return (!start || start <= today) && (!end || end >= today); 15517 } 15518 async loadCreditSummaryIntoCache(authorization, { force = false } = {}) { 15519 const normalized = this.normBuyerAuth(authorization) || authorization || {}; 15520 const authorizationId = this.creditAuthorizationKey(normalized); 15521 if (!authorizationId) return null; 15522 const cached = this.state.creditSummaryCache?.[authorizationId]; 15523 if (cached && !force) return cached; 15524 this._creditSummaryPromises = this._creditSummaryPromises || {}; 15525 if (this._creditSummaryPromises[authorizationId]) return this._creditSummaryPromises[authorizationId]; 15526 const request = (async () => { 15527 this.setState((state) => ({ 15528 creditSummaryLoadingIds: { ...(state.creditSummaryLoadingIds || {}), [authorizationId]: true }, 15529 creditSummaryErrors: { ...(state.creditSummaryErrors || {}), [authorizationId]: '' }, 15530 })); 15531 try { 15532 const data = await this.apiFetch('/api/BuyerAuthorizations/' + encodeURIComponent(authorizationId) + '/credit-summary'); 15533 const summary = this.normalizeCreditSummary(data, normalized); 15534 this.setState((state) => ({ 15535 creditSummaryCache: { ...(state.creditSummaryCache || {}), [authorizationId]: summary }, 15536 creditSummaryLoadingIds: { ...(state.creditSummaryLoadingIds || {}), [authorizationId]: false }, 15537 creditSummaryErrors: { ...(state.creditSummaryErrors || {}), [authorizationId]: '' }, 15538 })); 15539 return summary; 15540 } catch (error) { 15541 const message = error?.message || 'No se pudo cargar el historial del crédito.'; 15542 this.setState((state) => ({ 15543 creditSummaryLoadingIds: { ...(state.creditSummaryLoadingIds || {}), [authorizationId]: false }, 15544 creditSummaryErrors: { ...(state.creditSummaryErrors || {}), [authorizationId]: message }, 15545 })); 15546 throw error; 15547 } finally { 15548 delete this._creditSummaryPromises[authorizationId]; 15549 } 15550 })(); 15551 this._creditSummaryPromises[authorizationId] = request; 15552 return request; 15553 } 15554 prefetchCurrentCreditSummaries(authorizations) { 15555 (authorizations || []) 15556 .filter((authorization) => this.creditAuthorizationIsCurrent(authorization)) 15557 .forEach((authorization) => this.loadCreditSummaryIntoCache(authorization).catch(() => {})); 15558 } 15559 async refreshCreditRealtime({ refreshAuthorizations = false } = {}) { 15560 if (this._creditRealtimeSyncing || document.hidden || this.state.route !== 'profile' || !this.state.user) return; 15561 const canManage = this.canManageBuyerAccounts(this.state.apiUser); 15562 if (!canManage) return; 15563 this._creditRealtimeSyncing = true; 15564 try { 15565 let buyerAuths = this.state.buyerAuths || []; 15566 if (refreshAuthorizations || !this._lastCreditAuthorizationRefreshAt || Date.now() - this._lastCreditAuthorizationRefreshAt > 12000) { 15567 buyerAuths = await this.fetchBuyerAuths(); 15568 buyerAuths = Array.isArray(buyerAuths) ?buyerAuths : (this.state.buyerAuths || []); 15569 this._lastCreditAuthorizationRefreshAt = Date.now(); 15570 } 15571 const current = buyerAuths 15572 .filter((authorization) => this.creditAuthorizationIsCurrent(authorization)); 15573 const unique = [...new Map(current.map((authorization) => [this.creditAuthorizationKey(authorization), authorization])).values()] 15574 .filter((authorization) => this.creditAuthorizationKey(authorization)); 15575 await Promise.allSettled(unique.map((authorization) => this.loadCreditSummaryIntoCache(authorization, { force: true }))); 15576 } finally { 15577 this._creditRealtimeSyncing = false;
15578 } 15579 } 15580 openCreditHistory(authorization = null, perspective = 'buyer') { 15581 if (perspective !== 'seller' || !this.canManageBuyerAccounts(this.state.apiUser)) return; 15582 this.setState({ 15583 creditHistoryOpen: true, 15584 creditHistoryPerspective: 'seller', 15585 creditHistoryAuthorization: authorization ?(this.normBuyerAuth(authorization) || authorization) : null, 15586 }); 15587 } 15588 closeCreditHistory() { 15589 this.setState({ creditHistoryOpen: false, creditHistoryAuthorization: null }); 15590 } 15591 async openCreditSummary(authorization, perspective) { 15592 if (perspective !== 'seller' || !this.canManageBuyerAccounts(this.state.apiUser)) return; 15593 const normalized = this.normBuyerAuth(authorization) || authorization || {}; 15594 const authorizationId = normalized.summaryId || normalized.authorizationId || normalized.id; 15595 if (!authorizationId) { 15596 this.showToast('La autorización no incluye un identificador para consultar el historial.'); 15597 return; 15598 } 15599 const cached = this.state.creditSummaryCache?.[authorizationId] || null; 15600 this.setState({ 15601 creditHistoryOpen: false, 15602 creditSummaryOpen: true, 15603 creditSummaryLoading: !cached, 15604 creditSummaryError: '', 15605 creditSummary: cached, 15606 creditSummaryAuthorization: normalized, 15607 creditSummaryPerspective: 'seller', 15608 }); 15609 if (cached) return; 15610 try { 15611 const summary = await this.loadCreditSummaryIntoCache(normalized); 15612 this.setState({ creditSummary: summary, creditSummaryLoading: false, creditSummaryError: '' }); 15613 } catch (error) { 15614 this.setState({ creditSummary: null, creditSummaryLoading: false, creditSummaryError: error?.message || 'No se pudo cargar el historial del crédito.' }); 15615 } 15616 } 15617 closeCreditSummary() { 15618 this.setState({ creditSummaryOpen: false, creditSummaryLoading: false, creditSummaryError: '', creditSummary: null, creditSummaryAuthorization: null }); 15619 } 15620 refreshCreditSummary() { 15621 const authorization = this.state.creditSummaryAuthorization; 15622 if (!authorization || this.state.creditSummaryLoading) return; 15623 this.setState({ creditSummaryLoading: true, creditSummaryError: '' }); 15624 this.loadCreditSummaryIntoCache(authorization, { force: true }) 15625 .then((summary) => this.setState({ creditSummary: summary, creditSummaryLoading: false, creditSummaryError: '' })) 15626 .catch((error) => this.setState({ creditSummaryLoading: false, creditSummaryError: error?.message || 'No se pudo actualizar el historial del crédito.' })); 15627 } 15628 normBuyerAccount(user) { 15629 if (!user) return null; 15630 const roles = this.userRoleIds(user); 15631 if (!roles.includes('buyer') || roles.includes('consignatario')) return null; 15632 const uid = String(user.uid || user.userId || user.id || ''); 15633 const email = String(user.email || ''); 15634 if (!uid && !email) return null; 15635 return { 15636 uid, 15637 email, 15638 displayName: user.displayName || user.name || email.split('@')[0] || 'Comprador', 15639 photoUrl: user.photoUrl || user.photoURL || '', 15640 emailVerified: user.emailVerified === true, 15641 providers: Array.isArray(user.providers) ?user.providers : [], 15642 createdAt: user.createdAt || '', 15643 lastLoginAt: user.lastLoginAt || '', 15644 }; 15645 } 15646 async fetchBuyerAccounts() { 15647 if (this.state.buyerAccountsLoading) return; 15648 this.setState({ buyerAccountsLoading: true, buyerAccountsError: '' }); 15649 try { 15650 const raw = await this.fetchAllPages('/api/Users/buyers', ['data', 'items', 'users', 'buyers']); 15651 const accounts = raw.map((user) => this.normBuyerAccount(user)).filter(Boolean); 15652 this.setProfileFetchInfo('cuentas comprador', accounts.length, ''); 15653 this.setState({ buyerAccounts: accounts, buyerAccountsLoading: false, buyerAccountsError: '' }); 15654 } catch (e) { 15655 if (e?.status === 403) { 15656 this.setProfileFetchInfo('cuentas comprador', 0, ''); 15657 this.setState({ buyerAccounts: [], buyerAccountsLoading: false, buyerAccountsError: '' }); 15658 return; 15659 } 15660 this.setProfileFetchInfo('cuentas comprador', null, e?.message || 'Error'); 15661 this.setState({ buyerAccounts: [], buyerAccountsLoading: false, buyerAccountsError: e?.message || 'No se pudieron cargar las cuentas comprador.' }); 15662 } 15663 } 15664 authorizeBuyerAccount(account) { 15665 this.setState({ 15666 baFormOpen: true, 15667 baForm: { ...this.emptyBaForm(), buyerUid: account.uid || '', buyerEmail: account.email || '', creditStartDate: this.localIsoDate() }, 15668 baFormMessage: null, 15669 }, () => requestAnimationFrame(() => document.getElementById('buyer-auth-form')?.scrollIntoView({ behavior: 'smooth', block: 'center' }))); 15670 } 15671 async fetchBuyerAuths() { 15672 if (this._buyerAuthsFetching) return this.state.buyerAuths || []; 15673 this._buyerAuthsFetching = true; 15674 const isInitialLoad = this.state.buyerAuths === null; 15675 if (isInitialLoad) this.setState({ buyerAuthsLoading: true, buyerAuthsError: '' }); 15676 try { 15677 const data = await this.apiFetch('/api/BuyerAuthorizations/mine?limit=50'); 15678 const arr = this.listFrom(data, ['items', 'data', 'authorizations']); 15679 const list = arr.map((a) => this.normBuyerAuth(a)).filter((a) => a && (a.id || a.buyerEmail)); 15680 this.setProfileFetchInfo('autorizaciones', list.length, ''); 15681 this.setState({ buyerAuths: list, buyerAuthsLoading: false, buyerAuthsError: '' }); 15682 this.prefetchCurrentCreditSummaries(list); 15683 return list; 15684 } catch (e) { 15685 this.setProfileFetchInfo('autorizaciones', null, e?.message || 'Error'); 15686 this.setState({ 15687 buyerAuths: isInitialLoad ?[] : this.state.buyerAuths, 15688 buyerAuthsLoading: false, 15689 buyerAuthsError: e?.message || 'No se pudieron cargar autorizaciones.', 15690 }); 15691 return this.state.buyerAuths || []; 15692 } finally { 15693 this._buyerAuthsFetching = false;
15694 } 15695 } 15696 async fetchBuyerAccess() { 15697 // El crédito es una herramienta privada del vendedor. El comprador no 15698 // consulta autorizaciones ni resúmenes para evitar exponer montos en red, 15699 // estado de React o DOM. 15700 this.setState({ buyerAccess: [], buyerAccessLoading: false, buyerAccessError: '' }); 15701 return []; 15702 } 15703 async hydrateBuyerSellerLabels(authorizations) { 15704 const current = this.state.sellerLabels || {}; 15705 const sellerUids = [...new Set((authorizations || []) 15706 .map((authorization) => String(authorization?.sellerUid || '').trim()) 15707 .filter((uid) => uid && !current[uid]))]; 15708 if (!sellerUids.length) return; 15709 const resolved = await Promise.all(sellerUids.map(async (uid) => { 15710 try { 15711 const data = await this.apiFetch('/api/user-labels/' + encodeURIComponent(uid)); 15712 const source = data?.data || data; 15713 const displayName = String(source?.displayName || source?.name || '').trim(); 15714 return displayName ?[uid, displayName] : null; 15715 } catch { 15716 return null; 15717 } 15718 })); 15719 const labels = Object.fromEntries(resolved.filter(Boolean)); 15720 if (Object.keys(labels).length) { 15721 this.setState((state) => ({ sellerLabels: { ...(state.sellerLabels || {}), ...labels } })); 15722 } 15723 } 15724 setBaForm(key, val) { this.setState({ baForm: { ...(this.state.baForm || this.emptyBaForm()), [key]: val }, baFormMessage: null }); } 15725 async saveBuyerAuth() { 15726 if (this.state.baSaving) return; 15727 const f = this.state.baForm || this.emptyBaForm(); 15728 if (!String(f.buyerEmail).trim()) { this.setState({ baFormMessage: { text: 'Seleccioná un comprador del directorio.', ok: false } }); return; } 15729 if (!String(f.buyerUid || '').trim() || !(this.state.buyerAccounts || []).some((account) => account.uid === f.buyerUid && account.email === f.buyerEmail)) { this.setState({ baFormMessage: { text: 'Seleccioná una cuenta con rol comprador del directorio actualizado.', ok: false } }); return; } 15730 if (!String(f.creditStartDate || '').trim()) { this.setState({ baFormMessage: { text: 'Indicá el inicio del crédito.', ok: false } }); return; } 15731 const weeklyCreditAmount = this.numOrNull(f.weeklyCreditAmount); 15732 if (!(weeklyCreditAmount > 0)) { this.setState({ baFormMessage: { text: 'El crédito semanal debe ser mayor a $0.', ok: false } }); return; } 15733 const body = { 15734 buyerUid: String(f.buyerUid || '').trim(), 15735 buyerEmail: String(f.buyerEmail).trim(), 15736 creditStartDate: String(f.creditStartDate).trim(), 15737 weeklyCreditAmount, 15738 }; 15739 this.setState({ baSaving: true, baFormMessage: { text: 'Creando crédito semanalâ¦', ok: true } }); 15740 try { 15741 await this.apiFetch('/api/BuyerAuthorizations', { method: 'POST', body: JSON.stringify(body) }); 15742 await this.fetchBuyerAuths(); 15743 this.setState({ baSaving: false, baFormOpen: false, baForm: this.emptyBaForm(), baFormMessage: null }); 15744 this.showToast('Crédito semanal creado para "' + body.buyerEmail + '".'); 15745 } catch (error) { 15746 console.error('[Roslot] No se pudo crear el crédito semanal', { status: error?.status || null, message: error?.message || '' }); 15747 const detail = error?.message || 'No se pudo crear el crédito.'; 15748 const status = Number(error?.status); 15749 this.setState({ baSaving: false, baFormMessage: { text: status && !detail.includes('HTTP ' + status) ?'HTTP ' + status + ' · ' + detail : detail, ok: false } }); 15750 } 15751 } 15752 async pushBuyerAuth(id) { 15753 const cur = (this.state.buyerAuths || []).find((a) => a.id === id); if (!cur) return; 15754 if (!cur.creditStartDate || !(Number(cur.weeklyCreditAmount) > 0)) { this.showToast('Completá una fecha de inicio y un monto mayor a $0.'); return; } 15755 this.setState({ baActioning: id }); 15756 try { 15757 await this.apiFetch('/api/BuyerAuthorizations/' + encodeURIComponent(id), { method: 'PUT', body: JSON.stringify({ creditStartDate: cur.creditStartDate, weeklyCreditAmount: Number(cur.weeklyCreditAmount), isActive: cur.isActive }) }); 15758 await this.fetchBuyerAuths(); 15759 this.showToast('Crédito actualizado.'); 15760 } catch { this.showToast('No se pudo actualizar el crédito.'); } 15761 this.setState({ baActioning: '' }); 15762 } 15763 updateBuyerAuthField(id, key, value) { 15764 const list = this.state.buyerAuths || []; 15765 const cur = list.find((a) => a.id === id); if (!cur) return; 15766 const next = { ...cur, [key]: key === 'weeklyCreditAmount' ?String(value).replace(/\D/g, '').slice(0, 15) : value }; 15767 if (key === 'creditStartDate') next.creditEndDate = ''; 15768 this.setState({ buyerAuths: list.map((a) => a.id === id ?next : a) }); 15769 } 15770 toggleBaActive(id) { 15771 const list = this.state.buyerAuths || []; 15772 const cur = list.find((a) => a.id === id); if (!cur) return; 15773 const next = { ...cur, isActive: !cur.isActive }; 15774 this.setState({ buyerAuths: list.map((a) => a.id === id ?next : a) }); 15775 this._baDebounce = this._baDebounce || {}; 15776 clearTimeout(this._baDebounce[id]); 15777 this._baDebounce[id] = setTimeout(() => this.pushBuyerAuth(id), 250); 15778 } 15779 async deleteBuyerAuth(id) { 15780 if (!id || this.state.baActioning) return; 15781 this.setState({ baActioning: id }); 15782 try { 15783 await this.apiFetch('/api/BuyerAuthorizations/' + encodeURIComponent(id), { method: 'DELETE' }); 15784 this.setState({ buyerAuths: (this.state.buyerAuths || []).filter((a) => a.id !== id), baActioning: '' }); 15785 this.showToast('Autorización eliminada.'); 15786 } catch { this.setState({ baActioning: '' }); this.showToast('No se pudo eliminar.'); } 15787 } 15788 setEst(id) { 15789 const f = this.form(); 15790 if (!id) { this.setState({ form: { ...f, estId: '' }, formMessage: null }); return; } 15791 const e = (this.state.establishments || []).find((x) => x.id === id); 15792 if (!e) { this.setState({ form: { ...f, estId: '' }, formMessage: null }); return; } 15793 this.setState({ form: { ...f, estId: id, estName: e.nombre, comitente: e.comitente, prov: e.provincia, loc: e.localidad, lat: e.latitud != null ?String(e.latitud) : '', lon: e.longitud != null ?String(e.longitud) : '' }, formMessage: null }); 15794 } 15795 mapLot(l, i) { 15796 const cert = String(l.cert || l.lotCert || l.certificate || l.certificado || l.id || String(l.lotNumber || i + 1).padStart(4, '0')); 15797 const razas = (l.razas || []).map((r) =>
15797 r.name || r).filter(Boolean); 15798 const rawVideos = l.videos || l.videoAssets || l.videoUploads || l.videoUrls || []; 15799 const videos = (Array.isArray(rawVideos) ?rawVideos : []).map((v) => ({ 15800 url: typeof v === 'string' ?v : (v.url || v.publicUrl || v.downloadUrl || v.videoUrl || v.src || ''), 15801 thumb: typeof v === 'object' ?(v.thumbnailUrl || v.thumbnail || v.posterUrl || v.poster || v.coverUrl || v.frameUrl || v.previewUrl || '') : '', 15802 })).filter((v) => v.url); 15803 const singleVideoUrl = l.videoUrl || l.video || ''; 15804 if (!videos.length && singleVideoUrl) videos.push({ url: singleVideoUrl, thumb: '' }); 15805 const dateRaw = l.fechaSubasta || ''; 15806 const dateParts = String(dateRaw).slice(0, 10).split('-').map(Number); 15807 const d = dateParts.length === 3 && dateParts.every(Number.isFinite) 15808 ? new Date(dateParts[0], dateParts[1] - 1, dateParts[2]) 15809 : (dateRaw ?new Date(dateRaw) : null); 15810 const today = new Date(); 15811 const todayKey = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime(); 15812 const dateKey = d && !isNaN(d) ?new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime() : null; 15813 const isToday = dateKey !== null && dateKey === todayKey; 15814 const isFuture = dateKey !== null && dateKey > todayKey; 15815 const rawAuctionStatus = String(l.auctionStatus || l.auction_state || l.auctionState || '').trim().toLowerCase(); 15816 const rawStatus = String(l.status || l.estado || l.state || '').trim().toLowerCase(); 15817 const rawBids = l.bids || l.ofertas || []; 15818 const bidCount = Number(l.bidCount || l.bidsCount || l.totalBids || l.offerCount || l.totalOffers || l.ofertasCount || rawBids.length) || 0; 15819 const locallyClosed = this.state.closedRoslotLots?.[cert] || null; 15820 const statusSignals = [rawAuctionStatus, rawStatus].join(' '); 15821 const explicitSale = !!(l.soldAt || l.saleClosedAt || l.adjudicatedAt || l.winnerBidId || l.winningBidId || l.winnerBuyerUid || l.winningBuyerUid || Number(l.winnerAmount || l.winningBidAmount || l.soldAmount) > 0); 15822 const hasBidEvidence = !!(locallyClosed || l.currentBidId || l.highestBidId || l.currentBidBuyerUid || l.highestBidBuyerUid || l.winnerBidId || l.winningBidId || l.winnerBuyerUid || l.winningBuyerUid || Number(l.currentBidAmount || l.highestBidAmount || l.winningBidAmount || l.soldAmount) > 0 || bidCount > 0); 15823 const closedSignal = l.isClosed === true || l.closed === true || !!l.closedAt || /closed|cerrad|finaliz|finished|ended/.test(statusSignals); 15824 const auctionStatus = locallyClosed || explicitSale || /sold|vendid|adjudic/.test(statusSignals) 15825 ? 'sold' 15826 : /closed[_ -]?no[_ -]?bids|sin[_ -]?ofertas|caduc|expired|vencid/.test(statusSignals) 15827 ? 'closed_no_bids' 15828 : closedSignal 15829 ? (hasBidEvidence ?'sold' : 'closed_no_bids') 15830 : /open|abiert|en vivo|live/.test(statusSignals) 15831 ? 'open' 15832 : /scheduled|program|upcoming|pr[oó]xim/.test(statusSignals) 15833 ? 'scheduled' 15834 : rawAuctionStatus; 15835 const auctionStatusMap = { 15836 scheduled: 'Próximo', 15837 open: 'En vivo', 15838 closed_no_bids: 'Caducado', 15839 sold: 'Vendido', 15840 }; 15841 const legacyStatus = /expired|vencid|caduc|closed_no_bids/.test(rawStatus) 15842 ? 'Caducado' 15843 : /sold|vendid/.test(rawStatus) 15844 ? 'Vendido' 15845 : isToday 15846 ? 'En vivo' 15847 : isFuture 15848 ? 'Próximo' 15849 : (dateKey !== null && dateKey < todayKey) 15850 ? 'Caducado' 15851 : 'Publicado'; 15852 const displayStatus = auctionStatusMap[auctionStatus] || legacyStatus; 15853 const isClosed = ['closed_no_bids', 'sold'].includes(auctionStatus) || closedSignal; 15854 const isExpired = displayStatus === 'Caducado'; 15855 const hasOffers = auctionStatus === 'sold' || l.hasOffers === true || bidCount > 0; 15856 return { 15857 raw: l, 15858 cert, 15859 status: prebid.targets?.length && prebid.isWinner ?'Con animales ganadores':displayStatus, 15860 categoria: l.categoriaNombre || l.categoriaId || 'Sin categorÃa', 15861 categoriaSexType: String(l.categoriaSexType || ''), 15862 subcategoria: l.subcategoriaNombre || l.subcategoriaId || '', 15863 cabezas: l.totalCabezas || l.cantidadTotal || '?', 15864 pesoNum: Number(l.pesoPromedio) || 0, 15865 peso: l.pesoPromedio ?l.pesoPromedio + ' kg' : 'Pendiente', 15866 baseNum: Number(l.precioSugerido) || 0, 15867 condicion: l.condicionNombre || l.condicionId || 'Condición pendiente', 15868 destino: l.destinoNombre || l.destinoId || 'Pendiente', 15869 engorde: l.engordeNombre || l.engordeId || 'Pendiente', 15870 plazoId: String(l.plazoId || ''), 15871 plazoOtro: String(l.plazoOtro || '').trim(), 15872 plazoEsOtro: [l.plazoId, l.plazoNombre].some((value) => String(value || '').trim().toLowerCase() === 'otro'), 15873 plazo: [l.plazoId, l.plazoNombre].some((value) => String(value || '').trim().toLowerCase() === 'otro') ?'Otro' : (l.plazoNombre || l.plazoId || 'Pendiente'), 15874 aptoChina: l.aptoChina === true || l.aptoChina === 'true', 15875 razas, provincia: l.establecimientoProvincia || l.provincia || '', 15876 origen: [l.establecimientoLocalidad || l.localidad, l.establecimientoProvincia || l.provincia].filter(
15876Boolean).join(', ') || 'Origen pendiente', 15877 image: l.coverImage?.url || l.thumbnailUrl || videos[0]?.thumb || '', 15878 videos, videoPreviewUrl: videos[0]?.url || '', 15879 views: Number(l.views) || 0, 15880 seller: l.sellerName || l.sellerEmail || 'Productor Rosgan', 15881 sellerUid: String(l.sellerUid || l.sellerId || l.createdByUid || l.userId || ''), 15882 sellerEmail: String(l.sellerEmail || l.ownerEmail || l.createdByEmail || l.userEmail || ''), 15883 consignatariaId: String(l.consignatariaId || l.sellerConsignatariaId || l.companyId || ''), 15884 consignatariaName: String(l.consignatariaNombre || l.consignatariaName || l.consignataria || l.sellerCompany || ''), 15885 fecha: dateRaw, fechaDisponibleCarga: String(l.fechaDisponibleCarga || '').slice(0, 10), createdAt: l.createdAt || '', 15886 republishCount: Number(l.republishCount) || 0, 15887 auctionStatus, rawStatus, isClosed, hasOffers, bidCount, 15888 currentBidAmount: Number(l.currentBidAmount || l.highestBidAmount || l.topBidAmount || 0) || 0, 15889 currentBidBuyerUid: String(l.currentBidBuyerUid || l.highestBidBuyerUid || l.leadingBuyerUid || ''), 15890 currentBidId: String(l.currentBidId || l.highestBidId || ''), 15891 offerStartAt: l.offerStartAtUtc || l.offerStartAt || null, 15892 offerEndAt: l.offerEndAtUtc || l.offerEndAt || null, 15893 soldAmount: Number(locallyClosed?.amount || l.winningBidAmount || l.winnerAmount || l.acceptedBidAmount || l.soldAmount || l.salePrice || l.precioVenta || 0) || 0, 15894 winnerBidId: String(l.winnerBidId || l.winningBidId || ''), 15895 winnerBuyerUid: String(l.winnerBuyerUid || l.winningBuyerUid || ''), 15896 canRepublish: auctionStatus === 'closed_no_bids' || (!auctionStatus && isExpired && !hasOffers), 15897 establecimiento: l.establecimientoNombre || 'Pendiente', 15898 localidad: l.establecimientoLocalidad || l.localidad || 'Pendiente', 15899 comitente: l.establecimientoComitente || l.comitente || 'Pendiente', 15900 pesoMin: l.pesoMinimo ?l.pesoMinimo + ' kg' : 'Pendiente', pesoMax: l.pesoMaximo ?l.pesoMaximo + ' kg' : 'Pendiente', 15901 pesaje: l.pesajeNombre || 'Pendiente', desbaste: l.desbaste != null ?l.desbaste + '%' : 'Pendiente', 15902 rendimiento: l.rendimientoEstimado != null ?l.rendimientoEstimado + '%' : 'Pendiente', 15903 obs: l.observaciones || 'Sin observaciones cargadas.', 15904 bids: rawBids.map((b) => Number(b.amount || b.monto) || 0), 15905 }; 15906 } 15907 roslotMarketTiming(lots = [], nowMs = Date.now()) { 15908 const parseTime = (value) => { 15909 const time = value ?new Date(value).getTime() : NaN; 15910 return Number.isFinite(time) ?time : null; 15911 }; 15912 const windows = (lots || []).map((lot) => ({ 15913 status: String(lot?.auctionStatus || '').toLowerCase(), 15914 start: parseTime(lot?.offerStartAt || lot?.raw?.offerStartAtUtc || lot?.raw?.offerStartAt), 15915 end: parseTime(lot?.offerEndAt || lot?.raw?.offerEndAtUtc || lot?.raw?.offerEndAt), 15916 })).filter((window) => window.start || window.end); 15917 const active = windows.filter((window) => window.status === 'open' && (!window.start || window.start <= nowMs) && (!window.end || window.end > nowMs)); 15918 if (active.length) { 15919 const starts = active.map((window) => window.start).filter(Number.isFinite); 15920 const ends = active.map((window) => window.end).filter(Number.isFinite); 15921 const start = starts.length ?Math.min(...starts) : null; 15922 const end = ends.length ?Math.max(...ends) : null; 15923 const progress = start && end && end > start ?Math.min(100, Math.max(0, ((nowMs - start) / (end - start)) * 100)) : 100; 15924 return { isOpen: true, target: end, progress, hasTarget: !!end }; 15925 } 15926 // El mercado tiene horario propio aunque no haya lotes publicados. Tomamos 15927 // la ventana completa mas reciente como calendario diario; el respaldo 15928 // coincide con el horario UTC que actualmente entrega el backend (11â20). 15929 const scheduleWindow = windows 15930 .filter((window) => window.start && window.end && window.end > window.start) 15931 .sort((a, b) => b.end - a.end)[0]; 15932 const scheduleStart = new Date(scheduleWindow?.start || Date.UTC(2000, 0, 1, 11)); 15933 const scheduleEnd = new Date(scheduleWindow?.end || Date.UTC(2000, 0, 1, 20)); 15934 const baParts = Object.fromEntries(new Intl.DateTimeFormat('en-CA', { 15935 timeZone: 'America/Argentina/Buenos_Aires', year: 'numeric', month: '2-digit', day: '2-digit', 15936 }).formatToParts(new Date(nowMs)).filter((part) => part.type !== 'literal').map((part) => [part.type, Number(part.value)])); 15937 const baDayUtc = Date.UTC(baParts.year, baParts.month - 1, baParts.day); 15938 const startOffset = scheduleStart.getUTCHours() * 3600000 + sc
15938heduleStart.getUTCMinutes() * 60000 + scheduleStart.getUTCSeconds() * 1000; 15939 let endOffset = scheduleEnd.getUTCHours() * 3600000 + scheduleEnd.getUTCMinutes() * 60000 + scheduleEnd.getUTCSeconds() * 1000; 15940 if (endOffset <= startOffset) endOffset += 24 * 60 * 60 * 1000; 15941 const dailyStart = baDayUtc + startOffset; 15942 const dailyEnd = baDayUtc + endOffset; 15943 if (nowMs >= dailyStart && nowMs < dailyEnd) { 15944 const progress = Math.min(100, Math.max(0, ((nowMs - dailyStart) / (dailyEnd - dailyStart)) * 100)); 15945 return { isOpen: true, target: dailyEnd, progress, hasTarget: true, scheduleFallback: true }; 15946 } 15947 const nextStarts = windows 15948 .filter((window) => !['sold', 'closed_no_bids'].includes(window.status) && window.start && window.start > nowMs) 15949 .map((window) => window.start); 15950 const scheduledTarget = nowMs < dailyStart ?dailyStart : dailyStart + 24 * 60 * 60 * 1000; 15951 const target = Math.min(scheduledTarget, ...(nextStarts.length ?nextStarts : [scheduledTarget])); 15952 const pastEnds = windows.map((window) => window.end).filter((end) => end && end <= nowMs && end < target); 15953 const dailyLastClose = nowMs < dailyStart ?dailyEnd - 24 * 60 * 60 * 1000 : dailyEnd; 15954 const lastClose = Math.max(dailyLastClose, ...(pastEnds.length ?pastEnds : [dailyLastClose])); 15955 const progressStart = Math.max(lastClose || 0, target - 24 * 60 * 60 * 1000); 15956 const progress = target > progressStart ?Math.min(100, Math.max(0, ((nowMs - progressStart) / (target - progressStart)) * 100)) : 0; 15957 return { isOpen: false, target, progress, hasTarget: true, scheduleFallback: true }; 15958 } 15959 updateRoslotMarketClockDom(nowMs = Date.now()) { 15960 if (!['pool', 'lot'].includes(this.state.route)) return; 15961 const lots = this.state.lots || []; 15962 const timing = this.roslotMarketTiming(lots, nowMs); 15963 const baDateFormatter = this._roslotBaDateFormatter || (this._roslotBaDateFormatter = new Intl.DateTimeFormat('en-CA', { 15964 timeZone: 'America/Argentina/Buenos_Aires', year: 'numeric', month: '2-digit', day: '2-digit', 15965 })); 15966 const baTodayKey = baDateFormatter.format(new Date(nowMs)); 15967 const lotDay = (lot) => String(lot?.fecha || lot?.raw?.fechaSubasta || '').slice(0, 10); 15968 const marketOpen = lots.some((lot) => lot.auctionStatus === 'open' && lotDay(lot) === baTodayKey) || timing.isOpen; 15969 const target = marketOpen ?(timing.isOpen ?timing.target : null) : timing.target; 15970 const progress = marketOpen && !timing.isOpen ?100 : Math.round(Number(timing.progress || 0) * 10) / 10; 15971 const remaining = (value) => { 15972 const seconds = Math.max(0, Math.ceil((value - nowMs) / 1000)); 15973 const days = Math.floor(seconds / 86400); 15974 const hours = Math.floor((seconds % 86400) / 3600); 15975 const minutes = Math.floor((seconds % 3600) / 60); 15976 const secs = seconds % 60; 15977 if (days) return `${days} d ${String(hours).padStart(2, '0')} h`; 15978 return `${String(hours).padStart(2, '0')}
15978:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; 15979 }; 15980 const scheduleFormatter = this._roslotScheduleFormatter || (this._roslotScheduleFormatter = new Intl.DateTimeFormat('es-AR', { 15981 timeZone: 'America/Argentina/Buenos_Aires', weekday: 'short', day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23', 15982 })); 15983 const targetLabel = target ?scheduleFormatter.format(new Date(target)).replace(',', ' ·') : ''; 15984 const countdown = this.state.lotsLoading ?'Sincronizando horariosâ¦' : (marketOpen ?(target ?'Cierra en ' + remaining(target) : 'Operando ahora') : (target ?'Abre en ' + remaining(target) : 'Sin subastas programadas')); 15985 const schedule = this.state.lotsLoading ?'Consultando la próxima ventana del mercado' : (marketOpen ?(target ?'Cierre previsto: ' + targetLabel : 'Mercado operando en este momento') : (target ?'Próxima apertura: ' + targetLabel : 'Próxima apertura sin programar')); 15986 const countdownNodes = document.querySelectorAll('[data-roslot-market-countdown]'); 15987 const scheduleNodes = document.querySelectorAll('[data-roslot-market-schedule]'); 15988 const progressNodes = document.querySelectorAll('[data-roslot-market-progress]'); 15989 const fillNodes = document.querySelectorAll('[data-roslot-market-progress-fill]'); 15990 countdownNodes.forEach((countdownNode) => { 15991 countdownNode.textContent = countdown; 15992 countdownNode.style.color = marketOpen ?'#176c58' : target ?'#07589d' : '#687a8f'; 15993 }); 15994 scheduleNodes.forEach((scheduleNode) => { scheduleNode.textContent = schedule; }); 15995 progressNodes.forEach((progressNode) => { progressNode.setAttribute('aria-valuenow', String(progress)); }); 15996 fillNodes.forEach((fillNode) => { 15997 fillNode.style.width = progress + '%'; 15998 fillNode.style.background = marketOpen ?'linear-gradient(90deg,#1f7a63,#43b48f)' : target ?'linear-gradient(90deg,#075da8,#28a7d8)' : '#aebdca'; 15999 }); 16000 } 16001 fmtARS(n) { return '$' + Math.round(n).toLocaleString('es-AR'); } 16002 moneyInputRaw(value, maxLength = 15) { 16003 return String(value ?? '').replace(/\D/g, '').slice(0, maxLength); 16004 } 16005 moneyInputDisplay(value) { 16006 const digits = this.moneyInputRaw(value).replace(/^0+(?=\d)/, ''); 16007 return digits ?digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.') : ''; 16008 } 16009 formatMoneyInputEvent(event, onRaw) { 16010 const input = event?.target; 16011 const value = String(input?.value || ''); 16012 const caret = Number.isFinite(input?.selectionStart) ?input.selectionStart : value.length; 16013 const digitsBeforeCaret = this.moneyInputRaw(value.slice(0, caret)).length; 16014 const raw = this.moneyInputRaw(value); 16015 const formatted = this.moneyInputDisplay(raw); 16016 const applyFormattedValue = () => { 16017 if (!input || !input.isConnected) return; 16018 input.value = formatted; 16019 let nextCaret = formatted.length; 16020 if (digitsBeforeCaret < raw.length) { 16021 let seen = 0; 16022 nextCaret = 0; 16023 while (nextCaret < formatted.length && seen < digitsBeforeCaret) { 16024 if (/\d/.test(formatted[nextCaret])) seen += 1; 16025 nextCaret += 1; 16026 } 16027 } 16028 try { input.setSelectionRange(nextCaret, nextCaret); } catch {} 16029 }; 16030 if (typeof onRaw === 'function') onRaw(raw); 16031 applyFormattedValue(); 16032 if (typeof requestAnimationFrame === 'function') requestAnimationFrame(applyFormattedValue); 16033 } 16034 cuitInputRaw(value) { 16035 return String(value ?? '').replace(/\D/g, '').slice(0, 11); 16036 } 16037 cuitInputDisplay(value) { 16038 const digits = this.cuitInputRaw(value); 16039 if (digits.length <= 2) return digits; 16040 if (digits.length <= 10) return digits.slice(0, 2) + '-' + digits.slice(2); 16041 return digits.slice(0, 2) + '-' + digits.slice(2, 10) + '-' + digits.slice(10); 16042 } 16043 apiUserCuit(profile = this.state.apiUser || {}) { 16044 const candidates = [ 16045 profile?.cuitCuil, profile?.cuit, profile?.cuil, 16046 profile?.profile?.cuitCuil, profile?.profile?.cuit, profile?.profile?.cuil, 16047 profile?.user?.cuitCuil, profile?.user?.cuit, profile?.user?.cuil, 16048 profile?.userData?.cuitCuil, profile?.userData?.cuit, profile?.userData?.cuil, 16049 profile?.producer?.cuitCuil, profile?.producer?.cuit, 16050 ]; 16051 return this.cuitInputRaw(candidates.find((value) => this.cuitInputRaw(value).length === 11) || ''); 16052 } 16053 async persistCabanaPrebidCuit(value) { 16054 const digits = this.cuitInputRaw(value); 16055 if (digits.length !== 11) throw new Error('Ingresá un CUIT/CUIL de 11 dÃgitos.'); 16056 const response = await this.cabanaApiFetch('/api/users/me',{method:'PATCH',body:JSON.stringify({cuitCuil:this.cuitInputDisplay(digits)})}); 16057 const profile=response?.data ?? response; 16058 this.setState({cabanaPrebidProfile:profile,apiUser:{...this.state.apiUser,cuitCuil:profile.cuitCuil}}); 16059 return profile; 16060 } 16061 rollText(str) { 16062 str = String(str || ''); 16063 const R = React; 16064 return R.createElement('span', { style: { display: 'inline-flex', overflow: 'hidden' } }, 16065 [...str].map((ch, i) => { 16066 if (!/\d/.test(ch)) return R.createElement('span', { key: 'c' + i }, ch); 16067 const d = Number(ch); 16068 return R.createElement('span', { key: 'd' + (str.length - i), style: { display: 'inline-block', height: '1.15em', overflow: 'hidden' } }, 16069 R.createElement('span', { style: { display: 'flex', flexDirection: 'column', transform: 'translateY(' + (-d * 1.15) + 'em)', transition: 'transform 550ms cubic-bezier(0.22,0.72,0.18,1) ' + (i * 30) + 'ms' } }, 16070 Array.from({ length: 10 }, (_, k) => R.createElement('span', { key: k, style: { height: '1.15em', lineHeight: '1.15em' } }, String(k))))); 16071 })); 16072 } 16073 rollNumber(amount) { return this.rollText(this.fmtARS(amount)); } 16074 rollInteger(amount) { return this.rollText(Math.max(0, Number(amount) || 0).toLocaleStr
16074ing('es-AR')); } 16075 emptyRemateConfig(type = '') { 16076 return { tipo: type || 'Remate Cabañas', titulo: '', cabezas: '', consignataria: '', provincia: '', lugar: '', localidad: '', fecha: '', hora: '', urlStreaming: '', contactoId: '', contactoNombre: '', contactoTelefono: '', contactoEmail: '', contactoNotas: '', cargaDatos: 'abierta', cargaLotes: 'abierta', approverUserIds: [], categoryPricing: [], preInicioFecha: '', preInicioHora: '', preFinFecha: '', preFinHora: '', catalogoPdfName: '', catalogoPdfSize: 0, catalogoPdfUrl: '', catalogoMediaId: '', cabanasPrincipales: [this.newCabanaConfigEntry()], cabanasInvitadas: [this.newCabanaConfigEntry()], formulariosCategoria: [this.newRemateCategoryForm()] }; 16077 } 16078 newCabanaConfigEntry() { return { id: 'cabana-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7), nombre: '', logoName: '', logoDataUrl: '', logoWhiteDataUrl: '', brandColor: '' }; } 16079 newRemateFormField(order = 1) { return { id: 'campo-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7), nombre: '', tipo: 'texto', opciones: '', requerido: false, identificadorPrincipal: false, identificadorCaracteristico: false, mostrarTarjeta: false, orden: order }; } 16080 newRemateSubform(displayOrder = 1) { return { id: 'form-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7), templateId: null, nombre: 'Datos principales', orden: displayOrder, activo: true, campos: [this.newRemateFormField()] }; } 16081 newRemateCategoryForm() { return { id: 'categoria-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7), categoryId: null, categoria: '', formularios: [this.newRemateSubform()] }; } 16082 normalizeRemateCategoryForm(category) { 16083 if (Array.isArray(category?.formularios)) return category; 16084 const legacyFields = Array.isArray(category?.campos) && category.campos.length ?category.campos : [this.newRemateFormField()]; 16085 return { ...category, formularios: [{ id: 'form-legacy-' + category.id, templateId: null, nombre: 'Datos principales', orden: 1, activo: true, campos: legacyFields.map((field, index) => ({ ...field, orden: field.orden || index + 1 })) }], campos: undefined }; 16086 } 16087 emptyRemateConfigToggles() { 16088 return { requerirRegistro: false, videoFinal: false, logoRural: false, preHabilitada: false, preIndividual: false, apruebaAuto: false, preAgrupada: false, agrupaCorrales: false }; 16089 } 16090 remateLocalDateTimeParts(value) { 16091 if (!value) return { date: '', time: '' }; 16092 const raw = String(value); 16093 if (/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)?$/.test(raw)) return {date:raw.slice(0,10),time:raw.slice(11,16)}; 16094 const parsed = new Date(value); 16095 if (Number.isNaN(parsed.getTime())) return { date: '', time: '' }; 16096 const parts = Object.fromEntries(new Intl.DateTimeFormat('en-GB', {timeZone:'America/Argentina/Buenos_Aires',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',hourCycle:'h23'}).formatToParts(parsed).map(part => [part.type,part.value])); 16097 return {date:parts.year + '-' + parts.month + '-' + parts.day,time:parts.hour + ':' + parts.minute}; 16098 } 16099 openRemateConfig(type = 'Remate Cabañas', syncPath = true) { 16100 this.resetCabanaRematePlaces(); 16101 this.setState({ rcSchemaEditor: false }); 16102 this.fetchAdminUsers(); 16103 if (!this.canCreateCabanaRemate()) { this.showToast('Solo admin y sysadmin pueden crear remates de cabaña.'); return; } 16104 this.fetchProvinces(); 16105 const form = this.emptyRemateConfig(type); 16106 this.rememberRoute('remateConfig'); 16107 this.setState({ route: 'remateConfig', remateConfigForm: form, remateConfigToggles: this.emptyRemateConfigToggles(), remateConfigSaved: false, remateConfigEditingId: '', remateConfigSaving: '' }); 16108 if (syncPath) { try { this.writeAppHistory('/cabanas/crear'); } catch {} } 16109 window.scrollTo(0, 0); 16110 } 16111 remateConfigFromItem(item) { 16112 const fieldFromApi = (field, order) => ({ 16113 id: field?.id || 'campo-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7), 16114 nombre: field?.name || field?.nombre || '', tipo: String(field?.type || field?.tipo || 'texto').toLowerCase(), 16115 opcionesOriginales: field?.options || field?.opciones || [], 16116 opciones: (field?.options || field?.opciones || []).map((option) => typeof option === 'string' ?option : option?.label || option?.name || option?.value || '').filter(
16116Boolean).join(', '), 16117 identificadorCaracteristico: field?.isCharacteristicIdentifier === true, requerido: field?.required === true, identificadorPrincipal: field?.isPrimaryIdentifier === true || field?.identificadorPrincipal === true, 16118 mostrarTarjeta: field?.showOnCard === true || field?.mostrarTarjeta === true, orden: Number(field?.order || field?.orden || order || 1), 16119 }); 16120 const categories = this.cabanaRemateSchema(item).map((category, categoryIndex) => { 16121 const forms = [...(category.forms || [])].sort((a, b) => Number(a.displayOrder || 0) - Number(b.displayOrder || 0)); 16122 const formularios = forms.map((subform, subformIndex) => ({ 16123 id: subform.id || 'form-' + categoryIndex + '-' + subformIndex, 16124 templateId: subform.templateId || null, 16125 nombre: subform.name || subform.nombre || 'Datos principales', 16126 orden: subformIndex + 1, activo: subform.isActive !== false && subform.activo !== false, 16127 campos: (subform.fields || subform.campos || []).map(fieldFromApi), 16128 })); 16129 return { id: 'categoria-' + categoryIndex, categoryId: category.categoryId || null, categoria: category.categoryName || category.categoria || '', formularios: formularios.length ?formularios : [this.newRemateSubform()] }; 16130 }); 16131 const schedule = item?.schedule || {}, location = item?.location || {}, load = item?.load || {}, catalog = item?.catalog || {}, contact = item?.contacto || item?.contact || {}; 16132 const scheduledAt = this.remateLocalDateTimeParts(item?.fechaHoraInicio || item?.startAt || ''); 16133 const prebidStart = this.remateLocalDateTimeParts(item?.prebid?.fechaHoraInicioPreoferta || item?.prebid?.inicio || ''); 16134 const prebidEnd = this.remateLocalDateTimeParts(item?.prebid?.fechaHoraFinPreoferta || item?.prebid?.fin || ''); 16135 const mapCabana = (cabana) => ({ id: cabana?.id || this.newCabanaConfigEntry().id, nombre: cabana?.nombre || cabana?.name || '', logoName: cabana?.logoName || '', logoDataUrl: window.resolveCabanaMediaUrl(cabana?.logoDataUrl || cabana?.logoUrl), logoWhiteDataUrl: window.resolveCabanaMediaUrl(cabana?.logoWhiteDataUrl), brandColor: cabana?.brandColor || '' }); 16136 const participants = this.cabanaRemateParticipants(item); 16137 const principals = participants.filter((cabana) => cabana.type === 'principal').map(mapCabana), guests = participants.filter((cabana) => cabana.type !== 'principal').map(mapCabana); 16138 return { 16139 tipo: item?.tipo || 'Remate Cabañas', titulo: item?.title || item?.titulo || item?.nombre_remate || '', cabezas: item?.cabezasEstimadas || item?.cabezas || '', 16140 consignataria: item?.consignatariaId || item?.consignataria || '', provincia: location?.provinciaNombre || location?.provinciaId || item?.provincia || '', lugar: location?.lugar || item?.lugar || '', localidad: location?.localidad || item?.localidad || '', 16141 fecha: this.rematePayloadDate(schedule?.fecha || item?.fecha || scheduledAt.date), hora: String(schedule?.hora || item?.hora || scheduledAt.time).slice(0, 5), urlStreaming: this.rematePayloadUrl(schedule?.urlStreaming || item?.urlStreaming || ''), 16142 contactoId: contact?.id || item?.contactoId || '', contactoNombre: contact?.name || contact?.nombre || item?.contactName || item?.contactoNombre || '', contactoTelefono: contact?.phone || contact?.telefono || item?.contactPhone || item?.contactoTelefono || '', contactoEmail: contact?.email || item?.contactEmail || item?.contactoEmail || '', contactoNotas: contact?.notes || contact?.notas || item?.contactNotes || item?.contactoNotas || '', 16143 cargaDatos: load?.datos === false || String(item?.cargaDatos || '').toLowerCase() === 'cerrada' ?'cerrada' : 'abierta', cargaLotes: load?.lotes === false || String(item?.cargaLotes || '').toLowerCase() === 'cerrada' ?'cerrada' : 'abierta', 16144 approverUserIds: [...(item?.prebid?.approverUserIds || [])], categoryPricing: (item?.prebid?.categoryPricing || []).map(p => ({...p})), 16145 preInicioFecha: prebidStart.date, preInicioHora: prebidStart.time, preFinFecha: prebidEnd.date, preFinHora: prebidEnd.time, 16146 catalogoPdfName: catalog?.nombreArchivo || '', catalogoPdfSize: catalog?.tamanoBytes || 0, catalogoPdfUrl: catalog?.url || '', catalogoMediaId: catalog?.mediaId || '', 16147 cabanasPrincipales: principals.length ?principals : [this.newCabanaConfigEntry()], cabanasInvitadas: guests.length ?guests : [this.newCabanaConfigEntry()], formulariosCategoria: categories.length ?categories : [this.newRemateCategoryForm()], 16148 }; 16149 } 16150 remateTogglesFromItem(item) { 16151 return { requerirRegistro: item?.visualization?.requerirRegistro
16151=== true, videoFinal: item?.visualization?.habilitarVideoFinal === true, logoRural: item?.visualization?.mostrarLogoCanalRural === true, preHabilitada: item?.prebid?.habilitarPreofertas === true || item?.prebid?.habilitada === true, preIndividual: item?.prebid?.preofertaIndividual === true, apruebaAuto: item?.prebid?.aprobarPreofertasAutomaticamente === true || item?.prebid?.aprobarPrimerasOfertas === true, preAgrupada: item?.prebid?.agruparPreofertas === true, agrupaCorrales: item?.visualization?.agruparCorrales === true }; 16152 } 16153 openCabanaRemateEdit(item, updateHash = true) { 16154 this.resetCabanaRematePlaces(); 16155 this.setState({ rcSchemaEditor: false }); 16156 this.fetchAdminUsers(); 16157 if (!this.canEditCabanaRemate(item)) { this.showToast('No tenés permiso para editar este remate.'); return; } 16158 this.fetchProvinces(); 16159 const id = this.cabanaRemateId(item); 16160 this.rememberRoute('remateConfig'); 16161 this.setState({ route: 'remateConfig', remateConfigForm: this.remateConfigFromItem(item), remateConfigToggles: this.remateTogglesFromItem(item), remateConfigSaved: false, remateConfigEditingId: id, remateConfigStatus: item.status || 'borrador', remateConfigSaving: '' }); 16162 if (updateHash) this.syncCabanaLocation(item, 'editar'); 16163 window.scrollTo(0, 0); 16164 } 16165 cabanaEligibleApprovers(form = this.state.remateConfigForm) { 16166 const consignatariaId = String(form?.consignataria || '').trim(); 16167 if (!consignatariaId) return []; 16168 const self = this.state.user ? { 16169 uid: this.state.user.uid, displayName: this.state.user.displayName, email: this.state.user.email, 16170 roles: this.apiRoleIds(this.state.apiUser), 16171 consignatariaIds: [...(this.state.userConsignatarias || []), this.state.userConsignataria].filter(item => item && item.isActive !== false).map(item => String(item.id || item.consignatariaId || '')), 16172 } : null; 16173 return [...(this.state.adminUsers || []), ...(self ? [self] : [])] 16174 .filter((user, index, users) => user?.uid && users.findIndex(candidate => candidate?.uid === user.uid) === index) 16175 .filter(user => (user.roles || []).some(role => ['consignatario', 'certificador', 'cabanero'].includes(role)) 16176 && user.consignatariaUserIsActive !== false && user.source?.isActive !== false && !user.source?.disabled 16177 && (user.consignatariaIds || []).map(String).includes(consignatariaId)) 16178 .sort((a,b) => String(a.displayName || a.email || '').localeCompare(String(b.displayName || b.email || ''), 'es')); 16179 } 16180 setRemateConfigField(key, value, buffered = false) { 16181 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16182 const next = { ...form, [key]: value }; 16183 if (key === 'consignataria' && String(value) !== String(form.consignataria || '')) next.approverUserIds = []; 16184 if (key === 'provincia') { 16185 next.localidad = ''; 16186 this.resetCabanaRematePlaces(); 16187 } 16188 const patch = { remateConfigForm: next, remateConfigSaved: false }; 16189 if (buffered) this.bufferInputState('remate-config-' + key, patch); 16190 else this.setState(patch); 16191 } 16192 cabanaOptionalCategoryPricing(form, categories) { 16193 return (categories || []).filter(category => category.categoryId).flatMap(category => { 16194 const price = (form.categoryPricing || []).find(item => String(item.categoryId) === String(category.categoryId)); 16195 const base = String(price?.baseAmount ?? '').trim(); 16196 const increment = String(price?.incrementAmount ?? '').trim(); 16197 if (!base && !increment) return []; 16198 if (!base || !increment || !Number.isFinite(Number(base)) || Number(base) < 0 || !Number.isFinite(Number(increment)) || Number(increment) <= 0) { 16199 throw new Error('En ' + (category.categoria || 'la categorÃa') + ', completá una base válida y un incremento mayor a cero, o dejá ambos vacÃos para usar los importes de cada lote.'); 16200 } 16201 return [{categoryId:category.categoryId,baseAmount:Number(base),incrementAmount:Number(increment)}]; 16202 }); 16203 } 16204 setCabanaCategoryPrice(categoryId, key, value) { 16205 if (!categoryId || !['baseAmount','incrementAmount'].includes(key)) return; 16206 const form=this.state.remateConfigForm || {}; 16207 const prices=form.categoryPricing || []; 16208 const current=prices.find(p=>p.categoryId===categoryId) || {}; 16209 this.setRemateConfigField('categoryPricing',[...prices.filter(p=>p.categoryId!==categoryId),{...current,categoryId,[key]:value}],true); 16210 } 16211 addCabanaConfigEntry(group) { 16212 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16213 this.setState({ remateConfigForm: { ...form, [group]: [...(form[group] || []), this.newCabanaConfigEntry()] }, remateConfigSaved: false }); 16214 } 16215 setCabanaConfigEntry(group, id, patch) { 16216 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16217 const nextState = { remateConfigForm: { ...form, [group]: (form[group] || []).map((item) => item.id === id ?{ ...item, ...patch } : item) }, remateConfigSaved: false }; 16218 if (Object.keys(patch).every((key) => key === 'nombre')) this.bufferInputState('remate-config-text', nextState, 450); 16219 else this.setState(nextState); 16220 } 16221 removeCabanaConfigEntry(group, id) { 16222 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16223 const next = (form[group] || []).filter((item) => item.id !== id); 16224 this.setState({ remateConfigForm: { ...form, [group]: next.length ?next : [this.newCabanaConfigEntry()] }, remateConfigSaved: false }); 16225 } 16226 processCabanaLogoDataUrl(dataUrl) { 16227 return new Promise((resolve, reject) => { 16228 const image = new Image(); 16229 image.crossOrigin = 'anonymous'; 16230 image.onload = () => { 16231 try { 16232 const sampleMax = 360; 16233 const sampleScale = Math.min(1, sampleMax / Math.max(image.naturalWi
16233dth || 1, image.naturalHeight || 1)); 16234 const sample = document.createElement('canvas'); 16235 sample.width = Math.max(1, Math.round(image.naturalWidth * sampleScale)); 16236 sample.height = Math.max(1, Math.round(image.naturalHeight * sampleScale)); 16237 const sampleContext = sample.getContext('2d', { willReadFrequently: true }); 16238 sampleContext.drawImage(image, 0, 0, sample.width, sample.height); 16239 const pixels = sampleContext.getImageData(0, 0, sample.width, sample.height).data; 16240 const buckets = new Map(); 16241 for (let index = 0; index < pixels.length; index += 16) { 16242 const red = pixels[index], green = pixels[index + 1], blue = pixels[index + 2], alpha = pixels[index + 3]; 16243 if (alpha < 48 || (red > 242 && green > 242 && blue > 242)) continue; 16244 const max = Math.max(red, green, blue), min = Math.min(red, green, blue); 16245 const saturation = max - min; 16246 const lightness = (max + min) / 2; 16247 if (lightness < 12 || lightness > 246) continue; 16248 const key = (red >> 4) + '-' + (green >> 4) + '-' + (blue >> 4); 16249 const current = buckets.get(key) || { count: 0, red: 0, green: 0, blue: 0, saturation: 0 }; 16250 current.count += 1; current.red += red; current.green += green; current.blue += blue; current.saturation += saturation; 16251 buckets.set(key, current); 16252 } 16253 let winner = null, winnerScore = -1; 16254 buckets.forEach((bucket) => { 16255 const averageSaturation = bucket.saturation / Math.max(1, bucket.count); 16256 const score = bucket.count * (1 + averageSaturation / 85); 16257 if (score > winnerScore) { winner = bucket; winnerScore = score; } 16258 }); 16259 const channel = (value) => Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, '0'); 16260 const brandColor = winner ?'#' + channel(winner.red / winner.count) + channel(winner.green / winner.count) + channel(winner.blue / winner.count) : '#0a5ca8'; 16261 16262 const outputMax = 1800; 16263 const outputScale = Math.min(1, outputMax / Math.max(image.naturalWidth || 1, image.naturalHeight || 1)); 16264 const whiteCanvas = document.createElement('canvas'); 16265 whiteCanvas.width = Math.max(1, Math.round(image.naturalWidth * outputScale)); 16266 whiteCanvas.height = Math.max(1, Math.round(image.naturalHeight * outputScale)); 16267 const whiteContext = whiteCanvas.getContext('2d', { willReadFrequently: true }); 16268 whiteContext.drawImage(image, 0, 0, whiteCanvas.width, whiteCanvas.height); 16269 const whitePixels = whiteContext.getImageData(0, 0, whiteCanvas.width, whiteCanvas.height); 16270 let visibleWeight = 0, visibleLuminance = 0, visibleSaturation = 0; 16271 for (let index = 0; index < whitePixels.data.length; index += 4) { 16272 const alpha = whitePixels.data[index + 3] / 255; 16273 if (alpha < .08) continue; 16274 const red = whitePixels.data[index], green = whitePixels.data[index + 1], blue = whitePixels.data[index + 2]; 16275 visibleWeight += alpha; 16276 visibleLuminance += ((red * .2126) + (green * .7152) + (blue * .0722)) * alpha; 16277 visibleSaturation += (Math.max(red, green, blue) - Math.min(red, green, blue)) * alpha; 16278 } 16279 const averageLuminance = visibleWeight ?visibleLuminance / visibleWeight : 0; 16280 const averageSaturation = visibleWeight ?visibleSaturation / visibleWeight : 0; 16281 const sourceIsLightMark = averageLuminance > 205 && averageSaturation < 45; 16282 for (let index = 0; index < whitePixels.data.length; index += 4) { 16283 const sourceAlpha = whitePixels.data[index + 3]; 16284 if (!sourceIsLightMark) { 16285 const red = whitePixels.data[index], green = whitePixels.data[index + 1], blue = whitePixels.data[index + 2]; 16286 const distanceFromWhite = 255 - Math.min(red, green, blue); 16287 const mask = Math.max(0, Math.min(1, (distanceFromWhite - 10) / 55)); 16288 whitePixels.data[index + 3] = Math.round(sourceAlpha * mask); 16289 } 16290 whitePixels.data[index] = 255; 16291 whitePixels.data[index + 1] = 255; 16292 whitePixels.data[index + 2] = 255; 16293 } 16294 whiteContext.clearRect(0, 0, whiteCanvas.width, whiteCanvas.height); 16295 whiteContext.putImageData(whitePixels, 0, 0); 16296 resolve({ logoWhiteDataUrl: whiteCanvas.toDataURL('image/png'), brandColor }); 16297 } catch (error) { reject(error); } 16298 }; 16299 image.onerror = reject; 16300 image.src = window.resolveCabanaMediaUrl(dataUrl); 16301 }); 16302 } 16303 readCabanaConfigLogo(group, id, file) { 16304 if (!file) return;
16305 if (file.type !== 'image/png' && !/\.png$/i.test(file.name || '')) { this.showToast('El logo debe ser un archivo PNG.'); return; } 16306 if (file.size > 2 * 1024 * 1024) { this.showToast('El logo PNG no puede superar 2 MB.'); return; } 16307 const reader = new FileReader(); 16308 reader.onload = async () => { 16309 const logoDataUrl = String(reader.result || ''); 16310 try { 16311 const generated = await this.processCabanaLogoDataUrl(logoDataUrl); 16312 this.setCabanaConfigEntry(group, id, { logoName: file.name, logoDataUrl, ...generated }); 16313 } catch { 16314 this.setCabanaConfigEntry(group, id, { logoName: file.name, logoDataUrl, logoWhiteDataUrl: '', brandColor: '' }); 16315 this.showToast('El logo se cargó, pero no se pudo generar su variante blanca.'); 16316 } 16317 }; 16318 reader.onerror = () => this.showToast('No se pudo leer el logo seleccionado.'); 16319 reader.readAsDataURL(file); 16320 } 16321 readRemateCatalog(file) { 16322 if (!file) return; 16323 if (file.type !== 'application/pdf' && !/\.pdf$/i.test(file.name || '')) { this.showToast('El catálogo debe ser un archivo PDF.'); return; } 16324 if (file.size > 30 * 1024 * 1024) { this.showToast('El catálogo PDF no puede superar 30 MB.'); return; } 16325 this._remateCatalogFile = file; 16326 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16327 this.setState({ remateConfigForm: { ...form, catalogoPdfName: file.name, catalogoPdfSize: file.size }, remateConfigSaved: false }); 16328 } 16329 removeRemateCatalog() { 16330 this._remateCatalogFile = null; 16331 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16332 this.setState({ remateConfigForm: { ...form, catalogoPdfName: '', catalogoPdfSize: 0, catalogoPdfUrl: '', catalogoMediaId: '' }, remateConfigSaved: false }); 16333 } 16334 addRemateCategoryForm() { 16335 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16336 this.setState({ remateConfigForm: { ...form, formulariosCategoria: [...(form.formulariosCategoria || []), this.newRemateCategoryForm()] }, remateConfigSaved: false }); 16337 } 16338 setRemateCategoryForm(id, value) { 16339 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16340 this.bufferInputState('remate-config-text', { remateConfigForm: { ...form, formulariosCategoria: (form.formulariosCategoria || []).map((item) => item.id === id ?{ ...item, categoria: value } : item) }, remateConfigSaved: false }, 450); 16341 } 16342 removeRemateCategoryForm(id) { 16343 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16344 const next = (form.formulariosCategoria || []).filter((item) => item.id !== id); 16345 this.setState({ remateConfigForm: { ...form, formulariosCategoria: next.length ?next : [this.newRemateCategoryForm()] }, remateConfigSaved: false }); 16346 } 16347 addRemateSubform(categoryId) { 16348 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16349 this.setState({ remateConfigForm: { ...form, formulariosCategoria: (form.formulariosCategoria || []).map((rawCategory) => { const category = this.normalizeRemateCategoryForm(rawCategory); return category.id === categoryId ?{ ...category, formularios: [...category.formularios, this.newRemateSubform(category.formularios.length + 1)] } : category; }) }, remateConfigSaved: false }); 16350 } 16351 setRemateSubform(categoryId, subformId, patch) { 16352 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16353 const nextState = { remateConfigForm: { ...form, formulariosCategoria: (form.formulariosCategoria || []).map((rawCategory) => { const category = this.normalizeRemateCategoryForm(rawCategory); return category.id === categoryId ?{ ...category, formularios: category.formularios.map((subform) => subform.id === subformId ?{ ...subform, ...patch } : subform) } : category; }) }, remateConfigSaved: false }; 16354 if (Object.keys(patch).every((key) => key === 'nombre')) this.bufferInputState('remate-config-text', nextState, 450); 16355 else this.setState(nextState); 16356 } 16357 startRemateConfigDrag(type, categoryId, subformId, fieldId, event) { 16358 this._remateConfigDrag = { type, categoryId, subformId, fieldId }; 16359 if (event?.dataTransfer) { 16360 event.dataTransfer.effectAllowed = 'move'; 16361 event.dataTransfer.setData('text/plain', fieldId || subformId || 'remate-item'); 16362 } 16363 } 16364 allowRemateConfigDrop(event) { 16365 if (event?.preventDefault) event.preventDefault(); 16366 if (event?.dataTransfer) event.dataTransfer.dropEffect = 'move'; 16367 } 16368 finishRemateConfigDrag() { 16369 this._remateConfigDrag = null; 16370 } 16371 bindRemateConfigSorting() { 16372 this._onRemateSortPointerDown = (event) => { 16373 const handle = event.target?.closest?.('[aria-label^="Ordenar subformulario"], [aria-label^="Ordenar campo"]'); 16374 if (!handle) return; 16375 const [label, categoryId, subformId, fieldId] = (handle.getAttribute('aria-label') || '').split('~');
16376 const kind = label.startsWith('Ordenar campo') ?'field' : 'subform'; 16377 this.startRemateConfigDrag(kind, categoryId, subformId, fieldId || null, event); 16378 const element = handle.closest(kind === 'field' ?'[aria-label^="Destino campo"]' : '[aria-label^="Destino subformulario"]'); 16379 this._remateSortVisual = element ? { 16380 element, 16381 handle, 16382 startY: event.clientY, 16383 target: null, 16384 original: { 16385 transform: element.style.transform, 16386 transition: element.style.transition, 16387 position: element.style.position, 16388 zIndex: element.style.zIndex, 16389 boxShadow: element.style.boxShadow, 16390 opacity: element.style.opacity, 16391 pointerEvents: element.style.pointerEvents, 16392 }, 16393 } : null; 16394 if (element) { 16395 element.style.position = 'relative'; 16396 element.style.zIndex = '30'; 16397 element.style.boxShadow = '0 16px 34px rgba(0, 35, 70, 0.18)'; 16398 element.style.opacity = '0.96'; 16399 element.style.pointerEvents = 'none'; 16400 element.style.willChange = 'transform'; 16401 handle.style.cursor = 'grabbing'; 16402 } 16403 document.body.style.userSelect = 'none'; 16404 event.preventDefault(); 16405 }; 16406 this._onRemateSortPointerMove = (event) => { 16407 const drag = this._remateConfigDrag; 16408 const visual = this._remateSortVisual; 16409 if (!drag || !visual) return; 16410 visual.element.style.transform = `translate3d(0, ${event.clientY - visual.startY}px, 0)`; 16411 const target = document.elementFromPoint(event.clientX, event.clientY)?.closest?.('[aria-label^="Destino subformulario"], [aria-label^="Destino campo"]'); 16412 if (!target) { event.preventDefault(); return; } 16413 const [label, categoryId, subformId, fieldId] = (target.getAttribute('aria-label') || '').split('~'); 16414 const kind = label === 'Destino campo' ?'field' : 'subform'; 16415 if (kind === drag.type && categoryId === drag.categoryId && (drag.type !== 'field' || subformId === drag.subformId)) { 16416 visual.target = { kind, categoryId, subformId, fieldId }; 16417 } 16418 event.preventDefault(); 16419 }; 16420 this._onRemateSortPointerUp = () => { 16421 const drag = this._remateConfigDrag; 16422 const visual = this._remateSortVisual; 16423 if (visual?.element) { 16424 Object.assign(visual.element.style, visual.original); 16425 visual.element.style.willChange = ''; 16426 if (visual.handle) visual.handle.style.cursor = 'grab'; 16427 } 16428 this._remateSortVisual = null; 16429 if (drag && visual?.target) { 16430 if (drag.type === 'subform') this.reorderRemateSubforms(drag.categoryId, visual.target.subformId, null); 16431 if (drag.type === 'field') this.reorderRemateFormFields(drag.categoryId, drag.subformId, visual.target.fieldId, null); 16432 } else { 16433 this.finishRemateConfigDrag(); 16434 } 16435 document.body.style.userSelect = ''; 16436 }; 16437 document.addEventListener('pointerdown', this._onRemateSortPointerDown); 16438 window.addEventListener('pointermove', this._onRemateSortPointerMove, { passive: false }); 16439 window.addEventListener('pointerup', this._onRemateSortPointerUp); 16440 window.addEventListener('pointercancel', this._onRemateSortPointerUp); 16441 } 16442 reorderRemateSubforms(categoryId, targetSubformId, event, keepDragging = false) { 16443 if (event?.preventDefault) event.preventDefault(); 16444 const drag = this._remateConfigDrag; 16445 if (!keepDragging) this._remateConfigDrag = null; 16446 if (!drag || drag.type !== 'subform' || drag.categoryId !== categoryId || drag.subformId === targetSubformId) return; 16447 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16448 const categories = (form.formulariosCategoria || []).map((rawCategory) => { 16449 const category = this.normalizeRemateCategoryForm(rawCategory); 16450 if (category.id !== categoryId) return category; 16451 const items = [...category.formularios]; 16452 const sourceIndex = items.findIndex((item) => item.id === drag.subformId); 16453 const targetIndex = items.findIndex((item) => item.id === targetSubformId); 16454 if (sourceIndex < 0 || targetIndex < 0) return category; 16455 const [moved] = items.splice(sourceIndex, 1); 16456 items.splice(targetIndex, 0, moved); 16457 return { ...category, formularios: items.map((item, index) => ({ ...item, orden: index + 1 })) }; 16458 }); 16459 this.setState({ remateConfigForm: { ...form, formulariosCategoria: categories }, remateConfigSaved: false }); 16460 if (keepDragging) this._remateConfigDrag = drag; 16461 } 16462 removeRemateSubform(categoryId, subformId) { 16463 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16464 this.setState({ remateConfigForm: { ...form, formulariosCategoria: (form.formulariosCategoria || []).map((rawCategory) => { const category = this.normalizeRemateCategoryForm(rawCategory); if (category.id !== categoryId) return category; const next = category.formularios.filter((subform) => subform.id !== subformId); return { ...category, formularios: next.length ?next : [this.newRemateSubform()] }; }) }, remateConfigSaved: false }); 16465 } 16466 addRemateFormField(categoryId, subformId) { 16467 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16468 this.setState({ remateConfigForm: { ...form, formulariosCategoria: (form.formulariosCategoria || []).map((rawCategory) => { const category = this.normalizeRemateCategoryForm(rawCategory); return category.id === categoryId ?{ ...category, formularios: category.formularios.map((subform) => subform.id === subformId ?{ ...subform, campos: [...(subform.campos || []), this.newRemateFormField((subform.campos || []).length + 1)] } : subform) } : category; }) }, remateConfigSaved: false }); 16469 } 16470 setRemateFormField(categoryId, subformId, fieldId, patch) { 16471 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16472 const categories = (form.formulariosCategoria || []).map((category) => this.normalizeRemateCategoryForm(category)); 16473 if (patch.mostrarTarjeta === true) { 16474 const category = categories.find((item) => item.id === categoryId); 16475 const visibleCount = (category?.formularios || []).flatMap((subform) => subform.campos || []).filter((field) => field.mostrarTarjeta && field.id !== fieldId).length; 16476 if (visibleCount >= 4) { this.showToast('Podés mostrar hasta 4 campos por categorÃa en la tarjeta.'); return; } 16477 } 16478 const nextState = { remateConfigForm: { ...form, formulariosCategoria: categories.map((category) => { 16479 if (category.id !== categoryId) return category; 16480 const exclusiveIdentifier = patch.identificadorPrincipal === true; 16481 return { ...category, formularios: category.formularios.map((subform) => ({ 16482 ...subform, 16483 campos: (subform.campos || []).map((field) => field.id === fieldId 16484 ?{ ...field, ...patch } 16485 : subform.id === subformId ?{ ...field, ...(exclusiveIdentifier ?{identificadorPrincipal:false}:{}), ...(patch.identificadorCaracteristico === true ?{identificadorCaracteristico:false}:{}) } : field), 16486 })) }; 16487 }) }, remateConfigSaved: false }; 16488 if (Object.keys(patch).every((key) => ['nombre', 'opciones'].includes(key))) this.bufferInputState('remate-config-text', nextState, 450); 16489 else this.setState(nextState); 16490 } 16491 reorderRemateFormFields(categoryId, subformId, targetFieldId, event, keepDragging = false) { 16492 if (event?.preventDefault) event.preventDefault(); 16493 const drag = this._remateConfigDrag; 16494 if (!keepDragging) this._remateConfigDrag = null; 16495 if (!drag || drag.type !== 'field' || drag.categoryId !== categoryId || drag.subformId !== subformId || drag.fieldId === targetFieldId) return; 16496 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16497 const categories = (form.formulariosCategoria || []).map((rawCategory) => { 16498 const category = this.normalizeRemateCategoryForm(rawCategory); 16499 if (category.id !== categoryId) return category; 16500 return { ...category, formularios: category.formularios.map((subform) => { 16501 if (subform.id !== subformId) return subform; 16502 const items = [...(subform.campos || [])]; 16503 const sourceIndex = items.findIndex((item) => item.id === drag.fieldId); 16504 const targetIndex = items.findIndex((item) => item.id === targetFieldId); 16505 if (sourceIndex < 0 || targetIndex < 0) return subform; 16506 const [moved] = items.splice(sourceIndex, 1); 16507 items.splice(targetIndex, 0, moved); 16508 return { ...subform, campos: items.map((item, index) => ({ ...item, orden: index + 1 })) }; 16509 }) }; 16510 }); 16511 this.setState({ remateConfigForm: { ...form, formulariosCategoria: categories }, remateConfigSaved: false }); 16512 if (keepDragging) this._remateConfigDrag = drag; 16513 } 16514 removeRemateFormField(categoryId, subformId, fieldId) { 16515 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16516 this.setState({ remateConfigForm: { ...form, formulariosCategoria: (form.formulariosCategoria || []).map((rawCategory) => { const category = this.normalizeRemateCategoryForm(rawCategory); if (category.id !== categoryId) return category; return { ...category, formularios: category.formularios.map((subform) => { if (subform.id !== subformId) return subform; const next = (subform.campos || []).filter((field) => field.id !== fieldId); return { ...subform, campos: next.length ?next : [this.newRemateFormField()] }; }) }; }) }, remateConfigSaved: false }); 16517 } 16518 toggleRemateConfig(key) { this.setState({ remateConfigToggles: { ...(this.state.remateConfigToggles || this.emptyRemateConfigToggles()), [key]: !(this.state.remateConfigToggles || this.emptyRemateConfigToggles())[key] }, remateConfigSaved: false }); } 16519 resetRemateConfig() { 16520 this.resetCabanaRematePlaces(); 16521 this._remateCatalogFile = null; 16522 const editingId = String(this.state.remateConfigEditingId || ''); 16523 const item = editingId ?(this.state.cabanas || []).find((remate) => this.cabanaRemateId(remate) === editingId) : null; 16524 this.setState({ remateConfigForm: item ?this.remateConfigFromItem(item) : this.emptyRemateConfig('Remate Cabañas'), remateConfigToggles: item ?this.remateTogglesFromItem(item) : this.emptyRemateConfigToggles(), remateConfigSaved: false }); 16525 } 16526 rematePayloadKey(value) { 16527 return String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); 16528 } 16529 rematePayloadDate(value) { 16530 const raw = String(value || '').trim(); 16531 if (!raw) return ''; 16532 const iso = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/); 16533 if (iso) return raw; 16534 const local = raw.match(/^(\d{2})[-/](\d{2})[-/](\d{4})$/); 16535 if (local) return local[3] + '-' + local[2] + '-' + local[1]; 16536 const parsed = new Date(raw); 16537 if (Number.isNaN(parsed.getTime())) return raw; 16538 const pad = (number) => String(number).padStart(2, '0'); 16539 return parsed.getFullYear() + '-' + pad(parsed.getMonth() + 1) + '-' + pad(parsed.getDate()); 16540 } 16541 rematePayloadTime(value) { 16542 const raw = String(value || '').trim(); 16543 if (!raw) return ''; 16544 const match = raw.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?/); 16545 if (!match) return raw; 16546 return String(Number(match[1])).padStart(2, '0') + ':' + match[2] + ':' + (match[3] || '00'); 16547 } 16548 rematePayloadUrl(value) { 16549 const raw = String(value || '').trim(); 16550 if (!raw) return ''; 16551 const markdown = raw.match(/^\[[^\]]*\]\((https?:\/\/[^\s)]+)\)$/i); 16552 return markdown ?markdown[1] : raw; 16553 } 16554 rematePayloadNumber(value) { 16555 if (value === '' || value === null || value === undefined) return null; 16556 const number = Number(String(value).replace(/\./g, '').replace(',', '.')); 16557 return Number.isFinite(number) ?number : null; 16558 } 16559 rematePayloadDateTime(date, time) { 16560 if (!date || !time) return null; 16561 const normalizedTime = /^\d{2}:\d{2}:\d{2}$/.test(String(time)) ?String(time) : String(time).slice(0, 5) + ':00'; 16562 const localDate = new Date(String(date) + 'T' + normalizedTime + '-03:00'); 16563 return Number.isNaN(localDate.getTime()) ?String(date) + 'T' + normalizedTime : localDate.toISOString(); 16564 } 16565 buildCabanaRematePayload(form, toggles, principals, guests, categoryForms, status = 'borrador') { 16566 const fieldTypes = { texto: 'Texto', numero: 'Numero', fecha: 'Fecha', seleccion: 'Seleccion', booleano: 'Booleano' }; 16567 const serializeField = (field, fieldIndex) => ({ 16568 name: String(field.nombre || '').trim(), 16569 type: fieldTypes[String(field.tipo || 'texto').toLowerCase()] || 'Texto', 16570 required: !!field.requerido, 16571 showOnCard: !!field.mostrarTarjeta, 16572 isPrimaryIdentifier: !!field.identificadorPrincipal, 16573 isCharacteristicIdentifier: !!field.identificadorCaracteristico, 16574 order: fieldIndex + 1, 16575 options: field.tipo === 'seleccion' ?String(field.opciones || '').split(',').map((option) =>
16575 option.trim()).filter(Boolean).map((label) => { 16576 const original = (field.opcionesOriginales || []).find((option) => typeof option === 'object' && (option.label || option.name || option.value) === label); 16577 return { value: original?.value || label, label }; 16578 }) : [], 16579 }); 16580 const serializeCabana = (item, index) => ({ 16581 id: null, 16582 nombre: String(item.nombre || '').trim(), 16583 logoName: String(item.logoName || ''), 16584 logoDataUrl: String(item.logoDataUrl || ''), 16585 displayOrder: index + 1, 16586 }); 16587 return { 16588 title: String(form.titulo || '').trim(), 16589 consignatariaId: String(form.consignataria || '').trim(), 16590 cabezasEstimadas: this.rematePayloadNumber(form.cabezas), 16591 status, 16592 location: { 16593 provinciaId: this.rematePayloadKey(form.provincia), 16594 provinciaNombre: String(form.provincia || '').trim(), 16595 localidad: String(form.localidad || '').trim(), 16596 lugar: String(form.lugar || '').trim(), 16597 }, 16598 schedule: { 16599 fecha: this.rematePayloadDate(form.fecha), 16600 hora: this.rematePayloadTime(form.hora), 16601 urlStreaming: this.rematePayloadUrl(form.urlStreaming) || null, 16602 }, 16603 contacto: { 16604 ...(String(form.contactoId || '').trim() ?{ id: String(form.contactoId).trim() } : {}), 16605 name: String(form.contactoNombre || '').trim() || null, 16606 phone: String(form.contactoTelefono || '').trim() || null, 16607 email: String(form.contactoEmail || '').trim() || null, 16608 notes: String(form.contactoNotas || '').trim() || null, 16609 }, 16610 load: { 16611 datos: form.cargaDatos === 'abierta', 16612 lotes: form.cargaLotes === 'abierta', 16613 }, 16614 cabanasPrincipales: principals.map(serializeCabana), 16615 cabanasInvitadas: guests.map(serializeCabana), 16616 formsByCategory: categoryForms.map((category) => { 16617 const orderedForms = [...category.formularios].sort((a, b) => Number(a.orden || 0) - Number(b.orden || 0)); 16618 return { 16619 categoryId: category.categoryId || null, 16620 categoryName: String(category.categoria || '').trim(), 16621 forms: orderedForms.map((subform, subformIndex) => ({ 16622 templateId: subform.templateId || null, 16623 name: String(subform.nombre || '').trim(), 16624 displayOrder: subformIndex + 1, 16625 isActive: subform.activo !== false, 16626 fields: (subform.campos || []).filter((field) => field.nombre).map(serializeField), 16627 })), 16628 }; 16629 }), 16630 prebid: { 16631 habilitarPreofertas: !!toggles.preHabilitada, 16632 fechaHoraInicioPreoferta: this.rematePayloadDateTime(form.preInicioFecha, form.preInicioHora), 16633 fechaHoraFinPreoferta: this.rematePayloadDateTime(form.preFinFecha, form.preFinHora), 16634 approverUserIds: [...new Set(form.approverUserIds || [])], 16635 preofertaIndividual: !!toggles.preIndividual, 16636 categoryPricing: this.cabanaOptionalCategoryPricing(form, categoryForms), 16637 aprobarPreofertasAutomaticamente: !!toggles.apruebaAuto, 16638 agruparPreofertas: !!toggles.preAgrupada, 16639 }, 16640 visualization: { 16641 requerirRegistro: !!toggles.requerirRegistro, 16642 habilitarVideoFinal: !!toggles.videoFinal, 16643 mostrarLogoCanalRural: !!toggles.logoRural, 16644 agruparCorrales: !!toggles.agrupaCorrales, 16645 }, 16646 catalog: form.catalogoPdfName ? { 16647 nombreArchivo: String(form.catalogoPdfName), 16648 tamanoBytes: Number(form.catalogoPdfSize) || 0, 16649 url: String(form.catalogoPdfUrl || '').trim() || null, 16650 mediaId: String(form.catalogoMediaId || '').trim() || null, 16651 } : null, 16652 }; 16653 } 16654 remateConfigSchemaSignature(item) { 16655 return JSON.stringify(this.cabanaRemateSchema(item).map((category) => ({ 16656 name: String(category.categoryName || '').trim(), 16657 forms: [...(category.forms || [])].sort((a, b) => Number(a.displayOrder || 0) - Number(b.displayOrder || 0)).map((form) => ({ 16658 name: String(form.name || '').trim(), active: form.isActive !== false, 16659 fields: [...(form.fields || [])].sort((a, b) => Number(a.order || 0) - Number(b.order || 0)).map((field) => ({ 16660 name: String(field.name || '').trim(), type: String(field.type || '').toLowerCase(), 16661 card: !!field.showOnCard, primary: !!field.isPrimaryIdentifier, characteristic: !!field.isCharacteristicIdentifier, 16662 options: (field.options || []).map((option) => typeof option === 'string' ?{ value: option, label: option } : { value: option.value, label: option.label }), 16663 })), 16664 })), 16665 }
16665))); 16666 } 16667 async saveRemateConfig(status = 'borrador') { 16668 if (!this.state.remateConfigEditingId && !this.canCreateCabanaRemate()) { this.showToast('Solo admin y sysadmin pueden crear remates de cabaña.'); return; } 16669 if (this.state.remateConfigSaving) return; 16670 const form = this.state.remateConfigForm || this.emptyRemateConfig(); 16671 const required = [['titulo','tÃtulo'],['consignataria','consignataria'],['fecha','fecha'],['hora','hora']]; 16672 const missing = required.filter(([key]) => !String(form[key] || '').trim()).map(([, label]) => label); 16673 if (missing.length) { this.showToast('Completá ' + missing.join(', ') + '.'); return; } 16674 const scheduleDate = this.rematePayloadDate(form.fecha); 16675 const scheduleTime = this.rematePayloadTime(form.hora); 16676 const scheduledAt = new Date(scheduleDate + 'T' + scheduleTime); 16677 if (Number.isNaN(scheduledAt.getTime())) { this.showToast('Ingresá una fecha y hora válidas.'); return; } 16678 if (status === 'abierto' && !this.state.rcSchemaEditor && scheduledAt.getTime() < Date.now()) { this.showToast('La fecha y hora del remate deben ser futuras para publicarlo como abierto.'); return; } 16679 const streamingUrl = this.rematePayloadUrl(form.urlStreaming); 16680 if (streamingUrl) { 16681 try { 16682 const parsedUrl = new URL(streamingUrl); 16683 if (!['http:', 'https:'].includes(parsedUrl.protocol)) throw new Error('invalid_protocol'); 16684 } catch { this.showToast('Ingresá una URL de streaming válida, por ejemplo https://...'); return; } 16685 } 16686 if (form.contactoEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(form.contactoEmail).trim())) { this.showToast('Ingresá un email de contacto válido.'); return; } 16687 if (String(form.contactoTelefono || '').trim() && String(form.contactoTelefono).replace(/\D/g, '').length < 8) { this.showToast('Ingresá un teléfono de contacto válido.'); return; } 16688 const principals = (form.cabanasPrincipales || []).filter((item) => item.nombre || item.logoDataUrl); 16689 const guests = (form.cabanasInvitadas || []).filter((item) => item.nombre || item.logoDataUrl); 16690 if (!principals.length) { this.showToast('Agregá al menos una cabaña principal.'); return; } 16691 if ([...principals, ...guests].some((item) => { 16692 const logo = String(item.logoDataUrl || '').trim(); 16693 return !String(item.nombre || '').trim() || (logo && !/^data:image\/png;base64,/i.test(logo) && !/^https?:\/\//i.test(logo)); 16694 })) { this.showToast('Cada cabaña debe tener nombre. Si cargás un logo, debe ser un PNG válido.'); return; } 16695 const categoryForms = (this.state.remateConfigEditingId ?(form.formulariosCategoria || []) : []).map((category) => this.normalizeRemateCategoryForm(category)).filter((category) => category.categoria || category.formularios.some((subform) => (subform.campos || []).some((field) => field.nombre))); 16696 if (categoryForms.some((category) => !category.categoria || !category.formularios.length || category.formularios.some((subform) => !subform.nombre || !(subform.campos || []).some((field) => field.nombre)))) { this.showToast('Cada categorÃa y subformulario debe tener nombre y al menos un campo.'); return; } 16697 if (categoryForms.some((category) => category.formularios.some((subform) => (subform.campos || []).some((field) => field.nombre && field.tipo === 'seleccion' && !String(field.opciones || '').trim())))) { this.showToast('Completá las opciones de los campos de selección.'); return; } 16698 if (categoryForms.some((category) => category.formularios.flatMap((subform) => subform.campos || []).filter((field) => field.mostrarTarjeta).length > 4)) { this.showToast('Solo se permiten 4 campos visibles por categorÃa.'); return; } 16699 if (categoryForms.some((category) => category.formularios.some((subform) => (subform.campos || []).filter((field) => field.identificadorPrincipal).length > 1))) { this.showToast('Solo un campo por formulario puede ser el identificador principal.'); return; } 16700 const toggles = this.state.remateConfigToggles || this.emptyRemateConfigToggles(); 16701 if (categoryForms.some(c => c.formularios.some(f => (f.campos || []).filter(x => x.identificadorCaracteristico).length > 1))) { this.showToast('Solo un identificador caracterÃstico por formulario.'); return; } 16702 try { this.cabanaOptionalCategoryPricing(form, categoryForms); } catch (error) { this.showToast(error.message); return; } 16703 const payload = this.buildCabanaRematePayload(form, toggles, principals, guests, categoryForms, status); 16704 const editingId = String(this.state.remateConfigEditingId || ''); 16705 const currentUid = String(this.state.apiUser?.uid || this.state.apiUser?.userId || this.state.apiUser?.id || this.state.user?.uid || ''); 16706 const currentEmail = String(this.state.apiUser?.email || this.state.user?.email || '').trim().toLowerCase(); 16707 this.setState({ remateConfigSaving: status === 'abierto' ?'publish' : 'draft', remateConfigSaved: false }); 16708 try { 16709 const path = '/api/cabin-auctions' + (editingId ?'/' + encodeURIComponent(editingId) : ''); 16710 const response = await this.cabanaApiFetch(path, { method: editingId ?'PUT' : 'POST', body: JSON.stringify(payload) }); 16711 const returned = response?.data || response?.item || response?.auction || response?.cabinAuction || response || {}; 16712 const returnedId = this.cabanaRemateId(returned) || editingId; 16713 const saved = { ...payload, ...returned, id: returnedId || returned?.id, creatorUid: returned?.creatorUid || returned?.createdByUid || currentUid, creatorEmail: returned?.creatorEmail || returned?.createdByEmail || currentEmail }; 16714 if (Array.isArray(returned.forms) && !Array.isArray(returned.formsByCategory)) delete saved.formsByCategory; 16715 const savedId = this.cabanaRemateId(saved); 16716 if (savedId) { 16717 this.setState({ remateConfigEditingId: savedId }); 16718 const confirmation = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(savedId)); 16719 const persisted = confirmation?.data || confirmation?.item || confirmation?.auction || confirmation?.cabinAuction || confirmation; 16720 if (!!persisted?.visualization?.requerirRegistro !== !!payload.visualization.requerirRegistro) throw new Error('El servidor no confirmó el acceso bajo registro. Tus cambios siguen en el editor.'); 16721 Object.assign(saved,persisted); 16722 if (Array.isArray(persisted?.forms) && !Array.isArray(persisted?.formsByCategory)) delete saved.formsByCategory; 16723 if (this.remateConfigSchemaSignature(persisted) !== this.remateConfigSchemaSignature(payload)) { 16724 throw new Error('El servidor no conservó todos los cambios del formulario. Tus cambios siguen en el editor; no se confirmó el guardado.'); 16725 } 16726 } else { 16727 throw new Error('El servidor no devolvió el remate guardado. No se pudo confirmar el guardado.'); 16728 } 16729 this.setState({ remateConfigForm: this.remateConfigFromItem(saved), remateConfigSaving: '', remateConfigSaved: true, remateConfigStatus: status, remateConfigEditingId: savedId, cabanas: [saved, ...(this.state.cabanas || []).filter((item) => this.cabanaRemateId(item) !== savedId)] }); 16730 if (savedId) this.syncCabanaLocation(saved, 'editar', { replace: true }); 16731 this.showToast(this.state.rcSchemaEditor ?'Formulario de lotes guardado.' : status === 'abierto' ?'Remate publicado.' : 'Borrador guardado.'); 16732 if (status === 'abierto' && !this.state.rcSchemaEditor) { 16733 await this.fetchCabanas(true); 16734 this.go('cabanas'); 16735 } 16736 } catch (error) { 16737 this.setState({ remateConfigSaving: '' }); 16738 this.showToast(error?.message || 'No se pudo guardar el remate.'); 16739 } 16740 } 16741 scheduleRouteWork(route) { 16742 if (this._routeWorkTimer) clearTimeout(this._routeWorkTimer); 16743 const run = () => { 16744 this._routeWorkTimer = null; 16745 if (this.state.route !== route) return; 16746 if (route === 'home') this.animateStats(); 16747 if (route === 'pool') { 16748 this.syncAuctionStatuses(); 16749 this.syncLiveBids(); 16750 this.fetchNotifications(true); 16751 this.fetchNotificationHistory(true); 16752 if (!this.state.myBids || (this.canManageSellerData(this.state.apiUser) && !this.state.myLots)) this.refreshProfileData(); 16753 } 16754 if (route === 'roslotSummary') this.fetchRoslotDailyReport(); 16755 if (route === 'profile') this.refreshProfileData(); 16756 if (route === 'mailingAdmin') this.fetchMailing(); 16757 if (route === 'notifications') this.fetchNotificationHistory(); 16758 if (route === 'remateConfig') this.fetchProvinces(); 16759 if (route === 'load') { 16760 if (!this.state.comitentes) this.fetchComitentes(); 16761 if (!this.state.establishments) this.fetchEstablishments(); 16762 } 16763 if (route === 'rosganLotLoad') this.loadRosganLotCatalogs(); 16764 if (route === 'remates') { 16765 this.loadRosganRemateAdmin(); 16766 if (this.state.monthRemates === null) this.fetchMonthRemates(); 16767 this.fetchLatestFinishedRemates(); 16768 if (!this.state.cabanasCollectionReady && !this.state.cabanasLoading) this.fetchCabanas(); 16769 if (this.canManageSellerData(this.state.apiUser)) this.fetchProvinces(); 16770 } 16771 if (route === 'rosganRemateCreate' && this.canManageUsers(this.state.apiUser)) { 16772 this.loadRosganRemateAdmin(); 16773 this.fetchProvinces(); 16774 if (!this.state.rosganRemateFormOpen) this.openRosganRemateForm(null, { navigate: false }); 16775 } 16776 if (route === 'cabanas') { 16777 if (!this.state.cabanasCollectionReady && !this.state.cabanasLoading) this.fetchCabanas(true); 16778 else if (this.state.cabanasCollectionReady) this.fetchCabanaCardMedia(); 16779 } 16780 if (route === 'feria') { this.fetchProvinces(); this.prepareFeriaConsignataria(); } 16781 // The statistics component loads its validated server snapshot and cached MAV data. 16782 }; 16783 requestAnimationFrame(() => { this._routeWorkTimer = setTimeout(run, 0); }); 16784 } 16785 go(route, options = {}) { 16786 if (route !== 'rosganAuditor' && this._auditorPanel) { this._auditorPanel.close(); this._auditorPanel = null; } 16787 if (!this.state.user && !['home', 'remates', 'cabanas'].includes(route)) { 16788 this.syncAppLocation(route); 16789 this.setState({ route: 'login', routeLoading: false, apiUser: null, authReady: true }); 16790 return; 16791 } 16792 this._pathRequest = (this._pathRequest || 0) + 1; 16793 if (this.state.routeLoading) this.setState({ routeLoading: false }); 16794 if (route === 'mercado' && !this.canAccessStatistics()) { 16795 this.showToast('EstadÃsticas está en desarrollo y solo está disponible para admin y sysadmin.'); 16796 this.go('home', options); 16797 return; 16798 } 16799 if (['pool', 'lot', 'load', 'roslotSummary'].includes(route) && !this.canAccessRoslot()) { 16800 this.showToast('Por ahora Roslot está disponible solo para administradores.'); 16801 if (options.syncPath !== false) this.syncAppLocation('home', { replace: true }); 16802 return; 16803 } 16804 if (route === 'feriaConfig' && !this.canManageUsers()) { 16805 this.showToast('El control de dispositivos está disponible sólo para admin y sysadmin.'); 16806 this.syncAppLocation('home', { replace: true });
16806 this.go('home', { syncPath: false }); return; 16807 } 16808 if (route === 'feria' && !this.canAccessFeria()) { 16809 this.showToast('Feria está disponible para consignatarios, certificadores y administradores.'); 16810 if (options.syncPath !== false) this.syncAppLocation('home', { replace: true }); 16811 return; 16812 } 16813 if (route === 'load' && this.state.apiUser && !this.canManageSellerData(this.state.apiUser)) { this.showToast('Tu cuenta no tiene permiso para cargar lotes.'); return; } 16814 if (route === 'rosganLotLoad' && !this.canManageRosganLots(this.state.apiUser)) { this.showToast('Tu cuenta no tiene permiso para cargar lotes Rosgan.'); return; } 16815 if (route === 'rosganRemateCreate' && this.state.apiUser && !this.canManageUsers(this.state.apiUser)) { this.showToast('Solo admin y sysadmin pueden crear remates Rosgan.'); return; } 16816 if (route === 'mailingAdmin' && !this.canManageUsers()) { this.showToast('No tenés permiso para administrar el mailing.'); this.syncAppLocation('profile', { replace: true }); this.go('profile', { syncPath: false }); return; } 16817 if (route === 'remateConfig') { this.openRemateConfig(); return; } 16818 const allowed = ['home', 'pool', 'roslotSummary', 'load', 'rosganLotLoad', 'feria', 'feriaConfig', 'profile', 'mailingAdmin', 'devActivity', 'notifications', 'remates', 'rosganRemateCreate', 'remateDetail', 'rosganResults', 'rosganStats', 'rosganCommercial', 'rosganPriceBoard', 'rosganTvLower', 'rosganTvConfig', 'cabanas', 'cabanaDetail', 'cabanaAuditor', 'cabanaPrebidPanel', 'cabanaStats', 'cabanaCommercial', 'cabanaPriceBoard', 'cabanaTvLower', 'cabanaTvConfig', 'cabanaLotDetail', 'cabanaLotCreate', 'cabanaSalesOrder', 'channel', 'mercado']; 16819 if (!allowed.includes(route)) { this.showToast('Próxima etapa: ' + route); return; } 16820 if (route === this.state.route) { 16821 if (this.state.filtersOpen) this.setState({ filtersOpen: false }); 16822 if (route === 'cabanas') { 16823 if (!this.state.cabanasCollectionReady && !this.state.cabanasLoading) this.fetchCabanas(true); 16824 else if (this.state.cabanasCollectionReady) this.fetchCabanaCardMedia(); 16825 } 16826 window.scrollTo({ top: 0, behavior: 'auto' }); 16827 return; 16828 } 16829 if (options.syncPath !== false && this.appPathForRoute(route)) this.syncAppLocation(route); 16830 this.rememberRoute(route); 16831 if (route !== 'mailingAdmin') document.documentElement.classList.remove('mailing-editor-is-expanded'); 16832 this.setState({ 16833 route, 16834 filtersOpen: false, 16835 roslotMobilePanelOpen: false, 16836 mailingOpen: route === 'mailingAdmin', 16837 mailingEditorExpanded: route === 'mailingAdmin' ?this.state.mailingEditorExpanded : false, 16838 mailingInsertDialog: route === 'mailingAdmin' ?this.state.mailingInsertDialog : '', 16839 }, () => { 16840 window.scrollTo({ top: 0, behavior: 'auto' }); 16841 this.scheduleRouteWork(route); 16842 }); 16843 } 16844 async loadRemateListCatalogs(items, kind = 'active') { 16845 await Promise.all((items || []).map(async item => { 16846 const id = kind === 'cabana' ? this.cabanaRemateId(item) : this.remateRouteId(item); 16847 if (!id) return; 16848 const key = kind + ':' + id; 16849 let href = ''; 16850 try { 16851 if (kind === 'cabana') { 16852 // Cabañas does not yet support published catalogues. 16853 href = ''; 16854 } else { 16855 const suffix = kind === 'historical' ? '&historico=1' : ''; 16856 const result = await this.apiFetch('/api/catalog-publication/' + encodeURIComponent(id) + (suffix ? '?historico=1' : ''), { cache: 'no-store' }); 16857 if (result.published === true) href = '/catalogo.html?remate=' + encodeURIComponent(id) + suffix; 16858 } 16859 } catch { /* Unconfirmed availability stays hidden. */ } 16860 this.setState({ remateListCatalogs: { ...this.state.remateListCatalogs, [key]: href } }); 16861 })); 16862 } 16863 remateListCatalog(item, kind = 'active') { 16864 const id = kind === 'cabana' ? this.cabanaRemateId(item) : this.remateRouteId(item); 16865 const href = kind === 'cabana' ? '' : this.state.remateListCatalogs?.[kind + ':' + id] || ''; 16866 return { catalogAvailable: !!href, openCatalog: event => { event?.stopPropagation?.(); if (href) window.open(href, '_blank', 'noopener,noreferrer'); } }; 16867 } 16868 catalogPublicationKey(remate = this.state.currentRemate) { 16869 return (remate?.__rosganActive ? '' : 'historico-') + this.remateRouteId(remate); 16870 } 16871 catalogPublicationPath(remate = this.state.currentRemate) { 16872 return '/api/catalog-publication/' + encodeURIComponent(this.remateRouteId(remate)) + (remate?.__rosganActive ? '' : '?historico=1'); 16873 } 16874 async fetchCatalogPublication(remate) {
16875 const key = this.catalogPublicationKey(remate); 16876 this.setState({ catalogPublication: { key, published: false, loaded: false, busy: false } }); 16877 try { 16878 const result = await this.apiFetch(this.catalogPublicationPath(remate), { cache: 'no-store' }); 16879 if (this.catalogPublicationKey() === key && !this.state.catalogPublication?.busy) this.setState({ catalogPublication: { key, published: result.published === true, salesOrderDefined: result.salesOrderDefined === true, loaded: true, busy: false } }); 16880 } catch { if (this.catalogPublicationKey() === key) this.showToast('No se pudo consultar la publicación del catálogo.'); } 16881 } 16882 async toggleCatalogPublication() { 16883 const remate = this.state.currentRemate, key = this.catalogPublicationKey(remate), state = this.state.catalogPublication; 16884 if (!this.canManageUsers() || state?.key !== key || !state.loaded || state.busy) return; 16885 this.setState({ catalogPublication: { ...state, busy: true } }); 16886 try { 16887 const result = await this.apiFetch(this.catalogPublicationPath(remate), { method: 'PATCH', body: JSON.stringify({ published: !state.published }) }); 16888 if (this.catalogPublicationKey() === key) this.setState({ catalogPublication: { key, published: result.published === true, salesOrderDefined: result.salesOrderDefined === true, loaded: true, busy: false } }); 16889 this.showToast(result.published ? 'Catálogo publicado.' : 'Catálogo despublicado.'); 16890 } catch (error) { 16891 if (this.catalogPublicationKey() === key) this.setState({ catalogPublication: { ...state, busy: false } }); 16892 this.showToast(error.message || 'No se pudo guardar la publicación.'); 16893 } 16894 } 16895 openRemate(item, options = {}) { 16896 if (item?.__feriaAuction) { window.open(this.histUrl(item), '_blank', 'noopener,noreferrer'); return; } 16897 if (options.syncPath !== false) this.syncRemateLocation(item); 16898 this.setState({ route: 'remateDetail', currentRemate: item, rdQuery: '', rdSort: 'orden', rdView: 'grid', rdLightbox: null, remateLots: null }); 16899 window.scrollTo(0, 0); 16900 const loading = this.fetchRemateLots(item, options.lotId || ''); 16901 this.fetchCatalogPublication(item); 16902 return loading; 16903 } 16904 normalizeRosganActiveRemate(item = {}) { 16905 const rawDate = item.fechaHoraRemate || item.fecha_hora_remate || item.fecha || ''; 16906 const date = rawDate ?new Date(rawDate) : null; 16907 const time = date && !Number.isNaN(date.getTime()) ?date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', hour12: false }) : ''; 16908 return { 16909 ...item, 16910 __rosganActive: true, 16911 id_remate: item.id_remate || item.id, 16912 nombre_remate: item.nombre_remate || item.titulo || 'Remate Rosgan', 16913 nombre_cuenta: item.nombre_cuenta || item.consignatariaNombre || 'Rosgan', 16914 fechaHoraRemate: rawDate, 16915 hora_remate: item.hora_remate || time, 16916 ubicacion_remate: item.ubicacion_remate || [item.localidad, item.provincia].filter(Boolean).join(', ') || item.lugar || '', 16917 }; 16918 } 16919 rosganActiveLotThumbnail(source = {}) { 16920 const current = source.finalVideo || {}; 16921 const url = source.videoUrl || source.video_uri || current.url || ''; 16922 const proof = source.__verifiedThumbnail; 16923 if (!url || !proof || proof.sourceUrl !== url || proof.videoId !== current.id || proof.fileKey !== (current.fileKey || '') || Number(proof.sizeBytes) !== Number(current.sizeBytes) || proof.sha256 !== (current.sha256 || '')) return ''; 16924 return typeof proof.thumbnailUrl === 'string' && proof.thumbnailUrl.startsWith('https://') ? proof.thumbnailUrl : ''; 16925 } 16926 16927 isRosganLotPublished(lot) { 16928 return String(lot?.status ?? lot?.data?.status ?? lot?.lot?.status ?? '').trim().toLowerCase() === 'publicado'; 16929 } 16930 publicRemateLots(lots, remate = this.state.currentRemate) { 16931 const rows = Array.isArray(lots) ? lots : []; 16932 return remate?.__rosganActive ? rows.filter(lot => this.isRosganLotPublished(lot)) : rows; 16933 } 16934 publicRemateHeads(lots, remate = this.state.currentRemate) { 16935 return this.rosganRemateHeads({ ...remate, cabezas: 0 }, this.publicRemateLots(lots, remate)); 16936 } 16937 rosganLotWorkflowStatus(lot) {
16938 const value = String(lot?.status ?? lot?.data?.status ?? lot?.lot?.status ?? '').trim().toLowerCase(); 16939 return ['cargado', 'controlado', 'publicado', 'anulado', 'cerrado'].includes(value) ?value : (value || 'sin-estado'); 16940 } 16941 rosganLotIsAssigned(lot) { 16942 if (this.canManageUsers(this.state.apiUser)) return true; 16943 const id = String(lot?.consignatariaId ?? lot?.data?.consignatariaId ?? lot?.lot?.consignatariaId ?? ''); 16944 return !!id && (this.state.userConsignatarias || []).some((member) => String(member.id) === id); 16945 } 16946 canEditRosganLot(lot) { 16947 return this.canManageRosganLots(this.state.apiUser) && this.rosganLotIsAssigned(lot) && this.rosganLotWorkflowStatus(lot) === 'cargado'; 16948 } 16949 normalizeRosganActiveLot(item = {}, index = 0, remate = this.state.currentRemate) { 16950 const nested = item.data || item.lot || {}; 16951 const source = { ...nested, ...item }; 16952 const cert = source.cert || source.lotCert || source.certificate || source.nroCertificado || source.nro_certificado || source.id || ''; 16953 const male = Number(source.maleQuantity ?? source.cantidadMachos ?? 0) || 0; 16954 const female = Number(source.femaleQuantity ?? source.cantidadHembras ?? 0) || 0; 16955 const mothers = Number(source.motherQuantity ?? source.cantidadMadres ?? 0) || 0; 16956 const calves = Number(source.calfQuantity ?? source.cantidadCrias ?? 0) || 0; 16957 const quantity = Number(source.totalQuantity ?? source.quantity ?? source.cantidad ?? (male + female || mothers + calves)) || 0; 16958 const category = source.displayCategoryName || source.categoryName || source.categoriaNombre || source.nombreCategoria || source.category?.name || source.categoria?.nombre || source.categoryId || ''; 16959 const breedList = Array.isArray(source.breedNames) ?source.breedNames : Array.isArray(source.breeds) ?source.breeds.map((breed) => breed?.name || breed?.nombre || breed?.label || breed) : []; 16960 const breed = breedList.filter(Boolean).join(' · ') || source.raza || source.breedName || ''; 16961 const averageWeight = Number(source.averageWeight ?? source.pesoPromedio ?? source.peso_promedio ?? source.invernada?.averageWeight ?? source.invernada?.pesoPromedio ?? source.faena?.averageWeight ?? 0) || 0; 16962 const minimumWeight = Number(source.minimumWeight ?? source.pesoMinimo ?? source.peso_minimo ?? source.invernada?.minWeight ?? source.faena?.minWeight ?? 0) || 0; 16963 const maximumWeight = Number(source.maximumWeight ?? source.pesoMaximo ?? source.peso_maximo ?? source.invernada?.maxWeight ?? source.faena?.maxWeight ?? 0) || 0; 16964 const lotNumber = source.lotNumber ?? source.numeroLote ?? source.numero_lote ?? ''; 16965 const order = source.saleOrder ?? source.order ?? source.orden_venta ?? source.orden_final ?? source.displayOrder ?? source.orden ?? ''; 16966 const participant = (remate?.consignatariasParticipantes || []).find((entry) => String(entry?.consignatariaId || entry?.id || '') === String(source.consignatariaId || '')); 16967 const consignataria = source.consignatariaNombre || source.nombre_consignatario || participant?.consignatariaNombre || participant?.nombre || source.consignatariaId || remate?.consignatariaNombre || 'Rosgan'; 16968 const age = source.ageValue ?[source.ageValue, source.ageUnit].filter(Boolean).join(' ') : (source.edad || ''); 16969 const lotDescriptions = Array.isArray(source.lotDescriptions) ?source.lotDescriptions.filter(Boolean).join(' · ') : (source.lotDescriptions || source.descripcion_lote || ''); 16970 const weighing = source.invernada?.weighing || source.faena?.weighing || source.pesaje || ''; 16971 const desbaste = source.invernada?.desbaste ?? source.faena?.desbaste ?? source.desbaste ?? ''; 16972 return { 16973 ...source, 16974 __rosganActiveLot: true, 16975 cert, 16976 nro_certificado: cert, 16977 id_certificado: cert, 16978 lotNumber: lotNumber === '' ?'' : (Number(lotNumber) || lotNumber), 16979 saleOrder: order === '' ?'' : (Number(order) || order), 16980 orden_final: order === '' ?'' : (Number(order) || order), 16981 orden_venta: order === '' ?'' : (Number(order) || order), 16982 cantidad: quantity, 16983 cantidad_machos: male, 16984 cantidad_hembras: female, 16985 cantidad_madres: mothers, 16986 cantidad_crias: calves, 16987 nombre_categoria: category, 16988 raza: breed, 16989 peso_promedio: averageWeight, 16990 peso_minimo: minimumWeight, 16991 peso_maximo: maximumWeight, 16992 plazo: source.paymentTerm || source.plazo || '', 16993 nombre_localidad: source.locality || source.nombre_localidad || source.localidad || '', 16994 nombre_provincia: source.province || source.nombre_provincia || source.provincia || '', 16995 nombre_consignatario: consignataria, 16996 clase: source.animalClass || source.clase || '', 16997 edad: age, 16998 estado: source.bodyCondition || source.estado || '', 16999 destino: source.destination || source.destino || '', 17000 pastoreo: source.nutrition || source.pastoreo || '', 17001 descripcion_lote: lotDescriptions, 17002 denticion: source.dentition || source.denticion || '', 17003 estado_reproductivo: source.reproductiveStatus || source.estado_reproductivo || '', 17004 sanidad: source.health || source.sanidad || '', 17005 garrapata: /^no$/i.test(String(source.tickStatus || source.garrapata || '').trim()) ? '' : (source.tickStatus || source.garrapata || ''), 17006 pesaje: weighing, 17007 desbaste: desbaste === '' || desbaste === null ?'' : String(desbaste) + (String(desbaste).includes('%') ?'' : '%'), 17008 servicio: source.vientres?.service || source.servicio || '', 17009 prenez: source.vientres?.pregnancy || source.prenez || '', 17010 paricion: source.vientres?.calving || source.paricion || '', 17011 faena_subcategoria: source.faena?.subCategory || '', 17012 faena_condicion: source.faena?.conditionDisplayName || source.faena?.conditionType || '', 17013 faena_entrega: source.faena?.deliveryType || '', 17014 faena_terminacion: source.faena?.finishing || '', 17015 faena_rinde: source.faena?.estimatedYield || '', 17016 videoUrl: source.videoUrl || source.video_uri || source.finalVideo?.url || '', 17017 video_uri: source.video_uri || source.videoUrl || source.finalVideo?.url || '',
17018 thumbnailUrl: this.rosganActiveLotThumbnail(source), 17019 }; 17020 } 17021 enrichRosganActiveLotMetadata(item = {}, metadata = {}) { 17022 const nested = item.data || item.lot || {}; 17023 const source = { ...nested, ...item }; 17024 const cert = String(source.cert || source.lotCert || source.certificate || source.nroCertificado || source.nro_certificado || source.id || ''); 17025 const stored = metadata?.[cert] && typeof metadata[cert] === 'object' ?metadata[cert] : {}; 17026 const first = (...values) => values.find((value) => value !== undefined && value !== null && String(value).trim() !== '') || ''; 17027 return { 17028 ...item, 17029 locality: source.locationFromEstablishment ? source.locality : first(source.establishment?.locality, source.locality, source.nombre_localidad, source.localidad, stored.locality), 17030 province: source.locationFromEstablishment ? source.province : first(source.establishment?.province, source.province, source.nombre_provincia, source.provincia, stored.province), 17031 paymentTerm: first(source.paymentTerm, source.plazo, source.commercial?.paymentTerm, source.sale?.paymentTerm, stored.paymentTerm), 17032 }; 17033 } 17034 openRosganActiveRemate(item, options = {}) { 17035 const remate = this.normalizeRosganActiveRemate(item); 17036 if (options.syncPath !== false) this.syncRemateLocation(remate); 17037 this.setState({ route: 'remateDetail', currentRemate: remate, rdQuery: '', rdSort: 'orden', rdView: 'grid', rdLightbox: null, remateLots: null, remateLotAlerts: {}, remateLotAlertBusy: '' }); 17038 window.scrollTo(0, 0); 17039 const loading = this.fetchRosganActiveLots(remate, options.lotId || ''); 17040 this.fetchCatalogPublication(remate); 17041 return loading; 17042 } 17043 async openRosganActiveRemateById(remateId, lotId = '') { 17044 const requestedPath = window.location.pathname; 17045 try { 17046 const response = await this.apiFetch('/api/Remates/' + encodeURIComponent(remateId)); 17047 if (window.location.pathname !== requestedPath) return; 17048 const item = response?.data ?? response; 17049 if (!item || item?.cabanasConfig) throw new Error('No es un remate Rosgan activo.'); 17050 return this.openRosganActiveRemate(item, { lotId, syncPath: false }); 17051 } catch (error) { 17052 if (window.location.pathname !== requestedPath) return; 17053 if (Number(error?.status) !== 404) { 17054 this.setState({ route: 'remateDetail', currentRemate: { id_remate: remateId, __rosganActive: true }, remateLots: [], remateLotsLoading: false, remateLotsReal: false, rdLightbox: null }); 17055 this.showToast(error?.message || 'No se pudo cargar el remate desde el API actual.'); 17056 return; 17057 } 17058 const historical = (this.state.remates || []).find((candidate) => this.remateRouteId(candidate) === String(remateId)) || { id_remate: remateId }; 17059 return this.openRemate(historical, { lotId, syncPath: false }); 17060 } 17061 } 17062 async fetchRosganActiveLots(item, lotId = '') { 17063 const remateId = this.remateRouteId(item); 17064 const requestedPath = window.location.pathname; 17065 const request = {}; 17066 this._activeLotsRequest = request; 17067 const isCurrent = () => this._activeLotsRequest === request && window.location.pathname === requestedPath; 17068 this.setState({ remateLotsLoading: true, remateLotsReal: false }); 17069 try { 17070 let thumbnailManifest = {}, lotMetadata = {}, loaded = false; 17071 const thumbnailCacheKey = 'rosgan:verified-thumbnails:v1:' + remateId; 17072 try { 17073 const cached = JSON.parse(sessionStorage.getItem(thumbnailCacheKey) || 'null'); 17074 if (cached && Date.now() - cached.at < 3600000) thumbnailManifest = cached.manifest || {}; 17075 } catch {} 17076 const enrich = (lot, index) => { 17077 const source = lot?.data || lot?.lot || lot || {}; 17078 const cert = source.cert || source.lotCert || source.certificate || source.nroCertificado || source.nro_certificado || source.id || ''; 17079 const proof = thumbnailManifest?.[String(cert)]; 17080 const enriched = this.enrichRosganActiveLotMetadata({ ...lot, __verifiedThumbnail: proof && typeof proof === 'object' ? proof : null, thumbnailUrl: '', thumbnail: '', imagen: '' }, lotMetadata); 17081 return this.normalizeRosganActiveLot(enriched, index, item); 17082 }; 17083 const refreshExtras = () => { 17084 if (!loaded || !isCurrent()) return; 17085 this.setState((previous) => ({
17086 remateLots: (previous.remateLots || []).map(enrich), 17087 rdLightbox: previous.rdLightbox ? enrich(previous.rdLightbox, 0) : null, 17088 })); 17089 }; 17090 // Optional assets must not keep the catalog behind the loading screen. 17091 fetch('/api/remates/' + encodeURIComponent(remateId) + '/video-thumbnails', { cache: 'no-store' }) 17092 .then(result => result.ok ? result.json() : {}) 17093 .then(value => { 17094 thumbnailManifest = value || {}; 17095 try { sessionStorage.setItem(thumbnailCacheKey, JSON.stringify({ at: Date.now(), manifest: thumbnailManifest })); } catch {} 17096 refreshExtras(); 17097 }).catch(() => {}); 17098 fetch('/assets/remates/' + encodeURIComponent(remateId) + '/lot-metadata.json', { cache: 'no-store' }) 17099 .then(result => result.ok ? result.json() : {}) 17100 .then(value => { lotMetadata = value || {}; refreshExtras(); }).catch(() => {}); 17101 const response = await this.apiFetch('/api/remates/' + encodeURIComponent(remateId) + '/rosgan-lots'); 17102 const lots = this.publicRemateLots(this.rosganArray(response), item).map(enrich); 17103 if (!isCurrent()) return; 17104 loaded = true; 17105 const targetLot = lotId ?lots.find((candidate) => this.remateLotRouteId(candidate) === String(lotId)) || null : null; 17106 this.setState({ remateLots: lots, currentRemate: this.normalizeRosganActiveRemate(item), rdLightbox: targetLot, remateLotsReal: true, remateLotsLoading: false, cabanaLotVideoPlaying: false, cabanaLotVideoProgress: 0, cabanaLotVideoDuration: 0, cabanaLotVideoVolume: 0, cabanaLotVideoMuted: true }, () => { 17107 if (this.state.route === 'rosganTvLower') this.ensureRosganOpsSelectedVideo(); 17108 if (this.state.user) this.loadRemateLotAlerts(remateId); 17109 if (targetLot) { this.fetchRosganLotVideos(remateId, this.remateLotRouteId(targetLot)); this.fetchRosganRepresentative(targetLot); } 17110 }); 17111 } catch (error) { 17112 if (!isCurrent()) return; 17113 this.setState({ remateLots: [], remateLotsReal: false, remateLotsLoading: false }); 17114 this.showToast(error?.message || 'No se pudieron cargar los lotes del remate.'); 17115 } 17116 } 17117 rosganOpsStorageKey(remate = this.state.currentRemate) { 17118 return 'rosgan:remate-operations:' + (this.remateRouteId(remate) || 'unknown'); 17119 } 17120 normalizeRosganSalesOrderSchedule(value) { 17121 const source = value && typeof value === 'object' && !Array.isArray(value) ?value : {}; 17122 const seen = new Set(); 17123 const entries = (Array.isArray(source.entries) ?source.entries : []).slice(0, 1500).flatMap((raw) => { 17124 const cert = String(raw?.cert ?? '').trim().slice(0, 100); 17125 const alias = String(raw?.alias ?? '').trim().slice(0, 160); 17126 const averageSeconds = Number(raw?.averageSeconds); 17127 const estimatedStartSeconds = Number(raw?.estimatedStartSeconds); 17128 if (!cert || !alias || seen.has(cert) || !Number.isInteger(averageSeconds) || averageSeconds < 1 || averageSeconds > 86400 || !Number.isInteger(estimatedStartSeconds) || estimatedStartSeconds < 0 || estimatedStartSeconds > 604800) return []; 17129 seen.add(cert); 17130 return [{ cert, alias, averageSeconds, estimatedStartSeconds }]; 17131 }); 17132 const startSeconds = Number(source.startSeconds); 17133 const endSeconds = Number(source.endSeconds); 17134 return { 17135 fileName: String(source.fileName || '').trim().slice(0, 180), 17136 importedAt: String(source.importedAt || '').trim().slice(0, 50), 17137 startSeconds: Number.isInteger(startSeconds) && startSeconds >= 0 && startSeconds < 86400 ?startSeconds : null, 17138 endSeconds: Number.isInteger(endSeconds) && endSeconds >= 0 && endSeconds <= 604800 ?endSeconds : null, 17139 entries, 17140 }; 17141 } 17142 defaultRosganOpsState(remate = this.state.currentRemate) { 17143 const rawContacts = remate?.telefonos || remate?.phones || remate?.contactPhones || remate?.contactos || remate?.contactoTelefono || remate?.contactPhone || remate?.contacto?.telefono || remate?.contact?.phone || ''; 17144 const contacts = Array.isArray(rawContacts) 17145 ?rawContacts.map((item) => typeof item === 'string' ?item : (item?.telefono || item?.phone || '')).filter(
17145Boolean).join(' | ') 17146 : String(rawContacts || '').trim(); 17147 return { commercial: {}, ratings: {}, salesOrder: this.normalizeRosganSalesOrderSchedule(null), tv: { selectedCert: '', visible: true, logosVisible: true, lowerThirdVisible: true, contactsVisible: true, contacts, lotVersions: {}, contactsByConsignataria: {}, vmixBrowserInputNumber: 0, vmixBrowserInputKey: '' } }; 17148 } 17149 normalizeRosganOpsState(value, remate = this.state.currentRemate) { 17150 const base = this.defaultRosganOpsState(remate); 17151 const source = value && typeof value === 'object' ?value : {}; 17152 return { 17153 commercial: source.commercial && typeof source.commercial === 'object' ?source.commercial : {}, 17154 ratings: Object.fromEntries(Object.entries(source.ratings && typeof source.ratings === 'object' ?source.ratings : {}).flatMap(([cert, value]) => { 17155 const rating = Number(value); 17156 return cert && Number.isInteger(rating) && rating >= 1 && rating <= 6 ?[[String(cert), rating]] : []; 17157 })), 17158 salesOrder: this.normalizeRosganSalesOrderSchedule(source.salesOrder), 17159 tv: { ...base.tv, ...(source.tv && typeof source.tv === 'object' ?source.tv : {}) }, 17160 }; 17161 } 17162 loadRosganOpsState(remate = this.state.currentRemate) { 17163 try { return this.normalizeRosganOpsState(JSON.parse(localStorage.getItem(this.rosganOpsStorageKey(remate)) || '{}'), remate); } 17164 catch { return this.defaultRosganOpsState(remate); } 17165 } 17166 saveRosganOpsState(next, remate = this.state.currentRemate) { 17167 const normalized = this.normalizeRosganOpsState(next, remate); 17168 const patch = window.RosganOperationsPatch.diff(this.loadRosganOpsState(remate), normalized); 17169 if(this.remateRouteId(remate)===this.remateRouteId(this.state.currentRemate)){this._rosganOpsSignature=JSON.stringify(normalized);this._rosganOpsLocalUpdatedAt=Date.now();} 17170 try { 17171 localStorage.setItem(this.rosganOpsStorageKey(remate), JSON.stringify(normalized)); 17172 window.dispatchEvent(new CustomEvent('rosgan:remate-operations', { detail: { remateId: this.remateRouteId(remate) } })); 17173 } catch {} 17174 if (this.remateRouteId(remate) === this.remateRouteId(this.state.currentRemate)) { 17175 this._rosganOpsLocalUpdatedAt = Date.now(); 17176 this.setState((previous) => ({ rosganOpsDrafts: normalized.commercial, rosganOpsConfig: normalized.tv, rosganSalesOrderRatings: normalized.ratings, rosganSalesOrderSchedule: normalized.salesOrder, rosganOpsPulse: Number(previous.rosganOpsPulse || 0) + 1 })); 17177 } 17178 if(Object.keys(patch).length)this.queueRosganOpsRemoteSave(patch, remate); 17179 return normalized; 17180 } 17181 rosganOpsRemotePath(remate = this.state.currentRemate, tvOnly = false) { 17182 const id = this.remateRouteId(remate); 17183 return '/api/remate-operations/' + encodeURIComponent(id) + (tvOnly ?'?scope=tv' : ''); 17184 } 17185 normalizeRosganOpsRemotePayload(data, remate = this.state.currentRemate, tvOnly = false) { 17186 const root = data?.data || data?.item || data || {}; 17187 if (tvOnly) { 17188 const local = this.loadRosganOpsState(remate); 17189 return this.normalizeRosganOpsState({ commercial: local.commercial, ratings: local.ratings, salesOrder: local.salesOrder, tv: root.tv || root.state?.tv || {} }, remate); 17190 } 17191 return this.normalizeRosganOpsState(root.state || root, remate); 17192 } 17193 rosganOpsPendingWrites(remate, pending) { 17194 const key=this.rosganOpsStorageKey(remate)+':pending-writes'; 17195 this._rosganOpsWriterId ||= crypto.randomUUID(); 17196 try{ 17197 const writers=JSON.parse(localStorage.getItem(key)||'{}'),now=Date.now(); 17198 for(const id of Object.keys(writers))if(writers[id]<now)delete writers[id]; 17199 if(pending===true)writers[this._rosganOpsWriterId]=now+60000; 17200 if(pending===false)delete writers[this._rosganOpsWriterId]; 17201 if(pending!==undefined)localStorage.setItem(key,JSON.stringify(writers)); 17202 return Object.keys(writers).length>0; 17203 }catch{return false;} 17204 } 17205 async fetchRosganOpsRemoteState(options = {}) { 17206 if (!this.canOpenRosganOperation(this.state.currentRemate, 'commercial')) return null; 17207 if (!['rosganSalesOrder', 'rosganCommercial', 'rosganPriceBoard', 'rosganTvLower', 'rosganTvConfig'].includes(this.state.route) || !this.state.currentRemate) return null; 17208 const now = Date.now(); 17209 if (!options.force && this._lastRosganOpsRemoteFetchAt && now - this._lastRosganOpsRemoteFetchAt < 1800) return null; 17210 if (!options.force && this._rosganOpsLocalUpdatedAt && now - this._rosganOpsLocalUpdatedAt < 1400) return null; 17211 this._lastRosganOpsRemoteFetchAt = now; 17212 const remate = this.state.currentRemate; 17213 const tvOnly = this.state.route === 'rosganTvLower'; 17214 if(this.rosganOpsPendingWrites(remate))return null; 17215 const localSignature=JSON.stringify(this.loadRosganOpsState(remate)); 17216 const write = this._rosganOpsWrites?.[this.remateRouteId(remate)]; 17217 if (write && (write.busy || write.revision !== write.acknowledged)) return null; 17218 const revision = write?.revision || 0; 17219 const request = this._rosganOpsReadSequence = (this._rosganOpsReadSequence || 0) + 1; 17220 try { 17221 const data = await this.apiFetch(this.rosganOpsRemotePath(remate, tvOnly), { cache: 'no-store' }); 17222 if(this.rosganOpsPendingWrites(remate)||JSON.stringify(this.loadRosganOpsState(remate))!==localSignature)return null; 17223 const latestWrite = this._rosganOpsWrites?.[this.remateRouteId(remate)]; 17224 if (request !== this._rosganOpsReadSequence || (latestWrite?.revision || 0) !== revision || latestWrite?.busy) return null; 17225 if (this._rosganOpsLocalUpdatedAt > now) return null; 17226 if (this.remateRouteId(remate) !== this.remateRouteId(this.state.currentRemate)) return null; 17227 const next = this.normalizeRosganOpsRemotePayload(data, remate, tvOnly); 17228 this.syncRosganTvLotVersions(next.tv, remate); 17229 const signature = JSON.stringify(next); 17230 if (signature && signature !== this._rosganOpsSignature) { 17231 this._rosganOpsSignature = signature; 17232 try { localStorage.setItem(this.rosganOpsStorageKey(remate), JSON.stringify(next)); } catch {} 17233 this.setState((previous) => ({ rosganOpsDrafts: next.commercial, rosganOpsConfig: next.tv, rosganSalesOrderRatings: next.ratings, rosganSalesOrderSchedule: next.salesOrder, rosganOpsPulse: Number(previous.rosganOpsPulse || 0) + 1 }), () => { 17234 if (this.state.route === 'rosganTvLower') this.ensureRosganOpsSelectedVideo(); 17235 }); 17236 } 17237 return next; 17238 } catch (error) { 17239 if (!options.silent && error?.status && error.status !== 404) this.showToast(error.message || 'No se pudo sincronizar la operación del remate.'); 17240 return null; 17241 } 17242 } 17243 queueRosganOpsRemoteSave(state, remate = this.state.currentRemate) { 17244 const id = this.remateRouteId(remate); 17245 this._rosganOpsWrites ||= {}; 17246 const write = this._rosganOpsWrites[id] ||= { revision: 0, acknowledged: 0 }; 17247 write.revision += 1; 17248 this.rosganOpsPendingWrites(remate,true); 17249 // Keep the most recent selection even while a previous PUT is still in flight. 17250 write.pending = window.RosganOperationsPatch.merge(write.pending, state); 17251 if (write.busy) return; 17252 clearTimeout(write.timer); 17253 write.timer = setTimeout(() => { const pending=write.pending;write.pending=null;this.saveRosganOpsRemoteState(pending, remate); }, 350); 17254 } 17255 async saveRosganOpsRemoteState(state, remate = this.state.currentRemate) { 17256 if (!remate || !this.canManageCabanaPrivatePages()) return; 17257 const id = this.remateRouteId(remate); 17258 this._rosganOpsWrites ||= {}; 17259 const write = this._rosganOpsWrites[id] ||= { revision: 0, acknowledged: 0 }; 17260 if (write.busy) { write.pending = window.RosganOperationsPatch.merge(write.pending, state); return; } 17261 write.busy = true; 17262 this.rosganOpsPendingWrites(remate,true); 17263 const revision = write.revision; 17264 try { 17265 await this.apiFetch(this.rosganOpsRemotePath(remate), { method: 'PATCH', body: JSON.stringify({ protocol:2, patch:state }) }); 17266 write.acknowledged = revision; 17267 // Polling compares against the actual local state, never a response snapshot. 17268 } catch (error) { 17269 write.pending = window.RosganOperationsPatch.merge(state,write.pending); 17270 if (error?.status && error.status !== 404) this.showToast(error.message || 'No se pudo guardar la operación del remate.'); 17271 write.retry = true; 17272 } finally { 17273 write.busy = false;
17274 if(!write.pending)this.rosganOpsPendingWrites(remate,false); 17275 if (write.pending) { 17276 const retry=write.retry;write.retry=false;clearTimeout(write.timer); 17277 write.timer=setTimeout(()=>{const pending=write.pending;write.pending=null;this.saveRosganOpsRemoteState(pending,remate);},retry?3000:0); 17278 } 17279 } 17280 } 17281 syncRosganOpsExternalState() { 17282 if (!['rosganSalesOrder', 'rosganCommercial', 'rosganPriceBoard', 'rosganTvLower', 'rosganTvConfig'].includes(this.state.route) || !this.state.currentRemate) return; 17283 const next = this.loadRosganOpsState(this.state.currentRemate); 17284 this.syncRosganTvLotVersions(next.tv); 17285 const signature = JSON.stringify(next); 17286 if (signature === this._rosganOpsSignature) return; 17287 this._rosganOpsSignature = signature; 17288 this.setState((previous) => ({ rosganOpsDrafts: next.commercial, rosganOpsConfig: next.tv, rosganSalesOrderRatings: next.ratings, rosganSalesOrderSchedule: next.salesOrder, rosganOpsPulse: Number(previous.rosganOpsPulse || 0) + 1 }), () => { 17289 if (this.state.route === 'rosganTvLower') this.ensureRosganOpsSelectedVideo(); 17290 }); 17291 } 17292 rosganOpsRoute(action) { 17293 return ({ stats:'rosganStats', commercial: 'rosganCommercial', prices: 'rosganPriceBoard', tv: 'rosganTvLower', tvConfig: 'rosganTvConfig' })[action] || 'remateDetail'; 17294 } 17295 async updateRosganAirtime(body){ 17296 if(this._rosganAirtimeSaving)return; 17297 const id=this.remateRouteId(this.state.currentRemate); 17298 this._rosganAirtimeSaving=true; 17299 try{ 17300 const result=await this.apiFetch('/api/remate-operations/'+encodeURIComponent(id)+'/airtime',{method:'PATCH',body:JSON.stringify(body)}); 17301 if(this.remateRouteId(this.state.currentRemate)===id)this.setState({rosganAirtime:{...result.data,receivedAt:Date.now()},rosganTimeEdit:null}); 17302 }catch(error){this.showToast(error.message||'No se pudo guardar el tiempo.');} 17303 finally{this._rosganAirtimeSaving=false;} 17304 } 17305 saveRosganManualTime(cert){ 17306 const value=String(this.state.rosganTimeEdit?.value||'').trim(); 17307 if(!/^\d{1,2}:[0-5]\d:[0-5]\d$/.test(value)){this.showToast('Ingresá el tiempo como HH:MM:SS, por ejemplo 00:02:30.');return;} 17308 const [h,m,s]=value.split(':').map(Number); 17309 this.updateRosganAirtime({action:'set',cert,milliseconds:(h*3600+m*60+s)*1000}); 17310 } 17311 async refreshRosganLotInterest(){ 17312 if(this.state.route!=='rosganCommercial'||this._rosganInterestBusy)return; 17313 const id=this.remateRouteId(this.state.currentRemate); 17314 if(!id||(this._rosganInterestId===id&&Date.now()-(this._rosganInterestAt||0)<10000))return; 17315 this._rosganInterestId=id;this._rosganInterestAt=Date.now();this._rosganInterestBusy=true; 17316 try{ 17317 const result=await this.apiFetch('/api/remate-operations/'+encodeURIComponent(id)+'/interest'); 17318 if(this.remateRouteId(this.state.currentRemate)===id&&JSON.stringify(result.data)!==JSON.stringify(this.state.rosganLotInterest))this.setState({rosganLotInterest:result.data}); 17319 }catch(error){if(this.remateRouteId(this.state.currentRemate)===id)this.setState({rosganLotInterest:null});} 17320 finally{this._rosganInterestBusy=false;} 17321 } 17322 async refreshRosganAirtime() { 17323 const route=this.state.route,id=this.remateRouteId(this.state.currentRemate); 17324 if(!id||this._rosganAirtimeSaving||this._rosganAirtimeBusy||!['rosganCommercial','rosganTvLower'].includes(route))return; 17325 const path='/api/remate-operations/'+encodeURIComponent(id)+'/airtime'; 17326 this._rosganAirtimeBusy=true; 17327 try { 17328 if(route==='rosganTvLower'){ 17329 const outputId=new URLSearchParams(location.search).get('airtime'); 17330 if(!outputId)return; 17331 this._rosganAirtimeSession ||= (crypto.randomUUID?crypto.randomUUID():Date.now()+'-'+Math.random().toString(36).slice(2)); 17332 const plateTv=this.loadRosganOpsState().tv; 17333 const certs=plateTv.videoVisible===false||(plateTv.visible!==false&&['waiting','end'].includes(plateTv.fullScreenPlate))?[]:this._rosganTvAirSlots?.length?this._rosganTvAirSlots.map(slot=>String(slot.cert)):this._rosganTvDecks?.onAirCert?[String(this._rosganTvDecks.onAirCert)]:[]; 17334 await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({outputId,sessionId:this._rosganAirtimeSession,certs}),signal:AbortSignal.timeout(2500)}); 17335 } else { 17336 const result=await this.apiFetch(path,{signal:AbortSignal.timeout(3000)}); 17337 if(!this._rosganAirtimeSaving&&this.remateRouteId(this.state.currentRemate)===id){ 17338 const previous=this.state.rosganAirtime,next={...result.data,receivedAt:Date.now()}; 17339 const signature=value=>JSON.stringify([value?.remateId,value?.totals,value?.running,value?.reason,value?.outputConnected]); 17340 if(Date.now()-(this._rosganPriceTypingAt||0)>400&&(next.running?.length||!previous?.receivedAt||signature(next)!==signature(previous)))this.setState({rosganAirtime:next}); 17341 else if(previous&&signature(next)===signature(previous))Object.assign(previous,next); 17342 } 17343 } 17344 }catch(error){if(route==='rosganCommercial')this.setState({rosganAirtime:this.state.rosganAirtime?{...this.state.rosganAirtime,receivedAt:0,running:[]}:null});} 17345 finally {this._rosganAirtimeBusy=false;} 17346 } 17347 rosganOpsSuffix(action) { 17348 return ({ stats:'estadisticas', commercial: 'comercial', prices: 'precios', tv: 'tv', tvConfig: 'tv/config' })[action] || ''; 17349 } 17350 async fetchRosganOfficialPrices(remate,options={}) {
17351 const key=this.remateRouteId(remate),current=this.state.rosganOfficialPrices?.[key]; 17352 if(!key||current?.loading||(!options.force&¤t?.groups?.length))return; 17353 this.setState({rosganOfficialPrices:{...(this.state.rosganOfficialPrices||{}),[key]:{...current,loading:true,error:''}}}); 17354 try{ 17355 const candidates=await this.unwrap(window.RosganNetPriceResults.rematesUrl(remate)); 17356 const match=window.RosganNetPriceResults.findRemate(candidates,remate); 17357 if(!match?.id_remate)throw new Error('RosganNet no encontró un remate equivalente por fecha y nombre.'); 17358 const payload=await this.unwrap(window.RosganNetPriceResults.resultsUrl(match.id_remate)); 17359 const groups=window.RosganNetPriceResults.groups(payload); 17360 if(!groups.length)throw new Error('RosganNet todavÃa no publicó resultados para este remate.'); 17361 this.setState({rosganOfficialPrices:{...(this.state.rosganOfficialPrices||{}),[key]:{loading:false,error:'',groups,rosganNetRemateId:String(match.id_remate),loadedAt:Date.now()}}}); 17362 }catch(error){ 17363 this.setState({rosganOfficialPrices:{...(this.state.rosganOfficialPrices||{}),[key]:{...current,loading:false,error:error?.message||'No se pudieron cargar los resultados oficiales.'}}}); 17364 } 17365 } 17366 showRematePanelPicker() { 17367 if (!this.canManageRosganLots() || this._rematePanelDialog?.isConnected) return; 17368 const previousFocus = document.activeElement; 17369 const dialog = document.createElement('dialog'); 17370 dialog.id = 'remate-panel-picker'; 17371 dialog.setAttribute('aria-labelledby', 'remate-panel-title'); 17372 dialog.setAttribute('aria-describedby', 'remate-panel-description'); 17373 dialog.style.cssText = 'width:min(600px,calc(100vw - 28px));max-height:calc(100dvh - 32px);padding:0;box-sizing:border-box;border:1px solid #dce7f2;border-radius:22px;background:#fff;color:#112033;box-shadow:0 28px 90px #001d3b45;font-family:inherit;overflow:auto;'; 17374 dialog.innerHTML = `<style> 17375 #remate-panel-picker::backdrop{background:rgba(0,25,52,.52);backdrop-filter:blur(4px)} 17376 #remate-panel-picker .rp-heading{padding:24px 24px 18px;border-bottom:1px solid #e5edf5;position:sticky;top:0;background:#fff;z-index:1} 17377 #remate-panel-picker .rp-list{padding:16px;display:grid;gap:10px} 17378 #remate-panel-picker .rp-choice{width:100%;display:flex;align-items:center;gap:14px;padding:17px;text-align:left;border:1px solid #dce7f2;border-radius:14px;background:#f8fafd;color:#112033;font:inherit;cursor:pointer;transition:background .15s,border-color .15s} 17379 #remate-panel-picker .rp-choice:hover{background:#edf5ff;border-color:#0a5ca8} 17380 #remate-panel-picker button:focus-visible{outline:3px solid #0a5ca8;outline-offset:2px} 17381 #remate-panel-picker .rp-icon{width:42px;height:42px;border-radius:12px;background:#e7f0fa;display:grid;place-items:center;flex:none} 17382 #remate-panel-picker strong{display:block;font-size:15px;line-height:1.4;overflow-wrap:anywhere} 17383 #remate-panel-picker small{display:block;margin-top:5px;color:#65758a;font-size:12px;line-height:1.5} 17384 #remate-panel-picker .rp-message{margin:0;padding:22px 8px;color:#65758a;font-size:14px;line-height:1.5} 17385 @media(max-width:480px){#remate-panel-picker .rp-heading{padding:20px 18px 16px}#remate-panel-picker .rp-list{padding:12px}#remate-panel-picker .rp-choice{padding:14px;gap:10px}} 17386 </style><header class="rp-heading"><div style="display:flex;align-items:center;justify-content:space-between;gap:14px"><h2 id="remate-panel-title" style="margin:0;font-size:23px;letter-spacing:-.02em"></h2><button type="button" data-close aria-label="Cerrar selector de remates" style="width:36px;height:36px;border:1px solid #dce7f2;border-radius:50%;background:#fff;color:#12304d;font-size:24px;cursor:pointer;flex:none">Ã</button></div><p id="remate-panel-description" style="margin:9px 0 0;color:#65758a;font-size:14px">Elegà el remate para abrir su panel.</p></header><div class="rp-list" data-remates></div>`; 17387 dialog.querySelector('h2').textContent = this.isRosganProfile() ? 'Panel Consignatario' : 'Panel Auditor'; 17388 dialog.querySelector('[data-close]').onclick = () => dialog.close(); 17389 dialog.addEventListener('click', event => { 17390 if (event.target !== dialog) return; 17391 const r = dialog.getBoundingClientRect(); 17392 if (event.clientX < r.left || event.clientX > r.right || event.clientY < r.top || event.clientY > r.bottom) dialog.close(); 17393 }); 17394 dialog.addEventListener('close', () => { 17395 this._rematePanelRequest = (this._rematePanelRequest || 0) + 1; 17396 this._rematePanelDialog = null; 17397 this.setState({ rematePanelPickerOpen: false, rematePanelPickerLoading: false }); 17398 dialog.remove(); 17399 if (previousFocus?.isConnected) previousFocus.focus(); 17400 }, { once: true }); 17401 this._rematePanelDialog = dialog; 17402 document.body.appendChild(dialog); 17403 dialog.showModal(); 17404 this.loadRematePanelPicker(); 17405 } 17406 renderRematePanelDialog() { 17407 const dialog = this._rematePanelDialog; 17408 if (!dialog?.isConnected) return; 17409 if (this.state.rematePanelScopeUid !== this.state.user?.uid || !this.canManageRosganLots()) { dialog.close(); return; } 17410 const list = dialog.querySelector('[data-remates]'); 17411 list.replaceChildren(); 17412 const message = text => { const p = document.createElement('p'); p.className = 'rp-message'; p.setAttribute('role', 'status'); p.textContent = text; list.appendChild(p); };
17413 if (this.state.rematePanelPickerLoading) { message('Cargando tus rematesâ¦'); return; } 17414 if (this.state.rematePanelPickerError) { 17415 message(this.state.rematePanelPickerError); 17416 const retry = document.createElement('button'); retry.className = 'rp-choice'; retry.textContent = 'Reintentar'; retry.onclick = () => this.loadRematePanelPicker(); list.appendChild(retry); return; 17417 } 17418 const items = [...(this.state.rematePanelItems || [])].sort((a, b) => (Date.parse(a.fechaHoraRemate) || Infinity) - (Date.parse(b.fechaHoraRemate) || Infinity)); 17419 if (!items.length) { message('No tenés remates disponibles para administrar.'); return; } 17420 for (const item of items) { 17421 const button = document.createElement('button'); button.type = 'button'; button.className = 'rp-choice'; 17422 button.innerHTML = '<span class="rp-icon"><img src="/assets/icons/lucide/clipboard-list.svg" width="21" height="21" alt=""></span><span style="flex:1;min-width:0"><strong></strong><small></small></span><span aria-hidden="true" style="color:#0a5ca8;font-size:21px">â</span>'; 17423 button.querySelector('strong').textContent = item.titulo || item.nombre_remate || item.nombre || this.remateRouteId(item); 17424 const date = new Date(item.fechaHoraRemate); 17425 button.querySelector('small').textContent = [Number.isNaN(date.getTime()) ? '' : date.toLocaleDateString('es-AR', { day:'numeric', month:'long', year:'numeric' }), item.localidad].filter(Boolean).join(' · '); 17426 button.onclick = () => this.openSelectedRematePanel(this.remateRouteId(item)); 17427 list.appendChild(button); 17428 } 17429 } 17430 rosganPanelRemates(remates, relations = []) { 17431 if (!this.canManageRosganLots()) return []; 17432 const admin = this.canManageUsers(); 17433 const keys = new Set(relations.filter(r => r && r.isActive !== false).flatMap(r => [r.id, r.consignatariaId, r.slug, r.ref, r.name]).filter(Boolean).map(v => this.consKey(v))); 17434 const seen = new Set(); 17435 return this.rosganUpcomingRemates(remates).filter(item => { 17436 const id = this.remateRouteId(item); 17437 if (!id || seen.has(id)) return false; 17438 const participants = Array.isArray(item.consignatariasParticipantes) ? item.consignatariasParticipantes : []; 17439 const refs = [item.consignatariaId, item.consignatariaNombre, ...participants.flatMap(p => [p.consignatariaId, p.id, p.consignatariaNombre, p.name])]; 17440 const allowed = admin || (keys.size > 0 && (this.consKey(item.modoConsignataria) === 'todos' || refs.some(ref => ref && keys.has(this.consKey(ref))))); 17441 if (allowed) seen.add(id); 17442 return allowed; 17443 }); 17444 } 17445 async loadRematePanelPicker() { 17446 if (!this.canManageRosganLots()) return; 17447 const uid = this.state.user.uid, epoch = this._sessionEpoch || 0; 17448 const request = this._rematePanelRequest = (this._rematePanelRequest || 0) + 1; 17449 const current = () => request === this._rematePanelRequest && uid === this.state.user?.uid && epoch === (this._sessionEpoch || 0) && this.canManageRosganLots(); 17450 this.setState({ rematePanelPickerOpen: true, rematePanelPickerLoading: true, rematePanelPickerError: '', rematePanelScopeUid: uid, rematePanelItems: [], rematePanelSelectedId: '' }); 17451 this.renderRematePanelDialog(); 17452 try { 17453 const [response, relations] = await Promise.all([ 17454 this.apiFetch('/api/Remates', { cache: 'no-store' }), 17455 this.canManageUsers() ? Promise.resolve([]) : this.fetchUserConsignataria(uid), 17456 ]); 17457 if (!current()) return; 17458 if (!Array.isArray(relations)) throw new Error('No se pudieron verificar tus consignatarias. Reintentá.'); 17459 const items = this.rosganPanelRemates(this.rosganArray(response), relations); 17460 this.setState({ rematePanelItems: items, rematePanelPickerLoading: false, rematePanelSelectedId: items.length === 1 ? this.remateRouteId(items[0]) : '' }); 17461 this.renderRematePanelDialog(); 17462 } catch (error) { 17463 if (current()) this.setState({ rematePanelPickerLoading: false, rematePanelPickerError: error?.message || 'No se pudieron cargar tus remates.' }); 17464 if (current()) this.renderRematePanelDialog(); 17465 } 17466 } 17467 openSelectedRematePanel(selectedId = this.state.rematePanelSelectedId) { 17468 if (!this.canManageRosganLots() || this.state.rematePanelPickerLoading || this.state.rematePanelScopeUid !== this.state.user?.uid) return; 17469 const relations = [...(this.state.userConsignatarias || []), this.state.userConsignataria].filter(
17469Boolean); 17470 const item = this.rosganPanelRemates(this.state.rematePanelItems, relations).find(r => this.remateRouteId(r) === selectedId); 17471 if (!item) { this.showToast('Seleccioná un remate disponible para tu cuenta.'); return; } 17472 this.setState({ rematePanelPickerOpen: false }); 17473 this._rematePanelDialog?.close(); 17474 this.openRosganAuditor(item); 17475 } 17476 async openRosganAuditorById(remateId) { 17477 if (!this.state.apiUser) { this.setState({ rosganAuditorRouteError: 'No se pudo validar la sesión. Reintentá para abrir el panel.' }); return; } 17478 if (this._auditorLoadingId === String(remateId)) return; 17479 this._auditorLoadingId = String(remateId); 17480 const request = this._auditorOpenRequest = (this._auditorOpenRequest || 0) + 1; 17481 this.setState({ route: 'rosganAuditor', rosganAuditorRouteError: '' }); 17482 const controller = new AbortController(); 17483 const timeout = setTimeout(() => controller.abort(), 20000); 17484 try { 17485 const response = await this.apiFetch('/api/Remates/' + encodeURIComponent(remateId), { signal: controller.signal }); 17486 if (request !== this._auditorOpenRequest || this.rematePathTarget()?.action !== 'auditor' || this.rematePathTarget()?.remateId !== String(remateId)) return; 17487 const item = response?.data ?? response; 17488 if (!item || item.cabanasConfig) throw new Error('Este panel corresponde a remates Rosgan.'); 17489 this.openRosganAuditor(item, { syncPath: false }); 17490 } catch (error) { if (request === this._auditorOpenRequest && this.rematePathTarget()?.action === 'auditor') this.setState({ rosganAuditorRouteError: error.message || 'No se pudo abrir el panel.' }); } 17491 finally { clearTimeout(timeout); if (request === this._auditorOpenRequest) this._auditorLoadingId = ''; } 17492 } 17493 openRosganAuditor(item, options = {}) { 17494 if (!this.state.user || !this.state.apiUser) { this.showToast('Iniciá sesión para consultar tus lotes.'); return; } 17495 const remate = this.normalizeRosganActiveRemate(item); 17496 this.setState({ route: 'rosganAuditor', rosganAuditorRouteError: '' }); 17497 this._auditorPanel?.close(); 17498 if (options.syncPath !== false) this.syncRemateSectionLocation(remate, 'auditor'); 17499 this._auditorPanel = window.openRosganAuditor({ 17500 remateId: this.remateRouteId(remate), title: remate.nombre_remate, remateDate: remate.fechaHoraRemate, 17501 lotLoadingEnabled: remate.cargaDeLotes !== false, 17502 onLotLoadingChange: enabled => this.setRemateLotLoading(remate, enabled), 17503 canChangeStatus: this.canManageUsers(this.state.apiUser), 17504 canBulkControl: this.apiRoleIds(this.state.apiUser).includes('consignatario'), 17505 onSalesOrder: this.canManageUsers(this.state.apiUser) ? () => { 17506 if (!this.canManageUsers(this.state.apiUser)) { this.showToast('No tenés permisos para administrar el orden de venta.'); return; } 17507 this._auditorPanel?.close(); this._auditorPanel = null; 17508 this.openRosganSalesOrderPage(remate, null, true, true); 17509 } : null, 17510 consignatariaBrand: (lot) => { 17511 const record = (this.state.rosganLotConsignatarias || []).find(item => String(item.id) === String(lot.consignatariaId)); 17512 const name = lot.consignatariaName || record?.name || record?.nombre || this.consRecord(lot.consignatariaId)?.name || ''; 17513 return { name: name || 'Consignataria sin informar', logo: this.consLogo(name || lot.consignatariaId), whiteLogo: this.consWhiteLogo(name || lot.consignatariaId), color: this.consBrandColor(name || lot.consignatariaId) }; 17514 }, 17515 apiFetch: (path, opts) => this.apiFetch(path, opts), 17516 onDuplicate: (lot) => this.openRosganLotEditor(lot, remate, { duplicate: true }), 17517 onClose: () => { this._auditorPanel = null; this.openRosganActiveRemate(remate); }, 17518 }); 17519 } 17520 canOpenRosganOperation(item, action) { 17521 if (action === 'prices') return true; 17522 if (!this.state.user || !this.state.apiUser) return false; 17523 if (this.canManageUsers()) return true; 17524 if (action !== 'stats' || !this.apiRoleIds(this.state.apiUser).includes('consignatario')) return false; 17525 const memberships = [...(this.state.userConsignatarias || []), this.state.userConsignataria].filter(
17525Boolean); 17526 if (item?.modoConsignataria === 'Todos') return memberships.some(c => c.id || c.consignatariaId || c.ref); 17527 return this.canExportCabanaCatalog(item, this.state.apiUser); 17528 } 17529 openRosganOperations(item, action = 'commercial', options = {}) { 17530 const remate = this.normalizeRosganActiveRemate(item || {}); 17531 const route = this.rosganOpsRoute(action); 17532 if (!this.canOpenRosganOperation(remate, action)) { 17533 this.showToast('Esta herramienta está disponible para perfiles autorizados.'); 17534 if (options.syncPath !== false) this.syncRemateLocation(remate); 17535 this.openRosganActiveRemate(remate, { syncPath: false }); 17536 return; 17537 } 17538 if (options.syncPath !== false) this.syncRemateSectionLocation(remate, this.rosganOpsSuffix(action)); 17539 const stored = this.loadRosganOpsState(remate); 17540 if (action === 'tvConfig') { 17541 Promise.all([this.apiFetch('/api/rosgan-categories'), this.apiFetch('/api/rosganBreeds')]).then(([categories,breeds]) => { 17542 this.setState({ rosganLotCategories: this.rosganArray(categories), rosganLotBreeds: this.rosganArray(breeds) }); 17543 }).catch(() => this.showToast('No se pudieron cargar las categorÃas y razas.')); 17544 } 17545 const sameRemate = this.remateRouteId(this.state.currentRemate) === this.remateRouteId(remate); 17546 this._rosganOpsSignature = JSON.stringify(stored); 17547 this.setState({ route, currentRemate: remate, rdLightbox: null, rosganOpsDrafts: stored.commercial, rosganOpsConfig: stored.tv, rosganOpsLoading: !sameRemate || !Array.isArray(this.state.remateLots) }, () => { 17548 window.scrollTo(0, 0); 17549 if(action!=='stats')this.fetchRosganOpsRemoteState({ force: true, silent: true }); 17550 if(action==='prices')this.fetchRosganOfficialPrices(remate,{force:true}); 17551 if(action==='tv'||action==='tvConfig')this.fetchCatalogPublication(remate); 17552 if (!sameRemate || !Array.isArray(this.state.remateLots)) this.fetchRosganActiveLots(remate).finally(() => this.setState({ rosganOpsLoading: false })); 17553 else if (route === 'rosganTvLower') this.ensureRosganOpsSelectedVideo(); 17554 }); 17555 } 17556 async openRosganOperationsById(remateId, action = 'commercial') { 17557 const requestedPath = window.location.pathname; 17558 try { 17559 const response = await this.apiFetch('/api/Remates/' + encodeURIComponent(remateId)); 17560 if (window.location.pathname !== requestedPath) return; 17561 const item = response?.data ?? response; 17562 if (!item || item?.cabanasConfig) throw new Error('No es un remate Rosgan.'); 17563 return this.openRosganOperations(item, action, { syncPath: false }); 17564 } catch (error) { 17565 if (window.location.pathname !== requestedPath) return; 17566 this.showToast(error?.message || 'No se pudo abrir la herramienta del remate.'); 17567 } 17568 } 17569 backToRosganCatalog() { 17570 const remate = this.state.currentRemate; 17571 if (!remate) return this.go('remates'); 17572 this.syncRemateLocation(remate); 17573 this.setState({ route: 'remateDetail', rdLightbox: null }); 17574 window.scrollTo(0, 0); 17575 } 17576 openCurrentRosganOps(action) { 17577 if (this.state.currentRemate) this.openRosganOperations(this.state.currentRemate, action); 17578 } 17579 editRosganCommercialPrice(lot,priceMode,event){ 17580 const value=window.RosganPriceInput.input(event.target),remate=this.state.currentRemate; 17581 const cert=this.remateLotRouteId(lot),key=this.remateRouteId(remate)+'/'+cert; 17582 this._rosganPriceEdits ||= new Map(); 17583 const previous=this._rosganPriceEdits.get(key);clearTimeout(previous?.timer); 17584 const edit={remate,cert,patch:{...previous?.patch,priceMode,[priceMode==='kilo'?'precioKilo':'precioCabeza']:value}}; 17585 this._rosganPriceEdits.set(key,edit); 17586 this._rosganPriceTypingAt=Date.now(); 17587 edit.timer=setTimeout(()=>this.flushRosganCommercialPrice(key),350); 17588 } 17589 openRosganLotDivisions(lot){ 17590 const remate=this.state.currentRemate,key=this.remateRouteId(remate)+'/'+this.remateLotRouteId(lot); 17591 this.flushRosganCommercialPrice(key); 17592 const sale=this.loadRosganOpsState(remate).commercial[this.remateLotRouteId(lot)]||{}; 17593 window.openRosganSaleDivisions({lot,sale,mode:window.RosganPriceInput.mode(lot,sale),onSave:(divisions,priceMode)=>{ 17594 if(this.remateRouteId(this.state.currentRemate)!==this.remateRouteId(remate))throw new Error('Cambió el remate abierto. Volvé a abrir las divisiones del lote.'); 17595 const quantity=Number(lot.cantidad),priced=divisions.filter(p=>p.price>0),soldQuantity=priced.reduce((s,p)=>s+p.quantity,0); 17596 const average=soldQuantity?priced.reduce((s,p)=>s+p.quantity*p.price,0)/soldQuantity:0; 17597 this.updateRosganCommercial(lot,{divisions,divisionsQuantity:quantity,priceMode,precioKilo:priceMode==='kilo'?String(average):'',precioCabeza:priceMode==='cabeza'?String(average):'',comprador:'',status:soldQuantity===quantity?'vendido':'pendiente'}); 17598 }}); 17599 } 17600 editRosganCommercialBuyer(lot,event){ 17601 const remate=this.state.currentRemate,cert=this.remateLotRouteId(lot),key=this.remateRouteId(remate)+'/'+cert; 17602 this._rosganPriceEdits ||= new Map(); 17603 const previous=this._rosganPriceEdits.get(key);clearTimeout(previous?.timer); 17604 const edit={remate,cert,patch:{...previous?.patch,comprador:String(event.target.value||'').slice(0,200)}}; 17605 this._rosganPriceEdits.set(key,edit);this._rosganPriceTypingAt=Date.now(); 17606 edit.timer=setTimeout(()=>this.flushRosganCommercialPrice(key),600); 17607 } 17608 async downloadRosganBilling(format){ 17609 if(this._rosganBillingBusy)return; 17610 this._rosganBillingBusy=true; 17611 try{ 17612 const remate=this.state.currentRemate,commercial={...this.loadRosganOpsState(remate).commercial}; 17613 for(const edit of this._rosganPriceEdits?.values()||[])if(this.remateRouteId(edit.remate)===this.remateRouteId(remate))commercial[edit.cert]={...commercial[edit.cert],...edit.patch}; 17614 const report=window.RosganBilling.build(this.state.remateLots||[],commercial,{mode:(lot,sale)=>window.RosganPriceInput.mode(lot,sale),consignee:name=>this.consDisplayName(name)}); 17615 if(!report.rows.length){this.showToast('TodavÃa no hay lotes vendidos para facturar.');return;} 17616 const meta={title:remate.nombre_remate||remate.title||'Remate',date:this.remateDate(remate)?.toLocaleDateString('es-AR')||'Fecha a confirmar'}; 17617 const logoData=async(src,wide=false,background='')=>{try{const image=await this.loadCabanaCanvasImage(src);if(!image)return null;const canvas=document.createElement('canvas');canvas.width=wide?360:128;canvas.height=128;const ctx=canvas.getContext('2d');if(background){ctx.fillStyle=background;ctx.fillRect(0,0,canvas.width,canvas.height);}this.drawCabanaImageContain(ctx,image,0,0,canvas.width,canvas.height);return canvas.toDataURL('image/png');}catch{return null;}}; 17618 meta.logo=await logoData('/assets/rosgan-logo-blue.png',true); 17619 meta.logos=Object.fromEntries(await Promise.all(report.groups.map(async group=>[group.consignee,await logoData(this.consLogo(group.consignee))||await logoData(this.consWhiteLogo(group.consignee),false,this.consColor(group.consignee))]))); 17620 let buffer; 17621 if(format==='xlsx')buffer=await window.RosganBilling.excel(report,meta,await this.ensureCabanaExcelJs()); 17622 else{ 17623 for(const src of ['/assets/vendor/jspdf.umd.min.js','/assets/vendor/jspdf.plugin.autotable.min.js']){ 17624 this._rosganBillingScripts ||= {}; 17625 if(!this._rosganBillingScripts[src])this._rosganBillingScripts[src]=new Promise((resolve,reject)=>{const tag=document.createElement('script');tag.src=src;tag.onload=resolve;tag.onerror=()=>{delete this._rosganBillingScripts[src];reject(new Error('No se pudo cargar el generador PDF.'));};document.head.appendChild(tag);}); 17626 await this._rosganBillingScripts[src]; 17627 } 17628 buffer=window.RosganBilling.pdf(report,meta,window.jspdf.jsPDF,(doc,options)=>doc.autoTable(options)); 17629 } 17630 this.downloadCabanaBlob(new Blob([buffer],{type:format==='xlsx'?'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':'application/pdf'}),this.cabanaExportFileBase(remate)+'-facturacion.'+format); 17631 this.showToast('Informe de facturación descargado.'); 17632 }catch(error){this.showToast(error.message||'No se pudo generar el informe.');} 17633 finally{this._rosganBillingBusy=false;} 17634 } 17635 commitRosganCommercialPrice(lot,priceMode,event){ 17636 if(event.key!=='Enter'||event.isComposing||event.repeat)return; 17637 event.preventDefault(); 17638 const value=String(event.target.value||'').replace(/\D/g,''); 17639 const inputs=Array.from(document.querySelectorAll('.rg-commercial-row .commercial-price input')); 17640 const nextLabel=inputs[inputs.indexOf(event.target)+1]?.getAttribute('aria-label'); 17641 this.updateRosganCommercial(lot,{priceMode,[priceMode==='kilo'?'precioKilo':'precioCabeza']:value,status:Number(value)>0?'vendido':'no-vendido'}); 17642 setTimeout(()=>{ 17643 const next=Array.from(document.querySelectorAll('.rg-commercial-row .commercial-price input')).find(input=>nextLabel&&input.getAttribute('aria-label')===nextLabel); 17644 if(next){next.focus();next.select();}else event.target.blur(); 17645 },40); 17646 } 17647 flushRosganCommercialPrice(key){ 17648 const edit=this._rosganPriceEdits?.get(key);if(!edit)return; 17649 clearTimeout(edit.timer);this._rosganPriceEdits.delete(key); 17650 const current=this.loadRosganOpsState(edit.remate); 17651 current.commercial={...current.commercial,[edit.cert]:{...current.commercial[edit.cert],...edit.patch,updatedAt:new Date().toISOString()}}; 17652 this.saveRosganOpsState(current,edit.remate); 17653 } 17654 updateRosganCommercial(lot, patch = {}) { 17655 const cert = this.remateLotRouteId(lot); 17656 if (!cert) return; 17657 const current = this.loadRosganOpsState();
17658 const key=this.remateRouteId(this.state.currentRemate)+'/'+cert,edit=this._rosganPriceEdits?.get(key); 17659 if(edit){clearTimeout(edit.timer);this._rosganPriceEdits.delete(key);} 17660 const previous = current.commercial[cert] || {}; 17661 current.commercial = { ...current.commercial, [cert]: { ...previous, priceMode:window.RosganPriceInput.mode(lot,previous), ...edit?.patch, ...patch, updatedAt: new Date().toISOString() } }; 17662 this.saveRosganOpsState(current); 17663 } 17664 rosganTvCatalogRows(rows,tv,selectedCert,query){ 17665 const remateId=this.remateRouteId(this.state.currentRemate); 17666 if(this._rosganTvCatalogCache?.remateId!==remateId||this._rosganTvCatalogCache?.consignatarias!==this.state.consignatarias)this._rosganTvCatalogCache={remateId,consignatarias:this.state.consignatarias,brands:new Map(),rows:new Map()}; 17667 const cache=this._rosganTvCatalogCache,needle=this.consKey(query),live=new Set(rows.map(row=>row.cert)); 17668 for(const key of cache.rows.keys())if(!live.has(key))cache.rows.delete(key); 17669 return [...rows].sort((a,b)=>Number(a.saleOrder ?? a.lotNumber ?? a.order)-Number(b.saleOrder ?? b.lotNumber ?? b.order)||Number(a.lotNumber ?? a.order)-Number(b.lotNumber ?? b.order)).flatMap(row=>{ 17670 const catalogConsignataria=row.tvConsignataria||row.consignataria; 17671 const signature=JSON.stringify([row.saleOrder,row.lotNumber,row.order,row.category,catalogConsignataria,row.tvConsignatariaLogo,row.tvConsignatariaColor,row.status,row.cert===selectedCert,[0,1,2].map(i=>tv.extraSlots?.[i]===row.cert)]); 17672 let cached=cache.rows.get(row.cert); 17673 if(!cached||cached.signature!==signature){ 17674 let brand=row.tvConsignatariaLogo?{logo:row.tvConsignatariaLogo,color:row.tvConsignatariaColor}:cache.brands.get(catalogConsignataria); 17675 if(!brand){brand={logo:this.consWhiteLogo(catalogConsignataria)||this.consLogo(catalogConsignataria),color:this.consBrandColor(catalogConsignataria)};cache.brands.set(catalogConsignataria,brand);} 17676 cached={signature,search:this.consKey([row.order,row.cert,row.category,catalogConsignataria].join(' ')),value:{...row,...brand,consignataria:catalogConsignataria, 17677 disabled:row.status==='levantado',sold:row.status==='vendido', 17678 slots:[2,3,4].map(number=>({number,active:tv.extraSlots?.[number-2]===row.cert,disabled:row.cert===selectedCert||row.status==='levantado',className:tv.extraSlots?.[number-2]===row.cert?'is-active':'',label:(tv.extraSlots?.[number-2]===row.cert?'Quitar lote ':'Mostrar lote ')+row.order+' en pantalla '+number,select:()=>this.toggleRosganTvSlot(row.cert,number)})), 17679 selectedClass:(row.cert===selectedCert?'is-selected ':'')+(row.status==='levantado'?'is-lifted':''),select:()=>this.setRosganTvConfig({selectedCert:row.cert})}}; 17680 cache.rows.set(row.cert,cached); 17681 } 17682 return cached.search.includes(needle)?[cached.value]:[]; 17683 }); 17684 } 17685 setRosganNameDraft(field,value) { 17686 if(!['rosNameDraft','rosRoleDraft'].includes(field))return; 17687 // Keep native typing immediate; only explicit Save/Edit needs a render. 17688 this.state={...this.state,[field]:String(value||'').slice(0,field==='rosNameDraft'?100:120)}; 17689 } 17690 rosganNameStrapPresentation(tv) { 17691 const remate=this.remateRouteId(this.state.currentRemate); 17692 const person=tv.visible!==false?(tv.nameStraps||[]).find(p=>p.id===tv.activeNameStrap):null; 17693 if(person)this._rosganNameStrapLast={remate,name:person.name,role:person.role||''}; 17694 const copy=this._rosganNameStrapLast?.remate===remate?this._rosganNameStrapLast:{name:'',role:''}; 17695 return {rosNameOnAir:!!person,rosNameHidden:!person,rosNameStrapClass:(person?'is-on ':'')+(copy.name.length>32?'is-long':''),rosNameOnAirText:copy.name,rosRoleOnAirText:copy.role}; 17696 } 17697 setRosganTvConfig(patch = {}) { 17698 const current = this.loadRosganOpsState(); 17699 const lifted = cert => String(current.commercial?.[cert]?.status || '').toLowerCase() === 'levantado'; 17700 if (patch.selectedCert && lifted(patch.selectedCert)) return; 17701 if (Array.isArray(patch.extraSlots)) { 17702 if (patch.extraSlots.some((cert,index) => cert && lifted(cert) && current.tv.extraSlots?.[index] !== cert)) return; 17703 patch = {...patch,extraSlots:patch.extraSlots.map(cert => lifted(cert) ?'' :cert)}; 17704 } 17705 if (Object.prototype.hasOwnProperty.call(patch, 'selectedCert')) { 17706 patch = { ...patch, extraSlots:[0,1,2].map(index => (current.tv.extraSlots?.[index] === patch.selectedCert || lifted(current.tv.extraSlots?.[index])) ?'' :String(current.tv.extraSlots?.[index] || '')) }; 17707 } 17708 if(Object.keys(patch).every(key=>
17708JSON.stringify(current.tv[key])===JSON.stringify(patch[key])))return; 17709 current.tv = { ...current.tv, ...patch }; 17710 this.saveRosganOpsState(current); 17711 if (Object.prototype.hasOwnProperty.call(patch, 'selectedCert')) setTimeout(() => this.ensureRosganOpsSelectedVideo(), 0); 17712 } 17713 async copyRosganVmixUrl(){ 17714 try{ 17715 const id=this.remateRouteId(this.state.currentRemate); 17716 const result=await this.apiFetch('/api/remate-operations/'+encodeURIComponent(id)+'/airtime'); 17717 const url=new URL('/remates/'+encodeURIComponent(id)+'/tv','https://rosgan.app'); 17718 url.searchParams.set('airtime',result.data.outputId); 17719 await navigator.clipboard.writeText(url.href); 17720 this.showToast('URL vinculada copiada. Pegala en el Browser de vMix cuando esté fuera del aire.'); 17721 }catch(error){this.showToast('No se pudo copiar la URL de vMix. '+error.message);} 17722 } 17723 editRosganVmixConnection(field,value){ 17724 const id=this.remateRouteId(this.state.currentRemate),tv=this.loadRosganOpsState().tv; 17725 const draft=this.state.rosganVmixDraft?.id===id?this.state.rosganVmixDraft:{id,host:tv.vmixHost||'192.168.122.28:8088',input:tv.vmixBrowserInputNumber||''}; 17726 this.setState({rosganVmixDraft:{...draft,[field]:value}}); 17727 } 17728 saveRosganVmixConnection(){ 17729 const id=this.remateRouteId(this.state.currentRemate),tv=this.loadRosganOpsState().tv; 17730 const draft=this.state.rosganVmixDraft?.id===id?this.state.rosganVmixDraft:{host:tv.vmixHost||'192.168.122.28:8088',input:tv.vmixBrowserInputNumber}; 17731 try{ 17732 const raw=String(draft.host).trim().replace(/^https?:\/\//i,'').replace(/\/$/,''); 17733 if(!/^[a-z0-9.-]+(?::\d{1,5})?$/i.test(raw))throw Error('Ingresá una IP o nombre de equipo válido.'); 17734 const url=new URL('http://'+raw),input=Number(draft.input); 17735 if(!Number.isInteger(input)||input<1||input>1000)throw Error('La entrada debe ser un número entre 1 y 1000.'); 17736 const host=url.hostname+':'+(url.port||'8088'); 17737 this.setRosganTvConfig({vmixHost:host,vmixBrowserInputNumber:input,vmixBrowserInputKey:host!==(tv.vmixHost||'192.168.122.28:8088')||input!==Number(tv.vmixBrowserInputNumber)?'':tv.vmixBrowserInputKey||''}); 17738 this.setState({rosganVmixDraft:null}); 17739 }catch(error){this.showToast(error.message);} 17740 } 17741 selectRosganTvLot(event) { this.setRosganTvConfig({ selectedCert: String(event?.target?.value || '') }); } 17742 rosganTvPane() { 17743 const slot = Number(new URLSearchParams(window.location?.search || '').get('tvSlot')); 17744 return Number.isInteger(slot) && slot >= 1 && slot <= 4 ?slot :0; 17745 } 17746 rosganTvSelectedCert(tv = this.state.rosganOpsConfig, lots = this.state.remateLots || []) { 17747 const slot = this.rosganTvPane(); 17748 return String((slot > 1 ?tv?.extraSlots?.[slot - 2] :tv?.selectedCert) || this.remateLotRouteId(lots[0]) || ''); 17749 } 17750 rosganTvSlots(tv = this.state.rosganOpsConfig, lots = this.state.remateLots || []) { 17751 if (this.rosganTvPane()) return []; 17752 const certs = [String(tv?.selectedCert || this.remateLotRouteId(lots[0]) || ''), ...(Array.isArray(tv?.extraSlots) ?tv.extraSlots.slice(0,3) :[])]; 17753 return certs.flatMap((cert,index) => cert && certs.indexOf(cert) === index && lots.some(lot => this.remateLotRouteId(lot) === cert) ?[{ slot:index + 1, cert }] :[]); 17754 } 17755 toggleRosganTvSlot(cert, slot) { 17756 if (![2,3,4].includes(slot)) return; 17757 if (String(this.loadRosganOpsState().commercial?.[cert]?.status || '').toLowerCase() === 'levantado') return; 17758 const tv = this.loadRosganOpsState().tv; 17759 const primary = String(tv.selectedCert || this.remateLotRouteId(this.state.remateLots?.[0]) || ''); 17760 if (cert === primary) return; 17761 const slots = [0,1,2].map(index => String(tv.extraSlots?.[index] || '')); 17762 const remove = slots[slot - 2] === cert; 17763 const extraSlots = slots.map(value => value === cert ?'' :value); 17764 extraSlots[slot - 2] = remove ?'' :cert; 17765 this.setRosganTvConfig({ extraSlots }); 17766 } 17767 updateRosganTvField(cert, name, value) { 17768 const remate = this.state.currentRemate; 17769 const key = this.remateRouteId(remate) + '/' + cert; 17770 this._rosganTvEdits ||= {}; 17771 const entry = this._rosganTvEdits[key] ||= { remate, cert, changes: {}, status: '' }; 17772 entry.revision = (entry.revision || 0) + 1; 17773 entry.touched ||= {}; 17774 entry.touched[name] = true; 17775 entry.changes[name] = value; 17776 entry.status = 'Cambios pendientes. Presioná guardar para actualizar TV.'; 17777 this.setState((s) => ({ rosganOpsPulse: s.rosganOpsPulse + 1 })); 17778 } 17779 publishRosganTvPreview(entry, confirmedLot = null, failed = false) { 17780 try { 17781 // Only confirmed data goes on air; newer edits remain private drafts until the next save. 17782 const changes = {}; 17783 const packet = { remateId: this.remateRouteId(entry.remate), cert
17783: entry.cert, changes, confirmedLot, at: Date.now(), 17784 categories: (this.state.rosganLotCategories || []).map(({ id, name }) => ({ id, name })), 17785 breeds: (this.state.rosganLotBreeds || []).map(({ id, name }) => ({ id, name })) }; 17786 localStorage.setItem(this.rosganOpsStorageKey(entry.remate) + ':lot-preview', JSON.stringify(packet)); 17787 } catch { /* Incomplete numeric input stays in the editor until it is valid. */ } 17788 } 17789 receiveRosganTvPreview(value) { 17790 if (this.state.route !== 'rosganTvLower') return; 17791 try { 17792 const packet = JSON.parse(value || 'null'); 17793 if (!packet?.cert || packet.remateId !== this.remateRouteId(this.state.currentRemate) || Date.now() - packet.at > 30000) return; 17794 packet.changes = window.RosganTvEditor.serializeChanges(packet.changes); 17795 this._rosganTvPreviews ||= {}; 17796 this._rosganTvPreviews[packet.remateId + '/' + packet.cert] = packet; 17797 // Discard reads started before this edit/confirmation. 17798 const key = this.remateRouteId(this.state.currentRemate) + '/' + packet.cert; 17799 if (this._rosganTvPendingReads) delete this._rosganTvPendingReads[key]; 17800 this.setState(s => ({ 17801 ...(packet.confirmedLot ?{ remateLots: (s.remateLots || []).map(lot => this.remateLotRouteId(lot) === packet.cert ?this.mergeRosganTvConfirmedLot(lot, packet.confirmedLot) : lot) } : {}), 17802 rosganOpsPulse: Number(s.rosganOpsPulse || 0) + 1, 17803 })); 17804 } catch { /* Ignore malformed or expired preview messages. */ } 17805 } 17806 rosganTvPreviewLot(lot) { 17807 const packet = this._rosganTvPreviews?.[this.remateRouteId(this.state.currentRemate) + '/' + this.remateLotRouteId(lot)]; 17808 if (!packet || Date.now() - packet.at > 30000 || !Object.keys(packet.changes).length) return lot; 17809 return this.normalizeRosganActiveLot(window.RosganTvEditor.previewLot(lot, packet.changes, packet.categories, packet.breeds), 0, this.state.currentRemate); 17810 } 17811 async saveRosganTvLotEdits(entry) { 17812 if (entry.saving || !Object.keys(entry.changes).length) return; 17813 entry.saving = true; 17814 entry.status = 'Guardando en el loteâ¦'; 17815 const changes = entry.changes; 17816 entry.inFlight = changes; 17817 entry.changes = {}; 17818 clearTimeout(entry.timer); 17819 this.setState((s) => ({ rosganOpsPulse: s.rosganOpsPulse + 1 })); 17820 try { 17821 const payload = { changes: window.RosganTvEditor.serializeChanges(changes) }; 17822 const saved = await this.apiFetch('/api/remate-operations/' + encodeURIComponent(this.remateRouteId(entry.remate)) + '/lots/' + encodeURIComponent(entry.cert), { method: 'PUT', body: JSON.stringify(payload) }); 17823 const refreshed = saved?.data?.lot || await this.apiFetch('/api/remate-operations/' + encodeURIComponent(this.remateRouteId(entry.remate)) + '/lots/' + encodeURIComponent(entry.cert), { cache: 'no-store' }); 17824 const lot = this.normalizeRosganActiveLot(refreshed?.data || refreshed, 0, entry.remate); 17825 // Install the confirmed lot before publishing its version to other views. 17826 if (this.remateRouteId(this.state.currentRemate) === this.remateRouteId(entry.remate)) { 17827 this.setState((s) => ({ remateLots: (s.remateLots || []).map((item) => this.remateLotRouteId(item) === entry.cert ?this.mergeRosganTvConfirmedLot(item, lot) : item) })); 17828 } 17829 // Also notify on a no-op retry: an earlier write may have succeeded while its response was lost. 17830 { 17831 const current = this.loadRosganOpsState(entry.remate); 17832 const key = this.remateRouteId(entry.remate) + '/' + entry.cert; 17833 this._rosganTvSeenVersions ||= {}; 17834 const version = Math.max(Date.now(), Number(current.tv.lotVersions?.[entry.cert] || 0) + 1, Number(this._rosganTvSeenVersions[key] || 0) + 1); 17835 this._rosganTvSeenVersions[key] = version; 17836 current.tv.lotVersions = { ...current.tv.lotVersions, [entry.cert]: version }; 17837 this.saveRosganOpsState(current, entry.remate); 17838 } 17839 entry.status = Object.keys(entry.changes).length ?'Cambios pendientes. Presioná guardar para actualizar TV.' :'Guardado en el lote y actualizado en TV'; 17840 entry.inFlight = {}; 17841 this.publishRosganTvPreview(entry, lot); 17842 } catch (error) { 17843 entry.changes = { ...changes, ...entry.changes }; 17844 entry.status = 'No guardado: ' + (error?.message || 'Error de conexión.'); 17845 entry.failed = true; 17846 this.publishRosganTvPreview(entry, null, true); 17847 this.showToast(entry.status); 17848 } finally { 17849 entry.saving = false;
17850 entry.inFlight = {}; 17851 this.setState((s) => ({ rosganOpsPulse: s.rosganOpsPulse + 1 })); 17852 entry.failed = false; 17853 } 17854 } 17855 async syncRosganTvLotVersions(tv, remate = this.state.currentRemate) { 17856 this._rosganTvSeenVersions ||= {}; 17857 this._rosganTvPendingReads ||= {}; 17858 for (const [cert, version] of Object.entries(tv?.lotVersions || {})) { 17859 const key = this.remateRouteId(remate) + '/' + cert; 17860 // Do not consume versions before the initial catalog has installed the lot. 17861 if (!(this.state.remateLots || []).some(item => this.remateLotRouteId(item) === cert)) continue; 17862 if (Number(this._rosganTvSeenVersions[key] || 0) >= Number(version)) continue; 17863 const edit = this._rosganTvEdits?.[key]; 17864 if (edit?.saving || Object.keys(edit?.changes || {}).length) continue; 17865 if (this._rosganTvPendingReads[key]?.version === version) continue; 17866 const request = { version, revision: edit?.revision || 0 }; 17867 this._rosganTvPendingReads[key] = request; 17868 try { 17869 const response = await this.apiFetch('/api/remate-operations/' + encodeURIComponent(this.remateRouteId(remate)) + '/lots/' + encodeURIComponent(cert), { cache: 'no-store' }); 17870 if (this.remateRouteId(remate) !== this.remateRouteId(this.state.currentRemate)) continue; 17871 const latestEdit = this._rosganTvEdits?.[key]; 17872 // Never apply a read started before a newer edit, save or version request. 17873 if (this._rosganTvPendingReads[key] !== request || (latestEdit?.revision || 0) !== request.revision || latestEdit?.saving || Object.keys(latestEdit?.changes || {}).length || Number(this._rosganTvSeenVersions[key] || 0) >= Number(version)) continue; 17874 const lot = this.normalizeRosganActiveLot(response?.data || response, 0, remate); 17875 this._rosganTvSeenVersions[key] = version; 17876 this.setState((s) => ({ remateLots: (s.remateLots || []).map((item) => this.remateLotRouteId(item) === cert ?this.mergeRosganTvConfirmedLot(item, lot) : item) })); 17877 } catch { /* Keep the last confirmed version; the next poll can retry. */ } 17878 finally { if (this._rosganTvPendingReads[key] === request) delete this._rosganTvPendingReads[key]; } 17879 } 17880 } 17881 mergeRosganTvConfirmedLot(item, lot) { 17882 // The TV editor cannot change media. Its direct DB projection does not resolve video URLs. 17883 return { ...item, ...lot, videoUrl: item.videoUrl || lot.videoUrl || '', video_uri: item.video_uri || lot.video_uri || '', thumbnailUrl: item.thumbnailUrl || lot.thumbnailUrl || '' }; 17884 } 17885 rosganTvEditorFields(lot) { 17886 if (!lot) return []; 17887 const cert = this.remateLotRouteId(lot); 17888 const category = (this.state.rosganLotCategories || []).find((item) => String(item.id) === String(lot.categoryId)); 17889 const quantityMode = category?.quantityMode || (lot.maleQuantity != null || lot.femaleQuantity != null ?'male_female' : lot.motherQuantity != null || lot.calfQuantity != null ?'mothers_calves' : 'total'); 17890 const fields = [ 17891 ['categoryId','CategorÃa','category'], ['breedIds','Razas','breeds'], 17892 ['quantity','Cabezas','number'], ['maleQuantity','Machos','number'], ['femaleQuantity','Hembras','number'], ['motherQuantity','Madres','number'], ['calfQuantity','CrÃas','number'], 17893 ['locality','Localidad'], ['province','Provincia'], ['paymentTerm','Plazo de pago'], 17894 ['animalClass','Clase'], ['ageValue','Edad','number'], ['ageUnit','Unidad de edad'], ['bodyCondition','Estado'], ['lotDescriptions','Descripción (una por lÃnea)','array'], ['dentition','Dentición'], 17895 ['reproductiveStatus','Estado reproductivo'], ['destination','Destino'], 17896 ['nutrition','Alimentación'], ['health','Sanidad'], ['tickStatus','Garrapata'], 17897 ['hasMioMio','MÃo mÃo','boolean'], ['isAptoChina','Apto China','boolean'], ['isTermino','Término','boolean'], 17898 ...(lot.invernada ?[['invernada.averageWeight','Peso promedio (kg)','number'], ['invernada.minWeight','Peso mÃnimo (kg)','number'], ['invernada.maxWeight','Peso máximo (kg)','number'], ['invernada.weighing','Pesaje'], ['invernada.desbaste','Desbaste'], ['invernada.hasLiquidBrand','Marca lÃquida','boolean'], ['invernada.isAptoVisec','Apto Visec','boolean']] : []), 17899 ...(lot.vientres ?[['vientres.service','Servicio'], ['vientres.pregnancy','Preñez'], ['vientres.calving','Parición']] : []), 17900 ...(lot.faena ?[['faena.subCategory','SubcategorÃa faena'], ['faena.averageWeight','Peso promedio (kg)','number'], ['faena.minWeight','Peso mÃnimo (kg)','number'], ['faena.maxWeight','Peso máximo (kg)','number'], ['faena.weighing','Pesaje'], ['faena.desbaste','Desbaste'], ['faena.conditionType','Condición faena'], ['faena.deliveryType','Entrega'], ['faena.finishing','Terminación'], ['faena.estimatedYield','Rinde','number']] : []), 17901 ['observations','Observaciones (roll sobre el zócalo)','textarea'], 17902 ]; 17903 return fields.filter(([name]) => name === 'quantity' ?quantityMode === 'total' : ['maleQuantity','femaleQuantity'].includes(name) ?quantityMode === 'male_female' : ['motherQuantity','calfQuantity'].includes(name) ?quantityMode === 'mothers_calves' : true).map(([name,label,type = 'text']) => { 17904 const editState = this._rosganTvEdits?.[this.remateRouteId(this.state.currentRemate) + '/' + cert]; 17905 const edits = { ...editState?.inFlight, ...editState?.changes }; 17906 const raw = Object.prototype.hasOwnProperty.call(edits, name) ?edits[name] : name.split('.').reduce((obj,key) => obj?.[key], lot) ?? ''; 17907 const options = type === 'category' ?(this.state.rosganLotCategories || []).map((item) => ({ value: String(item.id), label: item.name })) : []; 17908 const breedOptions = (this.state.rosganLotBreeds || []).map((item) => ({ label: item.name, selectedClass: Array.isArray(raw) && raw.includes(String(item.id)) ?'is-selected' : '', toggle: () => {
17909 const values = Array.isArray(raw) ?raw : []; 17910 this.updateRosganTvField(cert,name,values.includes(String(item.id)) ?values.filter((v) => v !== String(item.id)) : [...values,String(item.id)]); 17911 } })); 17912 const hasValue = (value) => { 17913 if (typeof value === 'boolean') return true; 17914 if (value == null) return false; 17915 if (Array.isArray(value)) return value.some(hasValue); 17916 const text = String(value).trim(); 17917 if (!text || /^(?:\?|[-ââ]|null|undefined|sin datos?)$/i.test(text)) return false; 17918 return type !== 'number' || (Number.isFinite(Number(text)) && Number(text) > 0); 17919 }; 17920 // Keep a field available while its clear/replacement is being saved. 17921 const visible = hasValue(raw) || Object.prototype.hasOwnProperty.call(edits, name) || !!editState?.touched?.[name]; 17922 return { key: cert + '/' + name, name, label, type, options, breedOptions, visible, value: type === 'boolean' ?(raw === true ?'true' : raw === false ?'false' : '') : Array.isArray(raw) ?raw.join('\n') : String(raw), 17923 nullableBoolean: name === 'hasMioMio', isBoolean: type === 'boolean', isSelect: type === 'category', isBreeds: type === 'breeds', isTextarea: ['textarea','array'].includes(type), isInput: !['boolean','textarea','array','category','breeds'].includes(type), 17924 change: (event) => this.updateRosganTvField(cert, name, type === 'boolean' ?(event.target.value === '' ?null : event.target.value === 'true') : type === 'array' ?event.target.value.split('\n') : event.target.value) }; 17925 }).filter((field) => field.visible); 17926 } 17927 stepRosganTvLot(delta = 1) { 17928 const lots = (this.state.remateLots || []).slice().sort((a, b) => this.compareRosganSalesOrder(a,b)); 17929 if (!lots.length) return; 17930 const selected = String(this.state.rosganOpsConfig?.selectedCert || ''); 17931 let index = Math.max(0, lots.findIndex((lot) => this.remateLotRouteId(lot) === selected)); 17932 const commercial = this.loadRosganOpsState().commercial || {}; 17933 for (let step = 0; step < lots.length; step++) { 17934 index = (index + Number(delta || 0) + lots.length) % lots.length; 17935 const cert = this.remateLotRouteId(lots[index]); 17936 if (String(commercial[cert]?.status || '').toLowerCase() !== 'levantado') { 17937 this.setRosganTvConfig({ selectedCert: cert }); 17938 return; 17939 } 17940 } 17941 } 17942 playRosganTvVideo(event) { 17943 const video = event?.currentTarget; 17944 if (!video || video !== this._rosganTvDecks?.active) return; 17945 video.muted = true; 17946 video.loop = true; 17947 if (video.ended) video.currentTime = 0; 17948 video.play?.().catch(() => {}); 17949 } 17950 resetRosganTvDecks() { 17951 if (!this._rosganTvDecks) return; 17952 this._rosganTvDecks.entries.forEach((entry) => entry.cancel?.()); 17953 this._rosganTvDecks.players.forEach((video) => { 17954 video.classList.remove('is-on-air'); 17955 video.pause(); 17956 video.removeAttribute('src'); 17957 video.load(); 17958 }); 17959 this._rosganTvDecks = null; 17960 } 17961 rosganPriceCategory(category, breed) { 17962 const normalize = value => String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, ''); 17963 const key = normalize(category); 17964 const dairy = /holando|kiwi/.test(normalize(breed) + key); 17965 if (key.includes('prenez')) return /^(vaq|vaquillona)/.test(key) ? 'Vaq. c/gtÃa. de preñez' : key.startsWith('vaca') ? 'Vacas c/gtÃa. de preñez' : ''; 17966 if (key.startsWith('vaca') && key.includes('cria')) return 'Vacas con crÃa al pie'; 17967 if (key.startsWith('vaca') && /inv/.test(key)) return 'Vacas de invernada'; 17968 if (key.startsWith('ternero')) return /ternerosas/.test(key) ? (dairy ? 'Terneros/as Holando' : 'Terneros/as') : (dairy ? 'Terneros Holando' : 'Terneros'); 17969 if (key.startsWith('ternera')) return dairy ? 'Terneras Holando' : 'Terneras'; 17970 if (key.includes('novillito') && key.includes('vaquillona')) return 'Novillitos y Vaquillonas'; 17971 if (key.startsWith('toro')) return 'Toros'; 17972 if (key.includes('vientre') && dairy) return 'Vientres Holando'; 17973 if (key.includes('servicio') && dairy) return 'Vaq. para servicio Holando'; 17974 if (key.startsWith('novillito')) return dairy ? 'Novillos Holando' : 'Novillitos'; 17975 if (key.startsWith('novillo')) return dairy ? 'Novillos Holando' : 'Novillos'; 17976 if (key.startsWith('vaquillona')) return 'Vaquillonas'; 17977 return ''; 17978 } 17979 rosganTvExtraFields(lot) { 17980 if (!lot) return []; 17981 const numeric = (value, unit = '') => Number(value) >
17981 0 ?Number(value).toLocaleString('es-AR') + unit : ''; 17982 const yesNo = (value) => typeof value === 'boolean' ?(value ?'SÃ' : 'No') : ''; 17983 // Explicit public fields: never expose raw API keys, owners or internal metadata on air. 17984 const fields = [ 17985 ['PESO MÃNIMO', numeric(lot.peso_minimo, ' kg')], ['PESO MÃXIMO', numeric(lot.peso_maximo, ' kg')], 17986 ['CLASE', lot.clase], ['EDAD', lot.edad], ['ESTADO', lot.estado], 17987 ['DESCRIPCIÃN', lot.descripcion_lote], ['DENTICIÃN', lot.denticion], 17988 ['ESTADO REPRODUCTIVO', lot.estado_reproductivo], ['DESTINO', lot.destino], 17989 ['PESAJE', lot.pesaje], ['DESBASTE', lot.desbaste], ['ALIMENTACIÃN', lot.pastoreo], 17990 ['SANIDAD', lot.sanidad], ['MÃO MÃO', yesNo(lot.hasMioMio)], 17991 ['MARCA LÃQUIDA', yesNo(lot.invernada?.hasLiquidBrand)], 17992 ['SERVICIO', lot.servicio], 17993 ['PREÃEZ', this.rosganTvFixedPregnancy(lot) ? '' : lot.prenez], ['PARICIÃN', lot.paricion], ['SUBCATEGORÃA', lot.faena_subcategoria], 17994 ['CONDICIÃN DE VENTA', lot.faena_condicion], ['ENTREGA', lot.faena_entrega], 17995 ['TERMINACIÃN', lot.faena_terminacion], ['RINDE ESTIMADO', lot.faena_rinde], 17996 ]; 17997 return fields.flatMap(([label, raw]) => { 17998 if (raw == null || typeof raw === 'object') return []; 17999 const text = String(raw).replace(/\s+/g, ' ').trim(); 18000 if (!text || /^(?:\?|[-ââ]|null|undefined|n\/a|sin datos?)$/i.test(text)) return []; 18001 // Split long values into readable slides instead of clipping the information. 18002 const pages = []; 18003 let rest = text; 18004 while (rest.length > 48) { 18005 const space = rest.lastIndexOf(' ', 48); 18006 const end = space > 20 ?space : 48; 18007 pages.push(rest.slice(0, end)); 18008 rest = rest.slice(end).trim(); 18009 } 18010 if (rest) pages.push(rest); 18011 return pages.map((value) => ({ label, value })); 18012 }); 18013 } 18014 stageRosganTvVideo(lot) { 18015 const players = Array.from(document.querySelectorAll('video[data-rosgan-tv-deck]')); 18016 if (players.length !== 5) return; 18017 if (this._rosganTvDecks?.players[0] !== players[0]) { 18018 this.resetRosganTvDecks(); 18019 this._rosganTvDecks = { players, entries: new Map(), active: null, onAirCert: '', desiredCert: '', warmCerts: new Set() }; 18020 } 18021 const decks = this._rosganTvDecks; 18022 const cert = this.remateLotRouteId(lot); 18023 const url = lot.videoUrl || lot.video_uri || ''; 18024 const key = cert + '|' + url; 18025 decks.desiredCert = cert; 18026 decks.desiredKey = key; 18027 const onAir = decks.entries.get(decks.active); 18028 if (onAir && globalThis.RosganTvHealth?.stalled(onAir)) { 18029 onAir.unhealthy = true; 18030 decks.onAirKey = ''; 18031 } 18032 if (decks.onAirKey === key) { 18033 const active = decks.active; 18034 active.classList.add('is-on-air'); 18035 active.loop = true; 18036 active.muted = true; 18037 if (active.ended) active.currentTime = 0; 18038 if (active.paused) active.play().catch(() => {}); 18039 } else if (url) { 18040 this.prepareRosganTvDeck(lot, true); 18041 } 18042 // The on-air video has priority over speculative network requests. 18043 if (decks.onAirCert === cert) this.prewarmRosganTvNeighbors(lot); 18044 } 18045 prepareRosganTvDeck(lot, priority = false) { 18046 const decks = this._rosganTvDecks; 18047 if (!decks || this.state.route !== 'rosganTvLower') return; 18048 const cert = this.remateLotRouteId(lot); 18049 const url = lot.videoUrl || lot.video_uri || ''; 18050 if (!url) return; 18051 const key = cert + '|' + url; 18052 let entry = Array.from(decks.entries.values()).find((item) => item.key === key && !item.unhealthy); 18053 if (entry) { 18054 if (entry.ready && priority) this.activateRosganTvDeck(entry); 18055 if (!entry.failedAt || Date.now() - entry.failedAt < 15000) return; 18056 } 18057 const next = entry?.video || decks.players.find((video) => !decks.entries.has(video)) || 18058 decks.players.find((video) => video !== decks.active && !decks.warmCerts.has(decks.entries.get(video)?.cert) && decks.entries.get(video)?.cert !== decks.desiredCert) || 18059 (priority ?decks.players.find((video) => video !== decks.active && decks.entries.get(video)?.cert !== decks.desiredCert)
18059: null); 18060 if (!next) return; 18061 decks.entries.get(next)?.cancel?.(); 18062 entry = { key, cert, video: next, ready: false, starting: false, failedAt: 0 }; 18063 decks.entries.set(next, entry); 18064 let frameId = null; 18065 let cancelled = false; 18066 let preparationTimer = null; 18067 const cleanup = () => { 18068 clearTimeout(preparationTimer); 18069 next.removeEventListener('canplay', ready); 18070 next.removeEventListener('error', failed); 18071 if (frameId !== null) next.cancelVideoFrameCallback?.(frameId); 18072 }; 18073 const isCurrent = () => !cancelled && this._rosganTvDecks === decks && decks.entries.get(next) === entry && this.state.route === 'rosganTvLower'; 18074 const decoded = () => { 18075 if (!isCurrent()) return; 18076 cleanup(); 18077 entry.ready = true; 18078 next.pause(); 18079 next.dataset.tvPrepared = cert; 18080 if (decks.desiredKey === key) this.activateRosganTvDeck(entry); 18081 }; 18082 const failed = () => { 18083 if (!isCurrent()) return; 18084 cleanup(); 18085 cancelled = true; 18086 next.pause(); 18087 entry.failedAt = Date.now(); 18088 }; 18089 const ready = async () => { 18090 if (entry.starting || !isCurrent() || next.readyState < 3) return; 18091 entry.starting = true; 18092 try { 18093 await next.play(); 18094 if (!isCurrent()) return; 18095 if (next.requestVideoFrameCallback) frameId = next.requestVideoFrameCallback(decoded); 18096 else requestAnimationFrame(() => requestAnimationFrame(decoded)); 18097 } catch { failed(); } 18098 }; 18099 entry.cancel = () => { cancelled = true; cleanup(); next.pause(); }; 18100 next.classList.remove('is-on-air'); 18101 delete next.dataset.tvPrepared; 18102 next.muted = true; 18103 next.loop = true; 18104 next.playsInline = true; 18105 next.preload = 'auto'; 18106 next.addEventListener('canplay', ready); 18107 next.addEventListener('error', failed); 18108 preparationTimer = setTimeout(() => { if (isCurrent()) failed(); }, 20000); 18109 preparationTimer.unref?.(); 18110 next.src = url; 18111 next.load(); 18112 } 18113 activateRosganTvDeck(entry) { 18114 const decks = this._rosganTvDecks; 18115 if (!decks || !entry.ready || entry.activating || decks.desiredKey !== entry.key || entry.unhealthy || decks.active === entry.video) return; 18116 const next = entry.video; 18117 entry.activating = true; 18118 // The hidden player already owns a decoded frame. No src/load/seek at take time. 18119 (globalThis.RosganTvHealth ? globalThis.RosganTvHealth.play(next) : next.play()).then(() => { 18120 if (this._rosganTvDecks !== decks || decks.desiredKey !== entry.key || entry.unhealthy || decks.entries.get(next) !== entry || this.state.route !== 'rosganTvLower') { 18121 if (decks.entries.get(next) === entry && decks.active !== next) next.pause(); 18122 return; 18123 } 18124 if (decks.active === next) return; 18125 const previous = decks.active; 18126 next.classList.add('is-on-air'); 18127 decks.active = next; 18128 decks.onAirKey = entry.key; 18129 decks.onAirCert = entry.cert; 18130 entry.health = null; 18131 previous?.classList.remove('is-on-air'); 18132 previous?.pause(); 18133 this.setState({ rosganOpsPulse: Date.now(), rosganTvFieldStep: 0 }); 18134 const lot = this.state.remateLots?.find((item) => this.remateLotRouteId(item) === entry.cert); 18135 if (lot) this.prewarmRosganTvNeighbors(lot); 18136 }).catch(() => { entry.ready = false; entry.failedAt = Date.now(); }).finally(() => { entry.activating = false; }); 18137 } 18138 prewarmRosganTvNeighbors(lot) { 18139 if (this.rosganTvPane?.()) return; 18140 if (this.rosganTvSlots?.().length > 1) return; 18141 const decks = this._rosganTvDecks; 18142 if (!decks) return; 18143 const lots = (this.state.remateLots || []).slice().sort((a, b) => this.compareRosganSalesOrder(a,b)); 18144 const index = lots.findIndex((item) => this.remateLotRouteId(item) === this.remateLotRouteId(lot)); 18145 if (index < 0 || lots.length < 2) return; 18146 const neighbors = [...new Set([lots[(index + 1) % lots.length], lots[(index + 2) % lots.length], lots[(index - 1 + lots.length) % lots.length]])].filter((item) => item !== lots[index]); 18147 decks.warmCerts = new Set(neighbors.map((item) => this.remateLotRouteId(item)));
18148 neighbors.forEach((item) => { 18149 if (item.videoUrl || item.video_uri) this.prepareRosganTvDeck(item); 18150 else this.resolveRosganTvVideo(item); 18151 }); 18152 } 18153 async ensureRosganOpsSelectedVideo() { 18154 if (this.state.route !== 'rosganTvLower') return; 18155 const lots = this.state.remateLots || []; 18156 const selectedCert = this.rosganTvSelectedCert(this.state.rosganOpsConfig, lots); 18157 const lot = lots.find((item) => this.remateLotRouteId(item) === selectedCert) || lots[0]; 18158 if (!lot) return; 18159 const slots = this.rosganTvSlots(); 18160 if (slots.length > 1) { 18161 this.stageRosganTvMultiview(slots); 18162 // Hold the existing output while all requested videos decode off air. 18163 if (!this._rosganTvDecks?.active && !this._rosganTvAirSlots?.length) this.stageRosganTvVideo(lot); 18164 if (!lot.videoUrl && !lot.video_uri) this.resolveRosganTvVideo(lot); 18165 return; 18166 } 18167 this.stageRosganTvVideo(lot); 18168 if (this._rosganTvAirSlots?.length && this._rosganTvDecks?.onAirKey === selectedCert + '|' + (lot.videoUrl || lot.video_uri || '')) { 18169 this._rosganTvMultiview?.hide();this._rosganTvAirSlots=[];this.setState({rosganOpsPulse:Date.now()}); 18170 } 18171 if (!lot.videoUrl && !lot.video_uri) this.resolveRosganTvVideo(lot); 18172 } 18173 resetRosganTvMultiview() {
18174 this._rosganTvMultiview?.dispose();this._rosganTvMultiview=null;this._rosganTvAirSlots=[]; 18175 } 18176 rosganTvTemporaryConsignataria(remate, lot) { 18177 if (String(this.remateRouteId(remate)) !== '6' || !lot) return null; 18178 const comitente = lot.comitente && typeof lot.comitente === 'object' ?lot.comitente : {}; 18179 const establishment = lot.establishment || lot.establecimiento || {}; 18180 const values = [ 18181 lot.comitenteAlias, lot.comitenteNombreFiscal, lot.comitenteFiscalName, lot.comitenteNombre, lot.nombre_comitente, lot.comitenteName, 18182 typeof lot.comitente === 'string' ?lot.comitente : '', 18183 comitente.alias, comitente.nombreFiscal, comitente.razonSocial, comitente.fiscalName, comitente.name, comitente.nombre, 18184 establishment.comitenteAlias, establishment.comitenteNombreFiscal, establishment.comitenteNombre, 18185 establishment.comitente?.alias, establishment.comitente?.nombreFiscal, establishment.comitente?.name, 18186 ]; 18187 if (!values.some((value) => this.consKey(value).includes('consignacionescordoba'))) return null; 18188 return { 18189 name: 'Consignaciones Córdoba S.A.', 18190 logo: '/assets/consignatarias-white/consignaciones-cordoba-s-a.png', 18191 color: '#5985ad', 18192 }; 18193 } 18194 stageRosganTvMultiview(slots) { 18195 const players=Array.from(document.querySelectorAll('video[data-rosgan-tv-multi-deck]')); 18196 if(players.length!==8) return; 18197 if(this._rosganTvMultiview?.players[0]!==players[0]) { 18198 this.resetRosganTvMultiview(); 18199 this._rosganTvMultiview=new window.RosganTvMultiview.Multiview(players,airSlots=>{ 18200 if(this.state.route!=='rosganTvLower' || this.rosganTvSlots().length<2) return; 18201 this._rosganTvAirSlots=airSlots; 18202 this._rosganTvDecks?.active?.pause(); 18203 this.setState({rosganOpsPulse:Date.now(),rosganTvFieldStep:0}); 18204 }); 18205 } 18206 const prepared=slots.map(slot=>{ 18207 const lot=this.state.remateLots.find(lot=>this.remateLotRouteId(lot)===slot.cert); 18208 if(!lot.videoUrl&&!lot.video_uri)this.resolveRosganTvVideo(lot); 18209 return {...slot,url:lot.videoUrl||lot.video_uri||''}; 18210 }); 18211 this._rosganTvMultiview.stage(prepared); 18212 } 18213 rosganTvIsAptoChina(lot){ 18214 const value=lot?.isAptoChina??lot?.aptoChina??lot?.apto_china; 18215 return value===true||value===1||['true','1','si','sÃ'].includes(String(value??'').trim().toLowerCase()); 18216 } 18217 rosganTvIsAptoVisec(lot) { 18218 const value = lot?.invernada?.isAptoVisec ?? lot?.isAptoVisec ?? lot?.aptoVisec; 18219 return value === true || value === 1 || ['true','1','si','sÃ'].includes(String(value ?? '').trim().toLowerCase()); 18220 } 18221 rosganTvCategory(lot) { 18222 const category = lot?.nombre_categoria || 'Lote'; 18223 const term = lot?.isTermino === true || lot?.isTermino === 1 || ['true','1','si','sÃ'].includes(String(lot?.isTermino ?? '').toLowerCase()); 18224 return term && !/t[eé]rmino/i.test(category) ? category + ' a término' : category; 18225 } 18226 rosganTvQuantity(lot) { 18227 return Number(lot?.cantidad) > 0 ? Number(lot.cantidad).toLocaleString('es-AR') : ''; 18228 } 18229 rosganTvQuantityBreakdown(lot) { 18230 const number = value => value != null && value !== '' && Number.isFinite(Number(value)) && Number(value) >= 0 ? Number(value).toLocaleString('es-AR') : 'â'; 18231 if (Number(lot?.cantidad_madres) > 0 || Number(lot?.cantidad_crias) > 0) return number(lot.cantidad_madres) + ' M\n' + number(lot.cantidad_crias) + ' C'; 18232 if (Number(lot?.cantidad_machos) > 0 || Number(lot?.cantidad_hembras) > 0) return number(lot.cantidad_machos) + ' M\n' + number(lot.cantidad_hembras) + ' H'; 18233 return ''; 18234 } 18235 rosganTvFixedPregnancy(lot) { 18236 const name = String(lot?.nombre_categoria || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); 18237 const guaranteed = /(?:c\s*\/\s*|con\s+)(?:gtia\.?|garantia)(?:\s+de)?\s+prenez/.test(name); 18238 return guaranteed ? String(lot?.prenez || lot?.vientres?.pregnancy || 'Sin informar') : ''; 18239 } 18240 rosganTvLotNumber(lot) { 18241 if (!lot) return ''; 18242 const remate = this.state.currentRemate; 18243 const active = remate?.__rosganActive || lot.__rosganActiveLot; 18244 const publication = this.state.catalogPublication; 18245 if (active && (publication?.key !== this.catalogPublicationKey(remate) || publication?.salesOrderDefined !== true)) return ''; 18246 const value = active ? lot.lotNumber : (lot.orden_final ?? lot.orden_venta ?? lot.lotNumber); 18247 return Number.isFinite(Number(value)) && Number(value) > 0 ? String(value) : ''; 18248 } 18249 rosganTvAirPanes(tv) { 18250 return (this._rosganTvAirSlots || []).flatMap(slot=>{ 18251 const lot=(this.state.remateLots || []).find(lot=>this.remateLotRouteId(lot)===slot.cert); 18252 if(!lot)return []; 18253 const temporaryCons=this.rosganTvTemporaryConsignataria(this.state.currentRemate,lot); 18254 const cons=temporaryCons?.name||this.consDisplayName(lot.nombre_consignatario)||'Rosgan'; 18255 const logo=temporaryCons?.logo||this.consWhiteLogo(cons)||this.consLogo(cons);
18256 const key=this.consKey(cons); 18257 const record=(this.state.consignatarias||[]).find(item=>this.consKey(item.name||item.nombre)===key); 18258 const raw=record?.telefonos||record?.phones||record?.telefono||record?.phone||''; 18259 const contacts=tv.contactsByConsignataria?.[key]??(Array.isArray(raw)?raw.map(value=>typeof value==='string'?value:value.phone||value.telefono||'').filter(Boolean).join(' | '):String(raw)); 18260 const extra=this.rosganTvExtraFields(lot),field=extra[(this.state.rosganTvFieldStep||0)%Math.max(1,extra.length)]; 18261 const observations=String(lot.observations||lot.observaciones_certificado||lot.observaciones||'').replace(/\s+/g,' ').trim(); 18262 const number=value=>Number(value)>0?Number(value).toLocaleString('es-AR'):'â'; 18263 return [{style:'left:'+slot.x+'%;top:'+slot.y+'%;transform:scale('+slot.scale+')', 18264 LotNumber:this.rosganTvLotNumber(lot),Cert:slot.cert,Category:this.rosganTvCategory(lot),Breed:lot.raza||'', 18265 Quantity:this.rosganTvQuantity(lot),HasQuantity:!!this.rosganTvQuantity(lot),Pregn
18265ancy:this.rosganTvFixedPregnancy(lot),Weight:number(lot.peso_promedio)+' kg',HasWeight:Number(lot.peso_promedio)>0, 18266 QuantityFirst:this.rosganTvQuantityBreakdown(lot).split('\n')[0],QuantitySecond:this.rosganTvQuantityBreakdown(lot).split('\n')[1]||'', 18267 Location:[lot.nombre_localidad,lot.nombre_provincia].filter(Boolean).join(', '),Payment:lot.plazo||'',Tick:lot.garrapata||'', 18268 Consignataria:cons,ConsLogo:logo,ConsFullLogo:this.consFullHeroLogo(cons),HasConsLogo:!!logo,NoConsLogo:!logo,ConsInitial:cons.charAt(0),ConsColor:temporaryCons?.color||this.consBrandColor(cons), 18269 ShowChina:tv.visible!==false&&tv.lowerThirdVisible!==false&&this.rosganTvIsAptoChina(lot), 18270 ShowVisec:tv.visible!==false&&tv.lowerThirdVisible!==false&&this.rosganTvIsAptoVisec(lot), 18271 ShowBrandBug:tv.visible!==false&&tv.logosVisible!==false,ShowLower:tv.visible!==false&&tv.lowerThirdVisible!==false, 18272 Contacts:contacts,ShowContacts:tv.visible!==false&&tv.lowerThirdVisible!==false&&tv.contactsVisible!==false&&!!contacts, 18273 HasExtraFields:!!field,ExtraLabel:field?.label||'',ExtraValue:field?.value||'',Observations:observations,HasObservations:!!observations&&!/^(?:\?|[-ââ]|null|sin datos?)$/i.test(observations),ObservationsDuration:Math.max(18,observations.length/8)}]; 18274 }); 18275 } 18276 async resolveRosganTvVideo(lot) { 18277 if (lot.videoUrl || lot.video_uri || lot.__videoResolving || Date.now() - (lot.__tvVideoRetryAt || 0) < 15000) return; 18278 const remateId = this.remateRouteId(this.state.currentRemate); 18279 const selectedCert = this.remateLotRouteId(lot); 18280 lot.__videoResolving = true; 18281 try { 18282 const response = await this.apiFetch(this.rosganLotVideosEndpoint(remateId, selectedCert), { cache: 'no-store' }); 18283 if (this.remateRouteId(this.state.currentRemate) !== remateId) return; 18284 const videos = this.rosganArray(response?.videos || response); 18285 const primary = videos.find((item) => item?.url || item?.publicUrl || item?.videoUrl || item?.downloadUrl || item?.signedUrl); 18286 const url = primary?.url || primary?.publicUrl || primary?.videoUrl || primary?.downloadUrl || primary?.signedUrl || ''; 18287 if (url) { 18288 this.setState({ remateLots: this.state.remateLots.map((item) => this.remateLotRouteId(item) === selectedCert ?{ ...item, videoUrl: url, video_uri: url, __videoResolving: false } : item) }); 18289 this.ensureRosganOpsSelectedVideo(); 18290 } 18291 else lot.__tvVideoRetryAt = Date.now(); 18292 } catch { lot.__tvVideoRetryAt = Date.now(); } 18293 finally { lot.__videoResolving = false; } 18294 } 18295 remateLotAlertKey(remateId, lotCert) { 18296 return String(remateId || '') + '|' + String(lotCert || ''); 18297 } 18298 async loadRemateLotAlerts(remateId) { 18299 const id = String(remateId || '').trim(); 18300 if (!id || !this.state.user) { 18301 this.setState({ remateLotAlerts: {}, remateLotAlertsLoading: false }); 18302 return; 18303 } 18304 this.setState({ remateLotAlertsLoading: true }); 18305 try { 18306 const response = await this.apiFetch('/api/notifications/remate-lot-alerts?remateId=' + encodeURIComponent(id), { cache: 'no-store' }); 18307 if (this.remateRouteId(this.state.currentRemate) !== id) return; 18308 const alerts = Array.isArray(response?.alerts) ?response.alerts : []; 18309 const byKey = alerts.reduce((result, alert) => { 18310 result[this.remateLotAlertKey(alert.remateId, alert.lotCert)] = alert; 18311 return result; 18312 }, {}); 18313 this.setState({ remateLotAlerts: byKey, remateLotAlertsLoading: false }); 18314 } catch (error) { 18315 if (this.remateRouteId(this.state.currentRemate) === id) this.setState({ remateLotAlertsLoading: false }); 18316 if (Number(error?.status) !== 401) console.warn('remate lot alerts', error); 18317 } 18318 } 18319 async toggleRemateLotAlert(remate, lot, event) { 18320 event?.preventDefault?.(); 18321 event?.stopPropagation?.(); 18322 if (!this.state.user) { 18323 this.showToast('Inicia sesion para recibir el aviso del lote.'); 18324 return; 18325 } 18326 const remateId = this.remateRouteId(remate); 18327 const lotCert = this.remateLotRouteId(lot); 18328 const key = this.remateLotAlertKey(remateId, lotCert); 18329 if (!remateId || !lotCert || this.state.remateLotAlertBusy === key) return; 18330 const current = this.state.remateLotAlerts?.[key]; 18331 const enabled = current?.enabled === true && current?.status !== 'notified'; 18332 this.setState({ remateLotAlertBusy: key }); 18333 try { 18334 if (enabled) { 18335 await this.apiFetch('/api/notifications/remate-lot-alerts', { 18336 method: 'DELETE', 18337 body: JSON.stringify({ remateId, lotCert }), 18338 }); 18339 const next = { ...this.state.remateLotAlerts }; 18340 delete next[key]; 18341 this.setState({ remateLotAlerts: next, remateLotAlertBusy: '' }); 18342 this.showToast('Aviso cancelado.'); 18343 return; 18344 } 18345 const response = await this.apiFetch('/api/notifications/remate-lot-alerts', { 18346 method: 'POST', 18347 body: JSON.stringify({ remateId, lotCert }), 18348 }); 18349 this.setState({ 18350 remateLotAlerts: { ...this.state.remateLotAlerts, [key]: { ...(response?.alert || {}), remateId, lotCert, enabled: true, status: 'active' } }, 18351 remateLotAlertBusy: '', 18352 }); 18353 this.showToast('Aviso activado. Te contactaremos cuando el lote esté cerca.'); 18354 } catch (error) { 18355 this.setState({ remateLotAlertBusy: '' }); 18356 this.showToast(error?.message || 'No se pudo guardar el aviso.'); 18357 } 18358 } 18359 rosganSalesOrderValue(lot, index = 0) { 18360 const value = Number(lot?.saleOrder ?? lot?.lotNumber ?? lot?.order ?? lot?.orden_venta ?? lot?.orden_final ?? lot?.displayOrder); 18361 return Number.isFinite(value) && value > 0 ?value : index + 1; 18362 } 18363 normalizeRosganSalesOrder(lots) { 18364 return (Array.isArray(lots) ?lots : []).map((lot, index) => ({ 18365 ...lot, 18366 order: index + 1, 18367 saleOrder: index + 1, 18368 lotNumber: lot.lotNumber ?? lot.orden_final ?? lot.order ?? index + 1, 18369 orden_final: lot.lotNumber ?? lot.orden_final ?? lot.order ?? index + 1, 18370 orden_venta: index + 1, 18371 })); 18372 } 18373 compareRosganSalesOrder(a,b){ 18374 return this.rosganSalesOrderValue(a)-this.rosganSalesOrderValue(b) || Number(a.lotNumber ?? a.orden_final ?? 0)-Number(b.lotNumber ?? b.orden_final ?? 0); 18375 } 18376 sortedRosganSalesOrder(lots) { 18377 return [...(Array.isArray(lots) ?lots : [])].sort((a, b) => this.compareRosganSalesOrder(a,b)); 18378 } 18379 rosganSalesOrderMedia(lot = {}, videosOverride = null) { 18380 const urlOf = (video) => typeof video === 'string' ?video : (video?.url || video?.publicUrl || video?.videoUrl || video?.downloadUrl || video?.signedUrl || '');
18381 const kindOf = (video, fallback = 'crudo') => { 18382 const type = String(video?.videoType || video?.type || video?.kind || video?.category || video?.status || video?.fileName || video?.name || '').toLowerCase(); 18383 return video?.isFinal === true || /final|editado|aprobado/.test(type) ?'final' : fallback; 18384 }; 18385 const finalUrl = urlOf(lot.finalVideo); 18386 if (finalUrl) return { url: finalUrl, kind: 'final', label: 'FINAL' }; 18387 const media = (Array.isArray(videosOverride) ?videosOverride : [ 18388 ...(Array.isArray(lot.videos) ?lot.videos : []), 18389 ...(Array.isArray(lot.videoAssets) ?lot.videoAssets : []), 18390 ...(Array.isArray(lot.rawVideos) ?lot.rawVideos : []), 18391 ]).filter((video) => urlOf(video)); 18392 const final = media.find((video) => kindOf(video) === 'final'); 18393 if (final) return { url: urlOf(final), kind: 'final', label: 'FINAL' }; 18394 if (media[0]) return { url: urlOf(media[0]), kind: 'raw', label: 'CRUDO' }; 18395 const fallbackUrl = lot.videoUrl || lot.video_uri || ''; 18396 return fallbackUrl ?{ url: fallbackUrl, kind: 'final', label: 'FINAL' } : { url: '', kind: '', label: '' }; 18397 } 18398 async hydrateRosganSalesOrderMedia(remate, lots) { 18399 const remateId = this.remateRouteId(remate); 18400 if (!remateId || !Array.isArray(lots) || !lots.length) return; 18401 const queue = [...lots]; 18402 const resolved = new Map(); 18403 const worker = async () => { 18404 while (queue.length) { 18405 const lot = queue.shift(); 18406 const cert = this.remateLotRouteId(lot); 18407 if (!cert) continue; 18408 let media = this.rosganSalesOrderMedia(lot); 18409 try { 18410 const response = await this.apiFetch(this.rosganLotVideosEndpoint(remateId, cert), { cache: 'no-store' }); 18411 media = this.rosganSalesOrderMedia(lot, this.normalizeRosganLotVideos(response)); 18412 } catch {} 18413 resolved.set(cert, media); 18414 } 18415 }; 18416 await Promise.all(Array.from({ length: Math.min(4, lots.length) }, worker)); 18417 if (this.state.route !== 'rosganSalesOrder' || this.remateRouteId(this.state.currentRemate) !== remateId) return; 18418 this.setState((state) => ({ 18419 rosganSalesOrderLots: (state.rosganSalesOrderLots || []).map((lot) => { 18420 const media = resolved.get(this.remateLotRouteId(lot)); 18421 return media ?{ ...lot, salesVideoUrl: media.url, salesVideoKind: media.kind, salesVideoLabel: media.label } : lot; 18422 }), 18423 })); 18424 } 18425 async hydrateRosganSalesOrderMetadata(remate, lots) { 18426 const remateId = this.remateRouteId(remate); 18427 if (!remateId || !Array.isArray(lots) || !lots.length) return; 18428 let metadata = {}; 18429 try { 18430 const response = await fetch('/assets/remates/' + encodeURIComponent(remateId) + '/lot-metadata.json', { cache: 'no-store' }); 18431 if (response.ok) metadata = await response.json(); 18432 } catch {} 18433 const consignatariaIds = [...new Set(lots.map((lot) => String(lot?.consignatariaId || '').trim()).filter(Boolean))]; 18434 const establishments = new Map(); 18435 await Promise.all(consignatariaIds.map(async (consignatariaId) => { 18436 try { 18437 const response = await this.apiFetch('/api/rosgan-establishments?consignatariaId=' + encodeURIComponent(consignatariaId)); 18438 this.rosganArray(response).forEach((item) => { 18439 const id = String(item?.id || item?.establishmentId || item?.establecimientoId || '').trim(); 18440 if (id) establishments.set(id, item); 18441 }); 18442 } catch {} 18443 })); 18444 if (this.state.route !== 'rosganSalesOrder' || this.remateRouteId(this.state.currentRemate) !== remateId) return; 18445 const enrich = (lot, index) => { 18446 const cert = this.remateLotRouteId(lot); 18447 const stored = metadata?.[cert] && typeof metadata[cert] === 'object' ?metadata[cert] : {}; 18448 const establishmentId = String(lot?.establishmentId || lot?.establecimientoId || '').trim(); 18449 const establishment = establishments.get(establishmentId) || {}; 18450 return this.normalizeRosganActiveLot({ 18451 ...lot, 18452 locality: lot?.locality || lot?.nombre_localidad || lot?.localidad || lot?.establishmentLocality || lot?.establecimientoLocalidad || establishment?.locality || establishment?.localidad || stored.locality || '', 18453 province: lot?.province || lot?.nombre_provincia || lot?.provincia || lot?.establishmentProvince || lot?.establecimientoProvincia || establishment?.province || establishment?.provincia || stored.province || '', 18454 paymentTerm: lot?.paymentTerm || lot?.plazo || lot?.commercial?.paymentTerm || lot?.sale?.paymentTerm || stored.paymentTerm || '', 18455 }, index, remate); 18456 }; 18457 this.setState((state) => ({
18458 remateLots: (state.remateLots || []).map(enrich), 18459 rosganSalesOrderLots: (state.rosganSalesOrderLots || []).map(enrich), 18460 })); 18461 } 18462 setRosganSalesOrderRating(lot, value) { 18463 const cert = this.remateLotRouteId(lot); 18464 const rating = Number(value); 18465 if (!cert || !Number.isInteger(rating) || rating < 1 || rating > 6) return; 18466 const current = this.loadRosganOpsState(this.state.currentRemate); 18467 this.saveRosganOpsState({ ...current, ratings: { ...current.ratings, [cert]: rating } }, this.state.currentRemate); 18468 } 18469 toggleRosganSalesOrderVideo(lot) { 18470 const cert = this.remateLotRouteId(lot); 18471 if (!cert) return; 18472 this.setState({ rosganSalesOrderVideoCert: this.state.rosganSalesOrderVideoCert === cert ?'' : cert }); 18473 } 18474 rosganSalesOrderVideoStage(event) { 18475 return event?.currentTarget?.closest?.('.sales-order-video-stage') || event?.target?.closest?.('.sales-order-video-stage') || null; 18476 } 18477 async playRosganSalesOrderVideo(lot, event) { 18478 const cert = this.remateLotRouteId(lot); 18479 const stage = this.rosganSalesOrderVideoStage(event); 18480 const video = stage?.querySelector?.('video') || (cert ?document.getElementById('sales-order-video-' + cert) : null); 18481 if (!video) { this.showToast('No se encontró el reproductor del lote.'); return; } 18482 try { 18483 video.muted = false; 18484 await video.play(); 18485 stage?.classList?.add('is-playing'); 18486 stage?.classList?.remove('is-error'); 18487 } catch { 18488 stage?.classList?.add('is-error'); 18489 this.showToast('El navegador no pudo iniciar el video. Usá âAbrir video directamenteâ.'); 18490 } 18491 } 18492 markRosganSalesOrderVideoPlaying(event) { 18493 const stage = this.rosganSalesOrderVideoStage(event); 18494 stage?.classList?.add('is-playing'); 18495 stage?.classList?.remove('is-error'); 18496 } 18497 markRosganSalesOrderVideoPaused(event) { 18498 this.rosganSalesOrderVideoStage(event)?.classList?.remove('is-playing'); 18499 } 18500 markRosganSalesOrderVideoError(event) { 18501 const stage = this.rosganSalesOrderVideoStage(event); 18502 stage?.classList?.remove('is-playing'); 18503 stage?.classList?.add('is-error'); 18504 } 18505 openRosganSalesOrder() { 18506 this.openRosganSalesOrderPage(this.state.currentRemate, null, true, true); 18507 } 18508 recalculateRosganSalesOrderSchedule(lots, value = this.state.rosganSalesOrderSchedule) { 18509 const schedule = this.normalizeRosganSalesOrderSchedule(value); 18510 if (schedule.startSeconds == null || !schedule.entries.length) return schedule; 18511 const byCert = new Map(schedule.entries.map((entry) => [entry.cert, entry])); 18512 let elapsed = 0; 18513 const entries = (Array.isArray(lots) ?lots : []).flatMap((lot) => { 18514 const previous = byCert.get(this.remateLotRouteId(lot)); 18515 if (!previous) return []; 18516 const next = { ...previous, estimatedStartSeconds: schedule.startSeconds + elapsed }; 18517 elapsed += previous.averageSeconds; 18518 return [next]; 18519 }); 18520 return this.normalizeRosganSalesOrderSchedule({ ...schedule, entries, endSeconds: schedule.startSeconds + elapsed }); 18521 } 18522 rosganExcelCellValue(value) { 18523 if (value == null || value instanceof Date || typeof value !== 'object') return value; 18524 if (value.result != null) return this.rosganExcelCellValue(value.result); 18525 if (value.text != null) return value.text; 18526 if (Array.isArray(value.richText)) return value.richText.map((part) => part?.text || '').join(''); 18527 return String(value); 18528 } 18529 async exportRosganPreorderExcel() { 18530 if (!this.canManageUsers() || this.state.rosganPreorderExporting || this.state.remateLotsLoading) return; 18531 const remate = this.state.currentRemate, remateId = this.remateRouteId(remate); 18532 if (!remateId) return; 18533 const company = this.state.rosganSalesOrderConsignatariaFilter || ''; 18534 const ratings = { ...this.state.rosganSalesOrderRatings }; 18535 const companyIds = new Set((this.state.rosganSalesOrderLots || []).filter(lot => (this.consDisplayName(lot.nombre_consignatario || lot.consignatariaNombre || remate.nombre_cuenta || remate.nombre_consignatario) || 'Sin consignataria') === company).map(lot => String(lot.consignatariaId || ''))); 18536 this.setState({ rosganPreorderExporting: true, rosganSalesOrderError: '' }); 18537 try { 18538 const [fullResponse, auditResponse] = await Promise.all([ 18539 this.apiFetch('/api/remates/' + encodeURIComponent(remateId) + '/rosgan-lots/all', { cache: 'no-store' }), 18540 this.apiFetch('/api/auditor/remates/' + encodeURIComponent(remateId) + '/lotes', { cache: 'no-store' }), 18541 ]); 18542 const fullByCert = new Map(this.rosganArray(fullResponse).map(lot => [String(lot.lotCert || lot.cert || lot.id), lot])); 18543 const response = this.rosganArray(auditResponse).map(lot => { 18544 const full = fullByCert.get(String(lot.lotCert || lot.cert || lot.id)); 18545 if (!full) throw new Error('No se pudo obtener el detalle completo del certificado ' + lot.lotCert + '. Reintentá la descarga.'); 18546 return { ...lot, ...full, consignatariaName: full.consignatariaName || lot.consignatariaName, 18547 locality: lot.locality || full.locality, province: lot.province || full.province, 18548 paymentTerm: lot.paymentTerm || full.paymentTerm }; 18549 }); 18550 const lots = this.rosganArray(response).map(lot => ({ ...lot, consignatariaName: lot.consignatariaName || this.consName((this.state.consignatarias || []).find(item => String(item.id) === String(lot.consignatariaId)) || {}) || lot.consignatariaId })); 18551 const selected = lots.filter(lot => window.RosganPreorderExcel.isEligible(lot) && (!company || companyIds.has(String(lot.consignatariaId || '')))); 18552 if (!selected.length) throw new Error('No hay lotes para generar el preorden de esta selección.'); 18553 const ExcelJS = await this.ensureCabanaExcelJs(); 18554 const book = window.RosganPreorderExcel.build(ExcelJS, selected, { ratings, companyColor: value => this.consBrandColor(value) }); 18555 const blob = new Blob([await book.xlsx.writeBuffer()], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); 18556 this.downloadCabanaBlob(blob, window.RosganPreorderExcel.filename(remate.nombre_remate || remate.title || remate.name || 'Remate ' + remateId, company)); 18557 this.showToast('Preorden de venta descargado.'); 18558 } catch (error) { this.setState({ rosganSalesOrderError: error?.message || 'No se pudo generar el preorden. Reintentá la descarga.' }); } 18559 finally { this.setState({ rosganPreorderExporting: false }); } 18560 }
18561 async importRosganSalesOrderExcel(event) { 18562 const input = event?.target; 18563 const file = input?.files?.[0]; 18564 if (input) input.value = ''; 18565 if (!file || this.state.rosganSalesOrderImporting || this.state.rosganSalesOrderSaving) return; 18566 if (!/\.xlsx$/i.test(file.name || '')) { 18567 this.setState({ rosganSalesOrderError: 'El archivo debe estar en formato .xlsx.' }); 18568 return; 18569 } 18570 if (Number(file.size) > 8 * 1024 * 1024) { 18571 this.setState({ rosganSalesOrderError: 'El Excel supera el máximo permitido de 8 MB.' }); 18572 return; 18573 } 18574 this.setState({ rosganSalesOrderImporting: true, rosganSalesOrderError: '', rosganSalesOrderSaved: false, rosganOrderCommandMessage: '' }); 18575 try { 18576 const ExcelJS = await this.ensureCabanaExcelJs(); 18577 const workbook = new ExcelJS.Workbook(); 18578 await workbook.xlsx.load(await file.arrayBuffer()); 18579 const worksheet = workbook.worksheets.find((sheet) => Number(sheet.actualRowCount || sheet.rowCount) > 0); 18580 if (!worksheet) throw new Error('El Excel no contiene hojas con datos.'); 18581 const columnCount = Math.max(4, Math.min(50, Number(worksheet.actualColumnCount || worksheet.columnCount) || 4)); 18582 const rows = []; 18583 worksheet.eachRow({ includeEmpty: true }, (row) => { 18584 rows.push(Array.from({ length: columnCount }, (_, index) => this.rosganExcelCellValue(row.getCell(index + 1).value))); 18585 }); 18586 const plan = window.RosganSalesOrderImport.parse(rows); 18587 const ordered = window.RosganSalesOrderImport.apply(plan, this.state.rosganSalesOrderLots || [], (lot) => this.remateLotRouteId(lot)); 18588 const schedule = this.normalizeRosganSalesOrderSchedule({ 18589 fileName: file.name, 18590 importedAt: new Date().toISOString(), 18591 startSeconds: plan.startSeconds, 18592 endSeconds: plan.endSeconds, 18593 entries: plan.entries, 18594 }); 18595 this.setState({ 18596 rosganSalesOrderLots: this.normalizeRosganSalesOrder(ordered), 18597 rosganSalesOrderSchedule: schedule, 18598 rosganSalesOrderImporting: false, 18599 rosganSalesOrderError: '', 18600 rosganSalesOrderSaved: false, 18601 rosganOrderCommandMessage: 'Excel cargado: revisá el orden y guardalo para confirmarlo.', 18602 }); 18603 } catch (error) { 18604 this.setState({ rosganSalesOrderImporting: false, rosganSalesOrderError: error?.message || 'No se pudo interpretar el Excel.' }); 18605 } 18606 } 18607 openRosganSalesOrderPage(item, lotsOverride = null, syncPath = true, forceFetch = false) { 18608 const remate = this.normalizeRosganActiveRemate(item || {}); 18609 const remateId = this.remateRouteId(remate); 18610 if (!remateId) return; 18611 const useCurrent = this.remateRouteId(this.state.currentRemate) === remateId && Array.isArray(this.state.remateLots); 18612 const source = Array.isArray(lotsOverride) ?lotsOverride : (!forceFetch && useCurrent ?this.state.remateLots : null); 18613 const openWithLots = (lots) => { 18614 const normalized = (Array.isArray(lots) ?lots : []) 18615 .filter((lot) => !['anulado', 'cerrado'].includes(this.rosganLotWorkflowStatus(lot))) 18616 .map((lot, index) => lot.__rosganActiveLot?lot:this.normalizeRosganActiveLot(lot, index)); 18617 const stored = this.loadRosganOpsState(remate); 18618 this.setState({ 18619 route: 'rosganSalesOrder', rosganOrderCommandMessage:'', currentRemate: remate, remateLots: normalized, remateLotsLoading: false, 18620 rosganSalesOrderLots: this.sortedRosganSalesOrder(normalized), rosganSalesOrderSaving: false, 18621 rosganSalesOrderError: '', rosganSalesOrderSaved: false, rosganSalesOrderVideoCert: '', rdLightbox: null, 18622 rosganSalesOrderQuery: '', rosganSalesOrderStatusFilter: '', rosganSalesOrderCategoryFilter: '', rosganSalesOrderConsignatariaFilter: '', rosganSalesOrderRatingFilter: '', rosganSalesOrderRatingSnapshot: null, 18623 rosganOpsDrafts: stored.commercial, rosganOpsConfig: stored.tv, rosganSalesOrderRatings: stored.ratings, rosganSalesOrderSchedule: stored.salesOrder, 18624 }, () => { 18625 this.fetchRosganOpsRemoteState({ force: true, silent: true }); 18626 this.fetchCatalogPublication(remate); 18627 this.hydrateRosganSalesOrderMetadata(remate, normalized); 18628 this.hydrateRosganSalesOrderMedia(remate, normalized); 18629 }); 18630 window.scrollTo(0, 0); 18631 }; 18632 if (syncPath) {
18633 try { window.history.pushState({}, '', '/remates/' + encodeURIComponent(remateId) + '/orden-ventas'); } catch {} 18634 } 18635 if (source) { 18636 openWithLots(source); 18637 return; 18638 } 18639 this.setState({ 18640 route: 'rosganSalesOrder', currentRemate: remate, remateLotsLoading: true, 18641 rosganSalesOrderLots: [], rosganSalesOrderSaving: false, rosganSalesOrderImporting: false, rosganSalesOrderSchedule: null, rosganSalesOrderError: '', rosganSalesOrderSaved: false, 18642 rosganSalesOrderQuery: '', rosganSalesOrderStatusFilter: '', rosganSalesOrderCategoryFilter: '', rosganSalesOrderConsignatariaFilter: '', rosganSalesOrderRatingFilter: '', rosganSalesOrderRatingSnapshot: null, 18643 }); 18644 Promise.all([ 18645 this.apiFetch('/api/auditor/remates/' + encodeURIComponent(remateId) + '/lotes', { cache: 'no-store' }).catch(() => null), 18646 this.apiFetch('/api/remates/' + encodeURIComponent(remateId) + '/rosgan-lots', { cache: 'no-store' }).catch(() => null), 18647 ]) 18648 .then(([auditorResponse, catalogResponse]) => { 18649 const audited = auditorResponse ?this.rosganArray(auditorResponse) : []; 18650 const catalog = catalogResponse ?this.rosganArray(catalogResponse) : []; 18651 if (!audited.length && !catalog.length) throw new Error('No se pudieron cargar los lotes.'); 18652 const catalogByCert = new Map(catalog.map((lot) => [this.remateLotRouteId(lot), lot])); 18653 openWithLots(audited.length ?audited.map((lot) => ({ ...(catalogByCert.get(this.remateLotRouteId(lot)) || {}), ...lot })) : catalog); 18654 }) 18655 .catch((error) => this.setState({ remateLotsLoading: false, rosganSalesOrderError: error?.message || 'No se pudieron cargar los lotes.' })); 18656 } 18657 async openRosganSalesOrderById(remateId) { 18658 const requestedPath = window.location.pathname; 18659 try { 18660 const response = await this.apiFetch('/api/Remates/' + encodeURIComponent(remateId)); 18661 if (window.location.pathname !== requestedPath) return; 18662 const remate = response?.data ?? response; 18663 if (!remate || remate?.cabanasConfig) throw new Error('No es un remate Rosgan activo.'); 18664 return this.openRosganSalesOrderPage(remate, null, false, true); 18665 } catch (error) { 18666 if (window.location.pathname !== requestedPath) return; 18667 this.showToast(error?.message || 'No se pudo abrir el orden de venta.'); 18668 this.go('remates'); 18669 } 18670 } 18671 closeRosganSalesOrder() { 18672 if (this.state.rosganSalesOrderSaving) return; 18673 const remate = this.state.currentRemate; 18674 this.setState({ route: 'remateDetail', rdSort: 'orden', rosganSalesOrderError: '', rosganSalesOrderSaved: false }); 18675 this.syncRemateLocation(remate, null, { replace: true }); 18676 window.scrollTo(0, 0); 18677 } 18678 moveRosganSalesOrder(index, offset) { 18679 const lots = [...(this.state.rosganSalesOrderLots || [])]; 18680 const target = index + offset; 18681 if (index < 0 || target < 0 || index >= lots.length || target >= lots.length) return; 18682 const [moved] = lots.splice(index, 1); 18683 lots.splice(target, 0, moved); 18684 const normalized = this.normalizeRosganSalesOrder(lots); 18685 this.setState({ rosganSalesOrderLots: normalized, rosganSalesOrderSchedule: this.recalculateRosganSalesOrderSchedule(normalized), rosganSalesOrderError: '', rosganSalesOrderSaved: false }); 18686 } 18687 setRosganSalesOrderPosition(index, event) { 18688 const lots = [...(this.state.rosganSalesOrderLots || [])]; 18689 const requested=Number(event?.target?.value); 18690 if(!Number.isInteger(requested)||requested<1){if(event?.target)event.target.value=index+1;return;} 18691 const target = Math.max(0, Math.min(lots.length - 1, requested - 1)); 18692 if (index < 0 || index >= lots.length || target === index) return; 18693 const [moved] = lots.splice(index, 1); 18694 lots.splice(target, 0, moved); 18695 const normalized = this.normalizeRosganSalesOrder(lots); 18696 this.setState({ rosganSalesOrderLots: normalized, rosganSalesOrderSchedule: this.recalculateRosganSalesOrderSchedule(normalized), rosganSalesOrderError: '', rosganSalesOrderSaved: false }); 18697 } 18698 applyRosganOrderCommand(){ 18699 if(this.state.rosganSalesOrderSaving||this.state.remateLotsLoading)return; 18700 try{ 18701 const result=window.RosganOrderCommand.move(this.state.rosganSalesOrderLots||[],document.getElementById('rg-order-command')?.value); 18702 const normalized=this.normalizeRosganSalesOrder(result.lots); 18703 this.setState({rosganSalesOrderLots:normalized,rosganSalesOrderSchedule:this.recalculateRosganSalesOrderSchedule(normalized),rosganSalesOrderError:'',rosganSalesOrderSaved:false,rosganOrderCommandMessage:result.message}); 18704 }catch(error){this.setState({rosganSalesOrderError:error.message,rosganOrderCommandMessage:''});} 18705 } 18706 async saveRosganSalesOrder() { 18707 if (!this.canManageUsers() || this.state.rosganSalesOrderSaving) return; 18708 const remateId = this.remateRouteId(this.state.currentRemate); 18709 const ordered = this.normalizeRosganSalesOrder(this.state.rosganSalesOrderLots || []); 18710 const orders = ordered.map((lot, index) => ({ lotCert: this.remateLotRouteId(lot), displayOrder: index + 1 })).filter((item) => item.lotCert); 18711 if (!remateId || !orders.length) { 18712 this.setState({ rosganSalesOrderError: 'No hay lotes para ordenar.' }); 18713 return; 18714 } 18715 this.setState({ rosganSalesOrderSaving: true, rosganSalesOrderError: '', rosganSalesOrderSaved: false }); 18716 try { 18717 const publication = await this.apiFetch('/api/catalog-publication/' + encodeURIComponent(remateId) + '/sales-order', { 18718 method: 'PUT', body: JSON.stringify({ orders, schedule: this.recalculateRosganSalesOrderSchedule(ordered) }), 18719 }); 18720 this.setState({ catalogPublication: { key: this.catalogPublicationKey(), published: publication.published === true, salesOrderDefined: true, loaded: true, busy: false } }); 18721 const currentOps = this.loadRosganOpsState(this.state.currentRemate); 18722 this.saveRosganOpsState({ ...currentOps, salesOrder: this.recalculateRosganSalesOrderSchedule(ordered) }, this.state.currentRemate); 18723 this.setState({ remateLots: ordered, rosganSalesOrderLots: ordered, rosganOrderCommandMessage:'', rosganSalesOrderSaving: false, rosganSalesOrderSaved: true }); 18724 } catch (error) { 18725 this.setState({ rosganSalesOrderSaving: false, rosganSalesOrderError: error?.message || 'No se pudo guardar el orden de venta.' }); 18726 } 18727 } 18728 async fetchRemateLots(item, lotId = '') { 18729 const id = item.id_remate || item.sourceId || item.id || ''; 18730 const requestedPath = window.location.pathname; 18731 this.setState({ remateLotsLoading: true, remateLotsReal: false }); 18732 try { 18733 const data = await this.unwrap('/api/remateshistorico?remateId=' + encodeURIComponent(id) + '&lots=1'); 18734 if (data === null) throw 0; 18735 const lots = Array.isArray(data?.items) ?data.items : (Array.isArray(data) ?data : []); 18736 const currentRemate = { ...item, ...(data?.remate || {}) }; 18737 if (window.location.pathname !== requestedPath) return;
18738 const targetLot = lotId ?lots.find((candidate) => this.remateLotRouteId(candidate) === String(lotId)) || null : null; 18739 this.setState({ remateLots: lots, currentRemate, rdLightbox: targetLot, remateLotsReal: true, remateLotsLoading: false, cabanaLotVideoPlaying: false, cabanaLotVideoProgress: 0, cabanaLotVideoDuration: 0, cabanaLotVideoVolume: 0, cabanaLotVideoMuted: true }); 18740 } catch { if (window.location.pathname !== requestedPath) return; this.setState({ remateLots: [], remateLotsReal: false, remateLotsLoading: false }); } 18741 } 18742 remateLotNeighbors(lot, lots = this.state.remateLots) { 18743 const ordered = [...this.publicRemateLots(lots)].sort((a, b) => this.compareRosganSalesOrder(a, b)); 18744 const cert = lot && this.remateLotRouteId(lot); 18745 const index = cert ? ordered.findIndex(item => this.remateLotRouteId(item) === cert) : -1; 18746 return { previous: index > 0 ? ordered[index - 1] : null, next: index >= 0 ? ordered[index + 1] || null : null }; 18747 } 18748 openRemateLot(item, options = {}) { 18749 if (!item || !this.state.currentRemate) return; 18750 if (this.state.currentRemate.__rosganActive && !this.isRosganLotPublished(item)) return; 18751 if (options.syncPath !== false) this.syncRemateLocation(this.state.currentRemate, item); 18752 this.fetchRosganRepresentative(item); 18753 this.setState({ rdLightbox: item, cabanaLotVideoPlaying: false, cabanaLotVideoProgress: 0, cabanaLotVideoDuration: 0, cabanaLotVideoVolume: 0, cabanaLotVideoMuted: true }); 18754 if (this.state.currentRemate.__rosganActive) this.fetchRosganLotVideos(this.remateRouteId(this.state.currentRemate), this.remateLotRouteId(item)); 18755 } 18756 closeRemateLot() { 18757 if (this.state.currentRemate) this.syncRemateLocation(this.state.currentRemate, null, { replace: true }); 18758 this.setState({ rdLightbox: null, cabanaLotVideoPlaying: false, cabanaLotVideoProgress: 0, cabanaLotVideoDuration: 0, cabanaLotVideoVolume: 0, cabanaLotVideoMuted: true }); 18759 } 18760 rosganLotVideoKey(remateId, lotCert) { return String(remateId) + ':' + String(lotCert); } 18761 async prepareFieldLot(context = null) { 18762 const remateId = context?.remateId || this.remateRouteId(this.state.currentRemate); 18763 const cert = context?.lotCert || this.remateLotRouteId(this.state.rdLightbox); 18764 const lot = context?.lotCert ?context : this.state.rdLightbox; 18765 if (!remateId || !cert || !this.canEditRosganLot(lot) || this._preparingFieldLot) return; 18766 this._preparingFieldLot = true; 18767 this.showToast('Preparando cámara y lote para usar sin señalâ¦'); 18768 try { await window.RosganField.prepare(remateId, cert); } 18769 catch (error) { this.showToast(error.message || 'No se pudo preparar el modo campo.'); } 18770 finally { this._preparingFieldLot = false; } 18771 } 18772 rosganLotVideosEndpoint(remateId, lotCert, suffix = '') { 18773 return '/api/remates/' + encodeURIComponent(remateId) + '/rosgan-lots/' + encodeURIComponent(lotCert) + '/videos' + suffix; 18774 } 18775 updateRosganLotVideoState(key, patch) { 18776 this.setState((state) => ({ rosganLotVideoState: { ...state.rosganLotVideoState, [key]: { loading: false, videos: [], uploads: [], error: '', ...(state.rosganLotVideoState?.[key] || {}), ...patch } } })); 18777 } 18778 normalizeRosganLotVideos(data) { 18779 const root = data?.data ?? data; 18780 if (Array.isArray(root)) return root; 18781 for (const key of ['items', 'videos', 'results']) if (Array.isArray(root?.[key])) return root[key]; 18782 return root?.id || root?.videoId ?[root] : []; 18783 } 18784 formatFileSize(bytes) { 18785 const size = Number(bytes) || 0; 18786 if (!size) return 'Tamaño no informado'; 18787 if (size < 1024 * 1024) return Math.max(1, Math.round(size / 1024)) + ' KB'; 18788 return (size / (1024 * 1024)).toLocaleString('es-AR', { maximumFractionDigits: 1 }) + ' MB'; 18789 } 18790 async fetchRosganLotVideos(remateId, lotCert, force = false) { 18791 if (!remateId || !lotCert) return; 18792 const key = this.rosganLotVideoKey(remateId, lotCert); 18793 const current = this.state.rosganLotVideoState?.[key]; 18794 if (current?.loading || (!force && Array.isArray(current?.videos))) return; 18795 this.updateRosganLotVideoState(key, { loading: true, error: '' }); 18796 try { 18797 const response = await this.apiFetch(this.rosganLotVideosEndpoint(remateId, lotCert)); 18798 this.updateRosganLotVideoState(key, { loading: false, videos: this.normalizeRosganLotVideos(response), error: '' }); 18799 this.restoreRosganRawVideoSession(remateId, lotCert); 18800 } catch (error) { 18801 this.updateRosganLotVideoState(key, { loading: false, error: error?.message || 'No se pudieron cargar los videos.' }); 18802 } 18803 } 18804 updateRosganVideoUpload(key, uploadId, patch) { 18805 this.setState((state) => { 18806 const entry = state.rosganLotVideoState?.[key] || { loading: false, videos: [], uploads: [], error: '' }; 18807 const uploads = (entry.uploads || []).map((upload) => upload.id === uploadId ?{ ...upload, ...patch } : upload); 18808 return { rosganLotVideoState: { ...state.rosganLotVideoState, [key]: { ...entry, uploads } } }; 18809 }); 18810 } 18811 async uploadRosganLotVideos(fileList, context = null) { 18812 const explicitContext = !!(context?.remateId && context?.lotCert); 18813 const remateId = context?.remateId || this.remateRouteId(this.state.currentRemate); 18814 const lotCert = context?.lotCert || this.remateLotRouteId(this.state.rdLightbox); 18815 const lot = explicitContext ?context : this.state.rdLightbox; 18816 if ((!explicitContext && !this.state.currentRemate?.__rosganActive) || !remateId || !lotCert || !this.canEditRosganLot(lot)) return; 18817 const files = Array.from(fileList || []), key = this.rosganLotVideoKey(remateId, lotCert); 18818 this._rawVideoBatches ||= new Set(); 18819 if (this._rawVideoBatches.has(key)) return; 18820 const validation = window.RosganRawVideoBatch.validate(files); 18821 if (validation) { this.updateRosganLotVideoState(key, { error: validation }); return; } 18822 try { 18823 await window.RosganVideoQueue.enqueue(remateId,lotCert,files,'raw'); 18824 this.updateRosganLotVideoState(key,{error:''}); 18825 this.showToast('Videos guardados en la cola. Podés seguir el progreso en el panel de cargas.'); 18826 }catch(error){this.updateRosganLotVideoState(key,{error:error.message||'No se pudo iniciar la carga.'});} 18827 } 18828 18829 createRosganRawVideoSession(key, remateId, lotCert, files, pendingMetadata = []) { 18830 const uid = this.state.user?.uid; 18831 const session = window.RosganRawVideoBatch.createSession((...args) => { 18832 if (this.state.user?.uid !== uid) throw Error('La sesión cambió. Volvé a ingresar con la cuenta que inició la subida.'); 18833 return this.apiFetch(...args); 18834 }, remateId, lotCert, files, { 18835 pendingMetadata, 18836 onChange: snapshot => { if (this.state.user?.uid === uid) this.syncRosganRawVideoSession(key, snapshot); }, 18837 }); 18838 this._rawVideoSessions ||= new Map(); 18839 this._rawVideoSessions.set(key, { session, uid }); 18840 return session; 18841 } 18842 restoreRosganRawVideoSession(remateId, lotCert) {
18843 const key = this.rosganLotVideoKey(remateId, lotCert), uid = this.state.user?.uid; 18844 if (!uid || this._rawVideoSessions?.get(key)?.uid === uid) return; 18845 try { 18846 const storageKey = 'rosgan-raw-metadata:' + uid + ':' + key; 18847 const pending = JSON.parse(window.sessionStorage.getItem(storageKey) || '[]'); 18848 const current = this.state.rosganLotVideoState?.[key]?.videos || []; 18849 const missing = pending.filter(m => !current.some(v => v.fileKey === m.fileKey)); 18850 if (!missing.length) { window.sessionStorage.removeItem(storageKey); return; } 18851 const session = this.createRosganRawVideoSession(key, remateId, lotCert, [], missing); 18852 this.syncRosganRawVideoSession(key, session.snapshot()); 18853 } catch {} 18854 } 18855 syncRosganRawVideoSession(key, snapshot) { 18856 try { 18857 const storageKey = 'rosgan-raw-metadata:' + this.state.user?.uid + ':' + key; 18858 if (snapshot.pendingMetadata?.length) window.sessionStorage.setItem(storageKey, JSON.stringify(snapshot.pendingMetadata)); 18859 else window.sessionStorage.removeItem(storageKey); 18860 } catch {} 18861 this.setState(state => { 18862 const current = state.rosganLotVideoState?.[key] || { videos: [] }; 18863 const merged = new Map([...(current.videos || []), ...snapshot.jobs.map(j => j.video).filter(Boolean)].map(v => [v.id, v])); 18864 return { rosganLotVideoState: { ...state.rosganLotVideoState, [key]: { ...current, uploading: snapshot.running, uploads: snapshot.jobs, videos: [...merged.values()].sort((a,b) => a.order-b.order), error: snapshot.jobs.find(j => j.error)?.error || '' } } }; 18865 }); 18866 } 18867 async runRosganRawVideoSession(key, remateId, lotCert, session, retryId = null) { 18868 this._rawVideoBatches ||= new Set(); 18869 if (this._rawVideoBatches.has(key)) return; 18870 this._rawVideoBatches.add(key); 18871 try { 18872 const result = retryId == null ? await session.start() : await session.retry(retryId); 18873 if (this._rawVideoSessions?.get(key)?.uid !== this.state.user?.uid) return; 18874 if (result.jobs.every(j => j.status === 'Listo')) this.showToast(result.jobs.length + ' video(s) cargados correctamente.'); 18875 else { 18876 await this.fetchRosganLotVideos(remateId, lotCert, true); 18877 this.syncRosganRawVideoSession(key, session.snapshot()); 18878 } 18879 } finally { this._rawVideoBatches.delete(key); } 18880 } 18881 async retryRosganVideoUpload(uploadId, context = null) { 18882 const remateId = context?.remateId || this.remateRouteId(this.state.currentRemate); 18883 const lotCert = context?.lotCert || this.remateLotRouteId(this.state.rdLightbox); 18884 const lot = context?.remateId && context?.lotCert ? context : this.state.rdLightbox; 18885 if (!remateId || !lotCert || !this.canEditRosganLot(lot)) return; 18886 const key = this.rosganLotVideoKey(remateId, lotCert), entry = this._rawVideoSessions?.get(key); 18887 if (entry?.uid !== this.state.user?.uid || !entry?.session) return; 18888 await this.runRosganRawVideoSession(key, remateId, lotCert, entry.session, uploadId); 18889 } 18890 18891 async deleteRosganLotVideo(video, context = null) { 18892 const remateId = context?.remateId || this.remateRouteId(this.state.currentRemate); 18893 const lotCert = context?.lotCert || this.remateLotRouteId(this.state.rdLightbox); 18894 const videoId = video?.id || video?.videoId || video?.videoID; 18895 const lot = context?.remateId && context?.lotCert ?context : this.state.rdLightbox; 18896 if (!remateId || !lotCert || !videoId || !this.canEditRosganLot(lot) || String(video?.videoType || video?.type || '').toLowerCase() !== 'raw' || this.state.rosganLotVideoDeleting || !window.confirm('¿Eliminar este video del lote?')) return; 18897 const deletingKey = this.rosganLotVideoKey(remateId, lotCert) + ':' + videoId; 18898 this.setState({ rosganLotVideoDeleting: deletingKey }); 18899 try { 18900 await this.apiFetch(this.rosganLotVideosEndpoint(remateId, lotCert, '/' + encodeURIComponent(videoId)), { method: 'DELETE' }); 18901 this.setState({ rosganLotVideoDeleting: '' }); 18902 await this.fetchRosganLotVideos(remateId, lotCert, true); 18903 this.showToast('Video eliminado.'); 18904 } catch (error) { 18905 this.setState({ rosganLotVideoDeleting: '' }); 18906 this.updateRosganLotVideoState(this.rosganLotVideoKey(remateId, lotCert), { error: error?.message || 'No se pudo eliminar el video.' }); 18907 } 18908 } 18909 consName(c) { return (c && (c.name || c.nombre || c.slug)) || (typeof c === 'string' ?c : 'Canal'); } 18910 humanizeSlug(value) {
18911 return String(value || '').split(/[-_]+/).filter(Boolean).map((part) => { 18912 const lower = part.toLowerCase(); 18913 if (lower === 'y') return 'y'; 18914 if (lower === 's' || lower === 'a') return lower.toUpperCase() + '.'; 18915 return lower.charAt(0).toUpperCase() + lower.slice(1); 18916 }).join(' ').replace(/\bS\. A\./g, 'S.A.'); 18917 } 18918 // Imágenes del hero del canal ? placeholders de hacienda (reemplazables luego) 18919 channelHeroImages = ['assets/angus-field.png', 'assets/cabanas.png', 'assets/feria.png', 'assets/access-cabanas.png', 'assets/access-remates.png']; 18920 // Imágenes del hero de la home ? placeholders de hacienda (reemplazables luego) 18921 homeHeroImages = ['assets/angus-field.png', 'assets/feria.png', 'assets/cabanas.png', 'assets/access-remates.png', 'assets/access-cabanas.png']; 18922 rgba(color, a) { 18923 const value = String(color || '').trim(); 18924 const hex = value.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); 18925 if (hex) { 18926 const raw = hex[1].length === 3 ?hex[1].split('').map((char) => char + char).join('') : hex[1]; 18927 return 'rgba(' + parseInt(raw.slice(0, 2), 16) + ',' + parseInt(raw.slice(2, 4), 16) + ',' + parseInt(raw.slice(4, 6), 16) + ',' + a + ')'; 18928 } 18929 const rgb = value.match(/(\d+),\s*(\d+),\s*(\d+)/); 18930 return rgb ?'rgba(' + rgb[1] + ',' + rgb[2] + ',' + rgb[3] + ',' + a + ')' : value; 18931 } 18932 startChannelHeroAuto() { 18933 if (this._chHeroTimer || !(this.props.animaciones || true)) return; 18934 this._chHeroTimer = setInterval(() => { 18935 if (this.state.route !== 'channel') return; 18936 const n = (this.channelHeroImages || []).length; 18937 if (n > 1) this.setState((s) => ({ chHeroSlide: (s.chHeroSlide + 1) % n })); 18938 }, 5000); 18939 } 18940 openChannel(c) { 18941 const name = this.consName(c); 18942 const logo = this.consLogo(name) || (c && (c.logoUrl || c.logo || c.imageUrl)); 18943 if (logo) this.extractLogoColor(name, logo); 18944 this.setState({ route: 'channel', selectedCons: c, consRemates: null, channelTab: 'todos', channelYear: null, chHeroSlide: 0 }); 18945 window.scrollTo(0, 0); 18946 this.startChannelHeroAuto(); 18947 this.fetchConsRemates(name); 18948 } 18949 async fetchConsRemates(name) { 18950 const key = this.consKey(name); 18951 this.setState({ consRematesLoading: true }); 18952 const match = (r) => { const k = this.consKey(r.nombre_cuenta || r.nombre_consignatario); return k && (k === key || k.includes(key) || key.includes(k)); }; 18953 const pull = (d) => Array.isArray(d) ?d : (d?.items || d?.remates || []); 18954 const base = '/api/remateshistorico'; 18955 try { 18956 const yearNow = new Date().getFullYear(); 18957 // 1er fetch: trae el año actual + la lista de años disponibles 18958 const first = await this.unwrap(base + '?scope=all&year=' + yearNow + '&page=1&pageSize=120'); 18959 const years = [...new Set((first?.years || []).map(Number).filter(Boolean))].sort((a, b) => b - a); 18960 const yearList = (years.length ?years : [yearNow, yearNow - 1, yearNow - 2]).slice(0, 6); 18961 // resto de años en paralelo ? videoteca completa 18962 const rest = await Promise.all(yearList.filter((y) => y !== yearNow).map((y) => this.unwrap(base + '?scope=all&year=' + y + '&page=1&pageSize=120'))); 18963 const all = [...pull(first), ...rest.flatMap(pull), ...(this.state.monthRemates || [])]; 18964 const seen = new Set(); 18965 const list = all.filter(match).filter((r) => { const id = r.id_remate || r.sourceId || r.id || JSON.stringify(r).slice(0, 40); if (seen.has(id)) return false; seen.add(id); return true; }); 18966 this.setState({ consRemates: list, consYears: yearList, consRematesLoading: false }); 18967 } catch { this.setState({ consRemates: [], consRematesLoading: false }); } 18968 } 18969 captureVideoThumbnail(url) { 18970 return new Promise((resolve, reject) => { 18971 const video = document.createElement('video'); 18972 let settled = false; 18973 let timeoutId = null; 18974 video.crossOrigin = 'anonymous'; 18975 video.muted = true; 18976 video.playsInline = true; 18977 video.preload = 'auto'; 18978 const cleanup = () => { 18979 if (timeoutId) clearTimeout(timeoutId); 18980 video.pause(); 18981 video.removeAttribute('src'); 18982 video.load(); 18983 }; 18984 const fail = (message = 'No se pudo generar la miniatura.') => { 18985 if (settled) return; 18986 settled = true; 18987 cleanup(); 18988 reject(new Error(message)); 18989 }; 18990 const drawFrame = () => { 18991 if (settled || video.readyState < 2) return; 18992 try { 18993 const width = video.videoWidth || 640; 18994 const height = video.videoHeight || 360; 18995 const canvas = document.createElement('canvas'); 18996 canvas.width = 320; 18997 canvas.height = Math.max(1, Math.round((height / width) * canvas.width)); 18998 canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height); 18999 const thumbnail = canvas.toDataURL('image/jpeg', 0.78); 19000 settled = true; 19001 cleanup(); 19002 resolve(thumbnail); 19003 } catch { 19004 fail(); 19005 } 19006 }; 19007 video.addEventListener('loadedmetadata', () => { 19008 const duration = Number.isFinite(video.duration) && video.duration > 0 ?video.duration : 3; 19009 const target = Math.min(Math.max(duration * 0.18, 0.4), Math.max(duration - 0.1, 0)); 19010 if (Math.abs(video.currentTime - target) < .05) drawFrame(); 19011 else { 19012 try { video.currentTime = target; } 19013 catch { drawFrame(); } 19014 } 19015 }, { once: true }); 19016 video.addEventListener('seeked', drawFrame, { once: true }); 19017 video.addEventListener('loadeddata', () => { 19018 if (!video.seeking && video.currentTime > 0) drawFrame(); 19019 }); 19020 video.addEventListener('error', fail, { once: true }); 19021 timeoutId = setTimeout(() => fail('La miniatura tardó demasiado en responder.'), 10000); 19022 video.src = url; 19023 video.load(); 19024 }); 19025 } 19026 hydrateVideoThumbnails(videos) { 19027 this._videoThumbLoading = this._videoThumbLoading || new Set();
19028 (videos || []).forEach((video) => { 19029 const url = video?.url || ''; 19030 if (!url || video.thumb || this.state.videoThumbByUrl[url] || this.state.videoThumbFailedByUrl[url] || this._videoThumbLoading.has(url)) return; 19031 this._videoThumbLoading.add(url); 19032 this.captureVideoThumbnail(url) 19033 .then((thumbnail) => { 19034 this.setState((state) => ({ 19035 videoThumbByUrl: { ...(state.videoThumbByUrl || {}), [url]: thumbnail }, 19036 videoThumbFailedByUrl: { ...(state.videoThumbFailedByUrl || {}), [url]: false }, 19037 })); 19038 }) 19039 .catch((error) => { 19040 console.warn('[Roslot video thumbnail failed]', { url, message: error?.message }); 19041 this.setState((state) => ({ videoThumbFailedByUrl: { ...(state.videoThumbFailedByUrl || {}), [url]: true } })); 19042 }) 19043 .finally(() => this._videoThumbLoading.delete(url)); 19044 }); 19045 } 19046 lotViewSessionId() { 19047 const key = 'roslot.viewSessionId'; 19048 try { 19049 const current = localStorage.getItem(key); 19050 if (current) return current; 19051 const next = window.crypto?.randomUUID?.() || ('view-' + Date.now() + '-' + Math.random().toString(16).slice(2)); 19052 localStorage.setItem(key, next); 19053 return next; 19054 } catch { 19055 return 'view-' + Date.now() + '-' + Math.random().toString(16).slice(2); 19056 } 19057 } 19058 lotViewStorageKey(cert) { 19059 const bucket = Math.floor(Date.now() / (6 * 60 * 60 * 1000)); 19060 return 'roslot.lotView.' + cert + '.' + bucket; 19061 } 19062 async loadLotViewCounter(cert, force = false) { 19063 cert = String(cert || '').trim(); 19064 if (!cert || this.state.lotViewLoading[cert]) return; 19065 if (!force && Object.prototype.hasOwnProperty.call(this.state.lotViewCounts, cert)) return; 19066 this.setState({ lotViewLoading: { ...this.state.lotViewLoading, [cert]: true } }); 19067 try { 19068 const data = await this.apiFetch('/api/lots/' + encodeURIComponent(cert) + '/views'); 19069 if (Number.isFinite(Number(data?.views))) { 19070 this.setState({ 19071 lotViewCounts: { ...this.state.lotViewCounts, [cert]: Number(data.views) }, 19072 lotViewLoading: { ...this.state.lotViewLoading, [cert]: false }, 19073 }); 19074 return; 19075 } 19076 } catch (error) { 19077 if (![404, 405].includes(Number(error?.status))) console.warn('[Roslot lot view counter failed]', { cert, message: error?.message }); 19078 } 19079 this.setState({ lotViewLoading: { ...this.state.lotViewLoading, [cert]: false } }); 19080 } 19081 async recordLotView(cert) { 19082 cert = String(cert || '').trim(); 19083 if (!cert) return; 19084 const storageKey = this.lotViewStorageKey(cert); 19085 try { 19086 if (localStorage.getItem(storageKey) === '1') return; 19087 } catch {} 19088 try { 19089 const data = await this.apiFetch('/api/lots/' + encodeURIComponent(cert) + '/views', { 19090 method: 'POST', 19091 body: JSON.stringify({ 19092 sessionId: this.lotViewSessionId(), 19093 route: window.location.hash || '', 19094 referrer: document.referrer || '', 19095 screen: (window.screen?.width || 0) + 'x' + (window.screen?.height || 0), 19096 timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || '', 19097 qualifiedSeconds: 8, 19098 occurredAt: new Date().toISOString(), 19099 }), 19100 }); 19101 try { localStorage.setItem(storageKey, '1'); } catch {} 19102 if (Number.isFinite(Number(data?.views))) { 19103 this.setState({ lotViewCounts: { ...this.state.lotViewCounts, [cert]: Number(data.views) } }); 19104 } 19105 } catch (error) { 19106 if (![404, 405].includes(Number(error?.status))) console.warn('[Roslot lot view tracking failed]', { cert, message: error?.message }); 19107 } 19108 } 19109 startLotViewTracking(cert) { 19110 if (this._viewT) clearTimeout(this._viewT); 19111 if (this._lotViewPoll) clearInterval(this._lotViewPoll); 19112 this.loadLotViewCounter(cert); 19113 this._viewT = setTimeout(() => { 19114 if (this.state.route === 'lot' && this.state.selectedCert === cert && !document.hidden) this.recordLotView(cert); 19115 }, 8000); 19116 this._lotViewPoll = setInterval(() => { 19117 if (this.state.route === 'lot' && this.state.selectedCert === cert && !document.hidden) this.loadLotViewCounter(cert, true); 19118 }, 20000); 19119 } 19120 async refreshRoslotPresence() { 19121 const uid = this.state.user?.uid; 19122 if (!uid || this.state.route !== 'pool' || document.hidden || this._roslotPresenceRequest) return; 19123 this._roslotPresenceRequest = true; 19124 if (!this.state.roslotPresence) this.setState({ roslotPresenceLoa
19124ding: true, roslotPresenceError: false }); 19125 try { 19126 const now = Date.now(); 19127 const heartbeatKey = 'roslot-presence-heartbeat:' + uid; 19128 let lastHeartbeat = Number(this._roslotPresenceHeartbeatAt || 0); 19129 try { lastHeartbeat = Math.max(lastHeartbeat, Number(localStorage.getItem(heartbeatKey)) || 0); } catch {} 19130 const heartbeatDue = !lastHeartbeat || now - lastHeartbeat >= 120000; 19131 const data = await this.apiFetch('/api/roslot/presence', heartbeatDue ?{ method: 'POST', body: '{}' } : { method: 'GET' }); 19132 if (heartbeatDue) { 19133 this._roslotPresenceHeartbeatAt = now; 19134 try { localStorage.setItem(heartbeatKey, String(now)); } catch {} 19135 } 19136 this.setState({ 19137 roslotPresence: { 19138 buyers: Math.max(0, Number(data?.buyers) || 0), 19139 sellers: Math.max(0, Number(data?.sellers) || 0), 19140 total: Math.max(0, Number(data?.total) || 0), 19141 updatedAt: data?.updatedAt || new Date().toISOString(), 19142 }, 19143 roslotPresenceLoading: false, 19144 roslotPresenceError: false, 19145 }); 19146 } catch (error) { 19147 if (![401, 403, 404].includes(Number(error?.status))) console.warn('[Roslot presence failed]', error?.message || error); 19148 this.setState({ roslotPresenceLoading: false, roslotPresenceError: true }); 19149 } finally { 19150 this._roslotPresenceRequest = false; 19151 } 19152 } 19153 roslotLotPlayer() { 19154 return document.querySelector('video[data-lot-player]'); 19155 } 19156 syncRoslotLotVideoState(video = this.roslotLotPlayer(), force = false) { 19157 if (!video) return; 19158 const now = Date.now(); 19159 if (!force && this._roslotVideoStateAt && now - this._roslotVideoStateAt < 350) return; 19160 this._roslotVideoStateAt = now; 19161 const duration = Number.isFinite(video.duration) ?video.duration : 0; 19162 const progress = duration > 0 ?Math.min(100, Math.max(0, (Number(video.currentTime || 0) / duration) * 100)) : 0; 19163 this.setState({ 19164 lotVideoPlaying: !video.paused && !video.ended, 19165 lotVideoProgress: progress, 19166 lotVideoDuration: duration, 19167 lotVideoVolume: video.muted ?0 : Number(video.volume || 0), 19168 lotVideoMuted: !!video.muted || Number(video.volume || 0) === 0, 19169 }); 19170 } 19171 toggleRoslotLotVideoPlayback(event) { 19172 event?.preventDefault?.(); 19173 const video = this.roslotLotPlayer(); 19174 if (!video) return; 19175 if (video.paused) video.play?.().catch(() => {}); 19176 else video.pause?.(); 19177 this.syncRoslotLotVideoState(video, true); 19178 } 19179 seekRoslotLotVideo(event) { 19180 const video = this.roslotLotPlayer(); 19181 if (!video || !Number.isFinite(video.duration) || video.duration <= 0) return; 19182 const value = Math.max(0, Math.min(100, Number(event?.target?.value || 0))); 19183 video.currentTime = (value / 100) * video.duration; 19184 this.syncRoslotLotVideoState(video, true); 19185 } 19186 setRoslotLotVideoVolume(event) { 19187 const video = this.roslotLotPlayer(); 19188 if (!video) return; 19189 const value = Math.max(0, Math.min(1, Number(event?.target?.value || 0))); 19190 try { video.volume = value; } catch (_) {} 19191 video.muted = value <= 0; 19192 this.syncRoslotLotVideoState(video, true); 19193 } 19194 toggleRoslotLotVideoMute(event) { 19195 event?.preventDefault?.(); 19196 const video = this.roslotLotPlayer(); 19197 if (!video) return; 19198 const nextMuted = !video.muted && Number(video.volume || 0) > 0; 19199 if (!nextMuted && Number(video.volume || 0) <= 0) { 19200 try { video.volume = 0.8; } catch (_) {} 19201 } 19202 video.muted = nextMuted; 19203 this.syncRoslotLotVideoState(video, true); 19204 } 19205 async openRoslotLotVideoFullscreen(event) { 19206 event?.preventDefault?.(); 19207 const video = this.roslotLotPlayer(); 19208 if (!video) return; 19209 const frame = video.closest('.dc-cabana-video-frame') || video; 19210 try { 19211 if (frame.requestFullscreen) await frame.requestFullscreen(); 19212 else if (video.requestFullscreen) await video.requestFullscreen(); 19213 else if (video.webkitEnterFullscreen) video.webkitEnterFullscreen(); 19214 } catch (_) {} 19215 } 19216 openLot(cert, focusBid, options = {}) { 19217 if (!this.canAccessRoslot()) { 19218 this.showToast('Por ahora Roslot está disponible solo para administradores.'); 19219 if (options.syncPath !== false) this.syncAppLocation('home', { replace: true }); 19220 return; 19221 } 19222 const selectedCert = String(cert || '').trim(); 19223 if (!selectedCert) return; 19224 if (options.syncPath !== false) this.syncRoslotLotLocation(selectedCert, { replace: options.replace === true }); 19225 this.setState({ route: 'lot', selectedCert, selectedVideo: 0, lotVideoPlaying: false, lotVideoProgress: 0, lotVideoDuration: 0, lotVideoVolume: 0, lotVideoMuted: true, bidMessage: null }); 19226 window.scrollTo(0, 0); 19227 if (this.state.lotsReal) this.fetchLotBids(selectedCert);
19228 const knownLot = (this.state.lots || []).find((lot) => lot.cert === selectedCert); 19229 if (knownLot) this.fetchLotSellerConsignataria(knownLot); 19230 const loading = !knownLot ? this.fetchRoslotLotByCert(selectedCert) : null; 19231 this.startLotViewTracking(selectedCert); 19232 return loading; 19233 } 19234 showToast(msg) { this.setState({ toast: msg }); clearTimeout(this._tt); this._tt = setTimeout(() => this.setState({ toast: '' }), 2200); } 19235 openRoslotMobilePanel() { 19236 if (!this.state.isMobile || this.state.route !== 'pool') return; 19237 this.setState({ roslotMobilePanelOpen: true }); 19238 } 19239 closeRoslotMobilePanel() { this.setState({ roslotMobilePanelOpen: false }); } 19240 showRoslotLiveNotification(item) { 19241 if (!item || this.state.route !== 'pool') return; 19242 const pulseId = String(item.id || [item.type, item.createdAt, item.title].filter(Boolean).join(':')); 19243 clearTimeout(this._roslotLivePulseTimer); 19244 this.setState({ roslotLivePulseId: pulseId }); 19245 this._roslotLivePulseTimer = setTimeout(() => this.setState({ roslotLivePulseId: '' }), 4500); 19246 if (!this.state.isMobile) return; 19247 clearTimeout(this._roslotLiveToastTimer); 19248 this.setState({ roslotLiveToast: item }); 19249 this._roslotLiveToastTimer = setTimeout(() => this.setState({ roslotLiveToast: null }), 6500); 19250 } 19251 openRoslotLiveToast() { 19252 const item = this.state.roslotLiveToast; 19253 if (!item) return; 19254 clearTimeout(this._roslotLiveToastTimer); 19255 this.setState({ roslotLiveToast: null, roslotMobilePanelOpen: false }); 19256 this.openNotification(item); 19257 } 19258 enterHome() { this.setState({ route: 'home', authMessage: '' }); window.scrollTo(0, 0); this.animateStats(); } 19259 focusAccountAssistantField(step = this.state.accountAssistantStep) { 19260 const fields = { 1: 'name', 2: 'phone', 3: 'email', 4: 'password', 5: 'submit' }; 19261 const field = fields[Number(step)]; 19262 if (!field) return; 19263 window.setTimeout(() => { 19264 const target = document.querySelector('[data-account-field="' + field + '"]'); 19265 if (!target) return; 19266 try { target.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch {} 19267 if (field !== 'submit') { 19268 try { target.focus({ preventScroll: true }); } catch { try { target.focus(); } catch {} } 19269 } 19270 }, 100); 19271 } 19272 openAccountAssistant() { 19273 this.setState({ 19274 accountAssistantOpen: true, 19275 accountAssistantMethod: '', 19276 accountAssistantStep: 0, 19277 accountAssistantMessage: '', 19278 authMode: 'signin', 19279 authMessage: '', 19280 authPasswordVisible: false, 19281 }); 19282 } 19283 closeAccountAssistant() { 19284 this.setState({ accountAssistantOpen: false, accountAssistantMethod: '', accountAssistantStep: 0, accountAssistantMessage: '' }); 19285 } 19286 chooseAccountAssistantGoogle() { 19287 this.setState({ accountAssistantMethod: 'google', accountAssistantStep: 0, accountAssistantMessage: '', authMode: 'signin', authMessage: '' }); 19288 } 19289 chooseAccountAssistantEmail() { 19290 this.setState({ accountAssistantMethod: 'email', accountAssistantStep: 1, accountAssistantMessage: '', authMode: 'register', authMessage: '' }, () => this.focusAccountAssistantField(1)); 19291 } 19292 backAccountAssistant() { 19293 const method = this.state.accountAssistantMethod; 19294 const step = Number(this.state.accountAssistantStep) || 0; 19295 if (method === 'google' || step <= 1) { 19296 this.setState({ accountAssistantMethod: '', accountAssistantStep: 0, accountAssistantMessage: '', authMode: 'signin', authMessage: '' }); 19297 return; 19298 } 19299 const nextStep = step - 1; 19300 this.setState({ accountAssistantStep: nextStep, accountAssistantMessage: '', authMessage: '' }, () => this.focusAccountAssistantField(nextStep)); 19301 } 19302 nextAccountAssistant() { 19303 const step = Number(this.state.accountAssistantStep) || 1; 19304 const email = String(this.state.formEmail || '').trim(); 19305 const validations = { 19306 1: [!!String(this.state.formName || '').trim(), 'Escribà tu nombre y apellido para poder continuar.'], 19307 2: [!!this.phoneNumberE164(this.state.formPhone), 'Revisá el teléfono. Debe incluir el código de paÃs, por ejemplo +54 9 341 000 0000.'], 19308 3: [/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email), 'Escribà un correo válido, por ejemplo [email protected].'], 19309 4: [String(this.state.formPassword || '').length >= 8 && /[A-ZÃÃÃÃÃÃÃ]/.test(this.state.formPassword || ''), 'La contraseña necesita 8 caracteres o más y al menos una letra mayúscula.'], 19310 }; 19311 if (validations[step] && !validations[step][0]) { 19312 this.setState({ accountAssistantMessage: validations[step][1] }, () => this.focusAccountAssistantField(step)); 19313 return; 19314 } 19315 if (step >= 5) { 19316 this.setState({ accountAssistantMessage: '', authMessage: '' }, () => this.submitAuth()); 19317 return; 19318 } 19319 const nextStep = step + 1;
19320 this.setState({ accountAssistantStep: nextStep, accountAssistantMessage: '', authMessage: '' }, () => this.focusAccountAssistantField(nextStep)); 19321 } 19322 async submitAuth() { 19323 const { authMode, formEmail, formPassword, formName, formPhone } = this.state; 19324 const email = (formEmail || '').trim(); 19325 const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); 19326 if (authMode === 'reset') { 19327 if (!emailOk) { this.setState({ authMessage: 'Ingresá un email válido para recuperar la contraseña.' }); return; } 19328 if (this.state.authBusy) return; 19329 this.setState({ authBusy: true, authMessage: '' }); 19330 try { 19331 await (await this.ensureFirebase()).auth().sendPasswordResetEmail(email, this.authActionSettings('password-reset')); 19332 this.setState({ authBusy: false, authMessage: 'Te enviamos el enlace para crear una nueva contraseña. Revisá también Spam o Correo no deseado.' }); 19333 } catch (e) { this.setState({ authBusy: false, authMessage: this.authError(e) }); } 19334 return; 19335 } 19336 if (!emailOk) { this.setState({ authMessage: 'Ingresá un email válido.' }); return; } 19337 if (!formPassword || (authMode === 'register' && formPassword.length < 8) || (authMode !== 'register' && formPassword.length < 6)) { this.setState({ authMessage: authMode === 'register' ?'La contraseña debe tener al menos 8 caracteres.' : 'Ingresá tu contraseña.' }); return; } 19338 if (authMode === 'register' && !/[A-ZÃÃÃÃÃÃÃ]/.test(formPassword)) { this.setState({ authMessage: 'La contraseña debe incluir al menos una letra mayúscula.' }); return; } 19339 if (authMode === 'register' && !(formName || '').trim()) { this.setState({ authMessage: 'Ingresá tu nombre y apellido.' }); return; } 19340 if (authMode === 'register' && !this.phoneNumberE164(formPhone)) { this.setState({ authMessage: 'Ingresá tu teléfono con código de paÃs, por ejemplo +54 9 341 000 0000.' }); return; } 19341 if (this.state.authBusy) return; 19342 this.setState({ authBusy: true, authMessage: '' }); 19343 try { 19344 this.requestWelcomeAfterSignIn(); 19345 if (authMode === 'register') { 19346 const firebase = await this.ensureFirebase(); 19347 const cred = await firebase.auth().createUserWithEmailAndPassword(email, formPassword); 19348 if (formName) await cred.user.updateProfile({ displayName: formName.trim() }); 19349 const persistedUser = await this.persistFirebaseUser(firebase, cred.user); 19350 this.setState({ 19351 user: persistedUser, 19352 authBusy: false, 19353 emailVerificationRequired: false, 19354 emailVerificationMessage: '', 19355 phoneVerificationRequired: true, 19356 phoneVerificationStep: 'phone', 19357 phoneVerificationCode: '', 19358 phoneVerificationBusy: false, 19359 phoneVerificationMessage: '', 19360 phoneVerificationNumber: this.phoneNumberE164(formPhone) || '+54', 19361 }); 19362 if (this._authHandling?.promise) await this._authHandling.promise; 19363 await this.savePhoneAndContinue(); 19364 } else { 19365 const firebase = await this.ensureFirebase(); 19366 const cred = await firebase.auth().signInWithEmailAndPassword(email, formPassword); 19367 const persistedUser = await this.persistFirebaseUser(firebase, cred.user); 19368 this.setState({ user: persistedUser, authBusy: false }); 19369 } 19370 } catch (e) { this.clearWelcomeAfterSignIn(); this.setState({ authMessage: this.authError(e), authBusy: false }); } 19371 } 19372 loadScriptOnce(src, id) { 19373 if (window[id]) return Promise.resolve(); 19374 const existing = document.querySelector('script[data-loader-id="' + id + '"]'); 19375 if (existing) { 19376 if (existing.dataset.loaded === 'true') return Promise.resolve(); 19377 return new Promise((resolve, reject) => { 19378 existing.addEventListener('load', () => { existing.dataset.loaded = 'true'; resolve(); }, { once: true }); 19379 existing.addEventListener('error', reject, { once: true }); 19380 }); 19381 } 19382 return new Promise((resolve, reject) => { 19383 const el = document.createElement('script'); 19384 el.src = src; el.async = true; el.dataset.loaderId = id; 19385 const timer = window.setTimeout(() => { 19386 el.remove(); 19387 reject(new Error('Tiempo de espera agotado al cargar ' + src)); 19388 }, 12000); 19389 el.onload = () => { window.clearTimeout(timer); el.dataset.loaded = 'true'; resolve(); }; 19390 el.onerror = (error) => { window.clearTimeout(timer); reject(error); }; 19391 document.head.appendChild(el); 19392 }); 19393 } 19394 async ensureFirebase() { 19395 if (!window.firebase?.apps) await this.loadScriptOnce('/vendor/firebase-app-compat.js?v=10.12.5', 'firebase'); 19396 if (!window.firebase?.auth) await this.loadScriptOnce('/vendor/firebase-auth-compat.js?v=10.12.5', 'firebase-auth-loaded'); 19397 const firebase = window.firebase;
19398 if (!firebase.apps.length) firebase.initializeApp({ 19399 apiKey: 'AIzaSyBKbNby76W3g8xR6iuueqy_dErZTSnS1RE', 19400 appId: '1:627421676271:web:2823188bcb07a97516911a', 19401 authDomain: window.location.hostname === 'rosgan.app' ? 'rosgan.app' : 'rosganapp.firebaseapp.com', 19402 messagingSenderId: '627421676271', 19403 projectId: 'rosganapp', 19404 storageBucket: 'rosganapp.firebasestorage.app', 19405 }); 19406 const auth = firebase.auth(); 19407 try { auth.useDeviceLanguage(); } catch {} 19408 // Firebase owns durable persistence and cross-tab sign-in/sign-out synchronization. 19409 // Do not downgrade to SESSION: it isolates authentication to a single tab. 19410 if (!this._firebasePersistencePromise) { 19411 this._firebasePersistencePromise = auth.setPersistence(firebase.auth.Auth.Persistence.LOCAL) 19412 .then(() => 'local').catch((error) => { this._firebasePersistencePromise = null; throw error; }); 19413 } 19414 const persistenceMode = await this._firebasePersistencePromise; 19415 try { document.documentElement.dataset.authPersistence = persistenceMode; } catch {} 19416 return firebase; 19417 } 19418 async persistFirebaseUser(firebase, user) { 19419 if (!user || this._signingOut) return null; 19420 const epoch = this._sessionEpoch || 0; 19421 const auth = firebase.auth(); 19422 // Firebase sign-in already persists the user. Never replay an old credential 19423 // with updateCurrentUser: that can resurrect a session after sign-out. 19424 if (auth.currentUser?.uid !== user.uid) return null; 19425 await auth.currentUser.getIdToken(); 19426 if (this._signingOut || epoch !== (this._sessionEpoch || 0) || auth.currentUser?.uid !== user.uid) return null; 19427 return auth.currentUser; 19428 } 19429 async notificationToken(firebase, registration) { 19430 if (!firebase.messaging) { 19431 await this.loadScriptOnce('/vendor/firebase-messaging-compat.js?v=10.12.5', 'firebase-messaging-loaded'); 19432 } 19433 if (typeof firebase.messaging.isSupported === 'function') { 19434 const supported = await firebase.messaging.isSupported(); 19435 if (!supported) { 19436 const error = new Error('Este navegador no es compatible con notificaciones push.'); 19437 error.code = 'messaging/unsupported-browser'; 19438 throw error; 19439 } 19440 } 19441 const messaging = firebase.messaging(); 19442 if (!this._foregroundMessagingBound) { 19443 this._foregroundMessagingBound = true; 19444 messaging.onMessage((payload) => { 19445 const title = payload?.notification?.title || 'Roslot'; 19446 const body = payload?.notification?.body || 'Tenes una nueva notificacion.'; 19447 this.setState({ toast: `${title}: ${body}` }); 19448 this.fetchNotifications(true); 19449 if (this.state.route === 'notifications') this.fetchNotificationHistory(true); 19450 if (document.hidden && Notification.permission === 'granted') { 19451 const notification = new Notification(title, { 19452 body, 19453 icon: 'assets/rosgan-logo-blue.png', 19454 tag: payload?.data?.type 19455 ? `${payload.data.type}-${payload?.data?.cert || 'account'}` 19456 : 'roslot-notification', 19457 }); 19458 notification.onclick = () => { 19459 window.focus(); 19460 if (payload?.data?.href) location.hash = payload.data.href.replace(/^#/, ''); 19461 notification.close(); 19462 }; 19463 } 19464 }); 19465 } 19466 const vapidKey = String(window.ROSGAN_FCM_VAPID_KEY || localStorage.getItem('rosgan.fcmVapidKey') || '').trim(); 19467 return messaging.getToken({ 19468 serviceWorkerRegistration: registration, 19469 ...(vapidKey ?{ vapidKey } : {}), 19470 }); 19471 } 19472 async initializeNotifications() { 19473 if (!('Notification' in window) || !('serviceWorker' in navigator)) { 19474 this.setState({ notificationsStatus: 'unsupported' }); 19475 return; 19476 } 19477 if (Notification.permission === 'denied') {
19478 this.setState({ notificationsStatus: 'denied' }); 19479 return; 19480 } 19481 if (Notification.permission !== 'granted') { 19482 this.setState({ notificationsStatus: 'available' }); 19483 return; 19484 } 19485 if (this._notificationsRegistering) return; 19486 this._notificationsRegistering = true; 19487 this.setState({ notificationsStatus: 'loading', notificationsMessage: '' }); 19488 try { 19489 const firebase = await this.ensureFirebase(); 19490 const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js?v=3', { 19491 scope: '/', 19492 updateViaCache: 'none', 19493 }); 19494 await registration.update().catch(() => {}); 19495 const token = await this.notificationToken(firebase, registration); 19496 if (!token) throw new Error('Firebase no devolvio un identificador para este navegador.'); 19497 await this.apiFetch('/api/notifications/devices', { 19498 method: 'POST', 19499 body: JSON.stringify({ 19500 token, 19501 platform: navigator.userAgentData?.platform || navigator.platform || '', 19502 userAgent: navigator.userAgent || '', 19503 }), 19504 }); 19505 this._notificationToken = token; 19506 this.setState({ notificationsStatus: 'enabled', notificationsMessage: '' }); 19507 } catch (error) { 19508 console.warn('notifications init', error); 19509 const unsupported = error?.code === 'messaging/unsupported-browser'; 19510 this.setState({ 19511 notificationsStatus: unsupported ?'unsupported' : 'error', 19512 notificationsMessage: error?.message || 'No se pudieron activar las notificaciones.', 19513 }); 19514 } finally { 19515 this._notificationsRegistering = false; 19516 } 19517 } 19518 async enableNotifications() { 19519 if (!('Notification' in window)) { 19520 this.setState({ notificationsStatus: 'unsupported' }); 19521 return; 19522 } 19523 const permission = await Notification.requestPermission(); 19524 if (permission !== 'granted') { 19525 this.setState({ notificationsStatus: permission === 'denied' ?'denied' : 'available' }); 19526 return; 19527 } 19528 await this.initializeNotifications(); 19529 } 19530 async fetchWhatsAppNotificationPreference(silent = false) { 19531 if (!this.state.user || this.state.whatsappNotificationsLoading) return; 19532 this.setState({ whatsappNotificationsLoading: true, ...(!silent ?{ whatsappNotificationsMessage: '' } : {}) }); 19533 try { 19534 const data = await this.apiFetch('/api/notifications/whatsapp/preferences'); 19535 this.setState({ 19536 whatsappNotificationsEnabled: data?.enabled === true, 19537 whatsappNotificationsAvailable: data?.available === true && data?.phoneVerified === true, 19538 whatsappNotificationsLoading: false, 19539 whatsappNotificationsMessage: '', 19540 }); 19541 } catch (error) { 19542 this.setState({ 19543 whatsappNotificationsLoading: false, 19544 whatsappNotificationsMessage: error?.message || 'No se pudo consultar WhatsApp.', 19545 }); 19546 } 19547 } 19548 async toggleWhatsAppNotifications() { 19549 if (this.state.whatsappNotificationsSaving || this.state.whatsappNotificationsLoading) return; 19550 if (!this.state.whatsappNotificationsAvailable) { 19551 this.showToast('Verifica tu telefono para activar las notificaciones por WhatsApp.'); 19552 return; 19553 } 19554 const enabled = !this.state.whatsappNotificationsEnabled; 19555 this.setState({ whatsappNotificationsSaving: true, whatsappNotificationsMessage: '' }); 19556 try { 19557 const data = await this.apiFetch('/api/notifications/whatsapp/preferences', { 19558 method: 'PATCH', 19559 body: JSON.stringify({ enabled }), 19560 }); 19561 this.setState({ 19562 whatsappNotificationsEnabled: data?.enabled === true, 19563 whatsappNotificationsAvailable: data?.available === true && data?.phoneVerified === true, 19564 whatsappNotificationsSaving: false, 19565 }); 19566 this.showToast(data?.enabled ?'Notificaciones por WhatsApp activadas.' : 'Notificaciones por WhatsApp desactivadas.'); 19567 } catch (error) { 19568 this.setState({ 19569 whatsappNotificationsSaving: false, 19570 whatsappNotificationsMessage: error?.message || 'No se pudo actualizar WhatsApp.', 19571 }); 19572 this.showToast(error?.message || 'No se pudo actualizar WhatsApp.'); 19573 } 19574 } 19575 async fetchNotifications(silent = false, includeRead = this.state.notificationsOpen) { 19576 if (!this.state.user || this._notificationsFetching) return; 19577 this._lastNotificationsFetchAt = Date.now(); 19578 this._notificationsFetching = true; 19579 if (!silent) this.setState({ notificationsLoading: true, notificationsError: '' }); 19580 try { 19581 const data = await this.apiFetch('/api/notifications?limit=50' + (includeRead ?'&includeRead=true' : '')); 19582 const items = Array.isArray(data?.items) ?data.items : []; 19583 const previousItems = this.state.notificationsItems; 19584 const notificationKey = (item) => String(item?.id || [item?.type, item?.createdAt, item?.title].filter(
19584Boolean).join(':')); 19585 const previousKeys = new Set((Array.isArray(previousItems) ?previousItems : []).map(notificationKey)); 19586 const newRoslotItem = Array.isArray(previousItems) 19587 ?items.find((item) => String(item?.type || '').startsWith('roslot_') && !previousKeys.has(notificationKey(item))) 19588 : null; 19589 if (items.some(item => item.type === 'account_role_changed' && !previousKeys.has(notificationKey(item)))) this.refreshProfileData(true); 19590 const signature = JSON.stringify(items); 19591 if (silent && signature === this._notificationsSignature) return; 19592 this._notificationsSignature = signature; 19593 this.setState({ 19594 notificationsItems: items, 19595 notificationsLoading: false, 19596 notificationsError: '', 19597 }, () => { 19598 if (newRoslotItem) this.showRoslotLiveNotification(newRoslotItem); 19599 if (this.state.notificationsOpen && items.some((item) => !item.read)) { 19600 this.markAllNotificationsRead({ preserveItems: true }); 19601 } 19602 }); 19603 } catch (error) { 19604 if (!silent) { 19605 this.setState({ 19606 notificationsLoading: false, 19607 notificationsError: error?.message || 'No se pudieron cargar las notificaciones.', 19608 }); 19609 } 19610 } finally { 19611 this._notificationsFetching = false; 19612 } 19613 } 19614 async fetchNotificationHistory(silent = false) { 19615 if (!this.state.user || this._notificationHistoryFetching) return; 19616 this._notificationHistoryFetching = true; 19617 const cacheKey = 'rosgan:notification-history:v1:' + String(this.state.user.uid || ''); 19618 if (!Array.isArray(this.state.notificationHistory)) { 19619 const cached = this.readCollectionCache(cacheKey); 19620 if (Array.isArray(cached)) this.setState({ notificationHistory: cached, notificationHistoryError: '' }); 19621 } 19622 if (!silent) this.setState({ notificationHistoryLoading: true, notificationHistoryError: '' }); 19623 try { 19624 const data = await this.apiFetch('/api/notifications?limit=100&includeRead=true'); 19625 const items = Array.isArray(data?.items) ?data.items : []; 19626 const signature = JSON.stringify(items); 19627 if (silent && signature === this._notificationHistorySignature) return; 19628 this._notificationHistorySignature = signature; 19629 this.writeCollectionCache(cacheKey, items, 30 * 60 * 1000); 19630 this.setState({ 19631 notificationHistory: items, 19632 notificationHistoryLoading: false, 19633 notificationHistoryError: '', 19634 }); 19635 } catch (error) { 19636 this.setState({ 19637 notificationHistoryLoading: false, 19638 notificationHistoryError: error?.message || 'No se pudo cargar el historial.', 19639 }); 19640 } finally { 19641 this._notificationHistoryFetching = false; 19642 } 19643 } 19644 goNotificationCenter() { 19645 this.setState({ notificationsOpen: false, roslotMobilePanelOpen: false }); 19646 this.go('notifications'); 19647 this.fetchNotificationHistory(false); 19648 } 19649 toggleNotifications() { 19650 const open = !this.state.notificationsOpen; 19651 this.setState({ notificationsOpen: open }, () => { 19652 if (!open) return; 19653 this.markAllNotificationsRead({ preserveItems: true, force: true }); 19654 this.fetchNotifications(false, true); 19655 }); 19656 } 19657 notificationDestination(item) { 19658 const href = String(item?.href || '').trim(); 19659 if (item?.type !== 'remate_lot_workflow') return href; 19660 // Old inbox entries use a hash; state-change alerts always open the auction list. 19661 const match = href.match(/^(?:#)?\/remates\/([^/?#]+)(?:\/auditor)?(?:[?#]|$)/); 19662 if (!match) return href; 19663 return '/remates/' + match[1] + '/auditor'; 19664 } 19665 async openNotification(item) { 19666 if (!item) return; 19667 if (!item.read) { 19668 this.setState({ 19669 notificationsItems: (this.state.notificationsItems || []).filter((notification) => notification.id !== item.id), 19670 notificationHistory: Array.isArray(this.state.notificationHistory) 19671 ? this.state.notificationHistory.map((notification) => ( 19672 notification.id === item.id ?{ ...notification, read: true } : notification 19673 )) 19674 : this.state.notificationHistory, 19675 }); 19676 this.apiFetch('/api/notifications/' + encodeURIComponent(item.id) + '/read', { method: 'PATCH' }) 19677 .catch(() => this.fetchNotifications(true)); 19678 } 19679 this.setState({ notificationsOpen: false, roslotMobilePanelOpen: false }); 19680 const destination = this.notificationDestination(item); 19681 if (destination) { 19682 const href = destination; 19683 if (href.startsWith('#')) { 19684 this.navigateFromHash(href); 19685 return; 19686 }
19687 try { 19688 const target = new URL(href, window.location.origin); 19689 if (target.origin !== window.location.origin) { 19690 window.location.assign(target.href); 19691 return; 19692 } 19693 const nextUrl = target.pathname + target.search + target.hash; 19694 this.writeAppHistory(nextUrl); 19695 this.navigateFromPath(target.pathname); 19696 } catch { 19697 window.location.assign(href); 19698 } 19699 } 19700 } 19701 async markAllNotificationsRead({ preserveItems = false, force = false } = {}) { 19702 const items = this.state.notificationsItems || []; 19703 if (!force && !items.some((item) => !item.read)) return; 19704 this.setState({ 19705 notificationsItems: preserveItems 19706 ?items.map((item) => ({ ...item, read: true })) 19707 : [], 19708 notificationHistory: Array.isArray(this.state.notificationHistory) 19709 ?this.state.notificationHistory.map((item) => ({ ...item, read: true })) 19710 : this.state.notificationHistory, 19711 }); 19712 if (this._markAllNotificationsReadPromise) return this._markAllNotificationsReadPromise; 19713 this._markAllNotificationsReadPromise = this.apiFetch('/api/notifications/read-all', { method: 'PATCH' }); 19714 try { 19715 await this._markAllNotificationsReadPromise; 19716 } catch { 19717 this.fetchNotifications(true, this.state.notificationsOpen); 19718 } finally { 19719 this._markAllNotificationsReadPromise = null; 19720 } 19721 } 19722 async syncAuthUser(user) { 19723 try { 19724 const token = await user.getIdToken(); 19725 const response = await fetch('/api/auth/sync-user', { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify({ displayName: user.displayName || '', email: user.email || '', phoneNumber: user.phoneNumber || '', phoneVerified: !!user.phoneNumber }) }); 19726 const data = await response.json().catch(() => null); 19727 if (!response.ok) throw new Error(data?.message || 'No se pudo sincronizar el usuario.'); 19728 if (data?.session?.apiError) console.warn('sync user api', data.session.apiError); 19729 this.setState({ authSyncError: '' }); 19730 } catch (e) { console.warn('sync user', e); this.setState({ authSyncError: e?.message || 'No se pudo sincronizar tu usuario con Rosgan API.' }); } 19731 } 19732 authError(e) { 19733 const c = e?.code || ''; 19734 if (c.includes('invalid-credential') || c.includes('wrong-password') || c.includes('user-not-found')) return 'Email o contraseña incorrectos.'; 19735 if (c.includes('invalid-email')) return 'El email no es válido.'; 19736 if (c.includes('email-already-in-use')) return 'Ya existe una cuenta con ese email.'; 19737 if (c.includes('weak-password')) return 'La contraseña debe tener al menos 8 caracteres e incluir una letra mayúscula.'; 19738 if (c.includes('expired-action-code')) return 'El enlace venció. Solicitá uno nuevo.'; 19739 if (c.includes('invalid-action-code')) return 'El enlace no es válido o ya fue utilizado. Solicitá uno nuevo.'; 19740 if (c.includes('too-many-requests')) return 'Demasiados intentos. Esperá unos minutos e intentá de nuevo.'; 19741 if (c.includes('network')) return 'Sin conexión. Revisá tu internet e intentá de nuevo.'; 19742 if (c.includes('operation-not-allowed')) return 'Este método de acceso no está habilitado. Contactá a Rosgan.'; 19743 return 'No pudimos procesar la solicitud. Probá de nuevo.'; 19744 } 19745 async googleAuth() { 19746 if (this.state.authBusy) return; 19747 try { 19748 const firebase = await this.ensureFirebase(); 19749 const auth = firebase.auth(); 19750 const provider = new firebase.auth.GoogleAuthProvider(); 19751 provider.setCustomParameters({ prompt: 'select_account' }); 19752 this.setState({ authBusy: true, authMessage: 'Abriendo Google...' }); 19753 this.requestWelcomeAfterSignIn(); 19754 const prefersRedirect = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent || '') 19755 && window.location.hostname === 'rosgan.app'; 19756 if (prefersRedirect) { 19757 this.setState({ authMessage: 'Continuando con Googleâ¦' }); 19758 await auth.signInWithRedirect(provider); 19759 return; 19760 } 19761 try { 19762 const credential = await auth.signInWithPopup(provider); 19763 if (!credential?.user) throw new Error('Google no devolvió una sesión válida.'); 19764 const persistedUser = await this.persistFirebaseUser(firebase, credential.user); 19765 this.setState({ user: persistedUser, authBusy: false, authMessage: '' }); 19766 return; 19767 } catch (popupError) { 19768 const code = String(popupError?.code || ''); 19769 if (!code.includes('popup-blocked') && !code.includes('operation-not-supported-in-this-environment')) throw popupError; 19770 this.setState({ authMessage: 'El navegador bloqueó la ventana. Continuando con Googleâ¦' });
19771 await auth.signInWithRedirect(provider); 19772 } 19773 } catch (e) { 19774 this.clearWelcomeAfterSignIn(); 19775 if (String(e?.code || '').includes('account-exists-with-different-credential')) { 19776 const email = String(e?.email || this.state.formEmail || '').trim().toLowerCase(); 19777 this.setState({ 19778 authBusy: false, 19779 authMode: 'signin', 19780 formEmail: email, 19781 formPassword: '', 19782 authMessage: 'Esta cuenta utiliza acceso manual. Ingresá con el email y la contraseña; no uses el botón de Google. Si no recordás la contraseña, usá âOlvidé mi contraseñaâ.', 19783 }); 19784 return; 19785 } 19786 this.setState({ authBusy: false, authMessage: this.googleErr(e) }); 19787 } 19788 } 19789 googleErr(e) { 19790 const c = e?.code || ''; 19791 const raw = e?.message ?' (' + c + ')' : (c ?' (' + c + ')' : ''); 19792 if (c.includes('unauthorized-domain')) return 'Este dominio no está autorizado en Firebase. Agregá ' + window.location.hostname + ' en Authentication â Settings â Authorized domains.'; 19793 if (c.includes('account-exists-with-different-credential')) return 'Esta cuenta utiliza acceso manual. Ingresá con el email y la contraseña.'; 19794 if (c.includes('operation-not-allowed')) return 'Google Sign-In no está habilitado en el proyecto Firebase.'; 19795 if (c.includes('popup-closed')) return 'Se cerró la ventana de Google antes de completar el ingreso.'; 19796 if (c.includes('popup')) return 'El navegador bloqueó la ventana de Google. Permità pop-ups y tocá nuevamente Continuar con Google.'; 19797 if (c.includes('network-request-failed') || c.includes('network')) return 'Google no pudo conectarse. Revisá la conexión y volvé a intentarlo.'; 19798 if (c.includes('app-compat/no-app') || c.includes('no-app')) return 'Firebase todavÃa no terminó de inicializar. Recargá la página e intentá de nuevo.'; 19799 return 'No pudimos conectar con Google' + raw + '. Probá de nuevo o usá email.'; 19800 } 19801 ytId(url) { return (url || '').match(/(?:v=|youtu\.be\/|\/vi\/|embed\/|shorts\/)([\w-]{11})/)?.[1] || ''; } 19802 consKey(v) { 19803 const key = (v || '').toString().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]/g, ''); 19804 return key === 'todos' ?'rosgan' : key; 19805 } 19806 consDisplayName(value) { 19807 const name = String(value || '').trim(); 19808 return this.consKey(name) === 'rosgan' ?'Rosgan' : name; 19809 } 19810 isExcludedConsignataria(value) { 19811 const key = this.consKey(value); 19812 return key.includes('gananorpujol') || key.includes('reggi'); 19813 } 19814 isExcludedRemate(item) { 19815 // Entrada importada de agenda que duplica el remate de octubre vigente (API Remates, ID 5). 19816 const legacyId = String(item?.sourceId || item?.id_remate || item?.id || ''); 19817 if ((legacyId === '1854' || legacyId === 'rosgan-1854') 19818 && String(item?.fecha_remate || '').slice(0, 10) === '2026-10-08' 19819 && this.consKey(item?.nombre_remate) === 'rematehabitualoctubre') return true; 19820 return [item?.consignatariaId, item?.consignatariaNombre, item?.consignataria, item?.nombre_cuenta, item?.nombre_consignatario, item?.nombre_remate] 19821 .some((value) => this.isExcludedConsignataria(value)); 19822 } 19823 isRosganHistoricalRemate(item) { 19824 if (!item || this.isExcludedRemate(item)) return false; 19825 const typeAndName = this.consKey([item?.nombre_tipo, item?.tipo_remate, item?.nombre_remate].filter(Boolean).join(' ')); 19826 if (this.remateType(item)?.[0] === 'Cabañas' || typeAndName.includes('cabana')) return false; 19827 const dateKey = String(item?.fecha_remate || item?.fecha || '').slice(0, 10); 19828 return !dateKey || dateKey <= '2026-09-04'; 19829 } 19830 consLocalBrand(name) { 19831 const record = this.consRecord(name); 19832 if ([name,record?.name,record?.nombre,record?.slug,record?.id].some(value=>this.consKey(value).includes('ganaderosdeceres'))) return { 19833 color:'#36A9E1',rgb:'rgb(54,169,225)',logo:'assets/consignatarias/ganaderos-de-ceres-celeste.svg', 19834 whiteLogo:'assets/consignatarias-white/ganaderos-de-ceres-coop-ltda.png', 19835 fullLogo:'assets/consignatarias-full-white/ganaderos-de-ceres-coop-ltda.png', 19836 }; 19837 const matches = [name, record?.name, record?.nombre, record?.slug, record?.id] 19838 .some(value => /unionagricola(de)?avellaneda|coop(de)?avellaneda/.test(this.consKey(value))); 19839 if (!matches) return null; 19840 return { 19841 color: '#70292D', rgb: 'rgb(112,41,45)', 19842 logo: 'assets/consignatarias/coop-union-agricola-avellaneda.png', 19843 whiteLogo: 'assets/consignatarias-white/coop-union-agricola-avellaneda.png', 19844 fullLogo: 'assets/consignatarias-full-white/coop-union-agricola-avellaneda.png', 19845 }; 19846 } 19847 consLogo(name) {
19848 const key = this.consKey(name); if (!key) return ''; 19849 if (key === 'rosgan') return 'assets/brand/rosgan-iso-blue.png'; 19850 const brand = this.consLocalBrand(name); 19851 if (brand) return brand.logo; 19852 const list = this.state.consignatarias || []; 19853 const hit = list.find((c) => [c.name, c.nombre, c.slug, c.id].some((v) => this.consKey(v) === key)) 19854 || list.find((c) => { const k = this.consKey(c.name || c.nombre); return k && (k.includes(key) || key.includes(k)); }); 19855 return hit?.logoUrl || hit?.logo || hit?.imageUrl || ''; 19856 } 19857 consFullHeroLogo(name) { 19858 const brand = this.consLocalBrand(name); 19859 if (brand) return brand.fullLogo; 19860 const key = this.consKey(name); 19861 const logos = { 19862 aguirrevazquezsa: 'aguirre-vazquez-s-a.png', aguirrevazquez: 'aguirre-vazquez-s-a.png', 19863 alzagaunzueyciasa: 'alzaga-unzue-y-cia-s-a.png', alzagaunzue: 'alzaga-unzue-y-cia-s-a.png', 19864 colomboycolombosa: 'colombo-y-colombo-s-a.png', colomboycolombo: 'colombo-y-colombo-s-a.png', 19865 coopguillermolehmann: 'coop-guillermo-lehmann.png', guillermolehmann: 'coop-guillermo-lehmann.png', lehmann: 'coop-guillermo-lehmann.png', 19866 edgarepastoreyciasrl: 'edgar-e-pastore-y-cia-s-r-l.png', edgarepastore: 'edgar-e-pastore-y-cia-s-r-l.png', pastore: 'edgar-e-pastore-y-cia-s-r-l.png', 19867 etchevehereruralsrl: 'etchevehere-rural-s-r-l.png', etchevehere: 'etchevehere-rural-s-r-l.png', 19868 ferialvarezsrl: 'ferialvarez-s-r-l.png', ferialvarez: 'ferialvarez-s-r-l.png', 19869 ganaderosdecerescoopltda: 'ganaderos-de-ceres-coop-ltda.png', ganaderosdeceres: 'ganaderos-de-ceres-coop-ltda.png', 19870 coopldtaunionagricoladeavellaneda: 'coop-union-agricola-avellaneda.png', coopunionagricoladeavellaneda: 'coop-union-agricola-avellaneda.png', unionagricoladeavellaneda: 'coop-union-agricola-avellaneda.png', coopdeavellaneda: 'coop-union-agricola-avellaneda.png', 19871 ildarrazhnossa: 'ildarraz-hnos-s-a.png', ildarraz: 'ildarraz-hnos-s-a.png', 19872 rigran: 'rigran.png', 19873 }; 19874 const file = logos[key] || Object.entries(logos).find(([candidate]) => candidate && (key.includes(candidate) || candidate.includes(key)))?.[1]; 19875 return file ?'assets/consignatarias-full-white/' + file : ''; 19876 } 19877 consFullHeroLogoFilter(name) { 19878 return /lehmann/.test(this.consKey(name)) ?'brightness(0) invert(1)' : 'none'; 19879 } 19880 consRecord(name) { 19881 const key = this.consKey(name); 19882 if (!key) return null; 19883 const list = this.state.consignatarias || []; 19884 return list.find((c) => [c.name, c.nombre, c.slug, c.id].some((v) => this.consKey(v) === key)) 19885 || list.find((c) => { const candidate = this.consKey(c.name || c.nombre); return candidate && (candidate.includes(key) || key.includes(candidate)); }) 19886 || null; 19887 } 19888 consBackground(name) { 19889 const record = this.consRecord(name); 19890 if (record?.backgroundUrl || record?.bannerUrl) return record.backgroundUrl || record.bannerUrl; 19891 const key = this.consKey(name); 19892 const slugs = { 19893 alzagaunzueyciasa: 'alzaga-unzue-y-cia-s-a', alzagaunzue: 'alzaga-unzue-y-cia-s-a', 19894 ildarrazhnossa: 'ildarraz-hnos-s-a', ildarraz: 'ildarraz-hnos-s-a', 19895 aguirrevazquezsa: 'aguirre-vazquez-s-a', aguirrevazquez: 'aguirre-vazquez-s-a', 19896 colomboycolombosa: 'colombo-y-colombo-s-a', colomboycolombo: 'colombo-y-colombo-s-a', 19897 coopguillermolehmann: 'coop-guillermo-lehmann', guillermolehmann: 'coop-guillermo-lehmann', coopglehmann: 'coop-guillermo-lehmann', cooplehmann: 'coop-guillermo-lehmann', lehmann: 'coop-guillermo-lehmann', 19898 edgarepastoreyciasrl: 'edgar-e-pastore-y-cia-s-r-l', edgarepastore: 'edgar-e-pastore-y-cia-s-r-l', pastore: 'edgar-e-pastore-y-cia-s-r-l', 19899 etchevehereruralsrl: 'etchevehere-rural-s-r-l', etchevehere: 'etchevehere-rural-s-r-l', 19900 ferialvarezsrl: 'ferialvarez-s-r-l', ferialvarez: 'ferialvarez-s-r-l', 19901 ganaderosdecerescoopltda: 'ganaderos-de-ceres-coop-ltda', ganaderosdeceres: 'ganaderos-de-ceres-coop-ltda', 19902 rigran: 'rigran', 19903 }; 19904 const slug = slugs[key] || Object.entries(slugs).find(([candidate]) => candidate && (key.includes(candidate) || candidate.includes(key)))?.[1]; 19905 return slug ?'/api/consignatarias/' + slug + '/background' : ''; 19906 }
19907 consBackgroundPosition(name, usage = 'banner') { 19908 const record = this.consRecord(name); 19909 const stored = record?.backgroundPosition || record?.bannerPosition; 19910 if (stored && usage === 'banner') return stored; 19911 const key = this.consKey(name); 19912 const positions = { 19913 aguirrevazquezsa: { banner: 'center 55%', card: 'center 55%' }, 19914 aguirrevazquez: { banner: 'center 55%', card: 'center 55%' }, 19915 alzagaunzueyciasa: { banner: 'center top', card: 'center 38%' }, 19916 alzagaunzue: { banner: 'center top', card: 'center 38%' }, 19917 colomboycolombosa: { banner: 'center 45%', card: 'center 48%' }, 19918 colomboycolombo: { banner: 'center 45%', card: 'center 48%' }, 19919 etchevehereruralsrl: { banner: 'center 40%', card: 'center 44%' }, 19920 etchevehere: { banner: 'center 40%', card: 'center 44%' }, 19921 ferialvarezsrl: { banner: 'center 40%', card: 'center 47%' }, 19922 ferialvarez: { banner: 'center 40%', card: 'center 47%' }, 19923 ildarrazhnossa: { banner: 'center 40%', card: 'center 46%' }, 19924 ildarraz: { banner: 'center 40%', card: 'center 46%' }, 19925 coopguillermolehmann: { banner: 'center 22%', card: 'center 45%' }, 19926 guillermolehmann: { banner: 'center 22%', card: 'center 45%' }, 19927 lehmann: { banner: 'center 22%', card: 'center 45%' }, 19928 edgarepastoreyciasrl: { banner: 'center 28%', card: 'center 43%' }, 19929 edgarepastore: { banner: 'center 28%', card: 'center 43%' }, 19930 pastore: { banner: 'center 28%', card: 'center 43%' }, 19931 ganaderosdecerescoopltda: { banner: 'center 45%', card: 'center 48%' }, 19932 ganaderosdeceres: { banner: 'center 45%', card: 'center 48%' }, 19933 rigran: { banner: 'center 30%', card: 'center 49%' }, 19934 }; 19935 const match = positions[key] || Object.entries(positions).find(([candidate]) => candidate && (key.includes(candidate) || candidate.includes(key)))?.[1]; 19936 return match?.[usage] || match?.banner || stored || 'center'; 19937 } 19938 remateConsignatariaRef(item) { 19939 return item?.consignataria || item?.nombre_cuenta || item?.nombre_consignatario || item?.consignatariaName || item?.consignatariaId || ''; 19940 } 19941 feriaState(d) { if (!d) return 'proximo'; const now = new Date(); const diff = d - now; if (diff > 0) return 'proximo'; if (diff > -3 * 3600e3) return 'vivo'; return 'anterior'; } 19942 feriaFavoritesFor(consignataria) { 19943 // Plazas de ferias relevadas en Rosgan BCR (2026). No incluye especiales de cabañas. 19944 const groups = [ 19945 ['aguirre', [['Alcorta','Santa Fe'],['Arrecifes','Buenos Aires'],['General Alvear','Mendoza'],['Lincoln','Buenos Aires']]], 19946 ['ganaderosdeceres', [['Ceres','Santa Fe'],['Hersilia','Santa Fe'],['San Cristóbal','Santa Fe'],['Suardi','Santa Fe'],['Tostado','Santa Fe'],['Vera','Santa Fe'],['Malbrán','Santiago del Estero'],['QuimilÃ','Santiago del Estero'],['General Pinedo','Chaco']]], 19947 ['ildarraz', [['Federal','Entre RÃos','Sociedad Rural'],['San Jaime de la Frontera','Entre RÃos','Don Polo II'],['Ceibas','Entre RÃos','Las Achiras']]], 19948 ['etchevehere', [['General RamÃrez','Entre RÃos','MarÃa Dolores'],['Federal','Entre RÃos','Sociedad Rural']]], 19949 ['pastore', [['Cañada RosquÃn','Santa Fe'],['Santa Regina','Buenos Aires'],['Rosario','Santa Fe','Mercado de Ganado de Rosario']]], 19950 ['rigran', [['San Justo','Santa Fe','Sociedad Rural'],['Reconquista','Santa Fe','Sociedad Rural'],['Goya','Corrientes','Sociedad Rural'],['Esquina','Corrientes','Sociedad Rural']]], 19951 ['ferialvarez', [['RÃo Cuarto','Córdoba','Predio Ferialvarez']]], 19952 ]; 19953 const key = this.consKey(consignataria); 19954 const group = groups.find(([match]) => key.includes(match)); 19955 return (group?.[1] || []).map(([localidad, provincia, predio]) => ({ 19956 id: this.consKey(localidad), localidad, provincia, predio:predio || '', 19957 mapSrc: localidad === 'Rosario' ? '/assets/feria-localidades/mercado-rosario.svg' : '/assets/feria-localidades/' + this.consKey(provincia) + '-' + this.consKey(localidad) + '.svg', 19958 label: predio ?localidad + ' · ' + predio : localidad, 19959 nombre: predio === 'Mercado de Ganado de Rosario' ? predio : 'Feria ' + (predio === 'MarÃa Dolores' ?predio : localidad), 19960 })); 19961 } 19962 chooseFeriaFavorite(id) { 19963 const favorite = this.feriaFavoritesFor(this.state.feriaForm.consignataria).find(item => item.id === id); 19964 if (!favorite) return; 19965 clearTimeout(this._feriaPlaceTimer); 19966 this._feriaPlaceToken = (this._feriaPlaceToken || 0) + 1; 19967 const ff = { ...this.state.feriaForm, provincia:favorite.provincia, localidad:favorite.localidad, lat:'', lon:'' }; 19968 ff.nombre = favorite.nombre; 19969 this.setState({ feriaForm:ff, feriaTitleAuto:true, feriaManualOpen:false, feriaFavoriteId:id, feriaPlaceResults:[], feriaPlaceLoading:false, feriaPlaceError:'', feriaFormMessage:null }); 19970 } 19971 clearFeriaFavorite() { 19972 this.setFF('provincia', ''); 19973 this.setState({ feriaManualOpen:true }); 19974 } 19975 chooseFeriaConsignataria(name, logo) { 19976 if (!this.canManageUsers(this.state.apiUser)) return; 19977 if (this.state.feriaForm.consignataria === name) return; 19978 clearTimeout(this._feriaPlaceTimer); 19979 this._feriaPlaceToken = (this._feriaPlaceToken || 0) + 1; 19980 this.setState({ feriaForm:{ ...this.state.feriaForm, consignataria:name, consignatariaLogo:logo, provincia:'', localidad:'', nombre:'', lat:'', lon:'' }, feriaTitleAuto:true, feriaManualOpen:false, feriaFavoriteId:'', feriaPlaceResults:[], feriaPlaceLoading:false, feriaPlaceError:'', feriaFormMessage:null }, () => { if (logo) this.extractLogoColor(name, logo); }); 19981 } 19982 setFF(key, val) { 19983 const ff = { ...this.state.feriaForm, [key]: val }; 19984 if (key === 'localidad') { 19985 if (this.state.feriaTitleAuto || !ff.nombre) ff.nombre = val ?'Feria ' + val : ''; 19986 ff.lat = ''; ff.lon = ''; 19987 } 19988 const patch = { feriaForm: ff, feriaFormMessage: null }; 19989 if (key === 'provincia' || key === 'localidad') { 19990 patch.feriaFavoriteId = ''; 19991 clearTimeout(this._feriaPlaceTimer); 19992 this._feriaPlaceToken = (this._feriaPlaceToken || 0) + 1; 19993 patch.feriaPlaceResults = []; patch.feriaPlaceLoading = false;
19994 } 19995 if (key === 'nombre') patch.feriaTitleAuto = false; 19996 if (key === 'provincia') { 19997 clearTimeout(this._feriaPlaceTimer); 19998 this._feriaPlaceToken = (this._feriaPlaceToken || 0) + 1; 19999 ff.localidad = ''; ff.lat = ''; ff.lon = ''; 20000 if (this.state.feriaTitleAuto) ff.nombre = ''; 20001 patch.feriaPlaceLoading = false; patch.feriaPlaceResults = []; patch.feriaPlaceError = ''; 20002 } 20003 if (key === 'consignataria') { const lg = this.consLogo(val); if (lg) this.extractLogoColor(val, lg); } 20004 this.setState(patch); 20005 } 20006 onFeriaPlaceInput(value) { 20007 const localidad = String(value || ''); 20008 this.setFF('localidad', localidad); 20009 clearTimeout(this._feriaPlaceTimer); 20010 if (!this.state.feriaForm.provincia || localidad.trim().length < 3) { 20011 this.setState({ feriaPlaceLoading: false, feriaPlaceResults: [], feriaPlaceError: '' }); 20012 return; 20013 } 20014 const provincia = this.state.feriaForm.provincia; 20015 this._feriaPlaceTimer = setTimeout(() => this.searchFeriaPlaces(localidad, provincia), 280); 20016 } 20017 async searchFeriaPlaces(localidad, provincia) { 20018 const query = String(localidad || '').trim(); 20019 if (!provincia || query.length < 3) return; 20020 const token = (this._feriaPlaceToken || 0) + 1; 20021 this._feriaPlaceToken = token; 20022 this.setState({ feriaPlaceLoading: true, feriaPlaceError: '' }); 20023 try { 20024 const response = await this.apiFetch('/api/places/search', { method: 'POST', body: JSON.stringify({ query, provincia: String(provincia).trim() }) }); 20025 if (token !== this._feriaPlaceToken) return; 20026 const results = Array.isArray(response?.data) ?response.data : this.listFrom(response, ['data', 'items', 'results']); 20027 this.setState({ feriaPlaceLoading: false, feriaPlaceResults: results.slice(0, 7), feriaPlaceError: results.length ?'' : 'No encontramos esa localidad. Podés escribirla manualmente.' }); 20028 } catch { 20029 if (token !== this._feriaPlaceToken) return; 20030 this.setState({ feriaPlaceLoading: false, feriaPlaceResults: [], feriaPlaceError: 'Google no respondió. Podés escribir la localidad manualmente.' }); 20031 } 20032 } 20033 selectFeriaPlace(item) { 20034 if (!item) return; 20035 const localidad = String(item.label || item.address || ''); 20036 const ff = { ...this.state.feriaForm, localidad, lat: item.lat != null ?String(item.lat) : '', lon: item.lng != null ?String(item.lng) : '' }; 20037 if (this.state.feriaTitleAuto || !ff.nombre) ff.nombre = localidad ?'Feria ' + localidad : ''; 20038 this.setState({ feriaForm: ff, feriaFavoriteId:'', feriaPlaceLoading: false, feriaPlaceResults: [], feriaPlaceError: '', feriaFormMessage: null }); 20039 } 20040 closeFeriaPlaceResults() { 20041 setTimeout(() => this.setState({ feriaPlaceResults: [] }), 150); 20042 } 20043 async fetchProvinces() { 20044 if (this.state.geoProvinces || this.state.geoLoadingProv) return; 20045 this.setState({ geoLoadingProv: true }); 20046 try { 20047 const r = await fetch('/api/georef/provincias'); 20048 const j = await r.json(); 20049 if (!r.ok) throw new Error(j?.message || 'No se pudieron obtener las provincias.'); 20050 const provs = (j.items || []).map((p) => p.nombre).sort((a, b) => a.localeCompare(b, 'es')); 20051 this.setState({ geoProvinces: provs, geoLoadingProv: false }); 20052 } catch { this.setState({ geoProvinces: [...ARGENTINA_PROVINCES], geoLoadingProv: false }); } 20053 } 20054 async fetchLocalities(prov) { 20055 if (!prov || this.state.geoLocs[prov]) return; 20056 this.setState({ geoLoadingLoc: true }); 20057 try { 20058 const r = await fetch('/api/georef/localidades?provincia=' + encodeURIComponent(prov)); 20059 const j = await r.json(); 20060 if (!r.ok) throw new Error(j?.message || 'No se pudieron obtener las localidades.'); 20061 const locs = (j.items || []).map((l) => ({ nombre: l.nombre, lat: l.lat, lon: l.lon })).sort((a, b) => a.nombre.localeCompare(b.nombre, 'es')); 20062 this.setState({ geoLocs: { ...this.state.geoLocs, [prov]: locs }, geoLoadingLoc: false }); 20063 } catch { this.setState({ geoLoadingLoc: false }); } 20064 } 20065 feriaAssignedConsignataria() { 20066 if (this.canManageUsers(this.state.apiUser)) return null; 20067 const refs = (this.state.userConsignatarias || []).map((item) =>
20067 item?.ref || item?.id || item?.slug || item?.name).filter(Boolean); 20068 const primary = this.state.userConsignataria; 20069 refs.push(primary?.ref || primary?.id || primary?.slug || primary?.name || ''); 20070 const match = (this.state.consignatarias || []).find((item) => { 20071 const values = [item?.id, item?.slug, item?.name, item?.nombre].filter(Boolean); 20072 return refs.some((ref) => values.some((value) => this.consKey(value) === this.consKey(ref))); 20073 }); 20074 if (match) return { ...match, name: this.consName(match), logoUrl: this.consLogo(this.consName(match)) || match.logoUrl || match.logo || match.imageUrl }; 20075 const fallback = refs.find(Boolean); 20076 return fallback ?{ id: fallback, slug: fallback, name: fallback, logoUrl: this.consLogo(fallback) } : null; 20077 } 20078 async prepareFeriaConsignataria() { 20079 if (this.canManageUsers(this.state.apiUser)) return; 20080 if (!this.state.userConsignataria && this.state.user?.uid) await this.fetchUserConsignataria(this.state.user.uid); 20081 const assigned = this.feriaAssignedConsignataria(); 20082 if (!assigned) { 20083 this.setState({ feriaFormMessage: { text: 'Tu usuario no tiene una consignataria asignada.', ok: false } }); 20084 return; 20085 } 20086 const name = this.consName(assigned) || assigned.name; 20087 const logo = this.consLogo(name) || assigned.logoUrl; 20088 this.setState({ feriaForm: { ...this.state.feriaForm, consignataria: name, consignatariaLogo: logo }, feriaFormMessage: null }); 20089 if (logo) this.extractLogoColor(name, logo); 20090 } 20091 feriaValid(form = this.state.feriaForm) { const f = form || {}; return !!(f.consignataria && f.provincia && f.localidad && f.fecha && f.hora && f.cabezas && Number(f.cabezas) > 0); } 20092 editSavedFeria(f) { 20093 this.go('feria'); 20094 this.setState({feriaEditingId:f.id,feriaPhase:'idle',feriaResult:null,feriaError:'',feriaTitleAuto:false, 20095 feriaForm:{consignataria:f.consignataria,consignatariaLogo:this.consLogo(f.consignataria),provincia:f.provincia||'',localidad:f.localidad||'',nombre:f.nombreRemate||'',fecha:String(f.fecha||'').slice(0,10),hora:String(f.hora||'').slice(0,5),cabezas:f.cabezas,youtube:f.youtubeUrl||'',lat:f.latitud||'',lon:f.longitud||''}}); 20096 } 20097 async submitFeria() { 20098 if (this.state.feriaPhase === 'connecting' || this.state.feriaPhase === 'creating') return; 20099 let f = this.state.feriaForm; 20100 if (!this.canManageUsers(this.state.apiUser)) { 20101 const assigned = this.feriaAssignedConsignataria(); 20102 if (!assigned) { this.setState({ feriaFormMessage: { text: 'Tu usuario no tiene una consignataria asignada.', ok: false } }); return; } 20103 const assignedName = this.consName(assigned) || assigned.name; 20104 f = { ...f, consignataria: assignedName, consignatariaLogo: assigned.logoUrl || this.consLogo(assignedName) }; 20105 this.setState({ feriaForm: f }); 20106 } 20107 if (!this.feriaValid(f)) { this.setState({ feriaFormMessage: { text: 'Completá consignataria, provincia, localidad, fecha, hora y cabezas.', ok: false } }); return; } 20108 this.setState({ feriaPhase: 'connecting', feriaError: '', feriaResult: null, feriaFormMessage: null }); 20109 window.scrollTo(0, 0); 20110 setTimeout(() => { if (this.state.feriaPhase === 'connecting') this.setState({ feriaPhase: 'creating' }); }, 1100); 20111 const iso = new Date(f.fecha + 'T' + f.hora).toISOString(); 20112 const payload = { consignataria: f.consignataria, provincia: f.provincia, localidad: f.localidad, nombreRemate: f.nombre || ('Feria ' + f.localidad), fecha: f.fecha, hora: f.hora, cabezas: Number(f.cabezas) || 0, latitud: f.lat, longitud: f.lon, businessUnit: 'feria', youtubeUrl: f.youtube || '' }; 20113 const local = { title: payload.nombreRemate, consignataria: f.consignataria, provincia: f.provincia, localidad: f.localidad, cabezas: payload.cabezas, youtubeUrl: f.youtube || '', fecha: iso }; 20114 let token = ''; 20115 try { token = await (await this.ensureFirebase()).auth().currentUser?.getIdToken(); } catch {} 20116 if (!token) { this.setState({ feriaPhase: 'error', feriaError: 'Iniciá sesión para generar la transmisión.' }); return; } 20117 try {
20118 const r = await fetch('/api/ferias' + (this.state.feriaEditingId ? '/' + encodeURIComponent(this.state.feriaEditingId) : ''), { method: this.state.feriaEditingId ? 'PUT' : 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: 'Bearer ' + token }, body: JSON.stringify(payload) }); 20119 const j = await r.json().catch(() => null); 20120 if (!r.ok || j?.ok === false) throw Object.assign(new Error(j?.message || 'No se pudo crear la feria.'), {status:r.status}); 20121 if (this.state.feriaEditingId) { this.setState({feriaEditingId:null,feriaPhase:'idle'}); this.go('profile'); this.refreshProfileData(true); this.showToast('Feria actualizada.'); return; } 20122 const created = j?.data || { ...local, youtubeUrl: j?.youtubeUrl || local.youtubeUrl }; 20123 await this.waitFeriaYoutube(created); 20124 } catch (e) { 20125 const conflict = e.status === 409; 20126 const message = conflict ? 'No se creó la feria ni la transmisión: ya existe una feria en esa fecha y localidad. Elegà otra fecha o localidad.' : String(e.message || e); 20127 this.setState({ feriaPhase: conflict ? 'idle' : 'error', feriaError: message, ...(conflict ? {feriaResult:null,feriaFormMessage:{ok:false,text:message}} : {}) }); 20128 } 20129 } 20130 feriaYoutubeReady(auction) { 20131 const status = String(auction?.youtubeStatus || '').toLowerCase(); 20132 if (!['scheduled', 'created', 'ready', 'live', 'complete'].includes(status)) return false; 20133 try { const u = new URL(auction.youtubeUrl); return u.protocol === 'https:' && ['youtube.com', 'www.youtube.com', 'youtu.be'].includes(u.hostname); } catch { return false; } 20134 } 20135 async waitFeriaYoutube(auction) { 20136 const run = this._feriaWaitRun = (this._feriaWaitRun || 0) + 1; 20137 this.setState({ feriaResult: auction, feriaPhase: 'creating', feriaError: '' }); 20138 try { 20139 for (let attempt = 0; attempt < 25; attempt++) { 20140 if (this._feriaWaitRun !== run) return; 20141 this.setState({ feriaResult: auction }); 20142 if (String(auction.youtubeStatus).toLowerCase() === 'failed') { 20143 this.setState({ feriaPhase: 'error', feriaError: auction.youtubeError || 'La feria está guardada, pero YouTube no pudo crear la transmisión.' }); 20144 return; 20145 } 20146 if (this.feriaYoutubeReady(auction)) { this.finishFeria(auction); return; } 20147 if (!auction.id) throw new Error('No recibimos el identificador de la feria. Verificá tus ferias antes de volver a crearla.'); 20148 if (attempt === 24) break; 20149 await new Promise(resolve => setTimeout(resolve, attempt < 3 ? 2000 : 5000)); 20150 if (this._feriaWaitRun !== run) return; 20151 const response = await this.apiFetch('/api/ferias/' + encodeURIComponent(auction.id)); 20152 if (this._feriaWaitRun !== run) return; 20153 auction = { ...auction, ...(response.data || response) }; 20154 } 20155 this.setState({ feriaPhase: 'waiting', feriaError: 'La feria está guardada. YouTube todavÃa no confirmó la transmisión. Podés consultar nuevamente sin crear otra feria.' }); 20156 } catch (error) { 20157 if (this._feriaWaitRun === run) this.setState({ feriaPhase: 'waiting', feriaError: 'La feria está guardada. No pudimos confirmar el estado: ' + error.message }); 20158 } 20159 } 20160 async retryFeriaYoutube() { 20161 const auction = this.state.feriaResult; 20162 if (!auction?.id || this.state.feriaDeleting || ['connecting', 'creating'].includes(this.state.feriaPhase)) return; 20163 const retry = this.state.feriaPhase === 'error' && auction.youtubeStatus === 'failed'; 20164 this.setState({ feriaPhase: 'creating', feriaError: '' }); 20165 try { 20166 const response = await this.apiFetch('/api/ferias/' + encodeURIComponent(auction.id) + (retry ? '/youtube-link' : ''), retry ? { method: 'POST', body: JSON.stringify({ force: false }) } : {}); 20167 await this.waitFeriaYoutube({ ...auction, ...(response.data || response) }); 20168 } catch (error) { this.setState({ feriaPhase: retry ? 'error' : 'waiting', feriaError: error.message }); } 20169 } 20170 async deleteSavedFeria(auction) { 20171 if (!auction?.id || this.state.feriaDeleting || ['connecting', 'creating'].includes(this.state.feriaPhase)) return; 20172 if (!window.confirm('¿Eliminar la feria "' + (auction.nombreRemate || auction.title || auction.localidad || auction.id) + '"? También se eliminará su transmisión de YouTube si existe. Esta acción no se puede deshacer.')) return; 20173 this.setState({ feriaDeleting: true }); 20174 try { 20175 await this.apiFetch('/api/ferias/' + encodeURIComponent(auction.id), { method: 'DELETE' }); 20176 this.setState({ feriaCreated: (this.state.feriaCreated || []).filter(f => f.id !== auction.id), profileFerias: (this.state.profileFerias || []).filter(f => f.id !== auction.id), homeCalendarCache: {} }); 20177 if (this.state.feriaResult?.id === auction.id) this.resetFeriaCreator(); 20178 this.fetchMonthRemates(); 20179 this.showToast('Feria eliminada'); 20180 } catch (error) { 20181 this.showToast(error.message || 'No se pudo eliminar la feria.'); 20182 if (this.state.feriaResult?.id === auction.id) this.setState({ feriaError: error.message }); 20183 } finally { this.setState({ feriaDeleting: false }); } 20184 } 20185 shareFeriaFlyer() { 20186 const auction = this.state.feriaResult; 20187 if (!auction) return; 20188 const fullLogo = this.consFullHeroLogo(auction.consignataria); 20189 const logo = fullLogo || this.consLogo(auction.consignataria) || this.state.feriaForm.consignatariaLogo; 20190 window.RosganFeriaFlyer.open(auction, {color:this.consBrandColor(auction.consignataria),logo,mono:!!fullLogo}); 20191 } 20192 finishFeria(created) { 20193 if (!this.feriaYoutubeReady(created)) return; 20194 this.setState({ feriaPhase: 'done', feriaResult: created, feriaCreated: [created, ...this.state.feriaCreated.filter(f => f.id !== created.id)] });
20195 this.setState({ homeCalendarCache:{} }); 20196 this.fetchMonthRemates(); 20197 } 20198 resetFeriaCreator() { 20199 this._feriaWaitRun = (this._feriaWaitRun || 0) + 1; 20200 const assigned = this.feriaAssignedConsignataria(); 20201 const assignedName = assigned ?(this.consName(assigned) || assigned.name) : ''; 20202 this.setState({ feriaPhase: 'idle', feriaResult: null, feriaError: '', feriaTitleAuto: true, feriaFavoriteId:'', feriaManualOpen:false, feriaPlaceLoading: false, feriaPlaceResults: [], feriaPlaceError: '', 20203 feriaEditingId: null, feriaForm: { consignataria: assignedName, consignatariaLogo: assigned?.logoUrl || this.consLogo(assignedName), provincia: '', localidad: '', nombre: '', fecha: '', hora: '', cabezas: '', youtube: '', lat: '', lon: '' } }); 20204 } 20205 copyText(t) { try { navigator.clipboard.writeText(t); this.showToast('Enlace copiado'); } catch {} } 20206 accentFromName(name) { let h = 0; for (const ch of (name || 'Rosgan')) h = (h * 31 + ch.charCodeAt(0)) % 360; return 'hsl(' + h + ', 55%, 42%)'; } 20207 extractLogoColor(name, url) { 20208 if (!name || !url) return; 20209 const key = this.consKey(name); 20210 this._colors = this._colors || {}; 20211 if (this._colors[key]) return; 20212 this._colors[key] = 'pending'; 20213 const img = new Image(); img.crossOrigin = 'anonymous'; 20214 img.onload = () => { 20215 try { 20216 const c = document.createElement('canvas'); const S = 24; c.width = S; c.height = S; 20217 const ctx = c.getContext('2d'); ctx.drawImage(img, 0, 0, S, S); 20218 const d = ctx.getImageData(0, 0, S, S).data; 20219 let r = 0, g = 0, b = 0, n = 0; 20220 for (let i = 0; i < d.length; i += 4) { 20221 const R = d[i], G = d[i + 1], B = d[i + 2], A = d[i + 3]; 20222 if (A < 128) continue; 20223 const mx = Math.max(R, G, B), mn = Math.min(R, G, B); 20224 if (mx > 235 && mn > 220) continue; // skip near-white 20225 if (mx < 30) continue; // skip near-black 20226 if (mx - mn < 18 && mx > 150) continue; // skip light grays 20227 r += R; g += G; b += B; n++; 20228 } 20229 let col; 20230 if (n > 4) { col = 'rgb(' + Math.round(r / n) + ',' + Math.round(g / n) + ',' + Math.round(b / n) + ')'; } 20231 else col = this.accentFromName(name); 20232 this._colors[key] = col; 20233 this.setState({ colorTick: (this.state.colorTick || 0) + 1 }); 20234 } catch { this._colors[key] = this.accentFromName(name); this.setState({ colorTick: (this.state.colorTick || 0) + 1 }); } 20235 }; 20236 img.onerror = () => { this._colors[key] = this.accentFromName(name); this.setState({ colorTick: (this.state.colorTick || 0) + 1 }); }; 20237 img.src = url; 20238 } 20239 consColor(name) { if (!name) return '#0a3e78'; const brand = this.consLocalBrand(name); if (brand) return brand.rgb; const c = (this._colors || {})[this.consKey(name)]; return (c && c !== 'pending') ?c : this.accentFromName(name); } 20240 consBrandColor(name) { 20241 if (!this.consKey(name)) return '#0a3e78'; 20242 const brand = this.consLocalBrand(name); 20243 if (brand) return brand.color; 20244 const key = this.consKey(name); 20245 const accents = { 20246 rosgan: '#002e5f', 20247 alzagaunzueyciasa: '#32557B', alzagaunzue: '#32557B', 20248 ildarrazhnossa: '#D40612', ildarraz: '#D40612', 20249 aguirrevazquezsa: '#5967D8', aguirrevazquez: '#5967D8', 20250 colomboycolombosa: '#D71945', colomboycolombo: '#D71945', 20251 coopguillermolehmann: '#008D46', guillermolehmann: '#008D46', coopglehmann: '#008D46', cooplehmann: '#008D46', 20252 edgarepastoreyciasrl: '#1F325C', pastore: '#1F325C', 20253 etchevehereruralsrl: '#1F4AA1', etchevehere: '#1F4AA1', 20254 ferialvarezsrl: '#009D92', ferialvarez: '#009D92', 20255 ganaderosdecerescoopltda: '#36A9E1', ganaderosdeceres: '#36A9E1', 20256 coopldtaunionagricoladeavellaneda: '#70292D', coopunionagricoladeavellaneda: '#70292D', unionagricoladeavellaneda: '#70292D', coopdeavellaneda: '#70292D', 20257 rigran: '#F36C18', 20258 }; 20259 const exact = accents[key]; 20260 if (exact) return exact; 20261 const partial = Object.entries(accents).find(([candidate]) => candidate && (key.includes(candidate) || candidate.includes(key))); 20262 return partial?.[1] || this.consColor(name); 20263 } 20264 consWhiteLogo(name) { 20265 if (!this.consKey(name)) return ''; 20266 const brand = this.consLocalBrand(name); 20267 if (brand) return brand.whiteLogo;
20268 const key = this.consKey(name); 20269 if (key === 'rosgan') return 'assets/brand/rosgan-iso-white.png'; 20270 const logos = { 20271 alzagaunzueyciasa: 'alzaga-unzue-y-cia-s-a', alzagaunzue: 'alzaga-unzue-y-cia-s-a', 20272 ildarrazhnossa: 'ildarraz-hnos-s-a', ildarraz: 'ildarraz-hnos-s-a', 20273 aguirrevazquezsa: 'aguirre-vazquez-s-a', aguirrevazquez: 'aguirre-vazquez-s-a', 20274 colomboycolombosa: 'colombo-y-colombo-s-a', colomboycolombo: 'colombo-y-colombo-s-a', 20275 coopguillermolehmann: 'coop-guillermo-lehmann', guillermolehmann: 'coop-guillermo-lehmann', coopglehmann: 'coop-guillermo-lehmann', cooplehmann: 'coop-guillermo-lehmann', 20276 edgarepastoreyciasrl: 'edgar-e-pastore-y-cia-s-r-l', pastore: 'edgar-e-pastore-y-cia-s-r-l', 20277 etchevehereruralsrl: 'etchevehere-rural-s-r-l', etchevehere: 'etchevehere-rural-s-r-l', 20278 ferialvarezsrl: 'ferialvarez-s-r-l', ferialvarez: 'ferialvarez-s-r-l', 20279 ganaderosdecerescoopltda: 'ganaderos-de-ceres-coop-ltda', ganaderosdeceres: 'ganaderos-de-ceres-coop-ltda', 20280 coopldtaunionagricoladeavellaneda: 'coop-union-agricola-avellaneda', coopunionagricoladeavellaneda: 'coop-union-agricola-avellaneda', unionagricoladeavellaneda: 'coop-union-agricola-avellaneda', coopdeavellaneda: 'coop-union-agricola-avellaneda', 20281 rigran: 'rigran', 20282 }; 20283 const slug = logos[key] || Object.entries(logos).find(([candidate]) => candidate && (key.includes(candidate) || candidate.includes(key)))?.[1]; 20284 return slug ?'assets/consignatarias-white/' + slug + (slug.endsWith('.svg') ?'' : '.png') : ''; 20285 } 20286 shade(rgb, f) { 20287 const m = String(rgb).match(/(\d+),\s*(\d+),\s*(\d+)/); 20288 if (!m) return rgb; 20289 const adj = (v) => Math.max(0, Math.min(255, Math.round(f > 0 ?v + (255 - v) * f : v * (1 + f)))); 20290 return 'rgb(' + adj(+m[1]) + ',' + adj(+m[2]) + ',' + adj(+m[3]) + ')'; 20291 } 20292 REMATE_TYPES = { cabanas: ['Cabañas', '#53bec5'], especiales: ['Especiales', '#ea8d00'], faena: ['Faena', '#ab2f38'], habitual: ['Habitual', '#8a9000'], feria: ['Feria', '#7a8595'], libre: ['Libre', '#00973a'], aniversario: ['Aniversario', '#b99632'], rosgan: ['Rosgan', '#002e5f'] }; 20293 remateType(item) { 20294 if (item?.__feriaAuction) return this.REMATE_TYPES.feria; 20295 const s = (item.nombre_tipo || item.nombre_remate || item.tipo_remate || '').toLowerCase(); 20296 for (const k of ['cabañas', 'cabana', 'especial', 'faena', 'habitual', 'feria', 'libre', 'aniversario']) { 20297 if (s.includes(k)) { const key = k.startsWith('caban') ?'cabanas' : k.includes('especial') ?'especiales' : k; return this.REMATE_TYPES[key] || this.REMATE_TYPES.rosgan; } 20298 } 20299 return this.REMATE_TYPES.rosgan; 20300 } 20301 latestFinishedRemates(historical, current) { 20302 const active = new Set(this.rosganUpcomingRemates(current).map((item) => this.rosganArchiveIdentity(item))); 20303 return this.rosganArchiveRemates(historical, current, null) 20304 .filter((item) => !active.has(this.rosganArchiveIdentity(item)) && (item.__rosganClosedArchive || (this.remateDate(item) && this.remateDate(item) < new Date()))) 20305 .sort((a, b) => this.remateDate(b) - this.remateDate(a)).slice(0, 5); 20306 } 20307 unifiedArchiveRemates(historical, current, cabanas, year) { 20308 const selectedYear = Number(year); 20309 const rosgan = this.rosganArchiveRemates(historical, current, year); 20310 const cabana = (cabanas || []) 20311 .filter((item) => this.cabanaMode(item) === 'recent' && !this.isExcludedCabana(item)) 20312 .filter((item) => !selectedYear || this.cabanaAuctionStartAt(item)?.getFullYear() === selectedYear) 20313 .map((item) => ({ ...item, __cabanaAuction: true })); 20314 const seen = new Set(); 20315 return [...rosgan, ...cabana].filter((item) => { 20316 const date = item.__cabanaAuction ?this.cabanaAuctionStartAt(item) : this.remateDate(item); 20317 const title = item.nombre_remate || item.titulo || item.title || ''; 20318 const key = this.consKey(title) + ':' + (date?.getFullYear() || ''); 20319 if (!key || seen.has(key)) return false; 20320 seen.add(key); 20321 return true; 20322 }); 20323 } 20324 unifiedLatestFinishedRemates(historical, current, cabanas) { 20325 const active = new Set(this.rosganUpcomingRemates(current).map((item) => this.rosganArchiveIdentity(item))); 20326 return this.unifiedArchiveRemates(historical, current, cabanas, null) 20327 .filter((item) => item.__cabanaAuction || (!active.has(this.rosganArchiveIdentity(item)) && (item.__rosganClosedArchive || (this.remateDate(item) && this.remateDate(item) < new Date())))) 20328 .sort((a, b) => { 20329 const bDate = b.__cabanaAuction ?this.cabanaAuctionStartAt(b) : this.remateDate(b); 20330 const aDate = a.__cabanaAuction ?this.cabanaAuctionStartAt(a) : this.remateDate(a);
20331 return (bDate || 0) - (aDate || 0); 20332 }).slice(0, 5); 20333 } 20334 async fetchLatestFinishedRemates() { 20335 if (this.state.latestFinishedLoading || Array.isArray(this.state.latestFinished)) return; 20336 this.setState({ latestFinishedLoading: true }); 20337 const items = [], seen = new Set(); 20338 const years = [new Date().getFullYear()]; 20339 try { 20340 for (let yi = 0; yi < years.length; yi++) { 20341 for (let page = 1; ; page++) { 20342 const data = await this.unwrap('/api/remateshistorico?scope=all&year=' + years[yi] + '&page=' + page + '&pageSize=20'); 20343 if (yi === 0 && page === 1) years.push(...(data?.years || []).map(Number).filter((year) => year < years[0]).sort((a, b) => b - a)); 20344 for (const item of (data?.items || [])) { 20345 const key = this.rosganArchiveIdentity(item); 20346 if (!seen.has(key)) { seen.add(key); items.push(item); } 20347 } 20348 if (this.latestFinishedRemates(items, []).length >= 5 || page >= (data?.totalPages || 1)) break; 20349 } 20350 if (this.latestFinishedRemates(items, []).length >= 5) break; 20351 } 20352 this.setState({ latestFinished: items, latestFinishedLoading: false, latestFinishedError: '' }); 20353 this.loadRemateListCatalogs(items); 20354 } catch { this.setState({ latestFinishedLoading: false, latestFinishedError: 'No se pudieron cargar los últimos remates.' }); } 20355 } 20356 async fetchRemates(year) { 20357 this.setState({ rematesLoading: true }); 20358 try { 20359 const data = await this.unwrap('/api/remateshistorico?scope=all&year=' + year + '&page=1&pageSize=120'); 20360 const items = Array.isArray(data) ?data : (data?.items || data?.remates || []); 20361 this.loadRemateListCatalogs(items, 'historical'); 20362 this.setState({ remates: items, remateYears: (data?.years || []).map(String), rematesLoading: false }); 20363 } catch { this.setState({ remates: [], rematesLoading: false }); } 20364 } 20365 cabanaCatalogPdfUrl(item) { 20366 const value = window.resolveCabanaMediaUrl(item?.catalog?.url || ''); 20367 if (!value) return ''; 20368 try { 20369 const url = new URL(value, window.location.origin); 20370 return ['https:', 'http:'].includes(url.protocol) ? url.href : ''; 20371 } catch { return ''; } 20372 } 20373 cabanaBreedOf(item) { 20374 const text = String(item?.raza || item?.nombre_remate || item?.title || item?.titulo || '').toLowerCase(); 20375 const breeds = [ 20376 ['Angus Colorado', /angus\s+colorad/], ['Aberdeen Angus', /aberdeen|angus\s+negro|\bangus\b/], 20377 ['Polled Hereford', /polled\s+hereford/], ['Hereford', /hereford/], ['Brangus', /brangus/], 20378 ['Braford', /braford/], ['Limousin', /limousin/], ['Charolais', /charolais/], 20379 ]; 20380 return breeds.find(([, pattern]) => pattern.test(text))?.[0] || ''; 20381 } 20382 cabanaConsignatariaName(item, consignatarias = []) { 20383 const ref = item?.consignatariaId || item?.consignataria || item?.nombre_consignatario || item?.nombre_cuenta || ''; 20384 const record = (consignatarias || []).find((candidate) => [candidate?.id, candidate?.consignatariaId, candidate?.slug, candidate?.value, candidate?.name, candidate?.nombre, candidate?.label] 20385 .some((value) => this.consKey(value) && this.consKey(value) === this.consKey(ref))); 20386 const explicit = item?.consignatariaNombre || item?.nombreConsignataria || item?.consignataria?.nombre || item?.consignataria?.name || ''; 20387 return explicit || (record ?this.consName(record) : '') || (String(ref).includes('-') ?this.humanizeSlug(ref) : String(ref)) || 'Rosgan'; 20388 } 20389 cabanaColor(item) { 20390 const primary = Array.isArray(item?.cabanasPrincipales) && item.cabanasPrincipales.length 20391 ? item.cabanasPrincipales[0] 20392 : (Array.isArray(item?.cabins) ?item.cabins.find((cabana) => String(cabana?.type || '').toLowerCase() === 'principal') : null); 20393 const loadedColor = String(primary?.brandColor || item?.brandColor || ''); 20394 if (/^#[0-9a-f]{6}$/i.test(loadedColor)) return loadedColor; 20395 const colors = { 'Aberdeen Angus': '#26323d', 'Angus Colorado': '#8d3327', Brangus: '#8b552c', 'Polled Hereford': '#a43831', Hereford: '#a43831', Braford: '#b06a2e', Limousin: '#b28a31', Charolais: '#737f8d' }; 20396 return colors[this.cabanaBreedOf(item)] || '#0a5ca8'; 20397 } 20398 cabanaBrandDark(item) { 20399 const color = this.cabanaColor(item); 20400 const match = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i); 20401 if (!match) return '#002e5f'; 20402 const darken = (value) => Math.max(8, Math.round(parseInt(value, 16) * 0.5)).toString(16).padStart(2, '0'); 20403 return '#' + darken(match[1]) + darken(match[2]) + darken(match[3]); 20404 } 20405 async ensureCabanaBrand(item) { 20406 item = this.normalizeCabanaAuctionRecord(item); 20407 const groups = ['cabanasPrincipales', 'cabanasInvitadas'];
20408 let changed = false; 20409 const next = { ...item }; 20410 for (const group of groups) { 20411 next[group] = await Promise.all((item?.[group] || []).map(async (cabana) => { 20412 if (!cabana?.logoDataUrl || cabana?._whiteLogoVersion === 2) return cabana; 20413 try { 20414 const generated = await this.processCabanaLogoDataUrl(cabana.logoDataUrl); 20415 changed = true; 20416 return { ...cabana, ...generated, _whiteLogoVersion: 2 }; 20417 } catch { return cabana; } 20418 })); 20419 } 20420 if (!changed) return item; 20421 const id = this.cabanaRemateId(item); 20422 this.setState((state) => ({ 20423 currentCabana: this.cabanaRemateId(state.currentCabana) === id ?next : state.currentCabana, 20424 cabanas: Array.isArray(state.cabanas) ?state.cabanas.map((remate) => this.cabanaRemateId(remate) === id ?next : remate) : state.cabanas, 20425 })); 20426 return next; 20427 } 20428 normalizeCabanaAuctionRecord(item) { 20429 if (!item || typeof item !== 'object') return item; 20430 const normalized = { ...item }; 20431 const cabins = Array.isArray(normalized.cabins) ?normalized.cabins : []; 20432 const hasPrincipals = Array.isArray(normalized.cabanasPrincipales) && normalized.cabanasPrincipales.length; 20433 const hasGuests = Array.isArray(normalized.cabanasInvitadas) && normalized.cabanasInvitadas.length; 20434 if ((!hasPrincipals || !hasGuests) && cabins.length) { 20435 const mapCabin = (cabana) => ({ 20436 id: cabana?.id || cabana?.cabanaId || null, 20437 cabanaId: cabana?.cabanaId || cabana?.id || null, 20438 nombre: cabana?.nombre || cabana?.name || '', 20439 name: cabana?.name || cabana?.nombre || '', 20440 logoName: cabana?.logoName || '', 20441 logoDataUrl: cabana?.logoDataUrl || cabana?.logoUrl || '', 20442 logoUrl: cabana?.logoUrl || cabana?.logoDataUrl || '', 20443 logoWhiteDataUrl: cabana?.logoWhiteDataUrl || '', 20444 brandColor: cabana?.brandColor || '', 20445 displayOrder: cabana?.displayOrder || 0, 20446 }); 20447 const ordered = cabins.slice().sort((a, b) => Number(a?.displayOrder || 0) - Number(b?.displayOrder || 0)); 20448 if (!hasPrincipals) normalized.cabanasPrincipales = ordered.filter((cabana) => String(cabana?.type || '').toLowerCase() === 'principal').map(mapCabin); 20449 if (!hasGuests) normalized.cabanasInvitadas = ordered.filter((cabana) => String(cabana?.type || '').toLowerCase() !== 'principal').map(mapCabin); 20450 } 20451 return normalized; 20452 } 20453 cabanaAuctionStartAt(item) { 20454 if (!item || typeof item !== 'object') return null; 20455 const rawDate = item.schedule?.fecha || item.fecha_remate || item.fecha || item.fecha_hora_remate || item.fechaHoraInicio || item.startsAt || item.startAt || ''; 20456 const rawTime = String(item.schedule?.hora || item.hora_remate || item.hora || '').trim(); 20457 const parts = this.remateLocalDateTimeParts(rawDate); 20458 const time = rawTime || parts.time || '00:00'; 20459 const iso = this.rematePayloadDateTime(parts.date, time); 20460 const start = iso ? new Date(iso) : null; 20461 return start && !Number.isNaN(start.getTime()) ? start : null; 20462 } 20463 cabanaMode(item) { 20464 const start = this.cabanaAuctionStartAt(item); 20465 if (!start) return 'upcoming'; 20466 const now = new Date(); 20467 if (now >= start && now <= new Date(start.getTime() + 5 * 3600e3)) return 'live'; 20468 return now < start ?'upcoming' : 'recent'; 20469 } 20470 cabanaListingGroup(item) { 20471 if (this.cabanaMode(item) === 'recent') return 'recent'; 20472 const loadingClosed = item?.load?.lotes === false || item?.cargaLotes === false || String(item?.cargaLotes || '').toLowerCase() === 'cerrada'; 20473 return loadingClosed ?'upcoming' : 'active'; 20474 } 20475 compareCabanaListing(a, b) { 20476 const rank = {active:0,upcoming:1,recent:2}; 20477 const ga = this.cabanaListingGroup(a), gb = this.cabanaListingGroup(b); 20478 const group = rank[ga] - rank[gb]; 20479 if (group) return group; 20480 const da = this.cabanaAuctionStartAt(a)?.getTime(), db = this.cabanaAuctionStartAt(b)?.getTime(); 20481 if (da == null || db == null) return da == null && db == null ? String(this.cabanaRemateId(a)).localeCompare(String(this.cabanaRemateId(b))) : da == null ? 1 : -1; 20482 return (ga === 'recent' ? db - da : da - db) || String(this.cabanaRemateId(a)).localeCompare(String(this.cabanaRemateId(b))); 20483 } 20484 cabanaMedia(item) { 20485 return window.resolveCabanaMediaUrl(item?.youtube?.thumbnailUrl || item?.thumbnailUrl || item?.thumbnail || item?.
20485imagen); 20486 } 20487 cabanaWeatherLocation(item = this.state.currentCabana) { 20488 const location = item?.location || {}; 20489 const locality = String(item?.localidad || location.localidad || location.locality || '').trim(); 20490 const province = String(item?.provincia || location.provinciaNombre || location.provincia || location.province || '').trim(); 20491 let label = [locality, province].filter(Boolean).join(', '); 20492 if (!label) label = String(item?.ubicacion_remate || location.lugar || item?.lugar || '').trim(); 20493 label = label.replace(/\bSociedad Rural de\b/ig, '').replace(/\s+/g, ' ').trim(); 20494 const known = label.match(/trenque\s+lauquen/i); 20495 if (known) label = [known[0].replace(/\s+/g, ' '), province].filter(Boolean).join(', '); 20496 return label || ''; 20497 } 20498 cabanaWeatherCode(code) { 20499 const n = Number(code); 20500 if ([0].includes(n)) return { label: 'Despejado', icon: 'sun' }; 20501 if ([1, 2].includes(n)) return { label: 'Algo nublado', icon: 'cloud-sun' }; 20502 if ([3].includes(n)) return { label: 'Nublado', icon: 'cloud' }; 20503 if ([45, 48].includes(n)) return { label: 'Niebla', icon: 'cloud-fog' }; 20504 if ((n >= 51 && n <= 67) || (n >= 80 && n <= 82)) return { label: 'Lluvia', icon: 'cloud-rain' }; 20505 if (n >= 71 && n <= 77) return { label: 'Nieve', icon: 'cloud-snow' }; 20506 if (n >= 95) return { label: 'Tormenta', icon: 'cloud-lightning' }; 20507 return { label: 'Clima', icon: 'cloud-sun' }; 20508 } 20509 async fetchCabanaTvWeather(item = this.state.currentCabana, options = {}) { 20510 const locationLabel = this.cabanaWeatherLocation(item); 20511 if (!locationLabel) { 20512 if (this.state.cabanaTvWeather || this.state.cabanaTvWeatherLocation) this.setState({ cabanaTvWeather: null, cabanaTvWeatherLocation: '', cabanaTvWeatherError: '' }); 20513 return null; 20514 } 20515 const now = Date.now(); 20516 if (!options.force && this._cabanaTvWeatherLastAt && this._cabanaTvWeatherLocation === locationLabel && now - this._cabanaTvWeatherLastAt < 10 * 60 * 1000) return this.state.cabanaTvWeather; 20517 if (this._cabanaTvWeatherLoading) return this.state.cabanaTvWeather; 20518 this._cabanaTvWeatherLoading = true; 20519 this._cabanaTvWeatherLocation = locationLabel; 20520 this.setState({ cabanaTvWeatherLoading: true, cabanaTvWeatherLocation: locationLabel }); 20521 try { 20522 const geoUrl = 'https://geocoding-api.open-meteo.com/v1/search?name=' + encodeURIComponent(locationLabel) + '&count=1&language=es&format=json'; 20523 const geoResponse = await fetch(geoUrl, { cache: 'no-store' }); 20524 if (!geoResponse.ok) throw new Error('Geocoding HTTP ' + geoResponse.status); 20525 const geoData = await geoResponse.json(); 20526 const place = Array.isArray(geoData?.results) ?geoData.results[0] : null; 20527 if (!place || place.latitude == null || place.longitude == null) throw new Error('Sin ubicacion de clima'); 20528 const weatherUrl = 'https://api.open-meteo.com/v1/forecast?latitude=' + encodeURIComponent(place.latitude) + '&longitude=' + encodeURIComponent(place.longitude) + '¤t=temperature_2m,weather_code,wind_speed_10m&timezone=America%2FArgentina%2FBuenos_Aires'; 20529 const weatherResponse = await fetch(weatherUrl, { cache: 'no-store' }); 20530 if (!weatherResponse.ok) throw new Error('Weather HTTP ' + weatherResponse.status); 20531 const weatherData = await weatherResponse.json(); 20532 const currentWeather = weatherData?.current || {}; 20533 const codeInfo = this.cabanaWeatherCode(currentWeather.weather_code); 20534 const weather = { 20535 location: String(place.name || locationLabel), 20536 temperature: Math.round(Number(currentWeather.temperature_2m || 0)), 20537 unit: weatherData?.current_units?.temperature_2m || '°C', 20538 wind: Math.round(Number(currentWeather.wind_speed_10m || 0)), 20539 windUnit: weatherData?.current_units?.wind_speed_10m || 'km/h', 20540 label: codeInfo.label, 20541 icon: codeInfo.icon, 20542 updatedAt: now, 20543 }; 20544 this._cabanaTvWeatherLastAt = now; 20545 this.setState({ cabanaTvWeather: weather, cabanaTvWeatherLoading: false, cabanaTvWeatherError: '' }); 20546 return weather; 20547 } catch (error) { 20548 this.setState({ cabanaTvWeatherLoading: false, cabanaTvWeatherError: error?.message || 'No se pudo cargar clima' }); 20549 return null; 20550 } finally { 20551 this._cabanaTvWeatherLoading = false;
20552 } 20553 } 20554 isExcludedCabana(item) { 20555 return this.isExcludedRemate(item); 20556 } 20557 canEditCabanaRemate(item) { 20558 if (this.canManageUsers(this.state.apiUser)) return true; 20559 const currentUid = String(this.state.apiUser?.uid || this.state.apiUser?.userId || this.state.apiUser?.id || this.state.user?.uid || ''); 20560 const currentEmail = String(this.state.apiUser?.email || this.state.user?.email || '').trim().toLowerCase(); 20561 const ownerUid = String(item?.creatorUid || item?.createdByUid || item?.ownerUid || item?.userUid || ''); 20562 const ownerEmail = String(item?.creatorEmail || item?.createdByEmail || item?.ownerEmail || item?.userEmail || '').trim().toLowerCase(); 20563 if ((ownerUid && currentUid && ownerUid === currentUid) || (ownerEmail && currentEmail && ownerEmail === currentEmail)) return true; 20564 return false; 20565 } 20566 canEditCabanaLot(remate = this.state.currentCabana, lot = this.state.currentCabanaLot) { 20567 if (this.state.publicAdvanceMode || !remate || !lot) return false; 20568 return this.apiRoleIds(this.state.apiUser).some((role) => ['admin', 'sysadmin', 'consignatario', 'cabanero'].includes(role)); 20569 } 20570 canUploadCabanaLotMedia(remate = this.state.currentCabana, lot = this.state.currentCabanaLot) { 20571 if (!this.canEditCabanaLot(remate,lot)) return false; 20572 return this.canExportCabanaCatalog(remate, this.state.apiUser) || this.apiRoleIds(this.state.apiUser).includes('cabanero'); 20573 } 20574 canUploadCabanaFinal(remate = this.state.currentCabana) { 20575 return !this.state.publicAdvanceMode && window.CabanaPrebid.canUploadFinal(this.apiRoleIds(this.state.apiUser),remate); 20576 } 20577 async chooseCabanaVideoOptions(remate, primaryLotId = '') { 20578 const uid=this.state.user?.uid; 20579 const auctionId=this.cabanaRemateId(remate); 20580 const response=await this.cabanaApiFetch('/api/cabin-auctions/'+encodeURIComponent(auctionId)+'/lots'); 20581 if (this.state.user?.uid !== uid) return null; 20582 const lots=this.normalizeCabanaAuctionLots(this.listFrom(response,['data','items','lots','lotes','groups']),remate); 20583 const unique=[...new Map(lots.map(lot=>[this.cabanaLotId(lot),lot])).values()]; 20584 const options=[{value:'raw',label:'Original (raw)'}]; 20585 if(this.canUploadCabanaFinal(remate)) options.push({value:'final',label:'Video final'}); 20586 const result=await window.CabanaPrebid.editDialog({title:'Cargar video',description:'Elegà el tipo de video y los lotes del remate que lo compartirán. El lote actual siempre queda incluido.',submitLabel:'Continuar carga',fields:[{key:'videoType',label:'Tipo de video',value:options.at(-1).value,options}],choices:unique.map(lot=>({value:this.cabanaLotId(lot),label:'Lote '+this.cabanaLotNumber(lot)+' · '+(lot.categoryName || ''),checked:this.cabanaLotId(lot)===primaryLotId,disabled:this.cabanaLotId(lot)===primaryLotId}))}); 20587 if (this.state.user?.uid !== uid) return null; 20588 if(result?.videoType === 'final' && !this.canUploadCabanaFinal(remate)) throw new Error('No tenés permiso para cargar videos finales.'); 20589 return result; 20590 } 20591 async openCabanaAuditor(item) { 20592 const remate = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana); 20593 if (!this.canAuditCabana(remate)) return this.denyCabanaPrivatePage(remate); 20594 const same = this.cabanaRemateId(remate) === this.cabanaRemateId(this.state.currentCabana); 20595 this.syncCabanaLocation(remate, 'auditor'); 20596 this.setState({route:'cabanaAuditor',currentCabana:remate,currentCabanaLot:null,cabanaLots:same ? this.state.cabanaLots : null,cabanaLotsLoading:true,cabanaPublicDeepLoading:false,cabanaBrandPreparing:false,cabanaAuditorQuery:''}); 20597 window.scrollTo(0,0); 20598 await this.fetchCabanaLots(remate); 20599 } 20600 canAuditCabana(remate = this.state.currentCabana) { 20601 return !this.state.publicAdvanceMode && this.canExportCabanaCatalog(remate, this.state.apiUser); 20602 } 20603 async saveCabanaAuditorPricing(remate, lot, event) { 20604 event?.preventDefault?.(); 20605 if (!this.canAuditCabana(remate) || !this.canEditCabanaLot(remate, lot)) return; 20606 const key = this.cabanaRemateId(remate) + ':' + this.cabanaPrebidCert(lot); 20607 if (this.state.cabanaAuditorSaving?.[key]) return; 20608 const draft = this._cabanaAuditorDrafts?.[key]; 20609 if (!draft) return; 20610 const base = String(draft.base).trim(), increment = String(draft.increment).trim(); 20611 if ((base !== '' && (!Number.isFinite(Number(base)) || Number(base) < 0)) || (increment !== '' && (!Number.isFinite(Number(increment)) || Number(increment) <= 0))) { this.showToast('Ingresá una base mayor o igual a cero y un incremento mayor a cero.'); return; } 20612 this.setState(state => ({ cabanaAuditorSaving: { ...state.cabanaAuditorSaving, [key]: true } })); 20613 try { 20614 await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(this.cabanaRemateId(remate)) + '/lots/' + encodeURIComponent(this.cabanaPrebidCert(lot)), { method: 'PATCH', body: JSON.stringify({ updatePrebidPricing: true, prebidBaseOverride: base === '' ? null : Number(base), prebidIncrementOverride: increment === '' ? null : Number(increment) }) }); 20615 delete this._cabanaAuditorDrafts[key]; 20616 await this.fetchCabanaLots(remate); 20617 this.showToast('Base e incremento guardados.'); 20618 } catch (error) { this.showToast(error?.data?.detail || error.message || 'No se pudo guardar.'); } 20619 finally { this.setState(state => ({ cabanaAuditorSaving: { ...state.cabanaAuditorSaving, [key]: false } })); } 20620 } 20621 async editCabanaPrebidPricing() { 20622 const lot=this.state.currentCabanaLot, remate=this.state.currentCabana; 20623 if(!this.canEditCabanaLot(remate,lot) || this._cabanaPricingBusy) return; 20624 this._cabanaPricingBusy=true; 20625 try { 20626 const {inherited,base,increment}=window.CabanaPrebid.pricing(lot,remate); 20627 const fmt=v=>v==null?'Sin configurar':this.fmtARS(v); 20628 const result=await window.CabanaPrebid.editDialog({title:'Base e incremento del lote',description:'CategorÃa: base '+fmt(inherited.baseAmount)+' · incremento '+fmt(inherited.incrementAmount)+'. Valores efectivos: '+fmt(base)+' / '+fmt(increment)+'. Dejá un campo vacÃo para heredar ese valor.',resetLabel:'Volver a usar configuración de la categorÃa',fields:[{key:'base',label:'Base personalizada',value:lot.prebidBaseOverride,min:0},{key:'increment',label:'Incremento personalizado',value:lot.prebidIncrementOverride,min:0.01}]}); 20629 if(!result) return; 20630 const payload={updatePrebidPricing:true,prebidBaseOverride:result.reset || result.base === ''?null:Number(result.base),prebidIncrementOverride:result.reset || result.increment === ''?null:Number(result.increment)}; 20631 await this.cabanaApiFetch('/api/cabin-auctions/'+encodeURIComponent(this.cabanaRemateId(remate))+'/lots/'+encodeURIComponent(this.cabanaPrebidCert(lot)),{method:'PATCH',body:JSON.stringify(payload)}); 20632 const lots=await this.fetchCabanaLots(remate); 20633 const updated=lots.find(l=>this.cabanaPrebidCert(l)===this.cabanaPrebidCert(lot) && l.itemId===lot.itemId); 20634 if(updated && this.cabanaPrebidCert(this.state.currentCabanaLot)===this.cabanaPrebidCert(lot)) this.setState({currentCabanaLot:updated}); 20635 this.showToast('Configuración de preofertas actualizada.'); 20636 } catch(error){this.showToast(error?.data?.detail || error.message);} 20637 finally{this._cabanaPricingBusy=false;} 20638 } 20639 canDeleteCabanaLot(remate = this.state.currentCabana, lot = this.state.currentCabanaLot) { 20640 if (this.state.publicAdvanceMode || !remate || !lot) return false;
20641 return this.apiRoleIds(this.state.apiUser).some((role) => ['admin', 'sysadmin'].includes(role)); 20642 } 20643 cabanaLotDrafts(remateId = '') { 20644 try { 20645 const drafts = JSON.parse(localStorage.getItem('rosgan:cabanaLotDrafts') || '[]'); 20646 return Array.isArray(drafts) ?drafts.filter((item) => !remateId || String(item?.remateId || '') === String(remateId)) : []; 20647 } catch { return []; } 20648 } 20649 cabanaRemateId(item) { return String(item?.id_remate || item?.sourceId || item?.id || ''); } 20650 cabanaContactData(item = this.state.currentCabana) { 20651 const contact = item?.contacto || item?.contact || {}; 20652 const name = String(contact?.name || contact?.nombre || item?.contactName || item?.contactoNombre || 'Equipo Rosgan').trim(); 20653 const phone = String(contact?.phone || contact?.telefono || item?.contactPhone || item?.contactoTelefono || '').trim(); 20654 const email = String(contact?.email || item?.contactEmail || item?.contactoEmail || '').trim(); 20655 const notes = String(contact?.notes || contact?.notas || item?.contactNotes || item?.contactoNotas || '').trim(); 20656 const phoneDigits = phone.replace(/\D/g, ''); 20657 return { name, phone, email, notes, phoneDigits, phoneHref: phoneDigits ?'tel:+' + phoneDigits : '#', whatsappBase: phoneDigits ?'https://wa.me/' + phoneDigits : '#' }; 20658 } 20659 cabanaRemateSchema(item) { 20660 const normalizeSchema = (schema) => (Array.isArray(schema) ?schema : []).map((category) => { 20661 if (Array.isArray(category?.forms)) return category; 20662 const mainFields = Array.isArray(category?.fields) ?category.fields : []; 20663 const subforms = Array.isArray(category?.subforms) ?category.subforms : []; 20664 return { 20665 ...category, 20666 forms: [ 20667 { templateId: null, name: 'Datos principales', displayOrder: 0, isActive: true, fields: mainFields }, 20668 ...subforms.map((subform) => ({ ...subform, templateId: subform.id || null })), 20669 ].filter((form) => Array.isArray(form.fields) && form.fields.length), 20670 }; 20671 }); 20672 if (Array.isArray(item?.formsByCategory) && item.formsByCategory.length) return normalizeSchema(item.formsByCategory); 20673 const flatForms = Array.isArray(item?.forms) ?item.forms : []; 20674 if (!flatForms.length) return []; 20675 const grouped = new Map(); 20676 flatForms.forEach((form) => { 20677 const categoryId = form?.categoryId || null; 20678 const categoryName = form?.categoryName || form?.categoria || 'CategorÃa'; 20679 const key = String(categoryId || categoryName); 20680 if (!grouped.has(key)) grouped.set(key, { categoryId, categoryName, displayOrder: grouped.size + 1, forms: [] }); 20681 grouped.get(key).forms.push(form); 20682 }); 20683 return normalizeSchema([...grouped.values()].map((category) => ({ 20684 ...category, 20685 forms: category.forms.sort((a, b) => Number(a?.displayOrder || 0) - Number(b?.displayOrder || 0)), 20686 }))); 20687 } 20688 clearCabanaLotMedia() { 20689 [...(this.state.cabanaLotImages || []), ...(this.state.cabanaLotVideosUpload || [])].forEac
20689h((media) => { try { URL.revokeObjectURL(media.url); } catch {} }); 20690 } 20691 cabanaRemateParticipants(item) { 20692 const normalize = (cabana, type, index) => { 20693 const name = String(cabana?.nombre || cabana?.name || '').trim(); 20694 const rawId = cabana?.id || cabana?.cabanaId || null; 20695 return { 20696 id: rawId, 20697 key: type + ':' + String(rawId || name || index), 20698 name: name || 'Cabaña sin nombre', 20699 type, 20700 typeLabel: type === 'principal' ?'Cabaña principal' : 'Cabaña invitada', 20701 logoDataUrl: window.resolveCabanaMediaUrl(cabana?.logoDataUrl || cabana?.logoUrl || (this.consKey(name) === this.consKey('La Paterna') ?'/assets/la-paterna-logo.png' : '')), 20702 logoWhiteDataUrl: window.resolveCabanaMediaUrl(cabana?.logoWhiteDataUrl), 20703 brandColor: String(cabana?.brandColor || (type === 'principal' ?this.cabanaColor(item) : '#65758a')), 20704 }; 20705 }; 20706 const configured = [ 20707 ...(Array.isArray(item?.cabanasPrincipales) ?item.cabanasPrincipales.map((cabana, index) => normalize(cabana, 'principal', index)) : []), 20708 ...(Array.isArray(item?.cabanasInvitadas) ?item.cabanasInvitadas.map((cabana, index) => normalize(cabana, 'invitada', index)) : []), 20709 ]; 20710 if (configured.length) return configured; 20711 const cabins = Array.isArray(item?.cabins) ?item.cabins : []; 20712 return cabins 20713 .slice() 20714 .sort((a, b) => Number(a?.displayOrder || 0) - Number(b?.displayOrder || 0)) 20715 .map((cabana, index) => normalize(cabana, String(cabana?.type || '').toLowerCase() === 'principal' ?'principal' : 'invitada', index)); 20716 } 20717 cabanaLogoReady(event) { 20718 const image = event?.currentTarget; 20719 const tile = image?.closest?.('.cabana-card-main-logo,.cabana-card-guest-logo'); 20720 if (!image || !tile || !image.naturalWidth || !image.naturalHeight) return; 20721 try { 20722 const size = 40; 20723 const canvas = document.createElement('canvas'); 20724 canvas.width = size; 20725 canvas.height = size; 20726 const context = canvas.getContext('2d', { willReadFrequently: true }); 20727 if (!context) return; 20728 context.clearRect(0, 0, size, size); 20729 context.drawImage(image, 0, 0, size, size); 20730 const pixels = context.getImageData(0, 0, size, size).data; 20731 let luminance = 0; 20732 let alpha = 0; 20733 for (let index = 0; index < pixels.length; index += 4) { 20734 const opacity = pixels[index + 3] / 255; 20735 if (opacity < .08) continue; 20736 luminance += ((pixels[index] * .2126) + (pixels[index + 1] * .7152) + (pixels[index + 2] * .0722)) * opacity; 20737 alpha += opacity; 20738 } 20739 tile.classList.toggle('is-light-logo', alpha > 0 && (luminance / alpha) > 205); 20740 } catch {} 20741 } 20742 canCreateCabanaLot(item) { 20743 if (!item || !this.state.user || this.state.publicAdvanceMode) return false; 20744 const roles = this.apiRoleIds(this.state.apiUser); 20745 if (roles.some(role => ['admin', 'sysadmin'].includes(role))) return true; 20746 return roles.some(role => ['consignatario', 'certificador'].includes(role)) 20747 && this.belongsToCabanaConsignataria(item) 20748 && this.cabanaMode(item) !== 'recent' && item?.load?.lotes !== false 20749 && String(item?.cargaLotes || 'abierta').toLowerCase() !== 'cerrada'; 20750 } 20751 openCabanaLotCreate(item, updateHash = true) { 20752 if (!this.canCreateCabanaLot(item)) { this.showToast('No tenés habilitada la carga de lotes en este remate.'); return; } 20753 this.clearCabanaLotMedia(); 20754 const schema = this.cabanaRemateSchema(item).filter((category) => Array.isArray(category?.forms)); 20755 const first = schema[0]; 20756 const firstCabana = this.cabanaRemateParticipants(item)[0]; 20757 this.setState({ route: 'cabanaLotCreate', cabanaLotCreateRemate: { ...item, formsByCategory: schema }, cabanaLotCreateForm: { categoryId: String(first?.categoryId || first?.categoryName || ''), cabanaKey: firstCabana?.key || '', values: {}, observations: '' }, cabanaLotImages: [], cabanaLotVideosUpload: [], cabanaLotCreateLoading: false, cabanaLotEditingCert: '', cabanaLotEditingSource: null }); 20758 if (updateHash) this.syncCabanaLocation(item, 'crear-lote'); 20759 window.scrollTo(0, 0); 20760 } 20761 cabanaLotEditForm(remate, lot) { 20762 const schema = this.cabanaRemateSchema(remate).filter((category) => Array.isArray(category?.forms)); 20763 const categoryId = String(lot?.categoryId || lot?.categoriaId || ''); 20764 const categoryName = String(lot?.categoryName || lot?.categoria || ''); 20765 const category = schema.find((item) =>
20765 categoryId && String(item?.categoryId || '') === categoryId) 20766 || schema.find((item) => categoryName && this.consKey(item?.categoryName) === this.consKey(categoryName)) 20767 || schema[0]; 20768 const categoryKey = String(category?.categoryId || category?.categoryName || 'category'); 20769 const mainValues = lot?.values && typeof lot.values === 'object' ?lot.values : {}; 20770 const subformValues = lot?.subformValues && typeof lot.subformValues === 'object' ?lot.subformValues : {}; 20771 const values = {}; 20772 (category?.forms || []).forEach((subform, formIndex) => { 20773 const formKey = String(subform?.key || subform?.formKey || this.rematePayloadKey(subform?.name || ('subformulario-' + formIndex))); 20774 const source = formIndex === 0 ?mainValues : (subformValues[formKey] || subformValues[this.consKey(formKey)] || subformValues[this.consKey(formKey).replace(/-/g, '_')] || subformValues[this.consKey(subform?.name || '')] || {}); 20775 (subform?.fields || []).forEach((field, fieldIndex) => { 20776 const compositeKey = categoryKey + ':' + formIndex + ':' + fieldIndex; 20777 const fieldKey = String(field?.key || this.rematePayloadKey(field?.name || ('campo-' + fieldIndex))); 20778 let value = source?.[fieldKey]; 20779 if (value === undefined) value = mainValues?.[fieldKey]; 20780 if (value === undefined) value = lot?.[fieldKey]; 20781 if (value === undefined && field?.isPrimaryIdentifier) value = this.cabanaPrebidCert(lot) || this.cabanaLotId(lot); 20782 if (value !== undefined && value !== null) values[compositeKey] = value; 20783 }); 20784 }); 20785 const participants = this.cabanaRemateParticipants(remate); 20786 const selectedCabana = participants.find((cabana) => String(cabana.id || '') === String(lot?.cabanaId || '')) 20787 || participants.find((cabana) => this.consKey(cabana.name) === this.consKey(lot?.cabanaNombre || lot?.cabanaName || '')) 20788 || participants[0]; 20789 return { 20790 schema, 20791 form: { 20792 categoryId: categoryKey, 20793 cabanaKey: selectedCabana?.key || '', 20794 values, 20795 observations: String(lot?.observations || lot?.observaciones || mainValues?.observaciones || ''), 20796 }, 20797 }; 20798 } 20799 async openCabanaLotEdit(item, lotOrCert, updatePath = true) { 20800 const remate = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana); 20801 const requestedCert = typeof lotOrCert === 'object' ?(this.cabanaPrebidCert(lotOrCert) || this.cabanaLotId(lotOrCert)) : String(lotOrCert || '').trim(); 20802 const permissionLot = typeof lotOrCert === 'object' ?lotOrCert : { lotCert: requestedCert }; 20803 if (!this.canEditCabanaLot(remate, permissionLot)) { this.showToast('No tenés permiso para editar este lote.'); return; } 20804 if (!requestedCert) { this.showToast('No se pudo identificar el lote.'); return; } 20805 if (updatePath) this.syncCabanaLocation(remate, 'lotes/' + encodeURIComponent(requestedCert) + '/editar'); 20806 this.clearCabanaLotMedia(); 20807 this.setState({ route: 'cabanaLotCreate', cabanaLotCreateRemate: remate, cabanaLotCreateLoading: true, cabanaLotEditingCert: requestedCert, cabanaLotEditingSource: null, cabanaLotImages: [], cabanaLotVideosUpload: [] }); 20808 let lot = typeof lotOrCert === 'object' ?lotOrCert : null; 20809 if (!lot) { 20810 const sameAuction = this.cabanaRemateId(this.state.currentCabana) === this.cabanaRemateId(remate); 20811 const cached = sameAuction && Array.isArray(this.state.cabanaLots) ?this.state.cabanaLots : []; 20812 lot = cached.find((candidate) => this.cabanaPrebidCert(candidate) === requestedCert || this.cabanaLotId(candidate) === requestedCert) || null; 20813 } 20814 if (!lot) { 20815 const lots = await this.fetchCabanaLots(remate); 20816 lot = lots.find((candidate) => this.cabanaPrebidCert(candidate) === requestedCert || this.cabanaLotId(candidate) === requestedCert) || null; 20817 } 20818 if (!lot) { 20819 this.setState({ cabanaLotCreateLoading: false, cabanaLotEditingCert: '', cabanaLotEditingSource: null }); 20820 this.showToast('No encontramos el lote que querés editar.'); 20821 this.openCabana(remate); 20822 return; 20823 } 20824 const prepared = this.cabanaLotEditForm(remate, lot); 20825 this.setState({ 20826 route: 'cabanaLotCreate', 20827 cabanaLotCreateRemate: { ...remate, formsByCategory: prepared.schema }, 20828 cabanaLotCreateForm: prepared.form, 20829 cabanaLotCreateLoading: false, 20830 cabanaLotEditingCert: requestedCert, 20831 cabanaLotEditingSource: lot, 20832 }); 20833 window.scrollTo(0, 0); 20834 } 20835 async deleteCabanaLot(remate = this.state.currentCabana, lot = this.state.currentCabanaLot) { 20836 if (!this.canDeleteCabanaLot(remate, lot)) { this.showToast('No tenés permiso para eliminar este lote.'); return; } 20837 const auctionId = this.cabanaRemateId(remate); 20838 const lotCert = this.cabanaPrebidCert(lot) || this.cabanaLotId(lot); 20839 if (!auctionId || !lotCert || this.state.cabanaLotDeleting) return; 20840 const lotLabel = this.cabanaLotNumber(lot) || lotCert; 20841 if (!window.confirm('¿Eliminar el lote ' + lotLabel + '? Esta acción quitará el lote completo del remate.')) return; 20842 this.setState({ cabanaLotDeleting: lotCert }); 20843 try { 20844 await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert), { method: 'DELETE' }); 20845 this.flushCabanaLotViewTimer('lot_deleted'); 20846 this.setState({ cabanaLotDeleting: '', currentCabanaLot: null, currentCabanaLotVariants: [] }); 20847 this.showToast('Lote eliminado.'); 20848 this.openCabana(remate); 20849 } catch (error) { 20850 this.setState({ cabanaLotDeleting: '' }); 20851 this.showToast(error?.message || 'No se pudo eliminar el lote.'); 20852 } 20853 } 20854 closeCabanaLotCreate() { 20855 this.clearCabanaLotMedia(); 20856 const remate = this.state.cabanaLotCreateRemate; 20857 const editingCert = this.state.cabanaLotEditingCert; 20858 if (editingCert && remate) { 20859 this.setState({ cabanaLotCreateRemate: null, cabanaLotCreateForm: null, cabanaLotImages: [], cabanaLotVideosUpload: [], cabanaLotCreateLoading: false, cabanaLotEditingCert: '', cabanaLotEditingSource: null }, () => this.openCabana(remate, { focusLotCert: editingCert })); 20860 return; 20861 } 20862 this.syncAppLocation('cabanas'); 20863 this.setState({ route: 'cabanas', cabanaLotCreateRemate: null, cabanaLotCreateForm: null, cabanaLotImages: [], cabanaLotVideosUpload: [], cabanaLotCreateLoading: false, cabanaLotEditingCert: '', cabanaLotEditingSource: null }); 20864 window.scrollTo(0, 0); 20865 } 20866 setCabanaLotCabana(cabanaKey) { this.setState({ cabanaLotCreateForm: { ...(this.state.cabanaLotCreateForm || {}
20866), cabanaKey: String(cabanaKey || '') } }); } 20867 setCabanaLotCategory(categoryId) { this.setState({ cabanaLotCreateForm: { ...(this.state.cabanaLotCreateForm || {}), categoryId: String(categoryId || '') } }); } 20868 setCabanaLotValue(key, value, buffered = false) { 20869 const form = this.state.cabanaLotCreateForm || { values: {} }; 20870 const patch = { cabanaLotCreateForm: { ...form, values: { ...(form.values || {}), [key]: value } } }; 20871 if (buffered) this.bufferInputState('cabana-lot-' + key, patch); 20872 else this.setState(patch); 20873 } 20874 addCabanaLotMedia(kind, files) { 20875 const isVideo = kind === 'video'; 20876 const stateKey = isVideo ?'cabanaLotVideosUpload' : 'cabanaLotImages'; 20877 const limit = isVideo ?3 : 8; 20878 const current = this.state[stateKey] || []; 20879 const maxVideoSize = 500 * 1024 * 1024; 20880 const candidates = Array.from(files || []); 20881 const accepted = candidates.filter((file) => isVideo 20882 ?file.type.startsWith('video/') && file.size <= maxVideoSize 20883 : file.type.startsWith('image/')).slice(0, Math.max(0, limit - current.length)); 20884 if (isVideo && candidates.some((file) => file.size > maxVideoSize)) { 20885 this.showToast('Cada video puede pesar como máximo 500 MB.'); 20886 } 20887 if (!accepted.length) { this.showToast(current.length >= limit ?'Alcanzaste el máximo de archivos.' : 'Seleccioná archivos válidos.'); return; } 20888 const media = accepted.map((file) => ({ id: kind + '-' + Date.now() + '-' + Math.random().toString(36).slice(2), file, name: file.name, size: file.size, url: URL.createObjectURL(file) })); 20889 this.setState({ [stateKey]: [...current, ...media] }); 20890 } 20891 removeCabanaLotMedia(kind, id) { 20892 const stateKey = kind === 'video' ?'cabanaLotVideosUpload' : 'cabanaLotImages'; 20893 const current = this.state[stateKey] || []; 20894 const removed = current.find((media) => media.id === id); 20895 if (removed) { try { URL.revokeObjectURL(removed.url); } catch {} } 20896 this.setState({ [stateKey]: current.filter((media) => media.id !== id) }); 20897 } 20898 async uploadCabanaLotVideos(auctionId, lotCert, videos, metadata) { 20899 const uploadedMediaIds = []; 20900 const associatedLotIds = [...new Set((metadata.associatedLotIds || []).map(String))]; 20901 for (let index = 0; index < videos.length; index++) { 20902 const media = videos[index]; 20903 const file = media?.file; 20904 if (!file) continue; 20905 try { 20906 this.showToast('Subiendo video ' + (index + 1) + ' de ' + videos.length + 'â¦'); 20907 const common = { 20908 videoType: metadata.videoType === 'final' ? 'final' : 'raw', 20909 associatedLotIds, 20910 cabanaName: metadata.cabanaName, 20911 primaryIdentifierKey: metadata.primaryIdentifierKey, 20912 primaryIdentifierName: metadata.primaryIdentifierName, 20913 primaryIdentifierValue: metadata.primaryIdentifierValue, 20914 lotLabel: metadata.lotLabel, 20915 paintLabel: metadata.paintLabel || null, 20916 }; 20917 const upload = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert) + '/videos/upload-url', { 20918 method: 'POST', 20919 body: JSON.stringify({ fileName: file.name, contentType: file.type || 'application/octet-stream', sizeBytes: file.size, ...common }), 20920 }); 20921 const uploadUrl = upload?.uploadUrl || upload?.signedUrl; 20922 const fileKey = upload?.fileKey || upload?.key; 20923 if (!uploadUrl || !fileKey) throw new Error('La API no devolvió los datos necesarios para subir el video.'); 20924 await this.uploadSignedFile(uploadUrl, file, (progress) => metadata.onProgress?.(progress)); 20925 metadata.onProcessing?.(); 20926 await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert) + '/videos/metadata', { 20927 method: 'POST', 20928 body: JSON.stringify({ 20929 fileKey, 20930 fileName: file.name, 20931 contentType: file.type || 'application/octet-stream', 20932 sizeBytes: file.size, 20933 order: Number(upload?.order || index + 1), 20934 ...common, 20935 }), 20936 }); 20937 uploadedMediaIds.push(media.id); 20938 } catch (error) { 20939 error.uploadedMediaIds = uploadedMediaIds; 20940 throw error; 20941 } 20942 } 20943 return uploadedMediaIds; 20944 }
20945 async quickUploadCabanaLotVideo(event, targetLot = null) { 20946 const input = event?.target; 20947 const files = Array.from(input?.files || []); 20948 if (input) input.value = ''; 20949 const remate = this.state.currentCabana; 20950 const lot = targetLot || this.state.currentCabanaLot; 20951 const cardUpload = !!targetLot; 20952 const cardUploadKey = this.cabanaLotId(lot); 20953 const currentCardUpload = this.state.cabanaCardVideoUploads?.[cardUploadKey] || {}; 20954 if (!files.length || (cardUpload ?currentCardUpload.busy : this.state.cabanaLotQuickVideoUploading)) return; 20955 if (!this.canUploadCabanaLotMedia(remate, lot)) { this.showToast('No tenés permiso para subir videos a este remate.'); return; } 20956 if (files.some(file => !String(file.type || '').startsWith('video/') && !/\.(mp4|mov|m4v|webm)$/i.test(file.name || ''))) { this.showToast('Seleccioná un archivo de video válido.'); return; } 20957 if (files.some(file => file.size > 500 * 1024 * 1024)) { this.showToast('El video puede pesar como máximo 500 MB.'); return; } 20958 const auctionId = this.cabanaRemateId(remate); 20959 const lotCert = this.cabanaPrebidCert(lot) || this.cabanaLotId(lot); 20960 if (!auctionId || !lotCert) { this.showToast('No pudimos identificar el remate o el lote.'); return; } 20961 let videoOptions; 20962 try {const type = input?.dataset?.videoType; if (type === 'final' && !this.canUploadCabanaFinal(remate)) throw new Error('No tenés permiso para cargar videos finales.'); videoOptions = ['raw', 'final'].includes(type) ? {videoType:type, associatedLotIds:[lotCert]} : await this.chooseCabanaVideoOptions(remate,lotCert);if(!videoOptions)return;} catch(error){this.showToast(error?.data?.detail || error.message);return;} 20963 if (videoOptions.videoType === 'final' && files.length !== 1) { this.showToast('Seleccioná un solo video final. Para varios archivos elegà Crudos.'); return; } 20964 const participants = this.cabanaRemateParticipants(remate); 20965 const selectedCabana = participants.find((cabana) => String(cabana.id || '') === String(lot?.cabanaId || '')) 20966 || participants.find((cabana) => this.consKey(cabana.name) === this.consKey(lot?.cabanaNombre || lot?.cabanaName || '')) 20967 || participants[0] || {}; 20968 const values = lot?.values || {}; 20969 const isCorral = Object.keys(values).some((key) => this.consKey(key) === 'corral') || /corral/i.test(this.cabanaLotNumber(lot)); 20970 const identifier = String((isCorral ?values.corral : (values.rp || lot?.rp)) || this.cabanaPrebidAnimalCert(lot) || this.cabanaLotNumber(lot) || lotCert).trim(); 20971 const primaryIdentifierKey = isCorral ?'corral' : 'rp'; 20972 const updateProgress = (progress, stage, busy = true) => cardUpload 20973 ?this.setState((state) => ({ cabanaCardVideoUploads: { ...(state.cabanaCardVideoUploads || {}), [cardUploadKey]: { busy, progress, stage } } })) 20974 :this.setState({ cabanaLotQuickVideoUploading: busy, cabanaLotQuickVideoProgress: progress, cabanaLotQuickVideoStage: stage }); 20975 updateProgress(0, 'Preparando la cargaâ¦'); 20976 try { 20977 await this.uploadCabanaLotVideos(auctionId, lotCert, files.map((file,index) => ({ id: 'quick-' + Date.now() + '-' + index, file })), { 20978 ...videoOptions, 20979 onProgress: (progress) => updateProgress(progress, 'Subiendo videoâ¦'), 20980 onProcessing: () => updateProgress(100, 'Procesando portadaâ¦'), 20981 cabanaName: lot?.cabanaNombre || lot?.cabanaName || selectedCabana.name || 'Cabaña', 20982 primaryIdentifierKey, 20983 primaryIdentifierName: isCorral ?'Corral' : 'RP', 20984 primaryIdentifierValue: identifier, 20985 lotLabel: (isCorral ?'Corral ' : 'Lote ') + identifier, 20986 paintLabel: String(values.pintura || lot?.pintura || '').trim(), 20987 }); 20988 await this.fetchCabanaLotVideos({ ...lot, __videosLoaded: false }); 20989 this.showToast(files.length === 1 ? 'Video subido correctamente.' : files.length + ' videos subidos correctamente.'); 20990 } catch (error) { 20991 this.showToast(error?.message || 'No se pudo subir el video.'); 20992 } finally { 20993 updateProgress(0, '', false); 20994 } 20995 } 20996 async deleteCabanaLotVideo(remate, lot, video, event = null) { 20997 event?.preventDefault?.(); 20998 event?.stopPropagation?.(); 20999 const videoId = String(video?.id || '').trim(); 21000 const auctionId = this.cabanaRemateId(remate); 21001 const lotCert = this.cabanaPrebidCert(lot) || this.cabanaLotId(lot); 21002 if (!videoId || !auctionId || !lotCert || this.state.cabanaLotVideoDeleting) return; 21003 if (!this.canExportCabanaCatalog(remate, this.state.apiUser)) { this.showToast('No tenés permiso para eliminar videos de este remate.'); return; } 21004 const shared=[...new Set([lotCert,...(video.associatedLotIds || [])].map(String))]; 21005 if (!window.confirm('¿Eliminar este video? '+(shared.length > 1 ?'Se eliminará el archivo compartido de todos los lotes asociados ('+shared.join(', ')+'), no solamente de este lote. ':'')+'Esta acción no se puede deshacer.')) return; 21006 this.setState({ cabanaLotVideoDeleting: videoId }); 21007 try { 21008 await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert) + '/videos/' + encodeURIComponent(videoId), { method: 'DELETE' }); 21009 const refreshedLots = await this.fetchCabanaLots(remate); 21010 const targetItemId = String(lot?.itemId || ''); 21011 const refreshedLot = refreshedLots.find((item) => this.cabanaLotId(item) === String(lotCert) && (!targetItemId || String(item?.itemId || '')
21011=== targetItemId)); 21012 if (this.cabanaLotId(this.state.currentCabanaLot) === String(lotCert)) { 21013 this.setState({ currentCabanaLot: refreshedLot || { ...lot, videos: [], coverImage: null, lotCoverImage: null }, cbLotVideo: 0, cabanaLotMediaReady: false, cabanaLotMediaFailed: false }); 21014 } 21015 this.showToast('Video eliminado correctamente.'); 21016 } catch (error) { 21017 this.showToast(error?.message || 'No se pudo eliminar el video.'); 21018 } finally { 21019 this.setState({ cabanaLotVideoDeleting: '' }); 21020 } 21021 } 21022 async saveCabanaLotDraft() { 21023 const remate = this.state.cabanaLotCreateRemate; 21024 const form = this.state.cabanaLotCreateForm || {}; 21025 const editingCert = String(this.state.cabanaLotEditingCert || '').trim(); 21026 const editingSource = this.state.cabanaLotEditingSource; 21027 if (this.state.cabanaLotCreateSaving) return; 21028 if (editingCert && !this.canEditCabanaLot(remate, editingSource || { lotCert: editingCert })) { 21029 this.showToast('No tenés permiso para editar este lote.'); 21030 return; 21031 } 21032 if (!editingCert && !this.canCreateCabanaLot(remate)) { 21033 this.showToast('No tenés habilitada la carga de lotes en este remate.'); 21034 return; 21035 } 21036 const cabanas = this.cabanaRemateParticipants(remate); 21037 const selectedCabana = cabanas.find((cabana) => cabana.key === String(form.cabanaKey || '')); 21038 if (!selectedCabana) { this.showToast('Seleccioná la cabaña a la que pertenece el lote.'); return; } 21039 if (!form.categoryId) { this.showToast('Seleccioná una categorÃa.'); return; } 21040 const schema = this.cabanaRemateSchema(remate); 21041 const category = schema.find((item) => String(item.categoryId || item.categoryName) === String(form.categoryId)); 21042 const categoryValueKey = String(category?.categoryId || category?.categoryName || 'category'); 21043 const fields = (category?.forms || []).flatMap((subform, formIndex) => (subform.fields || []).map((field, fieldIndex) => ({ field, key: categoryValueKey + ':' + formIndex + ':' + fieldIndex }))); 21044 const primaryField = fields.find((item) => item.field.isPrimaryIdentifier); 21045 const primaryKey = primaryField?.key || ''; 21046 const identifier = String(primaryKey ?form.values?.[primaryKey] || '' : '').trim(); 21047 if (!identifier) { this.showToast('Completá el identificador principal del lote.'); return; } 21048 const mainValues = {}; 21049 const subforms = []; 21050 (category?.forms || []).forEach((subform, formIndex) => { 21051 const values = {}; 21052 (subform.fields || []).forEach((field, fieldIndex) => { 21053 const compositeKey = categoryValueKey + ':' + formIndex + ':' + fieldIndex; 21054 const value = form.values?.[compositeKey]; 21055 if (value === '' || value === null || value === undefined) return; 21056 const fieldKey = String(field?.key || this.rematePayloadKey(field?.name || ('campo-' + fieldIndex))); 21057 values[fieldKey] = value; 21058 }); 21059 if (!Object.keys(values).length) return; 21060 if (formIndex === 0) Object.assign(mainValues, values); 21061 else subforms.push({ formKey: String(subform?.key || this.rematePayloadKey(subform?.name || ('subformulario-' + formIndex))), values }); 21062 }); 21063 const auctionId = this.cabanaRemateId(remate); 21064 const editingItemId = String(editingSource?.itemId || editingSource?.cert || '').trim(); 21065 const itemPayload = { 21066 ...(editingCert && editingItemId ?{ id: editingItemId } : {}), 21067 cert: identifier, 21068 displayOrder: editingCert ?Number(editingSource?.itemDisplayOrder || editingSource?.displayOrder || 1) : 1, 21069 values: mainValues, 21070 subforms, 21071 ...(String(form.observations || '').trim() ?{ observations: String(form.observations).trim() } : {}), 21072 }; 21073 const editingItems = editingCert 21074 ?(this.state.cabanaLots || []).filter((lot) => this.cabanaPrebidCert(lot) === editingCert).map((lot) => { 21075 const lotItemId = String(lot?.itemId || lot?.id || '').trim(); 21076 const isEditingItem = editingItemId ?lotItemId === editingItemId : lot === editingSource; 21077 if (isEditingItem) return itemPayload; 21078 return { 21079 ...(lotItemId ?{ id: lotItemId } : {}), 21080 cert: this.cabanaPrebidAnimalCert(lot) || lotItemId, 21081 displayOrder: Number(lot?.itemDisplayOrder || lot?.displayOrder || 1), 21082 values: lot?.values && typeof lot.values === 'object' ?lot.values : {}, 21083 subforms: Array.isArray(lot?.subforms) ?lot.subforms : [], 21084 ...(String(lot?.observations || lot?.observaciones || '').trim() ?{ observations: String(lot?.observations || lot?.observaciones).trim() } : {}), 21085 }; 21086 }) 21087 : []; 21088 const payload = { 21089 updatePrebidPricing: false, 21090 categoryId: category?.categoryId || null, 21091 categoryName: category?.categoryName || '', 21092 cabanaId: editingCert ?(editingSource?.cabanaId || selectedCabana.id || null) : (selectedCabana.id || null), 21093 cabanaName: selectedCabana.name || '', 21094 displayOrder: editingCert ?Number(editingSource?.groupDisplayOrder || editingSource?.displayOrder || 1) : 1, 21095 items: editingItems.length ?editingItems : [itemPayload], 21096 }; 21097 const primaryIdentifierKey = String(primaryField?.field?.key || this.rematePayloadKey(primaryField?.field?.name || '')).trim(); 21098 const primaryIdentifierName = String(primaryField?.field?.name || primaryIdentifierKey).trim(); 21099 const paintField = fields.find((item) => this.consKey(item.field?.key || item.field?.name) === 'pintura'); 21100 const paintLabel = String(paintField ?form.values?.[paintField.key] || '' : '').trim(); 21101 const videoMetadata = { 21102 cabanaName: selectedCabana.name || '', 21103 primaryIdentifierKey, 21104 primaryIdentifierName, 21105 primaryIdentifierValue: identifier, 21106 lotLabel: (this.consKey(primaryIdentifierName).includes('corral') ?'Corral ' : 'Lote ') + identifier, 21107 paintLabel, 21108 }; 21109 const pendingVideos = [...(this.state.cabanaLotVideosUpload || [])]; 21110 if (pendingVideos.length) {try {const options=await this.chooseCabanaVideoOptions(remate,editingCert);if(!options)return;if(options.videoType==='final' && pendingVideos.length!==1){this.showToast('Seleccioná un solo video final. Para varios archivos elegà Crudos.');return;}Object.assign(videoMetadata,options);}catch(error){this.showToast(error?.data?.detail || error.message);return;}} 21111 let savedLotCert = editingCert; 21112 let savedLot = null; 21113 this.setState({ cabanaLotCreateSaving: true }); 21114 try { 21115 const path = '/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots' + (editingCert ?'/' + encodeURIComponent(editingCert) : ''); 21116 savedLot = await this.cabanaApiFetch(path, { method: editingCert ?'PATCH' : 'POST', body: JSON.stringify(payload) }); 21117 savedLotCert = String(savedLot?.lotCert || savedLot?.id || editingCert || '').trim(); 21118 if (!savedLotCert) throw new Error('El lote se guardó, pero la API no devolvió su identificador para subir videos.'); 21119 if (pendingVideos.length) await this.uploadCabanaLotVideos(auctionId, savedLotCert, pendingVideos, videoMetadata); 21120 this.showToast(editingCert ?'Lote actualizado.' : 'Lote creado en el remate.'); 21121 this.clearCabanaLotMedia(); 21122 if (editingCert) { 21123 const lots = await this.fetchCabanaLots(remate); 21124 const updatedLot = lots.find((lot) =>
21124 editingItemId && String(lot?.itemId || '') === editingItemId && (this.cabanaPrebidCert(lot) === editingCert || this.cabanaLotId(lot) === editingCert)) 21125 || lots.find((lot) => this.cabanaPrebidCert(lot) === editingCert || this.cabanaLotId(lot) === editingCert) 21126 || null; 21127 this.setState({ 21128 cabanaLotCreateSaving: false, 21129 cabanaLotCreateRemate: null, 21130 cabanaLotCreateForm: null, 21131 cabanaLotEditingCert: '', 21132 cabanaLotEditingSource: null, 21133 cabanaLotImages: [], 21134 cabanaLotVideosUpload: [], 21135 currentCabanaLot: updatedLot, 21136 }, () => { 21137 if (updatedLot) { 21138 const groupKey = this.cabanaLotGroupKey(updatedLot); 21139 const variants = lots.filter((lot) => this.cabanaLotGroupKey(lot) === groupKey); 21140 this.openCabanaLot(updatedLot, variants.length ?variants : [updatedLot]); 21141 } 21142 else this.openCabana(remate, { focusLotCert: editingCert }); 21143 }); 21144 } else { 21145 this.setState({ cabanaLotCreateSaving: false, cabanaLotCreateRemate: null, cabanaLotCreateForm: null, route: 'cabanas', cabanaLotEditingCert: '', cabanaLotEditingSource: null }); 21146 try { window.location.hash = '#/cabanas'; } catch {} 21147 await this.fetchCabanas(true); 21148 } 21149 } catch (error) { 21150 const normalizedSaved = savedLot ?this.normalizeCabanaAuctionLots([savedLot], remate)[0] : null; 21151 this.setState({ 21152 cabanaLotCreateSaving: false, 21153 ...(error?.uploadedMediaIds?.length ?{ 21154 cabanaLotVideosUpload: (this.state.cabanaLotVideosUpload || []).filter((media) => !error.uploadedMediaIds.includes(media.id)), 21155 } : {}), 21156 ...(!editingCert && savedLotCert ?{ cabanaLotEditingCert: savedLotCert, cabanaLotEditingSource: normalizedSaved || { ...payload, lotCert: savedLotCert } } : {}), 21157 }); 21158 const savedButUploadFailed = !!savedLot; 21159 this.showToast(savedButUploadFailed 21160 ?'El lote quedó guardado, pero el video no pudo subirse: ' + (error?.message || 'intentá nuevamente.') 21161 : (error?.message || (editingCert ?'No se pudo actualizar el lote.' : 'No se pudo crear el lote.'))); 21162 } 21163 } 21164 async fetchCabanas(force = false) { 21165 if (this._cabanasRequest) return this._cabanasRequest; 21166 const cacheKey = 'rosgan:cabanas:v2'; 21167 const cached = !force && !Array.isArray(this.state.cabanas) ?this.readCollectionCache(cacheKey) : null; 21168 const fallback = Array.isArray(cached) ?cached : this.state.cabanas; 21169 const fallbackIsComplete = Array.isArray(cached) || !!this.state.cabanasCollectionReady; 21170 this.setState({ 21171 cabanas: Array.isArray(fallback) ?fallback : this.state.cabanas, 21172 cabanasLoading: true, 21173 cabanasCollectionReady: fallbackIsComplete, 21174 cabanasError: '', 21175 }, () => { 21176 if (fallbackIsComplete && this.state.route === 'cabanas') this.fetchCabanaCardMedia(fallback); 21177 }); 21178 const request = (async () => { 21179 try { 21180 const data = await this.cabanaApiFetch('/api/cabin-auctions'); 21181 const items = this.listFrom(data, ['items', 'data', 'auctions', 'cabinAuctions']).map((item) => this.normalizeCabanaAuctionRecord(item)).filter((item) => !this.isExcludedCabana(item)).sort((a, b) => String(b?.updatedAt || b?.createdAt || '').localeCompare(String(a?.updatedAt || a?.createdAt || ''))); 21182 this.writeCollectionCache(cacheKey, items, 30 * 60 * 1000); 21183 this.setState({ cabanas: items, cabanasLoading: false, cabanasCollectionReady: true, cabanasError: '' }, () => { 21184 this.loadRemateListCatalogs(items, 'cabana'); 21185 if (this.state.route === 'cabanas') this.fetchCabanaCardMedia(items); 21186 }); 21187 return items; 21188 } catch (error) { 21189 this.setState({ 21190 cabanas: Array.isArray(this.state.cabanas) ?this.state.cabanas : (fallback || []), 21191 cabanasLoading: false, 21192 cabanasCollectionReady: fallbackIsComplete, 21193 cabanasError: error?.message || 'No se pudieron cargar los remates de cabaña.', 21194 }); 21195 return null; 21196 } 21197 })(); 21198 this._cabanasRequest = request; 21199 try { return await request; } 21200 finally { if (this._cabanasRequest === request) this._cabanasRequest = null; } 21201 } 21202 cabanaCardMediaFrames(urls = []) { 21203 const unique = [...new Set((urls || []).map(window.resolveCabanaMediaUrl).filter(
21203Boolean))].slice(0, 8); 21204 if (!unique.length) return []; 21205 return Array.from({ length: 8 }, (_, index) => ({ 21206 key: index + ':' + unique[index % unique.length], 21207 src: unique[index % unique.length], 21208 delay: (-index * 5) + 's', 21209 })); 21210 } 21211 async fetchCabanaCardMedia(items = this.state.cabanas || []) { 21212 const auctions = (Array.isArray(items) ?items : []).filter((item) => this.cabanaRemateId(item)); 21213 if (!auctions.length) return; 21214 if (!this._cabanaCardMediaRequests) this._cabanaCardMediaRequests = new Set(); 21215 const pending = auctions.filter((item) => { 21216 const id = this.cabanaRemateId(item); 21217 return !this.state.cabanaCardMedia?.[id]?.ready && !this._cabanaCardMediaRequests.has(id); 21218 }); 21219 await Promise.all(pending.map(async (item) => { 21220 const id = this.cabanaRemateId(item); 21221 this._cabanaCardMediaRequests.add(id); 21222 try { 21223 const data = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(id) + '/lots/'); 21224 const groups = this.listFrom(data, ['data', 'items', 'lots', 'lotes', 'groups']); 21225 const lots = this.normalizeCabanaAuctionLots(groups, item); 21226 const videoThumbs = lots.flatMap((lot) => this.cabanaLotVideos(lot).map((video) =>
21226 video.thumb).filter(Boolean)); 21227 const coverImages = lots.flatMap((lot) => this.cabanaLotImages(lot).map((image) => image.url).filter(Boolean)); 21228 const frames = this.cabanaCardMediaFrames([...videoThumbs, ...coverImages]); 21229 this.setState((state) => ({ cabanaCardMedia: { ...(state.cabanaCardMedia || {}), [id]: { ready: true, frames } } })); 21230 } catch { 21231 this.setState((state) => ({ cabanaCardMedia: { ...(state.cabanaCardMedia || {}), [id]: { ready: true, frames: [] } } })); 21232 } finally { 21233 this._cabanaCardMediaRequests.delete(id); 21234 } 21235 })); 21236 } 21237 async fetchCabanaById(id, options = {}) { 21238 const cleanId = String(id || '').trim(); 21239 if (!cleanId) return null; 21240 const existing = (this.state.cabanas || []).find((item) => this.cabanaRemateId(item) === cleanId); 21241 if (existing?.__detailLoaded && !options.force) return existing; 21242 const data = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(cleanId)); 21243 const item = this.normalizeCabanaAuctionRecord(data?.data || data?.item || data?.auction || data?.cabinAuction || data); 21244 if (!item || this.isExcludedCabana(item)) return null; 21245 const itemId = this.cabanaRemateId(item) || cleanId; 21246 const merged = { ...item, id: item?.id || itemId, __detailLoaded: true, __detailLoadedAt: Date.now() }; 21247 this.setState({ cabanas: [merged, ...(this.state.cabanas || []).filter((entry) => this.cabanaRemateId(entry) !== itemId)] }); 21248 return merged; 21249 } 21250 async fetchCabanaAnalyticsSummary(item = this.state.currentCabana) { 21251 const auctionId = this.cabanaRemateId(item); 21252 if (!auctionId) return null; 21253 this.setState({ cabanaAnalyticsLoading: true }); 21254 try { 21255 const summary = await this.fetchCabanaAnalyticsSummaryById(auctionId); 21256 if (this.cabanaRemateId(this.state.currentCabana) === auctionId) { 21257 this.setState({ cabanaAnalyticsSummary: summary, cabanaAnalyticsLoading: false }); 21258 } 21259 return summary; 21260 } catch (error) { 21261 if (this.cabanaRemateId(this.state.currentCabana) === auctionId) { 21262 this.setState({ cabanaAnalyticsSummary: null, cabanaAnalyticsLoading: false }); 21263 } 21264 return null; 21265 } 21266 } 21267 async fetchCabanaAnalyticsSummaryById(auctionId) { 21268 const cleanId = String(auctionId || '').trim(); 21269 if (!cleanId) return null; 21270 const token = await this.authToken(false); 21271 const response = await fetch('/api/cabanas-analytics/summary?auctionId=' + encodeURIComponent(cleanId) + '&limit=500', { 21272 headers: { accept: 'application/json', Authorization: 'Bearer ' + token }, 21273 cache: 'no-store', 21274 }); 21275 if (!response.ok) throw new Error('Analytics HTTP ' + response.status); 21276 return response.json(); 21277 } 21278 cabanaSalesCategoryKey(lot) { 21279 return String(lot?.categoryId || this.consKey(lot?.categoryName || 'sin-categoria')); 21280 } 21281 cabanaSalesCategoryName(lot) { 21282 return String(lot?.categoryName || 'Sin categorÃa'); 21283 } 21284 cabanaCategoryDisplayOrder(lot, fallback = Number.MAX_SAFE_INTEGER) { 21285 const candidates = [ 21286 lot?.categoryDisplayOrder, 21287 lot?.category_display_order, 21288 lot?.categoryOrder, 21289 lot?.category?.displayOrder, 21290 lot?.category?.categoryDisplayOrder, 21291 lot?.categoria?.displayOrder, 21292 lot?.categoria?.categoryDisplayOrder, 21293 ]; 21294 for (const value of candidates) { 21295 const parsed = Number(value); 21296 if (Number.isFinite(parsed)) return parsed; 21297 } 21298 return fallback; 21299 } 21300 cabanaLotSortNumber(value, fallback = Number.MAX_SAFE_INTEGER) { 21301 const parsed = Number(value); 21302 return Number.isFinite(parsed) && parsed >= 0 ?parsed : fallback; 21303 } 21304 cabanaSortLotsByCategoryOrder(lots) { 21305 return (Array.isArray(lots) ?lots : []).slice().sort((a, b) => { 21306 const categoryOrder = this.cabanaCategoryDisplayOrder(a) - this.cabanaCategoryDisplayOrder(b); 21307 if (categoryOrder) return categoryOrder; 21308 const groupOrder = this.cabanaLotSortNumber(a?.displayOrder ?? a?.groupDisplayOrder) - this.cabanaLotSortNumber(b?.displayOrder ?? b?.groupDisplayOrder); 21309 if (groupOrder) return groupOrder; 21310 const lotOrder = String(this.cabanaLotNumber(a)).localeCompare(String(this.cabanaLotNumber(b)), 'es', { numeric: true, sensitivity: 'base' }); 21311 if (lotOrder) return lotOrder; 21312 const itemOrder = this.cabanaLotSortNumber(a?.itemDisplayOrder ?? a?.displayOrder) - this.cabanaLotSortNumber(b?.itemDisplayOrder ?? b?.displayOrder); 21313 if (itemOrder) return itemOrder; 21314 return String(this.cabanaAnalyticsRpId(a)).localeCompare(String(this.cabanaAnalyticsRpId(b)), 'es', { numeric: true, sensitivity: 'base' }); 21315 }); 21316 } 21317 normalizeCabanaSalesOrder(lots) { 21318 return (Array.isArray(lots) ?lots : []).map((lot, index) => ({ ...lot, displayOrder: index + 1 })); 21319 } 21320 sortedCabanaSalesOrder(lots) { 21321 const displayOrderOf = (lot) => { 21322 const value = Number(lot?.displayOrder); 21323 return Number.isFinite(value) && value > 0 ?value : Number.MAX_SAFE_INTEGER; 21324 }; 21325 return this.normalizeCabanaSalesOrder([...(Array.isArray(lots) ?lots : [])].sort((a, b) => displayOrderOf(a) - displayOrderOf(b))); 21326 } 21327 openCabanaSalesOrder(lotsOverride = null) { 21328 this.openCabanaSalesOrderPage(this.state.currentCabana, lotsOverride, true); 21329 }
21330 openCabanaSalesOrderPage(item, lotsOverride = null, syncHash = true) { 21331 if (!this.canManageUsers() || !item) return; 21332 item = this.normalizeCabanaAuctionRecord(item); 21333 const auctionId = this.cabanaRemateId(item); 21334 const applyLots = (sourceLots) => { 21335 const lots = this.sortedCabanaSalesOrder(sourceLots); 21336 this.setState({ 21337 route: 'cabanaSalesOrder', currentCabana: item, cabanaLots: Array.isArray(sourceLots) ?sourceLots : [], 21338 cabanaLotsLoading: false, cabanaSalesOrderOpen: false, cabanaSalesOrderGrouped: false, 21339 cabanaSalesOrderLots: lots, cabanaSalesOrderSaving: false, cabanaSalesOrderError: '', cabanaSalesOrderSaved: false, 21340 }); 21341 window.scrollTo(0, 0); 21342 }; 21343 if (syncHash && auctionId) this.syncCabanaLocation(item, 'orden-ventas'); 21344 if (Array.isArray(lotsOverride)) { 21345 applyLots(lotsOverride); 21346 return; 21347 } 21348 const currentId = this.cabanaRemateId(this.state.currentCabana); 21349 if (currentId === auctionId && Array.isArray(this.state.cabanaLots)) { 21350 applyLots(this.state.cabanaLots); 21351 return; 21352 } 21353 this.setState({ 21354 route: 'cabanaSalesOrder', currentCabana: item, cabanaLotsLoading: true, 21355 cabanaSalesOrderLots: [], cabanaSalesOrderSaving: false, cabanaSalesOrderError: '', cabanaSalesOrderSaved: false, 21356 }); 21357 window.scrollTo(0, 0); 21358 this.fetchCabanaLots(item).then(applyLots).catch((error) => { 21359 this.setState({ cabanaLotsLoading: false, cabanaSalesOrderError: error?.message || 'No se pudieron cargar los lotes.' }); 21360 }); 21361 } 21362 closeCabanaSalesOrder() { 21363 if (this.state.cabanaSalesOrderSaving) return; 21364 this.destroyCabanaSalesOrderSorting(); 21365 const id = this.cabanaRemateId(this.state.currentCabana); 21366 this.setState({ route: 'cabanaDetail', cabanaSalesOrderOpen: false, cabanaSalesOrderError: '', cabanaSalesOrderSaved: false }); 21367 if (id) this.syncCabanaLocation(this.state.currentCabana); 21368 window.scrollTo(0, 0); 21369 } 21370 cabanaCommercialStorageKey(remate = this.state.currentCabana) { 21371 return 'rosgan:cabanaCommercial:' + (this.cabanaRemateId(remate) || 'lacassina'); 21372 } 21373 loadCabanaCommercialDrafts(remate = this.state.currentCabana) { 21374 try { 21375 const data = JSON.parse(localStorage.getItem(this.cabanaCommercialStorageKey(remate)) || '{}'); 21376 return data && typeof data === 'object' ?data : {}; 21377 } catch { 21378 return {}; 21379 } 21380 } 21381 saveCabanaCommercialDrafts(remate = this.state.currentCabana, drafts = this.state.cabanaCommercialDrafts) { 21382 try { 21383 localStorage.setItem(this.cabanaCommercialStorageKey(remate), JSON.stringify(drafts || {})); 21384 return true; 21385 } catch { 21386 return false; 21387 } 21388 } 21389 cabanaCommercialRowKey(lot) { 21390 const lotCert = String(lot?.lotCert || lot?.groupId || this.cabanaLotId(lot) || '').trim(); 21391 const itemId = String(lot?.itemId || '').trim(); 21392 return [lotCert, itemId].filter(Boolean).join(':') || [this.cabanaLotNumber(lot), this.cabanaAnalyticsRpId(lot)].filter(Boolean).join('-') || String(lot?.id || ''); 21393 } 21394 normalizeCabanaCommercialRecord(commercial) { 21395 if (!commercial || typeof commercial !== 'object') return {}; 21396 const statusMap = { sold: 'vendido', pending: 'pendiente', unsold: 'no-vendido', not_sold: 'no-vendido', lifted: 'levantado' }; 21397 const rawStatus = String(commercial.status || commercial.estado || 'pendiente').trim().toLowerCase().replace(/_/g, '-'); 21398 const rawMode = String(commercial.priceMode || commercial.priceType || commercial.tipoPrecio || commercial.price_type || '').toLowerCase(); 21399 const first = (...values) => values.find((value) => value !== undefined && value !== null && value !== ''); 21400 return { 21401 status: statusMap[rawStatus] || rawStatus || 'pendiente', 21402 priceMode: /kilo|kg/.test(rawMode) ?'kilo' : 'cabeza', 21403 precioKilo: commercial.divisions?.length?String(Math.round(Number(first(commercial.pricePerKilo,commercial.precioKilo,0)))):String(first(commercial.pricePerKilo, commercial.pricePerKg, commercial.precioKilo, commercial.precio_kilo, '') || ''), 21404 precioCabeza: commercial.divisions?.length?String(Math.round(Number(first(commercial.pricePerHead,commercial.precioCabeza,0)))):String(first(commercial.pricePerHead, commercial.precioCabeza, commercial.precio_cabeza, '') || ''), 21405 faenaEnabled: !!first(commercial.slaughter, commercial.isSlaughter, commercial.faena, commercial.faenaEnabled, false), 21406 faenaPercent: String(first(commercial.slaughterPercentage, commercial.faenaPercentage, commercial.faenaPercent, commercial.porcentajeFaena, '') || ''), 21407 cantidad: String(first(commercial.quantity, commercial.cantidad, '') || ''), 21408 pesoProm: String(first(commercial.averageWeight, commercial.pesoPromedio, commercial.pesoProm, '') || ''), 21409 plazo: String(first(commercial.paymentTerm, commercial.term, commercial.plazo, '') || ''), 21410 comprador: String(first(commercial.buyer, commercial.comprador, '') || ''), 21411 divisions: Array.isArray(commercial.divisions)?commercial.divisions:[], 21412 updatedAt: first(commercial.updatedAt, commercial.updated_at, new Date().toISOString()), 21413 }; 21414 } 21415 cabanaCommercialPayload(lot, draft = {}) { 21416 const digits = (value) => String(value ?? '').replace(/\D/g, ''); 21417 const numberOrNull = (value) => { const clean = digits(value); return clean ?Number(clean) : null; }; 21418 const numericTextOrNull = (value) => digits(value) || null; 21419 return { 21420 lotCert: String(lot?.lotCert || lot?.groupId || this.cabanaLotId(lot) || ''), 21421 itemId: String(lot?.itemId || lot?.id || ''), 21422 status: draft.status || 'pendiente', 21423 priceMode: draft.priceMode === 'kilo' ?'kilo' : 'cabeza', 21424 pricePerKilo: numberOrNull(draft.precioKilo), 21425 pricePerHead: numberOrNull(draft.precioCabeza), 21426 slaughter: !!draft.faenaEnabled, 21427 slaughterPercentage: numberOrNull(draft.faenaPercent), 21428 quantity: numericTextOrNull(draft.cantidad), 21429 averageWeight: numberOrNull(draft.pesoProm), 21430 paymentTerm: String(draft.plazo || '').trim() || null, 21431 buyer: String(draft.comprador || '').trim() || null, 21432 divisions: draft.divisions?.length?window.RosganSaleDivisions.normalize(draft.divisions,draft.cantidad):[], 21433 }; 21434 } 21435 async refreshCabanaTvCommercial() { 21436 if (this._cabanaTvCommercialFetching) return; 21437 const item = this.state.currentCabana; 21438 const auctionId = String(this.cabanaRemateId(item) || ''); 21439 if (!auctionId) return; 21440 this._cabanaTvCommercialFetching = true; 21441 try { 21442 const existing = this.state.cabanaLots || []; 21443 const selectedKey = this.state.cabanaTvLotKey || (existing.length ? this.cabanaLotGroupKey(existing[0], 0) : ''); 21444 const selected = existing.filter((lot, index) => this.cabanaLotGroupKey(lot, index) === selectedKey); 21445 if (!selected.length || this._cabanaTvCatalogAuction !== auctionId || Date.now() - (this._cabanaTvCatalogAt || 0) >= 30000) { 21446 await this.fetchCabanaCommercial(item, { silent: true, tvRefresh: true }); 21447 this._cabanaTvCatalogAuction = auctionId; 21448 this._cabanaTvCatalogAt = Date.now(); 21449 return; 21450 } 21451 const certs = [...new Set(selected.map(lot => String(lot.lotCert || lot.groupId || lot.id)))]; 21452 const groups = await Promise.all(certs.map(cert => this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(cert), { cache: 'no-store' }))); 21453 if (String(this.cabanaRemateId(this.state.currentCabana)) !== auctionId || !['cabanaTvLower', 'cabanaTvConfig'].includes(this.state.route)) return; 21454 const updated = this.normalizeCabanaAuctionLots(groups, item); 21455 const lots = this.cabanaSortLotsByCategoryOrder([ 21456 ...(this.state.cabanaLots || []).filter(lot => !certs.includes(String(lot.lotCert || lot.groupId || lot.id))), 21457 ...updated, 21458 ]); 21459 const drafts = { ...(this.state.cabanaCommercialDrafts || {}) }; 21460 existing.filter(lot => certs.includes(String(lot.lotCert || lot.groupId || lot.id))).forEac
21460h(lot => delete drafts[this.cabanaCommercialRowKey(lot)]); 21461 updated.forEach(lot => { 21462 if (lot.commercial) drafts[this.cabanaCommercialRowKey(lot)] = this.normalizeCabanaCommercialRecord(lot.commercial); 21463 }); 21464 this._lastPriceBoardDraftsRaw = JSON.stringify(drafts); 21465 this.saveCabanaCommercialDrafts(item, drafts); 21466 this.setState({ cabanaLots: lots, cabanaCommercialDrafts: drafts }); 21467 } catch { 21468 // A removed group or temporary failure requires a fresh catalog next time. 21469 this._cabanaTvCatalogAt = 0; 21470 } finally { 21471 this._cabanaTvCommercialFetching = false; 21472 } 21473 } 21474 canEditCabanaCommercialTerms(item = this.state.currentCabana) { 21475 const s=this.state,id=this.cabanaRemateId(item); 21476 return !!s.user?.uid && this.apiRoleIds(s.apiUser).some(r=>['admin','sysadmin','consignatario','cabanero'].includes(r)) 21477 && s.cabanaPrebidSummaryAccess?.uid===s.user.uid && s.cabanaPrebidSummaryAccess?.id===id; 21478 } 21479 async loadCabanaCommercialTerms(item) { 21480 const id=this.cabanaRemateId(item); if(!id)return; 21481 try { 21482 const data=await this.cabanaApiFetch('/api/cabana-commercial-terms/'+encodeURIComponent(id),{cache:'no-store'}); 21483 if(this.cabanaRemateId(this.state.currentCabana)===id)this.setState({cabanaCommercialTerms:{id,...data}}); 21484 } catch { if(this.cabanaRemateId(this.state.currentCabana)===id)this.setState({cabanaCommercialTerms:{id,error:true}}); } 21485 } 21486 async openCabanaCommercialTerms() { 21487 const item=this.state.currentCabana,id=this.cabanaRemateId(item); 21488 if(!id)return; 21489 await this.loadCabanaCommercialTerms(item); 21490 if(this.cabanaRemateId(this.state.currentCabana)!==id)return; 21491 const data=this.state.cabanaCommercialTerms; 21492 if(data?.error){this.showToast('No se pudieron cargar las condiciones comerciales. Volvé a intentarlo.');return;} 21493 window.CabanaCommercialTerms.open({imageUrl:data?.imageUrl||'',canEdit:this.canEditCabanaCommercialTerms(item), 21494 title:item.nombre_remate||item.titulo||item.title||'Remate de cabaña', 21495 save:input=>this.cabanaApiFetch('/api/cabana-commercial-terms/'+encodeURIComponent(id),{method:'PUT',body:JSON.stringify(input)}), 21496 onSaved:result=>{if(this.cabanaRemateId(this.state.currentCabana)===id)this.setState({cabanaCommercialTerms:{id,...result}});} 21497 }); 21498 } 21499 async loadCabanaInstallments(item) { 21500 const id=this.cabanaRemateId(item); 21501 const config=await this.cabanaApiFetch('/api/cabana-installments/'+encodeURIComponent(id),{cache:'no-store'}); 21502 item.installmentConfig=config; 21503 if(this.cabanaRemateId(this.state.currentCabana)===id)this.setState({currentCabana:{...this.state.currentCabana,installmentConfig:config}}); 21504 return config; 21505 } 21506 async editCabanaInstallments(lot=null) { 21507 if(this._cabanaInstallmentBusy)return; 21508 const remate=this.state.currentCabana,id=this.cabanaRemateId(remate); 21509 const config=remate.installmentConfig||{},effective=window.CabanaInstallments.resolve(config,lot); 21510 const result=await window.CabanaPrebid.editDialog({title:lot?'Cuotas de este lote':'Preoferta por cuota', 21511 description:lot?'Se aplica a todos los animales de este lote. Podés volver a la cantidad general del remate.':'El monto ingresado será una cuota. El total se calcula multiplicándolo por la cantidad de cuotas. Las excepciones por lote se conservan. Se recalculan también los importes ya cargados.', 21512 fields:lot?[{key:'count',label:'Cantidad de cuotas',value:effective.count,min:1}]:[{key:'enabled',label:'Modalidad',value:config.enabled?'yes':'no',options:[{value:'no',label:'Importe total (sin multiplicar)'},{value:'yes',label:'Preofertar por cuota'}]},{key:'count',label:'Cuotas para todo el remate',value:config.count||12,min:1}],resetLabel:lot?'Usar configuración del remate':null}); 21513 if(!result)return; 21514 if(!result.reset&&!window.CabanaInstallments.count(result.count)){this.showToast('Ingresá entre 1 y 120 cuotas enteras.');return;} 21515 this._cabanaInstallmentBusy=true; 21516 try{ 21517 const patch=lot?{lotCert:this.cabanaPrebidCert(lot),override:result.reset?null:Number(result.count)}:{enabled:result.enabled==='yes',count:Number(result.count)}; 21518 const saved=await this.cabanaApiFetch('/api/cabana-installments/'+encodeURIComponent(id),{method:'PATCH',body:JSON.stringify(patch)}); 21519 if(this.cabanaRemateId(this.state.currentCabana)===id)this.setState({currentCabana:{...this.state.currentCabana,installmentConfig:saved}}); 21520 this.showToast('Configuración de cuotas guardada.'); 21521 }catch(e){this.showToast(e.message||'No se pudo guardar.');}finally{this._cabanaInstallmentBusy=false;} 21522 } 21523 async fetchCabanaCommercial(item = this.state.currentCabana, options = {}) { 21524 const auctionId = this.cabanaRemateId(item); 21525 if (!auctionId) return []; 21526 if (!options.silent) this.setState({ cabanaCommercialLoading: true }); 21527 try { 21528 await this.loadCabanaInstallments(item); 21529 this.fetchCabanaCatalogPrices(item); 21530 const data = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/', { cache: 'no-store' }); 21531 if (options.tvRefresh && (String(this.cabanaRemateId(this.state.currentCabana)) !== String(auctionId) || !['cabanaTvLower', 'cabanaTvConfig'].includes(this.state.route))) return []; 21532 const groups = this.listFrom(data, ['data', 'items', 'lots', 'lotes', 'groups']); 21533 const normalizedLots = this.normalizeCabanaAuctionLots(groups, item); 21534 const lots = normalizedLots; 21535 const remoteDrafts = {};
21536 lots.forEach((lot) => { 21537 if (!lot?.commercial) return; 21538 remoteDrafts[this.cabanaCommercialRowKey(lot)] = this.normalizeCabanaCommercialRecord(lot.commercial); 21539 }); 21540 const raw = JSON.stringify(remoteDrafts); 21541 const changed = raw !== JSON.stringify(this.state.cabanaCommercialDrafts || {}); 21542 this._lastPriceBoardDraftsRaw = raw; 21543 this.saveCabanaCommercialDrafts(item, remoteDrafts); 21544 this.setState({ cabanaLots: lots, currentCabana: item, cabanaLotsLoading: false, cabanaCommercialLoading: false, cabanaCommercialDrafts: remoteDrafts, ...(changed ?{ cabanaPriceBoardPulse: Date.now() } : {}) }); 21545 return lots; 21546 } catch (error) { 21547 this.setState({ cabanaCommercialLoading: false, cabanaLotsLoading: false }); 21548 if (!options.silent) this.showToast(error?.message || 'No se pudieron cargar los precios guardados.'); 21549 return this.state.cabanaLots || []; 21550 } 21551 } 21552 openCabanaCommercial(item) { 21553 item = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana); 21554 if (!item) return; 21555 if (!this.canManageCabanaPrivatePages()) return this.denyCabanaPrivatePage(item); 21556 if (this.state.publicAdvanceMode === 'lacassina' && window.location.pathname !== '/lacassina/comercial') { 21557 this.writeAppHistory('/lacassina/comercial'); 21558 window.__ROSGAN_PUBLIC_PATH__ = '/lacassina/comercial'; 21559 window.__ROSGAN_PUBLIC_ROUTE__ = 'lacassina-commercial'; 21560 document.documentElement.dataset.publicRoute = 'lacassina-commercial'; 21561 } else if (this.state.publicAdvanceMode !== 'lacassina') this.syncCabanaLocation(item, 'comercial'); 21562 const hasLots = Array.isArray(this.state.cabanaLots); 21563 this.setState({ 21564 route: 'cabanaCommercial', 21565 currentCabana: item, 21566 currentCabanaLot: null, 21567 currentCabanaLotVariants: [], 21568 cabanaLotsLoading: !hasLots, 21569 cabanaCommercialDrafts: this.loadCabanaCommercialDrafts(item), 21570 cabanaCommercialLoading: true, 21571 cabanaCommercialView: 'carga', 21572 cabanaCommercialOpenRow: '', 21573 cabanaPublicDeepLoading: false, 21574 }); 21575 window.scrollTo(0, 0); 21576 this.fetchCabanaCommercial(item); 21577 this.ensureCabanaBrand(item); 21578 } 21579 openCabanaPriceBoard(item) { 21580 item = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana); 21581 if (!item) return; 21582 if (!this.canManageCabanaPrivatePages()) return this.denyCabanaPrivatePage(item); 21583 if (this.state.publicAdvanceMode === 'lacassina' && window.location.pathname !== '/lacassina/precios') { 21584 this.writeAppHistory('/lacassina/precios'); 21585 window.__ROSGAN_PUBLIC_PATH__ = '/lacassina/precios'; 21586 window.__ROSGAN_PUBLIC_ROUTE__ = 'lacassina-prices'; 21587 document.documentElement.dataset.publicRoute = 'lacassina-prices'; 21588 } else if (this.state.publicAdvanceMode !== 'lacassina') this.syncCabanaLocation(item, 'precios'); 21589 const hasLots = Array.isArray(this.state.cabanaLots); 21590 const drafts = this.loadCabanaCommercialDrafts(item); 21591 this._lastPriceBoardDraftsRaw = JSON.stringify(drafts || {}); 21592 this.setState({ 21593 route: 'cabanaPriceBoard', 21594 currentCabana: item, 21595 currentCabanaLot: null, 21596 currentCabanaLotVariants: [], 21597 cabanaLotsLoading: !hasLots, 21598 cabanaCommercialDrafts: drafts, 21599 cabanaPriceBoardPulse: Date.now(), 21600 cabanaPublicDeepLoading: false, 21601 }); 21602 window.scrollTo(0, 0); 21603 this.fetchCabanaCommercial(item, { silent: true }); 21604 this.ensureCabanaBrand(item); 21605 } 21606 openCabanaTvLower(item) { 21607 item = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana); 21608 if (!item) return; 21609 if (!this.canManageCabanaPrivatePages()) return this.denyCabanaPrivatePage(item); 21610 if (this.state.publicAdvanceMode === 'lacassina' && window.location.pathname !== '/lacassina/tv') { 21611 this.writeAppHistory('/lacassina/tv'); 21612 window.__ROSGAN_PUBLIC_PATH__ = '/lacassina/tv'; 21613 window.__ROSGAN_PUBLIC_ROUTE__ = 'cabana-tv'; 21614 document.documentElement.dataset.publicRoute = 'cabana-tv'; 21615 } else if (this.state.publicAdvanceMode !== 'lacassina') this.syncCabanaLocation(item, 'tv'); 21616 const hasLots = Array.isArray(this.state.cabanaLots); 21617 const storedKey = this.cabanaTvStorageKey(); 21618 const selectedKey = storedKey ?(localStorage.getItem(storedKey) || '') : ''; 21619 this.setState({ 21620 route: 'cabanaTvLower', 21621 currentCabana: item, 21622 currentCabanaLot: null, 21623 currentCabanaLotVariants: [], 21624 cabanaLotsLoading: !hasLots, 21625 cabanaPublicDeepLoading: false, 21626 cabanaTvLotKey: selectedKey, 21627 cabanaTvConfig: this.loadCabanaTvConfig(), 21628 cabanaTvPulse: Date.now(), 21629 cabanaTvCycleStartedAt: Date.now(), 21630 }); 21631 window.scrollTo(0, 0); 21632 if (!hasLots) this.fetchCabanaLots(item); 21633 this.ensureCabanaBrand(item); 21634 this.fetchCabanaCommercial(item, { silent: true }); 21635 this.fetchCabanaTvRemoteConfig({ silent: true, force: true }); 21636 this.fetchCabanaTvWeather(item, { force: true }); 21637 } 21638 openCabanaTvConfig(item) { 21639 item = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana); 21640 if (!item) return; 21641 if (!this.canManageCabanaPrivatePages()) return this.denyCabanaPrivatePage(item); 21642 this.destroyCabanaTvVideoMount(); 21643 if (this.state.publicAdvanceMode === 'lacassina' && window.location.pathname !== '/lacassina/tv/config') { 21644 this.writeAppHistory('/lacassina/tv/config'); 21645 window.__ROSGAN_PUBLIC_PATH__ = '/lacassina/tv/config'; 21646 window.__ROSGAN_PUBLIC_ROUTE__ = 'cabana-tv-config'; 21647 document.documentElement.dataset.publicRoute = 'cabana-tv-config'; 21648 } else if (this.state.publicAdvanceMode !== 'lacassina') this.syncCabanaLocation(item, 'tv/config'); 21649 const hasLots = Array.isArray(this.state.cabanaLots); 21650 let selectedKey = ''; 21651 try { selectedKey = localStorage.getItem(this.cabanaTvStorageKey()) || ''; } catch (error) {} 21652 this.setState({ 21653 route: 'cabanaTvConfig', 21654 currentCabana: item, 21655 currentCabanaLot: null, 21656 currentCabanaLotVariants: [], 21657 cabanaLotsLoading: !hasLots, 21658 cabanaPublicDeepLoading: false, 21659 cabanaTvLotKey: selectedKey, 21660 cabanaTvConfig: this.loadCabanaTvConfig(), 21661 cabanaTvPulse: Date.now(), 21662 }); 21663 window.scrollTo(0, 0); 21664 if (!hasLots) this.fetchCabanaLots(item); 21665 this.ensureCabanaBrand(item); 21666 this.fetchCabanaCommercial(item, { silent: true }); 21667 this.fetchCabanaTvRemoteConfig({ silent: true, force: true }); 21668 } 21669 cabanaTvStorageKey() { 21670 const auctionId = this.cabanaRemateId(this.state.currentCabana) || 'lacassina'; 21671 return 'rosgan:tvLower:' + auctionId + ':selectedLot'; 21672 } 21673 cabanaTvConfigStorageKey() { 21674 const auctionId = this.cabanaRemateId(this.state.currentCabana) || 'lacassina'; 21675 return 'rosgan:tvLower:' + auctionId + ':config'; 21676 } 21677 cabanaTvDefaultFields() { 21678 return ['rp']; 21679 } 21680 defaultCabanaTvConfig() { 21681 return { visible: true, lowerThirdVisible: true, saleColumnVisible: true, contactsVisible: false, contacts: '', waitingVisible: false, waitingRestore: {}, endVisible: false, endRestore: {}, fields: this.cabanaTvDefaultFields(), fieldsByCategory: {}, depsEnabled: false, logosVisible: true, weatherVisible: true, clockVisible: true, kickerVisible: true, videoVisible: false, videoMode: 'single', videoRpKey: '', videoPlaying: true, videoRestartToken: 0 }; 21682 } 21683 normalizeCabanaTvConfig(config, allowedFields = null, categoryKey = '') { 21684 const defaults = this.defaultCabanaTvConfig(); 21685 const allowedList = Array.isArray(allowedFields) && allowedFields.length ?allowedFields : defaults.fields; 21686 const allowed = new Set(allowedList); 21687 const normalizeFields = (source, fallback = allowedList) => { 21688 const list = Array.isArray(source) ?source : fallback; 21689 const fields = list.filter((field) => allowed.has(field)); 21690 return fields.length ?fields : [allowedList[0] || 'rp']; 21691 }; 21692 const fields = normalizeFields(config?.fields, allowedList); 21693 const fieldsByCategory = {}; 21694 if (config?.fieldsByCategory && typeof config.fieldsByCategory === 'object') {
21695 Object.entries(config.fieldsByCategory).forEach(([key, value]) => { 21696 if (!key || !Array.isArray(value)) return; 21697 fieldsByCategory[key] = value.filter(Boolean); 21698 }); 21699 } 21700 if (categoryKey) fieldsByCategory[categoryKey] = normalizeFields(fieldsByCategory[categoryKey] || fields, allowedList); 21701 const waitingElementKeys = ['lowerThirdVisible', 'saleColumnVisible', 'contactsVisible', 'logosVisible', 'weatherVisible', 'clockVisible', 'kickerVisible', 'videoVisible']; 21702 const waitingRestore = {}; 21703 if (config?.waitingRestore && typeof config.waitingRestore === 'object') { 21704 waitingElementKeys.forEach((key) => { 21705 if (typeof config.waitingRestore[key] === 'boolean') waitingRestore[key] = config.waitingRestore[key]; 21706 }); 21707 } 21708 const endRestore = {}; 21709 if (config?.endRestore && typeof config.endRestore === 'object') { 21710 waitingElementKeys.forEach((key) => { 21711 if (typeof config.endRestore[key] === 'boolean') endRestore[key] = config.endRestore[key]; 21712 }); 21713 } 21714 const endVisible = config?.endVisible === true; 21715 const waitingVisible = !endVisible && config?.waitingVisible === true; 21716 return { 21717 visible: config?.visible !== false, 21718 lowerThirdVisible: config?.lowerThirdVisible !== false, 21719 saleColumnVisible: config?.saleColumnVisible !== false, 21720 contactsVisible: config?.contactsVisible === true, 21721 contacts: String(config?.contacts || '').slice(0, 2000), 21722 waitingVisible, 21723 waitingRestore, 21724 endVisible, 21725 endRestore, 21726 fields, 21727 fieldsByCategory, 21728 depsEnabled: config?.depsEnabled === true, 21729 logosVisible: config?.logosVisible !== false, 21730 weatherVisible: config?.weatherVisible !== false, 21731 clockVisible: config?.clockVisible !== false, 21732 kickerVisible: config?.kickerVisible !== false, 21733 videoVisible: config?.videoVisible === true, 21734 videoMode: ['single', 'grid'].includes(config?.videoMode) ?config.videoMode : 'single', 21735 videoRpKey: String(config?.videoRpKey || '').trim(), 21736 videoPlaying: config?.videoPlaying !== false, 21737 videoRestartToken: Number.isFinite(Number(config?.videoRestartToken)) ?Number(config.videoRestartToken) : 0, 21738 }; 21739 } 21740 cabanaTvFieldsForCategory(config, categoryKey, allowedFields = null) { 21741 const normalized = this.normalizeCabanaTvConfig(config, allowedFields, categoryKey); 21742 return normalized.fieldsByCategory?.[categoryKey] || normalized.fields; 21743 } 21744 loadCabanaTvConfig() { 21745 try { 21746 const raw = localStorage.getItem(this.cabanaTvConfigStorageKey()); 21747 if (raw) return this.normalizeCabanaTvConfig(JSON.parse(raw)); 21748 } catch (error) {} 21749 return this.defaultCabanaTvConfig(); 21750 } 21751 saveCabanaTvConfig(patch = {}) { 21752 const allowed = this._cabanaTvFieldKeys?.length ?this._cabanaTvFieldKeys : null; 21753 const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), allowed); 21754 const next = this.normalizeCabanaTvConfig({ ...current, ...patch }, allowed); 21755 this._cabanaTvLocalConfigUpdatedAt = Date.now(); 21756 try { localStorage.setItem(this.cabanaTvConfigStorageKey(), JSON.stringify(next)); } catch (error) {} 21757 this.setState((previous) =>
21757 ({ cabanaTvConfig: next, cabanaTvConfigPulse: Number(previous.cabanaTvConfigPulse || 0) + 1 })); 21758 try { window.dispatchEvent(new CustomEvent('rosgan:tv-config')); } catch (error) {} 21759 this.queueCabanaTvRemoteSave({ config: next }); 21760 } 21761 toggleCabanaTvVisible() { 21762 const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys); 21763 this.saveCabanaTvConfig({ visible: !current.visible }); 21764 } 21765 toggleCabanaTvDeps() { 21766 const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys); 21767 this.saveCabanaTvConfig({ depsEnabled: !current.depsEnabled }); 21768 } 21769 toggleCabanaTvElement(key) { 21770 const cleanKey = String(key || ''); 21771 const waitingElementKeys = ['lowerThirdVisible', 'saleColumnVisible', 'contactsVisible', 'logosVisible', 'weatherVisible', 'clockVisible', 'kickerVisible', 'videoVisible']; 21772 const plateKeys = ['waitingVisible', 'endVisible']; 21773 if (![...waitingElementKeys, ...plateKeys].includes(cleanKey)) return; 21774 const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys); 21775 if (plateKeys.includes(cleanKey)) { 21776 const activePlateKey = current.endVisible === true ?'endVisible' : (current.waitingVisible === true ?'waitingVisible' : ''); 21777 const restoreKey = cleanKey === 'endVisible' ?'endRestore' : 'waitingRestore'; 21778 const memoryKey = cleanKey === 'endVisible' ?'_cabanaTvEndRestore' : '_cabanaTvWaitingRestore'; 21779 if (current[cleanKey] !== true) { 21780 const activeRestoreKey = activePlateKey === 'endVisible' ?'endRestore' : 'waitingRestore'; 21781 const activeMemoryKey = activePlateKey === 'endVisible' ?'_cabanaTvEndRestore' : '_cabanaTvWaitingRestore'; 21782 const activeRestore = activePlateKey ?(Object.keys(current[activeRestoreKey] || {}).length ?current[activeRestoreKey] : (this[activeMemoryKey] || {})) : {}; 21783 const waitingRestore = activePlateKey 21784 ?Object.fromEntries(waitingElementKeys.map((elementKey) => [elementKey, typeof activeRestore[elementKey] === 'boolean' ?activeRestore[elementKey] : this.defaultCabanaTvConfig()[elementKey]])) 21785 : Object.fromEntries(waitingElementKeys.map((elementKey) => [elementKey, current[elementKey] !== false])); 21786 const disabledElements = Object.fromEntries(waitingElementKeys.map((elementKey) => [elementKey, false])); 21787 this[memoryKey] = waitingRestore; 21788 if (activePlateKey && activeMemoryKey !== memoryKey) this[activeMemoryKey] = null; 21789 this.setState({ cabanaTvVideoIndex: 0 });
21790 this.saveCabanaTvConfig({ 21791 ...disabledElements, 21792 waitingVisible: cleanKey === 'waitingVisible', 21793 waitingRestore: cleanKey === 'waitingVisible' ?waitingRestore : {}, 21794 endVisible: cleanKey === 'endVisible', 21795 endRestore: cleanKey === 'endVisible' ?waitingRestore : {}, 21796 }); 21797 return; 21798 } 21799 const restoreSource = Object.keys(current[restoreKey] || {}).length ?current[restoreKey] : (this[memoryKey] || {}); 21800 const restoredElements = Object.fromEntries(waitingElementKeys.map((elementKey) => [elementKey, typeof restoreSource[elementKey] === 'boolean' ?restoreSource[elementKey] : this.defaultCabanaTvConfig()[elementKey]])); 21801 this._cabanaTvWaitingRestore = null; 21802 this._cabanaTvEndRestore = null; 21803 this.setState({ cabanaTvVideoIndex: 0 }); 21804 this.saveCabanaTvConfig({ ...restoredElements, waitingVisible: false, waitingRestore: {}, endVisible: false, endRestore: {} }); 21805 return; 21806 } 21807 if (current.waitingVisible === true || current.endVisible === true) return; 21808 if (cleanKey === 'videoVisible') { 21809 const becomingVisible = current.videoVisible !== true; 21810 this.setState({ cabanaTvVideoIndex: 0 });
21811 this.saveCabanaTvConfig({ 21812 videoVisible: becomingVisible, 21813 ...(becomingVisible ?{ videoPlaying: true, videoRestartToken: Number(current.videoRestartToken || 0) + 1 } : {}), 21814 }); 21815 return; 21816 } 21817 this.saveCabanaTvConfig({ [cleanKey]: current[cleanKey] === false }); 21818 } 21819 setCabanaTvVideoMode(mode) { 21820 const cleanMode = String(mode || '') === 'grid' ?'grid' : 'single'; 21821 this.setState({ cabanaTvVideoIndex: 0 }); 21822 this.saveCabanaTvConfig({ videoMode: cleanMode }); 21823 } 21824 setCabanaTvVideoRpKey(key) { 21825 const cleanKey = String(key || '').trim(); 21826 if (!cleanKey) return; 21827 const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys); 21828 this.saveCabanaTvConfig({ videoRpKey: cleanKey, videoPlaying: true, videoRestartToken: Number(current.videoRestartToken || 0) + 1 }); 21829 } 21830 stepCabanaTvVideoRp(items = [], direction = 1) { 21831 const ready = Array.isArray(items) ?items.filter((item) => item?.src) : []; 21832 if (!ready.length) return; 21833 const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys); 21834 const currentIndex = Math.max(0, ready.findIndex((item) => item.key === current.videoRpKey)); 21835 const next = ready[((currentIndex + Number(direction || 1)) % ready.length + ready.length) % ready.length]; 21836 if (next) this.setCabanaTvVideoRpKey(next.key); 21837 } 21838 toggleCabanaTvVideoPlayback(fallbackKey = '') { 21839 const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys);
21840 this.saveCabanaTvConfig({ videoRpKey: current.videoRpKey || String(fallbackKey || ''), videoPlaying: !current.videoPlaying }); 21841 } 21842 restartCabanaTvVideo(fallbackKey = '') { 21843 const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys); 21844 this.saveCabanaTvConfig({ videoRpKey: current.videoRpKey || String(fallbackKey || ''), videoPlaying: true, videoRestartToken: Number(current.videoRestartToken || 0) + 1 }); 21845 } 21846 handleCabanaTvVideoEnded(event) { 21847 const readyCount = Number(this._cabanaTvReadyVideoCount || 0); 21848 if (readyCount <= 1 && event?.target) { 21849 try { 21850 const video = event.target; 21851 video.currentTime = 0; 21852 const playResult = video.play?.(); 21853 if (playResult?.catch) playResult.catch(() => {}); 21854 } catch (error) {} 21855 return; 21856 } 21857 this.setState((previous) => ({ cabanaTvVideoIndex: Number(previous.cabanaTvVideoIndex || 0) + 1 })); 21858 } 21859 restartCabanaTvLoopVideo(event) { 21860 const video = event?.target; 21861 if (!video) return; 21862 try { 21863 video.currentTime = 0; 21864 const playResult = video.play?.(); 21865 if (playResult?.catch) playResult.catch(() => {}); 21866 } catch (error) {} 21867 } 21868 advanceCabanaTvPlaylist(event) { 21869 const currentVideo = event?.target; 21870 const currentTile = currentVideo?.closest?.('.tv-video-tile'); 21871 const mount = currentTile?.parentElement; 21872 const tiles = mount ?Array.from(mount.querySelectorAll('.tv-video-tile')) : []; 21873 if (!currentVideo || !currentTile || tiles.length <= 1) { 21874 this.handleCabanaTvVideoEnded(event); 21875 return; 21876 } 21877 if (currentTile.dataset.advancing === 'true') return; 21878 currentTile.dataset.advancing = 'true'; 21879 const currentIndex = Math.max(0, tiles.indexOf(currentTile)); 21880 const nextTile = tiles[(currentIndex + 1) % tiles.length]; 21881 const nextVideo = nextTile?.querySelector('video'); 21882 if (!nextVideo) { 21883 currentTile.dataset.advancing = ''; 21884 this.handleCabanaTvVideoEnded(event); 21885 return; 21886 } 21887 let activated = false; 21888 const activateNext = () => { 21889 if (activated) return; 21890 activated = true; 21891 nextVideo.classList.add('is-ready'); 21892 nextTile.classList.add('is-active'); 21893 currentTile.classList.remove('is-active'); 21894 const transitionMs = mount.classList.contains('is-waiting-background') ?1050 : 300; 21895 window.setTimeout(() => { 21896 try { currentVideo.pause(); } catch (error) {} 21897 try { currentVideo.currentTime = 0; } catch (error) {} 21898 currentTile.dataset.advancing = ''; 21899 }, transitionMs); 21900 }; 21901 nextVideo.addEventListener('playing', activateNext, { once: true }); 21902 try { 21903 nextVideo.currentTime = 0; 21904 const playResult = nextVideo.play?.(); 21905 if (playResult?.catch) playResult.catch(() => {}); 21906 if (nextVideo.readyState >= 3 && !nextVideo.paused) activateNext(); 21907 } catch (error) { 21908 currentTile.dataset.advancing = ''; 21909 try { 21910 currentVideo.currentTime = 0; 21911 currentVideo.play?.(); 21912 } catch (_) {} 21913 } 21914 } 21915 toggleCabanaTvField(fieldKey, categoryKeyOverride = '') { 21916 const key = String(fieldKey || ''); 21917 if (!key) return; 21918 const categoryKey = String(categoryKeyOverride || this._cabanaTvActiveCategoryKey || ''); 21919 const allowed = (categoryKey && this._cabanaTvFieldKeysByCategory?.[categoryKey]?.length) ?this._cabanaTvFieldKeysByCategory[categoryKey] : (this._cabanaTvFieldKeys?.length ?this._cabanaTvFieldKeys : this.cabanaTvDefaultFields()); 21920 const current = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig(), allowed, categoryKey); 21921 const set = new Set(this.cabanaTvFieldsForCategory(current, categoryKey, allowed)); 21922 if (set.has(key) && set.size > 1) set.delete(key); 21923 else set.add(key); 21924 const next = { 21925 ...current, 21926 fieldsByCategory: { 21927 ...(current.fieldsByCategory || {}), 21928 [categoryKey]: allowed.filter((field) => set.has(field)), 21929 }, 21930 }; 21931 this._cabanaTvLocalConfigUpdatedAt = Date.now(); 21932 try { localStorage.setItem(this.cabanaTvConfigStorageKey(), JSON.stringify(next)); } catch (error) {} 21933 this.setState((previous) =>
21933 ({ cabanaTvConfig: next, cabanaTvConfigPulse: Number(previous.cabanaTvConfigPulse || 0) + 1 })); 21934 try { window.dispatchEvent(new CustomEvent('rosgan:tv-config')); } catch (error) {} 21935 this.queueCabanaTvRemoteSave({ config: next }); 21936 } 21937 cabanaTvRemotePath() { 21938 const auctionId = this.cabanaRemateId(this.state.currentCabana) || this.cabanaRemateId(this.state.cabanas?.[0]) || '2'; 21939 return '/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/tv-config'; 21940 } 21941 normalizeCabanaTvRemotePayload(data) { 21942 const root = data?.data || data?.item || data || {}; 21943 return { 21944 selectedLotKey: String(root.selectedLotKey || root.lotKey || root.selected_lot_key || '').trim(), 21945 config: this.normalizeCabanaTvConfig(root.config || root), 21946 updatedAt: root.updatedAt || root.updated_at || '', 21947 }; 21948 } 21949 async fetchCabanaTvRemoteConfig(options = {}) { 21950 if (this.state.route !== 'cabanaTvLower' && this.state.route !== 'cabanaTvConfig') return null; 21951 if (!this.state.currentCabana) return null; 21952 const now = Date.now(); 21953 if (!options.force && this._lastCabanaTvRemoteFetchAt && now - this._lastCabanaTvRemoteFetchAt < 700) return null; 21954 if (!options.force && this._cabanaTvLocalConfigUpdatedAt && now - this._cabanaTvLocalConfigUpdatedAt < 1600) return null; 21955 this._lastCabanaTvRemoteFetchAt = now; 21956 try { 21957 const data = await this.cabanaApiFetch(this.cabanaTvRemotePath(), { cache: 'no-store' }); 21958 const remote = this.normalizeCabanaTvRemotePayload(data); 21959 const signature = JSON.stringify(remote); 21960 if (signature && signature !== this._lastCabanaTvRemoteSignature) { 21961 this._lastCabanaTvRemoteSignature = signature; 21962 try { 21963 if (remote.selectedLotKey) localStorage.setItem(this.cabanaTvStorageKey(), remote.selectedLotKey); 21964 localStorage.setItem(this.cabanaTvConfigStorageKey(), JSON.stringify(remote.config)); 21965 } catch (error) {} 21966 this.setState((previous) => ({ 21967 cabanaTvLotKey: remote.selectedLotKey || previous.cabanaTvLotKey, 21968 cabanaTvConfig: remote.config, 21969 cabanaTvPulse: remote.selectedLotKey && remote.selectedLotKey !== previous.cabanaTvLotKey ?Number(previous.cabanaTvPulse || 0) + 1 : previous.cabanaTvPulse,
21970 cabanaTvConfigPulse: Number(previous.cabanaTvConfigPulse || 0) + 1, 21971 cabanaTvCycleStartedAt: (remote.selectedLotKey && remote.selectedLotKey !== previous.cabanaTvLotKey) ?Date.now() : previous.cabanaTvCycleStartedAt, 21972 cabanaTvVideoIndex: (remote.selectedLotKey && remote.selectedLotKey !== previous.cabanaTvLotKey) ?0 : previous.cabanaTvVideoIndex, 21973 })); 21974 } 21975 return remote; 21976 } catch (error) { 21977 if (!options.silent && error?.status && error.status !== 404) this.showToast(error.message || 'No se pudo sincronizar el zocalo.'); 21978 return null; 21979 } 21980 } 21981 queueCabanaTvRemoteSave(patch = {}) { 21982 clearTimeout(this._cabanaTvRemoteSaveTimer); 21983 this._cabanaTvRemoteSaveTimer = setTimeout(() => this.saveCabanaTvRemoteConfig(patch), 220); 21984 } 21985 async saveCabanaTvRemoteConfig(patch = {}) { 21986 if (!this.state.currentCabana) return; 21987 const rawConfig = patch.config || this.state.cabanaTvConfig || this.loadCabanaTvConfig(); 21988 const payload = { 21989 selectedLotKey: patch.selectedLotKey || this.state.cabanaTvLotKey || '', 21990 config: this.normalizeCabanaTvConfig(rawConfig), 21991 }; 21992 try { 21993 const data = await this.cabanaApiFetch(this.cabanaTvRemotePath(), { method: 'PUT', body: JSON.stringify(payload) }); 21994 this._lastCabanaTvRemoteSignature = JSON.stringify(this.normalizeCabanaTvRemotePayload(data || payload)); 21995 } catch (error) { 21996 if (error?.status && error.status !== 404) this.showToast(error.message || 'No se pudo guardar la config del zocalo.'); 21997 } 21998 } 21999 syncCabanaTvExternalState() { 22000 if (this.state.route !== 'cabanaTvLower' && this.state.route !== 'cabanaTvConfig') return; 22001 let selectedKey = ''; 22002 try { selectedKey = localStorage.getItem(this.cabanaTvStorageKey()) || ''; } catch (error) {} 22003 const nextConfig = this.loadCabanaTvConfig(); 22004 const currentConfig = this.normalizeCabanaTvConfig(this.state.cabanaTvConfig || this.loadCabanaTvConfig()); 22005 const configChanged = JSON.stringify(nextConfig) !== JSON.stringify(currentConfig); 22006 const lotChanged = selectedKey && selectedKey !== this.state.cabanaTvLotKey; 22007 if (lotChanged || configChanged) { 22008 this.setState((previous) => ({ 22009 cabanaTvLotKey: lotChanged ?selectedKey : previous.cabanaTvLotKey, 22010 cabanaTvConfig: configChanged ?nextConfig : previous.cabanaTvConfig, 22011 cabanaTvPulse: lotChanged ?Number(previous.cabanaTvPulse || 0) + 1 : previous.cabanaTvPulse, 22012 cabanaTvConfigPulse: configChanged ?Number(previous.cabanaTvConfigPulse || 0) + 1 : previous.cabanaTvConfigPulse, 22013 cabanaTvCycleStartedAt: (lotChanged || configChanged) ?Date.now() : previous.cabanaTvCycleStartedAt, 22014 cabanaTvVideoIndex: (lotChanged || configChanged) ?0 : previous.cabanaTvVideoIndex, 22015 })); 22016 } 22017 } 22018 setCabanaTvLotKey(key) { 22019 const nextKey = String(key || ''); 22020 if (!nextKey) return; 22021 try { localStorage.setItem(this.cabanaTvStorageKey(), nextKey); } catch (error) {} 22022 this.setState((previous) => ({ cabanaTvLotKey: nextKey, cabanaTvPulse: Number(previous.cabanaTvPulse || 0) + 1, cabanaTvCycleStartedAt: Date.now(), cabanaTvVideoIndex: 0 })); 22023 try { window.dispatchEvent(new CustomEvent('rosgan:tv-config')); } catch (error) {} 22024 this.queueCabanaTvRemoteSave({ selectedLotKey: nextKey }); 22025 } 22026 refreshCabanaPriceBoardDrafts() { 22027 if (this.state.route !== 'cabanaPriceBoard') return; 22028 this.fetchCabanaCommercial(this.state.currentCabana, { silent: true }); 22029 } 22030 consumeCabanaCommercialPendingPrices(lot) { 22031 const key = this.cabanaCommercialRowKey(lot); 22032 if (!key) return {}; 22033 const patch = {}; 22034 ['kilo', 'cabeza'].forEach((priceMode) => { 22035 const timerKey = key + ':' + priceMode; 22036 if (!Object.prototype.hasOwnProperty.call(this._cabanaCommercialPriceDrafts || {}, timerKey)) return; 22037 const raw = String(this._cabanaCommercialPriceDrafts[timerKey] ?? ''); 22038 if (priceMode === 'kilo') patch.precioKilo = raw; 22039 else patch.precioCabeza = raw; 22040 patch.priceMode = priceMode; 22041 if (this._cabanaCommercialPriceInputTimers?.[timerKey]) { 22042 clearTimeout(this._cabanaCommercialPriceInputTimers[timerKey]); 22043 delete this._cabanaCommercialPriceInputTimers[timerKey]; 22044 } 22045 delete this._cabanaCommercialPriceDrafts[timerKey]; 22046 }); 22047 return patch; 22048 } 22049 openCabanaSaleDivisions(lot, quantity, lotNumber) {
22050 const key=this.cabanaCommercialRowKey(lot),sale={...(this.state.cabanaCommercialDrafts[key]||{}),...this.consumeCabanaCommercialPendingPrices(lot)}; 22051 if (!(Number(quantity)>0)) { this.showToast('Indicá primero la cantidad de animales.'); return; } 22052 window.openRosganSaleDivisions({lot:{cantidad:Number(quantity),lotNumber,cert:lot.itemId||lot.id},sale,mode:sale.priceMode||'cabeza',onSave:async(divisions,priceMode)=>{ 22053 const auctionId=this.cabanaRemateId(this.state.currentCabana),lotCert=String(lot.lotCert||lot.groupId||this.cabanaLotId(lot)); 22054 const latest=await this.cabanaApiFetch('/api/cabin-auctions/'+encodeURIComponent(auctionId)+'/lots/'+encodeURIComponent(lotCert),{cache:'no-store'}); 22055 if(!latest?.saleDivisionsSupported)throw new Error('La API necesita actualizarse para guardar ventas divididas. No se modificó la venta.'); 22056 const sold=divisions.filter(p=>p.price>0),soldQuantity=sold.reduce((n,p)=>n+p.quantity,0),average=soldQuantity?sold.reduce((n,p)=>n+p.price*p.quantity,0)/soldQuantity:0; 22057 this.updateCabanaCommercialRow(lot,{divisions,cantidad:String(quantity),priceMode,precioKilo:priceMode==='kilo'?String(Math.round(average)):'',precioCabeza:priceMode==='cabeza'?String(Math.round(average)):'',comprador:'',status:soldQuantity===Number(quantity)?'vendido':'pendiente'}); 22058 }}); 22059 } 22060 updateCabanaCommercialRow(lot, patch = {}) { 22061 const key = this.cabanaCommercialRowKey(lot); 22062 if (!key) return; 22063 const drafts = { ...(this.state.cabanaCommercialDrafts || {}) }; 22064 const pendingPrices = this.consumeCabanaCommercialPendingPrices(lot); 22065 drafts[key] = { ...(drafts[key] || {}), ...pendingPrices, ...patch, updatedAt: new Date().toISOString() }; 22066 this.setState({ cabanaCommercialDrafts: drafts }, () => {
22067 this.saveCabanaCommercialDrafts(this.state.currentCabana, drafts); 22068 this._cabanaCommercialPatchTimers = this._cabanaCommercialPatchTimers || {}; 22069 clearTimeout(this._cabanaCommercialPatchTimers[key]); 22070 this._cabanaCommercialPatchTimers[key] = setTimeout(() => { 22071 delete this._cabanaCommercialPatchTimers[key]; 22072 this.queueCabanaCommercialRemoteSave(lot, drafts[key]); 22073 }, 650); 22074 }); 22075 } 22076 queueCabanaCommercialPriceInput(lot, priceMode, raw) { 22077 const key = this.cabanaCommercialRowKey(lot); 22078 if (!key) return; 22079 const timerKey = key + ':' + priceMode; 22080 this._cabanaCommercialPriceDrafts = this._cabanaCommercialPriceDrafts || {}; 22081 this._cabanaCommercialPriceInputTimers = this._cabanaCommercialPriceInputTimers || {}; 22082 this._cabanaCommercialPriceDrafts[timerKey] = String(raw ?? ''); 22083 clearTimeout(this._cabanaCommercialPriceInputTimers[timerKey]); 22084 this._cabanaCommercialPriceInputTimers[timerKey] = setTimeout(() => { 22085 this.flushCabanaCommercialPriceInput(lot, priceMode); 22086 }, 500); 22087 } 22088 flushCabanaCommercialPriceInput(lot, priceMode, raw, patch = {}) { 22089 const key = this.cabanaCommercialRowKey(lot); 22090 if (!key) return; 22091 const timerKey = key + ':' + priceMode; 22092 const buffered = this._cabanaCommercialPriceDrafts?.[timerKey]; 22093 const nextRaw = raw === undefined ?buffered : String(raw ?? ''); 22094 if (nextRaw === undefined && !Object.keys(patch).length) return; 22095 if (this._cabanaCommercialPriceInputTimers?.[timerKey]) { 22096 clearTimeout(this._cabanaCommercialPriceInputTimers[timerKey]); 22097 delete this._cabanaCommercialPriceInputTimers[timerKey]; 22098 } 22099 if (this._cabanaCommercialPriceDrafts) delete this._cabanaCommercialPriceDrafts[timerKey]; 22100 const nextDraft = { 22101 ...((this.state.cabanaCommercialDrafts || {})[key] || {}), 22102 ...(priceMode === 'kilo' ?{ precioKilo: nextRaw ?? '', priceMode: 'kilo' } : { precioCabeza: nextRaw ?? '', priceMode: 'cabeza' }), 22103 ...patch, 22104 updatedAt: new Date().toISOString(), 22105 }; 22106 const drafts = { ...(this.state.cabanaCommercialDrafts || {}), [key]: nextDraft }; 22107 this.state.cabanaCommercialDrafts = drafts;
22108 this.saveCabanaCommercialDrafts(this.state.currentCabana, drafts); 22109 this.queueCabanaCommercialRemoteSave(lot, nextDraft); 22110 if (Object.keys(patch).length || this.state.currentCabana?.installmentConfig?.enabled) this.setState({ cabanaCommercialDrafts: drafts }); 22111 } 22112 queueCabanaCommercialRemoteSave(lot, draft) { 22113 const key = this.cabanaCommercialRowKey(lot); 22114 if (!key || !draft) return; 22115 this._cabanaCommercialRemoteDrafts = this._cabanaCommercialRemoteDrafts || {}; 22116 this._cabanaCommercialRemoteSaving = this._cabanaCommercialRemoteSaving || {}; 22117 this._cabanaCommercialRemoteDrafts[key] = { lot, draft }; 22118 if (this._cabanaCommercialRemoteSaving[key]) return; 22119 this._cabanaCommercialRemoteSaving[key] = true; 22120 const drain = async () => { 22121 try { 22122 while (this._cabanaCommercialRemoteDrafts[key]) { 22123 const pending = this._cabanaCommercialRemoteDrafts[key]; 22124 delete this._cabanaCommercialRemoteDrafts[key]; 22125 await this.saveCabanaCommercialRow(pending.lot, pending.draft); 22126 } 22127 } finally { 22128 delete this._cabanaCommercialRemoteSaving[key]; 22129 if (this._cabanaCommercialRemoteDrafts[key]) this.queueCabanaCommercialRemoteSave(this._cabanaCommercialRemoteDrafts[key].lot, this._cabanaCommercialRemoteDrafts[key].draft); 22130 } 22131 }; 22132 drain(); 22133 } 22134 async saveCabanaCommercialRow(lot, draftOverride = null) { 22135 const auctionId = this.cabanaRemateId(this.state.currentCabana); 22136 const key = this.cabanaCommercialRowKey(lot); 22137 const draft = draftOverride || (this.state.cabanaCommercialDrafts || {})[key]; 22138 const lotCert = String(lot?.lotCert || lot?.groupId || this.cabanaLotId(lot) || ''); 22139 const itemId = String(lot?.itemId || lot?.id || ''); 22140 if (!auctionId || !lotCert || !itemId || !draft) return; 22141 try { 22142 const payload = this.cabanaCommercialPayload(lot, draft); 22143 delete payload.lotCert; 22144 delete payload.itemId; 22145 await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert) + '/items/' + encodeURIComponent(itemId) + '/commercial', { method: 'PATCH', body: JSON.stringify(payload) }); 22146 } catch (error) { 22147 this.showToast(error?.message || 'No se pudo guardar el cambio.'); 22148 } 22149 } 22150 setCabanaCommercialFilter(filter) { 22151 this.setState({ cabanaCommercialFilter: filter || 'todos' }); 22152 } 22153 toggleCabanaCommercialMore(lot) { 22154 const key = this.cabanaCommercialRowKey(lot); 22155 this.setState({ cabanaCommercialOpenRow: this.state.cabanaCommercialOpenRow === key ?'' : key }); 22156 } 22157 toggleCabanaSalesOrderGrouped() { 22158 const grouped = !this.state.cabanaSalesOrderGrouped; 22159 let lots = [...(this.state.cabanaSalesOrderLots || [])]; 22160 if (grouped) { 22161 const order = []; 22162 const buckets = new Map(); 22163 lots.forEach((lot) => { 22164 const key = this.cabanaSalesCategoryKey(lot); 22165 if (!buckets.has(key)) { buckets.set(key, []); order.push(key); } 22166 buckets.get(key).push(lot); 22167 }); 22168 lots = order.flatMap((key) => buckets.get(key)); 22169 } 22170 this.setState({ cabanaSalesOrderGrouped: grouped, cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(lots), cabanaSalesOrderError: '', cabanaSalesOrderSaved: false }); 22171 } 22172 moveCabanaSalesLot(sourceCert, targetCert) { 22173 const lots = [...(this.state.cabanaSalesOrderLots || [])]; 22174 const sourceIndex = lots.findIndex((lot) => this.cabanaLotId(lot) === String(sourceCert)); 22175 const targetIndex = lots.findIndex((lot) => this.cabanaLotId(lot) === String(targetCert)); 22176 if (sourceIndex < 0 || targetIndex < 0 || sourceIndex === targetIndex) return; 22177 if (this.state.cabanaSalesOrderGrouped && this.cabanaSalesCategoryKey(lots[sourceIndex]) !== this.cabanaSalesCategoryKey(lots[targetIndex])) return; 22178 const [moved] = lots.splice(sourceIndex, 1); 22179 lots.splice(targetIndex, 0, moved); 22180 this.setState({ cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(lots), cabanaSalesOrderError: '' }); 22181 } 22182 moveCabanaSalesCategory(sourceKey, targetKey) { 22183 if (!sourceKey || !targetKey || sourceKey === targetKey) return; 22184 const lots = [...(this.state.cabanaSalesOrderLots || [])]; 22185 const categoryOrder = []; 22186 const buckets = new Map();
22187 lots.forEach((lot) => { 22188 const key = this.cabanaSalesCategoryKey(lot); 22189 if (!buckets.has(key)) { buckets.set(key, []); categoryOrder.push(key); } 22190 buckets.get(key).push(lot); 22191 }); 22192 const sourceIndex = categoryOrder.indexOf(sourceKey); 22193 const targetIndex = categoryOrder.indexOf(targetKey); 22194 if (sourceIndex < 0 || targetIndex < 0) return; 22195 const [moved] = categoryOrder.splice(sourceIndex, 1); 22196 categoryOrder.splice(targetIndex, 0, moved); 22197 this.setState({ cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(categoryOrder.flatMap((key) => buckets.get(key))), cabanaSalesOrderError: '' }); 22198 } 22199 setCabanaSalesLotPosition(cert, requestedPosition) { 22200 const lots = [...(this.state.cabanaSalesOrderLots || [])]; 22201 const sourceIndex = lots.findIndex((lot) => this.cabanaLotId(lot) === String(cert)); 22202 if (sourceIndex < 0) return; 22203 const scopedIndices = this.state.cabanaSalesOrderGrouped 22204 ? lots.map((lot, index) => this.cabanaSalesCategoryKey(lot) === this.cabanaSalesCategoryKey(lots[sourceIndex]) ?index : -1).filter((index) => index >= 0) 22205 : lots.map((_, index) => index); 22206 const currentScopeIndex = scopedIndices.indexOf(sourceIndex); 22207 const targetScopeIndex = Math.max(0, Math.min(scopedIndices.length - 1, Number(requestedPosition) - 1)); 22208 if (currentScopeIndex === targetScopeIndex) return; 22209 const targetIndex = scopedIndices[targetScopeIndex]; 22210 const [moved] = lots.splice(sourceIndex, 1); 22211 lots.splice(targetIndex, 0, moved); 22212 this.setState({ cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(lots), cabanaSalesOrderError: '', cabanaSalesOrderSaved: false }); 22213 } 22214 setCabanaSalesCategoryPosition(key, requestedPosition) { 22215 const lots = [...(this.state.cabanaSalesOrderLots || [])]; 22216 const order = []; 22217 const buckets = new Map(); 22218 lots.forEach((lot) => { 22219 const categoryKey = this.cabanaSalesCategoryKey(lot); 22220 if (!buckets.has(categoryKey)) { buckets.set(categoryKey, []); order.push(categoryKey); } 22221 buckets.get(categoryKey).push(lot); 22222 }); 22223 const sourceIndex = order.indexOf(String(key)); 22224 const targetIndex = Math.max(0, Math.min(order.length - 1, Number(requestedPosition) - 1)); 22225 if (sourceIndex < 0 || sourceIndex === targetIndex) return; 22226 const [moved] = order.splice(sourceIndex, 1); 22227 order.splice(targetIndex, 0, moved); 22228 this.setState({ cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(order.flatMap((categoryKey) => buckets.get(categoryKey))), cabanaSalesOrderError: '', cabanaSalesOrderSaved: false }); 22229 } 22230 destroyCabanaSalesOrderSorting() { 22231 if (this._cabanaSalesOrderBindTimer) clearTimeout(this._cabanaSalesOrderBindTimer); 22232 this._cabanaSalesOrderBindTimer = null; 22233 if (this._cabanaSalesOrderCleanup) { 22234 try { this._cabanaSalesOrderCleanup(); } catch (_) {} 22235 } 22236 this._cabanaSalesOrderCleanup = null; 22237 } 22238 bindCabanaSalesOrderSorting(attempt = 0) { 22239 if (attempt === 0) this.destroyCabanaSalesOrderSorting(); 22240 if (this.state.route !== 'cabanaSalesOrder') return; 22241 requestAnimationFrame(() => { 22242 if (this.state.route !== 'cabanaSalesOrder') return; 22243 const root = document.querySelector('.sales-order-page'); 22244 const hasLots = (this.state.cabanaSalesOrderLots || []).length > 0; 22245 const expectedList = root?.querySelector( 22246 this.state.cabanaSalesOrderGrouped ? '[data-sales-groups]' : '[data-sales-flat]' 22247 ); 22248 if (!root || (hasLots && !expectedList)) { 22249 if (attempt < 30) { 22250 this._cabanaSalesOrderBindTimer = setTimeout(() => { 22251 this._cabanaSalesOrderBindTimer = null; 22252 this.bindCabanaSalesOrderSorting(attempt + 1); 22253 }, 60); 22254 } else { 22255 this.setState({ cabanaSalesOrderError: 'No se pudo activar el ordenamiento. Recargá esta pantalla.' }); 22256 } 22257 return; 22258 } 22259 const certOfNode = (node) => String(node?.querySelector?.('.sales-order-cert')?.value || ''); 22260 const categoryKeyOfNode = (node) => String(node?.querySelector?.('.sales-order-category-key')?.value || ''); 22261 const commitDrop = (currentDrag) => { 22262 if (!currentDrag?.targetNode) return; 22263 const currentLots = [...(this.state.cabanaSalesOrderLots || [])]; 22264 let ordered = currentLots; 22265 if (currentDrag.kind === 'lot') { 22266 const draggedCert = certOfNode(currentDrag.node); 22267 const targetCert = certOfNode(currentDrag.targetNode); 22268 if (!draggedCert || !targetCert || draggedCert === targetCert) return; 22269 const fromIndex = currentLots.findIndex((lot) => String(this.cabanaLotId(lot)) === draggedCert); 22270 if (fromIndex < 0) return; 22271 const [movedLot] = ordered.splice(fromIndex, 1); 22272 const targetIndex = ordered.findIndex((lot) => String(this.cabanaLotId(lot)) === targetCert); 22273 if (targetIndex < 0) return; 22274 ordered.splice(targetIndex + (currentDrag.dropBefore ? 0 : 1), 0, movedLot); 22275 } else { 22276 const draggedKey = categoryKeyOfNode(currentDrag.node); 22277 const targetKey = categoryKeyOfNode(currentDrag.targetNode); 22278 if (!draggedKey || !targetKey || draggedKey === targetKey) return; 22279 const categoryKeys = []; 22280 const groupedLots = new Map();
22281 currentLots.forEach((lot) => { 22282 const key = this.cabanaSalesCategoryKey(lot); 22283 if (!groupedLots.has(key)) { 22284 categoryKeys.push(key); 22285 groupedLots.set(key, []); 22286 } 22287 groupedLots.get(key).push(lot); 22288 }); 22289 const fromIndex = categoryKeys.indexOf(draggedKey); 22290 if (fromIndex < 0) return; 22291 categoryKeys.splice(fromIndex, 1); 22292 const targetIndex = categoryKeys.indexOf(targetKey); 22293 if (targetIndex < 0) return; 22294 categoryKeys.splice(targetIndex + (currentDrag.dropBefore ? 0 : 1), 0, draggedKey); 22295 ordered = categoryKeys.flatMap((key) => groupedLots.get(key) || []); 22296 } 22297 this.setState({ 22298 cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(ordered), 22299 cabanaSalesOrderError: '', 22300 cabanaSalesOrderSaved: false, 22301 }); 22302 }; 22303 let drag = null; 22304 let target = null; 22305 const clearTarget = () => { 22306 target?.classList?.remove('sales-order-native-target'); 22307 target = null; 22308 }; 22309 const clearDrag = () => { 22310 clearTarget(); 22311 if (drag?.node) drag.node.classList.remove('sales-order-native-drag', 'sales-order-drag-source'); 22312 drag?.placeholder?.remove?.(); 22313 drag?.preview?.remove?.(); 22314 document.body.style.removeProperty('user-select'); 22315 document.body.classList.remove('sales-order-is-dragging'); 22316 root.classList.remove('is-sorting'); 22317 drag = null; 22318 }; 22319 const positionPreview = (event) => { 22320 if (!drag?.preview) return; 22321 drag.preview.style.transform = `translate3d(${event.clientX - drag.offsetX}px, ${event.clientY - drag.offsetY}px, 0)`; 22322 }; 22323 const beginPointerDrag = (event) => { 22324 const rect = drag.node.getBoundingClientRect(); 22325 const placeholder = document.createElement('div'); 22326 placeholder.className = 'sales-order-drag-placeholder'; 22327 placeholder.style.height = `${rect.height}px`; 22328 placeholder.style.width = `${rect.width}px`; 22329 drag.node.parentNode?.insertBefore(placeholder, drag.node); 22330 const preview = drag.node.cloneNode(true); 22331 preview.removeAttribute('data-sales-lot'); 22332 preview.removeAttribute('data-sales-category'); 22333 preview.classList.add('sales-order-native-preview'); 22334 preview.style.width = `${rect.width}px`; 22335 preview.style.height = `${rect.height}px`; 22336 document.body.appendChild(preview); 22337 drag.active = true; 22338 drag.placeholder = placeholder; 22339 drag.preview = preview; 22340 drag.offsetX = Math.max(0, Math.min(rect.width, event.clientX - rect.left)); 22341 drag.offsetY = Math.max(0, Math.min(rect.height, event.clientY - rect.top)); 22342 drag.node.classList.add('sales-order-native-drag', 'sales-order-drag-source'); 22343 document.body.style.userSelect = 'none'; 22344 document.body.classList.add('sales-order-is-dragging'); 22345 root.classList.add('is-sorting'); 22346 try { drag.node.setPointerCapture?.(event.pointerId); } catch (_) {} 22347 positionPreview(event); 22348 }; 22349 const resolveDragNode = (eventTarget) => { 22350 const categoryHead = eventTarget.closest?.('.sales-order-category-head'); 22351 if (categoryHead) { 22352 const category = categoryHead.closest('[data-sales-category]'); 22353 if (category) return { node: category, kind: 'category' }; 22354 } 22355 const row = eventTarget.closest?.('[data-sales-lot]'); 22356 if (row) return { node: row, kind: 'lot' }; 22357 return null; 22358 }; 22359 const resolveTarget = (clientX, clientY) => { 22360 if (!drag?.node) return null; 22361 const selector = drag.kind === 'category' ? '[data-sales-category]' : '[data-sales-lot]'; 22362 const sourceCategory = drag.kind === 'lot' && this.state.cabanaSalesOrderGrouped 22363 ? drag.node.closest?.('[data-sales-category]') 22364 : null; 22365 const candidates = [...root.querySelectorAll(selector)].filter((candidate) => { 22366 if (candidate === drag.node) return false; 22367 if (sourceCategory && candidate.closest?.('[data-sales-category]') !== sourceCategory) return false;
22368 return true; 22369 }); 22370 if (!candidates.length) return null; 22371 const direct = candidates.find((candidate) => { 22372 const rect = candidate.getBoundingClientRect(); 22373 return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom; 22374 }); 22375 if (direct) return direct; 22376 return candidates.reduce((nearest, candidate) => { 22377 const rect = candidate.getBoundingClientRect(); 22378 const distance = Math.abs(clientY - (rect.top + rect.height / 2)); 22379 return !nearest || distance < nearest.distance ? { candidate, distance } : nearest; 22380 }, null)?.candidate || null; 22381 }; 22382 const moveAtPointer = (candidate, clientY) => { 22383 if (!candidate || !drag) return; 22384 if (target !== candidate) { 22385 clearTarget(); 22386 target = candidate; 22387 target.classList.add('sales-order-native-target'); 22388 } 22389 const rect = candidate.getBoundingClientRect(); 22390 drag.targetNode = candidate; 22391 drag.dropBefore = clientY < rect.top + (rect.height / 2); 22392 const parent = candidate.parentNode; 22393 if (parent && drag.placeholder) { 22394 parent.insertBefore(drag.placeholder, drag.dropBefore ? candidate : candidate.nextSibling); 22395 } 22396 }; 22397 const onPointerMove = (event) => { 22398 if (!drag || event.pointerId !== drag.pointerId) return; 22399 const distance = Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY); 22400 if (!drag.active && distance < 6) return; 22401 if (!drag.active) beginPointerDrag(event); 22402 event.preventDefault(); 22403 positionPreview(event); 22404 moveAtPointer(resolveTarget(event.clientX, event.clientY), event.clientY); 22405 if (event.clientY < 86) window.scrollBy(0, -16); 22406 else if (event.clientY > window.innerHeight - 86) window.scrollBy(0, 16); 22407 }; 22408 const finishPointerDrag = (event) => { 22409 if (!drag || (event?.pointerId != null && event.pointerId !== drag.pointerId)) return; 22410 const completedDrag = drag.active ? { ...drag } : null; 22411 clearDrag(); 22412 window.removeEventListener('pointermove', onPointerMove); 22413 window.removeEventListener('pointerup', finishPointerDrag); 22414 window.removeEventListener('pointercancel', finishPointerDrag); 22415 if (completedDrag) commitDrop(completedDrag); 22416 }; 22417 const onPointerDown = (event) => { 22418 if (event.pointerType === 'mouse' && event.button !== 0) return; 22419 if (event.pointerType !== 'mouse' && event.isPrimary === false) return; 22420 if (event.target.closest?.('button,a,select,textarea,[contenteditable="true"]')) return; 22421 const resolved = resolveDragNode(event.target); 22422 if (!resolved) return; 22423 event.preventDefault(); 22424 drag = { 22425 ...resolved, 22426 pointerId: event.pointerId, 22427 startX: event.clientX, 22428 startY: event.clientY, 22429 active: false, 22430 }; 22431 window.addEventListener('pointermove', onPointerMove, { passive: false }); 22432 window.addEventListener('pointerup', finishPointerDrag); 22433 window.addEventListener('pointercancel', finishPointerDrag); 22434 }; 22435 root.addEventListener('pointerdown', onPointerDown); 22436 this._cabanaSalesOrderCleanup = () => { 22437 if (this._cabanaSalesOrderBindTimer) clearTimeout(this._cabanaSalesOrderBindTimer); 22438 this._cabanaSalesOrderBindTimer = null; 22439 root.removeEventListener('pointerdown', onPointerDown); 22440 window.removeEventListener('pointermove', onPointerMove); 22441 window.removeEventListener('pointerup', finishPointerDrag); 22442 window.removeEventListener('pointercancel', finishPointerDrag); 22443 clearDrag(); 22444 }; 22445 }); 22446 } 22447 bindCabanaSalesOrderSorting(attempt = 0) { 22448 if (attempt === 0) this.destroyCabanaSalesOrderSorting(); 22449 if (this.state.route !== 'cabanaSalesOrder') return; 22450 requestAnimationFrame(() => { 22451 if (this.state.route !== 'cabanaSalesOrder') return; 22452 const root = document.querySelector('.sales-order-page');
22453 const grouped = !!this.state.cabanaSalesOrderGrouped; 22454 const list = root?.querySelector(grouped ? '[data-sales-groups]' : '[data-sales-flat]'); 22455 const hasLots = (this.state.cabanaSalesOrderLots || []).length > 0; 22456 if (!root || (hasLots && !list) || typeof window.Sortable !== 'function') { 22457 if (attempt < 30) { 22458 this._cabanaSalesOrderBindTimer = setTimeout(() => { 22459 this._cabanaSalesOrderBindTimer = null; 22460 this.bindCabanaSalesOrderSorting(attempt + 1); 22461 }, 60); 22462 } else { 22463 this.setState({ cabanaSalesOrderError: 'No se pudo activar el ordenamiento. Recargá esta pantalla.' }); 22464 } 22465 return; 22466 } 22467 22468 const instances = []; 22469 const certOf = (node) => String(node?.querySelector?.('.sales-order-cert')?.value || ''); 22470 const syncFromDom = () => { 22471 const lots = [...(this.state.cabanaSalesOrderLots || [])]; 22472 const byCert = new Map(lots.map((lot) => [String(this.cabanaLotId(lot)), lot])); 22473 const certs = grouped 22474 ? [...list.querySelectorAll(':scope > [data-sales-category]')].flatMap((category) => 22475 [...category.querySelectorAll(':scope > .sales-order-category-list > [data-sales-lot]')].map(certOf) 22476 ) 22477 : [...list.querySelectorAll(':scope > [data-sales-lot]')].map(certOf); 22478 const seen = new Set(); 22479 const ordered = certs 22480 .map((cert) => { 22481 seen.add(cert); 22482 return byCert.get(cert); 22483 }) 22484 .filter(Boolean); 22485 lots.forEach((lot) => { 22486 if (!seen.has(String(this.cabanaLotId(lot)))) ordered.push(lot); 22487 }); 22488 this.setState({ 22489 cabanaSalesOrderLots: this.normalizeCabanaSalesOrder(ordered), 22490 cabanaSalesOrderError: '', 22491 cabanaSalesOrderSaved: false, 22492 }); 22493 }; 22494 const common = { 22495 animation: 180, 22496 forceFallback: true, 22497 fallbackOnBody: true, 22498 fallbackTolerance: 4, 22499 swapThreshold: 0.5, 22500 invertSwap: true, 22501 invertedSwapThreshold: 0.35, 22502 ghostClass: 'sales-order-sortable-ghost', 22503 chosenClass: 'sales-order-sortable-chosen', 22504 dragClass: 'sales-order-sortable-drag', 22505 onStart: () => document.body.classList.add('sales-order-is-dragging'), 22506 onEnd: () => { 22507 document.body.classList.remove('sales-order-is-dragging'); 22508 syncFromDom(); 22509 }, 22510 }; 22511 22512 if (grouped) { 22513 instances.push(window.Sortable.create(list, { 22514 ...common, 22515 draggable: '[data-sales-category]', 22516 handle: '.sales-order-category-head', 22517 })); 22518 list.querySelectorAll(':scope > [data-sales-category] > .sales-order-category-list').forEach((categoryList) => { 22519 instances.push(window.Sortable.create(categoryList, { 22520 ...common, 22521 draggable: '[data-sales-lot]', 22522 })); 22523 }); 22524 } else if (list) { 22525 instances.push(window.Sortable.create(list, { 22526 ...common, 22527 draggable: '[data-sales-lot]', 22528 })); 22529 } 22530 22531 this._cabanaSalesOrderCleanup = () => { 22532 if (this._cabanaSalesOrderBindTimer) clearTimeout(this._cabanaSalesOrderBindTimer); 22533 this._cabanaSalesOrderBindTimer = null; 22534 instances.forEach((instance) => { 22535 try { instance.destroy(); } catch (_) {} 22536 }); 22537 document.body.classList.remove('sales-order-is-dragging'); 22538 }; 22539 }); 22540 } 22541 async saveCabanaSalesOrder() { 22542 if (!this.canManageUsers() || this.state.cabanaSalesOrderSaving) return; 22543 const auctionId = this.cabanaRemateId(this.state.currentCabana); 22544 const ordered = this.normalizeCabanaSalesOrder(this.state.cabanaSalesOrderLots || []); 22545 const orders = ordered.map((lot, index) => ({ lotCert: this.cabanaLotId(lot), displayOrder: index + 1 })).filter((item) => item.lotCert); 22546 if (!auctionId || !orders.length) { 22547 this.setState({ cabanaSalesOrderError: 'No hay lotes para ordenar.' }); 22548 return; 22549 } 22550 this.setState({ cabanaSalesOrderSaving: true, cabanaSalesOrderError: '', cabanaSalesOrderSaved: false }); 22551 try { 22552 await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/reorder', { 22553 method: 'PUT', 22554 body: JSON.stringify({ categoryId: null, orders }), 22555 }); 22556 this.setState({ cabanaLots: ordered, cabanaSalesOrderLots: ordered, cabanaSalesOrderOpen: false, cabanaSalesOrderSaving: false, cabanaSalesOrderError: '', cabanaSalesOrderSaved: true }); 22557 } catch (error) { 22558 this.setState({ cabanaSalesOrderSaving: false, cabanaSalesOrderError: error?.message || 'No se pudo guardar el orden de ventas.' }); 22559 } 22560 } 22561 openCabana(item, options = {}) { 22562 const requestedPath = window.location.pathname; 22563 item = this.normalizeCabanaAuctionRecord(item); 22564 if (this.cabanaCatalogLocked(item)) { 22565 if (options.syncPath !== false) this.syncCabanaLocation(item); 22566 this.setState({ currentCabana: item, cabanaLots: null, cabanaLotsLoading: false }); 22567 this.loginForCabanaCatalog(); 22568 return Promise.resolve([]); 22569 } 22570 if (options.syncPath !== false) this.syncCabanaLocation(item); 22571 this.setState({ route: 'cabanaDetail', routeLoading: false, currentCabana: item, cabanaLots: null, cabanaLotsLoading: true, cbQuery: '', cbCat: 'todos', currentCabanaLot: null, cabanaAnalyticsSummary: null, cabanaStatsOverview: false, cabanaSalesOrderOpen: false, cabanaSalesOrderLots: [], cabanaSalesOrderError: '', cabanaPrebidPanelAllowed: fal
22571se, cabanaPrebidPanelOpen: false, cabanaPrebidPanelLoading: false, cabanaPrebidPanelError: '', cabanaPrebidPanelData: null, cabanaPrebidPanelQuery: '', cabanaPrebidPanelStatus: 'all', cabanaPrebidPanelLot: '', cabanaPublicDeepLoading: !!options.focusLotCert || (this.state.publicAdvanceMode === 'lacassina' && !!options.focusRp), cabanaBrandPreparing: false }); 22572 window.scrollTo(0, 0); 22573 const openSalesOrderWhenReady = (lots) => { 22574 if (options.syncPath === false && window.location.pathname !== requestedPath) return lots; 22575 if (this.cabanaRemateId(this.state.currentCabana) !== this.cabanaRemateId(item) || this.state.route !== 'cabanaDetail') return lots; 22576 const itemId = this.cabanaRemateId(item); 22577 if (options.openSalesOrder && this.state.route === 'cabanaDetail' && this.cabanaRemateId(this.state.currentCabana) === itemId) { 22578 this.openCabanaSalesOrder(lots); 22579 } 22580 if (options.focusRp && this.state.publicAdvanceMode === 'lacassina') { 22581 this.openCabanaLotByRp(options.focusRp, lots, options.focusLotNumber); 22582 } 22583 if (options.focusLotCert && !this.cabanaCatalogLocked(item)) { 22584 if (!this.openCabanaLotByCert(options.focusLotCert, lots)) { 22585 this.setState({ cabanaPublicDeepLoading: false }); 22586 this.showToast('No se encontró el lote solicitado.'); 22587 } 22588 } 22589 return lots; 22590 }; 22591 const loading = this.fetchCabanaLots(item).then(openSalesOrderWhenReady); 22592 this.checkCabanaPrebidSummary(item); 22593 this.loadCabanaCommercialTerms(item); 22594 if (this.state.user) this.fetchCabanaPrebidPanel(item, { probe: true }); 22595 this.fetchCabanaAnalyticsSummary(item); 22596 this.ensureCabanaBrand(item); 22597 const id = this.cabanaRemateId(item); 22598 if (id && (!item.__detailLoadedAt || Date.now() - item.__detailLoadedAt > 30000)) this.fetchCabanaById(id, { force: true }).then((fresh) => { 22599 if (fresh && this.state.route === 'cabanaDetail' && this.cabanaRemateId(this.state.currentCabana) === id) { 22600 this.setState({ currentCabana: fresh }); 22601 this.fetchCabanaLots(fresh); 22602 this.fetchCabanaAnalyticsSummary(fresh); 22603 this.ensureCabanaBrand(fresh); 22604 if (this.state.user) this.fetchCabanaPrebidPanel(fresh, { probe: true }); 22605 } 22606 }).catch(() => {}); 22607 return loading; 22608 } 22609 openCabanaLotByRp(rp, lots = this.state.cabanaLots, lotNumber = '') { 22610 const targetRp = this.consKey(rp); 22611 const targetLot = this.consKey(lotNumber); 22612 if (!targetRp || !Array.isArray(lots)) return false; 22613 const match = lots.find((lot) => 22614 this.consKey(lot?.rp || lot?.values?.rp || lot?.identifier || lot?.itemId || '') === targetRp && 22615 (!targetLot || this.consKey(this.cabanaLotNumber(lot)) === targetLot) 22616 ) || lots.find((lot) => this.consKey(lot?.rp || lot?.values?.rp || lot?.identifier || lot?.itemId || '') === targetRp); 22617 if (!match) { 22618 if (this.state.publicAdvanceMode === 'lacassina') this.setState({ cabanaPublicDeepLoading: false }); 22619 return false; 22620 } 22621 const matchKey = this.cabanaLotGroupKey(match); 22622 const variants = lots.filter((lot) => this.cabanaLotGroupKey(lot) === matchKey); 22623 this.openCabanaLot(match, variants.length ? variants : [match]); 22624 return true; 22625 } 22626 openCabanaLotByCert(lotCert, lots = this.state.cabanaLots) { 22627 const target = String(lotCert || '').trim(); 22628 if (!target || !Array.isArray(lots)) return false; 22629 const match = lots.find((lot) => this.cabanaPrebidCert(lot) === target || this.cabanaLotId(lot) === target); 22630 if (!match) return false; 22631 const matchKey = this.cabanaLotGroupKey(match); 22632 const variants = lots.filter((lot) => this.cabanaLotGroupKey(lot) === matchKey); 22633 this.openCabanaLot(match, variants.length ?variants : [match]); 22634 return true; 22635 } 22636 cabanaAnalyticsMetric(summary, keys) { 22637 const root = summary?.data || summary || {}; 22638 const sources = [root?.totals, root?.auction, root, root?.metrics, root?.counters].filter(Boolean); 22639 for (const source of sources) { 22640 for (const key of keys) { 22641 const value = Number(source?.[key]); 22642 if (Number.isFinite(value)) return value; 22643 } 22644 } 22645 return 0; 22646 } 22647 cabanaAnalyticsTotals(summary) { 22648 const whatsapp = this.cabanaAnalyticsMetric(summary, ['whatsappClicks', 'whatsAppClicks']); 22649 const calls = this.cabanaAnalyticsMetric(summary, ['callClicks', 'calls']); 22650 const directContacts = this.cabanaAnalyticsMetric(summary, ['contactClicks', 'contacts']); 22651 const durationSamples = this.cabanaAnalyticsMetric(summary, ['viewDurationSamples', 'durationSamples']); 22652 const durationTotal = this.cabanaAnalyticsMetric(summary, ['viewDurationMsTotal', 'durationMsTotal', 'totalDurationMs']); 22653 const avgDuration = this.cabanaAnalyticsMetric(summary, ['avgVie
22653wDurationMs', 'averageViewDurationMs']) || (durationSamples ?durationTotal / durationSamples : 0); 22654 return { 22655 events: this.cabanaAnalyticsMetric(summary, ['eventCount', 'events', 'totalEvents']), 22656 lotViews: this.cabanaAnalyticsMetric(summary, ['lotViews', 'views', 'viewCount']), 22657 rpViews: this.cabanaAnalyticsMetric(summary, ['rpViews']), 22658 videoPlays: this.cabanaAnalyticsMetric(summary, ['videoPlays', 'plays']), 22659 imageViews: this.cabanaAnalyticsMetric(summary, ['imageViews']), 22660 shares: this.cabanaAnalyticsMetric(summary, ['shares', 'shareClicks', 'event_share_click']), 22661 contacts: directContacts || (whatsapp + calls), 22662 avgDuration, 22663 }; 22664 } 22665 async openCabanaStatsIndex() { 22666 try { window.history.replaceState({}, '', '/cabanas/estadisticas'); } catch {} 22667 this.setState({ 22668 route: 'cabanaStats', 22669 currentCabana: null, 22670 cabanaLots: null, 22671 cabanaStatsOverview: true, 22672 cabanaStatsOverviewLoading: true, 22673 cabanaStatsOverviewError: '', 22674 cabanaStatsOverviewRows: [], 22675 }); 22676 window.scrollTo(0, 0); 22677 try { 22678 const remates = Array.isArray(this.state.cabanas) && this.state.cabanas.length ?this.state.cabanas : (await this.fetchCabanas(true) || []); 22679 const allowedRemates = remates.filter((item) => this.canSeeCabanaStats(item)); 22680 const rows = await Promise.all(allowedRemates.map(async (item) => { 22681 const auctionId = this.cabanaRemateId(item); 22682 try { 22683 const summary = await this.fetchCabanaAnalyticsSummaryById(auctionId); 22684 return { item, auctionId, ok: true, totals: this.cabanaAnalyticsTotals(summary) }; 22685 } catch (error) { 22686 return { item, auctionId, ok: false, totals: this.cabanaAnalyticsTotals(null), error: error?.message || 'Analytics no disponible' }; 22687 } 22688 })); 22689 if (this.state.route === 'cabanaStats' && this.state.cabanaStatsOverview) { 22690 this.setState({ cabanaStatsOverviewRows: rows, cabanaStatsOverviewLoading: false, cabanaStatsOverviewError: '' }); 22691 } 22692 } catch (error) { 22693 if (this.state.route === 'cabanaStats' && this.state.cabanaStatsOverview) { 22694 this.setState({ cabanaStatsOverviewRows: [], cabanaStatsOverviewLoading: false, cabanaStatsOverviewError: error?.message || 'No se pudieron cargar las estadÃsticas.' }); 22695 } 22696 } 22697 } 22698 openCabanaStats(item) { 22699 item = this.normalizeCabanaAuctionRecord(item || this.state.currentCabana); 22700 if (!item) return; 22701 if (!this.canSeeCabanaStats(item)) { 22702 this.showToast('No tenés permiso para ver las estadÃsticas de este remate.'); 22703 return; 22704 } 22705 if (this.state.publicAdvanceMode === 'lacassina' && window.location.pathname !== '/lacassina/estadisticas') { 22706 window.location.assign('/lacassina/estadisticas'); 22707 return; 22708 } 22709 if (this.state.publicAdvanceMode !== 'lacassina') this.syncCabanaLocation(item, 'estadisticas'); 22710 const itemId = this.cabanaRemateId(item); 22711 const hasLots = this.cabanaRemateId(this.state.currentCabana) === itemId && Array.isArray(this.state.cabanaLots); 22712 this.setState({ 22713 route: 'cabanaStats', 22714 currentCabana: item, 22715 cabanaLots: hasLots ?this.state.cabanaLots : null, 22716 cabanaLotsLoading: !hasLots, 22717 cabanaAnalyticsSummary: null, 22718 cabanaAnalyticsLoading: true, 22719 cabanaStatsVisitorPage: 1, 22720 cabanaStatsOverview: false, 22721 cabanaSalesOrderOpen: false, 22722 profileEditorOpen: false, 22723 profilePhotoViewerOpen: false, 22724 profileCropOpen: false, 22725 profileCropSource: '', 22726 adminUsersOpen: false, 22727 notificationsOpen: false, 22728 }); 22729 if (this.state.publicAdvanceMode === 'lacassina' && window.location.pathname !== '/lacassina/estadisticas') { 22730 this.writeAppHistory('/lacassina/estadisticas'); 22731 window.__ROSGAN_PUBLIC_PATH__ = '/lacassina/estadisticas'; 22732 window.__ROSGAN_PUBLIC_ROUTE__ = 'lacassina-stats'; 22733 document.documentElement.dataset.publicRoute = 'lacassina-stats'; 22734 } 22735 window.scrollTo(0, 0); 22736 if (!hasLots) this.fetchCabanaLots(item); 22737 this.fetchCabanaAnalyticsSummary(item); 22738 this.ensureCabanaBrand(item); 22739 const id = itemId; 22740 if (id) this.fetchCabanaById(id, { force: true }).then((fresh) => { 22741 if (fresh && this.state.route === 'cabanaStats') { 22742 this.setState({ currentCabana: fresh }); 22743 this.fetchCabanaLots(fresh); 22744 this.fetchCabanaAnalyticsSummary(fresh); 22745 this.ensureCabanaBrand(fresh); 22746 } 22747 }).catch(() => {}); 22748 } 22749 backToCabanaFromStats() { 22750 if (this.state.cabanaStatsOverview) {
22751 try { window.history.pushState({}, '', '/cabanas'); } catch {} 22752 this.setState({ route: 'cabanas', cabanaStatsOverview: false, currentCabana: null, cabanaLots: null }); 22753 if (!Array.isArray(this.state.cabanas)) this.fetchCabanas(); 22754 window.scrollTo(0, 0); 22755 return; 22756 } 22757 if (this.state.publicAdvanceMode === 'lacassina') { 22758 this.writeAppHistory('/lacassina'); 22759 window.__ROSGAN_PUBLIC_PATH__ = '/lacassina'; 22760 window.__ROSGAN_PUBLIC_ROUTE__ = 'lacassina'; 22761 document.documentElement.dataset.publicRoute = 'lacassina'; 22762 } else this.syncCabanaLocation(this.state.currentCabana); 22763 this.setState({ route: 'cabanaDetail' }); 22764 window.scrollTo(0, 0); 22765 } 22766 backToCabanaCatalog() { 22767 if (this.state.publicAdvanceMode === 'lacassina') { 22768 this.writeAppHistory('/lacassina'); 22769 window.__ROSGAN_PUBLIC_PATH__ = '/lacassina'; 22770 window.__ROSGAN_PUBLIC_ROUTE__ = 'lacassina'; 22771 document.documentElement.dataset.publicRoute = 'lacassina'; 22772 } else this.syncCabanaLocation(this.state.currentCabana); 22773 this.setState({ route: 'cabanaDetail' }); 22774 window.scrollTo(0, 0); 22775 } 22776 cabanaStatsData(remate = this.state.currentCabana, lots = this.state.cabanaLots) { 22777 if (this.state.cabanaStatsOverview) { 22778 const rows = (Array.isArray(this.state.cabanaStatsOverviewRows) ?this.state.cabanaStatsOverviewRows : []).map((row) => ({ 22779 id: row.auctionId, 22780 title: row.item?.nombre_remate || row.item?.titulo || row.item?.title || ('Remate ' + row.auctionId), 22781 events: Number(row.totals?.events || 0).toLocaleString('es-AR'),
22782 views: Number((row.totals?.lotViews || 0) + (row.totals?.rpViews || 0)).toLocaleString('es-AR'), 22783 videos: Number(row.totals?.videoPlays || 0).toLocaleString('es-AR'), 22784 contacts: Number(row.totals?.contacts || 0).toLocaleString('es-AR'), 22785 status: row.ok ?'Registrando' : 'Sin conexión', 22786 statusClass: row.ok ?'is-ok' : 'is-error', 22787 open: () => this.openCabanaStats(row.item), 22788 })); 22789 const totals = (this.state.cabanaStatsOverviewRows || []).reduce((acc, row) => { 22790 Object.keys(acc).forEach((key) => { acc[key] += Number(row.totals?.[key] || 0); }); 22791 return acc; 22792 }, { events: 0, lotViews: 0, rpViews: 0, videoPlays: 0, contacts: 0 }); 22793 return { 22794 cbsIsOverview: true, 22795 cbsIsAuction: false, 22796 cbsAccent: '#0a5ca8', 22797 cbsDark: '#002e5f', 22798 cbsOverviewLoading: !!this.state.cabanaStatsOverviewLoading, 22799 cbsOverviewReady: !this.state.cabanaStatsOverviewLoading, 22800 cbsOverviewRows: rows, 22801 cbsOverviewHasRows: rows.length > 0, 22802 cbsOverviewEmpty: !rows.length && !this.state.cabanaStatsOverviewLoading, 22803 cbsOverviewError: this.state.cabanaStatsOverviewError || '', 22804 cbsOverviewHasError: !!this.state.cabanaStatsOverviewError, 22805 cbsOverviewAuctionCount: rows.length.toLocaleString('es-AR'),
22806 cbsOverviewEventCount: totals.events.toLocaleString('es-AR'), 22807 cbsOverviewViewCount: (totals.lotViews + totals.rpViews).toLocaleString('es-AR'), 22808 cbsOverviewVideoCount: totals.videoPlays.toLocaleString('es-AR'), 22809 cbsOverviewContactCount: totals.contacts.toLocaleString('es-AR'), 22810 cbsBack: () => this.backToCabanaFromStats(), 22811 }; 22812 } 22813 const loading = !!this.state.cabanaLotsLoading || !Array.isArray(lots); 22814 const rows = Array.isArray(lots) ?lots : []; 22815 const hasValue = (value) => value !== null && value !== undefined && String(value).trim() !== ''; 22816 const getValue = (lot, keys) => { 22817 for (const key of keys) { 22818 const value = lot?.values?.[key] ?? lot?.[key]; 22819 if (hasValue(value)) return value; 22820 } 22821 return null; 22822 }; 22823 const numberOf = (value) => { 22824 if (!hasValue(value)) return null; 22825 const normalized = String(value).trim().replace(/\s/g, '').replace(/\.(?=\d{3}(?:\D|$))/g, '').replace(',', '.').replace(/[^\d.+-]/g, ''); 22826 const parsed = Number(normalized); 22827 return Number.isFinite(parsed) ?parsed : null; 22828 }; 22829 const average = (values) => { 22830 const valid = values.map(numberOf).filter((value) => value !== null); 22831 return valid.length ?valid.reduce((sum, value) => sum + value, 0) / valid.length : null; 22832 }; 22833 const categoryCounts = new Map(); 22834 const sireCounts = new Map(); 22835 const groupIds = new Set(); 22836 let mediaCount = 0; 22837 let populatedFields = 0; 22838 let totalFields = 0; 22839 rows.forEach((lot, index) => { 22840 const category = String(lot?.categoryName || lot?.categoriaNombre || lot?.category || 'Sin categorÃa').trim(); 22841 categoryCounts.set(category, (categoryCounts.get(category) || 0) + 1); 22842 const sire = String(getValue(lot, ['padre', 'sire', 'father']) || '').trim(); 22843 if (sire) sireCounts.set(sire, (sireCounts.get(sire) || 0) + 1); 22844 groupIds.add(String(lot?.groupId || lot?.group?.id || lot?.lotCert || lot?.id || index)); 22845 if (lot?.coverImage?.url || lot?.thumbnailUrl || lot?.mediaUrl || (Array.isArray(lot?.images) && lot.images.length) || (Array.isArray(lot?.videos) && lot.videos.length)) mediaCount += 1; 22846 Object.entries(lot?.values || {}).forEach(([key, value]) => { 22847 if (['id', 'displayOrder'].includes(key)) return; 22848 totalFields += 1; 22849 if (hasValue(value)) populatedFields += 1; 22850 }); 22851 }); 22852 const count = rows.length; 22853 const categories = [...categoryCounts.entries()].sort((a, b) => b[1] - a[1]).map(([label, value]) => { 22854 const percentage = count ?Math.round((value / count) * 100) : 0; 22855 return { label, value, percentage, width: `${Math.max(percentage, 3)}%` }; 22856 }); 22857 const sires = [...sireCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6).map(([label, value], index) => ({ 22858 label, value, rank: index + 1, 22859 })); 22860 const weightAverage = average(rows.map((lot) => getValue(lot, ['peso_actual', 'pesoActual', 'peso_promedio', 'pesoPromedio', 'peso']))); 22861 const ceAverage = average(rows.map((lot) => getValue(lot, ['ce', 'c_escr', 'circunferencia_escrotal']))); 22862 const mediaCoverage = count ?Math.round((mediaCount / count) * 100) : 0; 22863 const completeness = totalFields ?Math.round((populatedFields / totalFields) * 100) : 0; 22864 const primary = remate ? (this.cabanaRemateParticipants(remate)[0] || null) : null; 22865 const logo = primary?.logo || primary?.logoUrl || primary?.logoDataUrl || primary?.image || primary?.url || ''; 22866 const accent = remate ?this.cabanaColor(remate) : '#0a5ca8'; 22867 const analyticsSummary = this.state.cabanaAnalyticsSummary; 22868 const analyticsTotals = this.cabanaAnalyticsTotals(analyticsSummary); 22869 const analyticsRoot = analyticsSummary?.data || analyticsSummary || {}; 22870 const analyticsRps = this.listFrom(analyticsRoot, ['rps']).filter(Boolean); 22871 const analyticsVisitors = this.listFrom(analyticsRoot, ['visitors']).filter(Boolean); 22872 const formatVisitorDuration = (milliseconds) => { 22873 const seconds = Math.max(0, Math.round(Number(milliseconds || 0) / 1000)); 22874 const hours = Math.floor(seconds / 3600); 22875 const minutes = Math.floor((seconds % 3600) / 60); 22876 const rest = seconds % 60; 22877 return hours ?`${hours} h ${String(minutes).padStart(2, '0')} min` : (minutes ?`${minutes} min ${String(rest).padStart(2, '0')} s` : `${rest} s`); 22878 }; 22879 const visitorRowsAll = analyticsVisitors.map((visitor) => { 22880 const registered = String(visitor?.type || '').toLowerCase() === 'registered'; 22881 const lastSeenDate = visitor?.lastSeenAt ?new Date(visitor.lastSeenAt) : null; 22882 const lotViews = Number(visitor?.lotViews || 0); 22883 const rpViews = Number(visitor?.rpViews || 0); 22884 const videoPlays = Number(visitor?.videoPlays || 0); 22885 const contactClicks = Number(visitor?.contactClicks || 0); 22886 const visitedLots = (Array.isArray(visitor?.lots) ?visitor.lots : []).map((lot) => { 22887 const label = String(lot?.label || (lot?.lotId ?'Lote ' + lot.lotId : 'Lote')).trim(); 22888 const rp = String(lot?.rp || '').trim(); 22889 return `${label}${rp && !label.toLowerCase().includes(rp.toLowerCase()) ?' · RP ' + rp : ''} (${formatVisitorDuration(lot?.durationMs)})`; 22890 }); 22891 return { 22892 name: registered ?(String(visitor?.name || '').trim() || 'Usuario registrado') : 'Anónimo', 22893 email: registered ?(String(visitor?.email || '').trim() || 'Sin correo informado') : 'â', 22894 phone: registered ?(String(visitor?.phone || '').trim() || 'Sin teléfono informado') : 'â', 22895 type: registered ?'Registrado' : 'Anónimo',
22896 activity: Number(visitor?.eventCount || 0).toLocaleString('es-AR') + ' acciones', 22897 detail: [lotViews ?lotViews + ' lotes' : '', rpViews ?rpViews + ' RPs' : '', videoPlays ?videoPlays + ' videos' : '', contactClicks ?contactClicks + ' contactos' : ''].filter(Boolean).join(' · ') || 'Navegación registrada', 22898 lots: visitedLots.join(' · ') || 'Sin lote identificado', 22899 totalTime: formatVisitorDuration(visitor?.durationMs), 22900 lastSeen: lastSeenDate && !Number.isNaN(lastSeenDate.getTime()) ?lastSeenDate.toLocaleString('es-AR', { dateStyle: 'short', timeStyle: 'short' }) : 'Sin fecha', 22901 }; 22902 }).sort((a, b) => { 22903 if (a.type !== b.type) return a.type === 'Registrado' ?-1 : 1; 22904 return 0; 22905 }); 22906 const visitorPageSize = 25; 22907 const visitorPageCount = Math.max(1, Math.ceil(visitorRowsAll.length / visitorPageSize)); 22908 const visitorPage = Math.min(Math.max(1, Number(this.state.cabanaStatsVisitorPage || 1)), visitorPageCount); 22909 const visitorRows = visitorRowsAll.slice((visitorPage - 1) * visitorPageSize, visitorPage * visitorPageSize); 22910 const formatAnalyticsDuration = (milliseconds) => { 22911 const seconds = Math.max(0, Math.floor(Number(milliseconds || 0) / 1000)); 22912 const hours = Math.floor(seconds / 3600); 22913 const minutes = Math.floor((seconds % 3600) / 60); 22914 const rest = seconds % 60; 22915 if (hours > 0) return hours + ':' + String(minutes).padStart(2, '0') + ':' + String(rest).padStart(2, '0'); 22916 return minutes + ':' + String(rest).padStart(2, '0'); 22917 }; 22918 const rpActivityRows = analyticsRps.map((row) => { 22919 const events = Number(row?.eventCount || row?.events || 0); 22920 const lotViews = Number(row?.lotViews || 0); 22921 const durationTotal = Number(row?.viewDurationMsTotal || row?.durationMsTotal || row?.totalDurationMs || 0); 22922 const samples = Number(row?.viewDurationSamples || row?.durationSamples || 0); 22923 const durationAverage = Number(row?.avgViewDurationMs || row?.averageViewDurationMs || 0) || (samples ?durationTotal / samples : 0); 22924 const contacts = Number(row?.contactClicks || row?.contacts || 0) || (Number(row?.whatsappClicks || 0) + Number(row?.callClicks || 0)); 22925 const rpViews = Number(row?.rpViews || row?.views || 0); 22926 const videos = Number(row?.videoPlays || row?.plays || 0); 22927 const images = Number(row?.imageViews || 0); 22928 const shares = Number(row?.shares || row?.shareClicks || row?.event_share_click || 0); 22929 return { 22930 rp: String(row?.rp || row?.rpId || row?.itemId || 'Sin RP'), 22931 lot: row?.lotCert ?('Lote ' + row.lotCert) : (row?.lotTitle || 'Sin lote'), 22932 category: String(row?.categoryName || row?.lotTitle || 'Sin categorÃa'), 22933 lotViews: lotViews.toLocaleString('es-AR'),
22934 rpViews: rpViews.toLocaleString('es-AR'), 22935 videos: videos.toLocaleString('es-AR'), 22936 images: images.toLocaleString('es-AR'), 22937 contacts: contacts.toLocaleString('es-AR'), 22938 shares: shares.toLocaleString('es-AR'), 22939 samplesLabel: samples === 1 ?'1 medición de tiempo' : (samples > 1 ?(samples.toLocaleString('es-AR') + ' mediciones de tiempo') :'Sin mediciones de tiempo'), 22940 totalTime: formatAnalyticsDuration(durationTotal), 22941 avgTime: formatAnalyticsDuration(durationAverage), 22942 durationTotal, 22943 durationAverage, 22944 samples, 22945 lotViewsNumber: lotViews, 22946 rpViewsNumber: rpViews, 22947 videosNumber: videos, 22948 imagesNumber: images, 22949 contactsNumber: contacts, 22950 sharesNumber: shares, 22951 eventsNumber: events, 22952 }; 22953 }).filter((row) => row.eventsNumber > 0).sort((a, b) => b.durationTotal - a.durationTotal || b.eventsNumber - a.eventsNumber || a.rp.localeCompare(b.rp, 'es')); 22954 const avgDurationLabel = analyticsTotals.avgDuration > 0 ?this.formatCabanaVideoTime(analyticsTotals.avgDuration / 1000) : '0:00'; 22955 const percentage = (numerator, denominator) => denominator > 0 ?(numerator / denominator) * 100 : 0; 22956 const formatPercent = (value) => value.toLocaleString('es-AR', { minimumFractionDigits: value > 0 && value < 10 ?1 : 0, maximumFractionDigits: 1 }) + '%'; 22957 const formatDecimal = (value) => value.toLocaleString('es-AR', { minimumFractionDigits: 1, maximumFractionDigits: 1 }); 22958 const contactRateNumber = percentage(analyticsTotals.contacts, analyticsTotals.lotViews); 22959 const evaluationDepthNumber = analyticsTotals.lotViews > 0 ?analyticsTotals.rpViews / analyticsTotals.lotViews : 0; 22960 const videoRateNumber = percentage(analyticsTotals.videoPlays, analyticsTotals.rpViews); 22961 const attentionSeconds = analyticsTotals.avgDuration / 1000; 22962 const contactHealth = contactRateNumber >= 3 22963 ?{ label: 'Respuesta activa', className: 'is-good' } 22964 : (contactRateNumber >= 1 ?{ label: 'En desarrollo', className: 'is-watch' } : { label: 'Oportunidad de mejora', className: 'is-alert' }); 22965 const depthHealth = evaluationDepthNumber >= 1.5 22966 ?{ label: 'Exploración profunda', className: 'is-good' } 22967 : (evaluationDepthNumber >= 1 ?{ label: 'Exploración media', className: 'is-watch' } : { label: 'Exploración superficial', className: 'is-alert' }); 22968 const videoHealth = videoRateNumber >= 35 22969 ?{ label: 'Video decisivo', className: 'is-good' } 22970 : (videoRateNumber >= 20 ?{ label: 'Uso intermedio', className: 'is-watch' } : { label: 'Baja activación', className: 'is-alert' }); 22971 const attentionHealth = attentionSeconds >= 20 22972 ?{ label: 'Evaluación detenida', className: 'is-good' } 22973 : (attentionSeconds >= 8 ?{ label: 'Atención breve', className: 'is-watch' } : { label: 'Salida rápida', className: 'is-alert' }); 22974 22975 const categoryPerformanceMap = new Map(); 22976 categoryCounts.forEach((published, label) => categoryPerformanceMap.set(this.consKey(label), { 22977 label, published, views: 0, videos: 0, contacts: 0, shares: 0, durationTotal: 0, samples: 0, 22978 })); 22979 analyticsRps.forEach((row) => { 22980 const label = String(row?.categoryName || row?.lotTitle || 'Sin categorÃa'); 22981 const key = this.consKey(label); 22982 if (!categoryPerformanceMap.has(key)) categoryPerformanceMap.set(key, { label, published: 0, views: 0, videos: 0, contacts: 0, shares: 0, durationTotal: 0, samples: 0 }); 22983 const target = categoryPerformanceMap.get(key); 22984 target.views += Number(row?.rpViews || row?.views || 0); 22985 target.videos += Number(row?.videoPlays || row?.plays || 0); 22986 target.contacts += Number(row?.contactClicks || row?.contacts || 0) || (Number(row?.whatsappClicks || 0) + Number(row?.callClicks || 0)); 22987 target.shares += Number(row?.shares || row?.shareClicks || row?.event_share_click || 0); 22988 target.durationTotal += Number(row?.viewDurationMsTotal || row?.durationMsTotal || row?.totalDurationMs || 0); 22989 target.samples += Number(row?.viewDurationSamples || row?.durationSamples || 0); 22990 }); 22991 const categoryPerformance = [...categoryPerformanceMap.values()].filter((item) => item.views > 0).map((item) => { 22992 const viewsPerRpNumber = item.published > 0 ?item.views / item.published : item.views; 22993 const avgTimeMs = item.samples ?item.durationTotal / item.samples : 0; 22994 return { 22995 ...item, 22996 viewsPerRpNumber, 22997 viewsNumber: item.views, 22998 videosNumber: item.videos, 22999 contactsNumber: item.contacts, 23000 sharesNumber: item.shares, 23001 intentNumber: item.contacts + item.shares, 23002 avgTimeMs, 23003 viewsPerRp: formatDecimal(viewsPerRpNumber),
23004 views: item.views.toLocaleString('es-AR'), 23005 videos: item.videos.toLocaleString('es-AR'), 23006 videoRate: formatPercent(percentage(item.videos, item.views)), 23007 intent: (item.contacts + item.shares).toLocaleString('es-AR'), 23008 avgTime: formatAnalyticsDuration(avgTimeMs), 23009 }; 23010 }).sort((a, b) => b.viewsPerRpNumber - a.viewsPerRpNumber || b.contacts - a.contacts || a.label.localeCompare(b.label, 'es')); 23011 23012 const opportunities = [...rpActivityRows].sort((a, b) => b.rpViewsNumber - a.rpViewsNumber || (b.contactsNumber + b.sharesNumber) - (a.contactsNumber + a.sharesNumber) || b.durationAverage - a.durationAverage).slice(0, 12).map((row) => ({ 23013 ...row, 23014 videoRate: formatPercent(percentage(row.videosNumber, row.rpViewsNumber)), 23015 intent: (row.contactsNumber + row.sharesNumber).toLocaleString('es-AR'), 23016 })); 23017 23018 const analyticsDaily = this.listFrom(analyticsRoot, ['daily']).filter(Boolean).sort((a, b) => String(a?.dayKey || '').localeCompare(String(b?.dayKey || ''))).slice(-10); 23019 const dailyValues = analyticsDaily.map((row) => Number(row?.lotViews || 0) + Number(row?.rpViews || 0)); 23020 const maxDailyValue = Math.max(1, ...dailyValues); 23021 const trend = analyticsDaily.map((row, index) => { 23022 const rawKey = String(row?.dayKey || ''); 23023 const date = /^\d{4}-\d{2}-\d{2}$/.test(rawKey) ?new Date(rawKey + 'T12:00:00') : null; 23024 const value = dailyValues[index]; 23025 const intent = (Number(row?.contactClicks || row?.contacts || 0) + Number(row?.shares || row?.shareClicks || 0)); 23026 return { 23027 dayKey: rawKey, 23028 label: date && !Number.isNaN(date.getTime()) ?date.toLocaleDateString('es-AR', { day: '2-digit', month: 'short' }).replace('.', '') : rawKey, 23029 valueNumber: value, 23030 value: value.toLocaleString('es-AR'),
23031 width: Math.max(4, Math.round((value / maxDailyValue) * 100)) + '%', 23032 intentNumber: intent, 23033 intent: intent ?(intent.toLocaleString('es-AR') + ' int.') : 'â', 23034 }; 23035 }); 23036 23037 const topRp = [...rpActivityRows].sort((a, b) => b.rpViewsNumber - a.rpViewsNumber)[0] || null; 23038 const topFiveViews = [...rpActivityRows].sort((a, b) => b.rpViewsNumber - a.rpViewsNumber).slice(0, 5).reduce((sum, row) => sum + row.rpViewsNumber, 0); 23039 const concentration = percentage(topFiveViews, analyticsTotals.rpViews); 23040 const topCategory = categoryPerformance[0] || null; 23041 const insights = [ 23042 { 23043 index: '01', 23044 title: contactRateNumber < 1 ?'La consulta es el principal cuello de botella' : 'El catálogo ya transforma interés en consultas', 23045 detail: `${analyticsTotals.contacts.toLocaleString('es-AR')} contacto${analyticsTotals.contacts === 1 ?'' : 's'} sobre ${analyticsTotals.lotViews.toLocaleString('es-AR')} aperturas. ${contactRateNumber < 1 ?'Conviene reforzar el llamado a WhatsApp y la propuesta comercial en los RPs con más vistas.' : 'Identificá qué categorÃas generan esas consultas y replicá su presentación.'}`, 23046 }, 23047 { 23048 index: '02', 23049 title: topRp ?`${topRp.rp} concentra la mayor atención individual` :'TodavÃa no hay un RP destacado', 23050 detail: topRp ?`${topRp.rpViews} evaluaciones en ${topRp.lot}, con ${topRp.videos} reproducciones de video. Los cinco RPs más vistos concentran ${formatPercent(concentration)} de las evaluaciones.` :'Hace falta más actividad para priorizar reproductores.', 23051 }, 23052 { 23053 index: '03', 23054 title: topCategory ?`${topCategory.label} lidera la demanda relativa` :'TodavÃa no hay una categorÃa lÃder', 23055 detail: topCategory ?`${topCategory.viewsPerRp} vistas por RP publicado, comparando categorÃas sin favorecer a las que tienen más animales.` :'La comparación se habilitará cuando existan vistas por categorÃa.', 23056 }, 23057 ]; 23058 const exportData = { 23059 title: remate?.nombre_remate || remate?.titulo || remate?.title || 'Remate de cabaña', 23060 accent, 23061 dark: remate ?this.cabanaBrandDark(remate) : '#002e5f', 23062 logo, 23063 rpCount: count, 23064 lotCount: groupIds.size, 23065 categoryCount: categoryCounts.size, 23066 mediaCoverage, 23067 completeness, 23068 weightAverage, 23069 ceAverage, 23070 totals: analyticsTotals, 23071 contactRateNumber, 23072 evaluationDepthNumber, 23073 videoRateNumber, 23074 attentionSeconds, 23075 categories: categories.map((item) => ({ label: item.label, published: item.value, percentage: item.percentage })), 23076 categoryPerformance, 23077 opportunities, 23078 rpActivityRows, 23079 trend, 23080 insights, 23081 }; 23082 return { 23083 cbsIsOverview: false, 23084 cbsIsAuction: true, 23085 cbsLoading: loading, 23086 cbsReady: !loading, 23087 cbsTitle: remate?.nombre_remate || remate?.titulo || remate?.title || 'Remate de cabaña', 23088 cbsLogo: logo, 23089 cbsHasLogo: !!logo, 23090 cbsAccent: accent, 23091 cbsDark: remate ?this.cabanaBrandDark(remate) : '#002e5f', 23092 cbsRpCount: count, 23093 cbsLotCount: groupIds.size, 23094 cbsCategoryCount: categoryCounts.size, 23095 cbsMediaCoverage: `${mediaCoverage}%`, 23096 cbsCompleteness: `${completeness}%`, 23097 cbsAvgWeight: weightAverage === null ?'Sin datos' : `${Math.round(weightAverage).toLocaleString('es-AR')} kg`, 23098 cbsAvgCe: ceAverage === null ?'Sin datos' : ceAverage.toLocaleString('es-AR', { maximumFractionDigits: 1 }), 23099 cbsCategories: categories, 23100 cbsHasCategories: categories.length > 0, 23101 cbsNoCategories: categories.length === 0, 23102 cbsSires: sires, 23103 cbsHasSires: sires.length > 0, 23104 cbsNoSires: sires.length === 0, 23105 cbsAnalyticsLoading: !!this.state.cabanaAnalyticsLoading, 23106 cbsAnalyticsReady: !this.state.cabanaAnalyticsLoading, 23107 cbsAnalyticsEventCount: analyticsTotals.events.toLocaleString('es-AR'),
23108 cbsAnalyticsLotViews: analyticsTotals.lotViews.toLocaleString('es-AR'), 23109 cbsAnalyticsRpViews: analyticsTotals.rpViews.toLocaleString('es-AR'), 23110 cbsAnalyticsVideoPlays: analyticsTotals.videoPlays.toLocaleString('es-AR'), 23111 cbsAnalyticsImageViews: analyticsTotals.imageViews.toLocaleString('es-AR'), 23112 cbsAnalyticsShares: analyticsTotals.shares.toLocaleString('es-AR'), 23113 cbsAnalyticsContacts: analyticsTotals.contacts.toLocaleString('es-AR'), 23114 cbsAnalyticsContactLabel: analyticsTotals.contacts === 1 ?'1 contacto' : (analyticsTotals.contacts.toLocaleString('es-AR') + ' contactos'), 23115 cbsAnalyticsAvgDuration: avgDurationLabel, 23116 cbsContactRate: formatPercent(contactRateNumber), 23117 cbsEvaluationDepth: formatDecimal(evaluationDepthNumber), 23118 cbsVideoRate: formatPercent(videoRateNumber), 23119 cbsContactHealth: contactHealth.label, 23120 cbsContactHealthClass: contactHealth.className, 23121 cbsDepthHealth: depthHealth.label, 23122 cbsDepthHealthClass: depthHealth.className, 23123 cbsVideoHealth: videoHealth.label, 23124 cbsVideoHealthClass: videoHealth.className, 23125 cbsAttentionHealth: attentionHealth.label, 23126 cbsAttentionHealthClass: attentionHealth.className, 23127 cbsInsights: insights, 23128 cbsTrend: trend, 23129 cbsHasTrend: trend.length > 0, 23130 cbsNoTrend: trend.length === 0, 23131 cbsCategoryPerformance: categoryPerformance, 23132 cbsHasCategoryPerformance: categoryPerformance.length > 0, 23133 cbsNoCategoryPerformance: categoryPerformance.length === 0, 23134 cbsOpportunities: opportunities, 23135 cbsOpportunityCount: rpActivityRows.length.toLocaleString('es-AR'),
23136 cbsHasOpportunities: opportunities.length > 0, 23137 cbsNoOpportunities: opportunities.length === 0, 23138 cbsRpActivityRows: rpActivityRows, 23139 cbsRpActivityCount: rpActivityRows.length.toLocaleString('es-AR'), 23140 cbsHasRpActivity: rpActivityRows.length > 0, 23141 cbsNoRpActivity: rpActivityRows.length === 0, 23142 cbsVisitors: visitorRows, 23143 cbsVisitorCount: visitorRowsAll.length.toLocaleString('es-AR'), 23144 cbsHasVisitors: visitorRowsAll.length > 0, 23145 cbsNoVisitors: visitorRowsAll.length === 0, 23146 cbsVisitorPage: visitorPage.toLocaleString('es-AR'), 23147 cbsVisitorPageCount: visitorPageCount.toLocaleString('es-AR'), 23148 cbsVisitorPreviousDisabled: visitorPage <= 1, 23149 cbsVisitorNextDisabled: visitorPage >= visitorPageCount, 23150 cbsVisitorPrevious: () => this.setState({ cabanaStatsVisitorPage: Math.max(1, visitorPage - 1) }), 23151 cbsVisitorNext: () => this.setState({ cabanaStatsVisitorPage: Math.min(visitorPageCount, visitorPage + 1) }), 23152 cbsExportData: exportData, 23153 cbsExportDisabled: loading || !!this.state.cabanaAnalyticsLoading || !!this.state.cabanaStatsExportBusy, 23154 cbsExcelLabel: this.state.cabanaStatsExportBusy === 'excel' ?'Generando Excelâ¦' :'Exportar Excel', 23155 cbsStoryLabel: this.state.cabanaStatsExportBusy === 'story' ?'Generando Storyâ¦' :'Story 9:16', 23156 cbsDownloadExcel: () => this.downloadCabanaStatsExcel(), 23157 cbsDownloadStory: () => this.downloadCabanaStatsStory(), 23158 cbsBack: () => this.backToCabanaFromStats(), 23159 }; 23160 } 23161 async ensureCabanaExcelJs() { 23162 if (window.ExcelJS?.Workbook) return window.ExcelJS; 23163 if (this._cabanaExcelJsPromise) return this._cabanaExcelJsPromise; 23164 const sources = [ 23165 '/vendor/exceljs.min.js?v=4.4.0', 23166 ]; 23167 this._cabanaExcelJsPromise = (async () => { 23168 for (const src of sources) { 23169 try { 23170 await new Promise((resolve, reject) => { 23171 const script = document.createElement('script'); 23172 script.src = src; 23173 script.async = true; 23174 script.onload = resolve; 23175 script.onerror = () => reject(new Error('No se pudo cargar ExcelJS.')); 23176 document.head.appendChild(script); 23177 }); 23178 if (window.ExcelJS?.Workbook) return window.ExcelJS; 23179 } catch {} 23180 } 23181 throw new Error('No se pudo preparar el generador de Excel. Revisá la conexión e intentá nuevamente.'); 23182 })(); 23183 try { return await this._cabanaExcelJsPromise; } 23184 catch (error) { this._cabanaExcelJsPromise = null; throw error; } 23185 } 23186 async cabanaStatsLogoDataUrl(source) { 23187 const url = this.cabanaMediaUrl(source) || String(source || '').trim(); 23188 if (!url) return ''; 23189 try { 23190 const image = await this.loadCabanaCanvasImage(url); 23191 if (!image) return ''; 23192 const canvas = document.createElement('canvas'); 23193 canvas.width = 720; 23194 canvas.height = 240; 23195 const ctx = canvas.getContext('2d'); 23196 const scale = Math.min(canvas.width / image.naturalWidth, canvas.height / image.naturalHeight); 23197 const width = image.naturalWidth * scale; 23198 const height = image.naturalHeight * scale; 23199 ctx.drawImage(image, (canvas.width - width) / 2, (canvas.height - height) / 2, width, height); 23200 return canvas.toDataURL('image/png'); 23201 } catch { return ''; } 23202 } 23203 cabanaStatsExportSnapshot() { 23204 return this.cabanaStatsData(this.state.currentCabana, this.state.cabanaLots)?.cbsExportData || null; 23205 } 23206 async downloadCabanaStatsExcel() { 23207 if (this.state.cabanaStatsExportBusy) return; 23208 const snapshot = this.cabanaStatsExportSnapshot(); 23209 if (!snapshot) { this.showToast('TodavÃa no hay estadÃsticas para exportar.'); return; } 23210 this.setState({ cabanaStatsExportBusy: 'excel' }); 23211 try { 23212 const ExcelJS = await this.ensureCabanaExcelJs(); 23213 const current = this.state.currentCabana || {}; 23214 const participants = this.cabanaRemateParticipants(current); 23215 const mainParticipant = participants.find((item) => item.type === 'principal') || participants[0] || {}; 23216 const mainLogoSource = mainParticipant.logoDataUrl || mainParticipant.logoUrl || mainParticipant.logo || mainParticipant.imageUrl || ''; 23217 const consignatariaRaw = current?.consignataria || current?.nombre_cuenta || current?.consignatariaId || ''; 23218 const consignatariaName = current?.consignataria || current?.nombre_cuenta || current?.nombre_consignatario || current?.consignatariaName || 'Rosgan'; 23219 const consignatariaLogoSource = this.consLogo(consignatariaRaw || consignatariaName) || ''; 23220 const [rosganLogo, mainLogo, consignatariaLogo] = await Promise.all([ 23221 this.cabanaStatsLogoDataUrl('/assets/rosgan-logo-blue.png'), 23222 this.cabanaStatsLogoDataUrl(mainLogoSource), 23223 this.cabanaStatsLogoDataUrl(consignatariaLogoSource), 23224 ]); 23225 const workbook = new ExcelJS.Workbook(); 23226 workbook.creator = 'Rosgan'; 23227 workbook.lastModifiedBy = 'Rosgan'; 23228 workbook.created = new Date(); 23229 workbook.modified = new Date(); 23230 workbook.calcProperties.fullCalcOnLoad = true; 23231 const colorArgb = (value, fallback) => { 23232 const raw = String(value || fallback || '').replace('#', '').trim(); 23233 const expanded = raw.length === 3 ?raw.split('').map((char) => char + char).join('') : raw; 23234 return /^[0-9a-f]{6}$/i.test(expanded) ?('FF' + expanded.toUpperCase()) : String(fallback || 'FF002E5F').replace('#', '').toUpperCase().padStart(8, 'F'); 23235 };
23236 const dark = colorArgb(snapshot.dark, 'FF002E5F'); 23237 const accent = colorArgb(snapshot.accent, 'FF0A5CA8'); 23238 const pale = 'FFEAF3FA'; 23239 const border = 'FFD9E5EF'; 23240 const white = 'FFFFFFFF'; 23241 const text = 'FF17304D'; 23242 const muted = 'FF6D8094'; 23243 const setFill = (cell, argb) => { cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb } }; }; 23244 const paint = (sheet, r1, c1, r2, c2, argb) => { 23245 for (let row = r1; row <= r2; row += 1) for (let col = c1; col <= c2; col += 1) setFill(sheet.getCell(row, col), argb); 23246 }; 23247 const borderStyle = { style: 'thin', color: { argb: border } }; 23248 const addLogo = (sheet, dataUrl, position) => { 23249 if (!dataUrl) return false; 23250 const id = workbook.addImage({ base64: dataUrl, extension: 'png' }); 23251 sheet.addImage(id, position); 23252 return true; 23253 }; 23254 const summary = workbook.addWorksheet('Resumen', { properties: { defaultRowHeight: 20 }, views: [{ showGridLines: false, state: 'frozen', ySplit: 5 }] }); 23255 summary.columns = Array.from({ length: 12 }, () => ({ width: 12 })); 23256 paint(summary, 1, 1, 4, 12, dark); 23257 paint(summary, 1, 1, 4, 3, white); 23258 paint(summary, 1, 10, 4, 12, white); 23259 summary.mergeCells('D1:I2'); 23260 summary.getCell('D1').value = 'INFORME DE ESTADÃSTICAS'; 23261 summary.getCell('D1').font = { name: 'Aptos Display', size: 22, bold: true, color: { argb: white } }; 23262 summary.getCell('D1').alignment = { vertical: 'middle', horizontal: 'left' }; 23263 summary.mergeCells('D3:I3'); 23264 summary.getCell('D3').value = snapshot.title; 23265 summary.getCell('D3').font = { name: 'Aptos', size: 12, bold: true, color: { argb: 'FFDCEAF6' } }; 23266 summary.getCell('D3').alignment = { vertical: 'middle', horizontal: 'left', wrapText: true }; 23267 summary.mergeCells('D4:I4'); 23268 summary.getCell('D4').value = `Generado ${new Date().toLocaleString('es-AR')} · rosgan.app`; 23269 summary.getCell('D4').font = { name: 'Aptos', size: 9, color: { argb: 'FFB8CADE' } }; 23270 summary.getCell('D4').alignment = { vertical: 'middle', horizontal: 'left' }; 23271 summary.getRow(1).height = 28; summary.getRow(2).height = 28; summary.getRow(3).height = 24; summary.getRow(4).height = 22; 23272 if (!addLogo(summary, rosganLogo, { tl: { col: .2, row: .35 }, ext: { width: 190, height: 54 } })) { 23273 summary.mergeCells('A2:C3'); summary.getCell('A2').value = 'ROSGAN'; summary.getCell('A2').font = { size: 20, bold: true, color: { argb: dark } }; summary.getCell('A2').alignment = { horizontal: 'center', vertical: 'middle' }; 23274 } 23275 if (!addLogo(summary, mainLogo, { tl: { col: 9.1, row: .2 }, ext: { width: 190, height: 58 } })) { 23276 summary.mergeCells('J1:L2'); summary.getCell('J1').value = mainParticipant.name || snapshot.title; summary.getCell('J1').font = { size: 11, bold: true, color: { argb: dark } }; summary.getCell('J1').alignment = { horizontal: 'center', vertical: 'middle', wrapText: true }; 23277 } 23278 if (!addLogo(summary, consignatariaLogo, { tl: { col: 9.25, row: 2.25 }, ext: { width: 170, height: 42 } })) { 23279 summary.mergeCells('J3:L4'); summary.getCell('J3').value = consignatariaName; summary.getCell('J3').font = { size: 10, bold: true, color: { argb: accent } }; summary.getCell('J3').alignment = { horizontal: 'center', vertical: 'middle', wrapText: true }; 23280 } 23281 summary.mergeCells('A6:L6'); 23282 summary.getCell('A6').value = 'RESUMEN EJECUTIVO'; 23283 summary.getCell('A6').font = { size: 11, bold: true, color: { argb: accent } }; 23284 summary.getCell('A6').alignment = { vertical: 'middle' }; 23285 const kpis = [ 23286 { label: 'APERTURAS DE LOTE', value: snapshot.totals.lotViews, note: `${snapshot.lotCount} lotes publicados`, format: '#,##0' }, 23287 { label: 'EVALUACIONES DE RP', value: snapshot.totals.rpViews, note: `${snapshot.rpCount} RPs en catálogo`, format: '#,##0' }, 23288 { label: 'REPRODUCCIONES', value: snapshot.totals.videoPlays, note: 'Videos iniciados', format: '#,##0' }, 23289 { label: 'CONTACTOS', value: snapshot.totals.contacts, note: 'WhatsApp y llamadas', format: '#,##0' }, 23290 ]; 23291 kpis.forEach((item, index) => { 23292 const start = 1 + index * 3; 23293 const end = start + 2; 23294 summary.mergeCells(7, start, 7, end); summary.mergeCells(8, start, 8, end); summary.mergeCells(9, start, 9, end); 23295 paint(summary, 7, start, 9, end, pale); 23296 for (let col = start; col <= end; col += 1) { 23297 summary.getCell(7, col).border = { top: borderStyle, left: col === start ?borderStyle : undefined, right: col === end ?borderStyle : undefined }; 23298 summary.getCell(9, col).border = { bottom: borderStyle, left: col === start ?borderStyle : undefined, right: col === end ?borderStyle : undefined }; 23299 } 23300 const label = summary.getCell(7, start); label.value = item.label; label.font = { size: 9, bold: true, color: { argb: muted } }; label.alignment = { vertical: 'bottom', horizontal: 'center' }; 23301 const value = summary.getCell(8, start); value.value = item.value; value.numFmt = item.format; value.font = { size: 22, bold: true, color: { argb: dark } }; value.alignment = { vertical: 'middle', horizontal: 'center' }; 23302 const note = summary.getCell(9, start); note.value = item.note; note.font = { size: 9, color: { argb: muted } }; note.alignment = { vertical: 'top', horizontal: 'center' }; 23303 }); 23304 summary.getRow(7).height = 22; summary.getRow(8).height = 34; summary.getRow(9).height = 23; 23305 const executiveRows = [ 23306 ['Señales registradas', snapshot.totals.events, 'Total de eventos de analÃtica', '#,##0'], 23307 ['Imágenes vistas', snapshot.totals.imageViews, 'Aperturas de material fotográfico', '#,##0'], 23308 ['Compartidos', snapshot.totals.shares, 'Acciones de difusión registradas', '#,##0'], 23309 ['Conversión a consulta', snapshot.contactRateNumber / 100, 'Contactos sobre aperturas de lote', '0.0%'], 23310 ['Profundidad de evaluación', snapshot.evaluationDepthNumber, 'RPs examinados por apertura', '0.0'], 23311 ['Activación de video', snapshot.videoRateNumber / 100, 'Reproducciones sobre vistas de RP', '0.0%'], 23312 ['Tiempo promedio', snapshot.attentionSeconds, 'Segundos efectivos por evaluación', '0.0 "s"'], 23313 ['Cobertura multimedia', snapshot.mediaCoverage / 100, 'RPs con foto o video', '0%'], 23314 ['Completitud de ficha', snapshot.completeness / 100, 'Campos
23314informados del catálogo', '0%'], 23315 ]; 23316 summary.mergeCells('A11:L11'); summary.getCell('A11').value = 'INDICADORES COMERCIALES'; setFill(summary.getCell('A11'), accent); summary.getCell('A11').font = { size: 10, bold: true, color: { argb: white } }; summary.getCell('A11').alignment = { vertical: 'middle' }; 23317 summary.getRow(11).height = 25; 23318 executiveRows.forEach((item, index) => { 23319 const row = 12 + index; 23320 summary.mergeCells(row, 1, row, 4); summary.mergeCells(row, 5, row, 7); summary.mergeCells(row, 8, row, 12); 23321 summary.getCell(row, 1).value = item[0]; summary.getCell(row, 1).font = { bold: true, color: { argb: text } }; 23322 summary.getCell(row, 5).value = item[1]; summary.getCell(row, 5).numFmt = item[3]; summary.getCell(row, 5).font = { bold: true, color: { argb: accent } }; summary.getCell(row, 5).alignment = { horizontal: 'right' }; 23323 summary.getCell(row, 8).value = item[2]; summary.getCell(row, 8).font = { color: { argb: muted }, italic: true }; 23324 paint(summary, row, 1, row, 12, index % 2 ?'FFF7FAFC' : white); 23325 for (let col = 1; col <= 12; col += 1) summary.getCell(row, col).border = { bottom: borderStyle }; 23326 }); 23327 const insightHeaderRow = 13 + executiveRows.length; 23328 summary.mergeCells(insightHeaderRow, 1, insightHeaderRow, 12); summary.getCell(insightHeaderRow, 1).value = 'LECTURA EJECUTIVA'; setFill(summary.getCell(insightHeaderRow, 1), dark); summary.getCell(insightHeaderRow, 1).font = { size: 10, bold: true, color: { argb: white } }; 23329 snapshot.insights.forEach((item, index) => { 23330 const row = insightHeaderRow + 1 + index * 2; 23331 summary.mergeCells(row, 1, row, 12); summary.getCell(row, 1).value = `${item.index} · ${item.title}`; summary.getCell(row, 1).font = { size: 11, bold: true, color: { argb: dark } }; 23332 summary.mergeCells(row + 1, 1, row + 1, 12); summary.getCell(row + 1, 1).value = item.detail; summary.getCell(row + 1, 1).font = { size: 9, color: { argb: muted } }; summary.getCell(row + 1, 1).alignment = { wrapText: true, vertical: 'top' }; summary.getRow(row + 1).height = 30; 23333 }); 23334 summary.pageSetup = { orientation: 'landscape', fitToPage: true, fitToWidth: 1, fitToHeight: 1, paperSize: 9, margins: { left: .3, right: .3, top: .4, bottom: .4, header: .2, footer: .2 } }; 23335 23336 const prepareDataSheet = (name, title, subtitle, headers, widths) => { 23337 const sheet = workbook.addWorksheet(name, { views: [{ showGridLines: false, state: 'frozen', ySplit: 4 }] }); 23338 sheet.columns = widths.map((width) => ({ width })); 23339 sheet.mergeCells(1, 1, 1, headers.length); sheet.getCell(1, 1).value = title; setFill(sheet.getCell(1, 1), dark); sheet.getCell(1, 1).font = { size: 18, bold: true, color: { argb: white } }; sheet.getCell(1, 1).alignment = { vertical: 'middle' }; sheet.getRow(1).height = 34; 23340 sheet.mergeCells(2, 1, 2, headers.length); sheet.getCell(2, 1).value = subtitle; sheet.getCell(2, 1).font = { size: 10, color: { argb: muted } }; sheet.getCell(2, 1).alignment = { vertical: 'middle' }; 23341 headers.forEach((header, index) => { const cell = sheet.getCell(4, index + 1); cell.value = header; setFill(cell, accent); cell.font = { size: 9, bold: true, color: { argb: white } }; cell.alignment = { horizontal: index === 0 ?'left' :'center', vertical: 'middle', wrapText: true }; }); 23342 sheet.getRow(4).height = 30; 23343 return sheet; 23344 }; 23345 const finalizeDataSheet = (sheet, rowCount, columnCount, numberFormats = {}) => { 23346 const endRow = Math.max(4, 4 + rowCount); 23347 sheet.autoFilter = { from: { row: 4, column: 1 }, to: { row: endRow, column: columnCount } }; 23348 for (let row = 5; row <= endRow; row += 1) { 23349 if (row % 2 === 0) paint(sheet, row, 1, row, columnCount, 'FFF5F9FC'); 23350 for (let col = 1; col <= columnCount; col += 1) { 23351 const cell = sheet.getCell(row, col); cell.border = { bottom: borderStyle }; cell.font = { name: 'Aptos', size: 10, color: { argb: text }, bold: row === endRow && sheet.getCell(row, 1).value === 'TOTAL' }; cell.alignment = { vertical: 'middle', horizontal: col === 1 ?'left' :'right', wrapText: col <= 3 }; 23352 if (numberFormats[col]) cell.numFmt = numberFormats[col]; 23353 } 23354 } 23355 sheet.pageSetup = { orientation: 'landscape', fitToPage: true, fitToWidth: 1, fitToHeight: 0, margins: { left: .25, right: .25, top: .45, bottom: .45, header: .2, footer: .2 } }; 23356 }; 23357 const categoryRows = snapshot.categoryPerformance.length ?snapshot.categoryPerformance : snapshot.categories.map((item) => ({ ...item, viewsNumber: 0, videosNumber: 0, contactsNumber: 0, sharesNumber: 0, intentNumber: 0, avgTimeMs: 0, viewsPerRpNumber: 0 })); 23358 const categorySheet = prepareDataSheet('CategorÃas', 'Rendimiento por categorÃa', snapshot.title, ['CategorÃa', 'RPs publicados', 'Evaluaciones', 'Vistas / RP', 'Videos', 'Activación video', 'Contactos', 'Compartidos', 'Intención', 'Tiempo prom. (seg)'], [34, 15, 15, 14, 12, 17, 13, 14, 12, 18]); 23359 categoryRows.forEach((item, index) => {
23360 const row = 5 + index; 23361 categorySheet.getCell(row, 1).value = item.label; 23362 categorySheet.getCell(row, 2).value = Number(item.published || 0); 23363 categorySheet.getCell(row, 3).value = Number(item.viewsNumber || 0); 23364 categorySheet.getCell(row, 4).value = { formula: `IFERROR(C${row}/B${row},0)`, result: Number(item.viewsPerRpNumber || 0) }; 23365 categorySheet.getCell(row, 5).value = Number(item.videosNumber || 0); 23366 categorySheet.getCell(row, 6).value = { formula: `IFERROR(E${row}/C${row},0)`, result: Number(item.viewsNumber ?item.videosNumber / item.viewsNumber : 0) }; 23367 categorySheet.getCell(row, 7).value = Number(item.contactsNumber || 0); 23368 categorySheet.getCell(row, 8).value = Number(item.sharesNumber || 0); 23369 categorySheet.getCell(row, 9).value = { formula: `G${row}+H${row}`, result: Number(item.intentNumber || 0) }; 23370 categorySheet.getCell(row, 10).value = Number(item.avgTimeMs || 0) / 1000; 23371 }); 23372 finalizeDataSheet(categorySheet, categoryRows.length, 10, { 2: '#,##0', 3: '#,##0', 4: '0.0', 5: '#,##0', 6: '0.0%', 7: '#,##0', 8: '#,##0', 9: '#,##0', 10: '0.0' }); 23373 const rpSheet = prepareDataSheet('Ranking RP', 'Actividad por reproductor', snapshot.title, ['RP', 'Lote', 'CategorÃa', 'Vistas lote', 'Vistas RP', 'Videos', 'Activación', 'Imágenes', 'Contactos', 'Compartidos', 'Intención', 'Tiempo total (seg)', 'Tiempo prom. (seg)', 'Muestras'], [14, 18, 34, 13, 13, 11, 14, 12, 13, 14, 12, 18, 18, 12]); 23374 snapshot.rpActivityRows.forEach((item, index) => { 23375 const row = 5 + index; 23376 const values = [item.rp, item.lot, item.category, item.lotViewsNumber, item.rpViewsNumber, item.videosNumber, null, item.imagesNumber, item.contactsNumber, item.sharesNumber, null, item.durationTotal / 1000, item.durationAverage / 1000, item.samples]; 23377 values.forEach((value, col) => { if (value !== null) rpSheet.getCell(row, col + 1).value = value; }); 23378 rpSheet.getCell(row, 7).value = { formula: `IFERROR(F${row}/E${row},0)`, result: item.rpViewsNumber ?item.videosNumber / item.rpViewsNumber : 0 }; 23379 rpSheet.getCell(row, 11).value = { formula: `I${row}+J${row}`, result: item.contactsNumber + item.sharesNumber }; 23380 }); 23381 finalizeDataSheet(rpSheet, snapshot.rpActivityRows.length, 14, { 4: '#,##0', 5: '#,##0', 6: '#,##0', 7: '0.0%', 8: '#,##0', 9: '#,##0', 10: '#,##0', 11: '#,##0', 12: '#,##0.0', 13: '0.0', 14: '#,##0' }); 23382 const trendSheet = prepareDataSheet('Evolución', 'Evolución diaria del interés', snapshot.title, ['Fecha', 'Aperturas y evaluaciones', 'Intención'], [20, 26, 18]); 23383 snapshot.trend.forEach((item, index) => { 23384 const row = 5 + index; 23385 trendSheet.getCell(row, 1).value = /^\d{4}-\d{2}-\d{2}$/.test(item.dayKey) ?new Date(item.dayKey + 'T12:00:00') :item.label; 23386 trendSheet.getCell(row, 2).value = item.valueNumber; 23387 trendSheet.getCell(row, 3).value = item.intentNumber; 23388 }); 23389 finalizeDataSheet(trendSheet, snapshot.trend.length, 3, { 1: 'dd/mm/yyyy', 2: '#,##0', 3: '#,##0' }); 23390 const buffer = await workbook.xlsx.writeBuffer(); 23391 this.downloadCabanaBlob(new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), this.cabanaExportFileBase(current) + '-estadisticas.xlsx'); 23392 this.showToast('Informe de estadÃsticas exportado en Excel.'); 23393 } catch (error) { 23394 console.error('[Cabanas] No se pudo generar el Excel de estadÃsticas', error); 23395 this.showToast(error?.message || 'No se pudo generar el Excel.'); 23396 } finally { 23397 this.setState({ cabanaStatsExportBusy: '' }); 23398 } 23399 } 23400 async createCabanaStatsStoryBlob(snapshot) { 23401 const current = this.state.currentCabana || {}; 23402 const participants = this.cabanaRemateParticipants(current); 23403 const mainParticipant = participants.find((item) => item.type === 'principal') || participants[0] || {}; 23404 const mainLogoSource = mainParticipant.logoDataUrl || mainParticipant.logoUrl || mainParticipant.logo || mainParticipant.logoWhiteDataUrl || ''; 23405 const consignatariaRaw = current?.consignataria || current?.nombre_cuenta || current?.consignatariaId || ''; 23406 const consignatariaName = current?.consignataria || current?.nombre_cuenta || current?.nombre_consignatario || current?.consignatariaName || 'Rosgan'; 23407 const consignatariaLogoSource = this.consWhiteLogo(consignatariaRaw || consignatariaName) || this.consLogo(consignatariaRaw || consignatariaName) || ''; 23408 const [rosganLogo, mainLogo, consignatariaLogo, faviconLogo] = await Promise.all([ 23409 this.loadCabanaCanvasImage('/assets/rosgan-logo-blue.png'), 23410 this.loadCabanaCanvasImage(mainLogoSource), 23411 this.loadCabanaCanvasImage(consignatariaLogoSource), 23412 this.loadCabanaCanvasImage('/favicon.png'), 23413 ]); 23414 const canvas = document.createElement('canvas'); 23415 canvas.width = 1080; 23416 canvas.height = 1920; 23417 const ctx = canvas.getContext('2d'); 23418 const dark = snapshot.dark || '#002e5f'; 23419 const accent = snapshot.accent || '#0a5ca8'; 23420 const background = ctx.createLinearGradient(0, 0, 1080, 1920); 23421 background.addColorStop(0, dark); 23422 background.addColorStop(.42, accent); 23423 background.addColorStop(1, '#031a31'); 23424 ctx.fillStyle = background; 23425 ctx.fillRect(0, 0, 1080, 1920); 23426 ctx.fillStyle = 'rgba(255,255,255,.05)'; 23427 ctx.beginPath(); ctx.arc(930, 150, 360, 0, Math.PI * 2); ctx.fill(); 23428 ctx.beginPath(); ctx.arc(110, 1770, 430, 0, Math.PI * 2); ctx.fill(); 23429 ctx.fillStyle = 'rgba(255,255,255,.96)'; 23430 this.drawCabanaRoundRect(ctx, 54, 54, 972, 1812, 40); ctx.fill(); 23431 const topGradient = ctx.createLinearGradient(54, 54, 1026, 680); 23432 topGradient.addColorStop(0, dark); topGradient.addColorStop(1, accent); 23433 ctx.fillStyle = topGradient; 23434 this.drawCabanaRoundRect(ctx, 54, 54, 972, 700, 40); ctx.fill(); 23435 ctx.save(); ctx.beginPath(); this.drawCabanaRoundRect(ctx, 54, 54, 972, 700, 40); ctx.clip(); 23436 ctx.fillStyle = 'rgba(255,255,255,.06)'; ctx.beginPath(); ctx.arc(930, 80, 350, 0, Math.PI * 2); ctx.fill();
23436 ctx.restore(); 23437 ctx.fillStyle = 'rgba(255,255,255,.96)'; this.drawCabanaRoundRect(ctx, 78, 76, 278, 104, 22); ctx.fill(); this.drawCabanaRoundRect(ctx, 638, 76, 360, 104, 22); ctx.fill(); 23438 if (rosganLogo) this.drawCabanaImageContain(ctx, rosganLogo, 96, 92, 242, 72); 23439 else { ctx.fillStyle = dark; ctx.font = '900 34px Archivo, Arial, sans-serif'; ctx.fillText('ROSGAN', 102, 142); } 23440 if (mainLogo) this.drawCabanaImageContain(ctx, mainLogo, 664, 86, 308, 84); 23441 ctx.fillStyle = 'rgba(255,255,255,.72)'; ctx.font = '900 17px Archivo, Arial, sans-serif'; ctx.letterSpacing = '3px'; ctx.fillText('INTELIGENCIA COMERCIAL', 86, 222); ctx.letterSpacing = '0px'; 23442 ctx.fillStyle = '#fff'; ctx.font = '900 54px Archivo, Arial, sans-serif'; ctx.fillText('ESTADÃSTICAS', 86, 292); ctx.font = '900 49px Archivo, Arial, sans-serif'; ctx.fillText('DEL REMATE', 86, 346); 23443 ctx.fillStyle = 'rgba(255,255,255,.84)'; ctx.font = '800 22px Archivo, Arial, sans-serif'; this.wrapCabanaCanvasText(ctx, snapshot.title, 86, 391, 875, 28, 2); 23444 ctx.fillStyle = 'rgba(255,255,255,.12)'; this.drawCabanaRoundRect(ctx, 82, 454, 916, 82, 20); ctx.fill(); 23445 if (consignatariaLogo) this.drawCabanaImageTintContain(ctx, consignatariaLogo, 110, 467, 96, 56); 23446 ctx.fillStyle = 'rgba(255,255,255,.68)'; ctx.font = '900 12px Archivo, Arial, sans-serif'; ctx.fillText('CONSIGNATARIA', 228, 483); 23447 ctx.fillStyle = '#fff'; ctx.font = '900 22px Archivo, Arial, sans-serif'; ctx.fillText(consignatariaName, 228, 514); 23448 const formatNumber = (value) => Number(value || 0).toLocaleString('es-AR'); 23449 const storyKpis = [ 23450 ['APERTURAS DE LOTE', formatNumber(snapshot.totals.lotViews)], 23451 ['EVALUACIONES DE RP', formatNumber(snapshot.totals.rpViews)], 23452 ['VIDEOS REPRODUCIDOS', formatNumber(snapshot.totals.videoPlays)], 23453 ['CONTACTOS', formatNumber(snapshot.totals.contacts)], 23454 ]; 23455 storyKpis.forEach((item, index) => { 23456 const col = index % 2; const row = Math.floor(index / 2); 23457 const x = 82 + col * 462; const y = 566 + row * 92; 23458 ctx.fillStyle = 'rgba(255,255,255,.11)'; this.drawCabanaRoundRect(ctx, x, y, 442, 76, 17); ctx.fill(); 23459 ctx.fillStyle = 'rgba(255,255,255,.7)'; ctx.font = '900 11px Archivo, Arial, sans-serif'; ctx.fillText(item[0], x + 20, y + 24); 23460 ctx.fillStyle = '#fff'; ctx.font = '900 30px Archivo, Arial, sans-serif'; ctx.fillText(item[1], x + 20, y + 58); 23461 }); 23462 const panel = (x, y, width, height) => { ctx.save(); ctx.shadowColor = 'rgba(0,35,70,.12)'; ctx.shadowBlur = 18; ctx.shadowOffsetY = 7; ctx.fillStyle = '#fff'; this.drawCabanaRoundRect(ctx, x, y, width, height, 25); ctx.fill(); ctx.restore(); ctx.strokeStyle = 'rgba(0,46,95,.10)'; ctx.lineWidth = 2; this.drawCabanaRoundRect(ctx, x, y, width, height, 25); ctx.stroke(); }; 23463 panel(82, 786, 916, 218); 23464 ctx.fillStyle = accent; ctx.font = '900 13px Archivo, Arial, sans-serif'; ctx.fillText('PULSO COMERCIAL', 112, 825); 23465 ctx.fillStyle = '#102f50'; ctx.font = '900 25px Archivo, Arial, sans-serif'; ctx.fillText('Cómo responde la demanda', 112, 862); 23466 const pulse = [ 23467 ['CONVERSIÃN', snapshot.contactRateNumber.toLocaleString('es-AR', { maximumFractionDigits: 1 }) + '%'], 23468 ['ACTIVACIÃN VIDEO', snapshot.videoRateNumber.toLocaleString('es-AR', { maximumFractionDigits: 1 }) + '%'], 23469 ['TIEMPO PROMEDIO', this.formatCabanaVideoTime(snapshot.attentionSeconds)], 23470 ]; 23471 pulse.forEach((item, index) => { 23472 const x = 112 + index * 286; 23473 ctx.fillStyle = '#edf5fb'; this.drawCabanaRoundRect(ctx, x, 890, 264, 82, 16); ctx.fill(); 23474 ctx.fillStyle = '#71849a'; ctx.font = '900 10px Archivo, Arial, sans-serif'; ctx.fillText(item[0], x + 16, 916); 23475 ctx.fillStyle = dark; ctx.font = '900 27px Archivo, Arial, sans-serif'; ctx.fillText(item[1], x + 16, 955); 23476 }); 23477 panel(82, 1030, 916, 322); 23478 ctx.fillStyle = accent; ctx.font = '900 13px Archivo, Arial, sans-serif'; ctx.fillText('DEMANDA POR SEGMENTO', 112, 1068); 23479 ctx.fillStyle = '#102f50'; ctx.font = '900 25px Archivo, Arial, sans-serif'; ctx.fillText('CategorÃas con mayor interés', 112, 1104);
23480 const categoryRows = (snapshot.categoryPerformance.length ?snapshot.categoryPerformance :snapshot.categories).slice(0, 3); 23481 const maxCategory = Math.max(1, ...categoryRows.map((item) => Number(item.viewsPerRpNumber || item.published || 0))); 23482 categoryRows.forEach((item, index) => { 23483 const y = 1144 + index * 61; 23484 ctx.fillStyle = '#183a5d'; ctx.font = '900 15px Archivo, Arial, sans-serif'; const label = String(item.label || 'Sin categorÃa'); ctx.fillText(label.length > 43 ?label.slice(0, 40) + 'â¦' :label, 112, y + 18); 23485 const score = Number(item.viewsPerRpNumber || item.published || 0); 23486 ctx.fillStyle = '#e7eff6'; this.drawCabanaRoundRect(ctx, 112, y + 31, 690, 12, 6); ctx.fill(); 23487 ctx.fillStyle = accent; this.drawCabanaRoundRect(ctx, 112, y + 31, Math.max(24, 690 * score / maxCategory), 12, 6); ctx.fill(); 23488 ctx.textAlign = 'right'; ctx.fillStyle = '#60768c'; ctx.font = '900 14px Archivo, Arial, sans-serif'; ctx.fillText(item.viewsPerRpNumber != null ?(Number(item.viewsPerRpNumber).toLocaleString('es-AR', { maximumFractionDigits: 1 }) + ' vistas/RP') :(formatNumber(item.published) + ' RPs'), 962, y + 35); ctx.textAlign = 'left'; 23489 }); 23490 panel(82, 1378, 916, 354); 23491 ctx.fillStyle = accent; ctx.font = '900 13px Archivo, Arial, sans-serif'; ctx.fillText('OPORTUNIDADES PRIORITARIAS', 112, 1417); 23492 ctx.fillStyle = '#102f50'; ctx.font = '900 25px Archivo, Arial, sans-serif'; ctx.fillText('RPs que concentran atención', 112, 1453); 23493 const leaders = snapshot.opportunities.slice(0, 3); 23494 leaders.forEach((item, index) => { 23495 const y = 1490 + index * 72; 23496 ctx.fillStyle = index === 0 ?accent :'#eaf2f8'; this.drawCabanaRoundRect(ctx, 112, y, 42, 42, 12); ctx.fill(); 23497 ctx.fillStyle = index === 0 ?'#fff' :dark; ctx.font = '900 16px Archivo, Arial, sans-serif'; ctx.textAlign = 'center'; ctx.fillText(String(index + 1), 133, y + 27); ctx.textAlign = 'left'; 23498 ctx.fillStyle = '#102f50'; ctx.font = '900 17px Archivo, Arial, sans-serif'; ctx.fillText(`${item.rp} · ${item.lot}`, 174, y + 19); 23499 ctx.fillStyle = '#71849a'; ctx.font = '800 12px Archivo, Arial, sans-serif'; ctx.fillText(String(item.category || '').slice(0, 54), 174, y + 39); 23500 ctx.textAlign = 'right'; ctx.fillStyle = dark; ctx.font = '900 16px Archivo, Arial, sans-serif'; ctx.fillText(`${formatNumber(item.rpViewsNumber)} vistas · ${formatNumber(item.contactsNumber + item.sharesNumber)} intención`, 962, y + 27); ctx.textAlign = 'left'; 23501 if (index < leaders.length - 1) { ctx.strokeStyle = '#e3ebf2'; ctx.beginPath(); ctx.moveTo(112, y + 57); ctx.lineTo(962, y + 57); ctx.stroke(); } 23502 }); 23503 ctx.fillStyle = '#eef5fa'; this.drawCabanaRoundRect(ctx, 82, 1760, 916, 58, 16); ctx.fill(); 23504 ctx.fillStyle = '#506a82'; ctx.font = '800 13px Archivo, Arial, sans-serif'; const insight = snapshot.insights[0]?.title || 'Datos acumulados del remate'; this.wrapCabanaCanvasText(ctx, insight, 108, 1785, 846, 18, 2); 23505 ctx.fillStyle = '#17304d'; ctx.font = '900 28px Archivo, Arial, sans-serif'; const footer = 'rosgan.app'; const footerWidth = ctx.measureText(footer).width; const iconSize = faviconLogo ?42 :0; const footerX = 540 - (footerWidth + iconSize + (iconSize ?12 :0)) / 2; if (faviconLogo) this.drawCabanaImageContain(ctx, faviconLogo, footerX, 1833, iconSize, iconSize); ctx.fillText(footer, footerX + iconSize + (iconSize ?12 :0), 1865); 23506 return await new Promise((resolve, reject) => { 23507 try { canvas.toBlob((blob) => blob ?resolve(blob) :reject(new Error('canvas-empty')), 'image/png', .94); } 23508 catch (error) { reject(error); } 23509 }); 23510 } 23511 async downloadCabanaStatsStory() { 23512 if (this.state.cabanaStatsExportBusy) return; 23513 const snapshot = this.cabanaStatsExportSnapshot(); 23514 if (!snapshot) { this.showToast('TodavÃa no hay estadÃsticas para exportar.'); return; } 23515 this.setState({ cabanaStatsExportBusy: 'story' }); 23516 try { 23517 const blob = await this.createCabanaStatsStoryBlob(snapshot); 23518 this.downloadCabanaBlob(blob, this.cabanaExportFileBase(this.state.currentCabana || {}) + '-estadisticas-story-9x16.png'); 23519 this.showToast('Story de estadÃsticas descargada.'); 23520 } catch (error) { 23521 console.error('[Cabanas] No se pudo generar la story de estadÃsticas', error); 23522 this.showToast(error?.message || 'No se pudo generar la Story.'); 23523 } finally { 23524 this.setState({ cabanaStatsExportBusy: '' }); 23525 } 23526 } 23527 cabanaCatalogLocked(item = this.state.currentCabana) {
23528 return item?.visualization?.requerirRegistro === true && (!this.state.user || this.state.user.isAnonymous === true); 23529 } 23530 loginForCabanaCatalog() { 23531 this._initialPathname = window.location.pathname; 23532 this._initialSearch = window.location.search || ''; 23533 this._initialPathRoute = 'cabanaDetail'; 23534 this.setState({route:'login',routeLoading:false,authMode:'signin',authReady:true,publicAdvanceMode:'',authMessage:'Iniciá sesión o creá tu cuenta para ver los lotes de este remate.'}); 23535 } 23536 async fetchCabanaCatalogPrices(item = this.state.currentCabana) { 23537 const id=this.cabanaRemateId(item);if(!id || this._cabanaCatalogPricesBusy)return; 23538 this._cabanaCatalogPricesBusy=true; 23539 try{const data=await this.cabanaApiFetch('/api/cabana-prebid-prices/'+encodeURIComponent(id),{cache:'no-store'}); 23540 if(this.cabanaRemateId(this.state.currentCabana)===id)this.setState({cabanaCatalogPrices:{id,prices:data.prices||{}}}); 23541 }catch(error){console.warn('No se pudieron actualizar los importes del catálogo.');} 23542 finally{this._cabanaCatalogPricesBusy=false;} 23543 } 23544 async fetchCabanaLots(item) { 23545 if (this.cabanaCatalogLocked(item)) { 23546 this.setState({cabanaLots:[],cabanaLotsLoading:false,cabanaPublicDeepLoading:false}); 23547 if (this.state.route === 'cabanaDetail' && this.cabanaRemateId(this.state.currentCabana) === this.cabanaRemateId(item)) this.loginForCabanaCatalog(); 23548 return []; 23549 } 23550 const requestedPath = window.location.pathname; 23551 const id = this.cabanaRemateId(item); 23552 if (!id) { this.setState({ cabanaLots: [], cabanaLotsLoading: false }); return []; } 23553 this.setState({ cabanaLotsLoading: true }); 23554 try { 23555 this.fetchCabanaCatalogPrices(item); 23556 // Fetch independently, but keep installment terms ready before displaying prices. 23557 const [, data] = await Promise.all([ 23558 this.loadCabanaInstallments(item), 23559 this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(id) + '/lots/'), 23560 ]); 23561 const groups = this.listFrom(data, ['data', 'items', 'lots', 'lotes', 'groups']); 23562 const lots = this.normalizeCabanaAuctionLots(groups, item); 23563 if (window.location.pathname !== requestedPath || this.cabanaRemateId(this.state.currentCabana) !== id) return lots; 23564 // Lot responses must not replace newer auction details (participants and logos). 23565 this.setState({ cabanaLots: lots, cabanaLotsLoading: false, cabanasError: '' }, () => { 23566 this.hydrateVideoThumbnails(lots.flatMap((lot) => this.cabanaLotVideos(lot))); 23567 }); 23568 return lots; 23569 } catch (error) { 23570 if (window.location.pathname !== requestedPath) return []; 23571 console.error('[Cabanas] No se pudieron cargar los lotes del remate', id, error); 23572 const lots = Array.isArray(this.state.cabanaLots) ?this.state.cabanaLots : []; 23573 this.setState({ 23574 cabanaLots: lots, 23575 currentCabana: item, 23576 cabanaLotsLoading: false, 23577 cabanasError: error?.message || 'No se pudieron cargar los lotes del remate.', 23578 }); 23579 return lots; 23580 } 23581 } 23582 normalizeCabanaAuctionLots(groups, remate) { 23583 const participants = this.cabanaRemateParticipants(remate); 23584 const participantById = new Map(participants.map((cabana) => [String(cabana.id || cabana.name || ''), cabana])); 23585 const flat = []; 23586 (Array.isArray(groups) ?groups : []).forEach((group, groupIndex) => { 23587 const groupItems = Array.isArray(group?.items) ?group.items : (Array.isArray(group?.lots) ?group.lots : (Array.isArray(group?.lotes) ?group.lotes : (group && typeof group === 'object' ?[group] : []))); 23588 const cabanaRef = group?.cabanaId || group?.cabana?.id || group?.cabanaName || group?.cabanaNombre || group?.cabana || ''; 23589 const cabana = participantById.get(String(cabanaRef || '')) || participants.find((item) => this.consKey(item.name) === this.consKey(cabanaRef)) || null; 23590 groupItems.forEach((item, itemIndex) => { 23591 const values = item?.values && typeof item.values === 'object' ?item.values : {}; 23592 const subformValues = {}; 23593 (Array.isArray(item?.subforms) ?item.subforms : []).forEac
23593h((subform) => { 23594 const key = String(subform?.formKey || subform?.key || subform?.name || '').trim(); 23595 if (key) { 23596 const values = subform?.values || {}; 23597 subformValues[key] = values; 23598 subformValues[this.consKey(key)] = values; 23599 subformValues[this.consKey(key).replace(/-/g, '_')] = values; 23600 } 23601 }); 23602 const mergedValues = { ...values }; 23603 const primary = values.caravana || values.rp || values.lote || item?.lotCert || group?.lotCert || item?.id || (flat.length + 1); 23604 const itemCoverImage = item?.coverImage || item?.cover_image || null; 23605 const coverMedia = typeof itemCoverImage === 'string' ?{ url: itemCoverImage } : (itemCoverImage && typeof itemCoverImage === 'object' ?itemCoverImage : null); 23606 const groupCoverImage = group?.coverImage || group?.cover_image || null; 23607 const lotCoverMedia = typeof groupCoverImage === 'string' ?{ url: groupCoverImage } : (groupCoverImage && typeof groupCoverImage === 'object' ?groupCoverImage : null); 23608 const images = [ 23609 ...(coverMedia?.url || coverMedia?.downloadUrl ?[coverMedia] : []), 23610 ...this.listFrom(item, ['images', 'imagenes', 'photos', 'fotos', 'media']).map((media) => typeof media === 'string' ?{ url: media } : media), 23611 ]; 23612 const videos = this.listFrom(item, ['videos']).map((media) => typeof media === 'string' ?{ url: media } : media); 23613 flat.push({ 23614 ...item, 23615 id: group?.lotCert || group?.id || item?.lotCert || item?.id || (groupIndex + '-' + itemIndex), 23616 itemId: item?.id || (groupItems.length === 1 ?item?.cert : null), 23617 groupId: group?.id || group?.lotCert || null, 23618 groupDisplayOrder: Number(group?.displayOrder || groupIndex + 1), 23619 itemDisplayOrder: Number(item?.itemDisplayOrder || item?.rpDisplayOrder || item?.displayOrder || itemIndex + 1), 23620 commercial: item?.commercial || item?.comercial || null, 23621 groupItemCount: groupItems.length, 23622 prebidBaseOverride: group.prebidBaseOverride ?? item.prebidBaseOverride ?? null, 23623 prebidIncrementOverride: group.prebidIncrementOverride ?? item.prebidIncrementOverride ?? null, 23624 effectivePrebidBase: group.effectivePrebidBase ?? item.effectivePrebidBase ?? null, 23625 effectivePrebidIncrement: group.effectivePrebidIncrement ?? item.effectivePrebidIncrement ?? null, 23626 remateId: this.cabanaRemateId(remate), 23627 lotCert: group?.lotCert || item?.lotCert || group?.id || item?.id, 23628 categoryId: group?.categoryId || item?.categoryId || null, 23629 categoryDisplayOrder: this.cabanaCategoryDisplayOrder(group, this.cabanaCategoryDisplayOrder(item, groupIndex + 1)), 23630 categoryOrder: this.cabanaCategoryDisplayOrder(group, this.cabanaCategoryDisplayOrder(item, groupIndex + 1)), 23631 categoryName: group?.categoryName || item?.categoryName || 'CategorÃa', 23632 cabanaId: group?.cabanaId || item?.cabanaId || cabana?.id || null, 23633 cabanaNombre: group?.cabanaName || item?.cabanaName || cabana?.name || '', 23634 cabanaLogoDataUrl: cabana?.logoDataUrl || '', 23635 cabanaLogoWhiteDataUrl: cabana?.logoWhiteDataUrl || '', 23636 cabanaBrandColor: cabana?.brandColor || '', 23637 identifier: primary, 23638 values: mergedValues, 23639 subformValues, 23640 displayOrder: Number(group?.displayOrder || item?.displayOrder || groupIndex + 1), 23641 rp: values.rp || values.RP || primary, 23642 hba: values.hba, 23643 caravana: values.caravana, 23644 padre: values.padre, 23645 nacimiento: values.fecha_de_nacimiento, 23646 peso: values.peso_actual || values.peso || values.pesoPromedio, 23647 ce: values.ce, 23648 images, 23649 videos, 23650 coverImage: coverMedia, 23651 lotCoverImage: lotCoverMedia, 23652 thumbnailUrl: item?.thumbnailUrl || item?.coverImageUrl || coverMedia?.url || coverMedia?.downloadUrl || images[0]?.url || images[0]?.downloadUrl || '', 23653 lotThumbnailUrl: group?.thumbnailUrl || group?.coverImageUrl || lotCoverMedia?.url || lotCoverMedia?.downloadUrl || '', 23654 __videosLoaded: true, 23655 updatedAt: item?.updatedAt || group?.updatedAt || remate?.updatedAt || '', 23656 }); 23657 }); 23658 }); 23659 return this.cabanaSortLotsByCategoryOrder(flat); 23660 } 23661 openCabanaLot(lot, variants = null) { 23662 if (this.cabanaCatalogLocked()) { this.loginForCabanaCatalog(); return; } 23663 const lotId = this.cabanaLotId(lot); 23664 const selectedLot = { ...lot, videos: Array.isArray(lot?.videos) ?lot.videos : [], __videosLoaded: true }; 23665 this.syncLacassinaLotPath(lot); 23666 if (this.state.publicAdvanceMode !== 'lacassina') this.syncCabanaLocation(this.state.currentCabana, 'lotes/' + encodeURIComponent(this.cabanaPrebidCert(lot) || this.cabanaLotId(lot))); 23667 this.trackCabanaAnalyticsOnce('lot_view', lot, { metadata: { action: 'open_lot' } }); 23668 this.trackCabanaAnalyticsOnce('rp_view', lot, { metadata: { action: 'open_visible_rp' } }); 23669 this.startCabanaLotViewTimer(lot); 23670 this.setState({ 23671 route: 'cabanaLotDetail', 23672 currentCabanaLot: selectedLot, 23673 currentCabanaLotVariants: Array.isArray(variants) && variants.length ?variants : [lot], 23674 cbLotVideo: 0, 23675 cabanaLotVideosLoading: false, 23676 cabanaLotMediaReady: false, 23677 cabanaLotMediaFailed: false, 23678 cabanaLotVideoPlaying: false, 23679 cabanaLotVideoProgress: 0, 23680 cabanaLotVideoDuration: 0, 23681 cabanaLotVideoVolume: 0, 23682 cabanaLotVideoMuted: true, 23683 cabanaPrebids: [], 23684 cabanaPrebidMine: [], 23685 cabanaPrebidsError: '', 23686 cabanaPublicDeepLoading: false, 23687 }); 23688 window.scrollTo(0, 0); 23689 this.syncCabanaLotVideo(lotId); 23690 this.fetchCabanaPrebids(lot); 23691 } 23692 cabanaPrebidEnabled(remate = this.state.currentCabana) { 23693 return !!(remate?.prebid?.habilitarPreofertas || remate?.prebid?.habilitada || remate?.prebid?.enabled || remate?.prebid?.isActive || String(remate?.preoferta || '').toLowerCase() === 'activa'); 23694 } 23695 canDeleteCabanaPrebid() { 23696 return !!this.state.user && this.apiRoleIds(this.state.apiUser).some(role=>['admin','sysadmin'].includes(role)); 23697 } 23698 async deleteCabanaPrebid(bid) { 23699 if(!this.canDeleteCabanaPrebid()){this.showToast('No tenés permisos para eliminar preofertas');return;} 23700 if(this.state.cabanaPrebidDeleting)return; 23701 const remate=this.state.currentCabana,lot=this.state.currentCabanaLot; 23702 const auctionId=this.cabanaRemateId(remate),lotCert=bid.lotCert || this.cabanaPrebidCert(lot); 23703 if(!auctionId || !lotCert || !bid.id)return; 23704 if(!window.confirm('¿Eliminar esta preoferta? El sistema recalculará la oferta ganadora del lote.'))return; 23705 this.setState({cabanaPrebidDeleting:true}); 23706 try { 23707 await this.cabanaApiFetch('/api/cabin-auctions/'+encodeURIComponent(auctionId)+'/lots/'+encodeURIComponent(lotCert)+'/prebids/'+encodeURIComponent(bid.id),{method:'DELETE'}); 23708 }catch(error){ 23709 this.showToast(error.status===403?'No tenés permisos para eliminar preofertas':error.status===404?'L
23709a preoferta no existe o ya fue eliminada':error.status===400?(error.data?.detail || 'No se pudo eliminar la preoferta'):'No se pudo eliminar la preoferta'); 23710 this.setState({cabanaPrebidDeleting:false});return; 23711 } 23712 try { 23713 if(this.state.currentCabana===remate){ 23714 if(this.state.route==='cabanaPrebidPanel')await this.fetchCabanaPrebidPanel(remate); 23715 if(lot && this.state.currentCabanaLot===lot)await this.fetchCabanaPrebids(lot); 23716 } 23717 this.showToast('Preoferta eliminada correctamente'); 23718 }finally{this.setState({cabanaPrebidDeleting:false});} 23719 } 23720 async resolveCabanaPrebid(bid, action) { 23721 if (this.state.cabanaPrebidApprovalBusy || !['approve','reject'].includes(action)) return; 23722 const remate=this.state.currentCabana; 23723 this.setState({cabanaPrebidApprovalBusy:true}); 23724 try { 23725 await this.cabanaApiFetch('/api/cabin-auctions/'+encodeURIComponent(this.cabanaRemateId(remate))+'/lots/'+encodeURIComponent(bid.lotCert)+'/prebids/'+encodeURIComponent(bid.id)+'/'+action,{method:'POST'}); 23726 await this.fetchCabanaPrebidPanel(remate); 23727 if (this.state.currentCabanaLot) await this.fetchCabanaPrebids(); 23728 this.showToast(action === 'approve' ?'Preoferta aprobada.':'Preoferta rechazada.'); 23729 } catch(error) {this.showToast(error.status === 403 ?'No sos aprobador asignado ni administrador de este remate.':error?.data?.detail || error.message);} 23730 finally {this.setState({cabanaPrebidApprovalBusy:false});} 23731 } 23732 cabanaPrebidGrouped(remate = this.state.currentCabana) { 23733 return remate?.prebid?.agruparPreofertas === true; 23734 } 23735 cabanaPrebidNumber(value) { 23736 if (typeof value === 'number') return Number.isFinite(value) ?value : 0; 23737 const text = String(value ?? '').trim().replace(/\s/g, '').replace(/\$/g, ''); 23738 if (!text) return 0; 23739 const looksLikeThousands = /^-?\d{1,3}(\.\d{3})+$/.test(text); 23740 const normalized = text.includes(',') ?text.replace(/\./g, '').replace(',', '.') : (looksLikeThousands ?text.replace(/\./g, '') : text.replace(/[^\d.-]/g, '')); 23741 const number = Number(normalized); 23742 return Number.isFinite(number) ?number : 0; 23743 } 23744 cabanaPrebidCert(lot) { 23745 return String(lot?.lotCert || lot?.groupId || lot?.lotId || lot?.id || '').trim(); 23746 } 23747 cabanaPrebidAnimalCert(lot) { 23748 return String(lot?.cert || lot?.certificate || lot?.itemCert || lot?.values?.cert || lot?.values?.certificado || lot?.identifier || lot?.rp || lot?.values?.rp || '').trim(); 23749 } 23750 cabanaPrebidBase(lot = this.state.currentCabanaLot) { 23751 return window.CabanaPrebid.pricing(lot,this.state.currentCabana).base; 23752 } 23753 cabanaPrebidIncrement(remate = this.state.currentCabana) { 23754 return window.CabanaPrebid.pricing(this.state.currentCabanaLot,remate).increment; 23755 } 23756 cabanaPrebidIdentifier(lot) { 23757 return window.CabanaPrebid.identifier(lot || {},this.cabanaRemateSchema(this.state.currentCabana)); 23758 } 23759 cabanaPrebidMinimum(selected = this.state.cabanaPrebidForm?.selectedIdentifiers || []) { 23760 return window.CabanaPrebid.nextAmount(this.state.currentCabanaLot,this.state.currentCabana,this.state.cabanaPrebids,selected); 23761 } 23762 cabanaPrebidRows(data) { 23763 const root = data?.data ?? data; 23764 const raw = Array.isArray(root) ?root : (root?.prebids || root?.items || root?.results || root?.history || []); 23765 return (Array.isArray(raw) ?raw : []).map((item, index) => { 23766 const status = String(item?.status || item?.estado || 'Pendiente'); 23767 const certs = Array.isArray(item?.certs) ?item.certs : (Array.isArray(item?.selectedCerts) ?item.selectedCerts : []); 23768 return { 23769 ...item, 23770 targets: Array.isArray(item?.targets) ?item.targets : [], 23771 id: String(item?.id || item?.prebidId || index), 23772 amount: this.cabanaPrebidNumber(item?.amount ?? item?.monto), 23773 status, 23774 isWinner: Array.isArray(item?.targets) ?item.targets.some(t => t.isWinning === true) : /ganadora|winner|winning/i.test(status), 23775 certs: certs.map(String), 23776 cuitCuil: String(item?.cuitCuil || item?.cuit || item?.cuil || ''), 23777 buyerUid: String(item?.buyerUid || item?.userUid || item?.userId || item?.uid || item?.createdByUid || ''), 23778 buyerEmail: String(item?.buyerEmail || item?.userEmail || item?.email || ''), 23779 buyerName: String(item?.bidderName || item?.buyerName || item?.userName || item?.displayName || item?.nombre || ''), 23780 enComisionPara: String(item?.enComisionPara || item?.commissionFor || ''), 23781 createdAt: item?.createdAt || item?.fechaHora || item?.date || item?.timestamp || '', 23782 }; 23783 }).sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0)); 23784 } 23785 cabanaWinningPrebid(rows = this.state.cabanaPrebids) { 23786 const selected=this.state.cabanaPrebidModalOpen ?this.state.cabanaPrebidForm?.selectedIdentifiers || []:[this.cabanaPrebidIdentifier(this.state.currentCabanaLot).value]; 23787 return (Array.isArray(rows) ?rows : []).filter(item => window.CabanaPrebid.accepted(item) && (this.cabanaPrebidGrouped() || window.CabanaPrebid.identifiers(item).some(id=>selected.includes(id)))).sort((a,b)=>b.amount-a.amount)[0] || null; 23788 } 23789 cabanaPrebidOwnStorageKey(auctionId, lotCert) { 23790 const uid = String(this.state.apiUser?.uid || this.state.apiUser?.userId || this.state.apiUser?.id || this.state.user?.uid || '').trim(); 23791 if (!uid || !auctionId || !lotCert) return ''; 23792 return 'rosgan:cabana-prebids:mine:v1:' + encodeURIComponent(uid) + ':' + encodeURIComponent(String(auctionId)) + ':' + encodeURIComponent(String(lotCert)); 23793 } 23794 cabanaPrebidLocalMine(auctionId, lotCert) {
23795 const key = this.cabanaPrebidOwnStorageKey(auctionId, lotCert); 23796 if (!key || typeof window === 'undefined' || !window.localStorage) return []; 23797 try { 23798 const stored = JSON.parse(window.localStorage.getItem(key) || '[]'); 23799 return Array.isArray(stored) ?stored.filter((item) => this.cabanaPrebidNumber(item?.amount) > 0) : []; 23800 } catch (_) { 23801 return []; 23802 } 23803 } 23804 persistCabanaPrebidOwnOffer(auctionId, lotCert, offer) { 23805 const key = this.cabanaPrebidOwnStorageKey(auctionId, lotCert); 23806 if (!key || typeof window === 'undefined' || !window.localStorage || !offer) return; 23807 try { 23808 const current = this.cabanaPrebidLocalMine(auctionId, lotCert); 23809 const amount = this.cabanaPrebidNumber(offer.amount); 23810 const id = String(offer.id || ''); 23811 const withoutDuplicate = current.filter((item) => { 23812 if (id && String(item?.id || '') === id) return false; 23813 return this.cabanaPrebidNumber(item?.amount) !== amount; 23814 }); 23815 window.localStorage.setItem(key, JSON.stringify([...withoutDuplicate, { ...offer, amount }].slice(-50))); 23816 } catch (_) {} 23817 } 23818 cabanaPrebidPanelLots(data = this.state.cabanaPrebidPanelData) { 23819 const panelData = data || {}; 23820 const items = Array.isArray(panelData.items) ?panelData.items : []; 23821 const providedLots = Array.isArray(panelData.lots) ?panelData.lots : []; 23822 const lotsByKey = new Map(); 23823 const ensureLot = (source = {}) => { 23824 const key = String(source.lotCert || source.cert || source.id || '').trim(); 23825 if (!key) return null; 23826 if (!lotsByKey.has(key)) lotsByKey.set(key, { 23827 key, 23828 label: source.lotLabel || source.label || source.name || ('Lote ' + key), 23829 category: source.category || source.categoryName || '', 23830 cabanaName: source.cabanaName || source.cabana?.name || '', 23831 offerCount: Number(source.offerCount || 0), 23832 currentAmountValue: Number(source.currentAmount || 0), 23833 bidderKeys: new Set(), 23834 }); 23835 return lotsByKey.get(key); 23836 }; 23837 providedLots.forEach(ensureLot); 23838 items.forEach((item) => { 23839 const lot = ensureLot(item); 23840 if (!lot) return; 23841 lot.label = item?.lotLabel || lot.label; 23842 lot.category = item?.category || lot.category; 23843 lot.cabanaName = item?.cabanaName || lot.cabanaName; 23844 lot.offerCount += providedLots.length ?0 : 1; 23845 if (item?.isCurrent) lot.currentAmountValue = Number(item?.amount || 0); 23846 const bidderKey = String(item?.buyer?.uid || item?.buyer?.email || '').trim(); 23847 if (bidderKey) lot.bidderKeys.add(bidderKey); 23848 }); 23849 return Array.from(lotsByKey.values()).map((lot) => ({ 23850 ...lot, 23851 meta: [lot.category, lot.cabanaName].filter(Boolean).join(' · ') || 'Sin datos adicionales', 23852 currentAmount: lot.currentAmountValue > 0 ?this.fmtARS(lot.currentAmountValue) : 'Sin ofertas', 23853 offerLabel: lot.offerCount === 1 ?'1 preoferta' : lot.offerCount.toLocaleString('es-AR') + ' preofertas', 23854 bidderCount: lot.bidderKeys.size, 23855 })).sort((a, b) => String(a.label).localeCompare(String(b.label), 'es', { numeric: true, sensitivity: 'base' })); 23856 } 23857 syncCabanaPrebidPanelLotQuery(lotCert = this.state.cabanaPrebidPanelLot) { 23858 if (this.state.publicAdvanceMode || this.state.route !== 'cabanaPrebidPanel') return; 23859 const key = String(lotCert || '').trim(); 23860 const base = this.cabanaPath(this.state.currentCabana, 'preofertas'); 23861 const path = base + (key ?'?lote=' + encodeURIComponent(key) : ''); 23862 if (window.location.pathname + window.location.search === path) return; 23863 try { window.history.replaceState({}, '', path); } catch {} 23864 } 23865 selectCabanaPrebidPanelLot(lotCert) { 23866 const key = String(lotCert || '').trim(); 23867 if (!key || key === String(this.state.cabanaPrebidPanelLot || '')) return; 23868 this.setState({ cabanaPrebidPanelLot: key, cabanaPrebidPanelQuery: '', cabanaPrebidPanelStatus: 'all' }, () => {this.syncCabanaPrebidPanelLotQuery(key);this.fetchCabanaPrebidPanel();}); 23869 } 23870 async checkCabanaPrebidSummary(remate = this.state.currentCabana) { 23871 const uid=this.state.user?.uid, id=this.cabanaRemateId(remate); 23872 this.setState({cabanaPrebidSummaryAccess:null}); 23873 if(!uid || !id)return; 23874 try { const result=await this.cabanaApiFetch('/api/cabana-prebid-reports/'+encodeURIComponent(id)+'?access=1',{cache:'no-store'}); 23875 if(this.state.user?.uid===uid && this.cabanaRemateId(this.state.currentCabana)===id && result.allowed)this.setState({cabanaPrebidSummaryAccess:{uid,id}}); 23876 } catch {} 23877 } 23878 openCabanaPrebidSummary() { 23879 const id=this.cabanaRemateId(this.state.currentCabana); 23880 if(!id || !this.state.user)return; 23881 window.CabanaPrebidReport.open({ 23882 identity:()=>this.state.user?.uid, 23883 load:()=>this.cabanaApiFetch('/api/cabana-prebid-reports/'+encodeURIComponent(id),{cache:'no-store'}), 23884 excelJs:()=>this.ensureCabanaExcelJs(), download:(blob,name)=>this.downloadCabanaBlob(blob,name) 23885 }); 23886 } 23887 async fetchCabanaPrebidPanel(remate = this.state.currentCabana, options = {}) { 23888 const auctionId = this.cabanaRemateId(remate); 23889 if (!auctionId) return null; 23890 if (this._cabanaPrebidPanelRefreshBusy) {this._cabanaPrebidPanelReload = true;return null;} 23891 const requestSelection = String(this.state.cabanaPrebidPanelLot || ''); 23892 const probe = options.probe === true; 23893 const silent = options.silent === true || probe; 23894 this._cabanaPrebidPanelRefreshBusy = true; 23895 if (!silent) this.setState({ cabanaPrebidPanelLoading: true, cabanaPrebidPanelError: '' }); 23896 try { 23897 const uid=this.state.user?.uid; 23898 const base='/api/cabin-auctions/'+encodeURIComponent(auctionId); 23899 const pending=this.cabanaPrebidRows(await this.cabanaApiFetch(base+'/prebids/pending',{cache:'no-store'})); 23900 const response=await this.cabanaApiFetch(base+'/lots',{cache:'no-store'}); 23901 const lots=this.normalizeCabanaAuctionLots(this.listFrom(response,['data','items','lots','lotes','groups']),remate); 23902 const lotOptions=[...new Map(lots.map(l => [this.cabanaPrebidCert(l),{lotCert:this.cabanaPrebidCert(l),lotLabel:'Lote '+this.cabanaLotNumber(l),categoryName:l.categoryName}])).values()]; 23903 const requested=String(this.state.cabanaPrebidPanelLot || new URLSearchParams(window.location.search).get('lote') || pending[0]?.lotCert || lotOptions[0]?.lotCert || ''); 23904 const history=requested ?this.cabanaPrebidRows(await this.cabanaApiFetch(base+'/lots/'+encodeURIComponent(requested)+'/prebids/manage',{cache:'no-store'})):[]; 23905 if (this.state.user?.uid !== uid) return null; 23906 if (String(this.state.cabanaPrebidPanelLot || '') !== requestSelection) {this._cabanaPrebidPanelReload=true;return null;} 23907 const combined=[...new Map([...pending,...history].map(b => [b.id,b])).values()]; 23908 const data={updatedAt:new Date().toISOString(),lots:lotOptions,pendingCount:pending.length,items:combined.map(b => ({...b,isCurrent:b.targets.some(t => t.isWinning),buyer:{uid:b.buyerUid,name:b.buyerName,email:b.buyerEmail,cuitCuil:b.cuitCuil},animals:b.targets.map(t => ({cert:t.characteristicIdentifierValue ?? t.cert,identifierName:t.characteristicIdentifierName || 'Cert.',fields:[{label:t.characteristicIdentifierName || 'Cert.',value:t.isWinning ?'Ganad
23908ora':b.status === 'Ganadora' ?'Superada':b.status}]}))}))}; 23909 if (['cabanaDetail', 'cabanaAuditor', 'cabanaPrebidPanel'].includes(this.state.route) && this.cabanaRemateId(this.state.currentCabana) === String(auctionId)) { 23910 const lotOptions = this.cabanaPrebidPanelLots(data); 23911 const queryLot = requested; 23912 const currentLot = queryLot || String(this.state.cabanaPrebidPanelLot || ''); 23913 const selectedLot = lotOptions.some((lot) => lot.key === currentLot) ?currentLot : (lotOptions[0]?.key || ''); 23914 this.setState({ cabanaPrebidPanelAllowed: true, cabanaPrebidPanelData: data || null, cabanaPrebidPanelLoading: false, cabanaPrebidPanelError: '', cabanaPrebidPanelLot: selectedLot }, () => this.syncCabanaPrebidPanelLotQuery(selectedLot)); 23915 } 23916 return data; 23917 } catch (error) { 23918 const denied = error?.status === 401 || error?.status === 403; 23919 if (['cabanaDetail', 'cabanaAuditor', 'cabanaPrebidPanel'].includes(this.state.route) && this.cabanaRemateId(this.state.currentCabana) === String(auctionId)) {
23920 const panelPageDenied = denied && this.state.route === 'cabanaPrebidPanel'; 23921 if (panelPageDenied) this.syncCabanaLocation(remate, '', { replace: true }); 23922 this.setState({ 23923 route: panelPageDenied ?'cabanaDetail' : this.state.route, 23924 cabanaPrebidPanelAllowed: denied ?false : this.state.cabanaPrebidPanelAllowed, 23925 ...(denied ?{cabanaPrebidPanelData:null}:{}), 23926 cabanaPrebidPanelOpen: denied ?false : this.state.cabanaPrebidPanelOpen, 23927 cabanaPrebidPanelLoading: false, 23928 cabanaPrebidPanelError: denied || probe ?'' : (error?.data?.message || error?.message || 'No se pudo actualizar el panel de preofertas.'), 23929 }); 23930 if (panelPageDenied) this.showToast('No tenés permiso para ver el panel privado de preofertas.'); 23931 } 23932 return null; 23933 } finally { 23934 this._cabanaPrebidPanelRefreshBusy = false; 23935 if (this._cabanaPrebidPanelReload) {this._cabanaPrebidPanelReload=false;this.fetchCabanaPrebidPanel(this.state.currentCabana,{silent:true});} 23936 } 23937 } 23938 toggleCabanaPrebidPanel() { 23939 if (!this.state.cabanaPrebidPanelAllowed) return; 23940 this.openCabanaPrebidPanelPage(this.state.currentCabana); 23941 } 23942 openCabanaPrebidPanelPage(item = this.state.currentCabana, syncPath = true, requestedLotOverride = '') { 23943 item = this.normalizeCabanaAuctionRecord(item); 23944 if (!item) return; 23945 const requestedLot = syncPath ?'' : (String(requestedLotOverride || '').trim() || String(new URLSearchParams(window.location.search || '').get('lote') || '').trim()); 23946 if (syncPath) this.syncCabanaLocation(item, 'preofertas'); 23947 this.setState({ 23948 route: 'cabanaPrebidPanel', currentCabana: item, cabanaPrebidPanelOpen: true, 23949 cabanaPrebidPanelLoading: true, cabanaPrebidPanelError: '', cabanaPrebidPanelQuery: '', cabanaPrebidPanelStatus: 'all', cabanaPrebidPanelLot: requestedLot, 23950 }, () => this.fetchCabanaPrebidPanel(item)); 23951 this.ensureCabanaBrand(item); 23952 window.scrollTo(0, 0); 23953 } 23954 closeCabanaPrebidPanelPage() { 23955 const item = this.state.currentCabana; 23956 this.syncCabanaLocation(item); 23957 this.setState({ route: 'cabanaDetail', cabanaPrebidPanelOpen: false, cabanaPrebidPanelError: '' }); 23958 window.scrollTo(0, 0); 23959 } 23960 syncCabanaPrebidEvents() { 23961 const active=this.state.cabanaPrebidModalOpen && this.state.route==='cabanaLotDetail'; 23962 const key=active ? [this.cabanaRemateId(this.state.currentCabana),this.cabanaPrebidCert(this.state.currentCabanaLot)].map(encodeURIComponent).join('/') : ''; 23963 if(this._cabanaPrebidEventKey===key)return; 23964 this._cabanaPrebidEvents?.close();this._cabanaPrebidEvents=null;this._cabanaPrebidEventKey=key; 23965 if(!key || typeof EventSource==='undefined')return; 23966 const source=new EventSource('/api/cabana-prebid-events/'+key);this._cabanaPrebidEvents=source; 23967 source.addEventListener('change',()=>{ 23968 if(this._cabanaPrebidEvents!==source)return; 23969 this._cabanaPrebidRealtimePending=true;this.refreshCabanaPrebidRealtime(); 23970 }); 23971 } 23972 cabanaPrebidLiveForm(rows) { 23973 if(!this.state.cabanaPrebidModalOpen || this.state.cabanaPrebidSubmitting)return {}; 23974 const form=this.state.cabanaPrebidForm || {}; 23975 const minimum=window.CabanaPrebid.nextAmount(this.state.currentCabanaLot,this.state.currentCabana,rows,form.selectedIdentifiers || []); 23976 if(minimum==null || Number(form.amount)>=minimum)return {}; 23977 return {cabanaPrebidForm:{...form,amount:minimum}}; 23978 } 23979 refreshCabanaPrebidRealtime() { 23980 this.syncCabanaPrebidEvents(); 23981 if (typeof document !== 'undefined' && document.hidden) return; 23982 if (this.state.route === 'cabanaLotDetail' && this.state.currentCabanaLot && this.cabanaPrebidEnabled() && !this.state.cabanaPrebidSubmitting && !this._cabanaPrebidRealtimeBusy) { 23983 this._cabanaPrebidRealtimeBusy = true;this._cabanaPrebidRealtimePending=false; 23984 Promise.resolve(this.fetchCabanaPrebids(this.state.currentCabanaLot, { loading: false })) 23985 .finally(() => { this._cabanaPrebidRealtimeBusy = false;if(this._cabanaPrebidRealtimePending)this.refreshCabanaPrebidRealtime(); }); 23986 } 23987 if (this.state.route === 'cabanaDetail') this.fetchCabanaCatalogPrices(); 23988 if (this.state.route === 'cabanaPrebidPanel' && this.state.cabanaPrebidPanelAllowed) { 23989 this.fetchCabanaPrebidPanel(this.state.currentCabana, { silent: true }); 23990 } 23991 } 23992 async fetchCabanaPrebids(lot = this.state.currentCabanaLot, options = {}) { 23993 const apply = options.apply !== false; 23994 const showLoading = options.loading !== false; 23995 if (!lot || !this.cabanaPrebidEnabled()) {
23996 if (apply && (this.state.cabanaPrebids.length || this.state.cabanaPrebidsError)) this.setState({ cabanaPrebids: [], cabanaPrebidsError: '', cabanaPrebidsLoading: false }); 23997 return []; 23998 } 23999 const auctionId = this.cabanaRemateId(this.state.currentCabana); 24000 const lotCert = this.cabanaPrebidCert(lot); 24001 if (!auctionId || !lotCert) return []; 24002 if (apply && showLoading) this.setState({ cabanaPrebidsLoading: true, cabanaPrebidsError: '' }); 24003 const requestedUid=String(this.state.user?.uid || ''); 24004 try { 24005 const [data, mineData] = await Promise.all([ 24006 this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert) + '/prebids', { cache: 'no-store' }), 24007 this.apiFetch('/api/notifications/cabana-prebid/mine?auctionId=' + encodeURIComponent(auctionId) + '&lotCert=' + encodeURIComponent(lotCert), { cache: 'no-store' }) 24008 .catch(() => ({ items: [] })), 24009 ]); 24010 if(String(this.state.user?.uid || '')!==requestedUid)return []; 24011 const currentUid = String(this.state.apiUser?.uid || this.state.apiUser?.userId || this.state.apiUser?.id || this.state.user?.uid || ''); 24012 const currentEmail = String(this.state.apiUser?.email || this.state.user?.email || '').trim().toLowerCase(); 24013 const remoteMine = Array.isArray(mineData?.items) ?mineData.items : []; 24014 const localMine = this.cabanaPrebidLocalMine(auctionId, lotCert); 24015 const knownMine = [...localMine, ...remoteMine]; 24016 const ownIds = new Set(knownMine.map((item) => String(item?.id || '')).filter(Boolean)); 24017 const rows = this.cabanaPrebidRows(data).map((row) => ({ 24018 ...row, 24019 isOwn: !!( 24020 (currentUid && row.buyerUid && row.buyerUid === currentUid) 24021 || (currentEmail && row.buyerEmail && row.buyerEmail.trim().toLowerCase() === currentEmail) 24022 || ownIds.has(String(row.id || '')) 24023 ), 24024 })); 24025 if (apply && this.cabanaPrebidCert(this.state.currentCabanaLot) === String(lotCert)) this.setState({ ...this.cabanaPrebidLiveForm(rows), cabanaPrebids: rows, cabanaPrebidMine: knownMine, cabanaPrebidsLoading: false, cabanaPrebidsError: '' }); 24026 return rows; 24027 } catch (error) { 24028 const message = error?.data?.detail || error?.message || 'No se pudieron cargar las preofertas.'; 24029 if (apply && this.cabanaPrebidCert(this.state.currentCabanaLot) === String(lotCert)) this.setState({ cabanaPrebids: [], cabanaPrebidsLoading: false, cabanaPrebidsError: message }); 24030 return []; 24031 } 24032 } 24033 cabanaPrebidIdentifierLabel(value) { 24034 const variants = this.state.currentCabanaLotVariants?.length ?this.state.currentCabanaLotVariants : [this.state.currentCabanaLot]; 24035 const identifier = variants.map(lot => this.cabanaPrebidIdentifier(lot)).find(item => String(item.value) === String(value)); 24036 return [identifier?.name || 'Cert.', value].join(' '); 24037 } 24038 cabanaPrebidAvailableCerts() { 24039 const variants = Array.isArray(this.state.currentCabanaLotVariants) && this.state.currentCabanaLotVariants.length ?this.state.currentCabanaLotVariants : [this.state.currentCabanaLot]; 24040 return [...new Set(variants.map((lot) => this.cabanaPrebidIdentifier(lot).value).filter(Boolean))]; 24041 } 24042 async openCabanaPrebid() { 24043 if (!this.cabanaPrebidEnabled() || this._openingCabanaPrebid) return; 24044 const windowState = window.CabanaPrebid.windowState(this.state.currentCabana); 24045 if (!windowState.active) { this.showToast(windowState.upcoming ? 'Las preofertas todavÃa no comenzaron.' : 'Las preofertas están cerradas.'); return; } 24046 if (!this.state.user) { this.showToast('Iniciá sesión para preofertar.'); return; } 24047 const uid = this.state.user.uid, lot = this.state.currentCabanaLot; 24048 this._openingCabanaPrebid = true; 24049 try { 24050 await this.loadCabanaInstallments(this.state.currentCabana); 24051 const response = await this.cabanaApiFetch('/api/users/me'); 24052 if (this.state.user?.uid !== uid || this.state.currentCabanaLot !== lot) return; 24053 const profile = response?.data ?? response; 24054 await this.fetchCabanaPrebids(lot); 24055 if (this.state.user?.uid !== uid || this.state.currentCabanaLot !== lot) return; 24056 const selectedIdentifiers = [this.cabanaPrebidIdentifier(lot).value || this.cabanaPrebidAvailableCert
24056s()[0]].filter(Boolean); 24057 const amount = this.cabanaPrebidMinimum(selectedIdentifiers); 24058 if (amount == null) { this.showToast('Este lote todavÃa no tiene base e incremento configurados.'); return; } 24059 this.setState({cabanaPrebidCuitCheck:null, cabanaPrebidProfile:profile, cabanaPrebidModalOpen:true, cabanaPrebidMessage:null, cabanaPrebidRolling:false, 24060 cabanaPrebidForm:{cuitCuil:'',amount,selectedIdentifiers,enComision:false,enComisionPara:''}}, () => { this.syncCabanaPrebidEvents();if (profile?.cuitCuil) this.checkCabanaPrebidCuit(profile.cuitCuil); }); 24061 } catch(error) { this.showToast(error?.data?.detail || error.message || 'No se pudo consultar tu perfil.'); } 24062 finally { this._openingCabanaPrebid = false; } 24063 } 24064 closeCabanaPrebid() { 24065 if (this.state.cabanaPrebidSubmitting) return; 24066 this._cabanaPrebidCuitToken = (this._cabanaPrebidCuitToken || 0) + 1; 24067 this.setState({ cabanaPrebidModalOpen: false, cabanaPrebidCuitCheck:null, cabanaPrebidMessage: null },()=>this.syncCabanaPrebidEvents()); 24068 } 24069 setCabanaPrebidField(key, value) { 24070 if (this.state.cabanaPrebidSubmitting) return; 24071 this.setState({ cabanaPrebidForm: { ...(this.state.cabanaPrebidForm || {}), [key]: value }, cabanaPrebidMessage: null }); 24072 if (key === 'cuitCuil') this.checkCabanaPrebidCuit(value); 24073 } 24074 toggleCabanaPrebidCert(cert) { 24075 if (!this.cabanaPrebidAvailableCerts().includes(cert)) return; 24076 const form = this.state.cabanaPrebidForm || {}; 24077 const selected = new Set(form.selectedIdentifiers || []); 24078 if (this.state.currentCabana?.prebid?.preofertaIndividual === true) { selected.clear(); selected.add(cert); } 24079 else if (selected.has(cert)) selected.delete(cert); else selected.add(cert); 24080 const selectedIdentifiers = [...selected]; 24081 this.setState({cabanaPrebidForm:{...form,selectedIdentifiers,amount:this.cabanaPrebidMinimum(selectedIdentifiers)},cabanaPrebidMessage:null}); 24082 } 24083 adjustCabanaPrebidAmount(direction) { 24084 const form = this.state.cabanaPrebidForm || {}; 24085 const increment = this.cabanaPrebidIncrement(); 24086 const winner = this.cabanaWinningPrebid(); 24087 const minimum = this.cabanaPrebidMinimum(); 24088 if (minimum == null) { this.showToast("Configurá la base y el incremento del lote."); return; } 24089 const current = Math.max(minimum, this.cabanaPrebidNumber(form.amount)); 24090 const amount = Math.max(minimum, current + Number(direction || 0) * increment); 24091 if (this._cabanaPrebidRollTimer) clearTimeout(this._cabanaPrebidRollTimer); 24092 this.setState({ 24093 cabanaPrebidForm: { ...form, amount }, 24094 cabanaPrebidMessage: null, 24095 cabanaPrebidRolling: true, 24096 }); 24097 this._cabanaPrebidRollTimer = setTimeout(() => { 24098 this._cabanaPrebidRollTimer = null; 24099 this.setState({ cabanaPrebidRolling: false }); 24100 }, 760); 24101 } 24102 cabanaPrebidCuitReady() { 24103 const cuit = String(this.state.cabanaPrebidProfile?.cuitCuil || this.state.cabanaPrebidForm?.cuitCuil || '').replace(/\D/g, ''); 24104 const check = this.state.cabanaPrebidCuitCheck; 24105 return !!(check?.status === 'valid' && check.cuit === cuit && check.uid === this.state.user?.uid); 24106 } 24107 async checkCabanaPrebidCuit(value) { 24108 const cuit = String(value || '').replace(/\D/g, ''); 24109 const token = this._cabanaPrebidCuitToken = (this._cabanaPrebidCuitToken || 0) + 1; 24110 const uid = this.state.user?.uid; 24111 if (cuit.length !== 11) { this.setState({cabanaPrebidCuitCheck:null}); return; } 24112 this.setState({cabanaPrebidCuitCheck:{cuit,uid,status:'loading',text:'Validando CUIT/CUIL en ARCAâ¦'}}); 24113 try { 24114 const result = await this.validateCabanaPrebidCuit(cuit); 24115 if (token !== this._cabanaPrebidCuitToken || uid !== this.state.user?.uid || !this.state.cabanaPrebidModalOpen) return; 24116 this.setState({cabanaPrebidCuitCheck:{cuit,uid,status:'valid',text:'Validado en ARCA: ' + result.name}}); 24117 } catch (error) { 24118 if (token !== this._cabanaPrebidCuitToken || uid !== this.state.user?.uid || !this.state.cabanaPrebidModalOpen) return; 24119 this.setState({cabanaPrebidCuitCheck:{cuit,uid,status:'error',text:error?.data?.detail || error?.message || 'No se pudo validar en ARCA. Reintentá.'}}); 24120 } 24121 } 24122 async validateCabanaPrebidCuit(value) { 24123 const cuit = String(value || '').replace(/\D/g, ''); 24124 if (cuit.length !== 11) throw new Error('Ingresá un CUIT/CUIL válido de 11 dÃgitos.'); 24125 const response = await this.apiFetch(this.roslotCuitLookupEndpoint(cuit)); 24126 const data = response?.data ?? response ?? {}; 24127 if (data.error) throw new Error(String(data.error)); 24128 const record = data.comitente || data.item || data; 24129 const source = String(data.source || record.source || '').toLowerCase(); 24130 const name = String(record.nombreFiscal || record.fiscalName || record.razonSocial || record.name || record.nombre || '').trim(); 24131 const returnedCuit = String(record.cuitCuil || record.cuitNormalized || record.cuit || '').replace(/\D/g, ''); 24132 if (data.found === false || !name) throw new Error('ARCA no encontró este CUIT/CUIL. Revisá el número ingresado.'); 24133 if (source === 'local') throw new Error('El CUIT/CUIL figura en los registros locales, pero no se pudo validar en ARCA. Reintentá la consulta.'); 24134 if (returnedCuit && returnedCuit !== cuit) throw new Error('El CUIT/CUIL devuelto por ARCA no coincide con el consultado.'); 24135 return { cuit, name }; 24136 } 24137 async submitCabanaPrebid() { 24138 const windowState = window.CabanaPrebid.windowState(this.state.currentCabana); 24139 if (!windowState.active) { this.setState({cabanaPrebidMessage:{ok:false,text:windowState.upcoming ? 'Las preofertas todavÃa no comenzaron.' : 'Las preofertas están cerradas.'}}); return; } 24140 if (!this.cabanaPrebidCuitReady()) { this.setState({cabanaPrebidMessage:{ok:false,text:'Validá el CUIT/CUIL en ARCA antes de confirmar la preoferta.'}}); return; } 24141 if (this.state.cabanaPrebidSubmitting) return; 24142 const form = this.state.cabanaPrebidForm || {}; 24143 const digits = String(this.state.cabanaPrebidProfile?.cuitCuil || form.cuitCuil || '').replace(/\D/g, ''); 24144 if (digits.length !== 11) { this.setState({ cabanaPrebidMessage: { ok: false, text: 'Ingresá un CUIT/CUIL válido de 11 dÃgitos.' } }); return; } 24145 if (!this.state.user || !this.state.cabanaPrebidProfile) {this.showToast('Volvé a abrir el formulario para verificar tu perfil.');return;} 24146 const selectedCerts = (form.selectedIdentifiers || []).map(String); 24147 if (!window.CabanaPrebid.validateSelection(selectedCerts,this.cabanaPrebidAvailableCert
24147s(),this.state.currentCabana?.prebid?.preofertaIndividual === true)) { this.setState({cabanaPrebidMessage:{ok:false,text:'Seleccioná los animales del lote sin duplicados; en modo individual, elegà uno solo.'}}); return; } 24148 if (!selectedCerts.length) { this.setState({ cabanaPrebidMessage: { ok: false, text: 'Seleccioná al menos un animal.' } }); return; } 24149 if (form.enComision && !String(form.enComisionPara || '').trim()) { this.setState({ cabanaPrebidMessage: { ok: false, text: 'Indicá para quién realizás la preoferta.' } }); return; } 24150 const amount = this.cabanaPrebidNumber(form.amount); 24151 const winner = this.cabanaWinningPrebid(); 24152 const increment = this.cabanaPrebidIncrement(); 24153 const minimum = this.cabanaPrebidMinimum(); 24154 if (minimum == null) { this.showToast("Configurá la base y el incremento del lote."); return; } 24155 if (amount < minimum) { this.setState({ cabanaPrebidMessage: { ok: false, text: 'El monto mÃnimo para esta preoferta es ' + this.fmtARS(minimum) + '.' } }); return; } 24156 if ((amount - minimum) % increment !== 0) { this.setState({ cabanaPrebidMessage: { ok: false, text: 'El monto debe avanzar en incrementos de ' + this.fmtARS(increment) + '.' } }); return; } 24157 const auctionId = this.cabanaRemateId(this.state.currentCabana); 24158 const lotCert = this.cabanaPrebidCert(this.state.currentCabanaLot); 24159 if (!auctionId || !lotCert) { this.setState({ cabanaPrebidMessage: { ok: false, text: 'No se pudo identificar el lote.' } }); return; } 24160 this.setState({ cabanaPrebidSubmitting: true, cabanaPrebidMessage: null }); 24161 try { 24162 this.setState({ cabanaPrebidMessage: { ok: true, text: 'Validando CUIT/CUIL en ARCAâ¦' } }); 24163 if (!this.cabanaPrebidCuitReady()) throw new Error('Volvé a validar el CUIT/CUIL en ARCA.'); 24164 if (String(this.state.cabanaPrebidProfile?.cuitCuil || this.state.cabanaPrebidForm?.cuitCuil || '').replace(/\D/g, '') !== digits) throw new Error('El CUIT/CUIL cambió durante la consulta. Volvé a confirmar la preoferta.'); 24165 this.setState({ cabanaPrebidMessage: null }); 24166 const result = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotCert) + '/prebids', { 24167 method: 'POST', 24168 body: JSON.stringify({ cuitCuil: this.state.cabanaPrebidProfile?.cuitCuil ?null : this.cuitInputDisplay(form.cuitCuil), amount, selectedCerts, comisionFor: null, enComisionPara: form.enComision ?String(form.enComisionPara || '').trim() : null }), 24169 }); 24170 const resultRoot = result?.data ?? result; 24171 const status = String(resultRoot?.status || resultRoot?.estado || 'Registrada'); 24172 const pending = /pendiente/i.test(status); 24173 const currentLot = this.state.currentCabanaLot; 24174 const currentAuction = this.state.currentCabana || {}; 24175 if (this._cabanaPrebidSuccessTimer) clearTimeout(this._cabanaPrebidSuccessTimer); 24176 this.setState({ 24177 cabanaPrebidSubmitting: false, 24178 cabanaPrebidModalOpen: false, 24179 cabanaPrebidMessage: null, 24180 cabanaPrebidRolling: false, 24181 cabanaPrebidSuccessFx: { 24182 eyebrow: pending ?'PREOFERTA RECIBIDA' : 'PREOFERTA REALIZADA', 24183 title: pending ?'¡Preoferta enviada!' : '¡Tu preoferta se realizó con éxito!', 24184 amount: this.fmtARS(amount), 24185 message: pending ?'Quedó pendiente de aprobación.' : 'La registramos correctamente para este lote.', 24186 }, 24187 }); 24188 24189 const lotNumber = this.cabanaLotNumber(currentLot); 24190 const auctionName = String(currentAuction?.nombre_remate || currentAuction?.title || currentAuction?.titulo || '').trim(); 24191 const consignatariaValue = currentAuction?.consignataria; 24192 const consignatariaName = String(currentAuction?.consignatariaName || currentAuction?.consignatariaNombre || (typeof consignatariaValue === 'string' ?consignatariaValue : (consignatariaValue?.name || consignatariaValue?.nombre)) || currentAuction?.nombre_cuenta || '').trim(); 24193 const endAt = currentAuction?.prebid?.fechaHoraFinPreoferta || currentAuction?.prebid?.fin || currentAuction?.prebid?.endAt || currentAuction?.prebid?.endDate || ''; 24194 const ownOffer = { 24195 id: String(resultRoot?.id || resultRoot?.prebidId || ''), 24196 amount, 24197 status, 24198 selectedCerts, 24199 createdAt: new Date().toISOString(), 24200 };
24201 this.persistCabanaPrebidOwnOffer(auctionId, lotCert, ownOffer); 24202 this.setState((previous) => ({ 24203 cabanaPrebidMine: [...(Array.isArray(previous.cabanaPrebidMine) ?previous.cabanaPrebidMine : []), ownOffer], 24204 })); 24205 await this.apiFetch('/api/notifications/cabana-prebid', { 24206 method: 'POST', 24207 body: JSON.stringify({ 24208 auctionId, 24209 lotCert, 24210 eventId: resultRoot?.id || resultRoot?.prebidId || '', 24211 status, 24212 amount, 24213 selectedCerts, 24214 auctionName, 24215 lotLabel: lotNumber ?('lote ' + lotNumber) : ('lote ' + lotCert), 24216 consignatariaId: currentAuction?.consignatariaId || currentAuction?.consignataria?.id || '', 24217 consignatariaName, 24218 endAt, 24219 href: '#/cabanas/' + encodeURIComponent(auctionId) + '/lote/' + encodeURIComponent(lotCert), 24220 }), 24221 }).catch((error) => { 24222 console.warn('No se pudieron distribuir las notificaciones de la preoferta.', error); 24223 return null; 24224 }); 24225 const currentLotCert = this.cabanaPrebidCert(currentLot); 24226 let refreshedRows = null; 24227 const refreshedRowsPromise = this.fetchCabanaPrebids(currentLot, { apply: false, loading: false }).then((rows) => { 24228 refreshedRows = rows; 24229 return rows; 24230 }); 24231 const applyRefreshedRows = (rows) => { 24232 if (!Array.isArray(rows) || !rows.length) { 24233 this.fetchCabanaPrebids(currentLot); 24234 return; 24235 } 24236 if (this.cabanaPrebidCert(this.state.currentCabanaLot) !== String(currentLotCert)) return; 24237 this.setState({ cabanaPrebids: rows, cabanaPrebidsLoading: false, cabanaPrebidsError: '' }); 24238 }; 24239 this._cabanaPrebidSuccessTimer = setTimeout(() => { 24240 this._cabanaPrebidSuccessTimer = null; 24241 this.setState({ cabanaPrebidSuccessFx: null }, () => { 24242 if (refreshedRows) applyRefreshedRows(refreshedRows); 24243 else refreshedRowsPromise.then(applyRefreshedRows); 24244 }); 24245 }, 1850); 24246 } catch (error) { 24247 if (error?.status === 400) {await this.fetchCabanaPrebids();const amount=this.cabanaPrebidMinimum();if(amount!=null)this.setState({cabanaPrebidForm:{...this.state.cabanaPrebidForm,amount}});} 24248 this.setState({ cabanaPrebidSubmitting: false, cabanaPrebidMessage: { ok: false, text: window.CabanaPrebid.errorMessage(error, 'No se pudo enviar la preoferta.') } }); 24249 } 24250 } 24251 cabanaLotId(lot) { 24252 return String(lot?.lotCert || lot?.lotId || lot?.id || '').trim(); 24253 } 24254 cabanaLotNumber(lot) { 24255 return window.CabanaLotIdentity.resolve(lot, this.state.currentCabana).number; 24256 } 24257 cabanaPublicLotPath(lot) { 24258 const lotNumber = this.cabanaLotNumber(lot) || this.cabanaLotId(lot); 24259 const rp = this.cabanaAnalyticsRpId(lot) || lotNumber; 24260 if (this.state.publicAdvanceMode === 'lacassina') { 24261 if (!lotNumber || !rp) return '/lacassina'; 24262 return '/lacassina/' + encodeURIComponent(String(lotNumber).trim().toLowerCase()) + '/' + encodeURIComponent(String(rp).trim().toLowerCase()); 24263 } 24264 const auctionId = this.cabanaRemateId(this.state.currentCabana) || String(lot?.remateId || lot?.auctionId || '').trim(); 24265 const lotId = this.cabanaLotId(lot) || lotNumber; 24266 if (!auctionId) return '/cabanas'; 24267 if (!lotId) return '/cabanas/' + encodeURIComponent(auctionId); 24268 return '/cabanas/' + encodeURIComponent(auctionId) + '/lotes/' + encodeURIComponent(lotId); 24269 } 24270 syncLacassinaLotPath(lot) { 24271 if (this.state.publicAdvanceMode !== 'lacassina' || window.__ROSGAN_PUBLIC_ROUTE__ === 'lacassina-stats') return; 24272 const nextPath = this.cabanaPublicLotPath(lot); 24273 if (!nextPath || window.location.pathname === nextPath) return; 24274 this.writeAppHistory(nextPath); 24275 window.__ROSGAN_PUBLIC_PATH__ = nextPath; 24276 window.__ROSGAN_PUBLIC_LOT_NUMBER__ = this.cabanaLotNumber(lot) || ''; 24277 window.__ROSGAN_PUBLIC_RP__ = this.cabanaAnalyticsRpId(lot) || ''; 24278 } 24279 cabanaLotGroupKey(lot, index = 0) { 24280 const category = lot?.categoryId || lot?.categoryName || 'categoria'; 24281 const number = this.cabanaLotNumber(lot); 24282 const fallback = lot?.groupId || lot?.lotCert || lot?.id || index; 24283 return this.consKey(category) + '::' + this.consKey(number || fallback); 24284 } 24285 cabanaLotVariantId(lot) { 24286 return String(lot?.rp || lot?.values?.rp || lot?.identifier || lot?.itemId || lot?.id || '').trim(); 24287 } 24288 startCabanaCarouselCrossfade(groupKey, updateState) { 24289 if (this._cabanaCarouselFadeTimer) clearTimeout(this._cabanaCarouselFadeTimer); 24290 if (this._cabanaCarouselRevealTimer) clearTimeout(this._cabanaCarouselRevealTimer); 24291 const activeCards = this._cabanaActiveCardByGroup || {}; 24292 const previousMedia = groupKey === '*' 24293 ?Object.fromEntries(Object.entries(activeCards).map(([key, card]) => [key, { 24294 media: card.media, 24295 hasMedia: card.hasMedia, 24296 backgroundLogo: card.backgroundLogo,
24297 hasBackgroundLogo: card.hasBackgroundLogo, 24298 backgroundLogoFilter: card.backgroundLogoFilter, 24299 }])) 24300 :{ 24301 ...(this.state.cbCarouselPreviousMedia || {}), 24302 [groupKey]: activeCards[groupKey] ?{ 24303 media: activeCards[groupKey].media, 24304 hasMedia: activeCards[groupKey].hasMedia, 24305 backgroundLogo: activeCards[groupKey].backgroundLogo, 24306 hasBackgroundLogo: activeCards[groupKey].hasBackgroundLogo, 24307 backgroundLogoFilter: activeCards[groupKey].backgroundLogoFilter, 24308 } : null, 24309 }; 24310 this.setState((previous) => ({ 24311 ...(typeof updateState === 'function' ?updateState(previous) : {}), 24312 cbCarouselFading: true, 24313 cbCarouselFadingKey: groupKey, 24314 cbCarouselCrossfadeActive: false, 24315 cbCarouselPreviousMedia: previousMedia, 24316 }), () => { 24317 const nextCards = this._cabanaActiveCardByGroup || {}; 24318 const affectedKeys = groupKey === '*' ?Object.keys(previousMedia) : [groupKey]; 24319 const hasRenderableNextMedia = affectedKeys.some((key) => { 24320 const nextCard = nextCards[key]; 24321 return !!(nextCard?.hasMedia || nextCard?.hasBackgroundLogo); 24322 }); 24323 if (!hasRenderableNextMedia) this.revealCabanaCarouselCrossfade(); 24324 }); 24325 } 24326 revealCabanaCarouselCrossfade() { 24327 if (!this.state.cbCarouselFading || this.state.cbCarouselCrossfadeActive) return; 24328 if (this._cabanaCarouselRevealTimer) clearTimeout(this._cabanaCarouselRevealTimer); 24329 this.setState({ cbCarouselCrossfadeActive: true }, () => { 24330 this._cabanaCarouselRevealTimer = setTimeout(() => { 24331 this.setState({ 24332 cbCarouselFading: false, 24333 cbCarouselFadingKey: '', 24334 cbCarouselCrossfadeActive: false, 24335 cbCarouselPreviousMedia: {}, 24336 }); 24337 }, 370); 24338 }); 24339 } 24340 selectCabanaCarouselSlide(groupKey, index, event) { 24341 event?.preventDefault?.(); 24342 event?.stopPropagation?.(); 24343 this.startCabanaCarouselCrossfade(groupKey, (previous) => ({ 24344 cbCarouselOffsets: { 24345 ...(previous.cbCarouselOffsets || {}), 24346 [groupKey]: Number(index) - Number(previous.cbCarouselStep || 0), 24347 }, 24348 })); 24349 } 24350 selectCabanaLotVariant(lot, event) { 24351 event?.preventDefault?.(); 24352 event?.stopPropagation?.(); 24353 const lotId = this.cabanaLotId(lot); 24354 const selectedLot = { ...lot, videos: Array.isArray(lot?.videos) ?lot.videos : [], __videosLoaded: true }; 24355 this.flushCabanaLotViewTimer('switch_rp'); 24356 this.syncLacassinaLotPath(lot); 24357 this.trackCabanaAnalyticsOnce('rp_view', lot, { metadata: { action: 'select_rp' } }); 24358 this.startCabanaLotViewTimer(lot); 24359 this.setState({ 24360 currentCabanaLot: selectedLot, 24361 cbLotVideo: 0, 24362 cabanaLotVideosLoading: false, 24363 cabanaLotMediaReady: false, 24364 cabanaLotMediaFailed: false, 24365 cabanaLotVideoPlaying: false, 24366 cabanaLotVideoProgress: 0, 24367 cabanaLotVideoDuration: 0, 24368 cabanaLotVideoVolume: 0, 24369 cabanaLotVideoMuted: true, 24370 }, () => { 24371 this.syncCabanaLotVideo(lotId); 24372 this.fetchCabanaPrebids(lot); 24373 }); 24374 } 24375 syncCabanaLotVideo(expectedLotId) { 24376 window.setTimeout(() => { 24377 if (!expectedLotId || this.cabanaLotId(this.state.currentCabanaLot) !== String(expectedLotId)) return; 24378 const player = document.querySelector('video.dc-cabana-lot-player'); 24379 const videos = this.cabanaLotVideos(this.state.currentCabanaLot); 24380 const activeVideo = videos[Math.max(0, Number(this.state.cbLotVideo || 0))]; 24381 if (!player || !activeVideo?.url) return; 24382 const expectedUrl = new URL(activeVideo.url, window.location.href).href; 24383 if (player.currentSrc !== expectedUrl && player.src !== expectedUrl) player.src = activeVideo.url; 24384 player.autoplay = true; 24385 player.muted = true; 24386 player.loop = true; 24387 player.load(); 24388 player.play?.().catch(() => {}); 24389 this.syncCabanaVideoState(player, true); 24390 }, 0); 24391 } 24392 cabanaLotPlayer() { 24393 return document.querySelector('video.dc-cabana-lot-player'); 24394 } 24395 formatCabanaVideoTime(seconds) { 24396 const value = Math.max(0, Math.floor(Number(seconds) || 0)); 24397 const minutes = Math.floor(value / 60); 24398 const rest = value % 60; 24399 return minutes + ':' + String(rest).padStart(2, '0'); 24400 } 24401 syncCabanaVideoState(video = this.cabanaLotPlayer(), force = false) { 24402 if (!video) return; 24403 const now = Date.now(); 24404 if (!force && this._cabanaVideoStateAt && now - this._cabanaVideoStateAt < 350) return; 24405 this._cabanaVideoStateAt = now; 24406 const duration = Number.isFinite(video.duration) ?video.duration : 0; 24407 const progress = duration > 0 ?Math.min(100, Math.max(0, (Number(video.currentTime || 0) / duration) * 100)) : 0; 24408 this.setState({ 24409 cabanaLotVideoPlaying: !video.paused && !video.ended, 24410 cabanaLotVideoProgress: progress, 24411 cabanaLotVideoDuration: duration, 24412 cabanaLotVideoVolume: video.muted ?0 : Number(video.volume || 0), 24413 cabanaLotVideoMuted: !!video.muted || Number(video.volume || 0) === 0, 24414 }); 24415 } 24416 toggleCabanaVideoPlayback(event) { 24417 event?.preventDefault?.(); 24418 const video = this.cabanaLotPlayer(); 24419 if (!video) return; 24420 if (video.paused) video.play?.().catch(() => {}); 24421 else video.pause?.(); 24422 this.syncCabanaVideoState(video, true); 24423 } 24424 seekCabanaVideo(event) { 24425 const video = this.cabanaLotPlayer(); 24426 if (!video || !Number.isFinite(video.duration) || video.duration <= 0) return; 24427 const value = Math.max(0, Math.min(100, Number(event?.target?.value || 0
24427))); 24428 video.currentTime = (value / 100) * video.duration; 24429 this.syncCabanaVideoState(video, true); 24430 } 24431 setCabanaVideoVolume(event) { 24432 const video = this.cabanaLotPlayer(); 24433 if (!video) return; 24434 const value = Math.max(0, Math.min(1, Number(event?.target?.value || 0))); 24435 try { video.volume = value; } catch (_) {} 24436 video.muted = value <= 0; 24437 this.syncCabanaVideoState(video, true); 24438 } 24439 toggleCabanaVideoMute(event) { 24440 event?.preventDefault?.(); 24441 const video = this.cabanaLotPlayer(); 24442 if (!video) return; 24443 const nextMuted = !video.muted && Number(video.volume || 0) > 0; 24444 if (!nextMuted && Number(video.volume || 0) <= 0) { 24445 try { video.volume = 0.8; } catch (_) {} 24446 } 24447 video.muted = nextMuted; 24448 this.syncCabanaVideoState(video, true); 24449 } 24450 async openCabanaVideoFullscreen(event) { 24451 event?.preventDefault?.(); 24452 const video = this.cabanaLotPlayer(); 24453 if (!video) return; 24454 const frame = video.closest('.dc-cabana-video-frame') || video; 24455 try { 24456 if (frame.requestFullscreen) await frame.requestFullscreen(); 24457 else if (video.requestFullscreen) await video.requestFullscreen(); 24458 else if (video.webkitEnterFullscreen) { 24459 video.webkitEnterFullscreen(); 24460 return; 24461 } 24462 if (screen?.orientation?.lock) screen.orientation.lock('landscape').catch(() => {}); 24463 if (!this._cabanaFullscreenListener) { 24464 this._cabanaFullscreenListener = () => { 24465 if (!document.fullscreenElement && screen?.orientation?.unlock) { 24466 try { screen.orientation.unlock(); } catch (_) {} 24467 } 24468 }; 24469 document.addEventListener('fullscreenchange', this._cabanaFullscreenListener); 24470 } 24471 } catch (_) { 24472 try { video.webkitEnterFullscreen?.(); } catch (__) {} 24473 } 24474 } 24475 async fetchCabanaLotVideos(lot) { 24476 const auctionId = String(lot?.remateId || this.cabanaRemateId(this.state.currentCabana) || '').trim(); 24477 const lotId = this.cabanaLotId(lot); 24478 if (!auctionId || !lotId) { 24479 if (this.cabanaLotId(this.state.currentCabanaLot) === lotId) { 24480 this.setState({ 24481 currentCabanaLot: { ...lot, __videosLoaded: true }, 24482 cabanaLotVideosLoading: false, 24483 }); 24484 } 24485 return; 24486 } 24487 if (lot?.__videosLoaded !== true) this.setState({ cabanaLotVideosLoading: true }); 24488 try { 24489 const data = await this.cabanaApiFetch('/api/cabin-auctions/' + encodeURIComponent(auctionId) + '/lots/' + encodeURIComponent(lotId) + '/videos'); 24490 let videos = this.listFrom(data, ['items', 'data', 'videos', 'media', 'results', 'value']); 24491 const singleVideo = data?.video || data?.data?.video || data?.data || data?.item || data; 24492 if (!videos.length && singleVideo && typeof singleVideo === 'object' && (singleVideo.url || singleVideo.videoUrl || singleVideo.downloadUrl || singleVideo.streamUrl || singleVideo.mediaUrl || singleVideo.src)) videos = [singleVideo]; 24493 const normalizedVideos = videos.map((video) => typeof video === 'string' ?{ url: video } : video).filter(Boolean); 24494 const sameLot = (item) => this.cabanaLotId(item) === lotId; 24495 const mergedLot = { ...lot, videos: normalizedVideos, __videosLoaded: true }; 24496 this._cabanaTvVideoCache = this._cabanaTvVideoCache || new Map(); 24497 this._cabanaTvVideoCache.set(auctionId + ':' + lotId, { videos: normalizedVideos, loaded: true }); 24498 const isCurrentLot = sameLot(this.state.currentCabanaLot); 24499 this.setState((previous) => { 24500 const previousCurrentLot = previous.currentCabanaLot; 24501 const currentStillMatches = sameLot(previousCurrentLot); 24502 return { 24503 ...(currentStillMatches ?{ 24504 currentCabanaLot: { ...previousCurrentLot, videos: normalizedVideos, __videosLoaded: true }, 24505 cbLotVideo: 0, 24506 cabanaLotVideosLoading: false, 24507 cabanaLotMediaReady: false, 24508 cabanaLotMediaFailed: false, 24509 } : {}), 24510 cabanaLots: Array.isArray(previous.cabanaLots) ?previous.cabanaLots.map((item) => sameLot(item) ?{ ...item, videos: normalizedVideos, __videosLoaded: true } : item) : previous.cabanaLots, 24511 }; 24512 }); 24513 if (isCurrentLot) this.syncCabanaLotVideo(lotId); 24514 return mergedLot; 24515 } catch { 24516 this._cabanaTvVideoCache = this._cabanaTvVideoCache || new Map(); 24517 this._cabanaTvVideoCache.set(auctionId + ':' + lotId, { videos: [], loaded: true, failed: true }); 24518 const sameLot = (item) => this.cabanaLotId(item) === lotId; 24519 this.setState((previous) => ({ 24520 ...(sameLot(previous.currentCabanaLot) ?{ 24521 currentCabanaLot: { ...previous.currentCabanaLot, __videosLoaded: true }, 24522 cabanaLotVideosLoading: false, 24523 } : {}), 24524 cabanaLots: Array.isArray(previous.cabanaLots) 24525 ?previous.cabanaLots.map((item) => sameLot(item) ?{ ...item, __videosLoaded: true } : item) 24526 : previous.cabanaLots, 24527 })); 24528 } 24529 } 24530 cabanaLotVideos(lot) { 24531 const raw = Array.isArray(lot?.videos) ?lot.videos : []; 24532 return raw.map((video) => typeof video === 'string' 24533 ?{ url: video, thumb: '' } 24534 : { 24535 id: video?.id || video?.videoId || video?.mediaId || '', 24536 url: video?.url || video?.videoUrl || video?.downloadUrl || video?.streamUrl || video?.mediaUrl || video?.src || '', 24537 thumb: video?.thumbnailUrl || video?.thumb || video?.posterUrl || video?.coverUrl || video?.coverImage?.url || '', 24538 fileName: video?.fileName || video?.name || '', 24539 associatedLotIds: Array.isArray(video?.associatedLotIds) ?video.associatedLotIds.map(String):null, 24540 videoType: video?.videoType || video?.type || '', 24541 }).filter((video) => video.url); 24542 } 24543 cabanaTvVideoItems(group) { 24544 const variants = Array.isArray(group?.variants) ?group.variants : []; 24545 const fallbackPoster = (lot) => this.cabanaLotSelectedCoverUrl(lot) || ''; 24546 return variants.map((variant) => { 24547 const lotId = this.cabanaLotId(variant); 24548 const auctionId = String(variant?.remateId || this.cabanaRemateId(this.state.currentCabana) || '').trim(); 24549 const cached = this._cabanaTvVideoCache?.get(auctionId + ':' + lotId);
24550 const videos = cached?.loaded === true ?this.cabanaLotVideos({ videos: cached.videos }) : this.cabanaLotVideos(variant); 24551 const firstVideo = videos[0] || {}; 24552 const rp = this.cabanaAnalyticsRpId(variant) || this.cabanaLotVariantId(variant) || this.cabanaLotNumber(variant) || ''; 24553 return { 24554 key: this.cabanaCommercialRowKey(variant) || this.cabanaLotId(variant) || rp, 24555 lotId, 24556 variant, 24557 rp: rp ?('RP ' + rp) : 'RP', 24558 src: firstVideo.url || '', 24559 poster: firstVideo.thumb || fallbackPoster(variant), 24560 needsLoad: !firstVideo.url && cached?.loaded !== true && variant?.__videosLoaded !== true, 24561 }; 24562 }).filter((item) => item.src || item.needsLoad); 24563 } 24564 ensureCabanaTvVideos(group) { 24565 const variants = Array.isArray(group?.variants) ?group.variants : []; 24566 if (!variants.length) return; 24567 this._cabanaTvVideoFetches = this._cabanaTvVideoFetches || new Set(); 24568 variants.forEach((variant) => { 24569 const lotId = this.cabanaLotId(variant); 24570 if (!lotId || variant?.__videosLoaded === true || this.cabanaLotVideos(variant).length) return; 24571 const key = (this.cabanaRemateId(this.state.currentCabana) || '2') + ':' + lotId; 24572 if (this._cabanaTvVideoFetches.has(key)) return; 24573 this._cabanaTvVideoFetches.add(key); 24574 this.fetchCabanaLotVideos(variant).finally(() => { 24575 this._cabanaTvVideoFetches.delete(key); 24576 }); 24577 }); 24578 } 24579 clearCabanaTvVideoRecovery(video) { 24580 if (!video || !this._cabanaTvVideoRecoveryTimers) return; 24581 const timer = this._cabanaTvVideoRecoveryTimers.get(video); 24582 if (timer) window.clearTimeout(timer); 24583 this._cabanaTvVideoRecoveryTimers.delete(video); 24584 } 24585 scheduleCabanaTvVideoRecovery(video, immediate = false) { 24586 if (!video?.isConnected) return; 24587 this._cabanaTvVideoRecoveryTimers = this._cabanaTvVideoRecoveryTimers || new Map(); 24588 if (this._cabanaTvVideoRecoveryTimers.has(video) && !immediate) return; 24589 this.clearCabanaTvVideoRecovery(video); 24590 const timer = window.setTimeout(() => { 24591 this._cabanaTvVideoRecoveryTimers?.delete(video); 24592 if (!video.isConnected) return; 24593 const shouldPlay = video.dataset.tvShouldPlay === 'true'; 24594 if (video.readyState >= 3 && (!shouldPlay || (!video.paused && !video.ended))) return; 24595 const attempts = Number(video.dataset.tvRecoveryAttempts || 0) + 1; 24596 video.dataset.tvRecoveryAttempts = String(attempts); 24597 const source = String(video.dataset.tvSource || video.currentSrc || video.src || '').trim(); 24598 if (!source) return; 24599 if (attempts > 3) { 24600 const tile = video.closest?.('.tv-video-tile'); 24601 const mount = tile?.parentElement; 24602 if (shouldPlay && tile?.classList.contains('is-active') && mount?.classList.contains('is-single') && mount.querySelectorAll('.tv-video-tile').length > 1) { 24603 video.dataset.tvRecoveryAttempts = '0'; 24604 this.advanceCabanaTvPlaylist({ target: video }); 24605 return; 24606 } 24607 video.dataset.tvRecoveryAttempts = '0'; 24608 } 24609 const resumeAt = Number.isFinite(Number(video.currentTime)) ?Number(video.currentTime) : 0; 24610 try { 24611 video.classList.remove('is-ready'); 24612 video.src = source; 24613 video.load(); 24614 if (resumeAt > 0) video.addEventListener('loadedmetadata', () => { 24615 try { video.currentTime = Math.min(resumeAt, Math.max(0, Number(video.duration) - .2)); } catch (error) {} 24616 }, { once: true }); 24617 if (shouldPlay) { 24618 const playResult = video.play?.(); 24619 if (playResult?.catch) playResult.catch(() => this.scheduleCabanaTvVideoRecovery(video)); 24620 } 24621 } catch (error) { 24622 this.scheduleCabanaTvVideoRecovery(video); 24623 } 24624 }, immediate ?350 : 4500); 24625 this._cabanaTvVideoRecoveryTimers.set(video, timer); 24626 } 24627 prepareCabanaTvVideo(video, shouldPlay = false) { 24628 if (!video) return; 24629 video.dataset.tvShouldPlay = shouldPlay ?'true' : 'false'; 24630 if (!shouldPlay) { 24631 try { video.pause(); } catch (error) {} 24632 if (video.readyState < 3) { 24633 try { video.load?.(); } catch (error) {} 24634 this.scheduleCabanaTvVideoRecovery(video); 24635 } 24636 return; 24637 } 24638 const playResult = video.play?.(); 24639 if (playResult?.catch) playResult.catch(() => this.scheduleCabanaTvVideoRecovery(video)); 24640 if (video.readyState < 3 || video.paused) this.scheduleCabanaTvVideoRecovery(video); 24641 } 24642 syncCabanaTvVideoPlayback(playback = {}) { 24643 if (typeof document === 'undefined') return; 24644 const portal = document.getElementById('tv-video-portal'); 24645 const mount = portal?.querySelector('#tv-video-mount'); 24646 if (!portal || !mount?.classList.contains('is-single')) return; 24647 const video = mount.querySelector('.tv-video-tile.is-active video') || mount.querySelector('video'); 24648 if (!video) return; 24649 const restartToken = String(Number(playback.restartToken || 0)); 24650 if (portal.dataset.restartToken !== restartToken) { 24651 portal.dataset.restartToken = restartToken;
24652 try { video.currentTime = 0; } catch (error) {} 24653 } 24654 if (playback.playing === false) { 24655 this.prepareCabanaTvVideo(video, false); 24656 return; 24657 } 24658 this.prepareCabanaTvVideo(video, true); 24659 } 24660 syncCabanaTvSoldVisuals(items = [], mode = 'single') { 24661 if (typeof document === 'undefined') return; 24662 const mount = document.querySelector('#tv-video-mount'); 24663 if (!mount) return; 24664 const safeItems = Array.isArray(items) ?items : []; 24665 Array.from(mount.querySelectorAll('.tv-video-tile')).forEach((tile, itemIndex) => { 24666 const isSold = mode === 'grid' && safeItems[itemIndex]?.isSold === true; 24667 tile.dataset.saleState = isSold ?'sold' : 'active'; 24668 let soldBadge = tile.querySelector('.tv-video-sale-badge'); 24669 if (isSold) { 24670 if (!soldBadge) { 24671 soldBadge = document.createElement('span'); 24672 soldBadge.className = 'tv-video-sale-badge'; 24673 const soldCheck = document.createElement('i'); 24674 soldCheck.setAttribute('aria-hidden', 'true'); 24675 soldCheck.textContent = '\u2713'; 24676 const soldLabel = document.createElement('strong'); 24677 soldLabel.textContent = 'Vendido'; 24678 soldBadge.appendChild(soldCheck); 24679 soldBadge.appendChild(soldLabel); 24680 tile.appendChild(soldBadge); 24681 } 24682 const currentBadge = soldBadge; 24683 window.requestAnimationFrame(() => { 24684 if (tile.dataset.saleState !== 'sold') return; 24685 tile.classList.add('is-sold'); 24686 currentBadge.classList.add('is-visible'); 24687 }); 24688 return; 24689 } 24690 tile.classList.remove('is-sold'); 24691 if (!soldBadge) return; 24692 soldBadge.classList.remove('is-visible'); 24693 const staleBadge = soldBadge; 24694 window.setTimeout(() => { 24695 if (tile.dataset.saleState === 'active' && staleBadge.parentNode === tile) staleBadge.remove(); 24696 }, 520); 24697 }); 24698 } 24699 syncCabanaTvVideoMount(items = [], mode = 'single', playback = {}) { 24700 if (typeof document === 'undefined') return; 24701 if (this.state.route !== 'cabanaTvLower') { 24702 this.destroyCabanaTvVideoMount(); 24703 return; 24704 } 24705 const safeMode = mode === 'grid' ?'grid' : 'single'; 24706 const safeItems = Array.isArray(items) ?items.filter((item) => item?.src) : []; 24707 const signature = safeMode + '|' + safeItems.map((item) => [item.key, item.src].join('~')).join('|'); 24708 let portal = document.getElementById('tv-video-portal'); 24709 const mountExists = !!portal?.querySelector('#tv-video-mount'); 24710 if (this._cabanaTvVideoMountSignature === signature && mountExists) { 24711 const existingMount = portal.querySelector('#tv-video-mount'); 24712 const prewarming = playback.prewarming === true; 24713 portal.classList.toggle('is-prewarming', prewarming); 24714 existingMount?.classList.toggle('is-without-lower-third', playback.lowerThirdVisible === false); 24715 existingMount?.classList.toggle('is-waiting-background', playback.waitingBackgroun
24715d === true); 24716 this.syncCabanaTvSoldVisuals(safeItems, safeMode); 24717 if (safeMode === 'single') this.syncCabanaTvVideoPlayback({ ...playback, playing: !prewarming && playback.playing !== false }); 24718 else existingMount?.querySelectorAll('video').forEach((video) => this.prepareCabanaTvVideo(video, !prewarming)); 24719 return; 24720 } 24721 this._cabanaTvVideoMountSignature = signature; 24722 if (!portal) { 24723 portal = document.createElement('div'); 24724 portal.id = 'tv-video-portal'; 24725 portal.className = 'tv-video-portal'; 24726 const bg = document.createElement('div'); 24727 bg.className = 'tv-video-bg'; 24728 const mountNode = document.createElement('div'); 24729 mountNode.id = 'tv-video-mount'; 24730 portal.appendChild(bg); 24731 portal.appendChild(mountNode); 24732 document.body.appendChild(portal); 24733 } else if (portal.parentNode !== document.body) { 24734 document.body.appendChild(portal); 24735 } 24736 const mount = portal.querySelector('#tv-video-mount'); 24737 if (!mount) return; 24738 portal.classList.toggle('is-prewarming', playback.prewarming === true); 24739 mount.className = 'tv-video-layer ' + (safeMode === 'grid' ?('is-grid is-count-' + Math.min(4, safeItems.length || 1)) :'is-single') + (playback.lowerThirdVisible === false ?' is-without-lower-third' : '') + (playback.waitingBackgroun
24739d === true ?' is-waiting-background' : ''); 24740 mount.querySelectorAll('video').forEach((video) => this.clearCabanaTvVideoRecovery(video)); 24741 while (mount.firstChild) mount.removeChild(mount.firstChild); 24742 safeItems.forEach((item, itemIndex) => { 24743 const tile = document.createElement('article'); 24744 tile.className = 'tv-video-tile' + (safeMode === 'single' && itemIndex === 0 ?' is-active' : ''); 24745 if (item.poster) tile.style.backgroundImage = 'url("' + String(item.poster).replace(/"/g, '\\"') + '")'; 24746 tile.style.backgroundSize = 'cover'; 24747 tile.style.backgroundPosition = 'center'; 24748 const video = document.createElement('video'); 24749 video.src = item.src; 24750 if (item.poster) video.poster = item.poster; 24751 video.autoplay = safeMode === 'grid' || (itemIndex === 0 && playback.playing !== false); 24752 video.muted = true; 24753 video.defaultMuted = true; 24754 video.playsInline = true; 24755 video.preload = 'auto'; 24756 video.setAttribute('muted', ''); 24757 video.setAttribute('playsinline', ''); 24758 video.setAttribute('webkit-playsinline', ''); 24759 if (video.autoplay) video.setAttribute('autoplay', ''); 24760 video.loop = safeMode === 'grid' || (safeMode === 'single' && safeItems.length === 1); 24761 video.dataset.tvSource = item.src; 24762 video.dataset.tvRecoveryAttempts = '0'; 24763 const markReady = () => { 24764 video.classList.add('is-ready'); 24765 video.dataset.tvRecoveryAttempts = '0'; 24766 this.clearCabanaTvVideoRecovery(video); 24767 }; 24768 video.addEventListener('canplay', markReady); 24769 video.addEventListener('playing', markReady); 24770 video.addEventListener('loadeddata', markReady); 24771 video.addEventListener('waiting', () => this.scheduleCabanaTvVideoRecovery(video)); 24772 video.addEventListener('stalled', () => this.scheduleCabanaTvVideoRecovery(video)); 24773 video.addEventListener('error', () => this.scheduleCabanaTvVideoRecovery(video, true)); 24774 if (safeMode === 'single' && safeItems.length > 1) { 24775 video.addEventListener('timeupdate', (event) => { 24776 const current = event.currentTarget; 24777 const currentTile = current?.closest?.('.tv-video-tile'); 24778 const remaining = Number(current?.duration) - Number(current?.currentTime); 24779 const transitionLead = currentTile?.parentElement?.classList.contains('is-waiting-background') ?1.25 : .55; 24780 if (!currentTile?.classList.contains('is-active') || !Number.isFinite(remaining) || remaining > transitionLead || remaining <= 0) return; 24781 this.advanceCabanaTvPlaylist(event); 24782 }); 24783 video.addEventListener('ended', (event) => this.advanceCabanaTvPlaylist(event)); 24784 } 24785 const badge = document.createElement('span'); 24786 badge.className = 'tv-video-badge'; 24787 badge.textContent = item.rp || 'RP'; 24788 tile.appendChild(video); 24789 tile.appendChild(badge); 24790 mount.appendChild(tile); 24791 const shouldPlay = playback.prewarming !== true && (safeMode === 'grid' || (itemIndex === 0 && playback.playing !== false)); 24792 this.prepareCabanaTvVideo(video, shouldPlay); 24793 }); 24794 this.syncCabanaTvSoldVisuals(safeItems, safeMode); 24795 if (safeMode === 'single') this.syncCabanaTvVideoPlayback({ ...playback, playing: playback.prewarming !== true && playback.playing !== false }); 24796 } 24797 destroyCabanaTvVideoMount() { 24798 this._cabanaTvVideoMountSignature = ''; 24799 if (typeof document === 'undefined') return; 24800 const portal = document.getElementById('tv-video-portal'); 24801 portal?.querySelectorAll('video').forEach((video) => this.clearCabanaTvVideoRecovery(video)); 24802 if (portal?.parentNode) portal.parentNode.removeChild(portal); 24803 } 24804 cabanaMediaUrl(media) { 24805 if (!media) return ''; 24806 if (typeof media === 'string') return window.resolveCabanaMediaUrl(media); 24807 if (typeof media !== 'object') return ''; 24808 return window.resolveCabanaMediaUrl(media.url || media.publicUrl || media.downloadUrl || media.imageUrl || media.src); 24809 } 24810 cabanaLotItemCoverUrl(lot) { 24811 return this.cabanaMediaUrl(lot?.coverImage || lot?.cover_image); 24812 } 24813 cabanaLotFallbackCoverUrl(lot) { 24814 return this.cabanaMediaUrl(lot?.lotCoverImage || lot?.lot_cover_image) 24815 || window.resolveCabanaMediaUrl(lot?.lotThumbnailUrl); 24816 } 24817 cabanaLotSelectedCoverUrl(lot) { 24818 return this.cabanaLotItemCoverUrl(lot) || this.cabanaLotFallbackCoverUrl(lot); 24819 } 24820 cabanaLotImages(lot) { 24821 const candidates = [ 24822 lot?.coverImage, 24823 lot?.cover_image, 24824 lot?.thumbnailUrl, 24825 lot?.coverImageUrl, 24826 ...(Array.isArray(lot?.images) ?lot.images : []), 24827 ...(Array.isArray(lot?.imagenes) ?lot.imagenes : []), 24828 ...(Array.isArray(lot?.photos) ?lot.photos : []), 24829 ...(Array.isArray(lot?.fotos) ?lot.fotos : []), 24830 ...(Array.isArray(lot?.media) ?lot.media : []), 24831 ]; 24832 const seen = new Set(); 24833 return candidates.map((media) => { 24834 if (!media) return null; 24835 if (typeof media === 'string') return { url: window.resolveCabanaMediaUrl(media), fileName: '' }; 24836 if (typeof media !== 'object') return null; 24837 return { 24838 url: window.resolveCabanaMediaUrl(media.url || media.publicUrl || media.downloadUrl || media.imageUrl || media.src), 24839 fileName: media.fileName || media.name || media.nombre || '', 24840 }; 24841 }).filter((media) => { 24842 if (!media?.url || seen.has(media.url)) return false;
24843 seen.add(media.url); 24844 return true; 24845 }); 24846 } 24847 cabanaAnalyticsRpId(lot) { 24848 return String(lot?.rp || lot?.values?.rp || lot?.identifier || lot?.itemId || '').trim(); 24849 } 24850 cabanaAnalyticsOnceKey(eventType, lot = null, extra = {}) { 24851 return [ 24852 String(lot?.remateId || this.cabanaRemateId(this.state.currentCabana) || ''), 24853 eventType, 24854 this.cabanaLotId(lot), 24855 this.cabanaAnalyticsRpId(lot), 24856 extra.mediaId || extra.videoId || extra.imageId || '', 24857 ].join('|'); 24858 } 24859 trackCabanaAnalyticsOnce(eventType, lot = null, extra = {}) { 24860 if (!this._cabanaAnalyticsSeen) this._cabanaAnalyticsSeen = new Set(); 24861 const key = this.cabanaAnalyticsOnceKey(eventType, lot, extra); 24862 if (this._cabanaAnalyticsSeen.has(key)) return; 24863 this._cabanaAnalyticsSeen.add(key); 24864 this.trackCabanaAnalytics(eventType, lot, extra); 24865 } 24866 startCabanaLotViewTimer(lot) { 24867 this.flushCabanaLotViewTimer('switch_lot'); 24868 if (this._cabanaLotViewHeartbeat) { 24869 clearInterval(this._cabanaLotViewHeartbeat); 24870 this._cabanaLotViewHeartbeat = null; 24871 } 24872 if (!lot || !String(lot?.remateId || this.cabanaRemateId(this.state.currentCabana) || '').trim()) return; 24873 const lotId = this.cabanaLotId(lot); 24874 const rpId = this.cabanaAnalyticsRpId(lot); 24875 this._cabanaLotViewTimer = { 24876 lot, 24877 startedAt: Date.now(), 24878 rpId, 24879 viewId: [this.cabanaAnalyticsClientId(), lotId, rpId || 'rp', Date.now()].join('|'), 24880 }; 24881 this._cabanaLotViewHeartbeat = setInterval(() => { 24882 this.flushCabanaLotViewTimer('heartbeat', true); 24883 }, 10000); 24884 } 24885 flushCabanaLotViewTimer(reason = 'leave_lot', keepActive = false) { 24886 const active = this._cabanaLotViewTimer; 24887 if (!active?.lot) { 24888 this._cabanaLotViewTimer = null; 24889 if (this._cabanaLotViewHeartbeat) { 24890 clearInterval(this._cabanaLotViewHeartbeat); 24891 this._cabanaLotViewHeartbeat = null; 24892 } 24893 return; 24894 } 24895 const durationMs = Date.now() - Number(active.startedAt || Date.now()); 24896 if (keepActive) active.startedAt = Date.now(); 24897 if (!keepActive) { 24898 this._cabanaLotViewTimer = null; 24899 if (this._cabanaLotViewHeartbeat) { 24900 clearInterval(this._cabanaLotViewHeartbeat); 24901 this._cabanaLotViewHeartbeat = null; 24902 } 24903 } 24904 if (durationMs < 3000) return; 24905 this.trackCabanaAnalytics('view_time', active.lot, { 24906 durationMs, 24907 metadata: { action: 'view_time', reason, viewId: active.viewId, cumulative: false, interval: true }, 24908 }); 24909 } 24910 trackCabanaVideoPlay(lot, videoUrl, mediaIndex = 0) { 24911 if (!videoUrl) return; 24912 this.trackCabanaAnalyticsOnce('video_play', lot, { 24913 mediaId: videoUrl, 24914 videoId: videoUrl, 24915 mediaType: 'video', 24916 metadata: { action: 'video_play', mediaIndex }, 24917 }); 24918 } 24919 cabanaAnalyticsClientId() { 24920 const key = 'rosgan:lacassinaAnalyticsClientId'; 24921 try { 24922 const existing = localStorage.getItem(key); 24923 if (existing) return existing; 24924 const next = (window.crypto?.randomUUID?.() || ('client-' + Date.now() + '-' + Math.random().toString(36).slice(2))); 24925 localStorage.setItem(key, next); 24926 return next; 24927 } catch { 24928 return 'client-' + Date.now(); 24929 } 24930 } 24931 cabanaSocialKey(lot = null) { 24932 const current = this.state.currentCabana || {}; 24933 return [ 24934 this.cabanaRemateId(current) || '2', 24935 this.cabanaLotId(lot) || 'lot', 24936 this.cabanaAnalyticsRpId(lot) || 'rp', 24937 ].join('|'); 24938 } 24939 bumpCabanaSocialMetric(metric, lot = null) { 24940 const key = this.cabanaSocialKey(lot); 24941 const bumps = this.state.cabanaSocialBumps || {}; 24942 const current = bumps[key] || {}; 24943 this.setState({ 24944 cabanaSocialBumps: { 24945 ...bumps, 24946 [key]: { ...current, [metric]: Number(current[metric] || 0) + 1 }, 24947 }, 24948 }); 24949 } 24950 cabanaPublicShareUrl(lot = null) { 24951 const path = lot ?this.cabanaPublicLotPath(lot) : (this.state.currentCabana ?this.cabanaPath(this.state.currentCabana) : '/cabanas'); 24952 try { return new URL(path, window.location.origin).href; } 24953 catch { return path; } 24954 } 24955 async shareCabanaLot(lot = null) { 24956 const target = lot || this.state.currentCabanaLot; 24957 if (!target) return; 24958 const { title, auctionTitle } = this.cabanaLotShareTitle(target); 24959 const url = this.cabanaPublicShareUrl(target); 24960 let shared = false;
24961 try { 24962 if (navigator.share) { 24963 await navigator.share({ title, text: title + ' - ' + auctionTitle, url }); 24964 shared = true; 24965 } else if (navigator.clipboard?.writeText) { 24966 await navigator.clipboard.writeText(url); 24967 this.showToast('Enlace copiado'); 24968 shared = true; 24969 } else { 24970 window.prompt('Copiar enlace', url); 24971 shared = true; 24972 } 24973 } catch (error) { 24974 if (error?.name !== 'AbortError') this.showToast('No se pudo compartir.'); 24975 return; 24976 } 24977 if (!shared) return; 24978 const socialKey = this.cabanaSocialKey(target); 24979 this.bumpCabanaSocialMetric('shares', target); 24980 this.trackCabanaAnalytics('share_click', target, { 24981 metadata: { action: 'share_rp', url }, 24982 }); 24983 setTimeout(() => { 24984 this.fetchCabanaAnalyticsSummary(this.state.currentCabana).finally(() => { 24985 const bumps = { ...(this.state.cabanaSocialBumps || {}) }; 24986 if (!bumps[socialKey]) return; 24987 delete bumps[socialKey]; 24988 this.setState({ cabanaSocialBumps: bumps }); 24989 }); 24990 }, 1500); 24991 } 24992 cabanaLotValue(lot, keys, fallback = '') { 24993 for (const key of keys) { 24994 const value = lot?.[key] ?? lot?.values?.[key] ?? lot?.subformValues?.datos_principales?.[key]; 24995 if (value !== undefined && value !== null && value !== '') return value; 24996 } 24997 return fallback; 24998 } 24999 cabanaStoryValueIsEmpty(value) { 25000 const text = String(value || '').trim(); 25001 return !text || text === '-' || /^[-\u2013\u2014]+$/.test(text) || text.toLowerCase() === 'null' || text.toLowerCase() === 'undefined'; 25002 } 25003 cabanaStoryCleanLabel(value) { 25004 return String(value || '').replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim(); 25005 } 25006 cabanaStoryDisplayValue(value) { 25007 if (Array.isArray(value)) return value.map((item) => this.cabanaStoryDisplayValue(item)).filter((item) => !this.cabanaStoryValueIsEmpty(item)).join(', '); 25008 if (value && typeof value === 'object') return value.name || value.nombre || value.label || value.value || ''; 25009 if (typeof value === 'boolean') return value ?'Si' : 'No'; 25010 return value === undefined || value === null ?'' : String(value); 25011 } 25012 cabanaInstallmentField(lot,field) { 25013 const config=this.state.currentCabana?.installmentConfig; 25014 if(!config?.configured||![field?.key,field?.name].some(v=>this.consKey(v)==='preoferta'))return null; 25015 const payment=window.CabanaInstallments.resolve(config,lot); 25016 return payment.enabled?'Se preoferta la cuota ('+payment.count+' cuotas)':'Se preoferta el importe total'; 25017 } 25018 cabanaLotFieldValue(lot, field, form = {}) { 25019 const installmentText=this.cabanaInstallmentField(lot,field); 25020 if(installmentText!==null)return installmentText; 25021 25022 const fieldNames = [ 25023 field?.key, 25024 field?.name, 25025 this.consKey(field?.key), 25026 this.consKey(field?.name), 25027 this.cabanaStoryCleanLabel(field?.name).toLowerCase().replace(/\s+/g, '_'), 25028 ].filter(Boolean); 25029 const formKeys = [ 25030 form?.key, 25031 form?.formKey, 25032 form?.name, 25033 this.consKey(form?.key), 25034 this.consKey(form?.name), 25035 ].filter(Boolean); 25036 const buckets = [lot?.values, lot]; 25037 formKeys.forEach((formKey) => { 25038 const bucket = lot?.subformValues?.[formKey] || lot?.subformValues?.[this.consKey(formKey)] || lot?.subformValues?.[String(formKey).replace(/-/g, '_')]; 25039 if (bucket) buckets.unshift(bucket); 25040 }); 25041 for (const bucket of buckets) { 25042 if (!bucket || typeof bucket !== 'object') continue; 25043 for (const name of fieldNames) { 25044 if (bucket[name] !== undefined && bucket[name] !== null && bucket[name] !== '') return bucket[name]; 25045 const hit = Object.keys(bucket).find((key) => this.consKey(key) === this.consKey(name)); 25046 if (hit && bucket[hit] !== undefined && bucket[hit] !== null && bucket[hit] !== '') return bucket[hit]; 25047 } 25048 } 25049 return ''; 25050 } 25051 cabanaLotOnCardFacts(lot, remate = null, limit = 4) { 25052 const current = remate || this.state.currentCabana || {}; 25053 const lotCategoryKey = lot?.categoryId || lot?.categoryName || lot?.categoriaNombre || lot?.nombre_categoria || lot?.categoria || this.cabanaLotValue(lot, ['categoriaNombre', 'nombre_categoria', 'categoria', 'categoryName'], ''); 25054 const schema = this.cabanaRemateSchema(current).find((category) => this.consKey(category?.categoryId || category?.categoryName || category?.categoria || category?.name) === this.consKey(lotCategoryKey)); 25055 if (!schema) return []; 25056 const forms = [ 25057 ...((schema.fields || []).length ?[{ name: 'Datos principales', displayOrder: 0, isActive: true, fields: schema.fields }] : []), 25058 ...(schema.forms || []), 25059 ...(schema.subforms || []), 25060 ]; 25061 return forms 25062 .filter((form) => form?.isActive !== false) 25063 .sort((a, b) =>
25063 Number(a?.displayOrder || 0) - Number(b?.displayOrder || 0)) 25064 .flatMap((form) => (form?.fields || []) 25065 .filter((field) => field?.showOnCard === true || field?.mostrarTarjeta === true) 25066 .sort((a, b) => Number(a?.order || a?.displayOrder || 0) - Number(b?.order || b?.displayOrder || 0)) 25067 .map((field) => { 25068 const value = this.cabanaStoryDisplayValue(this.cabanaLotFieldValue(lot, field, form)); 25069 return { label: String(field?.name || this.cabanaStoryCleanLabel(field?.key) || 'Dato').toUpperCase(), value }; 25070 })) 25071 .filter((fact) => !this.cabanaStoryValueIsEmpty(fact.value)) 25072 .slice(0, limit); 25073 } 25074 cabanaLotShareTitle(lot = null) { 25075 const current = this.state.currentCabana || {}; 25076 const identity = window.CabanaLotIdentity.resolve(lot, current); 25077 const lotNumber = String(identity?.number || this.cabanaLotNumber(lot) || '').trim(); 25078 const lotLabel = String(identity?.label || 'Lote').trim(); 25079 const rp = String(this.cabanaAnalyticsRpId(lot) || '').trim(); 25080 const auctionTitle = current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabaña'; 25081 return { 25082 lotNumber, 25083 lotLabel, 25084 rp, 25085 title: [lotNumber ?lotLabel + ' ' + lotNumber : lotLabel, rp ?'RP ' + rp : ''].filter(Boolean).join(' - '), 25086 auctionTitle, 25087 }; 25088 } 25089 loadCabanaCanvasImage(src) { 25090 return new Promise((resolve) => { 25091 if (!src) return resolve(null); 25092 const image = new Image(); 25093 image.crossOrigin = 'anonymous'; 25094 image.onload = () => resolve(image); 25095 image.onerror = () => resolve(null); 25096 image.src = this.cabanaCanvasAssetUrl(src); 25097 }); 25098 } 25099 cabanaCanvasAssetUrl(src) { 25100 const value = String(src || '').trim(); 25101 if (!value || /^(data|blob):/i.test(value)) return value; 25102 if (/^assets\//i.test(value)) return '/' + value; 25103 try { 25104 const url = new URL(value, window.location.href); 25105 if (url.origin === window.location.origin) return url.href; 25106 return '/api/image-proxy?url=' + encodeURIComponent(url.href); 25107 } catch { 25108 return value; 25109 } 25110 } 25111 cabanaStoryMediaUrl(lot = null) { 25112 const images = this.cabanaLotImages(lot); 25113 const videos = this.cabanaLotVideos(lot); 25114 const candidates = [ 25115 this.cabanaLotSelectedCoverUrl(lot), 25116 images[0]?.url, 25117 lot?.coverImage?.url, 25118 lot?.coverImage?.downloadUrl, 25119 lot?.cover_image?.url, 25120 this.cabanaLotValue(lot, ['coverImageUrl', 'thumbnailUrl', 'thumbnail', 'imagen', 'imageUrl'], ''), 25121 videos[0]?.thumb, 25122 document.querySelector('.dc-cabana-video-frame img.dc-media-image')?.currentSrc, 25123 document.querySelector('.dc-cabana-lot-player')?.poster, 25124 ]; 25125 return window.resolveCabanaMediaUrl(candidates.find((item) => String(item || '').trim())); 25126 } 25127 cabanaStoryCabanaLogoUrl(lot = null, current = this.state.currentCabana || {}) { 25128 const participants = current ?this.cabanaRemateParticipants(current) : []; 25129 const selected = participants.find((cabana) => 25130 (lot?.cabanaKey && cabana.key === lot.cabanaKey) || 25131 (lot?.cabanaId && String(cabana.id || '') === String(lot.cabanaId)) || 25132 (lot?.cabanaNombre && cabana.name === lot.cabanaNombre) || 25133 (lot?.cabanaName && cabana.name === lot.cabanaName) 25134 ) || participants.find((cabana) => cabana.type === 'principal') || participants[0] || {}; 25135 return window.resolveCabanaMediaUrl(lot?.cabanaLogoDataUrl || selected.logoDataUrl || selected.logoUrl || selected.logo || selected.imageUrl || selected.logoWhiteDataUrl || lot?.cabanaLogoWhiteDataUrl); 25136 } 25137 cabanaStoryCabanaLogoNeedsDarkTint(lot = null, current = this.state.currentCabana || {}) { 25138 const participants = current ?this.cabanaRemateParticipants(current) : []; 25139 const selected = participants.find((cabana) => 25140 (lot?.cabanaKey && cabana.key === lot.cabanaKey) || 25141 (lot?.cabanaId && String(cabana.id || '') === String(lot.cabanaId)) || 25142 (lot?.cabanaNombre && cabana.name === lot.cabanaNombre) || 25143 (lot?.cabanaName && cabana.name === lot.cabanaName) 25144 ) || participants.find((cabana) => cabana.type === 'principal') || participants[0] || {}; 25145 const colorLogo = lot?.cabanaLogoDataUrl || selected.logoDataUrl || selected.logoUrl || selected.logo || selected.imageUrl || ''; 25146 const whiteLogo = selected.logoWhiteDataUrl || lot?.cabanaLogoWhiteDataUrl || ''; 25147 return !colorLogo && !!whiteLogo; 25148 } 25149 drawCabanaRoundRect(ctx, x, y, width, height, radius) { 25150 const r = Math.min(radius, width / 2, height / 2); 25151 ctx.beginPath(); 25152 ctx.moveTo(x + r, y); 25153 ctx.arcTo(x + width, y, x + width, y + height, r); 25154 ctx.arcTo(x + width, y + height, x, y + height, r); 25155 ctx.arcTo(x, y + height, x, y, r); 25156 ctx.arcTo(x, y, x + width, y, r); 25157 ctx.closePath(); 25158 } 25159 drawCabanaImageCover(ctx, image, x, y, width, height, radius = 0) { 25160 if (!image) return;
25161 const scale = Math.max(width / image.naturalWidth, height / image.naturalHeight); 25162 const sw = width / scale; 25163 const sh = height / scale; 25164 const sx = (image.naturalWidth - sw) / 2; 25165 const sy = (image.naturalHeight - sh) / 2; 25166 ctx.save(); 25167 if (radius) { 25168 this.drawCabanaRoundRect(ctx, x, y, width, height, radius); 25169 ctx.clip(); 25170 } 25171 ctx.drawImage(image, sx, sy, sw, sh, x, y, width, height); 25172 ctx.restore(); 25173 } 25174 drawCabanaImageContain(ctx, image, x, y, width, height, filter = '') { 25175 if (!image) return; 25176 const scale = Math.min(width / image.naturalWidth, height / image.naturalHeight); 25177 const dw = image.naturalWidth * scale; 25178 const dh = image.naturalHeight * scale; 25179 ctx.save(); 25180 if (filter) ctx.filter = filter; 25181 ctx.drawImage(image, x + (width - dw) / 2, y + (height - dh) / 2, dw, dh); 25182 ctx.restore(); 25183 } 25184 drawCabanaImageTintContain(ctx, image, x, y, width, height, color = '#ffffff') { 25185 if (!image) return; 25186 const scale = Math.min(width / image.naturalWidth, height / image.naturalHeight); 25187 const dw = image.naturalWidth * scale; 25188 const dh = image.naturalHeight * scale; 25189 const dx = x + (width - dw) / 2; 25190 const dy = y + (height - dh) / 2; 25191 const mask = document.createElement('canvas'); 25192 mask.width = Math.max(1, Math.ceil(dw)); 25193 mask.height = Math.max(1, Math.ceil(dh)); 25194 const maskCtx = mask.getContext('2d'); 25195 maskCtx.drawImage(image, 0, 0, mask.width, mask.height); 25196 maskCtx.globalCompositeOperation = 'source-in'; 25197 maskCtx.fillStyle = color; 25198 maskCtx.fillRect(0, 0, mask.width, mask.height); 25199 ctx.drawImage(mask, dx, dy, dw, dh); 25200 } 25201 drawCabanaImageStoryFit(ctx, image, x, y, maxWidth, maxHeight, radius = 0) { 25202 if (!image) return { x, y, width: maxWidth, height: maxHeight, bottom: y + maxHeight }; 25203 const scale = Math.min(maxWidth / image.naturalWidth, maxHeight / image.naturalHeight); 25204 const width = image.naturalWidth * scale; 25205 const height = image.naturalHeight * scale; 25206 const dx = x + (maxWidth - width) / 2; 25207 ctx.save(); 25208 if (radius) { 25209 this.drawCabanaRoundRect(ctx, dx, y, width, height, radius); 25210 ctx.clip(); 25211 } 25212 ctx.drawImage(image, dx, y, width, height); 25213 ctx.restore(); 25214 return { x: dx, y, width, height, bottom: y + height }; 25215 } 25216 wrapCabanaCanvasText(ctx, text, x, y, maxWidth, lineHeight, maxLines = 3) { 25217 const words = String(text || '').split(/\s+/).filter(Boolean); 25218 const lines = []; 25219 let line = ''; 25220 words.forEach((word) => { 25221 const test = line ?line + ' ' + word : word; 25222 if (ctx.measureText(test).width > maxWidth && line) { 25223 lines.push(line); 25224 line = word; 25225 } else { 25226 line = test; 25227 } 25228 }); 25229 if (line) lines.push(line); 25230 const visible = lines.slice(0, maxLines); 25231 if (lines.length > maxLines) { 25232 let last = visible[visible.length - 1] || ''; 25233 while (ctx.measureText(last + '...').width > maxWidth && last.length > 0) last = last.slice(0, -1); 25234 visible[visible.length - 1] = last.trim() + '...'; 25235 } 25236 visible.forEach((row, index) => ctx.fillText(row, x, y + index * lineHeight)); 25237 return y + visible.length * lineHeight; 25238 } 25239 async createCabanaLotStoryBlob(lot = null, context = {}) { 25240 const target = lot || this.state.currentCabanaLot; 25241 if (!target) throw new Error('lot-missing'); 25242 const current = context?.remate || this.state.currentCabana || {}; 25243 const { lotNumber, lotLabel, rp, auctionTitle } = this.cabanaLotShareTitle(target); 25244 const category = String(target?.categoryName || target?.categoriaNombre || this.cabanaLotValue(target, ['categoriaNombre', 'nombre_categoria', 'categoria', 'categoryName'], 'Reproductor')); 25245 const participants = current ?this.cabanaRemateParticipants(current) : []; 25246 const selectedCabana = participants.find((cabana) => 25247 (target?.cabanaKey && cabana.key === target.cabanaKey) || 25248 (target?.cabanaId && String(cabana.id || '') === String(target.cabanaId)) || 25249 (target?.cabanaNombre && cabana.name === target.cabanaNombre) || 25250 (target?.cabanaName && cabana.name === target.cabanaName) 25251 ) || participants.find((cabana) => cabana.type === 'principal') || participants[0] || {}; 25252 const cabanaName = String(target?.cabanaNombre || target?.cabanaName || selectedCabana?.name || selectedCabana?.nombre || auctionTitle || 'Rosgan'); 25253 const mediaUrl = this.cabanaStoryMediaUrl(target); 25254 const mainLogoUrl = this.cabanaStoryCabanaLogoUrl(target, current); 25255 const mainLogoNeedsDarkTint = this.cabanaStoryCabanaLogoNeedsDarkTint(target, current); 25256 const rosganLogoUrl = '/assets/rosgan-logo-blue.png'; 25257 const consignatariaRecord = (this.state.consignatarias || []).find((candidate) => [candidate?.id, candidate?.consignatariaId, candidate?.slug, candidate?.name, candidate?.nombre].some((value) => this.consKey(value) && this.consKey(value) === this.consKey(current?.consignatariaId || current?.consignataria || current?.nombre_cuenta))); 25258 const consignatariaRaw = current?.consignataria || current?.nombre_cuenta || current?.consignatariaId || consignatariaRecord?.slug || ''; 25259 const consignatariaName = (consignatariaRecord ?this.consName(consignatariaRecord) : '') || current?.consignataria || current?.nombre_cuenta || current?.nombre_consignatario || current?.consignatariaName || 'Rosgan'; 25260 const isLaCassina = this.state.publicAdvanceMode === 'lacassina' || /cassina/i.test(String(auctionTitle || current?.nombre_remate || current?.titulo || current?.title || ''));
25261 let consignatariaLogoUrl = this.consWhiteLogo(consignatariaRaw || consignatariaName || current?.nombre_cuenta || '') || this.consLogo(consignatariaRaw || consignatariaName || '') || consignatariaRecord?.logoUrl || consignatariaRecord?.logo || consignatariaRecord?.imageUrl || ''; 25262 if (isLaCassina || /colombo/i.test(consignatariaName)) consignatariaLogoUrl = '/assets/consignatarias-white/colombo-y-colombo-s-a.png'; 25263 const consignatariaColor = (isLaCassina || /colombo/i.test(consignatariaName)) ?'#D71945' : this.consBrandColor(consignatariaRaw || consignatariaName || 'Rosgan'); 25264 const consignatariaColorDeep = (isLaCassina || /colombo/i.test(consignatariaName)) ?'#A80F35' : this.shade(consignatariaColor, -0.2); 25265 const lotContact = context?.contact || this.cabanaContactData(current); 25266 const lotContactName = lotContact.name || 'Equipo Rosgan'; 25267 const lotContactPhone = lotContact.phone || 'rosgan.app'; 25268 const [mediaImage, mainLogo, rosganLogo, consignatariaLogo, faviconLogo, storyBackground] = await Promise.all([ 25269 this.loadCabanaCanvasImage(mediaUrl), 25270 this.loadCabanaCanvasImage(mainLogoUrl), 25271 this.loadCabanaCanvasImage(rosganLogoUrl), 25272 this.loadCabanaCanvasImage(consignatariaLogoUrl), 25273 this.loadCabanaCanvasImage('/favicon.png'), 25274 isLaCassina ?this.loadCabanaCanvasImage('/assets/lacassina-story-background.png') : Promise.resolve(null), 25275 ]); 25276 const canvas = document.createElement('canvas'); 25277 canvas.width = 1080; 25278 canvas.height = 1920; 25279 const ctx = canvas.getContext('2d'); 25280 ctx.fillStyle = '#ffffff'; 25281 ctx.fillRect(0, 0, 1080, 1920); 25282 ctx.fillStyle = '#ffffff'; 25283 this.drawCabanaRoundRect(ctx, 54, 86, 972, 1738, 42); 25284 ctx.fill(); 25285 if (storyBackground) { 25286 ctx.save(); 25287 ctx.globalAlpha = 1; 25288 this.drawCabanaImageCover(ctx, storyBackground, 54, 86, 972, 1738, 42); 25289 ctx.restore(); 25290 } else { 25291 const bg = ctx.createLinearGradient(54, 86, 1026, 1824); 25292 bg.addColorStop(0, '#e8f6ff'); 25293 bg.addColorStop(0.56, '#f4d39b'); 25294 bg.addColorStop(1, '#f6fafc'); 25295 ctx.fillStyle = bg; 25296 this.drawCabanaRoundRect(ctx, 54, 86, 972, 1738, 42); 25297 ctx.fill(); 25298 } 25299 ctx.strokeStyle = '#d8e4ef'; 25300 ctx.lineWidth = 2; 25301 this.drawCabanaRoundRect(ctx, 54, 86, 972, 1738, 42); 25302 ctx.stroke(); 25303 if (rosganLogo) this.drawCabanaImageContain(ctx, rosganLogo, 82, 190, 246, 76); 25304 else { 25305 ctx.fillStyle = '#17304d'; 25306 ctx.font = '900 38px Archivo, Arial, sans-serif'; 25307 ctx.fillText('ROSGAN', 82, 238); 25308 } 25309 if (mainLogo) { 25310 if (mainLogoNeedsDarkTint) this.drawCabanaImageTintContain(ctx, mainLogo, 610, 174, 388, 132, '#17304d'); 25311 else this.drawCabanaImageContain(ctx, mainLogo, 610, 174, 388, 132); 25312 } 25313 ctx.font = '900 24px Archivo, Arial, sans-serif'; 25314 ctx.fillStyle = '#073763'; 25315 ctx.fillText('REMATES DE CABANAS', 82, 370); 25316 ctx.strokeStyle = '#073763'; 25317 ctx.lineWidth = 2; 25318 ctx.beginPath(); 25319 ctx.moveTo(82, 400); 25320 ctx.lineTo(998, 400); 25321 ctx.stroke(); 25322 let mediaBox = { x: 82, y: 436, width: 916, height: 600, bottom: 1036 }; 25323 if (mediaImage) { 25324 mediaBox = this.drawCabanaImageStoryFit(ctx, mediaImage, 82, 436, 916, 600, 28); 25325 } else { 25326 const ph = ctx.createLinearGradient(82, 436, 998, 1036); 25327 ph.addColorStop(0, '#12375c'); 25328 ph.addColorStop(1, '#0d8fb2'); 25329 ctx.fillStyle = ph; 25330 this.drawCabanaRoundRect(ctx, 82, 436, 916, 600, 30); 25331 ctx.fill(); 25332 ctx.fillStyle = 'rgba(255,255,255,.86)'; 25333 ctx.font = '900 74px Archivo, Arial, sans-serif'; 25334 ctx.fillText(rp ?'RP ' + rp : 'Rosgan', 132, 780); 25335 } 25336 ctx.strokeStyle = '#d7e3ef'; 25337 ctx.lineWidth = 2; 25338 this.drawCabanaRoundRect(ctx, mediaBox.x, mediaBox.y, mediaBox.width, mediaBox.height, 28); 25339 ctx.stroke(); 25340 const imageFade = ctx.createLinearGradient(mediaBox.x, mediaBox.bottom - 324, mediaBox.x, mediaBox.bottom); 25341 imageFade.addColorStop(0, 'rgba(0,0,0,0)'); 25342 imageFade.addColorStop(1, 'rgba(0,24,48,.78)'); 25343 ctx.fillStyle = imageFade; 25344 this.drawCabanaRoundRect(ctx, mediaBox.x, mediaBox.y, mediaBox.width, mediaBox.height, 28); 25345 ctx.fill(); 25346 ctx.fillStyle = '#ffffff'; 25347 ctx.font = '900 66px Archivo, Arial, sans-serif'; 25348 const lotTitle = lotNumber ?lotLabel + ' ' + lotNumber : lotLabel; 25349 ctx.fillText(lotTitle, 132, mediaBox.bottom - 144); 25350 ctx.font = '900 40px Archivo, Arial, sans-serif'; 25351 ctx.fillText(rp ?'RP ' + rp : 'RP', 132, mediaBox.bottom - 90); 25352 ctx.font = '800 25px Archivo, Arial, sans-serif'; 25353 ctx.fillStyle = 'rgba(255,255,255,.86)'; 25354 this.wrapCabanaCanvasText(ctx, category, 132, mediaBox.bottom - 50, 760, 33, 2); 25355 const detailY = mediaBox.bottom + 48; 25356 const detailPanelH = 304; 25357 ctx.fillStyle = 'rgba(254,251,246,.9)'; 25358 this.drawCabanaRoundRect(ctx, 82, detailY, 916, detailPanelH, 30); 25359 ctx.fill(); 25360 ctx.strokeStyle = 'rgba(253,250,245,.72)'; 25361 ctx.lineWidth = 2; 25362 this.drawCabanaRoundRect(ctx, 82, detailY, 916, detailPanelH, 30); 25363 ctx.stroke(); 25364 ctx.textAlign = 'center'; 25365 ctx.fillStyle = '#17304d'; 25366 ctx.font = '900 34px Archivo, Arial, sans-serif'; 25367 ctx.fillText(cabanaName, 540, detailY + 60); 25368 ctx.fillStyle = '#6b7b90'; 25369 ctx.font = '800 21px Archivo, Arial, sans-serif'; 25370 ctx.fillText(auctionTitle, 540, detailY + 106); 25371 ctx.strokeStyle = '#e0e9f2'; 25372 ctx.lineWidth = 1.5; 25373 ctx.beginPath(); 25374 ctx.moveTo(154, detailY + 142); 25375 ctx.lineTo(926, detailY + 142); 25376 ctx.stroke(); 25377 const onCardFacts = this.cabanaLotOnCardFacts(target, current, 4); 25378 const fallbackFacts = [ 25379 ['N', this.cabanaLotValue(target, ['numero', 'nro', 'nro_animal', 'orden_final'], '')], 25380 ['Caravana', this.cabanaLotValue(target, ['caravana', 'caravanaNumero', 'caravana_numero'], '')], 25381 ['Padre', this.cabanaLotValue(target, ['padre', 'nombrePadre'], '')], 25382 ['Nacimiento', this.cabanaLotValue(target, ['nacimiento', 'fecha_nacimiento', 'anio_nacimiento'], '')], 25383 ['C. escrotal', this.cabanaLotValue(target, ['ce', 'circunferenciaEscrotal'], '')], 25384 ['Peso', this.cabanaLotValue(target, ['peso', 'pesoPromedio', 'peso_promedio'], '')], 25385 ].filter(([, value]) => String(value || '').trim()).slice(0, 4).map(([label, value]) => ({ label, value })); 25386 const facts = onCardFacts.length ?onCardFacts : fallbackFacts;
25387 facts.forEach((fact, index) => { 25388 const { label, value } = fact; 25389 const y = detailY + 176 + index * 38; 25390 if (index > 0) { 25391 ctx.strokeStyle = 'rgba(23,48,77,.18)'; 25392 ctx.lineWidth = 1; 25393 ctx.beginPath(); 25394 ctx.moveTo(126, y - 22); 25395 ctx.lineTo(954, y - 22); 25396 ctx.stroke(); 25397 } 25398 ctx.textAlign = 'left'; 25399 ctx.fillStyle = '#7d8da3'; 25400 ctx.font = '900 18px Archivo, Arial, sans-serif'; 25401 ctx.fillText(String(label).toUpperCase(), 126, y); 25402 ctx.textAlign = 'right'; 25403 ctx.fillStyle = '#0b315d'; 25404 ctx.font = '900 25px Archivo, Arial, sans-serif'; 25405 ctx.fillText(String(value), 954, y + 2); 25406 }); 25407 ctx.textAlign = 'left'; 25408 const consignatariaPanelY = detailY + detailPanelH + 34; 25409 const consignatariaPanelH = 284; 25410 const consPanel = ctx.createLinearGradient(82, consignatariaPanelY, 998, consignatariaPanelY + consignatariaPanelH); 25411 consPanel.addColorStop(0, consignatariaColor); 25412 consPanel.addColorStop(1, consignatariaColorDeep); 25413 ctx.save(); 25414 ctx.globalAlpha = 0.94; 25415 ctx.fillStyle = consPanel; 25416 this.drawCabanaRoundRect(ctx, 82, consignatariaPanelY, 916, consignatariaPanelH, 30); 25417 ctx.fill(); 25418 ctx.restore(); 25419 ctx.strokeStyle = 'rgba(255,255,255,.22)'; 25420 ctx.lineWidth = 1.5; 25421 this.drawCabanaRoundRect(ctx, 82, consignatariaPanelY, 916, consignatariaPanelH, 30); 25422 ctx.stroke(); 25423 if (consignatariaLogo) this.drawCabanaImageTintContain(ctx, consignatariaLogo, 144, consignatariaPanelY + 52, 104, 96); 25424 ctx.textAlign = 'center'; 25425 ctx.fillStyle = 'rgba(255,255,255,.66)'; 25426 ctx.font = '900 18px Archivo, Arial, sans-serif'; 25427 ctx.fillText('CONSIGNATARIA', 620, consignatariaPanelY + 68); 25428 ctx.fillStyle = '#ffffff'; 25429 ctx.font = '900 38px Archivo, Arial, sans-serif'; 25430 ctx.fillText(consignatariaName, 620, consignatariaPanelY + 118); 25431 ctx.strokeStyle = 'rgba(255,255,255,.24)'; 25432 ctx.beginPath(); 25433 ctx.moveTo(126, consignatariaPanelY + 164); 25434 ctx.lineTo(954, consignatariaPanelY + 164); 25435 ctx.stroke(); 25436 ctx.fillStyle = 'rgba(255,255,255,.72)'; 25437 ctx.font = '900 17px Archivo, Arial, sans-serif'; 25438 ctx.fillText('CONTACTO DEL LOTE', 310, consignatariaPanelY + 206); 25439 ctx.fillText('TELEFONO', 770, consignatariaPanelY + 206); 25440 ctx.fillStyle = '#ffffff'; 25441 ctx.font = '900 28px Archivo, Arial, sans-serif'; 25442 ctx.fillText(lotContactName, 310, consignatariaPanelY + 240); 25443 ctx.font = '900 27px Archivo, Arial, sans-serif'; 25444 ctx.fillText(lotContactPhone, 770, consignatariaPanelY + 240); 25445 ctx.textAlign = 'left'; 25446 ctx.fillStyle = '#17304d'; 25447 ctx.font = '900 32px Archivo, Arial, sans-serif'; 25448 ctx.shadowColor = 'transparent'; 25449 ctx.shadowBlur = 0; 25450 ctx.shadowOffsetY = 0; 25451 const footerText = 'rosgan.app'; 25452 const footerIconSize = faviconLogo ?50 : 0; 25453 const footerGap = faviconLogo ?14 : 0; 25454 const footerTextWidth = ctx.measureText(footerText).width; 25455 const footerTotalWidth = footerIconSize + footerGap + footerTextWidth; 25456 const footerX = 540 - footerTotalWidth / 2; 25457 const footerY = 1708; 25458 if (faviconLogo) this.drawCabanaImageContain(ctx, faviconLogo, footerX, footerY - 39, footerIconSize, footerIconSize); 25459 ctx.fillText(footerText, footerX + footerIconSize + footerGap, footerY); 25460 ctx.shadowColor = 'transparent'; 25461 ctx.shadowBlur = 0; 25462 ctx.shadowOffsetY = 0; 25463 return await new Promise((resolve, reject) => { 25464 try { 25465 canvas.toBlob((blob) => blob ?resolve(blob) : reject(new Error('canvas-empty')), 'image/png', 0.94); 25466 } catch (error) { 25467 reject(error); 25468 } 25469 }); 25470 } 25471 async createCabanaPriceBoardStoryBlob(rows = [], rosgan = null) { 25472 const visibleRows = (Array.isArray(rows) ?rows : []).filter((row) => rosgan || Number(row?.count || 0) > 0); 25473 if (!visibleRows.length) throw new Error('No hay precios vendidos para exportar.'); 25474 const current = rosgan?.remate || this.state.currentCabana || {}; 25475 const auctionTitle = current?.nombre_remate || current?.titulo || current?.title || 'La Cassina'; 25476 const participants = rosgan?[]:this.cabanaRemateParticipants(current); 25477 const mainParticipant = participants.find((cabana) => cabana.type === 'principal') || participants[0] || {}; 25478 const mainLogoUrl = mainParticipant.logoDataUrl || mainParticipant.logoUrl || mainParticipant.logo || mainParticipant.imageUrl || ''; 25479 const rosganLogoUrl = '/assets/rosgan-logo-blue.png'; 25480 const consignatariaRecord = (this.state.consignatarias || []).find((candidate) => [candidate?.id, candidate?.consignatariaId, candidate?.slug, candidate?.name, candidate?.nombre].some((value) => this.consKey(value) && this.consKey(value) === this.consKey(current?.consignatariaId || current?.consignataria || current?.nombre_cuenta))); 25481 const consignatariaRaw = current?.consignataria || current?.nombre_cuenta || current?.consignatariaId || consignatariaRecord?.slug || ''; 25482 const consignatariaName = rosgan?'Mercado Ganadero · Rosgan':(consignatariaRecord ?this.consName(consignatariaRecord) : '') || current?.consignataria || current?.nombre_cuenta || current?.nombre_consignatario || current?.consignatariaName || 'Colombo y Colombo S.A.';
25483 let consignatariaLogoUrl = this.consWhiteLogo(consignatariaRaw || consignatariaName) || this.consLogo(consignatariaRaw || consignatariaName) || consignatariaRecord?.logoUrl || consignatariaRecord?.logo || ''; 25484 if (/cassina/i.test(auctionTitle) || /colombo/i.test(consignatariaName)) consignatariaLogoUrl = '/assets/consignatarias-white/colombo-y-colombo-s-a.png'; 25485 const [mainLogo, rosganLogo, consignatariaLogo, faviconLogo, storyBackground] = await Promise.all([ 25486 this.loadCabanaCanvasImage(mainLogoUrl), 25487 this.loadCabanaCanvasImage(rosganLogoUrl), 25488 this.loadCabanaCanvasImage(consignatariaLogoUrl), 25489 this.loadCabanaCanvasImage('/favicon.png'), 25490 rosgan?Promise.resolve(null):this.loadCabanaCanvasImage('/assets/lacassina-story-background.png'), 25491 ]); 25492 const canvas = document.createElement('canvas'); 25493 canvas.width = 1080; 25494 canvas.height = 1920; 25495 const ctx = canvas.getContext('2d'); 25496 ctx.fillStyle = '#ffffff'; 25497 ctx.fillRect(0, 0, canvas.width, canvas.height); 25498 if (storyBackground) this.drawCabanaImageCover(ctx, storyBackground, 54, 54, 972, 1812, 42); 25499 else { 25500 const fallback = ctx.createLinearGradient(54, 54, 1026, 1866); 25501 fallback.addColorStop(0, '#fffaf2'); 25502 fallback.addColorStop(0.55, '#f7fbff'); 25503 fallback.addColorStop(1, '#e6f3fb'); 25504 ctx.fillStyle = fallback; 25505 this.drawCabanaRoundRect(ctx, 54, 54, 972, 1812, 42); 25506 ctx.fill(); 25507 } 25508 ctx.strokeStyle = '#d6e3ee'; 25509 ctx.lineWidth = 2; 25510 this.drawCabanaRoundRect(ctx, 54, 54, 972, 1812, 42); 25511 ctx.stroke(); 25512 25513 if (rosgan) { 25514 const header = ctx.createLinearGradient(54, 54, 1026, 280); 25515 header.addColorStop(0, '#052e51'); header.addColorStop(1, '#096487'); 25516 ctx.fillStyle = header; this.drawCabanaRoundRect(ctx, 54, 54, 972, 218, 32); ctx.fill(); 25517 if (rosganLogo) this.drawCabanaImageTintContain(ctx, rosganLogo, 88, 94, 218, 66, '#ffffff'); 25518 ctx.fillStyle = '#ffffff'; ctx.font = '900 34px Archivo, Arial, sans-serif'; 25519 ctx.fillText('RESULTADOS DEL REMATE', 338, 137); 25520 ctx.font = '700 24px Archivo, Arial, sans-serif'; 25521 this.wrapCabanaCanvasText(ctx, auctionTitle, 84, 211, 890, 28, 1); 25522 const auctionDate = this.remateDate(current); 25523 ctx.fillStyle = '#bce7ec'; ctx.font = '600 20px Archivo, Arial, sans-serif'; 25524 ctx.fillText(auctionDate ? auctionDate.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric', timeZone: 'America/Argentina/Buenos_Aires' }) : 'Fecha a confirmar', 84, 245); 25525 ctx.fillStyle = '#48677d'; ctx.font = '800 18px Archivo, Arial, sans-serif'; 25526 ctx.fillText('CATEGORÃA', 104, 317); 25527 ctx.textAlign = 'center'; ctx.fillText('MÃXIMO', 597, 317); 25528 ctx.fillStyle = '#08796e'; ctx.fillText('PROMEDIO', 857, 317); ctx.textAlign = 'left'; 25529 } else { 25530 if (rosganLogo) this.drawCabanaImageContain(ctx, rosganLogo, 86, 108, 242, 78); 25531 else { 25532 ctx.fillStyle = '#17304d'; 25533 ctx.font = '900 38px Archivo, Arial, sans-serif'; 25534 ctx.fillText('ROSGAN', 86, 160); 25535 } 25536 if (mainLogo) this.drawCabanaImageContain(ctx, mainLogo, 624, 86, 372, 122); 25537 25538 const topConsPanel = ctx.createLinearGradient(82, 230, 998, 340); 25539 topConsPanel.addColorStop(0, rosgan?'#002e5f':'#D71945'); 25540 topConsPanel.addColorStop(1, rosgan?'#075995':'#A80F35'); 25541 ctx.fillStyle = topConsPanel; 25542 this.drawCabanaRoundRect(ctx, 82, 230, 916, 110, 26); 25543 ctx.fill(); 25544 if (consignatariaLogo) this.drawCabanaImageTintContain(ctx, consignatariaLogo, 128, 254, 96, 62); 25545 ctx.fillStyle = 'rgba(255,255,255,.68)'; 25546 ctx.font = '900 15px Archivo, Arial, sans-serif'; 25547 ctx.fillText(rosgan?'REMATES':'CONSIGNATARIA', 270, 268); 25548 ctx.fillStyle = '#ffffff'; 25549 ctx.font = '900 32px Archivo, Arial, sans-serif'; 25550 ctx.fillText(consignatariaName, 270, 310); 25551 25552 ctx.fillStyle = '#073763'; 25553 this.drawCabanaRoundRect(ctx, 82, 366, 916, 220, 32); 25554 ctx.fill(); 25555 ctx.fillStyle = '#d8c49b'; 25556 ctx.font = '900 20px Archivo, Arial, sans-serif'; 25557 ctx.letterSpacing = '3px'; 25558 ctx.fillText('ROSGAN', 126, 415); 25559 ctx.letterSpacing = '0px'; 25560 ctx.fillStyle = '#ffffff'; 25561 ctx.font = '900 51px Archivo, Arial, sans-serif'; 25562 ctx.fillText('PLACA DE PRECIOS', 126, 469); 25563 ctx.fillStyle = 'rgba(255,255,255,.84)'; 25564 ctx.font = '800 22px Archivo, Arial, sans-serif'; 25565 this.wrapCabanaCanvasText(ctx, auctionTitle, 126, 510, 820, 29, 2); 25566 ctx.fillStyle = 'rgba(255,255,255,.68)'; 25567 ctx.font = '800 16px Archivo, Arial, sans-serif'; 25568 ctx.fillText('RESULTADOS POR CATEGORIA', 126, 559); 25569 } 25570 const cardsTop = rosgan ? 338 : 620; 25571 const cardsBottom = 1760; 25572 const gap = rosgan ? 4 : 12; 25573 const compactGrid = !rosgan && visibleRows.length > 9; 25574 const columns = compactGrid ?2 : 1;
25575 const rowCount = Math.ceil(visibleRows.length / columns); 25576 const columnGap = compactGrid ?14 : 0; 25577 const cardWidth = Math.floor((916 - columnGap * (columns - 1)) / columns); 25578 const cardHeight = Math.max(rosgan ? 0 : 84, Math.min(rosgan ? 156 : 108, Math.floor((cardsBottom - cardsTop - gap * (rowCount - 1)) / rowCount))); 25579 const money = (value) => rosgan && value == null ? 'â' : '$' + new Intl.NumberFormat('es-AR', { maximumFractionDigits: 0 }).format(Number(value || 0)); 25580 visibleRows.forEach((row, index) => { 25581 const columnIndex = compactGrid ?index % columns : 0; 25582 const rowIndex = compactGrid ?Math.floor(index / columns) : index; 25583 const isLastUnpaired = compactGrid && visibleRows.length % 2 === 1 && index === visibleRows.length - 1; 25584 const x = isLastUnpaired ?82 + Math.floor((916 - cardWidth) / 2) : 82 + columnIndex * (cardWidth + columnGap); 25585 const y = cardsTop + rowIndex * (cardHeight + gap); 25586 if (rosgan) { 25587 ctx.fillStyle = index % 2 ? '#edf3f7' : '#ffffff'; 25588 this.drawCabanaRoundRect(ctx, 82, y, 916, cardHeight, 10); ctx.fill(); 25589 ctx.fillStyle = '#e0f1ee'; 25590 this.drawCabanaRoundRect(ctx, 732, y + 6, 254, cardHeight - 12, 9); ctx.fill(); 25591 ctx.fillStyle = '#082f5c'; 25592 ctx.font = '800 32px Archivo, Arial, sans-serif'; 25593 const categoryLines = ctx.measureText(row.category).width > 360 ? 2 : 1; 25594 this.wrapCabanaCanvasText(ctx, row.category, 104, y + cardHeight / 2 + 11 - (categoryLines - 1) * 18, 360, 36, 2); 25595 for (const [label, value, center] of [['MÃXIMO', row.maxValue, 597], ['PROMEDIO', row.avgValue, 857]]) { 25596 ctx.textAlign = 'center'; 25597 const text = money(value); 25598 let size = 36; 25599 ctx.font = `900 ${size}px Archivo, Arial, sans-serif`; 25600 while (ctx.measureText(text).width > 238 && size > 24) ctx.font = `900 ${--size}px Archivo, Arial, sans-serif`; 25601 ctx.fillStyle = value == null ? '#9ab0bf' : label === 'PROMEDIO' ? '#09695f' : '#073763'; 25602 ctx.fillText(text, center, y + cardHeight / 2 + size * .34); 25603 } 25604 ctx.textAlign = 'left'; 25605 return; 25606 } 25607 if (compactGrid) { 25608 ctx.save(); 25609 ctx.shadowColor = 'rgba(15,42,70,.09)'; 25610 ctx.shadowBlur = 12; 25611 ctx.shadowOffsetY = 4; 25612 ctx.fillStyle = 'rgba(255,255,255,.97)'; 25613 this.drawCabanaRoundRect(ctx, x, y, cardWidth, cardHeight, 18); 25614 ctx.fill(); 25615 ctx.restore(); 25616 ctx.strokeStyle = 'rgba(7,55,99,.13)'; 25617 ctx.lineWidth = 1.5; 25618 this.drawCabanaRoundRect(ctx, x, y, cardWidth, cardHeight, 18); 25619 ctx.stroke(); 25620 const splitX = x + Math.floor(cardWidth * .49); 25621 ctx.strokeStyle = 'rgba(7,55,99,.10)'; 25622 ctx.beginPath(); 25623 ctx.moveTo(splitX, y + 14); 25624 ctx.lineTo(splitX, y + cardHeight - 14); 25625 ctx.stroke(); 25626 ctx.fillStyle = '#6e8198'; 25627 ctx.font = '900 9px Archivo, Arial, sans-serif'; 25628 ctx.fillText('CATEGORIA', x + 18, y + 21); 25629 ctx.fillStyle = '#082f5c'; 25630 ctx.font = `900 ${cardHeight >= 100 ?16 : 14}px Archivo, Arial, sans-serif`; 25631 this.wrapCabanaCanvasText(ctx, row.category, x + 18, y + 43, splitX - x - 32, cardHeight >= 100 ?18 : 16, 3); 25632 const metricX = splitX + 16; 25633 ctx.fillStyle = '#6e8198'; 25634 ctx.font = '900 9px Archivo, Arial, sans-serif'; 25635 ctx.fillText('PROMEDIO', metricX, y + 21); 25636 ctx.fillStyle = '#073763'; 25637 ctx.font = `900 ${cardHeight >= 100 ?22 : 19}px Archivo, Arial, sans-serif`; 25638 ctx.fillText(money(row.avgValue), metricX, y + 47); 25639 ctx.fillStyle = '#6e8198'; 25640 ctx.font = '900 9px Archivo, Arial, sans-serif'; 25641 ctx.fillText('MAXIMO', metricX, y + cardHeight - 31); 25642 ctx.fillStyle = '#405b78'; 25643 ctx.font = `900 ${cardHeight >= 100 ?17 : 15}px Archivo, Arial, sans-serif`; 25644 ctx.fillText(money(row.maxValue), metricX, y + cardHeight - 12); 25645 return; 25646 } 25647 ctx.save(); 25648 ctx.shadowColor = 'rgba(15,42,70,.10)'; 25649 ctx.shadowBlur = 14; 25650 ctx.shadowOffsetY = 5; 25651 ctx.fillStyle = 'rgba(255,255,255,.96)'; 25652 this.drawCabanaRoundRect(ctx, 82, y, 916, cardHeight, 19); 25653 ctx.fill();
25654 ctx.restore(); 25655 ctx.strokeStyle = 'rgba(7,55,99,.11)'; 25656 ctx.lineWidth = 1.5; 25657 this.drawCabanaRoundRect(ctx, 82, y, 916, cardHeight, 19); 25658 ctx.stroke(); 25659 ctx.fillStyle = '#6e8198'; 25660 ctx.font = '900 11px Archivo, Arial, sans-serif'; 25661 ctx.fillText('CATEGORIA', 112, y + 25); 25662 ctx.fillStyle = '#082f5c'; 25663 const categoryFont = cardHeight >= 100 ?20 : 17; 25664 const categoryLine = cardHeight >= 100 ?22 : 19; 25665 ctx.font = `900 ${categoryFont}px Archivo, Arial, sans-serif`; 25666 this.wrapCabanaCanvasText(ctx, row.category, 112, y + 51, 366, categoryLine, 3); 25667 const priceX = 510; 25668 const priceY = y + 9; 25669 const priceW = 452; 25670 const priceH = cardHeight - 18; 25671 ctx.fillStyle = '#073763'; 25672 this.drawCabanaRoundRect(ctx, priceX, priceY, priceW, priceH, 15); 25673 ctx.fill(); 25674 ctx.fillStyle = 'rgba(255,255,255,.66)'; 25675 ctx.font = '900 10px Archivo, Arial, sans-serif'; 25676 ctx.fillText('PRECIO PROMEDIO', priceX + 24, priceY + 22); 25677 ctx.fillStyle = '#ffffff'; 25678 ctx.font = `900 ${cardHeight >= 100 ?29 : 25}px Archivo, Arial, sans-serif`; 25679 ctx.fillText(money(row.avgValue), priceX + 24, priceY + 54); 25680 ctx.strokeStyle = 'rgba(255,255,255,.18)'; 25681 ctx.lineWidth = 1; 25682 ctx.beginPath(); 25683 ctx.moveTo(priceX + 24, priceY + priceH - 30); 25684 ctx.lineTo(priceX + priceW - 24, priceY + priceH - 30); 25685 ctx.stroke(); 25686 ctx.fillStyle = 'rgba(255,255,255,.72)'; 25687 ctx.font = '800 10px Archivo, Arial, sans-serif'; 25688 ctx.fillText('MAXIMO', priceX + 24, priceY + priceH - 10); 25689 ctx.textAlign = 'right'; 25690 ctx.fillStyle = '#ffffff'; 25691 ctx.font = '900 16px Archivo, Arial, sans-serif'; 25692 ctx.fillText(money(row.maxValue), priceX + priceW - 24, priceY + priceH - 9); 25693 ctx.textAlign = 'left'; 25694 }); 25695 25696 ctx.fillStyle = '#17304d'; 25697 ctx.font = '900 28px Archivo, Arial, sans-serif'; 25698 const footer = 'rosgan.app'; 25699 const footerWidth = ctx.measureText(footer).width; 25700 const iconSize = faviconLogo ?42 : 0; 25701 const footerX = 540 - (footerWidth + iconSize + (iconSize ?12 : 0)) / 2; 25702 if (faviconLogo) this.drawCabanaImageContain(ctx, faviconLogo, footerX, 1810, iconSize, iconSize); 25703 ctx.fillText(footer, footerX + iconSize + (iconSize ?12 : 0), 1842); 25704 return await new Promise((resolve, reject) => { 25705 try { canvas.toBlob((blob) => blob ?resolve(blob) : reject(new Error('canvas-empty')), 'image/png', 0.94); } 25706 catch (error) { reject(error); } 25707 }); 25708 } 25709 async downloadRosganPriceStory(rows,model){ 25710 if(this.state.rosganPriceStoryBusy)return; 25711 if(!rows?.length){this.showToast('Para generar la Story, cargá al menos un lote vendido con precio en Comercial.');return;} 25712 this.setState({rosganPriceStoryBusy:true,rosganPriceExportMessage:'Generando imagenâ¦'}); 25713 try{const pages=window.RosganPriceBoard.pages(model);for(let i=0;i<pages.length;i++){ 25714 const blob=await window.RosganPriceBoard.png({...pages[i],pageLabel:pages.length>1?(i+1)+' / '+pages.length:''}); 25715 this.downloadCabanaBlob(blob,this.cabanaExportFileBase(this.state.currentRemate)+'-precios-'+model.mode+'-'+(i+1)+'.png'); 25716 }this.setState({rosganPriceExportMessage:'Placa de precios descargada.'});this.showToast('Placa de precios descargada.');} 25717 catch(error){this.setState({rosganPriceExportMessage:error.message||'No se pudo generar la placa.'});this.showToast(error.message||'No se pudo generar la placa.');} 25718 finally{this.setState({rosganPriceStoryBusy:false});} 25719 } 25720 async downloadCabanaPriceBoardStory(rows = [], model = null) { 25721 if (this.state.cabanaPriceStoryBusy) return; 25722 this.setState({ cabanaPriceStoryBusy: true }); 25723 try { 25724 const allRows = (Array.isArray(rows) ?rows : []).filter((row) => Number(row?.count || 0) > 0); 25725 if (!allRows.length) throw new Error('No hay precios vendidos para exportar.'); 25726 if(model){const pages=window.RosganPriceBoard.pages(model);for(let i=0;i<pages.length;i++){const blob=await window.RosganPriceBoard.png(pages[i]);this.downloadCabanaBlob(blob,this.cabanaExportFileBase(this.state.currentCabana)+'-precios-'+(i+1)+'.png');}this.showToast('Placa de precios descargada.');return;} 25727 const blob = await this.createCabanaPriceBoardStoryBlob(allRows); 25728 const remate = this.state.currentCabana || {}; 25729 this.downloadCabanaBlob(blob, this.cabanaExportFileBase(remate) + '-placa-precios-story-9x16.png'); 25730 this.showToast('Story 9:16 descargada.'); 25731 } catch (error) { 25732 this.showToast(error?.message || 'No se pudo generar la Story.'); 25733 } finally { 25734 this.setState({ cabanaPriceStoryBusy: false }); 25735 } 25736 } 25737 downloadCabanaBlob(blob, fileName) { 25738 const url = URL.createObjectURL(blob); 25739 const link = document.createElement('a'); 25740 link.href = url; 25741 link.download = fileName; 25742 document.body.appendChild(link); 25743 link.click(); 25744 link.remove(); 25745 setTimeout(() => URL.revokeObjectURL(url), 1200); 25746 } 25747 cabanaExportEscape(value) { 25748 return String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); 25749 } 25750 cabanaExportFileBase(remate = this.state.currentCabana || {}) { 25751 const raw = String(remate?.slug || remate?.nombre_remate || remate?.title || remate?.titulo || 'catalogo-cabanas') 25752 .toLowerCase() 25753 .normalize('NFD') 25754 .replace(/[\u0300-\u036f]/g, '') 25755 .replace(/[^a-z0-9]+/g, '-') 25756 .replace(/^-+|-+$/g, ''); 25757 return raw || 'catalogo-cabanas'; 25758 } 25759 cabanaCatalogLotCategory(lot) { 25760 return String(this.cabanaLotValue(lot, ['categoriaNombre', 'nombre_categoria', 'categoria', 'categoryName'], lot?.categoryName || lot?.categoriaNombre || 'Reproductor')); 25761 } 25762 cabanaCatalogMediaUrl(lot) { 25763 const images = this.cabanaLotImages(lot); 25764 const videos = this.cabanaLotVideos(lot); 25765 return window.resolveCabanaMediaUrl(this.cabanaLotSelectedCoverUrl(lot) 25766 || images[0]?.url 25767 || lot?.coverImage?.url 25768 || lot?.coverImage?.downloadUrl 25769 || lot?.cover_image?.url 25770 || this.cabanaLotValue(lot, ['coverImageUrl', 'thumbnailUrl', 'thumbnail', 'imagen', 'imageUrl'], '') 25771 || videos[0]?.thumb 25772 || ''); 25773 } 25774 cabanaCatalogExportData(remate = this.state.currentCabana || {}, sourceLots = this.state.cabanaLots || []) { 25775 const lots = this.cabanaSortLotsByCategoryOrder(sourceLots || []); 25776 const participants = this.cabanaRemateParticipants(remate || {}); 25777 const cabanaForLot = (lot) => participants.find((cabana) => 25778 (lot?.cabanaKey && cabana.key === lot.cabanaKey) ||
25779 (lot?.cabanaId && String(cabana.id || '') === String(lot.cabanaId)) || 25780 (lot?.cabanaNombre && cabana.name === lot.cabanaNombre) || 25781 (lot?.cabanaName && cabana.name === lot.cabanaName) 25782 ) || participants.find((cabana) => cabana.type === 'principal') || participants[0] || {}; 25783 const factLabels = []; 25784 const rows = lots.map((lot, index) => { 25785 const facts = this.cabanaLotOnCardFacts(lot, remate, 8); 25786 facts.forEach((fact) => { 25787 const label = String(fact?.label || '').trim(); 25788 if (label && !factLabels.includes(label)) factLabels.push(label); 25789 }); 25790 const cabana = cabanaForLot(lot); 25791 const rp = this.cabanaAnalyticsRpId(lot) || this.cabanaLotValue(lot, ['rp', 'RP', 'numero_rp'], ''); 25792 const lotNumber = this.cabanaLotNumber(lot); 25793 const url = this.cabanaPublicLotPath(lot); 25794 return { 25795 raw: lot, 25796 categoryDisplayOrder: this.cabanaCategoryDisplayOrder(lot, index + 1), 25797 order: Number(lot?.displayOrder || 0) || index + 1, 25798 itemOrder: Number(lot?.itemDisplayOrder || 0) || index + 1, 25799 lotNumber, 25800 rp, 25801 category: this.cabanaCatalogLotCategory(lot), 25802 cabanaName: lot?.cabanaNombre || lot?.cabanaName || cabana.name || '', 25803 consignataria: remate?.consignataria || remate?.nombre_cuenta || 'Rosgan', 25804 media: this.cabanaCatalogMediaUrl(lot), 25805 facts, 25806 factMap: facts.reduce((acc, fact) => ({ ...acc, [String(fact.label || '').trim()]: fact.value }), {}), 25807 url, 25808 }; 25809 }); 25810 const groupMap = new Map(); 25811 rows.forEach((row) => { 25812 const key = this.consKey(row.category) + '::' + this.consKey(row.lotNumber || row.order); 25813 if (!groupMap.has(key)) groupMap.set(key, { key, category: row.category, categoryDisplayOrder: row.categoryDisplayOrder, lotNumber: row.lotNumber, order: row.order, rows: [] }); 25814 groupMap.get(key).rows.push(row); 25815 }); 25816 const groups = Array.from(groupMap.values()).map((group) => ({ 25817 ...group, 25818 rows: group.rows.sort((a, b) => a.itemOrder - b.itemOrder), 25819 })).sort((a, b) => { 25820 const categoryOrder = a.categoryDisplayOrder - b.categoryDisplayOrder; 25821 if (categoryOrder) return categoryOrder; 25822 return a.order - b.order; 25823 }); 25824 return { rows, groups, factLabels, title: remate?.nombre_remate || remate?.title || remate?.titulo || 'Remate de cabanas' }; 25825 } 25826 downloadCabanaCatalogExcel() { 25827 const current = this.state.currentCabana || {}; 25828 if (!this.canExportCabanaCatalog(current)) { 25829 this.showToast('No tenés permiso para exportar este catálogo.'); 25830 return; 25831 } 25832 const data = this.cabanaCatalogExportData(current, this.state.cabanaLots || []); 25833 if (!data.rows.length) { this.showToast('No hay lotes para exportar.'); return; } 25834 const columns = ['Orden', 'Lote', 'RP', 'Categoria', 'Cabana', 'Consignataria', ...data.factLabels, 'Enlace']; 25835 const body = data.rows.map((row) => { 25836 const cells = [ 25837 row.order, 25838 row.lotNumber, 25839 row.rp, 25840 row.category, 25841 row.cabanaName, 25842 row.consignataria, 25843 ...data.factLabels.map((label) => row.factMap[label] || ''), 25844 row.url, 25845 ]; 25846 return '<tr>' + cells.map((cell) => '<td>' + this.cabanaExportEscape(cell) + '</td>').join('') + '</tr>'; 25847 }).join(''); 25848 const html = `<!doctype html><html><head><meta charset="utf-8"><style> 25849 body{font-family:Arial,sans-serif;color:#10233b} 25850 h1{font-size:22px;margin:0 0 4px;color:#003b73} 25851 p{margin:0 0 16px;color:#607188} 25852 table{border-collapse:collapse;width:100%} 25853 th{background:#003b73;color:#fff;text-align:left;font-size:12px;letter-spacing:.04em;padding:10px;border:1px solid #003b73} 25854 td{font-size:12px;padding:9px;border:1px solid #d7e4f0;vertical-align:top} 25855 tr:nth-child(even) td{background:#f5f9fd} 25856 </style></head><body> 25857 <h1>${this.cabanaExportEscape(data.title)}</h1> 25858 <p>Catalogo de lotes y RPs ordenado para carga y control.</p> 25859 <table><thead><tr>${columns.map((column) => '<th>' + this.cabanaExportEscape(column) + '</th>').join('')}</tr></thead><tbody>${body}
25859</tbody></table> 25860 </body></html>`; 25861 this.downloadCabanaBlob(new Blob(['\ufeff', html], { type: 'application/vnd.ms-excel;charset=utf-8' }), this.cabanaExportFileBase(current) + '-lotes-rps.xls'); 25862 } 25863 downloadCabanaCommercialExcel(rows = []) { 25864 const current = this.state.currentCabana || {}; 25865 const reportRows = Array.isArray(rows) ?rows : []; 25866 if (!reportRows.length) { this.showToast('No hay resultados comerciales para exportar.'); return; } 25867 const groupLabels = Array.from(new Set(reportRows.map((row) => row.groupLabel).filter(Boolean))); 25868 const groupLabel = groupLabels.length === 1 ?groupLabels[0] : 'Lote / Corral'; 25869 const columns = ['Orden', groupLabel, 'RP / Parte', 'N° / Pintura', 'CategorÃa', 'Cabaña', 'Consignataria', 'Cantidad', 'Peso prom.', 'Estado', 'Modalidad', 'Precio', 'Total vendido', 'Plazo', 'Comprador']; 25870 const textCell = (value) => '<td class="text">' + this.cabanaExportEscape(value) + '</td>'; 25871 const body = reportRows.map((row) => '<tr>' + [ 25872 textCell(row.order), 25873 textCell(row.lotNumber), 25874 textCell(row.rp), 25875 textCell(row.paintNumber),
25876 textCell(row.category), 25877 textCell(row.cabana), 25878 textCell(row.consignataria), 25879 '<td class="number">' + this.cabanaExportEscape(row.cantidad) + '</td>', 25880 '<td class="number">' + this.cabanaExportEscape(row.pesoProm) + '</td>', 25881 textCell(row.statusLabel), 25882 textCell(row.priceUnitLabel), 25883 '<td class="currency">' + (row.priceNumeric || '') + '</td>', 25884 '<td class="currency">' + (row.totalNumeric || '') + '</td>', 25885 textCell(row.plazo), 25886 textCell(row.comprador), 25887 ].join('') + '</tr>').join(''); 25888 const soldCount = reportRows.filter((row) => row.status === 'vendido').length; 25889 const generatedAt = new Date().toLocaleString('es-AR'); 25890 const html = `<!doctype html><html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel"><head><meta charset="utf-8"><style> 25891 body{font-family:Arial,sans-serif;color:#10233b} 25892 h1{font-size:22px;margin:0 0 4px;color:#003b73} 25893 p{margin:0 0 14px;color:#607188} 25894 table{border-collapse:collapse;width:100%} 25895 th{background:#003b73;color:#fff;text-align:left;font-size:11px;padding:9px;border:1px solid #003b73} 25896 td{font-size:11px;padding:8px;border:1px solid #d7e4f0;vertical-align:top} 25897 tr:nth-child(even) td{background:#f5f9fd} 25898 .text{mso-number-format:"\\@"}.number{mso-number-format:"0";text-align:right}.currency{mso-number-format:"\\$"#,##0;text-align:right;font-weight:bold} 25899 </style></head><body> 25900 <h1>${this.cabanaExportEscape(current?.nombre_remate || current?.title || current?.titulo || 'Informe comercial')}</h1> 25901 <p>Resultados de remate de cabañas · ${reportRows.length} registros · ${soldCount} vendidos · Exportado ${this.cabanaExportEscape(generatedAt)}</p> 25902 <table><thead><tr>${columns.map((column) => '<th>' + this.cabanaExportEscape(column) + '</th>').join('')}</tr></thead><tbody>${body}</tbody></table> 25903 </body></html>`; 25904 this.downloadCabanaBlob(new Blob(['\ufeff', html], { type: 'application/vnd.ms-excel;charset=utf-8' }), this.cabanaExportFileBase(current) + '-informe-comercial.xls'); 25905 this.showToast('Informe comercial exportado.'); 25906 } 25907 downloadCabanaCatalogPdf() { 25908 const current = this.state.currentCabana || {}; 25909 const data = this.cabanaCatalogExportData(current, this.state.cabanaLots || []); 25910 if (!data.groups.length) { this.showToast('No hay lotes para exportar.'); return; } 25911 const participants = this.cabanaRemateParticipants(current || {}); 25912 const mainLogo = participants.find((cabana) => cabana.type === 'principal')?.logoDataUrl || participants[0]?.logoDataUrl || ''; 25913 const guests = participants.filter((cabana) => cabana.type !== 'principal' && cabana.logoDataUrl).slice(0, 3); 25914 const consName = current?.consignataria || current?.nombre_cuenta || 'Rosgan'; 25915 const consColor = this.consBrandColor(consName); 25916 const consLogo = this.consWhiteLogo(consName) || this.consLogo(consName) || ''; 25917 const logoImg = (src, alt, cls = '') => src ?`<img class="${cls}" src="${this.cabanaExportEscape(src)}" alt="${this.cabanaExportEscape(alt)}">` :`<strong class="${cls}">${this.cabanaExportEscape(alt)}</strong>`; 25918 const pages = data.groups.map((group) => { 25919 const first = group.rows[0] || {}; 25920 const media = first.media || ''; 25921 const rpList = group.rows.map((row) => { 25922 const factItems = (row.facts.length ?row.facts : [ 25923 { label: 'RP', value: row.rp }, 25924 { label: 'Lote', value: row.lotNumber }, 25925 ]).slice(0, 6).map((fact) => `<span><b>${this.cabanaExportEscape(fact.label)}</b><em>${this.cabanaExportEscape(fact.value)}</em></span>`).join(''); 25926 return `<article class="rp-card"> 25927 <div class="rp-head"><strong>RP ${this.cabanaExportEscape(row.rp)}</strong><small>${this.cabanaExportEscape(row.cabanaName)}</small></div> 25928 <div class="rp-facts">${factItems}</div> 25929 </article>`; 25930 }).join(''); 25931 return `<section class="print-page"> 25932 <header class="print-head"> 25933 <img src="/assets/rosgan-logo-blue.png" alt="Rosgan"> 25934 ${logoImg(mainLogo, 'La Cassina', 'main-brand')} 25935 </header> 25936 <div class="print-title"> 25937 <small>${this.cabanaExportEscape(data.title)}</small> 25938 <h1>Lote ${this.cabanaExportEscape(group.lotNumber || group.order)}</h1> 25939 <p>${this.cabanaExportEscape(group.category)}</p> 25940 </div> 25941 <figure class="lot-cover"> 25942 ${media ?`<img src="${this.cabanaExportEscape(media)}" alt="Lote ${this.cabanaExportEscape(group.lotNumber)}">` :'<div class="cover-empty">Sin imagen cargada</div>'}
25943 <figcaption><strong>Lote ${this.cabanaExportEscape(group.lotNumber || group.order)}</strong><span>${group.rows.map((row) => 'RP ' + row.rp).join(' · ')}</span></figcaption> 25944 </figure> 25945 <div class="rp-grid">${rpList}</div> 25946 <footer class="print-footer"> 25947 <div class="cons" style="background:${this.cabanaExportEscape(consColor)}">${consLogo ?`<img src="${this.cabanaExportEscape(consLogo)}" alt="${this.cabanaExportEscape(consName)}">` :''}<span><small>Consignataria</small><strong>${this.cabanaExportEscape(consName)}</strong></span></div> 25948 <span class="site">rosgan.app</span> 25949 </footer> 25950 </section>`; 25951 }).join(''); 25952 const html = `<!doctype html><html><head><meta charset="utf-8"><title>${this.cabanaExportEscape(data.title)} - PDF</title><style> 25953 @page{size:A4;margin:12mm} 25954 *{box-sizing:border-box} 25955 body{margin:0;background:#eef5fb;color:#10233b;font-family:Arial,Helvetica,sans-serif} 25956 .print-page{width:210mm;min-height:297mm;margin:0 auto 18px;padding:18mm;background:#fff;page-break-after:always;display:flex;flex-direction:column;gap:15mm} 25957 .print-head{display:flex;align-items:center;justify-content:space-between;gap:18mm} 25958 .print-head img,.print-head .main-brand{max-width:78mm;max-height:22mm;object-fit:contain;color:#203153;font-size:24px} 25959 .print-title small{display:block;color:#0a5ca8;font-weight:900;text-transform:uppercase;letter-spacing:.12em} 25960 .print-title h1{margin:4mm 0 1mm;font-size:40px;line-height:1;color:#002e5f} 25961 .print-title p{margin:0;color:#607188;font-size:16px;font-weight:800;text-transform:uppercase} 25962 .lot-cover{position:relative;margin:0;border-radius:16px;overflow:hidden;min-height:92mm;background:#e8eff6} 25963 .lot-cover>img{display:block;width:100%;height:92mm;object-fit:cover} 25964 .cover-empty{height:92mm;display:grid;place-items:center;color:#607188;font-weight:800} 25965 .lot-cover:after{content:"";position:absolute;inset:50% 0 0;background:linear-gradient(180deg,rgba(0,0,0,0),rgba(0,29,61,.78))} 25966 .lot-cover figcaption{position:absolute;left:10mm;right:10mm;bottom:9mm;z-index:1;color:#fff} 25967 .lot-cover figcaption strong{display:block;font-size:34px;line-height:1} 25968 .lot-cover figcaption span{display:block;margin-top:2mm;font-size:15px;font-weight:800} 25969 .rp-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7mm} 25970 .rp-card{border:1px solid #d7e4f0;border-radius:14px;padding:7mm;background:#f8fbfe} 25971 .rp-head{display:flex;align-items:flex-start;justify-content:space-between;gap:6mm;border-bottom:1px solid #dce7f2;padding-bottom:4mm;margin-bottom:4mm} 25972 .rp-head strong{font-size:20px;color:#002e5f}.rp-head small{color:#607188;font-weight:800;text-align:right} 25973 .rp-facts{display:grid;gap:3mm}.rp-facts span{display:flex;justify-content:space-between;gap:8mm;border-bottom:1px solid #e7eef6;padding-bottom:2mm} 25974 .rp-facts b{color:#7b8aa0;text-transform:uppercase;font-size:11px}.rp-facts em{font-style:normal;color:#10233b;font-weight:900;text-align:right} 25975 .print-footer{margin-top:auto;display:flex;align-items:center;justify-content:space-between;gap:10mm} 25976 .cons{display:flex;align-items:center;gap:6mm;border-radius:14px;padding:5mm 7mm;color:#fff;min-width:95mm} 25977 .cons img{width:18mm;height:18mm;object-fit:contain}.cons small{display:block;opacity:.75;font-weight:900;text-transform:uppercase;letter-spacing:.08em}.cons strong{font-size:18px} 25978 .site{font-size:18px;font-weight:900;color:#002e5f} 25979 @media print{body{background:#fff}.print-page{margin:0;box-shadow:none}} 25980 </style></head><body>${pages}</body></html>`; 25981 const popup = window.open('', '_blank'); 25982 if (!popup) { 25983 this.downloadCabanaBlob(new Blob([html], { type: 'text/html;charset=utf-8' }), this.cabanaExportFileBase(current) + '-catalogo-pdf.html'); 25984 this.showToast('Se descargo el catalogo HTML para imprimir como PDF.'); 25985 return; 25986 } 25987 popup.document.open(); 25988 popup.document.write(html); 25989 popup.document.close(); 25990 popup.setTimeout(() => { try { popup.focus(); popup.print(); } catch {} }, 700); 25991 } 25992 async shareCabanaLotStory(lot = null, context = {}) { 25993 const target = lot || this.state.currentCabanaLot; 25994 if (!target || this.state.cabanaLotStoryBusy) return; 25995 const current = context?.remate || this.state.currentCabana || {}; 25996 const shareContext = { remate: current, contact: context?.contact || this.cabanaContactData(current) }; 25997 this.setState({ cabanaLotStoryBusy: true }); 25998 const { lotNumber, rp, title, auctionTitle } = this.cabanaLotShareTitle(target); 25999 const url = this.cabanaPublicShareUrl(target); 26000 const auctionId = this.cabanaRemateId(this.state.currentCabana) || String(target?.remateId || target?.auctionId || 'remate'); 26001 const fileBase = ['rosgan', 'remate-' + auctionId, 'lote-' + (lotNumber || 'rp'), rp || 'rp'].join('-').toLowerCase().replace(/[^a-z0-9-]+/g, '-');
26002 let completed = false; 26003 try { 26004 const blob = await this.createCabanaLotStoryBlob(target, shareContext); 26005 const file = new File([blob], fileBase + '.png', { type: 'image/png' }); 26006 const text = title + ' - ' + auctionTitle + ' ' + url; 26007 if (navigator.share && (!navigator.canShare || navigator.canShare({ files: [file] }))) { 26008 await navigator.share({ title, text, files: [file] }); 26009 completed = true; 26010 } else { 26011 this.downloadCabanaBlob(blob, file.name); 26012 if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(text); 26013 this.showToast('Imagen para historias descargada y enlace copiado'); 26014 completed = true; 26015 } 26016 } catch (error) { 26017 if (error?.name !== 'AbortError') this.showToast('No se pudo generar la imagen para historias.'); 26018 } finally { 26019 this.setState({ cabanaLotStoryBusy: false }); 26020 } 26021 if (!completed) return; 26022 this.trackCabanaAnalytics('story_card_generated', target, { 26023 metadata: { action: 'story_card_generated', url, format: 'story_9_16' }, 26024 }); 26025 } 26026 cabanaAnalyticsPayload(eventType, lot = null, extra = {}) { 26027 const current = this.state.currentCabana || {}; 26028 const values = lot?.values || {}; 26029 const auctionId = String(lot?.remateId || this.cabanaRemateId(current) || '').trim(); 26030 const participants = current ?this.cabanaRemateParticipants(current) : []; 26031 const selectedCabana = participants.find((cabana) => 26032 (lot?.cabanaKey && cabana.key === lot.cabanaKey) || 26033 (lot?.cabanaId && String(cabana.id || '') === String(lot.cabanaId)) || 26034 (lot?.cabanaNombre && cabana.name === lot.cabanaNombre) || 26035 (lot?.cabanaName && cabana.name === lot.cabanaName) 26036 ) || {}; 26037 const lotId = lot ?this.cabanaLotId(lot) : ''; 26038 const lotIdentity = lot ?window.CabanaLotIdentity.resolve(lot, current) : null; 26039 const rp = String(lot?.rp || values.rp || lot?.identifier || '').trim(); 26040 const clientId = this.cabanaAnalyticsClientId(); 26041 return { 26042 auctionId, 26043 auctionTitle: current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabaña', 26044 eventType, 26045 lotId, 26046 lotCert: lotId, 26047 lotTitle: lot?.categoryName || lot?.categoriaNombre || lot?.name || '', 26048 lotLabel: lotIdentity ?[lotIdentity.label, lotIdentity.number].filter(Boolean).join(' ') : '', 26049 categoryId: lot?.categoryId || '', 26050 categoryName: lot?.categoryName || lot?.categoriaNombre || values.categoriaNombre || '', 26051 cabanaId: lot?.cabanaId || selectedCabana.id || '', 26052 cabanaName: lot?.cabanaNombre || lot?.cabanaName || selectedCabana.name || '', 26053 rpId: rp, 26054 rp, 26055 sessionId: clientId, 26056 clientId, 26057 source: this.state.publicAdvanceMode === 'lacassina' ?'lacassina-public' : (this.state.publicAdvanceMode === 'cabanas' ?'cabanas-public' : 'cabanas-app'), 26058 metadata: { 26059 route: window.location.pathname, 26060 ...(extra.metadata || {}), 26061 }, 26062 ...extra, 26063 }; 26064 } 26065 async trackCabanaAnalytics(eventType, lot = null, extra = {}) { 26066 try { 26067 const payload = this.cabanaAnalyticsPayload(eventType, lot, extra); 26068 if (!payload.auctionId || !String(eventType || '').trim()) return; 26069 const body = JSON.stringify(payload); 26070 const endpoint = '/api/cabanas-analytics/events'; 26071 let token = ''; 26072 if (this.state.user && !this.state.user.isAnonymous) { token = await this.authToken(false); if (!token) return; } 26073 if (this.cabanaCatalogLocked()) return; 26074 if (!token && navigator.sendBeacon) { 26075 const queued = navigator.sendBeacon(endpoint, new Blob([body], { type: 'application/json' })); 26076 if (queued) return; 26077 } 26078 fetch(endpoint, { 26079 method: 'POST', 26080 headers: { 'Content-Type': 'application/json', ...(token ?{ Authorization: 'Bearer ' + token } : {}) }, 26081 body, 26082 keepalive: true, 26083 }).catch(() => {}); 26084 } catch {} 26085 } 26086 remateDate(item) { if (!item) return null; const raw = item.fecha_remate || item.fecha || item.fecha_hora_remate || item.fechaHoraRemate || item.fechaHoraInicio || item.startsAt || (item?.schedule?.fecha ?item.schedule.fecha + 'T' + (item.schedule.hora || '00:00:00') : ''); const d = raw ?new Date(raw) : null; return d && !isNaN(d) ?d : null; } 26087 feriaDate(f) { 26088 const raw = f?.fecha || f?.date || f?.startsAt; 26089 if (!raw) return null; 26090 const day = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(raw)); 26091 const d = day ? new Date(raw + 'T' + (f.hora || '12:00')) : new Date(raw); 26092 return !isNaN(d) ? d : null; 26093 } 26094 feriaLabel(f) { const d = this.feriaDate(f); if (!d) return 'Fecha a confirmar'; return d.toLocaleDateString('es-AR', { day: 'numeric', month: 'short' }) + ' · ' + d.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' }) + ' h'; } 26095 ytThumb(url) { const id = (url || '').match(/(?:v=|youtu\.be\/|\/vi\/|embed\/)([\w-]{11})/)?.[1]; return id ?'https://img.youtube.com/vi/' + id + '/hqdefault.jpg' : 'assets/feria.png'; } 26096 currentLot() { return (this.state.lots || []).find((l) => l.cert === this.state.selectedCert) || null; } 26097 // ---- Ofertas / bids API ---- 26098 get API() { return ''; } 26099 async authToken(forceRefresh = false) { 26100 const epoch = this._sessionEpoch || 0; 26101 const firebase = await this.ensureFirebase(); 26102 const auth = firebase.auth(); 26103 if (this._signingOut || epoch !== (this._sessionEpoch || 0)) throw new Error('La sesión cambió. Volvé a intentar.'); 26104 if (!auth.currentUser) { 26105 await new Promise((resolve, reject) => { 26106 let unsubscribe = () => {}; 26107 const timer = setTimeout(() => { unsubscribe(); reject(new Error('No pudimos restaurar la sesión. Revisá la conexión.')); }, 12000); 26108 unsubscribe = auth.onAuthStateChanged(() => { clearTimeout(timer); unsubscribe(); resolve(); }, (error) => { clearTimeout(timer); unsubscribe(); reject(error); }); 26109 }); 26110 } 26111 if (this._signingOut || epoch !== (this._sessionEpoch || 0)) throw new Error('La sesión cambió. Volvé a intentar.'); 26112 const user = auth.currentUser; 26113 if (!user) { 26114 if (this.state.user) throw new Error('La sesión ya no está disponible. Volvé a ingresar.'); 26115 return ''; 26116 } 26117 // A network/token error must not silently turn an authenticated call into a guest request. 26118 const token = await user.getIdToken(forceRefresh); 26119 if (this._signingOut || epoch !== (this._sessionEpoch || 0) || auth.currentUser?.uid !== user.uid) throw new Error('La sesión cambió. Volvé a intentar.'); 26120 return token; 26121 } 26122 async cabanaApiFetch(path, opts = {}) { 26123 path = String(path || '').replace(/\s+\|\|\s+/, '?'); 26124 const request = async (forceRefresh) => { 26125 const method = String(opts.method || 'GET').toUpperCase(); 26126 const token = await this.authToken(forceRefresh); 26127 if (!token && method !== 'GET') throw new Error('Inicia sesion para guardar el remate.'); 26128 const headers = { Accept: 'application/json', ...(opts.body ?{ 'Content-Type': 'application/json' } : {}), ...(opts.headers || {}), ...(token ?{ Authorization: 'Bearer ' + token } : {}) }; 26129 const response = await fetch(path, { ...opts, headers }); 26130 const text = await response.text(); 26131 let data = null;
26132 try { data = text ?JSON.parse(text.replace(/^\uFEFF/, '')) : null; } catch { data = text || null; } 26133 return { response, data }; 26134 }; 26135 let result = await request(false); 26136 if (result.response.status === 401) result = await request(true); 26137 if (!result.response.ok) { 26138 const validation = result.data?.errors && typeof result.data.errors === 'object' ?Object.values(result.data.errors).flat().filter(Boolean)[0] : ''; 26139 const error = new Error(result.data?.detail || validation || result.data?.message || result.data?.title || ('HTTP ' + result.response.status)); 26140 error.status = result.response.status; 26141 error.data = result.data; 26142 throw error; 26143 } 26144 return result.data; 26145 } 26146 async apiFetch(path, opts) { 26147 window.RosganDevActivity?.start(this); 26148 path = String(path || '') 26149 .replace(/\s+\|\|\s+/, '?') 26150 .replace(/^\/api\/Lots/i, '/api/lots') 26151 .replace(/^\/api\/Bids/i, '/api/bids') 26152 .replace(/^\/api\/Establishments/i, '/api/establishments') 26153 .replace(/^\/api\/BuyerAuthorizations/i, '/api/buyerauthorizations') 26154 .replace(/^\/api\/Users/i, '/api/users') 26155 .replace(/^\/api\/Values/i, '/api/values') 26156 .replace(/^\/api\/Catalogs/i, '/api/catalogs'); 26157 const request = async (forceRefresh) => { 26158 const token = await this.authToken(forceRefresh); 26159 const isFormData = typeof FormData !== 'undefined' && opts?.body instanceof FormData; 26160 const headers = { Accept: 'application/json', ...(!isFormData ?{ 'Content-Type': 'application/json' } : {}), ...(opts?.headers || {}), ...(token ?{ Authorization: 'Bearer ' + token } : {}) }; 26161 const response = await fetch(this.API + path, { ...opts, headers }); 26162 const txt = await response.text(); 26163 let data = null; 26164 try { data = txt ?JSON.parse(txt) : null; } catch { data = txt || null; } 26165 return { response, data }; 26166 }; 26167 let result = await request(false); 26168 if (result.response.status === 401) result = await request(true); 26169 if (!result.response.ok) { 26170 const validationErrors = result.data?.errors && typeof result.data.errors === 'object' ?Object.values(result.data.errors).flat().filter((value) => typeof value === 'string').join(' · ') : ''; 26171 const plainTextDetail = typeof result.data === 'string' && !/<(?:!doctype|html|head|body)\b/i.test(result.data) 26172 ?result.data.trim() 26173 : ''; 26174 const detail = plainTextDetail || result.data?.message || result.data?.detail || (typeof result.data?.error === 'string' ?result.data.error : '') || validationErrors || result.data?.title; 26175 const statusMessage = result.response.status === 401 26176 ?'No pudimos autenticar la solicitud. Actualizá la página e intentá nuevamente.' 26177 : result.response.status === 429 ? 'El servicio está recibiendo demasiadas consultas. Esperá un momento antes de volver a intentar.' : ('HTTP ' + result.response.status); 26178 const error = new Error(detail || statusMessage); 26179 error.status = result.response.status; 26180 const retryAfter = result.response.headers.get('Retry-After'); 26181 const retryDelay = retryAfter && /^\d+(?:\.\d+)?$/.test(retryAfter.trim()) ? Number(retryAfter) * 1000 : Date.parse(retryAfter || '') - Date.now(); 26182 error.retryAfterMs = Number.isFinite(retryDelay) ? Math.max(0,retryDelay) : 0; 26183 error.data = result.data; 26184 throw error; 26185 } 26186 return result.data; 26187 } 26188 normBid(b, cert) { 26189 if (b == null) return null; 26190 if (typeof b === 'number') return { id: '', amount: b, status: 'active', bidder: '', date: '' }; 26191 const amount = Number(b.amount || b.price || b.pricePerKg || b.value || b.monto || 0) || 0; 26192 const status = String(b.status || b.state || b.estado || 'pending').toLowerCase(); 26193 return { 26194 id: String(b.id || b.bidId || b._id || b.bidCert || ''), 26195 cert: b.lotCert || b.cert || cert || '', 26196 amount, 26197 status, 26198 bidder: b.bidderName || b.buyerName || b.buyerDisplayName || b.buyer?.name || b.buyer?.displayName || b.userName || b.comprador || '', 26199 buyerUid: String(b.buyerUid || b.userUid || b.bidderUid || b.createdByUid || ''), 26200 buyerEmail: String(b.buyerEmail || b.userEmail || b.bidderEmail || '').toLowerCase(), 26201 lotName: b.lotName || b.categoria || b.category || '', 26202 date: b.createdAt || b.date || b.fecha || '', 26203 isWinning: b.isWinning || b.winning || b.isLeading || b.leading || b.isHighest || null, 26204 topAmount: Number(b.highestBid || b.topBid || b.currentHighest || b.leadingAmount || b.maxBid || 0) || 0, 26205 }; 26206 } 26207 ownBidsOnly(bids) { 26208 const uid = String(this.state.user?.uid || ''); 26209 const email = String(this.state.user?.email || '').toLowerCase(); 26210 return (bids || []).filter((bid) => { 26211 if (bid.buyerUid) return !!uid && bid.buyerUid === uid; 26212 if (bid.buyerEmail) return !!email && bid.buyerEmail === email; 26213 return true; 26214 }); 26215 } 26216 computeStanding(cert, myAmount, isWinning, topAmount) { 26217 const liveTop = Math.max(0, ...(this.state.lotBids[cert] || []).map((b) => Number(b.amount) || 0)); 26218 const best = Math.max(Number(topAmount) || 0, liveTop, Number(myAmount) || 0); 26219 if (!best || !myAmount) return null; 26220 const winning = typeof isWinning === 'boolean' ?isWinning : Number(myAmount) >= best; 26221 return winning 26222 ?{ key: 'winning', label: 'Vas ganando', bg: 'rgba(31,122,99,0.14)', color: '#1f7a63', myAmount: Number(myAmount), topAmount: best } 26223 : { key: 'outbid', label: 'Te superaron', bg: 'rgba(230,57,70,0.14)', color: '#c22', myAmount: Number(myAmount), topAmount: best }; 26224 } 26225 openRepublish(lot) { 26226 const plazoId = lot.plazoEsOtro ?'otro' : String(lot.plazoId || ''); 26227 this.setState({ 26228 republishLot: lot, 26229 republishForm: { 26230 fechaSubasta: '', 26231 listoParaCargar: lot.raw?.listoParaCargar === true, 26232 fechaDisponibleCarga: lot.fechaDisponibleCarga || '', 26233 precioSugerido: String(lot.baseNum || ''), 26234 plazoId, 26235 plazoOtro: plazoId === 'otro' ?String(lot.plazoOtro || '') : '', 26236 }, 26237 republishMessage: null, 26238 }); 26239 } 26240 closeRepublish() { if (!this.state.republishSaving) this.setState({ republishLot: null, republishForm: null, republishMessage: null }); } 26241 setRepublish(key, value) { this.setState({ republishForm: { ...(this.state.republishForm || {}), [key]: value, ...(key === 'listoParaCargar' && value === true ?{ fechaDisponibleCarga: '' } : {}) }, republishMessage: null }); } 26242 async submitRepublish() { 26243 if (this.state.republishSaving) return; 26244 const form = this.state.republishForm; 26245 const lot = this.state.republishLot; 26246 if (!form || !lot) return; 26247 const fail = (text) => this.setState({ republishMessage: { text, ok: false } }); 26248 const isIsoDate = (value) => { 26249 const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || '').trim()); 26250 if (!match) return false;
26251 const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])); 26252 return date.getFullYear() === Number(match[1]) && date.getMonth() === Number(match[2]) - 1 && date.getDate() === Number(match[3]); 26253 }; 26254 const fechaSubasta = String(form.fechaSubasta || '').trim(); 26255 const fechaDisponibleCarga = String(form.fechaDisponibleCarga || '').trim(); 26256 const precioSugerido = this.numOrNull(form.precioSugerido); 26257 if (!isIsoDate(fechaSubasta)) return fail('Elegà una fecha de subasta válida.'); 26258 const availabilityError = this.loadAvailabilityError(fechaSubasta, form.listoParaCargar, fechaDisponibleCarga); 26259 if (availabilityError) return fail(availabilityError); 26260 if (!(precioSugerido > 0)) return fail('El precio sugerido debe ser mayor a $0.'); 26261 if (!form.plazoId) return fail('Elegà el plazo de pago.'); 26262 if (form.plazoId === 'otro' && !String(form.plazoOtro).trim()) return fail('Especificá el plazo.'); 26263 const body = { 26264 fechaSubasta, 26265 listoParaCargar: form.listoParaCargar === true, 26266 fechaDisponibleCarga: form.listoParaCargar === true ?null : fechaDisponibleCarga, 26267 precioSugerido, 26268 plazoId: String(form.plazoId), 26269 plazoOtro: form.plazoId === 'otro' ?String(form.plazoOtro).trim() : null, 26270 }; 26271 this.setState({ republishSaving: true, republishMessage: { text: 'Republicando loteâ¦', ok: true } }); 26272 try { 26273 await this.apiFetch('/api/Lots/republish/' + encodeURIComponent(lot.cert), { method: 'POST', body: JSON.stringify(body) }); 26274 await Promise.all([this.fetchLots(), this.fetchMyLots()]); 26275 this.setState({ republishSaving: false, republishLot: null, republishForm: null, republishMessage: null }); 26276 this.showToast('Lote republicado para el ' + fechaSubasta + '.'); 26277 } catch (error) { 26278 this.setState({ republishSaving: false, republishMessage: { text: error?.message || 'No se pudo republicar el lote.', ok: false } }); 26279 } 26280 } 26281 bidStatusMeta(st) { 26282 const s = String(st || '').toLowerCase(); 26283 if (/(approv|aprob|won|adjud|accept)/.test(s)) return { label: 'Aprobada', bg: 'rgba(31,122,99,0.14)', color: '#1f7a63' }; 26284 if (/(outbid|superad)/.test(s)) return { label: 'Superada', bg: 'rgba(230,57,70,0.14)', color: '#c22' }; 26285 if (/(reject|rechaz|declin|lost)/.test(s)) return { label: 'Rechazada', bg: 'rgba(230,57,70,0.14)', color: '#c22' }; 26286 if (/(pend|espera)/.test(s)) return { label: 'Pendiente', bg: 'rgba(230,158,20,0.16)', color: '#a8730a' }; 26287 return { label: 'Activa', bg: 'rgba(10,92,168,0.14)', color: '#0a5ca8' }; 26288 } 26289 async fetchLotBids(cert) { 26290 if (!cert) return; 26291 this.setState({ lotBidsLoading: { ...this.state.lotBidsLoading, [cert]: true } }); 26292 try { 26293 const data = await this.apiFetch('/api/Lots/bids/' + encodeURIComponent(cert)); 26294 const arr = this.listFrom(data, ['items', 'bids', 'data']); 26295 const bids = arr.map((b) => this.normBid(b, cert)).filter(Boolean).sort((a, b) => b.amount - a.amount); 26296 this.setState({ lotBids: { ...this.state.lotBids, [cert]: bids }, lotBidsLoading: { ...this.state.lotBidsLoading, [cert]: false } }); 26297 } catch { this.setState({ lotBidsLoading: { ...this.state.lotBidsLoading, [cert]: false } }); } 26298 } 26299 async fetchMyBids() { 26300 this.setState({ myBidsLoading: true, myBidsError: '' }); 26301 try { 26302 const data = await this.apiFetch('/api/Bids/mine?limit=50'); 26303 const arr = this.listFrom(data, ['items', 'bids', 'data']); 26304 const list = this.ownBidsOnly(arr.map((b) => this.normBid(b)).filter(Boolean)); 26305 this.setProfileFetchInfo('ofertas', list.length, ''); 26306 this.setState({ myBids: list, myBidsLoading: false, myBidsError: '' }); 26307 } catch (e) { 26308 if (e?.status === 403) { 26309 this.setProfileFetchInfo('ofertas', 0, ''); 26310 this.setState({ myBids: [], myBidsLoading: false, myBidsError: '' }); 26311 return; 26312 } 26313 this.setProfileFetchInfo('ofertas', null, e?.message || 'Error'); 26314 this.setState({ myBids: [], myBidsLoading: false, myBidsError: e?.message || 'No se pudieron cargar tus ofertas.' }); 26315 } 26316 } 26317 async fetchRoslotDailyReport(force = false) { 26318 if (this.state.roslotDailyReportLoading && !force) return; 26319 const date = this.state.roslotDailyReportDate || this.localIsoDate(); 26320 this.setState({ 26321 roslotDailyReportLoading: true, 26322 roslotDailyReportError: '', 26323 roslotDailyReportDate: date, 26324 }); 26325 try { 26326 const data = await this.apiFetch('/api/roslot/daily-summary?date=' + encodeURIComponent(date)); 26327 this.setState({ 26328 roslotDailyReport: data && typeof data === 'object' ?data : null, 26329 roslotDailyReportLoading: false, 26330 roslotDailyReportError: '', 26331 }); 26332 } catch (error) { 26333 this.setState({ 26334 roslotDailyReport: null, 26335 roslotDailyReportLoading: false, 26336 roslotDailyReportError: error?.message || 'No se pudo cargar el cierre comercial del dÃa.', 26337 }); 26338 } 26339 } 26340 setRoslotDailyReportDate(event) { 26341 const date = String(event?.target?.value || '').trim(); 26342 if (!date) return; 26343 this.setState({ roslotDailyReportDate: date }, () => this.fetchRoslotDailyReport(true)); 26344 } 26345 setRoslotDailyReportFilter(filter) { 26346 this.setState({ roslotDailyReportFilter: ['buyer', 'seller'].includes(filter) ?filter : 'all' }); 26347 } 26348 setRoslotDailyReportSearch(event) { 26349 this.setState({ roslotDailyReportSearch: String(event?.target?.value || '') }); 26350 } 26351 roslotDailyReportView() { 26352 const s = this.state; 26353 const report = s.roslotDailyReport && typeof s.roslotDailyReport === 'object' ?s.roslotDailyReport : null; 26354 const stats = report?.stats || {}; 26355 const selectedDate = s.roslotDailyReportDate || report?.date || this.localIsoDate(); 26356 const parsedDate = /^\d{4}-\d{2}-\d{2}$/.test(selectedDate) ?new Date(selectedDate + 'T12:00:00') : null; 26357 const dateLabel = parsedDate && !Number.isNaN(parsedDate.getTime()) 26358 ?parsedDate.toLocaleDateString('es-AR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }) 26359 : selectedDate; 26360 const items = Array.isArray(report?.items) ?report.items : []; 26361 const query = String(s.roslotDailyReportSearch || '').trim().toLocaleLowerCase('es'); 26362 const filter = ['buyer', 'seller'].includes(s.roslotDailyReportFilter) ?s.roslotDailyReportFilter : 'all'; 26363 const initials = (name) =>
26363 String(name || '?').split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part.charAt(0)).join('').toUpperCase() || '?'; 26364 const rows = items.filter((item) => { 26365 if (report?.scope !== 'all' && filter !== 'all' && ![filter, 'both'].includes(item.viewerRole)) return false; 26366 if (!query) return true; 26367 return [item.cert, item.category, item.establishment, item.location, item.buyer?.name, item.buyer?.email, item.seller?.name, item.seller?.email] 26368 .some((value) => String(value || '').toLocaleLowerCase('es').includes(query)); 26369 }).map((item) => { 26370 const amount = Number(item.amount) || 0; 26371 const total = Number(item.estimatedTotal); 26372 const closedAt = item.closedAt ?new Date(item.closedAt) : null; 26373 const priceUnit = item.priceBasis === 'kg' ?'/ kg vivo' : (item.priceBasis === 'meat' ?'/ kg carne' : (item.priceBasis === 'head' ?'/ cabeza' : '/ unidad')); 26374 const yieldPercent = Number(item.yieldPercent) || 0; 26375 const contact = (person, type) => { 26376 const phone = String(person?.phone || '').trim(); 26377 const email = String(person?.email || '').trim(); 26378 return { 26379 type, 26380 name: person?.name || 'Sin nombre informado', 26381 initials: initials(person?.name), 26382 phone: phone || 'Sin teléfono informado', 26383 email: email || 'Sin email informado', 26384 hasPhone: !!phone, 26385 hasNoPhone: !phone, 26386 hasEmail: !!email, 26387 hasNoEmail: !email, 26388 phoneHref: phone ?'tel:' + phone.replace(/[^+\d]/g, '') : '', 26389 emailHref: email ?'mailto:' + email : '', 26390 }; 26391 }; 26392 return { 26393 cert: item.cert || 'â', 26394 category: item.category || 'Hacienda', 26395 thumbnail: item.thumbnail || '', 26396 hasThumbnail: !!item.thumbnail, 26397 hasNoThumbnail: !item.thumbnail, 26398 heads: (Number(item.heads) || 0).toLocaleString('es-AR'),
26399 weight: Number(item.averageWeight) > 0 26400 ?Math.round(Number(item.averageWeight)).toLocaleString('es-AR') + ' kg prom.' + (item.priceBasis === 'meat' && yieldPercent > 0 ?' · ' + yieldPercent.toLocaleString('es-AR') + '% rinde' : '') 26401 : 'Peso a confirmar', 26402 price: amount > 0 ?this.fmtARS(amount) : 'A confirmar', 26403 priceUnit, 26404 total: Number.isFinite(total) && total > 0 ?this.fmtARS(total) : 'A confirmar', 26405 condition: item.condition || 'Condición no informada', 26406 location: item.location || 'Ubicación no informada', 26407 establishment: item.establishment || 'Establecimiento no informado', 26408 closedTime: closedAt && !Number.isNaN(closedAt.getTime()) 26409 ?closedAt.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' h' 26410 : 'Horario no informado', 26411 buyer: contact(item.buyer, 'Comprador'), 26412 seller: contact(item.seller, 'Vendedor'), 26413 open: () => this.openLot(item.cert), 26414 }; 26415 }); 26416 const estimatedTotal = Number(stats.estimatedTotal) || 0; 26417 const transactions = Number(stats.transactions) || 0; 26418 const heads = Number(stats.heads) || 0; 26419 const buyers = Number(stats.buyers) || 0; 26420 const sellers = Number(stats.sellers) || 0; 26421 return { 26422 goRoslotSummary: () => this.go('roslotSummary'), 26423 goRoslotPool: () => this.go('pool'), 26424 refreshRoslotReport: () => this.fetchRoslotDailyReport(true), 26425 onRoslotReportDate: (event) => this.setRoslotDailyReportDate(event), 26426 onRoslotReportSearch: (event) => this.setRoslotDailyReportSearch(event), 26427 roslotReportLoading: !!s.roslotDailyReportLoading, 26428 roslotReportReady: !!report && !s.roslotDailyReportLoading && !s.roslotDailyReportError, 26429 roslotReportHasError: !!s.roslotDailyReportError && !s.roslotDailyReportLoading, 26430 roslotReportError: s.roslotDailyReportError || '', 26431 roslotReportDate: selectedDate, 26432 roslotReportDateLabel: dateLabel ?dateLabel.charAt(0).toUpperCase() + dateLabel.slice(1) : '', 26433 roslotReportScopeLabel: report?.scope === 'all' ?'Vista general del mercado' : 'Tus operaciones del dÃa', 26434 roslotReportHeadline: estimatedTotal > 0 ?this.fmtARS(estimatedTotal) : 'Sin monto estimado', 26435 roslotReportHeadlineDetail: estimatedTotal > 0 ?'Volumen bruto estimado adjudicado' : 'Los lotes sin base por kg o cabeza no se proyectan', 26436 roslotReportRows: rows, 26437 roslotReportHasRows: rows.length > 0, 26438 roslotReportEmpty: rows.length === 0, 26439 roslotReportSearch: s.roslotDailyReportSearch || '', 26440 roslotReportShowRoleFilters: report?.scope !== 'all', 26441 roslotReportKpis: [ 26442 { icon: 'dc-lucide-banknote', label: 'Volumen estimado', value: estimatedTotal > 0 ?this.fmtARS(estimatedTotal) : '$0', detail: 'Importe bruto comercial' }, 26443 { icon: 'dc-lucide-beef', label: 'Cabezas vendidas', value: heads.toLocaleString('es-AR'), detail: transactions === 1 ?'1 lote adjudicado' : transactions.toLocaleString('es-AR') + ' lotes adjudicados' }, 26444 { icon: 'dc-lucide-users', label: 'Compradores', value: buyers.toLocaleString('es-AR'), detail: 'Participantes adjudicados' }, 26445 { icon: 'dc-lucide-handshake', label: 'Vendedores', value: sellers.toLocaleString('es-AR'), detail: 'Contrapartes vendedoras' }, 26446 ], 26447 roslotReportFilters: [ 26448 { label: 'Todas', active: filter === 'all', cls: filter === 'all' ?' is-active' : '', go: () => this.setRoslotDailyReportFilter('all') }, 26449 { label: 'Mis compras', active: filter === 'buyer', cls: filter === 'buyer' ?' is-active' : '', go: () => this.setRoslotDailyReportFilter('buyer') }, 26450 { label: 'Mis ventas', active: filter === 'seller', cls: filter === 'seller' ?' is-active' : '', go: () => this.setRoslotDailyReportFilter('seller') }, 26451 ], 26452 }; 26453 } 26454 async fetchPendingApprovals() { 26455 this.setState({ pendingBidsLoading: true, pendingBidsError: '' }); 26456 try { 26457 const data = await this.apiFetch('/api/Bids/pending-approvals?limit=50'); 26458 const arr = this.listFrom(data, ['items', 'bids', 'data']); 26459 const list = arr.map((b) =>
26459 this.normBid(b)).filter(Boolean); 26460 this.setProfileFetchInfo('pendientes', list.length, ''); 26461 this.setState({ pendingBids: list, pendingBidsLoading: false, pendingBidsError: '' }); 26462 } catch (e) { this.setProfileFetchInfo('pendientes', null, e?.message || 'Error'); this.setState({ pendingBids: [], pendingBidsLoading: false, pendingBidsError: e?.message || 'No se pudieron cargar ofertas pendientes.' }); } 26463 } 26464 async actOnBid(cert, bidId, action) { 26465 if (!cert || !bidId || this.state.bidActioning) return; 26466 this.setState({ bidActioning: bidId }); 26467 try { 26468 await this.apiFetch('/api/Lots/bids/' + encodeURIComponent(cert) + '/' + encodeURIComponent(bidId) + '/' + action, { method: 'POST' }); 26469 this.showToast(action === 'approve' ?'Oferta aprobada.' : 'Oferta rechazada.'); 26470 this.setState({ bidActioning: '' }); 26471 this.fetchPendingApprovals(); 26472 this.fetchLotBids(cert); 26473 } catch { 26474 this.setState({ bidActioning: '' }); 26475 this.showToast('No se pudo procesar la oferta.'); 26476 } 26477 } 26478 currentBid(lot) { 26479 const placed = this.state.bidsPlaced[lot.cert] || 0; 26480 const live = (this.state.lotBids[lot.cert] || []).map((b) => b.amount); 26481 return Math.max(lot.baseNum, lot.currentBidAmount || 0, lot.soldAmount || 0, placed, ...(lot.bids || []), ...live, 0); 26482 } 26483 bidAdj(cert) { const a = this.state.bidByCert[cert]; return a || { increment: this.INCREMENTS[0], offset: this.INCREMENTS[0] }; } 26484 setBidAdj(cert, adj) { this.setState({ bidByCert: { ...this.state.bidByCert, [cert]: adj } }); } 26485 showRoslotBidSuccess(cert, amount) { 26486 if (this._roslotBidSuccessTimer) clearTimeout(this._roslotBidSuccessTimer); 26487 this.setState({ roslotBidSuccessFx: { cert: String(cert || ''), amount: Number(amount) || 0 } }); 26488 this._roslotBidSuccessTimer = setTimeout(() => { 26489 this._roslotBidSuccessTimer = null; 26490 this.setState({ roslotBidSuccessFx: null }); 26491 }, 1750); 26492 } 26493 async quickBid(cert, marketAmount, quickOffset = null) { 26494 if (!cert || this.state.quickBidCert) return; 26495 const lot = (this.state.lots || []).find((item) => item.cert === cert); 26496 if (!lot || lot.auctionStatus !== 'open' || lot.isClosed) { 26497 this.showToast('La subasta de este lote ya está cerrada. No se pueden realizar más ofertas.'); 26498 return; 26499 } 26500 if (lot?.sellerUid && this.state.user?.uid && lot.sellerUid === this.state.user.uid) { 26501 this.showToast('No podés ofertar sobre un lote publicado por tu usuario.'); 26502 return; 26503 } 26504 if (lot?.currentBidBuyerUid && this.state.user?.uid && lot.currentBidBuyerUid === this.state.user.uid) { 26505 this.showToast('¡Vas ganando! No necesitás volver a ofertar.'); 26506 return; 26507 } 26508 const token = await this.authToken(); 26509 if (!token) { this.showToast('Iniciá sesión para enviar una oferta.'); return; } 26510 const adjustment = this.bidAdj(cert); 26511 const myAmount = Math.max(0, ...(this.state.myBids || []).filter((bid) => bid.cert === cert).map((bid) => Number(bid.amount) || 0)); 26512 const liveTop = Math.max(0, ...(this.state.lotBids[cert] || []).map((bid) => Number(bid.amount) || 0)); 26513 const offset = Number.isFinite(Number(quickOffset)) ?Number(quickOffset) : adjustment.offset; 26514 const target = Math.max(Number(marketAmount) || 0, myAmount, liveTop) + offset; 26515 this.setState({ quickBidCert: cert }); 26516 try { 26517 await this.apiFetch('/api/Lots/bids/' + encodeURIComponent(cert), { method: 'POST', body: JSON.stringify({ amount: target }) }); 26518 this.showRoslotBidSuccess(cert, target); 26519 this.setState({ bidsPlaced: { ...this.state.bidsPlaced, [cert]: target }, quickBidCert: '' }); 26520 await Promise.all([this.fetchMyBids(), this.fetchLotBids(cert), this.syncAuctionStatuses()]); 26521 } catch (error) { 26522 this.setState({ quickBidCert: '' }); 26523 this.showToast(this.bidError(error)); 26524 } 26525 } 26526 bidError(e) { 26527 const msg = String(e?.message || e || ''); 26528 if (/No est(?:á|a|\?)s autorizado para ofertar/i.test(msg)) return 'No estás autorizado para ofertar en lotes de este vendedor. Pedà autorización antes de ofertar.'; 26529 return 'No se pudo enviar la oferta (' + msg + '). Verificá tu sesión.'; 26530 } 26531 async submitBid() { 26532 const lot = this.currentLot(); if (!lot || this.state.bidSubmitting) return; 26533 if (lot.auctionStatus !== 'open' || lot.isClosed) { 26534 this.setState({ bidMessage: { text: 'La subasta está cerrada. No se pueden realizar más ofertas.', ok: false } }); 26535 setTimeout(() => this.setState({ bidMessage: null }), 4000); 26536 return; 26537 } 26538 if (lot.sellerUid && this.state.user?.uid && lot.sellerUid === this.state.user.uid) { 26539 this.setState({ bidMessage: { text: 'No podés ofertar sobre un lote publicado por tu usuario.', ok: false } }); 26540 setTimeout(() => this.setState({ bidMessage: null }), 4000); 26541 return; 26542 } 26543 const detailStanding = !!lot.currentBidBuyerUid && String(lot.currentBidBuyerUid) === String(this.state.user?.uid || ''); 26544 if (detailStanding) { 26545 this.setState({ bidMessage: { text: '¡Vas ganando! No necesitás volver a ofertar.', ok: true } }); 26546 setTimeout(() => this.setState({ bidMessage: null }), 4000); 26547 return; 26548 } 26549 const target = this.currentBid(lot) + this.bidAdj(lot.cert).offset; 26550 this.setState({ bidSubmitting: true, bidMessage: { text: 'Enviando oferta...', ok: true } }); 26551 const token = await this.authToken(); 26552 if (!this.state.lotsReal || !token) { 26553 this.setState({ bidSubmitting: false, bidMessage: { text: token ?'No se puede ofertar sobre un lote no sincronizado.' : 'Iniciá sesión para enviar una oferta.', ok: false } }); 26554 setTimeout(() => this.setState({ bidMessage: null }), 4000); 26555 return; 26556 } 26557 try { 26558 await this.apiFetch('/api/Lots/bids/' + encodeURIComponent(lot.cert), { method: 'POST', body: JSON.stringify({ amount: target }) }); 26559 this.showRoslotBidSuccess(lot.cert, target); 26560 this.setState({ bidsPlaced: { ...this.state.bidsPlaced, [lot.cert]: target }, bidSubmitting: false, bidMessage: { text: 'Oferta confirmada por ' + this.fmtARS(target) + '.', ok: true } }); 26561 this.fetchLotBids(lot.cert); 26562 this.fetchMyBids(); 26563 } catch (e) { 26564 this.setState({ bidSubmitting: false, bidMessage: { text: this.bidError(e), ok: false } }); 26565 } 26566 setTimeout(() => this.setState({ bidMessage: null }), 4000); 26567 } 26568 async closeRoslotLot(cert) { 26569 const safeCert = String(cert || '').trim(); 26570 if (!safeCert || this.state.lotClosingCert) return; 26571 const lot = (this.state.lots || []).find((item) => String(item.cert) === safeCert) 26572 || (this.state.myLots || []).find((item) => String(item.cert) === safeCert); 26573 const ownUid = String(this.state.user?.uid || ''); 26574 const isOwnLot = !!(lot?.sellerUid && ownUid && String(lot.sellerUid) === ownUid); 26575 const offers = this.state.lotBids?.[safeCert] || []; 26576 const hasOffer = !!(lot?.currentBidId || lot?.currentBidBuyerUid || Number(lot?.bidCount) > 0 || offers.length > 0); 26577 if (!lot || !isOwnLot) { 26578 this.showToast('Solo el dueño del lote puede bajar el martillo.'); 26579 return; 26580 } 26581 if (lot.auctionStatus !== 'open') { 26582 this.showToast('Este lote ya no está abierto.'); 26583 return; 26584 } 26585 if (!hasOffer) { 26586 this.showToast('TodavÃa no hay una oferta para adjudicar.'); 26587 return; 26588 } 26589 const leadingAmount = Math.max(Number(lot.currentBidAmount) || 0, ...offers.map((bid) => Number(bid.amount) || 0)); 26590 const accepted = window.confirm( 26591 '¿Bajar el martillo del lote ' + safeCert + '?\n\nLa subasta se cerrará ahora y la oferta lÃder' 26592 + (leadingAmount ?' de ' + this.fmtARS(leadingAmount) : '') 26593 + ' quedará como ganadora. Esta acción no se puede deshacer.' 26594 ); 26595 if (!accepted) return; 26596 this.setState({ lotClosingCert: safeCert, bidMessage: { text: 'Cerrando el lote y adjudicando la oferta lÃderâ¦', ok: true } }); 26597 try { 26598 await this.apiFetch('/api/Lots/close/' + encodeURIComponent(safe
26598Cert), { method: 'POST' }); 26599 this.showToast('Martillo bajado. El lote ' + safeCert + ' quedó vendido.'); 26600 this.setState((previous) => { 26601 const markSold = (item) => String(item?.cert || '') === safeCert 26602 ? { ...item, auctionStatus: 'sold', status: 'Vendido', isClosed: true, hasOffers: true, soldAmount: leadingAmount || Number(item.currentBidAmount) || Number(item.soldAmount) || 0 } 26603 : item; 26604 return { 26605 lotClosingCert: '', 26606 closedRoslotLots: { ...(previous.closedRoslotLots || {}), [safeCert]: { amount: leadingAmount } }, 26607 lots: (previous.lots || []).map(markSold), 26608 myLots: (previous.myLots || []).map(markSold), 26609 bidMessage: { text: 'Subasta cerrada. El lote quedó vendido.', ok: true }, 26610 }; 26611 }); 26612 await Promise.all([ 26613 this.fetchLots(), 26614 this.fetchMyLots(), 26615 this.fetchLotBids(safeCert), 26616 this.fetchMyBids(), 26617 ]); 26618 await this.fetchRoslotLotByCert(safeCert); 26619 } catch (error) { 26620 this.setState({ lotClosingCert: '', bidMessage: { text: error?.message || 'No se pudo cerrar el lote.', ok: false } }); 26621 } 26622 setTimeout(() => this.setState({ bidMessage: null }), 4000); 26623 } 26624 toggleFilter(key, value) { 26625 const f = this.state.poolFilters; const list = f[key]; 26626 const next = list.includes(value) ?list.filter((v) => v !== value) : [...list, value]; 26627 this.setState({ poolFilters: { ...f, [key]: next } }); 26628 } 26629 renderVals() { 26630 const s = this.state; 26631 const anim = this.props.animaciones || true; 26632 const authMode = s.authMode; 26633 const accountAssistantStep = Math.max(0, Math.min(5, Number(s.accountAssistantStep) || 0)); 26634 const accountAssistantMethod = String(s.accountAssistantMethod || ''); 26635 const accountAssistantEmailActive = !!s.accountAssistantOpen && accountAssistantMethod === 'email'; 26636 const accountAssistantEmailContent = { 26637 1: { title: 'Paso 1: tu nombre', text: 'Escribà tu nombre y apellido en el campo que quedó iluminado.', tip: 'Usá tu nombre real. Asà podremos identificarte y ayudarte si alguna vez necesitás recuperar el acceso.' }, 26638 2: { title: 'Paso 2: tu celular', text: 'Escribà el número de tu teléfono móvil. Ya dejamos colocado el código de Argentina (+54).', tip: 'Ejemplo: +54 9 341 000 0000. Lo guardaremos como teléfono de contacto.' }, 26639 3: { title: 'Paso 3: tu correo', text: 'Ahora escribà una dirección de correo que uses y puedas abrir.', tip: 'Revisá que no tenga espacios y que incluya el signo @. Este correo será tu usuario para ingresar.' }, 26640 4: { title: 'Paso 4: una contraseña', text: 'Creá una contraseña que recuerdes, pero que otra persona no pueda adivinar.', tip: 'Debe tener al menos 8 caracteres y una letra mayúscula. Podés tocar el ojito para comprobar lo que escribiste.' }, 26641 5: { title: 'Paso 5: revisar y crear', text: 'Ya completaste todos los datos. Si están correctos, tocá âCrear mi cuentaâ.', tip: 'Con tu teléfono de contacto guardado podrás continuar.' }, 26642 }; 26643 const accountAssistantContent = accountAssistantMethod === 'google' 26644 ?{ title: 'Crear la cuenta con Google', text: 'Esta es la opción más sencilla si ya usás Gmail o tenés una cuenta de Google en el teléfono.', tip: 'Tocá âContinuar con Googleâ y elegà tu cuenta. Rosgan hará el resto; no necesitás inventar otra contraseña.' } 26645 : accountAssistantMethod === 'email' 26646 ?accountAssistantEmailContent[accountAssistantStep] 26647 : { title: 'Vamos a crear tu cuenta', text: 'Primero elegà la forma que te resulte más cómoda. No te preocupes: te voy a indicar exactamente qué tocar.', tip: 'Si usás Gmail, Google suele ser la opción más rápida. También podés registrarte con cualquier correo y una contraseña.' }; 26648 const ferias = (s.ferias || []).filter((item) => !this.isExcludedRemate(item)); 26649 const now = new Date(); const todayKey = now.toDateString(); 26650 const sorted = [...ferias].sort((a, b) => (this.feriaDate(a) || 0) - (this.feriaDate(b) || 0)); 26651 const upcoming = sorted.filter((f) => this.homeAgendaIncludes(this.feriaDate(f), now)); 26652 const todays = upcoming.filter((f) => this.feriaDate(f)?.toDateString() === todayKey); 26653 const hero = todays[0] || upcoming[0] || ferias[0]; 26654 const heroIsLive = !!todays.length; 26655 const p = anim ?(s.statProgress || (s.route === 'home' ?1 : 0)) : 1; 26656 const fmt = (n) =>
26656 Math.round(n).toLocaleString('es-AR'); 26657 const accents = ['#002e5f', '#e63946', '#0a5ca8', '#1f7a63', '#8a5a2b', '#5b3fa8']; 26658 const CONS_ORDER = ['Alzaga Unzué', 'Ildarraz Hnos.', 'Aguirre Vazquez', 'Colombo y Colombo', 'Coop. G. Lehmann', 'Pastore', 'Etchevehere', 'Ferialvarez', 'Ganaderos de Ceres', 'Rigran']; 26659 // Official public destinations for the home cards (independent of internal channels). 26660 const CONS_WEBS = { 26661 'Alzaga Unzué': 'https://www.alzagaunzue.com/', 26662 'Ildarraz Hnos.': 'https://ildarraz.com.ar/', 26663 'Aguirre Vazquez': 'https://aguirrevazquez.com.ar/', 26664 'Colombo y Colombo': 'https://www.colomboycolombo.com.ar/', 26665 'Coop. G. Lehmann': 'https://cooperativalehmann.coop/', 26666 // Their public link page remains available while epastore.com.ar is unavailable. 26667 'Pastore': 'https://linktr.ee/PastoreConsignataria', 26668 'Etchevehere': 'https://etchevehere-rural.com.ar/', 26669 'Ferialvarez': 'https://www.ferialvarez.com.ar/', 26670 'Ganaderos de Ceres': 'https://www.ganaderosdeceres.com.ar/', 26671 'Rigran': 'https://rigran.com.ar/', 26672 }; 26673 const apiCons = s.consignatarias || []; 26674 const consList = apiCons.slice(0, 10).map((c, i) => ({ ...c, name: CONS_ORDER[i] || (c.name || c.nombre || c.slug) })); 26675 const vids = (s.videos || []).slice(0, 8); 26676 const homeIndexCards = this.indexCards(); 26677 const esFecha = (d) => { const dt = new Date(d); return isNaN(dt) ?'' : dt.toLocaleDateString('es-AR', { day: 'numeric', month: 'long', year: 'numeric' }); }; 26678 const esCorta = (d) => { const dt = new Date(d); return isNaN(dt) ?'' : dt.toLocaleDateString('es-AR', { day: '2-digit', month: 'short' }).replace('.', ''); }; 26679 const loteRaw = (s.loteNoticias || []).filter((n) => n.isActive !== false).slice().sort((a, b) => new Date(b.fecha || b.createdAt) - new Date(a.fecha || a.createdAt)); 26680 const loteCards = loteRaw.slice(0, 6).map((n, i) => ({ 26681 title: n.titulo || 'Lote de noticias', href: n.url || n.publicUrl || '#', 26682 date: esFecha(n.fecha), day: esCorta(n.fecha).split(' ')[0] || '', mon: (esCorta(n.fecha).split(' ')[1] || '').toUpperCase(), 26683 featured: i === 0, badge: 'Informe', target: '_blank', 26684 })); 26685 const noticiaRaw = (s.noticias || []).filter((n) => n.isPublished !== false).slice().sort((a, b) => new Date(b.publicationDate) - new Date(a.publicationDate)); 26686 const noticiaLead = noticiaRaw[0] ? { 26687 title: noticiaRaw[0].title, href: noticiaRaw[0].url || '#', category: (noticiaRaw[0].category || 'Noticias').trim(), 26688 excerpt: (noticiaRaw[0].excerpt || '').trim(), date: esFecha(noticiaRaw[0].publicationDate), 26689 hero: noticiaRaw[0].heroImageUrl || 'assets/feria.png', 26690 } : null; 26691 const noticiaCards = noticiaRaw.slice(1, 9).map((n) => ({ 26692 title: n.title, href: n.url || '#', category: (n.category || 'Noticias').trim(), 26693 excerpt: (n.excerpt || '').trim(), date: esFecha(n.publicationDate), hero: n.heroImageUrl || 'assets/feria.png', 26694 })); 26695 this._notiCount = noticiaCards.length; 26696 const notiIdx = noticiaCards.length ?((s.notiSlide % noticiaCards.length) + noticiaCards.length) % noticiaCards.length : 0; 26697 const notiCurrent = noticiaCards[notiIdx] || null; 26698 const notiDots = noticiaCards.map((_, i) => ({ 26699 go: () => this.setState({ notiSlide: i }), 26700 w: i === notiIdx ?'22px' : '7px', bg: i === notiIdx ?'#e63946' : '#c9d5e2', 26701 })); 26702 const navDefs = [ 26703 { label: 'Inicio', route: 'home', path: 'M3 10.5L12 3l9 7.5M5 9.5V21h5v-6h4v6h5V9.5' }, 26704 { label: 'Roslot', route: 'pool', path: 'M20.6 13.4l-7.2 7.2a2 2 0 0 1-2.8 0l-8-8V4a1 1 0 0 1 1-1h8.6l8.4 8.4a2 2 0 0 1 0 2.9zM7.6 7.6h.01' }, 26705 { label: 'Remates', route: 'remates', path: 'M23 7l-7 5 7 5V7zM3 5h13v14H3z' }, 26706 { label: 'Cabañas', route: 'cabanas', path: 'M4 20V9l8-5 8 5v11M8 20v-6h8v6M9 9h.01M15 9h.01' }, 26707 { label: 'Feria', route: 'feria', path: 'M4.9 4.9a10 10 0 0 0 0 14.2M19.1 4.9a10 10 0 0 1 0 14.2M7.8 7.8a6 6 0 0 0 0 8.4M16.2 7.8a6 6 0 0 1 0 8.4M12.9 12a.9.9 0 1 1-1.8 0 .9.9 0 0 1 1.8 0z' }, 26708 { label: 'EstadÃsticas', route: 'mercado', path: 'M4 19V5M4 19h16M8 16l3-4 3 2 4-6' }, 26709 { label: 'Perfil', route: 'profile', path: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z' }, 26710 ].filter((item) => { 26711 if (item.route === 'home') return true; 26712 if (!s.user) return ['remates', 'cabanas'].includes(item.route); 26713 if (item.route === 'pool') return this.canAccessRoslot(s.apiUser); 26714 if (item.route === 'feria') return this.canAccessFeria(s.apiUser); 26715 if (item.route === 'mercado') return this.canAccessStatistics(s.apiUser); 26716 return true; 26717 }); 26718 const activeNav = (s.route === 'lot' || s.route === 'load' || s.route === 'roslotSummary') ?'pool' : ((s.route === 'remateDetail' || s.route === 'rosganResults' || s.route === 'rosganSalesOrder') ?'remates' : ((s.route === 'cabanaDetail' || s.route === 'cabanaAuditor' || s.route === 'cabanaPrebidPanel' || s.route === 'cabanaLotDetail' || s.route === 'cabanaLotCreate' || s.route === 'cabanaStats' || s.route === 'cabanaCommercial' || s.route === 'cabanaPriceBoard' || s.route === 'cabanaSalesOrder' || s.route === 'remateConfig') ?'cabanas' : s.route)); 26719 const rawNotifications = Array.isArray(s.notificationsItems) ?s.notificationsItems : []; 26720 const unreadNotifications = rawNotifications.filter((notification) => !notification.read).length; 26721 const notificationTime = (value) => { 26722 const date = value ?new Date(value) : null; 26723 if (!date || Number.isNaN(date.getTime())) return '';
26724 const elapsed = Math.max(0, Date.now() - date.getTime()); 26725 if (elapsed < 60000) return 'Ahora'; 26726 if (elapsed < 3600000) return `Hace ${Math.floor(elapsed / 60000)} min`; 26727 if (date.toDateString() === new Date().toDateString()) { 26728 return date.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' }); 26729 } 26730 return date.toLocaleDateString('es-AR', { day: '2-digit', month: 'short' }); 26731 }; 26732 const formatNotification = (notification) => { 26733 const type = String(notification.type || ''); 26734 const safeCreditContent = type === 'roslot_credit_granted' 26735 ?{ title: 'Autorización de compra habilitada', body: 'Tu autorización para operar en Roslot fue habilitada.' } 26736 : type === 'roslot_credit_updated' 26737 ?{ title: 'Autorización de compra actualizada', body: 'Tu autorización para operar en Roslot fue actualizada.' } 26738 : type === 'roslot_credit_revoked' 26739 ?{ title: 'Autorización de compra desactivada', body: 'Tu autorización para operar en Roslot fue desactivada.' } 26740 : null; 26741 const notificationBody = String(notification.body || ''); 26742 const privateBidAmount = notificationBody.match(/\$\s?[\d.]+(?:,\d+)?/)?.[0] || ''; 26743 const privateBidCert = String(notification.lotCert || notification.cert || notification.data?.cert || notificationBody.match(/lote\s+([A-Za-z0-9_-]+)/i)?.[1] || ''); 26744 const safeBidContent = type === 'roslot_bid_received' 26745 ?{ 26746 title: 'Recibiste una nueva oferta', 26747 body: 'Oferta reservada' + (privateBidAmount ?' por ' + privateBidAmount : '') + (privateBidCert ?' en el lote ' + privateBidCert : '') + '.', 26748 } 26749 : null; 26750 const legacyLotCreated = type === 'remate_lot_workflow' 26751 ? notificationBody.match(/^(.+?) pasó (el certificado|los certificados) (.+?) a Cargado en (.+)$/) 26752 : null; 26753 const lotCreatedContent = legacyLotCreated ? { 26754 title: 'Nuevo lote cargado', 26755 body: legacyLotCreated[1] + ' cargó ' + (legacyLotCreated[2] === 'el certificado' ? 'un lote (certificado ' : 'lotes (certificados ') + legacyLotCreated[3] + ') en ' + legacyLotCreated[4], 26756 } : null; 26757 const safeContent = safeCreditContent || safeBidContent || lotCreatedContent; 26758 const visual = type === 'roslot_outbid' || type === 'roslot_lost' 26759 ?{ icon: 'trending-down', color: '#c92b38', bg: '#ffe7ea' } 26760 : type === 'cabana_prebid_outbid' 26761 ?{ icon: 'trending-up', color: '#c92b38', bg: '#ffe7ea' } 26762 : type === 'cabana_prebid_won' 26763 ?{ icon: 'trophy', color: '#14745f', bg: '#ddf2eb' } 26764 : type === 'cabana_prebid_received' 26765 ?{ icon: 'gavel', color: '#07518f', bg: '#dcecff' } 26766 : type === 'roslot_won' 26767 ?{ icon: 'trophy', color: '#14745f', bg: '#ddf2eb' } 26768 : type === 'roslot_bid_received' 26769 ?{ icon: 'gavel', color: '#07518f', bg: '#dcecff' } 26770 : type === 'roslot_auction_closing' 26771 ?{ icon: 'clock-3', color: '#a76200', bg: '#fff0d5' } 26772 : type === 'account_role_changed' 26773 ?{ icon: 'user-cog', color: '#07518f', bg: '#dcecff' } 26774 : type.includes('credit') 26775 ?{ icon: 'badge-dollar-sign', color: '#6445c2', bg: '#ece7ff' } 26776 : { icon: 'bell', color: '#07518f', bg: '#dcecff' }; 26777 const companyRef = String(notification.consignatariaId || ''); 26778 const whiteLogo = this.consWhiteLogo(companyRef); 26779 const companyLogo = whiteLogo || this.consLogo(companyRef) || (type === 'remate_heads_milestone' ? '/assets/rosgan-logo-blue.png' : ''); 26780 return { 26781 ...notification, 26782 title: safeContent?.title || notification.title || 'Roslot', 26783 body: safeContent?.body || notification.body || 'Tenés una nueva notificación.', 26784 time: notificationTime(notification.createdAt), 26785 bg: notification.read ?'#fff' : '#eef6ff', 26786 icon: companyLogo || `assets/icons/lucide/${visual.icon}.svg`, 26787 iconBg: companyLogo ? (whiteLogo ? this.consBrandColor(companyRef) : '#fff') : (notification.read ? '#eef2f6' : visual.bg), 26788 iconSize: companyLogo ? '28px' : '17px', 26789 historyIconSize: companyLogo ? '32px' : '19px', 26790 iconRadius: companyLogo ? '10px' : '50%', 26791 iconLabel: companyLogo ? 'Consignataria' : '', 26792 dotBg: notification.read ?'transparent' : visual.color, 26793 status: notification.read ?'LeÃda' : 'Nueva', 26794 statusBg: notification.read ?'#edf1f5' : visual.bg, 26795 statusColor: notification.read ?'#65758a' : visual.color, 26796 open: () => this.openNotification(notification), 26797 }; 26798 }; 26799 const notificationItems = rawNotifications.map(formatNotification); 26800 const notificationHistoryItems = (Array.isArray(s.notificationHistory) ?s.notificationHistory : []).map(formatNotification); 26801 const roslotLiveToastItem = s.roslotLiveToast ?formatNotification(s.roslotLiveToast) : null; 26802 // ---- POOL ---- 26803 const lots = s.lots || []; 26804 const q = s.poolQuery.trim().toLowerCase(); 26805 const f = s.poolFilters; 26806 const baDayFormatter = new Intl.DateTimeFormat('en-CA', { 26807 timeZone: 'America/Argentina/Buenos_Aires', year: 'numeric', month: '2-digit', day: '2-digit', 26808 }); 26809 const baTodayParts = baDayFormatter.formatToParts(new Date()); 26810 const baPart = (type) => baTodayParts.find((part) => part.type === type)?.value || ''; 26811 const baTodayKey = baPart('year') + '-' + baPart('month') + '-' + baPart('day'); 26812 const auctionDay = (lot) => String(lot.fecha || lot.raw?.fechaSubasta || '').slice(0, 10); 26813 const isPoolDay = (lot, view = s.poolStatus) => { 26814 const day = auctionDay(lot); 26815 return view === 'upcoming' ?day > baTodayKey : day === baTodayKey; 26816 }; 26817 const poolEligibleLots = lots.filter((lot) => lot.status !== 'Caducado');
26818 const filterSourceLots = poolEligibleLots.filter((lot) => isPoolDay(lot)); 26819 const todayCategoryMap = new Map(); 26820 lots.filter((lot) => auctionDay(lot) === baTodayKey).forEach((lot) => { 26821 const label = String(lot.categoria || 'Sin categorÃa').trim() || 'Sin categorÃa'; 26822 const key = label.toLocaleLowerCase('es-AR'); 26823 const current = todayCategoryMap.get(key) || { label, lots: 0, heads: 0 }; 26824 current.lots += 1; 26825 current.heads += Math.max(0, Number(lot.cabezas) || 0); 26826 todayCategoryMap.set(key, current); 26827 }); 26828 const roslotDailyCategories = Array.from(todayCategoryMap.values()).map((category) => ({ 26829 label: category.label, 26830 quantity: (category.heads || category.lots).toLocaleString('es-AR'), 26831 unit: category.heads ?'cabezas' : (category.lots === 1 ?'lote' : 'lotes'), 26832 lotsLabel: category.lots === 1 ?'1 lote' : category.lots.toLocaleString('es-AR') + ' lotes', 26833 })); 26834 let filtered = filterSourceLots.filter((l) => 26835 (!q || [l.categoria, l.origen, l.provincia, l.cert, ...(l.razas || [])].join(' ').toLowerCase().includes(q)) 26836 && (!f.category.length || f.category.includes(l.categoria)) 26837 && (!f.destination.length || f.destination.includes(l.destino)) 26838 && (!f.fattening.length || f.fattening.includes(l.engorde)) 26839 && (!f.province.length || f.province.includes(l.provincia)) 26840 && (!f.china || l.aptoChina)); 26841 if (s.poolSort === 'precio-asc') filtered = [...filtered].sort((a, b) => a.baseNum - b.baseNum); 26842 if (s.poolSort === 'precio-desc') filtered = [...filtered].sort((a, b) => b.baseNum - a.baseNum); 26843 if (s.poolSort === 'peso') filtered = [...filtered].sort((a, b) => b.pesoNum - a.pesoNum); 26844 if (s.poolSort === 'cabezas') filtered = [...filtered].sort((a, b) => (Number(b.cabezas) || 0) - (Number(a.cabezas) || 0)); 26845 const failedBidPattern = /(reject|rechaz|declin|lost)/i; 26846 const lotByCert = Object.fromEntries(lots.map((lot) => [lot.cert, lot])); 26847 const ownBidByCert = {}; 26848 (s.myBids || []).forEach((bid) => { 26849 const bidLot = lotByCert[bid.cert]; 26850 if (!bid.cert || bidLot?.auctionStatus !== 'open' || failedBidPattern.test(bid.status)) return; 26851 const current = ownBidByCert[bid.cert]; 26852 const amount = Math.max(Number(current?.amount) || 0, Number(bid.amount) || 0); 26853 const topAmount = Math.max(Number(current?.topAmount) || 0, Number(bid.topAmount) || 0); 26854 const useBidFlag = !current || Number(bid.amount) >= Number(current.amount); 26855 const accepted = /(approv|aprob|accept)/i.test(bid.status); 26856 const outbid = /(outbid|superad)/i.test(bid.status); 26857 ownBidByCert[bid.cert] = { amount, topAmount, isWinning: outbid ?false : (accepted ?true : (useBidFlag ?bid.isWinning : current.isWinning)) }; 26858 }); 26859 Object.entries(s.bidsPlaced || {}).forEach(([cert, amount]) => { 26860 if (lotByCert[cert]?.auctionStatus !== 'open') return; 26861 const current = ownBidByCert[cert] || {}; 26862 ownBidByCert[cert] = { 26863 amount: Math.max(Number(current.amount) || 0, Number(amount) || 0), 26864 topAmount: Number(current.topAmount) || 0, 26865 isWinning: current.isWinning || null, 26866 }; 26867 }); 26868 const standingByCert = {}; 26869 Object.entries(ownBidByCert).forEach(([cert, bid]) => { 26870 const bidLot = lotByCert[cert]; 26871 const authoritativeTop = Number(bidLot?.currentBidAmount) || 0; 26872 const authoritativeBuyer = String(bidLot?.currentBidBuyerUid || ''); 26873 const signedInUid = String(s.user?.uid || ''); 26874 const isWinning = authoritativeBuyer 26875 ?!!signedInUid && authoritativeBuyer === signedInUid 26876 : bid.isWinning; 26877 const effectiveMyAmount = isWinning && authoritativeBuyer 26878 ? Math.max(Number(bid.amount) || 0, authoritativeTop) 26879 : Number(bid.amount) || 0; 26880 const standing = this.computeStanding(cert, effectiveMyAmount, isWinning, Math.max(Number(bid.topAmount) || 0, authoritativeTop)); 26881 if (standing) standingByCert[cert] = standing; 26882 }); 26883 const activeStandingCount = filtered.filter((lot) => !!standingByCert[lot.cert]).length; 26884 const roslotCanBuy = !!s.user && this.canUseBuyerData(s.apiUser); 26885 const roslotCanSell = !!s.user && this.canManageSellerData(s.apiUser); 26886 const ownUid = String(s.user?.uid || ''); 26887 const ownLots = Array.isArray(s.myLots) ?s.myLots : []; 26888 const ownLotCerts = new Set(ownLots.map((item) =>
26888 String(item.cert || '')).filter(Boolean)); 26889 const pendingOwnBids = (s.pendingBids || []) 26890 .filter((bid) => !ownLotCerts.size || ownLotCerts.has(String(bid.cert || ''))) 26891 .filter((bid) => (lotByCert[bid.cert] || ownLots.find((item) => item.cert === bid.cert))?.auctionStatus === 'open'); 26892 const operationLotTitle = (lot, fallbackCategory = '', cert = '') => { 26893 const category = String(lot?.categoria || fallbackCategory || '').trim(); 26894 const quantityRaw = lot?.cabezas ?? lot?.totalCabezas ?? lot?.cantidadTotal; 26895 const quantity = Number(quantityRaw) > 0 ?Number(quantityRaw).toLocaleString('es-AR') : ''; 26896 const weightNumber = Number(lot?.pesoNum ?? lot?.pesoPromedio) || 0; 26897 const weight = weightNumber > 0 ?weightNumber.toLocaleString('es-AR') + ' kg' : (/\d/.test(String(lot?.peso || '')) ?String(lot.peso) : ''); 26898 const location = String(lot?.origen || [lot?.localidad, lot?.provincia].filter(Boolean).join(', ')).trim(); 26899 const subject = [quantity, category].filter(Boolean).join(' ') || ('Lote ' + cert); 26900 return subject + (weight ?' de ' + weight : '') + (location && !/pendiente/i.test(location) ?' en ' + location : ''); 26901 }; 26902 const buyerOperationRows = Object.entries(ownBidByCert).map(([cert, bid]) => { 26903 const operationLot = lotByCert[cert]; 26904 const standing = standingByCert[cert] || this.computeStanding(cert, bid.amount, bid.isWinning, bid.topAmount); 26905 const liveTop = Math.max(0, Number(operationLot?.currentBidAmount) || 0, Number(bid.topAmount) || 0, Number(bid.amount) || 0, ...(s.lotBids?.[cert] || []).map((item) => Number(item.amount) || 0)); 26906 const canBidAgain = standing?.key === 'outbid' && operationLot?.auctionStatus === 'open' && (!operationLot?.sellerUid || operationLot.sellerUid !== ownUid); 26907 const isOutbid = standing?.key === 'outbid'; 26908 return { 26909 priority: isOutbid ?0 : 2, 26910 kind: 'Compra', kindBg: '#e9f3ff', kindColor: '#075ca8', cert, 26911 title: operationLotTitle(operationLot, '', cert), 26912 meta: 'Lote ' + cert + ' · Seguimiento de tu posición en tiempo real', 26913 status: standing?.label || 'Oferta activa', statusColor: standing?.color || '#0a5ca8', 26914 canBid: !!canBidAgain, canApprove: false, disabled: s.quickBidCert === cert, 26915 bid20: () => this.quickBid(cert, liveTop, 20), bid50: () => this.quickBid(cert, liveTop, 50), 26916 open: () => this.openLot(cert), standingKey: standing?.key || '', liveTop, 26917 rowClass: 'is-buyer ' + (isOutbid ?'is-outbid' : 'is-winning'), 26918 isOutbid, 26919 showPrice: true, 26920 priceLabel: isOutbid ?'Oferta lÃder' : 'Vas ganando', 26921 priceValue: this.fmtARS(liveTop), 26922 priceDetail: 'Tu oferta ' + this.fmtARS(Number(bid.amount) || 0), 26923 bidSuccess: s.roslotBidSuccessFx?.cert === cert, 26924 bidSuccessAmount: s.roslotBidSuccessFx?.cert === cert ?this.fmtARS(s.roslotBidSuccessFx.amount) : '', 26925 }; 26926 }); 26927 const sellerPendingRows = pendingOwnBids.map((bid) => { 26928 const operationLot = lotByCert[bid.cert] || ownLots.find((item) => item.cert === bid.cert); 26929 return { 26930 priority: 1, kind: 'Venta', kindBg: '#e7f6f0', kindColor: '#14745f', cert: bid.cert, 26931 title: operationLotTitle(operationLot, bid.lotName, bid.cert), 26932 meta: 'Nueva oferta reservada por ' + this.fmtARS(bid.amount), 26933 status: 'Requiere aprobación', statusColor: '#b66b00', 26934 canBid: false, canApprove: true, disabled: s.bidActioning === bid.id, 26935 approve: () => this.actOnBid(bid.cert, bid.id, 'approve'), reject: () => this.actOnBid(bid.cert, bid.id, 'reject'), 26936 open: () => this.openLot(bid.cert), bidId: bid.id, 26937 rowClass: 'is-seller', showPrice: true, priceLabel: 'Nueva oferta', priceValue: this.fmtARS(bid.amount), priceDetail: 'Lote ' + bid.cert, 26938 }; 26939 }); 26940 const pendingCerts = new Set(sellerPendingRows.map((row) => row.cert)); 26941 const sellerLotRows = ownLots.filter((item) => { 26942 const soldTodayOrJustClosed = item.auctionStatus === 'sold' && (auctionDay(item) === baTodayKey || !!s.closedRoslotLots?.[item.cert]); 26943 return (item.auctionStatus === 'open' || soldTodayOrJustClosed) && !pendingCerts.has(item.cert); 26944 }).map((item) => { 26945 const offers = s.lotBids?.[item.cert] || []; 26946 const isSold = item.auctionStatus === 'sold' || item.status === 'Vendido'; 26947 const top = Math.max(Number(isSold ?item.soldAmount : item.currentBidAmount) || 0, Number(item.currentBidAmount) || 0, ...offers.map((bid) => Number(bid.amount) || 0), 0); 26948 const count = Math.max(Number(item.bidCount) || 0, offers.length); 26949 const canClose = isSold || !!(item.currentBidId || item.currentBidBuyerUid || count > 0); 26950 const closing = s.lotClosingCert === item.cert; 26951 return {
26952 priority: isSold ?4 : 3, kind: 'Venta', kindBg: '#e7f6f0', kindColor: '#14745f', cert: item.cert, 26953 title: operationLotTitle(item, '', item.cert), 26954 meta: 'Lote ' + item.cert + ' · ' + (isSold ?'Vendido' : count + (count === 1 ?' oferta' : ' ofertas')), 26955 status: isSold ?'Vendido' : count ?'Recibiendo ofertas' : 'Publicado', statusColor: isSold ?'#14745f' : count ?'#14745f' : '#65758a', 26956 canBid: false, canApprove: false, canClose, disabled: closing || isSold, open: () => this.openLot(item.cert), 26957 close: () => this.closeRoslotLot(item.cert), closeLabel: isSold ?'Subasta cerrada' : closing ?'Cerrandoâ¦' : 'Bajar el martillo', 26958 rowClass: 'is-seller', showPrice: top > 0, priceLabel: isSold ?'Precio final' : 'Oferta lÃder', priceValue: top ?this.fmtARS(top) : '', priceDetail: isSold ?'Lote vendido' : count + (count === 1 ?' oferta' : ' ofertas'), 26959 }; 26960 }); 26961 const roslotOperations = [...(roslotCanBuy ?buyerOperationRows : []), ...(roslotCanSell ?sellerPendingRows : []), ...(roslotCanSell ?sellerLotRows : [])] 26962 .sort((a, b) => a.priority - b.priority || String(a.cert).localeCompare(String(b.cert), 'es')) 26963 .slice(0, 6); 26964 const buyerWinningCount = buyerOperationRows.filter((row) => row.standingKey === 'winning').length; 26965 const buyerOutbidCount = buyerOperationRows.filter((row) => row.standingKey === 'outbid').length; 26966 const buyerWonCerts = new Set((s.myBids || []).filter((bid) => /(won|adjud|ganad)/i.test(bid.status)).map((bid) => bid.cert)); 26967 const sellerActiveLots = ownLots.filter((item) => item.auctionStatus === 'open').length; 26968 const sellerSoldLots = ownLots.filter((item) => item.auctionStatus === 'sold').length; 26969 const sellerReceivedOffers = ownLots.reduce((sum, item) => sum + (Number(item.bidCount) || (s.lotBids?.[item.cert] || []).length || 0), 0); 26970 const roslotKpis = roslotCanBuy && roslotCanSell 26971 ?[ 26972 { label: 'Posiciones activas', value: buyerOperationRows.length, detail: 'compras en curso', color: '#075ca8' }, 26973 { label: 'Vas ganando', value: buyerWinningCount, detail: buyerOutbidCount + ' superadas', color: '#14745f' }, 26974 { label: 'Ofertas recibidas', value: sellerReceivedOffers, detail: sellerActiveLots + ' lotes activos', color: '#075ca8' }, 26975 { label: 'Por resolver', value: buyerOutbidCount + pendingOwnBids.length, detail: 'requieren acción', color: '#c92b38' }, 26976 ] 26977 : roslotCanSell 26978 ?[ 26979 { label: 'Lotes activos', value: sellerActiveLots, detail: ownLots.length + ' publicados', color: '#075ca8' }, 26980 { label: 'Ofertas recibidas', value: sellerReceivedOffers, detail: 'sobre tus lotes', color: '#14745f' }, 26981 { label: 'Por aprobar', value: pendingOwnBids.length, detail: 'requieren acción', color: '#c92b38' }, 26982 { label: 'Vendidos', value: sellerSoldLots, detail: 'operaciones cerradas', color: '#6b4bb5' }, 26983 ] 26984 :[ 26985 { label: 'Ofertas activas', value: buyerOperationRows.length, detail: 'posiciones abiertas', color: '#075ca8' }, 26986 { label: 'Vas ganando', value: buyerWinningCount, detail: 'liderando ahora', color: '#14745f' }, 26987 { label: 'Te superaron', value: buyerOutbidCount, detail: 'requieren acción', color: '#c92b38' }, 26988 { label: 'Lotes ganados', value: buyerWonCerts.size, detail: 'operaciones cerradas', color: '#6b4bb5' }, 26989 ]; 26990 const syntheticActivity = [ 26991 ...buyerOperationRows.filter((row) => row.standingKey === 'outbid').map((row) => ({ 26992 key: 'outbid:' + row.cert, cert: row.cert, title: 'Te superaron en el lote ' + row.cert, 26993 body: 'La oferta lÃder es ' + this.fmtARS(row.liveTop) + '. Podés volver a ofertar desde tu panel.', time: 'Ahora', status: 'Atención', statusColor: '#c92b38', 26994 icon: 'assets/icons/lucide/trending-down.svg', iconBg: '#ffe7ea', open: row.open, 26995 })), 26996 ...sellerPendingRows.map((row) => ({ 26997 key: 'received:' + row.cert, cert: row.cert, title: 'Recibiste una oferta en el lote ' + row.cert, 26998 body: row.meta, time: 'Ahora', status: 'Por resolver', statusColor: '#b66b00', 26999 icon: 'assets/icons/lucide/gavel.svg', iconBg: '#dcecff', open: row.open, 27000 })), 27001 ]; 27002 const activityKey = (event) => { 27003 const type = String(event.type || ''); 27004 const directCert = String(event.cert || event.lotCert || event.data?.cert || ''); 27005 const textCert = [event.title, event.body, event.href].filter(
27005Boolean).join(' ').match(/lote\s+([A-Za-z0-9_-]+)/i)?.[1] || ''; 27006 const cert = directCert || textCert; 27007 if (type === 'roslot_outbid' || /superaron/i.test(event.title || '')) return 'outbid:' + cert; 27008 if (type === 'roslot_bid_received' || /recibiste una oferta/i.test(event.title || '')) return 'received:' + cert; 27009 return 'notification:' + String(event.id || [event.title, event.createdAt].filter(Boolean).join(':')); 27010 }; 27011 const activityDayKey = (value) => { 27012 const date = value ?new Date(value) : null; 27013 if (!date || Number.isNaN(date.getTime())) return ''; 27014 const parts = baDayFormatter.formatToParts(date); 27015 const part = (type) => parts.find((item) => item.type === type)?.value || ''; 27016 return part('year') + '-' + part('month') + '-' + part('day'); 27017 }; 27018 const towerNotificationSeen = new Set(); 27019 const towerNotifications = [...notificationItems, ...notificationHistoryItems] 27020 .filter((event) => activityDayKey(event.createdAt) === baTodayKey) 27021 .filter((event) => { 27022 const key = String(event.id || activityKey(event)); 27023 return !towerNotificationSeen.has(key) && towerNotificationSeen.add(key); 27024 }); 27025 const towerSemanticKeys = new Set(towerNotifications.map(activityKey)); 27026 const pulseKey = String(s.roslotLivePulseId || ''); 27027 const towerHistory = towerNotifications.map((event) => { 27028 const notificationKey = String(event.id || [event.type, event.createdAt, event.title].filter(Boolean).join(':')); 27029 return { ...event, key: 'notification:' + notificationKey, rowClass: notificationKey === pulseKey ?'is-arriving' : '' }; 27030 }); 27031 const activitySeen = new Set(); 27032 const roslotActivity = [...towerHistory, ...syntheticActivity.filter((event) => !towerSemanticKeys.has(event.key)).map((event) => ({ ...event, rowClass: '' }))] 27033 .filter((event) => !activitySeen.has(event.key) && activitySeen.add(event.key)) 27034 .slice(0, 100); 27035 const uniq = (arr) => [...new Set(arr.filter((v) => v && v !== 'Pendiente'))]; 27036 const mkOptions = (key, values) => uniq(values).map((v) => ({ 27037 label: v, toggle: () => this.toggleFilter(key, v), 27038 bg: f[key].includes(v) ?'#002e5f' : '#f2f6fb', color: f[key].includes(v) ?'#fff' : '#3d4c5f', 27039 border: f[key].includes(v) ?'#002e5f' : '#dde7f2', 27040 })); 27041 const activeFilterCount = f.category.length + f.destination.length + f.fattening.length + f.province.length + (f.china ?1 : 0); 27042 const statusDefs = [ 27043 { key: 'today', label: 'Subasta de hoy' }, 27044 ]; 27045 const marketNow = Date.now(); 27046 const marketTiming = this.roslotMarketTiming(lots, marketNow); 27047 const marketOpen = lots.some((lot) => lot.auctionStatus === 'open' && auctionDay(lot) === baTodayKey) || marketTiming.isOpen; 27048 const marketTarget = marketOpen ?(marketTiming.isOpen ?marketTiming.target : null) : marketTiming.target; 27049 const marketProgressValue = marketOpen && !marketTiming.isOpen ?100 : Math.round(Number(marketTiming.progress || 0) * 10) / 10; 27050 const marketRemainingLabel = (target) => { 27051 const seconds = Math.max(0, Math.ceil((target - marketNow) / 1000)); 27052 const days = Math.floor(seconds / 86400); 27053 const hours = Math.floor((seconds % 86400) / 3600); 27054 const minutes = Math.floor((seconds % 3600) / 60); 27055 const secs = seconds % 60; 27056 if (days) return `${days} d ${String(hours).padStart(2, '0')} h`; 27057 return `${String(hours).padStart(2, '0')}
27057:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; 27058 }; 27059 const marketTargetLabel = marketTarget ?new Intl.DateTimeFormat('es-AR', { 27060 timeZone: 'America/Argentina/Buenos_Aires', weekday: 'short', day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23', 27061 }).format(new Date(marketTarget)).replace(',', ' ·') : ''; 27062 const marketStatus = s.lotsLoading 27063 ?{ label: 'Consultando mercado', bg: 'rgba(10,92,168,0.08)', color: '#45627d', border: '#d7e5f2', icon: 'assets/icons/lucide/loader-circle.svg' } 27064 : marketOpen 27065 ?{ label: 'Mercado abierto', bg: 'rgba(31,122,99,0.12)', color: '#176c58', border: 'rgba(31,122,99,0.28)', icon: 'assets/icons/lucide/radio.svg' } 27066 : { label: 'Mercado cerrado', bg: 'rgba(101,117,138,0.1)', color: '#526174', border: '#d7e0ea', icon: 'assets/icons/lucide/lock-keyhole.svg' }; 27067 const hoverIn = (e) => { 27068 const v = e.currentTarget.querySelector('[data-lot-preview]'); 27069 if (v) { 27070 v.dataset.hoverActive = '1'; 27071 v.style.opacity = '0'; 27072 v.defaultMuted = true; 27073 v.muted = true; 27074 v.volume = 0; 27075 v.loop = true; 27076 v.playsInline = true; 27077 v.setAttribute('muted', ''); 27078 v.setAttribute('loop', ''); 27079 if (v.dataset.muteLocked !== '1') { 27080 v.dataset.muteLocked = '1'; 27081 v.addEventListener('volumechange', () => { 27082 if (!v.muted || v.volume !== 0) { 27083 v.muted = true; 27084 v.volume = 0; 27085 } 27086 }); 27087 } 27088 if (v.dataset.previewBound !== '1') { 27089 v.dataset.previewBound = '1'; 27090 const showWhenReady = () => { 27091 if (v.dataset.hoverActive !== '1' || v.paused || v.readyState < 2) return; 27092 const revealFrame = () => { 27093 v.dataset.previewFramePending = '0'; 27094 if (v.dataset.hoverActive === '1' && !v.paused) v.style.opacity = '1'; 27095 }; 27096 if (typeof v.requestVideoFrameCallback === 'function') { 27097 if (v.dataset.previewFramePending === '1') return; 27098 v.dataset.previewFramePending = '1'; 27099 v.requestVideoFrameCallback(revealFrame); 27100 } else { 27101 revealFrame(); 27102 } 27103 }; 27104 const hideBrokenPreview = () => { v.style.opacity = '0'; }; 27105 v.addEventListener('playing', showWhenReady); 27106 v.addEventListener('loadeddata', showWhenReady); 27107 v.addEventListener('error', hideBrokenPreview); 27108 v.addEventListener('emptied', hideBrokenPreview); 27109 } 27110 if (!v.src && v.dataset.src) v.src = v.dataset.src; 27111 const playback = v.play(); 27112 if (playback?.catch) playback.catch(() => { v.style.opacity = '0'; }); 27113 } 27114 const img = e.currentTarget.querySelector('[data-lot-img]'); 27115 if (img) img.style.transform = 'scale(1.06)'; 27116 }; 27117 const hoverOut = (e) => { const v = e.currentTarget.querySelector('[data-lot-preview]'); if (v) { v.dataset.hoverActive = '0'; v.style.opacity = '0'; v.pause(); } const img = e.currentTarget.querySelector('[data-lot-img]'); if (img) img.style.transform = 'scale(1)'; }; 27118 // ---- LOT DETAIL ---- 27119 const lot = this.currentLot(); 27120 const adj = lot ?this.bidAdj(lot.cert) : { increment: 20, offset: 20 }; 27121 const curBid = lot ?this.currentBid(lot) : 0; 27122 const target = curBid + adj.offset; 27123 const selVideo = lot?.videos[Math.min(s.selectedVideo, Math.max((lot?.videos.length || 1) - 1, 0))]; 27124 // ---- LOAD WIZARD ---- 27125 const fm = this.form(); 27126 const opt = (cat, key) => this.CATALOGS[cat].map(([id, label]) => ({ id, label, pick: () => this.setF(key, id), 27127 bg: fm[key] === id ?'#002e5f' : '#f2f6fb', color: fm[key] === id ?'#fff' : '#3d4c5f', border: fm[key] === id ?'#002e5f' : '#dde7f2' })); 27128 const wizFilledVideos = fm.videos.filter(Boolean); 27129 const canLoadLot = this.canManageSellerData(s.apiUser); 27130 const rc = s.remateConfigForm || this.emptyRemateConfig(); 27131 const rcToggles = s.remateConfigToggles || this.emptyRemateConfigToggles(); 27132 const rcOption = (value, label = value) => ({ value, label }); 27133 const rcField = (key, label, kind = 'text', options = [], placeholder = '', help = '') => { 27134 const value = rc[key] || ''; 27135 const isOpen = value === 'abierta'; 27136 return { key, label, value, placeholder, help, options, isPlace: kind === 'place', isText: kind === 'text', isSelect: kind === 'select', isDate: kind === 'date', isTime: kind === 'time', isSwitch: kind === 'switch', switchState: isOpen ?'Abierta' : 'Cerrada', switchTrack: isOpen ?'#1f8a70' : '#cfd8e3', switchKnob: isOpen ?'20px' : '0px', on: (e) => this.setRemateConfigField(key, e.target.value, kind === 'text'), toggle: () => this.setRemateConfigField(key, isOpen ?'cerrada' : 'abierta') }; 27137 }; 27138 const rawConsignatarias = (Array.isArray(s.consignatarias) ?s.consignatarias : (Array.isArray(s.consignatarias?.items) ?s.c
27138onsignatarias.items : [])) 27139 .filter((item) => !this.isExcludedConsignataria(item?.name || item?.nombre || item?.slug || item?.id)); 27140 const remateConsignatarias = rawConsignatarias.map((item) => ({ 27141 value: String(item?.id || item?.consignatariaId || item?.slug || this.rematePayloadKey(this.consName(item))), 27142 label: this.consName(item), 27143 })).filter((item) => item.value && item.label && !/reggi|gananor/i.test(item.label)).filter((item, index, all) => all.findIndex((candidate) => candidate.value === item.value) === index).sort((a, b) => a.label.localeCompare(b.label, 'es')); 27144 const lotSellerUid = String(lot?.sellerUid || '').trim(); 27145 const lotBelongsToCurrentUser = !!lotSellerUid && lotSellerUid === String(s.user?.uid || ''); 27146 const lotSellerRelation = (s.lotSellerConsignatarias || {})[lotSellerUid] 27147 || (lotBelongsToCurrentUser ?s.userConsignataria : null); 27148 const lotSellerConsignatariaRef = lotSellerRelation?.ref || lotSellerRelation?.id || lotSellerRelation?.slug || lotSellerRelation?.name || ''; 27149 const lotConsignatariaRef = String(lot?.consignatariaId || lot?.consignatariaName || lotSellerConsignatariaRef || '').trim(); 27150 const lotConsignatariaKey = this.consKey(lotConsignatariaRef); 27151 const lotConsignatariaIsGeneric = /^(productorrosgan|productor|vendedor|seller)$/.test(lotConsignatariaKey); 27152 const lotConsignatariaRecord = !lotConsignatariaIsGeneric && lotConsignatariaKey 27153 ?rawConsignatarias.find((item) => [item?.id, item?.consignatariaId, item?.slug, item?.name, item?.nombre] 27154 .some((value) => this.consKey(value) === lotConsignatariaKey)) 27155 : null; 27156 const lotHasConsignataria = !!lotConsignatariaKey && !lotConsignatariaIsGeneric; 27157 const lotPublisherRelationResolved = !lotSellerUid 27158 || Object.prototype.hasOwnProperty.call(s.lotSellerConsignatarias || {}, lotSellerUid) 27159 || (lotBelongsToCurrentUser && !!s.userConsignataria); 27160 const lotPublisherPending = !!lot && !lotHasConsignataria && !lotPublisherRelationResolved; 27161 const lotPublisherName = lotHasConsignataria 27162 ?((lotConsignatariaRecord ?this.consName(lotConsignatariaRecord) : '') || lot?.consignatariaName || this.humanizeSlug(lotConsignatariaRef)) 27163 : (lotPublisherPending ?'Cargando consignatariaâ¦' : 'CAF'); 27164 const lotPublisherLogo = lotHasConsignataria 27165 ?(this.consLogo(lotConsignatariaRef) || this.consLogo(lotPublisherName) || lotConsignatariaRecord?.logoUrl || lotConsignatariaRecord?.logo || lotConsignatariaRecord?.imageUrl || this.consFullHeroLogo(lotPublisherName)) 27166 : ''; 27167 const lotPublisherUsesWhiteLogo = /\/consignatarias-full-white\//i.test(lotPublisherLogo); 27168 const remateProvincias = s.geoProvinces || []; 27169 const remateLocalidades = (s.geoLocs?.[rc.provincia] || []).map((item) => item.nombre); 27170 const remateConfigSections = [ 27171 { step: '01', title: 'Datos del remate', fields: [ 27172 rcField('titulo', 'TÃtulo', 'text', [], 'Nombre completo del remate'), 27173 rcField('cabezas', 'Cabezas estimadas', 'text', [], 'Ej. 1.500'), 27174 rcField('consignataria', 'Consignataria', 'select', [rcOption('', 'Seleccioná la consignataria'), ...remateConsignatarias.map((item) => rcOption(item.value, item.label))]), 27175 rcField('provincia', 'Provincia', 'select', ['', ...remateProvincias].map((value) => rcOption(value, value || (s.geoLoadingProv ?'Cargando provinciasâ¦' : 'Seleccioná la provincia')))), 27176 rcField('localidad', 'Localidad', 'place'), rcField('lugar', 'Lugar', 'text', [], 'Predio o establecimiento'), 27177 ] }, 27178 { step: '02', title: 'Fecha y transmisión', fields: [rcField('fecha', 'Fecha', 'date'), rcField('hora', 'Hora', 'time'), rcField('urlStreaming', 'URL de streaming', 'text', [], 'https://...', 'Podés dejarla vacÃa y agregarla más adelante')] }, 27179 { step: '03', title: 'Contacto del remate · Opcional', fields: [ 27180 rcField('contactoNombre', 'Nombre', 'text', [], 'Nombre y apellido'), 27181 rcField('contactoTelefono', 'Teléfono', 'text', [], '+54 9 ...', 'Se utilizará para llamadas y consultas por WhatsApp'), 27182 rcField('contactoEmail', 'Email', 'text', [], '[email protected]'), 27183 rcField('contactoNotas', 'Notas', 'text', [], 'Información interna opcional'), 27184 ] }, 27185 { step: '04', title: 'Carga de datos y lotes', fields: [ 27186 rcField('cargaDatos', 'Carga de datos', 'switch'), 27187 rcField('cargaLotes', 'Habilitar carga de lotes', 'switch'), 27188 ] }, 27189 ]; 27190 const remateConfigOptions = [['videoFinal','Habilitar video final'],['preIndividual','Preoferta individual'],['apruebaAuto','Aprobar preofertas automáticamente'],['preAgrupada','Agrupar preofertas'],['agrupaCorrales','Agrupar corrales']].map(([key, label]) => { const on = !!rcToggles[key]; return { label, toggle: () => this.toggleRemateConfig(key), track: on ?'#0a5ca8' : '#cfdbe9', knob: on ?'16px' : '0px', bg: on ?'#f1f7fd' : '#f8fafc', border: on ?'#b8d5f2' : '#e3eaf2' }; }); 27191 const preofferFields = [rcField('preInicioFecha', 'Inicio', 'date'), rcField('preInicioHora', 'Hora de inicio', 'time'), rcField('preFinFecha', 'Finalización', 'date'), rcField('preFinHora', 'Hora de finalización', 'time')]; 27192 const cabanaRows = (group) => (rc[group] || []).map((item) => ({ id: item.id, name: item.nombre || '', logo: item.logoDataUrl || '', hasLogo: !!item.logoDataUrl, noLogo: !item.logoDataUrl, fileLabel: item.logoName || 'Seleccionar logo PNG', onName: (e) => this.setCabanaConfigEntry(group, item.id, { nombre: e.target.value }), onFile: (e) => this.readCabanaConfigLogo(group, item.id, e.target.files?.[0]), remove: () => this.removeCabanaConfigEntry(group, item.id) })); 27193 const cabanaParticipantGroups = [ 27194 { title: 'Cabañas principales', help: 'Organizadoras y protagonistas del remate.', rows: cabanaRows('cabanasPrincipales'), add: () => this.addCabanaConfigEntry('cabanasPrincipales') }, 27195 { title: 'Cabañas invitadas', help: 'Participantes adicionales del catálogo.', rows: cabanaRows('cabanasInvitadas'), add: () => this.addCabanaConfigEntry('cabanasInvitadas') }, 27196 ]; 27197 const hasRemateCatalog = !!rc.catalogoPdfName; 27198 const remateCatalogSizeMb = Number(rc.catalogoPdfSize || 0) / (1024 * 1024); 27199 const remateFieldTypeOptions = [rcOption('texto','Texto'), rcOption('numero','Número'), rcOption('fecha','Fecha'), rcOption('seleccion','Selección'), rcOption('booleano','Sà / No')]; 27200 const remateCategoryForms = (rc.formulariosCategoria || []).map((rawCategory, c
27200ategoryIndex) => { 27201 const category = this.normalizeRemateCategoryForm(rawCategory); 27202 const visibleCount = category.formularios.flatMap((subform) => subform.campos || []).filter((field) => field.mostrarTarjeta).length; 27203 return { 27204 id: category.id, 27205 number: categoryIndex + 1, 27206 category: category.categoria || '', 27207 visibleLabel: visibleCount + ' / 4 en tarjeta', 27208 onCategory: (e) => this.setRemateCategoryForm(category.id, e.target.value), 27209 addSubform: () => this.addRemateSubform(category.id), 27210 remove: () => this.removeRemateCategoryForm(category.id), 27211 forms: category.formularios.map((subform, subformIndex) => ({ 27212 id: subform.id, 27213 categoryId: category.id, 27214 dragLabel: `Ordenar subformulario ${subformIndex + 1}~${category.id}~${subform.id}`, 27215 dropLabel: `Destino subformulario~${category.id}~${subform.id}`, 27216 number: subformIndex + 1, 27217 name: subform.nombre || '', 27218 order: subform.orden || subformIndex + 1, 27219 active: subform.activo !== false, 27220 activeLabel: subform.activo !== false ?'Activo' : 'Inactivo', 27221 activeTrack: subform.activo !== false ?'#1f8a70' : '#cfd8e3', 27222 activeKnob: subform.activo !== false ?'16px' : '0px', 27223 onName: (e) => this.setRemateSubform(category.id, subform.id, { nombre: e.target.value }), 27224 onDragStart: (e) => this.startRemateConfigDrag('subform', category.id, subform.id, null, e), 27225 onDragEnter: (e) => this.reorderRemateSubforms(category.id, subform.id, e, true), 27226 onMouseDown: (e) => this.startRemateConfigDrag('subform', category.id, subform.id, null, e), 27227 onPointerDown: (e) => this.startRemateConfigDrag('subform', category.id, subform.id, null, e), 27228 onMouseEnter: (e) => { if (e.buttons === 1 && this._remateConfigDrag?.type === 'subform') this.reorderRemateSubforms(category.id, subform.id, e, true); }, 27229 onPointerEnter: (e) => { if (this._remateConfigDrag?.type === 'subform') this.reorderRemateSubforms(category.id, subform.id, e, true); }, 27230 onMouseUp: () => this.finishRemateConfigDrag(), 27231 onDragOver: (e) => this.allowRemateConfigDrop(e), 27232 onDrop: (e) => this.reorderRemateSubforms(category.id, subform.id, e), 27233 toggleActive: () => this.setRemateSubform(category.id, subform.id, { activo: subform.activo === false }), 27234 addField: () => this.addRemateFormField(category.id, subform.id), 27235 remove: () => this.removeRemateSubform(category.id, subform.id), 27236 fields: (subform.campos || []).map((field, fieldIndex) => ({ 27237 id: field.id, 27238 categoryId: category.id, 27239 subformId: subform.id, 27240 dragLabel: `Ordenar campo ${fieldIndex + 1}~${category.id}~${subform.id}~${field.id}`, 27241 dropLabel: `Destino campo~${category.id}~${subform.id}~${field.id}`, 27242 number: fieldIndex + 1, 27243 name: field.nombre || '', 27244 type: field.tipo || 'texto', 27245 order: field.orden || fieldIndex + 1, 27246 optionsText: field.opciones || '', 27247 isSelection: field.tipo === 'seleccion', 27248 onName: (e) => this.setRemateFormField(category.id, subform.id, field.id, { nombre: e.target.value }), 27249 onType: (e) => this.setRemateFormField(category.id, subform.id, field.id, { tipo: e.target.value }), 27250 onOptions: (e) => this.setRemateFormField(category.id, subform.id, field.id, { opciones: e.target.value }), 27251 onDragStart: (e) => this.startRemateConfigDrag('field', category.id, subform.id, field.id, e), 27252 onDragEnter: (e) => this.reorderRemateFormFields(category.id, subform.id, field.id, e, true), 27253 onMouseDown: (e) => this.startRemateConfigDrag('field', category.id, subform.id, field.id, e), 27254 onPointerDown: (e) => this.startRemateConfigDrag('field', category.id, subform.id, field.id, e), 27255 onMouseEnter: (e) => { if (e.buttons === 1 && this._remateConfigDrag?.type === 'field') this.reorderRemateFormFields(category.id, subform.id, field.id, e, true); }, 27256 onPointerEnter: (e) => { if (this._remateConfigDrag?.type === 'field') this.reorderRemateFormFields(category.id, subform.id, field.id, e, true); }, 27257 onMouseUp: () => this.finishRemateConfigDrag(), 27258 onDragOver: (e) => this.allowRemateConfigDrop(e), 27259 onDrop: (e) => this.reorderRemateFormFields(category.id, subform.id, field.id, e), 27260 typeOptions: remateFieldTypeOptions, 27261 remove: () => this.removeRemateFormField(category.id, subform.id, field.id), 27262 characteristic: !!field.identificadorCaracteristico, 27263 characteristicLabel: field.identificadorCaracteristico ?'SÃ':'No', 27264 toggleCharacteristic: () => this.setRemateFormField(category.id, subform.id, field.id, {identificadorCaracteristico: !field.identificadorCaracteristico}), 27265 toggleIdentifier: () => this.setRemateFormField(category.id, subform.id, field.id, { identificadorPrincipal: !field.identificadorPrincipal }), 27266 identifierTrack: field.identificadorPrincipal ?'#0a5ca8' : '#f7f9fc', 27267 identifierBorder: field.identificadorPrincipal ?'#0a5ca8' : '#dbe4ee', 27268 identifierText: field.identificadorPrincipal ?'#fff' : '#526276', 27269 identifierIconFilter: field.identificadorPrincipal ?'brightness(0) invert(1)' : 'none', 27270 toggleCard: () => this.setRemateFormField(category.id, subform.id, field.id, { mostrarTarjeta: !field.mostrarTarjeta }), 27271 cardTrack: field.mostrarTarjeta ?'#1f7a63' : '#f7f9fc', 27272 cardBorder: field.mostrarTarjeta ?'#1f7a63' : '#dbe4ee', 27273 cardText: field.mostrarTarjeta ?'#fff' : '#64748b', 27274 cardIconFilter: field.mostrarTarjeta ?'brightness(0) invert(1)' : 'none', 27275 cardOpacity: !field.mostrarTarjeta && visibleCount >= 4 ?'.5' : '1', 27276 })), 27277 })), 27278 }; 27279 }); 27280 const avatarCompany=s.user ? (s.userConsignataria || s.userConsignatarias?.[0]) : null; 27281 const userConsignatariaRef = avatarCompany?.id || avatarCompany?.slug || avatarCompany?.ref || avatarCompany?.name || ''; 27282 const userConsignatariaMatch = userConsignatariaRef && (s.consignatarias || []).find((consignataria) => [consignataria.id, consignataria.slug, consignataria.name, consignataria.nombre].some((value) => this.consKey(value) === this.consKey(userConsignatariaRef))); 27283 const currentProfileData = this.profileApiData(); 27284 const currentProfileName = [currentProfileData.firstName || currentProfileData.nombre, currentProfileData.lastName || currentProfileData.apellido].filter(
27284Boolean).join(' ').trim(); 27285 const advanceNameRaw = currentProfileData.firstName || currentProfileData.nombre || String(s.user?.displayName || '').trim().split(/\s+/)[0] || String(s.user?.email || '').split('@')[0]; 27286 const advanceName = String(advanceNameRaw || '').trim(); 27287 const advanceGreeting = advanceName ?'¡Hola, ' + advanceName.charAt(0).toUpperCase() + advanceName.slice(1) + '!' : '¡Hola!'; 27288 const launchInterestSubscribed = s.launchInterest?.status === 'subscribed'; 27289 const currentProfilePhoto = s.profilePhotoFailed ?'' : this.profilePhotoUrl(currentProfileData); 27290 const currentAvatar = this.commercialAvatar(currentProfileData, userConsignatariaRef, s.profilePhotoFailed); 27291 const userConsignatariaAvatar = currentAvatar.avatar; 27292 const userHasConsignatariaAvatar = !!userConsignatariaAvatar; 27293 const userHasProfilePhoto = !!currentProfilePhoto; 27294 const rgf = this.rosganForm(); 27295 const rgCertifierOptions=[{uid:'',displayName:'Sin responsable comercial'},...(s.rosganCertifiers||[])].map(c=>{ 27296 const own=String(c.uid)===String(s.user?.uid); 27297 const avatar=c.uid?this.commercialAvatar(own?currentProfileData:c,c.consignatariaId||rgf.consignatariaId,!!s.rosganCertifierPhotoFailed?.[c.uid]||(own&&s.profilePhotoFailed)):{avatar:'',hasAvatar:false,noAvatar:true,bg:'#edf3f8',size:'22px',fit:'contain'}; 27298 const label=c.displayName||[c.firstName,c.lastName].filter(Boolean).join(' ')||c.email||c.uid; 27299 const selected=String(c.uid)===String(rgf.certifiedBy||''); 27300 return {...avatar,value:c.uid,label,initial:c.uid?label.charAt(0):'â',selected,rowBg:selected?'#edf4fa':'#fff',check:selected?'â':'',avatarError:()=>{if(avatar.photo&&!this.state.rosganCertifierPhotoFailed?.[c.uid])this.setState({rosganCertifierPhotoFailed:{...this.state.rosganCertifierPhotoFailed,[c.uid]:true}});},pick:()=>this.setState({rosganLotForm:{...this.rosganForm(),certifiedBy:c.uid||null},rosganCertifierMenuOpen:false})}; 27301 }); 27302 const rgCategory = this.rosganSelectedCategory(); 27303 const rgFormType = String(rgCategory?.formType || '').toLowerCase(); 27304 const rgLotType = String(rgf.formType || rgCategory?.formType || '').toLowerCase(); 27305 const rgFilteredCategories = (s.rosganLotCategories || []).filter((item) => String(item?.formType || '').toLowerCase() === rgLotType); 27306 const rgQuantityMode = String(rgCategory?.quantityMode || 'total').toLowerCase(); 27307 const rgInput = (key) => (event) => { 27308 const tag = String(event.currentTarget?.tagName || '').toLowerCase(); 27309 const type = String(event.currentTarget?.type || 'text').toLowerCase(); 27310 const buffered = tag === 'textarea' || (tag === 'input' && !['checkbox', 'radio', 'range', 'file', 'date', 'time'].includes(type)); 27311 this.setRosganLotField(key, event.target.value, buffered); 27312 }; 27313 const rgToggle = (key) => () => this.setRosganLotField(key, !this.rosganForm()[key]); 27314 const rgOption = (...aliases) => this.rosganFieldOptions(...aliases); 27315 const rgDescriptionOptions = this.rosganDescriptionChoices(); 27316 const rgSelectedDescriptionOptions = Array.isArray(rgf.descriptionOptionValues) ?rgf.descriptionOptionValues : []; 27317 const rgAgeUnitOptions = rgOption('ageUnits', 'ageUnit', 'unidadEdad', 'unidadDeEdad'); 27318 const rgDentitionOptions = rgOption('dentitions', 'dentition', 'denticion'); 27319 const rgReproductiveStatusOptions = rgFormType === 'vientres' 27320 ?rgOption('vientresReproductiveStatuses', 'reproductiveStatuses', 'reproductiveStatus', 'estadoReproductivo') 27321 :rgOption('invernadaReproductiveStatuses', 'reproductiveStatuses', 'reproductiveStatus', 'estadoReproductivo'); 27322 const rgDestinationOptions = rgFormType === 'faena' 27323 ?rgOption('faenaDestinations', 'destinations', 'destination', 'destino') 27324 :rgOption('generalDestinations', 'destinations', 'destination', 'destino'); 27325 const rgBodyConditionOptions = rgOption('bodyConditions', 'bodyCondition', 'condicionCorporal'); 27326 const rgAnimalClassOptions = rgOption('classes', 'animalClasses', 'animalClass', 'claseAnimal', 'clase'); 27327 const rgTickStatusOptions = rgOption('tickStatuses', 'tickStatus', 'estadoGarrapata', 'garrapata'); 27328 const rgHealthOptions = rgOption('healthStatuses', 'healthStatus', 'health', 'sanidad'); 27329 const rgNutritionOptions = rgOption('nutritions', 'nutrition', 'nutricion'); 27330 const rgWeighingOptions = rgOption('weighings', 'weighing', 'pesaje'); 27331 const rgServiceOptions = rgOption('service', 'tipoServicio', 'servicio'); 27332 const rgPregnancyOptions = rgOption('pregnancy', 'prenez', 'preñez'); 27333 const rgCalvingOptions = rgOption('calving', 'paricion'); 27334 const rgSubCategoryOptions = this.rosganFaenaSubCategoryChoices(); 27335 const rgConditionTypeOptions = rgOption('faenaConditionTypes', 'conditionTypes', 'conditionType', 'condicion'); 27336 const rgDeliveryTypeOptions = rgOption('faenaDeliveryTypes', 'deliveryTypes', 'deliveryType', 'entrega'); 27337 const rgFinishingOptions = rgOption('faenaFinishings', 'finishings', 'finishing', 'terminacion'); 27338 const rgDate = (value) => { try { return new Date(value).toLocaleDateString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric' }); } catch { return ''; } }; 27339 const rgStep = Number(s.rosganLotStep || 0); 27340 const rgRemate = (s.rosganLotRemates || []).find((item) => String(item.id) === String(rgf.remateId)); 27341 const rgConsignataria = (s.rosganLotConsignatarias || []).find((item) => String(item.id) === String(rgf.consignatariaId)); 27342 const rgConsignatariaOptions = this.rosganConsignatariasForRemate().map((item) => { 27343 const id = String(item.id); 27344 const name = item.name || item.nombre || item.consignatariaNombre || item.id; 27345 const logo = this.consLogo(name) || item.logoUrl || item.logo || item.imageUrl;
27346 const initials = String(name || '').split(/\s+/).filter(Boolean).slice(0, 2).map((word) => word.charAt(0).toUpperCase()).join('') || 'C'; 27347 const selected = id === String(rgf.consignatariaId); 27348 return { 27349 id, name, logo, initials, selected, hasLogo: !!logo, showsInitials: !logo, 27350 className: 'rg-cons-picker-option' + (selected ?' is-selected' : ''), 27351 pick: () => this.onRosganConsignatariaChange(id), 27352 }; 27353 }); 27354 const rgSelectedConsignataria = rgConsignatariaOptions.find((item) => item.selected) || null; 27355 const rgRelationScope = this.rosganLotRelationScope(); 27356 const rgRelationsReady = !!rgRelationScope && s.rosganLotRelationsScope === rgRelationScope && !s.rosganLotRelationsLoading; 27357 const rgScopedComitentes = rgRelationsReady ?(s.rosganLotComitentes || []) : []; 27358 const rgComitenteOptions = rgScopedComitentes; 27359 const rgComitente = rgComitenteOptions.find((item) => String(item.id) === String(rgf.comitenteId)); 27360 const rgComitenteSearch = rgRelationsReady ?(s.rosganComitenteSearch || (rgComitente ?String(rgComitente.alias || rgComitente.name || rgComitente.nombre || rgComitente.nombreFiscal || rgComitente.fiscalName || '') : '')) : ''; 27361 const rgComitenteQuery = this.consKey(rgComitenteSearch); 27362 const rgComitenteMatches = rgComitenteOptions.filter((item) => { 27363 if (!rgComitenteQuery) return true; 27364 return this.consKey([item.alias, item.name, item.nombre, item.nombreFiscal, item.fiscalName, item.razonSocial, item.cuitCuil, item.cuit, item.cuitNormalized, item.locality, item.localidad].filter(Boolean).join(' ')).includes(rgComitenteQuery); 27365 }).sort((a,b) => String(a.alias || a.name || '').localeCompare(String(b.alias || b.name || ''), 'es')); 27366 const rgFilteredComitentes = rgComitenteMatches.slice(0, s.rosganComitenteLimit || 60).map((item) => { 27367 const id = String(item.id || item.comitenteId || ''); 27368 const name = item.alias || item.name || item.nombre || item.nombreFiscal || item.fiscalName || item.razonSocial || id; 27369 const meta = [item.cuitCuil || item.cuit || item.cuitNormalized, item.fiscalName || item.nombreFiscal || item.razonSocial].filter(Boolean).join(' · '); 27370 const selected = id === String(rgf.comitenteId); 27371 return { id, name, meta, hasMeta: !!meta, selected, bg: selected ?'#eaf4ff' : '#fff', icon:item.validated ? 'dc-lucide dc-lucide-circle-check' : 'dc-lucide dc-lucide-triangle-alert', color:item.validated ? '#14805e' : '#b56b08', status:item.validated ? 'CUIT validado' : 'Pendiente de validar CUIT', pick: () => this.selectRosganComitente(item) }; 27372 }); 27373 const rgComitenteForm = s.rosganComitenteForm || { name: '', alias: '', cuit: '', fiscalName: '', province: '', locality: '', source: '' }; 27374 const rgComitenteLocalityOptions = (s.geoLocs?.[rgComitenteForm.province] || []).map((locality) => locality?.nombre || locality?.name || locality).filter(Boolean); 27375 if (rgComitenteForm.locality && !rgComitenteLocalityOptions.some((item) => this.consKey(item) === this.consKey(rgComitenteForm.locality))) rgComitenteLocalityOptions.unshift(rgComitenteForm.locality); 27376 const rgComitenteFiscalLocked = !rgComitenteForm.source || !!s.rosganComitenteLookupLoading; 27377 const rgComitenteReady = !!String(rgComitenteForm.alias || '').trim() && !s.rosganComitenteLookupLoading; 27378 const rgComitenteLocalityDisabled = rgComitenteFiscalLocked || !rgComitenteForm.province || !!s.geoLoadingLoc; 27379 const rgAvailableEstablishments = (rgRelationsReady && rgf.comitenteId ?(s.rosganLotEstablishments || []) : []).filter((item) => { 27380 return (!item.consignatariaId || String(item.consignatariaId) === String(rgf.consignatariaId)) && item.isActive !== false && String(item.comitenteId || '') === this.rosganSelectedOperationComitenteId(); 27381 }); 27382 const rgEstablishment = rgAvailableEstablishments.find((item) => String(item.id || item.establishmentId || item.establecimientoId) === String(rgf.establishmentId)); 27383 const rgEstablishmentQuery = this.consKey(s.rosganEstablishmentSearch || ''); 27384 const rgEstablishmentMatches = rgAvailableEstablishments.filter(item => !rgEstablishmentQuery || this.consKey([item.name, item.locality, item.province].filter(
27384Boolean).join(' ')).includes(rgEstablishmentQuery)).sort((a,b) => String(a.name || '').localeCompare(String(b.name || ''), 'es')); 27385 const rgEstablishmentOptions = rgEstablishmentMatches.slice(0, s.rosganEstablishmentLimit || 60).map(item => ({ 27386 id:String(item.id), name:item.name || item.id, selected:String(item.id) === String(rgf.establishmentId), 27387 color:item.validated ? '#14805e' : '#b56b08', background:item.validated ? '#e6f5ed' : '#fff2d8', 27388 icon:item.validated ? 'dc-lucide dc-lucide-circle-check' : 'dc-lucide dc-lucide-triangle-alert', status:item.validated ? 'CUIT validado' : 'Pendiente de validar CUIT', 27389 meta:[item.locality, item.province].filter(Boolean).join(' · '), pick:() => this.selectRosganEstablishment(item.id), 27390 })); 27391 const rgEstablishmentForm = s.rosganEstablishmentForm || this.emptyRosganEstablishmentForm(); 27392 const rgEstablishmentLocalities = (s.geoLocs?.[rgEstablishmentForm.provincia] || []).map((locality) => locality?.nombre || locality?.name || locality).filter(Boolean); 27393 const rgBreedFamilies = this.rosganBreedFamilies(); 27394 const rgBreedIconMap = this.rosganBreedIconMap(); 27395 const rgBreedQuery = this.consKey(s.rosganBreedSearch || ''); 27396 const rgSelectedBreedFamilies = rgBreedFamilies.filter((group) => group.key !== '__crosses__' && group.items.some((item) => rgf.breedIds.includes(String(item.id)))); 27397 const rgBreedCrosses = s.rosganBreedCrosses || {}; 27398 const rgBreedNames = rgSelectedBreedFamilies.map((group) => { 27399 const selectedItems = group.items.filter((item) => rgf.breedIds.includes(String(item.id))); 27400 const blackSelected = (!!group.both && selectedItems.some((item) => String(item.id) === String(group.both.id))) || (!!group.black && selectedItems.some((item) => String(item.id) === String(group.black.id))); 27401 const redSelected = (!!group.both && selectedItems.some((item) => String(item.id) === String(group.both.id))) || (!!group.red && selectedItems.some((item) => String(item.id) === String(group.red.id)));
27402 let selectedName = selectedItems[0]?.name || group.label; 27403 if (group.supportsColors && blackSelected && redSelected) selectedName = group.both?.name || `${group.label} Negro y Colorado`; 27404 return selectedName + (rgBreedCrosses[group.key] ?' y sus cruzas' : ''); 27405 }); 27406 const rgActiveBreedFamily = rgBreedFamilies.find((group) => group.key === s.rosganActiveBreedFamily) || rgSelectedBreedFamilies.find((group) => group.supportsColors) || null; 27407 const rgBreedMatches = rgBreedQuery 27408 ?rgBreedFamilies.filter((group) => group.key !== '__crosses__' && this.consKey([group.label, ...group.items.map((item) => item.name)].join(' ')).includes(rgBreedQuery)) 27409 : []; 27410 const rgBreedSearchFamilies = rgBreedMatches.slice(0, 16); 27411 const rgBreedFamilyView = (group) => { 27412 const ids = group.items.map((item) => String(item.id)); 27413 const selected = ids.some((id) => rgf.breedIds.includes(id)); 27414 const blackSelected = (!!group.both && rgf.breedIds.includes(String(group.both.id))) || (!!group.black && rgf.breedIds.includes(String(group.black.id))); 27415 const redSelected = (!!group.both && rgf.breedIds.includes(String(group.both.id))) || (!!group.red && rgf.breedIds.includes(String(group.red.id))); 27416 return { ...group, selected, blackSelected, redSelected }; 27417 }; 27418 const rgActiveBreedView = rgActiveBreedFamily ?rgBreedFamilyView(rgActiveBreedFamily) : null; 27419 const rgHeads = rgQuantityMode === 'male_female' ?(Number(rgf.maleQuantity) || 0) + (Number(rgf.femaleQuantity) || 0) : rgQuantityMode === 'mothers_calves' ?(Number(rgf.motherQuantity) || 0) + (Number(rgf.calfQuantity) || 0) : (Number(rgf.quantity) || 0); 27420 const rgCompleteGroups = [this.rosganLotStepComplete(0), this.rosganLotStepComplete(1), this.rosganLotStepComplete(2)]; 27421 const rgCompleteness = Math.round((rgCompleteGroups.filter(Boolean).length / rgCompleteGroups.length) * 100); 27422 const rgCreatedLot = s.rosganCreatedLot || null; 27423 const rgCreatedVideoKey = rgCreatedLot?.remateId && rgCreatedLot?.lotCert ?this.rosganLotVideoKey(rgCreatedLot.remateId, rgCreatedLot.lotCert) : ''; 27424 const rgCreatedVideoState = rgCreatedVideoKey ?(s.rosganLotVideoState?.[rgCreatedVideoKey] || { loading: false, videos: [], uploads: [], error: '' }) : { loading: false, videos: [], uploads: [], error: '' }; 27425 const rgCreatedVideoUploadsRaw = Array.isArray(rgCreatedVideoState.uploads) ?rgCreatedVideoState.uploads : []; 27426 const rgCreatedVideoBusy = !!rgCreatedVideoState.uploading; 27427 const rgCreatedHasVideos = !!(rgCreatedVideoState.videos || []).length; 27428 const rgPreviewBrandName = rgConsignataria?.name || rgConsignataria?.nombre || rgConsignataria?.consignatariaNombre || ''; 27429 const rgPreviewLogo = rgPreviewBrandName ? (this.consWhiteLogo(rgPreviewBrandName) || this.consLogo(rgPreviewBrandName) || rgConsignataria?.logoUrl || '') : ''; 27430 const rgPreviewThumbnail = this.rosganActiveLotThumbnail({ videos: rgCreatedVideoState.videos || [] }); 27431 return { 27432 // La restauracion de una sesion existente es silenciosa: el usuario no 27433 // debe volver a ver una pantalla de acceso cada vez que recarga la app. 27434 isAuthRestoring: !!s.routeLoading || (!s.publicAdvanceMode && !s.authReady && !s.authWelcomeRequested), 27435 isWelcome: s.authReady && s.route === 'welcome', 27436 showConnectingScreen: !s.publicAdvanceMode && !s.authReady && !!s.authWelcomeRequested, 27437 showWelcomeScreen: !s.publicAdvanceMode && s.authReady && s.route === 'welcome', 27438 connectingLottieSrc: !s.publicAdvanceMode && !s.authReady && !!s.authWelcomeRequested ?'assets/auth/options/connecting-1.json?v=ceres-celeste-1' : '', 27439 welcomeLottieSrc: !s.publicAdvanceMode && s.authReady && s.route === 'welcome' ?'assets/auth/options/success-1.json' : '', 27440 authTransitioning: !!s.authTransitioning, 27441 advanceGreeting, 27442 showStatistics: s.route === 'mercado' && this.canAccessStatistics(s.apiUser), 27443 launchInterestSubscribed, 27444 launchInterestSaving: !!s.launchInterestSaving, 27445 launchInterestButtonDisabled: !!s.launchInterestLoading || !!s.lau
27445nchInterestSaving || launchInterestSubscribed, 27446 launchInterestButtonClass: 'advance-splash-launch' + (launchInterestSubscribed ?' is-subscribed' : ''), 27447 launchInterestButtonLabel: s.launchInterestLoading ?'Consultandoâ¦' : (s.launchInterestSaving ?'Guardandoâ¦' : (launchInterestSubscribed ?'¡Listo! Te avisaremos' : 'Quiero enterarme del lanzamiento')), 27448 launchInterestHasMessage: !!s.launchInterestMessage, 27449 launchInterestMessage: s.launchInterestMessage || '', 27450 toggleLaunchInterest: () => this.toggleLaunchInterest(), 27451 isLogin: s.authReady && !s.publicAdvanceMode && s.route === 'login' && !s.emailVerificationRequired && !s.phoneVerificationRequired, 27452 isEmailVerification: s.authReady && !s.publicAdvanceMode && !!s.user && !!s.emailVerificationRequired, 27453 isPhoneVerification: s.authReady && !s.publicAdvanceMode && !!s.user && !s.emailVerificationRequired && !!s.phoneVerificationRequired, 27454 isApp: !s.routeLoading && s.authReady && !s.emailVerificationRequired && !s.phoneVerificationRequired && !['login', 'welcome'].includes(s.route), 27455 isDevActivityPage: s.route === 'devActivity' && !!s.user && this.userRoleIds(s.apiUser).includes('dev'), 27456 isHome: s.route === 'home', isPool: s.route === 'pool', isRoslotSummary: s.route === 'roslotSummary', isLot: s.route === 'lot' && !!lot, isLoad: s.route === 'load', isRosganLotLoad: s.route === 'rosganLotLoad', isFeria: s.route === 'feria', isFeriaConfig: s.route === 'feriaConfig' && this.canManageUsers(s.apiUser), isProfile: !!s.user && s.route === 'profile', isProfileArea: !!s.user && ['profile', 'mailingAdmin'].includes(s.route), profileDashboardVisible: !!s.user && s.route === 'profile', profileActivityVisible: !!s.user && s.route === 'profile' && !this.isBasicUserProfile(s.apiUser), isMailingAdmin: s.route === 'mailingAdmin', isNotifications: s.route === 'notifications', isRemates: ['remates', 'rosganRemateCreate'].includes(s.route), isRematesIndex: s.route === 'remates', isRosganRemateCreate: s.route === 'rosganRemateCreate', isRemateDetail: s.route === 'remateDetail', isRosganSalesOrder: s.route === 'rosganSalesOrder' && !!s.currentRemate, isRosganStats:s.route==='rosganStats', isRosganResults:s.route==='rosganResults', resultsPageKey:s.resultsPageKey||'', resultsBackHref:s.resultsBackHref||'/remates', rdResultsHref:'/remates/'+encodeURIComponent(this.remateRouteId(s.currentRemate))+'/resultados'+(s.currentRemate?.__rosganActive?'':'?origen=historico'), rgAudienceId:this.remateRouteId(s.currentRemate), rdResultsKey:(s.currentRemate?.__rosganActive?'actual:':'historico:')+this.remateRouteId(s.currentRemate), 27457 isRosganCommercial: s.route === 'rosganCommercial' && !!s.currentRemate, isRosganPriceBoard: s.route === 'rosganPriceBoard' && !!s.currentRemate, isRosganTvLower: s.route === 'rosganTvLower' && !!s.currentRemate, isRosganTvConfig: s.route === 'rosganTvConfig' && !!s.currentRemate, isCabanas: s.route === 'cabanas' && !s.cabanaPublicDeepLoading, isCabanaPublicLoading: s.authReady && (!!s.cabanaBrandPreparing || (s.publicAdvanceMode === 'lacassina' && !!s.cabanaPublicDeepLoading)), isCabanaDetail: s.route === 'cabanaDetail' && !!s.currentCabana && !s.cabanaPublicDeepLoading && !s.cabanaBrandPreparing, isCabanaPrebidPanel: s.route === 'cabanaPrebidPanel' && !!s.currentCabana, isCabanaLotDetail: s.route === 'cabanaLotDetail' && !!s.currentCabanaLot && !s.cabanaPublicDeepLoading, isCabanaLotCreate: s.route === 'cabanaLotCreate' && !!s.cabanaLotCreateRemate, isCabanaStats: s.route === 'cabanaStats' && (!!s.cabanaStatsOverview || !!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/estadisticas')), isCabanaCommercial: s.route === 'cabanaCommercial' && (!!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/comercial')), isCabanaPriceBoard: s.route === 'cabanaPriceBoard' && (!!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/precios')), isCabanaTvLower: s.route === 'cabanaTvLower' && (!!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/tv')), isRemateConfig: s.route === 'remateConfig', isCabanaSalesOrder: s.route === 'cabanaSalesOrder' && !!s.currentCabana, isChannel: s.route === 'channel', isMercado: s.route === 'mercado', 27458 ...this.roslotDailyReportView(), 27459 ...this.mercadoData(), 27460 ...this.mavData(), 27461 ...this.mapaData(), 27462 isRosganAuditor: s.route === 'rosganAuditor',
27463 rgAuditorRouteBusy: !s.rosganAuditorRouteError, 27464 rgAuditorRouteError: !!s.rosganAuditorRouteError, 27465 rgAuditorRouteMessage: s.rosganAuditorRouteError || 'Cargando panel auditorâ¦', 27466 retryRgAuditor: () => { const target = this.rematePathTarget(); if (target?.action === 'auditor') this.openRosganAuditorById(target.remateId); }, 27467 exitRgAuditor: () => this.go('remates'), 27468 showLightBar: !s.publicAdvanceMode && ['pool', 'roslotSummary', 'lot', 'load', 'rosganLotLoad', 'feria', 'feriaConfig', 'profile', 'mailingAdmin', 'devActivity', 'notifications', 'remates', 'rosganRemateCreate', 'remateDetail', 'rosganAuditor', 'rosganSalesOrder', 'rosganResults', 'rosganStats', 'rosganCommercial', 'rosganPriceBoard', 'rosganTvConfig', 'cabanas', 'cabanaDetail', 'cabanaAuditor', 'cabanaPrebidPanel', 'cabanaLotDetail', 'cabanaLotCreate', 'cabanaStats', 'cabanaCommercial', 'cabanaPriceBoard', 'remateConfig', 'cabanaSalesOrder', 'channel', 'mercado'].includes(s.route), 27469 isMobile: s.isMobile, isDesktop: !s.isMobile, compactNavigation: s.compactNavigation, desktopNavigation: !s.compactNavigation, 27470 notificationsOpen: !s.publicAdvanceMode && !!s.notificationsOpen, 27471 notificationItems, 27472 notificationsLoading: !!s.notificationsLoading, 27473 notificationsHasError: !!s.notificationsError, 27474 notificationsError: s.notificationsError, 27475 notificationsEmpty: !s.notificationsLoading && !s.notificationsError && Array.isArray(s.notificationsItems) && notificationItems.length === 0, 27476 hasUnreadNotifications: unreadNotifications > 0, 27477 notificationsUnreadLabel: unreadNotifications > 99 ?'99+' : String(unreadNotifications), 27478 notificationBellTitle: unreadNotifications ?`${unreadNotifications} notificaciones sin leer` : 'Notificaciones', 27479 notificationBellBg: s.notificationsOpen ?'#dcecff' : '#fff', 27480 toggleNotifications: () => this.toggleNotifications(), 27481 markAllNotificationsRead: () => this.markAllNotificationsRead(), 27482 goNotificationCenter: () => this.goNotificationCenter(), 27483 notificationHistoryItems, 27484 notificationHistoryLoading: !!s.notificationHistoryLoading, 27485 notificationHistoryHasError: !!s.notificationHistoryError, 27486 notificationHistoryError: s.notificationHistoryError, 27487 notificationHistoryEmpty: !s.notificationHistoryLoading && !s.notificationHistoryError && Array.isArray(s.notificationHistory) && notificationHistoryItems.length === 0, 27488 canLoadLot, 27489 canLoadRosganLot: this.canManageRosganLots(s.apiUser), 27490 canCreateEstablishment: this.canManageComitentes(s.apiUser), 27491 manualEstLocked: true, 27492 goRosganLotLoad: () => this.go('rosganLotLoad'), 27493 toggleRematePanelPicker: () => this.showRematePanelPicker(), 27494 rgCanViewPanel: this.canManageRosganLots(), 27495 rgPanelLabel: this.isRosganProfile() ? 'Panel Consignatario' : 'Panel Auditor', 27496 rgPanelDisabled: !rgRemate, 27497 rgPanelOpacity: rgRemate ? 1 : 0.5, 27498 rgPanelHint: rgRemate ? 'Abrir el panel del remate seleccionado' : 'Seleccioná un remate para abrir su panel', 27499 openRgPanel: () => { if (this.canManageRosganLots() && rgRemate) this.openRosganAuditor(rgRemate); }, 27500 rgWizardTitle: s.rosganEditingLot ?'Editar lote Rosgan · ' + s.rosganEditingLot.lotCert : 'Cargar lote Rosgan', rgWizardSubtitle: 'Remates generales · Invernada, Vientres y Faena', 27501 rgEditingLot: !!s.rosganEditingLot, 27502 rgStepKey: 'rg-step-' + rgStep, rgIsStep0: rgStep === 0, rgIsStep1: rgStep === 1, rgIsStep2: rgStep === 2, rgIsStep3: rgStep === 3, 27503 rgWizardSteps: [ 27504 { title: 'Procedencia', text: 'Remate y origen' }, { title: 'CategorÃa', text: 'Raza y cantidad' }, { title: 'Detalles', text: 'Datos del lote' }, { title: 'Videos', text: 'Opcionales' }, 27505 ].map((item, index) => ({ ...item, go: () => { 27506 const canGo = rgCreatedLot ?index === 3 : (index <= rgStep || (index === rgStep + 1 && index < 3 && this.rosganLotStepComplete(rgStep))); 27507 if (canGo) this.setState({ rosganLotStep: index, rosganLotMessage: null }); 27508 }, barBg: index <= rgStep ?'#0a5ca8' : '#dce6f0', dotBg: index < rgStep ?'#1f7a63' : index === rgStep ?'#0a5ca8' : '#fff', dotColor: index <= rgStep ?'#fff' : '#8b9bad', dotBorder: index <= rgStep ?
27508(index < rgStep ?'#1f7a63' : '#0a5ca8') : '#d5e0eb', done: index < rgStep, numberVisible: index >= rgStep, number: String(index + 1), titleColor: index === rgStep ?'#112033' : '#708196' })), 27509 rgPrevVisible: rgStep > 0 && rgStep < 3 ?'visible' : 'hidden', rgPrevStep: () => { if (rgStep < 3) this.setState({ rosganLotStep: Math.max(0, rgStep - 1), rosganLotMessage: null }); }, rgNextStep: () => this.rosganLotNext(), 27510 rgNextLabel: rgStep === 2 ?(s.rosganLotPublishing ?'Guardando loteâ¦' : s.rosganEditingLot ?'Guardar cambios' : 'Cargar lote') : rgStep === 3 ?(rgCreatedVideoBusy ?'Subiendo videosâ¦' : (rgCreatedHasVideos ?'Finalizar carga' : 'Finalizar sin videos')) : 'Continuar', 27511 rgNextShowsArrow: rgStep < 2 && !s.rosganLotPublishing, rgNextBg: rgStep === 2 ?'#e63946' : rgStep === 3 ?'#1f7a63' : '#0a5ca8', rgNextDisabled: !!s.rosganLotPublishing || rgCreatedVideoBusy, 27512 rgCreatedVideoBusy, rgCreatedLotCert: rgCreatedLot?.lotCert || 'no informado', rgCreatedLotCanUpload: !!rgCreatedVideoKey, rgCreatedLotCannotUpload: !!rgCreatedLot && !rgCreatedVideoKey, 27513 uploadRgCreatedLotVideos: (event) => { const files = event.target.files; this.uploadRosganLotVideos(files, rgCreatedLot); event.target.value = ''; }, 27514 prepareCreatedFieldLot: () => this.prepareFieldLot(rgCreatedLot), 27515 rgCreatedVideoLoading: !!rgCreatedVideoState.loading, 27516 rgCreatedVideoHasError: !!rgCreatedVideoState.error, rgCreatedVideoError: rgCreatedVideoState.error || '', 27517 rgCreatedVideoHasUploads: !!rgCreatedVideoUploadsRaw.length, 27518 rgCreatedVideoUploads: rgCreatedVideoUploadsRaw.map((upload) => ({ 27519 name: upload.name || 'Video', progress: Math.max(0, Math.min(100, Number(upload.progress) || 0)), size: this.formatFileSize(upload.sizeBytes), 27520 status: upload.status || 'En espera', color: upload.status === 'Error' ?'#b42332' : upload.status === 'Listo' ?'#16846e' : '#0a5ca8', 27521 bar: upload.status === 'Error' ?'#d94a58' : upload.status === 'Listo' ?'#16846e' : '#0a5ca8', 27522 error: upload.error || '', hasError: !!upload.error, canRetry: !!upload.canRetry, retryLabel: upload.retryLabel, retry: () => this.retryRosganVideoUpload(upload.id, rgCreatedLot), 27523 })), 27524 rgCreatedVideosHasItems: !rgCreatedVideoState.loading && !!(rgCreatedVideoState.videos || []).length, 27525 rgCreatedVideosEmpty: !rgCreatedVideoState.loading && !rgCreatedHasVideos && !rgCreatedVideoBusy, 27526 rgCreatedVideos: (rgCreatedVideoState.videos || []).map((video, index) => { 27527 const id = String(video?.id || video?.videoId || video?.videoID || ''); 27528 const url = video?.url || video?.publicUrl || video?.videoUrl || video?.downloadUrl || video?.signedUrl || ''; 27529 const name = video?.fileName || video?.name || video?.title || ('Video ' + (index + 1)); 27530 return { id, url, hasUrl: !!url, noUrl: !url, name, size: this.formatFileSize(video?.sizeBytes || video?.fileSize || video?.size), deleting: s.rosganLotVideoDeleting === rgCreatedVideoKey + ':' + id, remove: () => this.deleteRosganLotVideo(video, rgCreatedLot) }; 27531 }), 27532 rgLayoutCols: s.isMobile ?'minmax(0,1fr)' : 'minmax(0,1fr) 320px', rgShowPreview: !s.isMobile, 27533 rgPreviewCategory: rgCategory ?rgCategory.name + (rgf.isTermino ?' a Término' : '') : 'CategorÃa pendiente', rgPreviewType: rgLotType ?rgLotType.charAt(0).toUpperCase() + rgLotType.slice(1) : 'Tipo de lote', 27534 rgPreviewRemate: rgRemate?.titulo || rgRemate?.nombre || 'Seleccioná un remate', rgPreviewConsignataria: rgConsignataria?.name || rgConsignataria?.nombre || 'Consignataria pendiente', 27535 rgPreviewBrandColor: rgPreviewBrandName ? this.consBrandColor(rgPreviewBrandName) : '#002e5f', 27536 rgPreviewLogo, rgPreviewHasLogo: !!rgPreviewLogo, 27537 rgPreviewThumbnail, rgPreviewHasThumbnail: !!rgPreviewThumbnail, 27538 rgPreviewImageError: (event) => { event.currentTarget.style.display = 'none'; }, 27539 rgPreviewImageLoaded: (event) => { event.currentTarget.style.display = 'block'; }, 27540 rgPreviewComitente: rgComitente?.alias || rgComitente?.name || rgComitente?.nombre || rgComitente?.nombreFiscal || rgComitente?.fiscalName || 'Comitente pendiente', rgPreviewEstablishment: rgEstablishment?.name || rgEstablishment?.nombre || 'Establecimiento pendiente', 27541 rgPreviewHeads: rgHeads ?String(rgHeads) : 'â', rgPreviewBreeds: rgBreedNames.length ?rgBreedNames.join(' · ') : 'Raza pendiente', rgPreviewWeight: rgf.averageWeight ?rgf.averageWeight + ' kg' : 'â', 27542 rgPreviewCompleteness: rgCompleteness + '%', rgPreviewProgress: rgCompleteness + '%', 27543 rosganLotLoading: !!s.rosganLotLoading, 27544 rosganLotReady: !s.rosganLotLoading && Array.isArray(s.rosganLotRemates) && Array.isArray(s.rosganLotCategories), 27545 rosganLotMessageVisible: !!s.rosganLotMessage, 27546 rosganLotMessage: s.rosganLotMessage?.text || '', 27547 rosganLotMessageBg: s.rosganLotMessage?.ok ?'#e7f5ef' : '#fff0f1', 27548 rosganLotMessageColor: s.rosganLotMessage?.ok ?'#176c58' : '#b42335', 27549 rosganLotRemates: (s.rosganLotRemates || []).map((item) => ({ id: String(item.id), name: item.titulo || item.nombre || item.id, meta: [rgDate(item.fechaHoraRemate), item.localidad].filter(
27549Boolean).join(' · ') })), 27550 rosganLotConsignatarias: rgConsignatariaOptions, 27551 rosganLotComitentes: rgScopedComitentes.map((item) => ({ id: String(item.id), name: item.alias || item.name || item.nombre || item.nombreFiscal || item.fiscalName || item.razonSocial || item.id })), 27552 rosganLotEstablishments: rgAvailableEstablishments.map((item) => ({ id: String(item.id || item.establishmentId || item.establecimientoId), name: item.name || item.nombre || item.id, meta: [item.locality || item.localidad, item.province || item.provincia].filter(Boolean).join(' · ') })), 27553 rgEstablishmentOptions, 27554 rgEstablishmentMenuOpen: !!s.rosganEstablishmentMenuOpen && rgRelationsReady && !!rgf.comitenteId, 27555 rgEstablishmentSearch: s.rosganEstablishmentSearch || '', 27556 rgHasSelectedEstablishment: !!rgEstablishment, 27557 rgEstablishmentButtonLabel: !rgf.comitenteId ?'Elegà un comitente primero' : rgEstablishment?.name || 'Seleccionar establecimientoâ¦', 27558 rgEstablishmentStatusColor: rgEstablishment?.validated ? '#14805e' : '#b56b08', 27559 rgEstablishmentStatusBg: rgEstablishment?.validated ? '#e6f5ed' : '#fff2d8', 27560 rgEstablishmentStatusIcon: rgEstablishment?.validated ? 'dc-lucide dc-lucide-circle-check' : 'dc-lucide dc-lucide-triangle-alert', 27561 rgEstablishmentNoMatches: !rgEstablishmentMatches.length, 27562 rgEstablishmentHasMore: rgEstablishmentOptions.length < rgEstablishmentMatches.length, 27563 rgEstablishmentCountLabel: rgEstablishmentOptions.length + ' de ' + rgEstablishmentMatches.length + ' establecimientos', 27564 toggleRgEstablishmentMenu: () => rgf.comitenteId && rgRelationsReady && this.setState({ rosganEstablishmentMenuOpen:!s.rosganEstablishmentMenuOpen, rosganEstablishmentSearch:'', rosganEstablishmentLimit:60 }), 27565 onRgEstablishmentSearch: event => this.bufferInputState('rosgan-establishment-search', { rosganEstablishmentSearch:event.target.value, rosganEstablishmentLimit:60 }, 100), 27566 moreRgEstablishments: () => this.setState({ rosganEstablishmentLimit:(s.rosganEstablishmentLimit || 60) + 60 }), 27567 blurRgEstablishmentPicker: () => setTimeout(() => { if (!document.activeElement?.closest('.rg-est-picker')) this.setState({ rosganEstablishmentMenuOpen:false }); }, 150), 27568 onRgEstablishmentPickerKey: event => { 27569 if (!rgf.comitenteId || !rgRelationsReady) return; 27570 if (event.key === 'Escape') { event.preventDefault(); this.setState({ rosganEstablishmentMenuOpen:false }); document.querySelector('.rg-est-trigger')?.focus(); } 27571 else if (['ArrowDown','ArrowUp'].includes(event.key)) { event.preventDefault(); const move=()=>{ const choices=[...document.querySelectorAll('[data-rg-est-choice]')]; const current=choices.indexOf(document.activeElement); choices[current < 0 ? (event.key === 'ArrowDown' ? 0 : choices.length - 1) : (current + (event.key === 'ArrowDown' ? 1 : -1) + choices.length) % choices.length]?.focus(); }; if (!s.rosganEstablishmentMenuOpen) this.setState({ rosganEstablishmentMenuOpen:true },move); else move(); } 27572 }, 27573 rosganLotCategories: rgFilteredCategories.map((item) => ({ id: String(item.id), name: item.name })), 27574 rgLotTypeOptions: [ 27575 { value: 'invernada', label: 'Invernada' }, 27576 { value: 'faena', label: 'Faena' }, 27577 { value: 'vientres', label: 'Vientres' }, 27578 ].map((item) => { 27579 const selected = rgLotType === item.value; 27580 return { ...item, selected: selected ?'true' : 'false', className: selected ?'is-selected' : '', bg: selected ?'#0a5ca8' : '#f5f8fb', color: selected ?'#fff' : '#40566d', border: selected ?'#0a5ca8' : '#ccdae8', pick: () => this.selectRosganLotType(item.value) }; 27581 }), 27582 rgCategoryDisabled: !rgLotType, 27583 rgCategoryBg: rgLotType ?'#fff' : '#eef2f6', 27584 rgCategoryPlaceholder: rgLotType ?'Seleccionar categorÃaâ¦' : 'Elegà primero Invernada, Faena o Vientres', 27585 rosganLotBreeds: (s.rosganLotBreeds || []).map((item) => ({ id: String(item.id), name: item.name })), 27586 rosganLotOptionsLoading: !!s.rosganLotOptionsLoading, 27587 rgDescriptionOptions: rgDescriptionOptions.map((option) => { 27588 const selected = rgSelectedDescriptionOptions.some((item) => [option.value,option.label].some(v => this.consKey(item) === this.consKey(v))); 27589 return { ...option, selected: selected ?'true' : 'false', bg: selected ?'#0a5ca8' : '#f5f8fb',
27589color: selected ?'#fff' : '#52667b', border: selected ?'#0a5ca8' : '#c9daea', shadow: selected ?'0 6px 14px rgba(10,92,168,.18)' : 'none', markBg: selected ?'rgba(255,255,255,.2)' : '#e1e8ef', markColor: selected ?'#fff' : '#718397', mark: selected ?'â' : '+', pick: () => this.toggleRosganLotDescription(option.value) }; 27590 }), 27591 rgHasDescriptionOptions: !!rgDescriptionOptions.length, 27592 rgAgeUnitOptions: rgAgeUnitOptions.length ?rgAgeUnitOptions : [{ value: 'meses', label: 'Meses' }, { value: 'años', label: 'Años' }], 27593 rgDentitionOptions, rgHasDentitionOptions: !!rgDentitionOptions.length, rgHasNoDentitionOptions: !rgDentitionOptions.length, 27594 rgReproductiveStatusOptions, rgHasReproductiveStatusOptions: !!rgReproductiveStatusOptions.length, rgHasNoReproductiveStatusOptions: !rgReproductiveStatusOptions.length, 27595 rgDestinationOptions, rgHasDestinationOptions: !!rgDestinationOptions.length, rgHasNoDestinationOptions: !rgDestinationOptions.length, 27596 rgDestinationChoices: rgDestinationOptions.map((o) => ({ ...o, selected: String(rgf.destination) === o.value ? 'true' : 'false', border: String(rgf.destination) === o.value ? '#0a5ca8' : '#ccdae8', bg: String(rgf.destination) === o.value ? '#0a5ca8' : '#fff', color: String(rgf.destination) === o.value ? '#fff' : '#3d4c5f', pick: () => this.setRosganLotField('destination', rgf.destination === o.value ? '' : o.value) })), 27597 rgNotFaena: rgFormType !== 'faena', rgShowConfinement: this.rosganShowsConfinement(), 27598 rgDaysOfConfinement: rgf.daysOfConfinement, onRgDaysOfConfinement: rgInput('daysOfConfinement'), 27599 rgBodyConditionOptions, rgHasBodyConditionOptions: !!rgBodyConditionOptions.length, rgHasNoBodyConditionOptions: !rgBodyConditionOptions.length, 27600 rgAnimalClassOptions, rgHasAnimalClassOptions: !!rgAnimalClassOptions.length, rgHasNoAnimalClassOptions: !rgAnimalClassOptions.length, 27601 rgTickStatusOptions, rgHasTickStatusOptions: !!rgTickStatusOptions.length, rgHasNoTickStatusOptions: !rgTickStatusOptions.length, 27602 rgHealthOptions, rgHasHealthOptions: !!rgHealthOptions.length, rgHasNoHealthOptions: !rgHealthOptions.length, 27603 rgNutritionOptions, rgHasNutritionOptions: !!rgNutritionOptions.length, rgHasNoNutritionOptions: !rgNutritionOptions.length, 27604 rgWeighingOptions, rgHasWeighingOptions: !!rgWeighingOptions.length, rgHasNoWeighingOptions: !rgWeighingOptions.length, 27605 rgServiceOptions, rgHasServiceOptions: !!rgServiceOptions.length, rgHasNoServiceOptions: !rgServiceOptions.length, 27606 rgPregnancyOptions, rgHasPregnancyOptions: !!rgPregnancyOptions.length, rgHasNoPregnancyOptions: !rgPregnancyOptions.length, 27607 rgCalvingOptions, rgHasCalvingOptions: !!rgCalvingOptions.length, rgHasNoCalvingOptions: !rgCalvingOptions.length, 27608 rgSubCategoryOptions, rgHasSubCategoryOptions: !!rgSubCategoryOptions.length, rgHasNoSubCategoryOptions: !rgSubCategoryOptions.length, 27609 rgConditionTypeOptions, rgHasConditionTypeOptions: !!rgConditionTypeOptions.length, rgHasNoConditionTypeOptions: !rgConditionTypeOptions.length, 27610 rgDeliveryTypeOptions, rgHasDeliveryTypeOptions: !!rgDeliveryTypeOptions.length, rgHasNoDeliveryTypeOptions: !rgDeliveryTypeOptions.length, 27611 rgFinishingOptions, rgHasFinishingOptions: !!rgFinishingOptions.length, rgHasNoFinishingOptions: !rgFinishingOptions.length, 27612 rgBreedSearch: s.rosganBreedSearch || '', 27613 onRgBreedSearch: (event) => this.setState({ rosganBreedSearch: event.target.value }), 27614 rgBreedSearchHint: !rgBreedQuery, 27615 rgBreedSearchHasResults: !!rgBreedSearchFamilies.length, 27616 rgBreedNoResults: !!rgBreedQuery && !rgBreedMatches.length, 27617 rgBreedSearchResults: rgBreedSearchFamilies.map((group) => { 27618 const view = rgBreedFamilyView(group); 27619 return { 27620 key: group.key, name: group.label, hasColors: !!group.supportsColors, icon: rgBreedIconMap[group.key] || '', hasIcon: !!rgBreedIconMap[group.key], 27621 pick: () => this.selectRosganBreedFamily(group.key), 27622 bg: view.selected ?'#e7f2ff' : '#f4f7fb', color: view.selected ?'#07589d' : '#334a62', border: view.selected ?'#70aee2' : '#d6e1ec', 27623 }; 27624 }), 27625 rgHasSelectedBreedFamilies: !!rgSelectedBreedFamilies.length, 27626 rgSelectedBreedFamilies: rgSelectedBreedFamilies.map((group) => { 27627 const view = rgBreedFamilyView(group); 27628 return { 27629 key: group.key, name: group.label, hasColors: !!group.supportsColors, icon: rgBreedIconMap[group.key] || '', hasIcon: !!rgBreedIconMap[group.key], 27630 showBlack: !!group.supportsColors && !!view.blackSelected, 27631 showRed: !!group.supportsColors && !!view.redSelected, 27632 blackAvailable: !!group.black, redAvailable: !!group.red, 27633 blackBg: view.blackSelected ?'#16191d' : '#f1f3f5', blackColor: view.blackSelected ?'#fff' : '#1f2933', blackBorder: view.blackSelected ?'#16191d' : '#c8d0d8', 27634 redBg: view.redSelected ?'#a84735' : '#faefec', redColor: view.redSelected ?'#fff' : '#8d3b2d', redBorder: view.redSelected ?'#a84735' : '#dfb5ab', 27635 toggleBlack: () => this.toggleRosganBreedColor(group.key, 'black'), toggleRed: () => this.toggleRosganBreedColor(group.key, 'red'), 27636 crossesSelected: !!rgBreedCrosses[group.key], 27637 crossesBg: rgBreedCrosses[group.key] ?'#07589d' : '#fff', crossesColor: rgBreedCrosses[group.key] ?'#fff' : '#42617f', crossesBorder: rgBreedCrosses[group.key] ?'#07589d' : '#b9cada', 27638 toggleCrosses: () => this.toggleRosganBreedCrosses(group.key), 27639 remove: () => this.removeRosganBreedFamily(group.key), 27640 }; 27641 }), 27642 rgBreedColorVisible: !!rgActiveBreedView?.supportsColors, 27643 rgActiveBreedLabel: rgActiveBreedView?.label || '', 27644 rgBreedBlackAvailable: !!rgActiveBreedView?.black, 27645 rgBreedRedAvailable: !!rgActiveBreedView?.red, 27646 rgBreedBlackBg: rgActiveBreedView?.blackSelected ?'#16191d' : '#f1f3f5', 27647 rgBreedBlackColor: rgActiveBreedView?.blackSelected ?'#fff' : '#1f2933', 27648 rgBreedBlackBorder: rgActiveBreedView?.blackSelected ?'#16191d' : '#c8d0d8', 27649 rgBreedRedBg: rgActiveBreedView?.redSelected ?'#a84735' : '#faefec', 27650 rgBreedRedColor: rgActiveBreedView?.redSelected ?'#fff' : '#8d3b2d', 27651 rgBreedRedBorder: rgActiveBreedView?.redSelected ?'#a84735' : '#dfb5ab', 27652 toggleRgBreedBlack: () => rgActiveBreedView && this.toggleRosganBreedColor(rgActiveBreedView.key, 'black'), 27653 toggleRgBreedRed: () => rgActiveBreedView && this.toggleRosganBreedColor(rgActiveBreedView.key, 'red'), 27654 rgBreedsCount: String(rgSelectedBreedFamilies.length), 27655 rgRemateId: rgf.remateId, rgConsignatariaId: rgf.consignatariaId, rgComitenteId: rgf.comitenteId, rgEstablishmentId: rgf.establishmentId, 27656 rgCertifiedBy: rgf.certifiedBy || '', 27657 rgCertifierDisabled: !rgf.consignatariaId || !!s.rosganCertifiersLoading, 27658 rgCertifierStatus: s.rosganCertifiersError || (!rgf.consignatariaId ?'Seleccioná una consignataria' : s.rosganCertifiersLoading ?'Cargando responsables comercialesâ¦' : !(s.rosganCertifiers || []).length ?'No hay responsables comerciales disponibles' : ''), 27659 rgCertifierOptions, 27660 rgSelectedCertifier: rgCertifierOptions.find(c=>c.selected)||rgCertifierOptions[0], 27661 rgCertifierMenuOpen: !!s.rosganCertifierMenuOpen && !!rgf.consignatariaId && !s.rosganCertifiersLoading, 27662 toggleRgCertifierMenu:()=>{if(rgf.consignatariaId&&!s.rosganCertifiersLoading)this.setState({rosganCertifierMenuOpen:!s.rosganCertifierMenuOpen});}, 27663 onRgCertifierKey:event=>{if(event.key==='Escape')this.setState({rosganCertifierMenuOpen:false});}, 27664 onRgCertifierChange: event => this.setState({ rosganLotForm: { ...this.rosganForm(), certifiedBy: event.target.value || null } }), 27665 rgConsignatariaDisabled: !rgConsignatariaOptions.length || !!s.rosganEditingLot, 27666 rgConsignatariaMenuOpen: !!s.rosganConsignatariaMenuOpen && !!rgConsignatariaOptions.length, 27667 rgConsignatariaMenuExpanded: s.rosganConsignatariaMenuOpen ?'true' : 'false', 27668 rgConsignatariaChevronRotation: s.rosganConsignatariaMenuOpen ?'180deg' : '0deg', 27669 rgSelectedConsignatariaName: rgSelectedConsignataria?.name || 'Seleccionarâ¦', 27670 rgSelectedConsignatariaLogo: rgSelectedConsignataria?.logo || '', 27671 rgSelectedConsignatariaInitials: rgSelectedConsignataria?.initials || 'C', 27672 rgSelectedConsignatariaHasLogo: !!rgSelectedConsignataria?.logo, 27673 rgSelectedConsignatariaShowsInitials: !rgSelectedConsignataria?.logo, 27674 toggleRgConsignatariaMenu: () => this.setState({ rosganConsignatariaMenuOpen: !this.state.rosganConsignatariaMenuOpen }), 27675 rgCanCreateComitente: this.canManageComitentes(s.apiUser), 27676 rgComitenteFormOpen: !!s.rosganComitenteFormOpen && this.canManageComitentes(s.apiUser), 27677 toggleRgComitenteForm: () => { this.toggleRosganComitenteForm(); if(this.state.rosganComitenteFormOpen) requestAnimationFrame(()=>document.getElementById('rg-inline-comitente')?.scrollIntoView({behavior:window.matchMedia('(prefers-reduced-motion: reduce)').matches?'auto':'smooth',block:'nearest'})); }, 27678 openRgLegacyComitentes: () => setTimeout(() => window.RosganLegacyComitentes?.open({ consignatariaId: this.rosganForm().consignatariaId, apiFetch: this.apiFetch.bind(this), onChanged: () => this.loadRosganLotRelations() }), 0), 27679 openRgComitenteMerge: () => this.openComitenteMerge(true), 27680 rgComitenteCreateDisabled: !rgRelationScope, 27681 rgCanEditComitenteAlias: !!rgComitente && this.canManageComitentes(s.apiUser), 27682 editRgComitenteAlias: () => this.editComitenteAlias(rgComitente.id), 27683 rgComitenteSearchDisabled: !rgRelationsReady, 27684 rgComitenteCreateOpacity: '1', 27685 rgComitenteSearch, 27686 onRgComitenteSearch: (event) => this.onRosganComitenteSearch(event.target.value), 27687 openRgComitenteMenu: () => this.openRosganComitenteMenu(), 27688 closeRgComitenteMenu: event => this.closeRosganComitenteMenu(event), 27689 rgComitenteMenuOpen: !!s.rosganComitenteMenuOpen && rgRelationsReady, 27690 rgFilteredComitentes, 27691 rgComitenteHasMore: rgComitenteMatches.length > rgFilteredComitentes.length, 27692 moreRgComitentes: () => this.setState({ rosganComitenteLimit: (s.rosganComitenteLimit || 60) + 60 }), 27693 keepRgComitenteMenu: e => { if (e.button === 0) e.preventDefault(); }, 27694 rgComitenteNoResults: !rgFilteredComitentes.length, 27695 rgComitenteCuit: rgComitenteForm.cuit || '', 27696 rgComitenteAlias: rgComitenteForm.alias || '', onRgComitenteAlias: e => this.setRosganComitenteField('alias', e.target.value), 27697 rgComitenteName: rgComitenteForm.name || '', 27698 rgComitenteFiscalName: rgComitenteForm.fiscalName || '', 27699 rgComitenteProvince: rgComitenteForm.province || '', 27700 rgComitenteLocality: rgComitenteForm.locality || '', 27701 onRgComitenteCuit: (event) => this.onRosganComitenteCuit(event.target.value), 27702 onRgComitenteName: (event) => this.setRosganComitenteField('name', event.target.value, true), 27703 onRgComitenteFiscalName: (event) => this.setRosganComitenteField('fiscalName', event.target.value, true), 27704 onRgComitenteProvince: (event) => this.setRosganComitenteField('province', event.target.value), 27705 onRgComitenteLocality: (event) => this.setRosganComitenteField('locality', event.target.value), 27706 rgComitenteProvinceOptions: s.geoProvinces?.length ?s.geoProvinces : ARGENTINA_PROVINCES, 27707 rgComitenteLocalityOptions, 27708 rgComitenteFiscalLocked, 27709 rgComitenteFiscalBg: rgComitenteFiscalLocked ?'#eef2f6' : '#fff', 27710 rgComitenteLocalityDisabled, 27711 rgComitenteLocalityBg: rgComitenteLocalityDisabled ?'#eef2f6' : '#fff', 27712 rgComitenteLocalityPlaceholder: !rgComitenteForm.province ?'Elegà provincia primero' : (s.geoLoadingLoc ?'Cargando localidadesâ¦' : 'Seleccionarâ¦'), 27713 rgComitenteMessageVisible: !!s.rosganComitenteMessage, 27714 rgComitenteMessage: s.rosganComitenteMessage?.text || '', 27715 rgComitenteMessageBg: (s.rosganComitenteMessage?.ok || s.rosganComitenteMessage?.kind === 'success') ?'#e7f5ef' : (s.rosganComitenteMessage?.kind === 'warning' ?'#fff8e5' : (s.rosganComitenteMessage?.kind === 'info' ?'#eaf4ff' : '#fff0f1')), 27716 rgComitenteMessageColor: (s.rosganComitenteMessage?.ok || s.rosganComitenteMessage?.kind === 'success') ?'#176c58' : (s.rosganComitenteMessage?.kind === 'warning' ?'#8a5a00' : (s.rosganComitenteMessage?.kind === 'info' ?'#07589d' : '#b42335')), 27717 rgComitenteSaving: !!s.rosganComitenteSaving, 27718 rgComitenteLookupLoading: !!s.rosganComitenteLookupLoading, 27719 rgComitenteBusy: !!s.rosganComitenteSaving || !!s.rosganComitenteLookupLoading, 27720 rgComitenteSaveDisabled: !!s.rosganComitenteSaving || !rgComitenteReady, 27721 rgComitenteSaveCursor: (s.rosganComitenteSaving || !rgComitenteReady) ?'not-allowed' : 'pointer', 27722 rgComitenteSaveOpacity: (s.rosganComitenteSaving || !rgComitenteReady) ?'.55' : '1', 27723 rgComitenteSaveLabel: s.rosganComitenteSaving ?'Creandoâ¦' : (s.rosganComitenteLookupLoading ?'Consultandoâ¦' : (!rgComitenteForm.source ?'Esperando validación ARCA' : (!rgComitenteReady ? 'Datos fiscales incompletos' : 'Crear comitente'))), 27724 saveRgComitente: () => this.saveRosganComitente(), 27725 rgCanCreateEstablishment: this.canManageComitentes(s.apiUser), 27726 rgEstablishmentDisabled: !rgRelationsReady || !rgf.comitenteId, 27727 rgEstablishmentPlaceholder: !rgf.consignatariaId ?'Elegà una consignataria primero' : (rgAvailableEstablishments.length ?'Seleccionar establecimientoâ¦' : 'Sin establecimientos para este comitente'), 27728 rgEstablishmentFormOpen: !!s.rosganEstablishmentFormOpen, 27729 toggleRgEstablishmentForm: () =>
27729 { this.toggleRosganEstablishmentForm(); if(this.state.rosganEstablishmentFormOpen) requestAnimationFrame(()=>document.getElementById('rg-inline-establishment')?.scrollIntoView({behavior:window.matchMedia('(prefers-reduced-motion: reduce)').matches?'auto':'smooth',block:'nearest'})); }, 27730 rgEstablishmentName: rgEstablishmentForm.nombre || '', 27731 rgEstablishmentProvince: rgEstablishmentForm.provincia || '', 27732 rgEstablishmentLocality: rgEstablishmentForm.localidad || '', 27733 rgEstablishmentRenspa: rgEstablishmentForm.renspa || '', 27734 onRgEstablishmentName: (event) => this.setRosganEstablishmentField('nombre', event.target.value), 27735 onRgEstablishmentProvince: (event) => this.setRosganEstablishmentField('provincia', event.target.value), 27736 onRgEstablishmentLocality: (event) => this.onRosganEstPlaceInput(event.target.value), 27737 onRgEstablishmentRenspa: (event) => this.setRosganEstablishmentField('renspa', event.target.value), 27738 rgEstablishmentProvinceOptions: s.geoProvinces?.length ?s.geoProvinces : ARGENTINA_PROVINCES, 27739 rgEstablishmentLocalityOptions: [], 27740 rgEstablishmentLocalityDisabled: !rgEstablishmentForm.provincia, 27741 rgEstablishmentLocalityBg: !rgEstablishmentForm.provincia ?'#eef2f6' : '#fff', 27742 rgEstablishmentLocalityPlaceholder: !rgEstablishmentForm.provincia ?'Elegà provincia primero' : 'Escribà una localidad', 27743 rgEstablishmentPlaceLoading: !!s.rosganEstPlaceLoading, 27744 rgEstablishmentPlacesOpen: !!(s.rosganEstPlaceResults || []).length, 27745 rgEstablishmentPlaces: (s.rosganEstPlaceResults || []).map((item) => ({ label: item.label || 'Localidad', address: item.address || '', pick: () => this.selectRosganEstPlace(item) })), 27746 openRgEstablishmentPlaces: () => { if (rgEstablishmentForm.provincia && String(rgEstablishmentForm.localidad || '').trim().length >= 3) this.searchRosganEstPlaces(rgEstablishmentForm.localidad, rgEstablishmentForm.provincia); }, 27747 closeRgEstablishmentPlaces: () => this.closeRosganEstPlaceResults(), 27748 rgEstablishmentPlaceError: s.rosganEstPlaceError || '', 27749 rgEstablishmentMessageVisible: !!s.rosganEstablishmentMessage, 27750 rgEstablishmentMessage: s.rosganEstablishmentMessage?.text || '', 27751 rgEstablishmentMessageBg: s.rosganEstablishmentMessage?.ok ?'#e7f5ef' : '#fff0f1', 27752 rgEstablishmentMessageColor: s.rosganEstablishmentMessage?.ok ?'#176c58' : '#b42335', 27753 rgEstablishmentSaveDisabled: !!s.rosganEstablishmentSaving, 27754 rgEstablishmentSaveLabel: s.rosganEstablishmentSaving ?'Creandoâ¦' : 'Crear establecimiento', 27755 saveRgEstablishment: () => this.saveRosganEstablishment(), 27756 rgCategoryId: rgf.categoryId, rgBreedIds: rgf.breedIds, rgRelationsLoading: !!s.rosganLotRelationsLoading, 27757 onRgRemate: (event) => this.onRosganRemateChange(event.target.value), 27758 onRgConsignataria: (event) => this.onRosganConsignatariaChange(event.target.value), 27759 onRgEstablishment: (event) => this.selectRosganEstablishment(event.target.value), onRgCategory: (event) => this.selectRosganLotCategory(event.target.value), 27760 onRgBreeds: (event) => this.setRosganLotField('breedIds', Array.from(event.target.selectedOptions || []).map((option) => option.value)), 27761 rgIsTotalQuantity: !['male_female', 'mothers_calves'].includes(rgQuantityMode), rgIsMaleFemale: rgQuantityMode === 'male_female', rgIsMothersCalves: rgQuantityMode === 'mothers_calves', 27762 rgQuantity: rgf.quantity, rgMaleQuantity: rgf.maleQuantity, rgFemaleQuantity: rgf.femaleQuantity, rgMotherQuantity: rgf.motherQuantity, rgCalfQuantity: rgf.calfQuantity, 27763 onRgQuantity: rgInput('quantity'), onRgMaleQuantity: rgInput('maleQuantity'), onRgFemaleQuantity: rgInput('femaleQuantity'), onRgMotherQuantity: rgInput('motherQuantity'), onRgCalfQuantity: rgInput('calfQuantity'), 27764 rgPaymentDays: ['30', '60', '90', '120', 'other'].map(day => ({ label: day === 'other' ? 'Otro' : day + ' dÃas', checked: (rgf.paymentTermDays || []).includes(day), toggle: () => { const selected = this.rosganForm().paymentTermDays || []; this.setRosganLotField('paymentTermDays', selected.includes(day) ? selected.filter(value => value !== day) : [...selected, day]); } })), 27765 rgOtherPaymentDay: (rgf.paymentTermDays || []).includes('other'), rgOtherPaymentDayValue: rgf.otherPaymentTermDay || '', onRgOtherPaymentDay: rgInput('otherPaymentTermDay'), 27766 rgPaymentReference: rgf.paymentTermReference || '', onRgPaymentReference: rgInput('paymentTermReference'), 27767 rgOtherPaymentReference: rgf.paymentTermReference === 'other', rgOtherPaymentReferenceValue: rgf.otherPaymentTermReference || '', onRgOtherPaymentReference: rgInput('otherPaymentTermReference'), 27768 rgPreviewPaymentTerm: window.RosganPaymentTerm.format(rgf) || 'Plazo pendiente', 27769 rgDescription: rgf.description, onRgDescription: event => this.setRosganDescription(event), 27770 rgAllowsTermino: !!rgCategory?.allowsTermino && ['inv-terneros', 'inv-terneras', 'inv-terneros-as'].includes(rgCategory?.id), rgAllowsAge: !!rgCategory?.allowsAge, rgAllowsDentition: !!rgCategory?.allowsDentition, 27771 rgAllowsReproductiveStatus: !!rgCategory?.allowsReproductiveStatus, rgDestinationLabel: rgFormType === 'faena' ? 'DESTINO *' : 'DESTINO', rgAllowsDestination: rgFormType === 'faena' || !!rgCategory?.allowsDestination, rgAllowsAptoChina: !!rgCategory?.allowsAptoChina, 27772 rgIsTermino: !!rgf.isTermino, rgIsAptoChina: !!rgf.isAptoChina, rgHasMioMio: !!rgf.hasMioMio, rgHasLiquidBrand: !!rgf.hasLiquidBrand, rgIsAptoVisec: !!rgf.isAptoVisec, 27773 rgTerminoPressed: rgf.isTermino ?'true' : 'false', rgTerminoBg: rgf.isTermino ?'#0a5ca8' : '#eef2f5', rgTerminoColor: rgf.isTermino ?'#fff' : '#52606d', rgTerminoBorder: rgf.isTermino ?'#084d8c' : '#d5dde5', rgTerminoShadow: rgf.isTermino ?'0 7px 17px rgba(10,92,168,.22)' : 'none', rgTerminoIconFilter: rgf.isTermino ?'brightness(0) invert(1)' : 'none', rgTerminoIconOpacity: rgf.isTermino ?'1' : '.68', rgTerminoCheckBg: rgf.isTermino ?'rgba(255,255,255,.2)' : '#d8dfe6', rgTerminoCheckColor: rgf.isTermino ?'#fff' : '#8b98a5', rgTerminoCheck: rgf.isTermino ?'â' : '', 27774 rgChinaPressed: rgf.isAptoChina ?'true' : 'false', rgChinaBg: rgf.isAptoChina ?'#d71920' : '#eef2f5', rgChinaColor: rgf.isAptoChina ?'#fff' : '#52606d', rgChinaBorder: rgf.isAptoChina ?'#b81218' : '#d5dde5', rgChinaShadow: rgf.isAptoChina ?'0 7px 17px rgba(215,25,32,.22)' : 'none', rgChinaFlagFilter: rgf.isAptoChina ?'none' : 'grayscale(1)', rgChinaFlagOpacity: rgf.isAptoChina ?'1' : '.55', rgChinaCheckBg: rgf.isAptoChina ?'rgba(255,255,255,.2)' : '#d8dfe6', rgChinaCheckColor: rgf.isAptoChina ?'#fff' : '#8b98a5', rgChinaCheck: rgf.isAptoChina ?'â' : '', 27775 rgMioValue: typeof rgf.hasMioMio === 'boolean' ? (rgf.hasMioMio ? 'yes' : 'no') : '', 27776 onRgMio: (event) => this.setRosganLotField('hasMioMio', event.target.value === '' ? null : event.target.value === 'yes'), 27777 rgMioPressed: rgf.hasMioMio ?'true' : 'false', rgMioBg: rgf.hasMioMio ?'#a84412' : '#eef2f5', rgMioColor: rgf.hasMioMio ?'#fff' : '#52606d', rgMioBorder: rgf.hasMioMio ?'#84350e' : '#d5dde5', rgMioShadow: rgf.hasMioMio ?'0 7px 17px rgba(168,68,18,.22)' : 'none', rgMioIconFilter: rgf.hasMioMio ?'brightness(0) invert(1)' : 'none', rgMioIconOpacity: rgf.hasMioMio ?'1' : '.72', rgMioCheckBg: rgf.hasMioMio ?'rgba(255,255,255,.2)' : '#d8dfe6', rgMioCheckColor: rgf.hasMioMio ?'#fff' : '#8b98a5', rgMioCheck: rgf.hasMioMio ?'â' : '', 27778 rgLiquidPressed: rgf.hasLiquidBrand ?'true' : 'false', rgLiquidBg: rgf.hasLiquidBrand ?'#8a4b24' : '#eef2f5', rgLiquidColor: rgf.hasLiquidBrand ?'#fff' : '#52606d', rgLiquidBorder: rgf.hasLiquidBrand ?'#6f3919' : '#d5dde5', rgLiquidShadow: rgf.hasLiquidBrand ?'0 7px 17px rgba(138,75,36,.22)' : 'none', rgLiquidIconFilter: rgf.hasLiquidBrand ?'brightness(0) invert(1)' : 'none', rgLiquidIconOpacity: rgf.hasLiquidBrand ?'1' : '.68', rgLiquidCheckBg: rgf.hasLiquidBrand ?'rgba(255,255,255,.2)' : '#d8dfe6', rgLiquidCheckColor: rgf.hasLiquidBrand ?'#fff' : '#8b98a5', rgLiquidCheck: rgf.hasLiquidBrand ?'â' : '', 27779 rgVisecPressed: rgf.isAptoVisec ?'true' : 'false', rgVisecBg: rgf.isAptoVisec ?'#f7faef' : '#eef2f5', rgVisecColor: rgf.isAptoVisec ?'#2b404c' : '#52606d', rgVisecBorder: rgf.isAptoVisec ?'#90b436' : '#d5dde5', rgVisecShadow: rgf.isAptoVisec ?'0 7px 17px rgba(144,180,54,.24)' : 'none', rgVisecIconFilter: rgf.isAptoVisec ?'none' : 'grayscale(1)', rgVisecIconOpacity: rgf.isAptoVisec ?'1' : '.55', rgVisecCheckBg: rgf.isAptoVisec ?'#90b436' : '#d8dfe6', rgVisecCheckColor: rgf.isAptoVisec ?'#fff' : '#8b98a5', rgVisecCheck: rgf.isAptoVisec ?'â' : '', 27780 toggleRgTermino: rgToggle('isTermino'), toggleRgChina: rgToggle('isAptoChina'), toggleRgMio: rgToggle('hasMioMio'), toggleRgLiquid: rgToggle('hasLiquidBrand'), toggleRgVisec: rgToggle('isAptoVisec'), 27781 rgAgeValue: rgf.ageValue, rgAgeUnit: rgf.ageUnit, rgDentition: rgf.dentition, rgReproductiveStatus: rgf.reproductiveStatus, rgDestination: rgf.destination, 27782 onRgAgeValue: rgInput('ageValue'), onRgAgeUnit: rgInput('ageUnit'), onRgDentition: rgInput('dentition'), onRgReproductiveStatus: rgInput('reproductiveStatus'), onRgDestination: rgInput('destination'), 27783 rgBodyCondition: rgf.bodyCondition, rgAnimalClass: rgf.animalClass, rgTickStatus: rgf.tickStatus, rgHealth: rgf.health, rgNutrition: rgf.nutrition, 27784 onRgBodyCondition: rgInput('bodyCondition'), onRgAnimalClass: rgInput('animalClass'), onRgTickStatus: rgInput('tickStatus'), onRgHealth: rgInput('health'), onRgNutrition: rgInput('nutrition'), 27785 rgIsInvernada: rgFormType === 'invernada', rgIsVientres: rgFormType === 'vientres', rgIsFaena: rgFormType === 'faena', rgHasCategory: !!rgCategory, 27786 rgAverageWeight: rgf.averageWeight, rgMaxWeight: rgf.maxWeight, rgMinWeight: rgf.minWeight, rgWeighing: rgf.weighing, rgDesbaste: rgf.desbaste, 27787 onRgAverageWeight: rgInput('averageWeight'), onRgMaxWeight: rgInput('maxWeight'), onRgMinWeight: rgInput('minWeight'), onRgWeighing: rgInput('weighing'), onRgDesbaste: rgInput('desbaste'), 27788 rgService: rgf.service, rgPregnancy: rgf.pregnancy, rgCalving: rgf.calving, onRgService: rgInput('service'), onRgPregn
27788ancy: rgInput('pregnancy'), onRgCalving: rgInput('calving'), 27789 rgSubCategory: rgf.subCategory, rgConditionType: rgf.conditionType, rgDeliveryType: rgf.deliveryType, rgFinishing: rgf.finishing, rgEstimatedYield: rgf.estimatedYield, 27790 onRgSubCategory: rgInput('subCategory'), onRgConditionType: rgInput('conditionType'), onRgDeliveryType: rgInput('deliveryType'), onRgFinishing: rgInput('finishing'), onRgEstimatedYield: rgInput('estimatedYield'), 27791 rosganLotPublishing: !!s.rosganLotPublishing, rosganLotPublishLabel: s.rosganLotPublishing ?'Cargando loteâ¦' : 'Cargar lote', 27792 rosganLotPublishOpacity: s.rosganLotPublishing || rgCreatedVideoBusy ?'.58' : '1', 27793 publishRosganLot: () => this.publishRosganLot(), resetRosganLot: () => this.resetRosganLotForm(), 27794 showCreateRemate: this.canCreateCabanaRemate(s.apiUser), 27795 showCabanaStatsNavigation: this.canSeeCabanaStatsNavigation(), 27796 createCabanaRemate: () => this.openRemateConfig('Remate Cabañas'), 27797 openAllCbStats: () => this.openCabanaStatsIndex(), 27798 hasRemateCatalog, 27799 remateCatalogName: rc.catalogoPdfName || 'Sin catálogo seleccionado', 27800 remateCatalogMeta: hasRemateCatalog ?`${remateCatalogSizeMb.toFixed(remateCatalogSizeMb >= 10 ?0 : 1)} MB · PDF` : 'Archivo PDF de hasta 30 MB', 27801 remateCatalogAction: hasRemateCatalog ?'Reemplazar PDF' : 'Seleccionar PDF', 27802 onRemateCatalogFile: (e) => this.readRemateCatalog(e.target.files?.[0]), 27803 removeRemateCatalog: () => this.removeRemateCatalog(), 27804 rcCatalogPrivate: !!rcToggles.requerirRegistro, 27805 rcToggleCatalogPrivate: () => this.toggleRemateConfig('requerirRegistro'), 27806 rcCatalogAccessLabel: rcToggles.requerirRegistro ? 'Bajo registro · Visitas identificadas' : 'Público · Usuarios e invitados', 27807 rcCatalogSwitchBg: rcToggles.requerirRegistro ? '#0a5ca8' : '#bccbd9', rcCatalogSwitchOffset: rcToggles.requerirRegistro ? '22px' : '0px', 27808 rcHasSavedRemate: !!s.remateConfigEditingId, rcDetailsMode: !s.rcSchemaEditor || !s.remateConfigEditingId, rcSchemaMode: !!s.rcSchemaEditor && !!s.remateConfigEditingId, 27809 rcDetailsTabBg: s.rcSchemaEditor ?'#fff':'#0a5ca8', rcDetailsTabColor: s.rcSchemaEditor ?'#12304d':'#fff', rcSchemaTabBg: s.rcSchemaEditor ?'#0a5ca8':'#fff', rcSchemaTabColor: s.rcSchemaEditor ?'#fff':'#12304d', 27810 rcShowDetails: () => this.setState({rcSchemaEditor:false}), rcShowSchema: () => { if(s.remateConfigEditingId) this.setState({rcSchemaEditor:true}); }, rcSaveSchema: () => this.saveRemateConfig(this.state.remateConfigStatus || 'borrador'), 27811 rcPlacesExpanded: String(!!s.rcPlaceOpen && !!s.rcPlaceResults?.length), rcPlacesVisible: !!s.rcPlaceOpen && !!s.rcPlaceResults?.length, 27812 rcPlaceActiveId: s.rcPlaceIndex >= 0 ?'rc-place-'+s.rcPlaceIndex:'', rcPlaces: (s.rcPlaceResults || []).map((item,index)=>({...item,id:'rc-place-'+index,source:item.sourceLabel || (item.source === 'google'?'Google Places':'Localidad argentina'),selected:String(index === s.rcPlaceIndex),bg:index === s.rcPlaceIndex?'#edf5fc':'#fff',pickPointer:event=>{event.preventDefault();this.selectCabanaRematePlace(item);},pick:()=>this.selectCabanaRematePlace(item)})), 27813 rcLocality: rc.localidad || '', rcLocalityDisabled: !rc.provincia, rcLocalityPlaceholder: rc.provincia ?'Escribà la localidad':'Elegà una provincia primero', 27814 setRcLocality: event => this.onCabanaRematePlaceInput(event.target.value), rcPlaceKey: event => this.cabanaRematePlaceKey(event), rcPlaceMouseDown: event => event.preventDefault(), 27815 rcPlaceFocus: () => { clearTimeout(this._rcPlaceBlur); this.setState({rcPlaceOpen:true}); }, rcPlaceBlur: () => { this._rcPlaceBlur=setTimeout(()=>this.setState({rcPlaceOpen:false}),150); }, 27816 rcPlaceStatus: s.rcPlaceLoading ?'Buscando localidadesâ¦':s.rcPlaceError || 'Buscá por nombre (mÃnimo 3 letras).', 27817 rcApproversDisabled: !!rcToggles.apruebaAuto, 27818 rcApproversError: s.adminUsersError || (s.adminUsersLoading ? 'Cargando aprobadoresâ¦' : (!rc.consignataria ? 'Seleccioná una consignataria.' : (!this.cabanaEligibleApprovers(rc).length ? 'No hay usuarios habilitados asociados a esta consignataria.' : ''))), 27819 rcApprovers: this.cabanaEligibleApprovers(rc).map(u =>
27819 ({uid:u.uid,label:[u.displayName,u.email].filter(Boolean).join(' · '),checked:(rc.approverUserIds || []).includes(u.uid),toggle:() => {const ids=new Set(rc.approverUserIds || []);if(ids.has(u.uid))ids.delete(u.uid);else ids.add(u.uid);this.setRemateConfigField('approverUserIds',[...ids]);}})), 27820 rcPricing: (rc.formulariosCategoria || []).filter(c => c.categoria).map(c => {const price=(rc.categoryPricing || []).find(p => p.categoryId === c.categoryId) || {}; const set=(key,event) => this.setCabanaCategoryPrice(c.categoryId,key,event.target.value);return {label:c.categoria,disabled:!c.categoryId,base:price.baseAmount ?? '',increment:price.incrementAmount ?? '',onBase:e=>set('baseAmount',e),onIncrement:e=>set('incrementAmount',e)};}), 27821 remateConfigSections, remateConfigOptions, preofferFields, cabanaParticipantGroups, remateCategoryForms, addRemateCategoryForm: () => this.addRemateCategoryForm(), 27822 preofferEnabled: !!rcToggles.preHabilitada, 27823 preofferLabel: rcToggles.preHabilitada ?'Preoferta habilitada' : 'Habilitar preoferta', 27824 preofferBg: rcToggles.preHabilitada ?'#e2f2ee' : '#fff', preofferColor: rcToggles.preHabilitada ?'#1f7a63' : '#65758a', preofferBorder: rcToggles.preHabilitada ?'#a9d7ca' : '#cfe0f5', preofferDot: rcToggles.preHabilitada ?'#1f7a63' : '#9fb0c3', 27825 togglePreoffer: () => this.toggleRemateConfig('preHabilitada'), 27826 saveRemateDraft: () => this.saveRemateConfig('borrador'), publishRemateConfig: () => this.saveRemateConfig('abierto'), resetRemateConfig: () => this.resetRemateConfig(), closeRemateConfig: () => { this.resetCabanaRematePlaces(); this.go('cabanas'); }, 27827 remateConfigTitle: s.rcSchemaEditor && s.remateConfigEditingId ?'Formulario de lotes' : s.remateConfigEditingId ?'Editar remate de cabaña' : 'Crear remate de cabaña', 27828 remateConfigSubtitle: s.rcSchemaEditor && s.remateConfigEditingId ?'Configurá las categorÃas y los campos que se pedirán al cargar lotes.' : s.remateConfigEditingId ?'Actualizá los datos, participantes y configuración del remate.' : 'Definà el remate, sus cabañas participantes, la carga y la preoferta.', 27829 remateConfigBusy: !!s.remateConfigSaving, 27830 remateConfigDraftLabel: s.remateConfigSaving === 'draft' ?'Guardandoâ¦' : (s.remateConfigSaved ?'Borrador guardado' : 'Guardar borrador'), 27831 remateConfigPublishLabel: s.remateConfigSaving === 'publish' ?'Publicandoâ¦' : (s.remateConfigEditingId ?'Publicar cambios' : 'Publicar remate'), 27832 remateConfigActionOpacity: s.remateConfigSaving ?0.58 : 1, 27833 remateConfigSaveBg: s.remateConfigSaved ?'#1f7a63' : '#e63946', 27834 showDesktopLoad: !s.publicAdvanceMode && !s.compactNavigation && (canLoadLot || this.canManageRosganLots(s.apiUser)) && !['remateConfig', 'cabanaLotCreate', 'rosganLotLoad', 'rosganRemateCreate'].includes(s.route), 27835 showLoadFab: !s.publicAdvanceMode && s.isMobile && (canLoadLot || this.canManageRosganLots(s.apiUser)) && !['load', 'rosganLotLoad', 'rosganRemateCreate', 'login', 'profile', 'mailingAdmin', 'remateConfig', 'cabanaLotCreate', 'remates', 'cabanaLotDetail'].includes(s.route), 27836 isRegister: authMode === 'register', isSignIn: authMode === 'signin', showPassword: authMode !== 'reset', 27837 showAccountAssistantLauncher: !s.accountAssistantOpen && authMode !== 'reset', 27838 showAuthNameField: authMode === 'register' && (!accountAssistantEmailActive || accountAssistantStep >= 1), 27839 showAuthPhoneField: authMode === 'register' && (!accountAssistantEmailActive || accountAssistantStep >= 2), 27840 showAuthEmailField: !(s.accountAssistantOpen && authMode === 'signin') && (!accountAssistantEmailActive || accountAssistantStep >= 3), 27841 showAuthPasswordField: !(s.accountAssistantOpen && authMode === 'signin') && (!accountAssistantEmailActive || accountAssistantStep >= 4), 27842 showPrimaryAuthSubmit: !(s.accountAssistantOpen && accountAssistantMethod === 'email'), 27843 showDefaultGoogleAuth: authMode === 'signin' && !s.accountAssistantOpen, 27844 showAuthFooterLinks: !s.accountAssistantOpen, 27845 accountAssistantOpen: !!s.accountAssistantOpen, 27846 accountAssistantTitle: accountAssistantContent?.title || 'Asistente de cuenta', 27847 accountAssistantText: accountAssistantContent?.text || '', 27848 accountAssistantTip: accountAssistantContent?.tip || '', 27849 accountAssistantHasTip: !!accountAssistantContent?.tip, 27850 accountAssistantMessage: s.accountAssistantMessage || '', 27851 accountAssistantIsChoice: !!s.accountAssistantOpen && !accountAssistantMethod, 27852 accountAssistantIsGoogle: !!s.accountAssistantOpen && accountAssistantMethod === 'google', 27853 accountAssistantIsEmail: !!s.accountAssistantOpen && accountAssistantMethod === 'email', 27854 accountAssistantHasProgress: !!s.accountAssistantOpen && accountAssistantMethod === 'email', 27855 accountAssistantProgressLabel: accountAssistantMethod === 'email' ?`Paso ${accountAssistantStep} de 5` : '', 27856 accountAssistantProgress: accountAssistantMethod === 'email' ?`${accountAssistantStep * 20}%` : '0%', 27857 accountAssistantBackLabel: accountAssistantStep <= 1 ?'Elegir otra opción' : 'Atrás', 27858 accountAssistantNextLabel: accountAssistantStep >= 5 ?(s.authBusy ?'Creandoâ¦' : 'Crear mi cuenta') : 'Siguiente', 27859 accountAssistantNextDisabled: !!s.authBusy, 27860 openAccountAssistant: () => this.openAccountAssistant(), 27861 closeAccountAssistant: () => this.closeAccountAssistant(), 27862 chooseAccountAssistantGoogle: () => this.chooseAccountAssistantGoogle(), 27863 chooseAccountAssistantEmail: () => this.chooseAccountAssistantEmail(), 27864 backAccountAssistant: () => this.backAccountAssistant(), 27865 nextAccountAssistant: () => this.nextAccountAssistant(), 27866 authNameClass: 'rosgan-auth-input' + (s.accountAssistantOpen && accountAssistantMethod === 'email' && accountAssistantStep === 1 ?' rosgan-auth-field-assisted' : ''), 27867 authPhoneClass: 'rosgan-auth-input' + (s.accountAssistantOpen && accountAssistantMethod === 'email' && accountAssistantStep === 2 ?' rosgan-auth-field-assisted' : ''), 27868 authEmailClass: 'rosgan-auth-input' + (s.accountAssistantOpen && accountAssistantMethod === 'email' && accountAssistantStep === 3 ?' rosgan-auth-field-assisted' : ''), 27869 authPasswordClass: 'rosgan-auth-input' + (s.accountAssistantOpen && accountAssistantMethod === 'email' && accountAssistantStep === 4 ?' rosgan-auth-field-assisted' : ''), 27870 authSubmitClass: s.accountAssistantOpen && accountAssistantMethod === 'email' && accountAssistantStep === 5 ?'rosgan-auth-field-assisted' : '', 27871 loginTitle: authMode === 'register' ?'Crear cuenta' : authMode === 'reset' ?'Recuperar contraseña' : 'Bienvenido a Rosgan', 27872 loginSubtitle: authMode === 'register' ?'Completá tus datos y tu teléfono de contacto.' : authMode === 'reset' ?'Te enviaremos un enlace de recupero.' : 'Ingresá para continuar a tu cuenta.', 27873 submitLabel: s.authBusy ?'Procesandoâ¦' : (authMode === 'register' ?'Crear cuenta' : authMode === 'reset' ?'Enviar enlace' : 'Ingresar'), 27874 authBusy: !!s.authBusy, authBtnOpacity: s.authBusy ?0.6 : 1, 27875 resetLink: authMode === 'reset' ?'Volver a ingresar' : 'Olvidé mi contraseña', 27876 registerLink: authMode === 'register' ?'¿Ya tengo cuenta?' : 'Crear cuenta', 27877 toggleReset: () => this.setState({ authMode: authMode === 'reset' ?'signin' : 'reset', authMessage: '', authPasswordVisible: false }), 27878 toggleRegister: () => this.setState({ authMode: authMode === 'register' ?'signin' : 'register', authMessage: '', authPasswordVisible: false }), 27879 formName: s.formName, formEmail: s.formEmail, formPassword: s.formPassword, formPhone: s.formPhone, 27880 authPasswordVisible: !!s.authPasswordVisible, 27881 passwordInputType: s.authPasswordVisible ?'text' : 'password', 27882 passwordVisibilityLabel: s.authPasswordVisible ?'Ocultar contraseña' : 'Mostrar c
27882ontraseña', 27883 toggleAuthPasswordVisibility: () => this.setState({ authPasswordVisible: !s.authPasswordVisible }), 27884 passwordAutocomplete: authMode === 'register' ?'new-password' : 'current-password', 27885 passwordLengthValid: s.formPassword.length >= 8, 27886 passwordLengthColor: s.formPassword.length >= 8 ?'#baffdf' : '#d6e4f2', 27887 passwordLengthBorder: s.formPassword.length >= 8 ?'#72e7b3' : 'rgba(255,255,255,.32)', 27888 passwordLengthBg: s.formPassword.length >= 8 ?'rgba(22,184,133,.34)' : 'rgba(255,255,255,.07)', 27889 passwordLengthIconColor: s.formPassword.length >= 8 ?'#ffffff' : '#b8cadb', 27890 passwordUpperValid: /[A-ZÃÃÃÃÃÃÃ]/.test(s.formPassword), 27891 passwordUpperColor: /[A-ZÃÃÃÃÃÃÃ]/.test(s.formPassword) ?'#baffdf' : '#d6e4f2', 27892 passwordUpperBorder: /[A-ZÃÃÃÃÃÃÃ]/.test(s.formPassword) ?'#72e7b3' : 'rgba(255,255,255,.32)', 27893 passwordUpperBg: /[A-ZÃÃÃÃÃÃÃ]/.test(s.formPassword) ?'rgba(22,184,133,.34)' : 'rgba(255,255,255,.07)', 27894 passwordUpperIconColor: /[A-ZÃÃÃÃÃÃÃ]/.test(s.formPassword) ?'#ffffff' : '#b8cadb', 27895 onName: (e) => this.bufferInputState('auth-name', { formName: e.target.value }), 27896 onEmail: (e) => this.bufferInputState('auth-email', { formEmail: e.target.value }), 27897 onPassword: (e) => this.bufferInputState('auth-password', { formPassword: e.target.value }), 27898 onFormPhone: (e) => this.bufferInputState('auth-phone', { formPhone: e.target.value }), 27899 submitAuthForm: (event) => { 27900 event.preventDefault(); 27901 if (!s.authBusy) this.submitAuth(); 27902 }, 27903 submitAuthOnEnter: (event) => { 27904 if (event.key !== 'Enter' || event.isComposing || event.nativeEvent?.isComposing) return; 27905 event.preventDefault(); 27906 if (!s.authBusy) this.submitAuth(); 27907 }, 27908 submitAuth: () => this.submitAuth(), googleAuth: () => this.googleAuth(), 27909 authMessage: s.authMessage, 27910 emailVerificationEmail: s.user?.email || s.formEmail, 27911 emailVerificationBusy: !!s.emailVerificationBusy, 27912 emailVerificationMessage: s.emailVerificationMessage, 27913 emailVerificationOpacity: s.emailVerificationBusy ?0.58 : 1, 27914 emailVerificationCheckLabel: s.emailVerificationBusy ?'Comprobandoâ¦' : 'Ya verifiqué mi correo', 27915 emailVerificationResendLabel: s.emailVerificationBusy ?'Procesandoâ¦' : 'Reenviar correo', 27916 checkEmailVerification: () => this.checkEmailVerification(), 27917 resendEmailVerification: () => this.resendEmailVerification(), 27918 logoutFromEmailVerification: () => this.logoutFromEmailVerification(), 27919 isPhoneEntryStep: s.phoneVerificationStep !== 'code', 27920 isPhoneCodeStep: s.phoneVerificationStep === 'code', 27921 phoneVerificationNumber: s.phoneVerificationNumber, 27922 phoneVerificationCode: s.phoneVerificationCode, 27923 phoneVerificationBusy: !!s.phoneVerificationBusy, 27924 phoneVerificationMessage: s.phoneVerificationMessage, 27925 phoneVerificationLocalhost: ['localhost', '127.0.0.1', '::1'].includes(window.location.hostname), 27926 phoneVerificationMasked: this.maskedPhoneNumber(s.phoneVerificationNumber), 27927 phoneVerificationOpacity: s.phoneVerificationBusy ?0.58 : 1, 27928 phoneVerificationSendLabel: s.phoneVerificationBusy ?'Guardandoâ¦' : 'Guardar y continuar', 27929 phoneVerificationVerifyLabel: s.phoneVerificationBusy ?'Verificandoâ¦' : 'Verificar y continuar', 27930 onPhoneVerificationNumber: (e) => this.setState({ phoneVerificationNumber: e.target.value, phoneVerificationMessage: '' }), 27931 onPhoneVerificationCode: (e) => this.setState({ phoneVerificationCode: String(e.target.value || '').replace(/\D/g, '').slice(0, 6), phoneVerificationMessage: '' }), 27932 savePhoneAndContinue: () => this.savePhoneAndContinue(), 27933 changePhoneNumber: () => this.changePhoneNumber(), 27934 logoutFromPhoneVerification: () => this.logoutFromPhoneVerification(), 27935 logout: () => { 27936 this._sessionEpoch = (this._sessionEpoch || 0) + 1; 27937 this._pathRequest = (this._pathRequest || 0) + 1; 27938 this._initialPathname = '/perfil'; 27939 this._initialPathRoute = 'profile'; 27940 this._initialSearch = ''; 27941 this.writeAppHistory('/perfil', { replace: true }); 27942 try { sessionStorage.removeItem('rosgan:lastRoute'); } catch {} 27943 this.clearWelcomeAfterSignIn(); 27944 this.clearCachedApiUser(); 27945 if (this._welcomeTimer) { clearTimeout(this._welcomeTimer); this._welcomeTimer = null; } 27946 this._phoneConfirmationResult = null; 27947 this._phoneVerificationId = null; 27948 this.clearPhoneRecaptcha(); 27949 this.signOutSession(); 27950 this.setState({ user: null, route: 'login', authReady: true, authMode: 'signin', emailVerificationRequired: false, emailVerificationBusy: false, emailVerificationMessage: '', phoneVerificationRequired: false, phoneVerificationStep: 'phone', phoneVerificationCode: '', phoneVerificationBusy: false, phoneVerificationMessage: '', notificationsItems: null, notificationsOpen: false, lau
27950nchInterest: null, launchInterestLoading: false, launchInterestSaving: false, launchInterestMessage: '' }); 27951 }, 27952 userInitials: (currentProfileName || s.user?.displayName || s.user?.email || '?').split(/[\s@.]+/).slice(0, 2).map((w) => w[0]?.toUpperCase() || '').join(''), 27953 userHasConsignatariaAvatar, 27954 userShowsInitials: !userHasConsignatariaAvatar, 27955 userConsignatariaAvatar, 27956 userConsignatariaName: userConsignatariaMatch?.name || userConsignatariaMatch?.nombre || s.userConsignataria?.name || userConsignatariaRef, 27957 userAvatarBg: userHasConsignatariaAvatar ?currentAvatar.bg : '#0a5ca8', 27958 userAvatarBorder: userHasConsignatariaAvatar ?'rgba(255,255,255,0.72)' : '#8fb6dc', 27959 userAvatarNavSize: userHasProfilePhoto ?'100%' : '27px', 27960 userAvatarHeroSize: userHasProfilePhoto ?'100%' : '52px', 27961 userAvatarObjectFit: userHasProfilePhoto ?'cover' : 'contain', 27962 profilePhotoViewerOpen: !s.publicAdvanceMode && !!s.profilePhotoViewerOpen && userHasConsignatariaAvatar, 27963 profilePhotoViewerUrl: userConsignatariaAvatar, 27964 profilePhotoViewerAlt: currentProfileName || userConsignatariaMatch?.name || userConsignatariaMatch?.nombre || 'Foto de perfil', 27965 profileAvatarCursor: userHasConsignatariaAvatar ?'zoom-in' : 'default', 27966 openProfilePhotoViewer: (event) => this.openProfilePhotoViewer(event), 27967 closeProfilePhotoViewer: () => this.closeProfilePhotoViewer(), 27968 parallaxY: (anim ?Math.round(s.parallax * 0.35) : 0) + 'px', 27969 logoH: s.isMobile ?'22px' : '30px', 27970 homePadBottom: s.compactNavigation ?'calc(90px + env(safe-area-inset-bottom))' : '20px', 27971 heroPadBottom: s.isMobile ?'64px' : '72px', 27972 accessPull: s.isMobile ?'12px' : '-58px', 27973 homeHeroLoading: s.monthRemates === null, 27974 homeHeroReady: s.monthRemates !== null, 27975 homeIndicesLoading: !Array.isArray(s.indices), 27976 homeIndicesReady: Array.isArray(s.indices) && homeIndexCards.length > 0, 27977 homeIndicesEmpty: Array.isArray(s.indices) && homeIndexCards.length === 0, 27978 homeMonthLoading: s.monthRemates === null, 27979 homeAgendaLoading: s.ferias === null, 27980 homeAgendaReady: s.ferias !== null, 27981 homeVideosLoading: s.videos === null, 27982 homeCafeVideosLoading: s.cafeVideos === null, 27983 homeLoteNewsLoading: false, // The local interactive edition is available before the PDF archive arrives. 27984 homeNewsLoading: s.noticias == null, 27985 ...(s.route === 'home' ? this.homeChannelView() : {}), 27986 homeChannelsLoading: s.consignatarias === null, 27987 homeChannelsReady: s.consignatarias !== null, 27988 homeIndexSkeletons: Array.from({ length: 3 }, (_, i) => ({ key: 'hi' + i })), 27989 homeAgendaSkeletons: Array.from({ length: 3 }, (_, i) => ({ key: 'ha' + i })), 27990 homeSectionSkeletons: Array.from({ length: 4 }, (_, i) => ({ key: 'hs' + i })), 27991 homeChannelSkeletons: Array.from({ length: 6 }, (_, i) => ({ key: 'hc' + i })), 27992 ...(() => { 27993 const monthAll = this.homeFeaturedRemates(s.monthRemates, s.rosganRemates).filter(it => !it.__feriaAuction && this.remateType(it)[0] !== 'Feria').map((it) => ({ it, mode: this.histMode(it), heroImage: this.histThumb(it) })); 27994 const cabanaSlides = (s.cabanas || []) 27995 .filter(it => !this.isExcludedCabana(it) && it.activo !== false && it.closeRemate !== true && this.cabanaMode(it) !== 'recent') 27996 .map(cabana => ({ cabana, it: { ...cabana, nombre_remate: cabana.nombre_remate || cabana.title || cabana.titulo }, mode: this.cabanaMode(cabana), heroImage: this.cabanaMedia(cabana) || 'assets/angus-field.png' })); 27997 const cabanaTitles = new Set(cabanaSlides.map(({ it }) => this.consKey(it.nombre_remate))); 27998 const slideDate = slide => slide.cabana ?this.cabanaAuctionStartAt(slide.cabana) : this.remateDate(slide.it); 27999 const slidesRaw = [{ congress: true }, ...[...monthAll.filter(({ it }) => !cabanaTitles.has(this.consKey(it.nombre_remate))), ...cabanaSlides].sort((a, b) => (slideDate(a) || Infinity) - (slideDate(b) || Infinity))]; 28000 this._homeHeroRosganIndices = slidesRaw.flatMap(({it, congress, cabana}, i) => { 28001 if (congress || cabana) return []; 28002 const ref = this.remateConsignatariaRef(it) || 'Rosgan'; 28003 const name = this.consRecord(ref)?.name || this.consRecord(ref)?.nombre || it.nombre_cuenta || it.consignatariaNombre || ''; 28004 return this.consKey(ref) === 'rosgan' || this.consKey(name) === 'rosgan' ?[i]:[]; 28005 }); 28006 this._heroCount = slidesRaw.length || 1; 28007 const idx = slidesRaw.length ?(s.heroSlide % slidesRaw.length + slidesRaw.length) % slidesRaw.length : 0; 28008 const active = slidesRaw[idx]; 28009 if (active?.congress) return { 28010 heroPrebidsUpcoming:false, heroPrebidsActive:false, heroIsCongress:true, heroIsAuction:false, heroCongressClass:'home-congress', heroSurface:'#6c86fe', 28011 heroHasSlides:slidesRaw.length>1, heroTextKey:'congress-2026', 28012 heroDots:slidesRaw.map((slide,i)=>({go:()=>this.selectHomeHero(i),label:slide.congress?'Congreso Federal Ganadero':slide.it.nombre_remate || 'Remate',active:i===idx,w:i===id
28012x?'28px':'10px',bg:i===idx?'#060f52':'#ffffff99'})), 28013 }; 28014 28015 if (active) { 28016 const it = active.it; const live = active.mode === 'live'; 28017 const cabana = active.cabana; 28018 const consRef = (cabana ?cabana.consignatariaId || cabana.consignataria || cabana.nombre_cuenta : this.remateConsignatariaRef(it)) || 'Rosgan'; 28019 const brandColor = this.consBrandColor(consRef); 28020 const consignatariaHeroLogo = this.consFullHeroLogo(consRef); 28021 const heroConsName = this.consRecord(consRef)?.name || this.consRecord(consRef)?.nombre || it.nombre_cuenta || it.consignatariaNombre || ''; 28022 const isRosganHero = !cabana && (this.consKey(consRef) === 'rosgan' || this.consKey(heroConsName) === 'rosgan'); 28023 const heroConsLogo = isRosganHero ? 'assets/rosgan-logo-blue.png' : consignatariaHeroLogo; 28024 const anniversaryImage = cabana ?'' : this.rosganAnniversaryImage(it); 28025 const prebidWindow = window.CabanaPrebid.windowState(cabana || it); 28026 return { 28027 heroPrebidsActive:prebidWindow.active, heroPrebidsUpcoming:prebidWindow.upcoming, heroPrebidStart:prebidWindow.start || 0, heroPrebidStartLabel:prebidWindow.upcoming ?new Date(prebidWindow.start).toLocaleString('es-AR', {timeZone:'America/Argentina/Buenos_Aires',weekday:'long',day:'numeric',month:'long',hour:'2-digit',minute:'2-digit',hourCycle:'h23'}) + ' h (Argentina)' : '', heroIsCongress:false, heroIsAuction:true, heroCongressClass:'', heroSurface:'#002e5f', 28028 heroHasSlides: slidesRaw.length > 1, heroTextKey: 'h' + idx, 28029 heroImage: anniversaryImage || (isRosganHero ?this.rosganBanner(Math.max(0,s.homeRosganBannerIndex || 0)).src : (cabana ?this.consBackground(consRef) || this.consBackground(heroConsName) : '') || active.heroImage), 28030 heroImagePosition: anniversaryImage ?'center 48%' : isRosganHero ?this.rosganBanner(Math.max(0,s.homeRosganBannerIndex || 0)).position : this.consBackgroundPosition(consRef, 'banner'), 28031 heroImageMobileHeight: (this.consKey(consRef).includes('ildarraz') || this.consKey(heroConsName).includes('ildarraz')) ?'72%' : '100%', 28032 heroImageMobilePosition: anniversaryImage ?'center 48%' : isRosganHero ?this.rosganBanner(Math.max(0,s.homeRosganBannerIndex || 0)).position : (this.consKey(consRef).includes('ildarraz') || this.consKey(heroConsName).includes('ildarraz')) ?'85% center' : (this.consRecord(consRef)?.backgroundMobilePosition || this.consRecord(consRef)?.bannerMobilePosition || this.consBackgroundPosition(consRef, 'card')), 28033 heroOverlay: 'linear-gradient(90deg, rgba(0,0,0,.76) 0%, rgba(0,0,0,.58) 42%, rgba(0,0,0,.18) 100%)', 28034 heroConsLogo, heroHasConsLogo: !!heroConsLogo, heroConsName: isRosganHero ? 'Rosgan' : heroConsName, heroConsLogoFilter: isRosganHero ? 'brightness(0) invert(1)' : this.consFullHeroLogoFilter(consRef), 28035 heroBadge: live ?'EN VIVO' : cabana ?'REMATE DE CABAÃA' : 'PRÃXIMO REMATE', heroBadgeBg: live ?'rgba(230,57,70,0.94)' : 'rgba(255,255,255,0.17)', 28036 heroTitle: it.nombre_remate || 'Rosgan Streaming', 28037 heroSubtitle: cabana ?[this.cabanaAuctionStartAt(cabana)?.toLocaleString('es-AR', { timeZone:'America/Argentina/Buenos_Aires', weekday:'long', day:'numeric', month:'long', hour:'2-digit', minute:'2-digit', hourCycle:'h23' }), this.cabanaWeatherLocation(cabana)].filter(Boolean).join(' · ') : (this.remateDate(it) ?this.remateDate(it).toLocaleDateString('es-AR', { weekday: 'long', day: 'numeric', month: 'long' }) + (it.hora_remate ?' · ' + String(it.hora_remate).slice(0, 5) + ' h' : '') : 'Transmisión oficial') + ' · ' + (it.ubicacion_remate || it.nombre_cuenta || 'Rosgan'), 28038 heroHref: cabana ?(cabana.youtube?.watchUrl || (cabana.youtube?.videoId ?'https://www.youtube.com/watch?v=' + cabana.youtube.videoId : this.cabanaPath(cabana))) : this.histUrl(it), 28039 heroOpenLots: () => cabana ?this.openCabana(cabana) : it.__rosganActive ?this.openRosganActiveRemate(it) : this.openRemate(it), 28040 heroStats: [{ value: '$' + fmt(3412 * p), label: 'PROMEDIO/KG' }, { value: fmt(18240 * p), label: 'CABEZAS' }], 28041 heroDots: slidesRaw.map((_, i) => ({ go: () =>
28041 this.selectHomeHero(i), w: i === idx ?'26px' : '6px', bg: i === idx ?'#fff' : 'rgba(255,255,255,0.4)' })), 28042 }; 28043 } 28044 return { 28045 heroPrebidsUpcoming:false, heroPrebidsActive:false, heroHasSlides: false, heroTextKey: 'h0', heroImage: 'assets/angus-field.png', 28046 heroImagePosition: 'center', 28047 heroOverlay: 'linear-gradient(90deg, rgba(0,24,52,.96) 0%, rgba(0,35,72,.84) 48%, rgba(0,24,52,.24) 100%)', 28048 heroConsLogo: '', heroHasConsLogo: false, heroConsName: '', heroConsLogoFilter: 'none', 28049 heroBadge: 'AGENDA ROSGAN', heroBadgeBg: 'rgba(10,92,168,0.9)', 28050 heroTitle: 'El próximo encuentro, en Rosgan', 28051 heroSubtitle: 'Consultá los remates y las transmisiones de nuestra red.', 28052 heroHref: 'https://www.youtube.com/@rosgantv', 28053 heroOpenLots: () => this.go('remates'), 28054 heroStats: [{ value: '$' + fmt(3412 * p), label: 'PROMEDIO/KG' }, { value: fmt(18240 * p), label: 'CABEZAS' }], 28055 heroDots: [], 28056 }; 28057 })(), 28058 goHome: () => this.go('home'), goPool: () => this.go('pool'), goRemates: () => this.go('remates'), 28059 heroPrevious: () => this.moveHomeHero(-1), heroNext: () => this.moveHomeHero(1), 28060 heroPointerDown: (event) => this.startHomeHeroGesture(event), 28061 heroPointerUp: (event) => this.endHomeHeroGesture(event), 28062 heroPointerCancel: (event) => this.endHomeHeroGesture(event, true), 28063 heroKeyDown: (event) => { if (event.target !== event.currentTarget) return; if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { event.preventDefault(); this.moveHomeHero(event.key === 'ArrowLeft' ?-1 : 1); } }, 28064 ...(() => { 28065 const c = s.selectedCons; 28066 const name = c ?this.consName(c) : 'Canal'; 28067 const logo = c ?(this.consLogo(name) || c.logoUrl || c.logo || c.imageUrl) : ''; 28068 const channelBackground = c ?(c.backgroundUrl || c.bannerUrl || this.consBackground(name)) : ''; 28069 const col = this.consColor(name); 28070 const list = (s.consRemates || []).map((it) => ({ it, mode: this.histMode(it) })) 28071 .sort((a, b) => { const o = { live: 0, upcoming: 1, recent: 2 }; const md = o[a.mode] - o[b.mode]; if (md) return md; return a.mode === 'recent' ?(this.remateDate(b.it) || 0) - (this.remateDate(a.it) || 0) : (this.remateDate(a.it) || 0) - (this.remateDate(b.it) || 0); }); 28072 const nLive = list.filter((x) => x.mode === 'live').length; 28073 const nUp = list.filter((x) => x.mode === 'upcoming').length; 28074 const nRec = list.filter((x) => x.mode === 'recent').length; 28075 const tab = s.channelTab; 28076 const feat = list.find((x) => x.mode === 'live') || list.find((x) => x.mode === 'upcoming') || list[0]; 28077 const shown = tab === 'todos' ?list : list.filter((x) => (tab === 'vivo' ?x.mode === 'live' : tab === 'proximo' ?x.mode === 'upcoming' : x.mode === 'recent')); 28078 const grid = shown.filter((x) => x !== feat); 28079 const badgeFor = (m) => m === 'live' ?{ badge: 'EN VIVO', badgeBg: '#e63946', live: true } : m === 'upcoming' ?{ badge: 'PRÃXIMO', badgeBg: col, live: false } : { badge: 'FINALIZADO', badgeBg: 'rgba(0,29,61,0.72)', live: false }; 28080 const fmtD = (it) => this.remateDate(it) ?this.remateDate(it).toLocaleDateString('es-AR', { weekday: 'short', day: 'numeric', month: 'short' }) + (it.hora_remate ?' · ' + String(it.hora_remate).slice(0, 5) + ' h' : '') : 'Fecha a confirmar'; 28081 const tabs = [ 28082 { key: 'todos', label: 'Todos', n: list.length }, { key: 'vivo', label: 'En vivo', n: nLive, live: true }, 28083 { key: 'proximo', label: 'Próximos', n: nUp }, { key: 'grabado', label: 'Grabados', n: nRec }, 28084 ]; 28085 // ---- asociación de videos de YouTube al canal ---- 28086 // cada remate de la consignataria trae su id de YouTube; con eso filtramos el feed del canal 28087 const ckey = this.consKey(name); 28088 const remYt = (it) => it.youtubeApiId || it.youtubeVideoId || (it.youtube && it.youtube.id) || this.ytId(it.url_streaming || ''); 28089 const consYtIds = new Set(list.map((x) =>
28089 remYt(x.it)).filter(Boolean).map(String)); 28090 const vidId = (v) => String(v.id || v.videoId || this.ytId(v.url || '') || ''); 28091 const vidMatch = (v) => { if (consYtIds.has(vidId(v))) return true; const tk = this.consKey(v.title); return !!(tk && ckey && tk.includes(ckey)); }; 28092 const chVids = (s.videos || []).filter(vidMatch).filter((v, i, a) => a.findIndex((w) => vidId(w) === vidId(v)) === i); 28093 const channelOriginalThumb = (it) => { 28094 const id = String(remYt(it) || ''); 28095 const matched = id ?chVids.find((v) => vidId(v) === id) : null; 28096 return matched?.thumbnailUrl || matched?.thumbnail || this.youtubeOriginalThumb(it); 28097 }; 28098 // ---- videoteca por año (remates grabados con video) ---- 28099 const recents = list.filter((x) => x.mode === 'recent'); 28100 const yearOf = (x) => { const d = this.remateDate(x.it); return d ?d.getFullYear() : null; }; 28101 const libYears = [...new Set(recents.map(yearOf).filter(Boolean))].sort((a, b) => b - a); 28102 const curYear = (s.channelYear && libYears.includes(s.channelYear)) ?s.channelYear : (libYears[0] || null); 28103 const libItems = recents.filter((x) => yearOf(x) === curYear).sort((a, b) => (this.remateDate(b.it) || 0) - (this.remateDate(a.it) || 0)); 28104 return { 28105 chName: name, chInitial: name.trim()[0].toUpperCase(), 28106 chLogo: logo, chHasLogo: !!logo, chNoLogo: !logo, 28107 chColor: col, chGradA: this.shade(col, 0.28), chGradB: this.shade(col, -0.45), chGlow: this.shade(col, 0.5), 28108 chHeroOverlay: 'linear-gradient(125deg, ' + this.rgba(this.shade(col, -0.45), 0.93) + ' 0%, ' + this.rgba(col, 0.8) + ' 52%, ' + this.rgba(this.shade(col, 0.28), 0.6) + ' 100%)', 28109 chHeroImages: (() => { const imgs = channelBackground ?[channelBackground] : this.channelHeroImages; const hi = imgs.length ?((s.chHeroSlide % imgs.length) + imgs.length) % imgs.length : 0; return imgs.map((src, i) => ({ src, opacity: i === hi ?1 : 0, position: channelBackground ?this.consBackgroundPosition(name, 'banner') : 'center' })); })(), 28110 chHasHeroDots: !channelBackground && this.channelHeroImages.length > 1, 28111 chHeroDots: (() => { const imgs = channelBackground ?[channelBackground] : this.channelHeroImages; const hi = imgs.length ?((s.chHeroSlide % imgs.length) + imgs.length) % imgs.length : 0; return imgs.map((_, i) => ({ go: () => this.setState({ chHeroSlide: i }), w: i === hi ?'22px' : '6px', bg: i === hi ?'#fff' : 'rgba(255,255,255,0.45)' })); })(), 28112 chLoading: s.consRematesLoading && !s.consRemates, 28113 chEmpty: !!s.consRemates && list.length === 0 && !s.consRematesLoading, 28114 chHasVideos: chVids.length > 0, 28115 chHasLibrary: libYears.length > 0, 28116 chLibCount: libItems.length + (libItems.length === 1 ?' grabación' : ' grabaciones'), 28117 chYearTabs: libYears.map((y) => ({ 28118 label: String(y), go: () => this.setState({ channelYear: y }), 28119 bg: y === curYear ?col : 'transparent', color: y === curYear ?'#fff' : '#4a5c72', 28120 border: y === curYear ?col : '#d5e0ec', weight: y === curYear ?800 : 600, 28121 })), 28122 chLibrary: libItems.map(({ it }) => { const t = this.remateType(it); return { 28123 title: it.nombre_remate || 'Remate ' + name, thumb: channelOriginalThumb(it), thumbPosition: 'center', 28124 date: this.remateDate(it) ?this.remateDate(it).toLocaleDateString('es-AR', { day: 'numeric', month: 'long' }) : '', 28125 type: t[0].toUpperCase(), typeColor: t[1], open: () => this.openRemate(it), 28126 }; }), 28127 chVideos: chVids.map((v) => { const id = vidId(v); const live = String(v.liveBroadcastContent || v.status || '') === 'live' || v.isLive; return { 28128 title: v.title || 'Video del canal', 28129 href: v.url || (id ?'https://www.youtube.com/watch?v=' + id : this.histUrl({})), 28130 thumb: v.thumbnailUrl || v.thumbnail || (id ?'https://i.ytimg.com/vi/' + id + '/hqdefault.jpg' : 'assets/feria.png'), 28131 meta: v.publishedAt ?new Date(v.publishedAt).toLocaleDateString('es-AR', { day: 'numeric', month: 'long', year: 'numeric' }) : name, 28132 live: !!live, 28133 }; }), 28134 chStats: [ 28135 { value: String(list.length), label: 'REMATES' }, 28136 { value: String(nLive + nUp), label: 'AGENDADOS' }, 28137 { value: String(nRec), label: 'GRABADOS' }, 28138 ], 28139 chTabs: tabs.map((t) => ({ 28140 label: t.label, count: t.n, live: !!t.live, go: () =>
28140 this.setState({ channelTab: t.key }), 28141 bg: t.key === tab ?col : 'rgba(255,255,255,0.75)', color: t.key === tab ?'#fff' : '#33465c', 28142 border: t.key === tab ?col : '#e2eaf3', 28143 })), 28144 chHasFeatured: !!feat, 28145 chFeat: feat ? (() => { const it = feat.it; const b = badgeFor(feat.mode); const t = this.remateType(it); return { 28146 title: it.nombre_remate || 'Remate ' + name, thumb: channelOriginalThumb(it), thumbPosition: 'center', date: fmtD(it), 28147 loc: it.ubicacion_remate || [it.localidad, it.provincia].filter(Boolean).join(', ') || '', 28148 badge: b.badge, badgeBg: b.badgeBg, live: b.live, type: t[0].toUpperCase(), 28149 open: () => this.openRemate(it), 28150 }; })() : {}, 28151 chHasCards: grid.length > 0, 28152 chCards: grid.map(({ it, mode }) => { const b = badgeFor(mode); const t = this.remateType(it); return { 28153 title: it.nombre_remate || 'Remate ' + name, thumb: channelOriginalThumb(it), thumbPosition: 'center', date: fmtD(it), 28154 badge: b.badge, badgeBg: b.badgeBg, live: b.live, type: t[0].toUpperCase(), typeColor: t[1], 28155 open: () => this.openRemate(it), 28156 }; }), 28157 }; 28158 })(), 28159 ...(() => {
28160 const month = this.homeFeaturedRemates(s.monthRemates, s.rosganRemates).map((it) => ({ it, mode: this.histMode(it) })) 28161 .sort((a, b) => this.remateDate(a.it) - this.remateDate(b.it)); 28162 const badgeFor = (m) => m === 'live' ?{ badge: 'EN VIVO', badgeBg: '#e63946', badgeColor: '#fff', live: true } : m === 'upcoming' ?{ badge: 'PRÃXIMO', badgeBg: '#0a5ca8', badgeColor: '#fff', live: false } : { badge: 'FINALIZADO', badgeBg: 'rgba(0,29,61,0.82)', badgeColor: '#fff', live: false }; 28163 const cafePlaylistId = 'PLAa_PiRkH0KOCs4VtYFA7NpUeuKs_rt4f'; 28164 const cafeVids = (s.cafeVideos || []).slice(0, 10); 28165 const real = month.length; 28166 const loop = real > 1; 28167 this._monthReal = real; this._monthLoop = loop; 28168 const cs = Math.min(s.carSlide, Math.max(real - 1, 0)); 28169 let rosganCardIndex = 0; 28170 const cards = month.map(({ it, mode }) => { 28171 const consRef = this.remateConsignatariaRef(it) || 'Rosgan'; 28172 const consName = this.consDisplayName(this.consRecord(consRef)?.name || this.consRecord(consRef)?.nombre || it.nombre_cuenta || it.nombre_tipo || 'Rosgan'); 28173 const isRosganCard = this.consKey(consRef) === 'rosgan' || this.consKey(consName) === 'rosgan'; 28174 const anniversaryImage = this.rosganAnniversaryImage(it); 28175 const rosganCardBanner = isRosganCard && !anniversaryImage ?this.rosganBanner(rosganCardIndex++):null; 28176 const whiteConsLogo = this.consWhiteLogo(consRef); 28177 const consLogo = whiteConsLogo || this.consLogo(consRef); 28178 const brandColor = this.consBrandColor(consRef); 28179 return { 28180 cta: it.__feriaAuction && !it.url_streaming ?'Transmisión a confirmar' : 'Ver transmisión', 28181 title: it.nombre_remate || 'Remate Rosgan', select:(event)=>this.selectHomeChannel(it,event), href: this.histUrl(it), thumb: anniversaryImage || (rosganCardBanner ?rosganCardBanner.src : this.histThumb(it)), 28182 day: this.remateDate(it)?.toLocaleDateString('es-AR', { day: '2-digit', timeZone: 'America/Argentina/Buenos_Aires' }), 28183 month: this.remateDate(it)?.toLocaleDateString('es-AR', { month: 'short', timeZone: 'America/Argentina/Buenos_Aires' }), 28184 time: it.hora_remate ?String(it.hora_remate).slice(0, 5) + ' h' : 'Horario a confirmar', 28185 location: it.ubicacion_remate || 'Transmisión online', 28186 cons: consName, consLogo, hasConsLogo: !!consLogo, consLogoFilter: whiteConsLogo ?'none' : 'brightness(0) invert(1)', brandColor, thumbPosition: anniversaryImage ?'center 48%' : rosganCardBanner ?rosganCardBanner.position : this.consBackgroundPosition(consRef, 'card'), 28187 thumbOverlay: this.rgba(this.shade(brandColor, -0.06), 0.82), 28188 date: this.remateDate(it) ?this.remateDate(it).toLocaleDateString('es-AR', { day: 'numeric', month: 'short' }) + (it.hora_remate ?' · ' + String(it.hora_remate).slice(0, 5) : '') : '', 28189 ...badgeFor(mode), badge: mode === 'live' ?'EN VIVO' : this.homeAgendaDay(this.remateDate(it)) === this.homeAgendaDay(new Date()) ?'HOY' : 'PRÃXIMO', 28190 }; 28191 }); 28192 return { 28193 hasMonthCarousel: real > 0, 28194 monthHasDots: real > 1, 28195 monthDots: month.map((_, i) => ({ 28196 go: () => { const el = document.querySelector('[data-drag-scroll]'); const card = el?.querySelector('[data-drag-item]'); const step = card ?card.offsetWidth + 16 : 300; const base = loop ?real * step : 0; el?.scrollTo({ left: base + i * step, behavior: 'smooth' }); this._carPause = true; setTimeout(() => { this._carPause = false; }, 6000); }, 28197 w: i === cs ?'26px' : '6px', bg: i === cs ?'#002e5f' : '#cfdbe9', 28198 })), 28199 monthCarousel: loop ?[...cards, ...cards, ...cards] : cards, 28200 hasCafeVideos: cafeVids.length > 0, 28201 hasCafePlaylistFallback: s.cafeVideos !== null && cafeVids.length === 0, 28202 cafePlaylistHref: 'https://www.youtube.com/playlist?list=' + cafePlaylistId, 28203 cafePlaylistEmbed: 'https://www.youtube-nocookie.com/embed/videoseries?list=' + cafePlaylistId, 28204 cafePlaylistGrid: s.isMobile ?'1fr' : 'minmax(0, 1.5fr) minmax(250px, .7fr)', 28205 cafeCards: cafeVids.map((v) => { 28206 const vid = v.id || v.videoId || this.ytId(v.url); 28207 return { title: v.title || 'Café Ganadero', select:(event)=>this.selectHomeChannel(v,event), href: v.url || (vid ?'https://www.youtube.com/watch?v=' + vid : '#'), 28208 thumb: v.thumbnailUrl || (vid ?'https://i.ytimg.com/vi/' + vid + '/hqdefault.jpg' : 'assets/feria.png'), 28209 meta: v.publishedAt ?new Date(v.publishedAt).toLocaleDateString('es-AR', { day: 'numeric', month: 'long', year: 'numeric' }) : 'Café Ganadero' }; 28210 }), 28211 }; 28212 })(), 28213 ...(() => { 28214 const canAdminRosganRemates = this.canManageUsers(s.apiUser); 28215 const form = s.rosganRemateForm || this.emptyRosganRemateForm(); 28216 const consignatarias = (s.rosganLotConsignatarias || []).filter((item) => !this.isExcludedConsignataria(this.consName(item))).map((item) => ({ 28217 value: String(item?.id || item?.consignatariaId || ''), label: this.consName(item), raw: item, 28218 })).filter((item) => item.value && item.label).sort((a, b) => a.label.localeCompare(b.label, 'es')); 28219 const consById = Object.fromEntries(consignatarias.map((item) => [item.value, item])); 28220 const assigned = this.rosganAssignedConsignataria(s.rosganLotConsignatarias || []); 28221 const activeTypes = s.rosganRemateTypes || []; 28222 const selectedType = this.rosganRemateType(form.tipoRemate, activeTypes); 28223 const typeAllowsAll = selectedType?.permiteModoTodos === true; 28224 const remates = this.rosganUpcomingRemates(s.rosganRemates).filter((item) => item.cargaDeLotes !== false && !this.isExcludedRemate(item)); 28225 const activeCabanas = (s.cabanas || []).filter((item) => this.cabanaListingGroup(item) === 'active' && !this.isExcludedCabana(item)); 28226 const dateLabel = (item) => { 28227 const date = item?.__cabanaAuction ?this.cabanaAuctionStartAt(item) : (item?.fechaHoraRemate ?new Date(item.fechaHoraRemate) : null); 28228 return date && !Number.isNaN(date.getTime()) 28229 ?date.toLocaleDateString('es-AR', { weekday: 'short', day: '2-digit', month: 'short', year: 'numeric' }) + ' · ' + date.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }) + ' h' 28230 : 'Fecha pendiente'; 28231 }; 28232 const toggles = [ 28233 ['canalRural', 'Canal Rural'], ['cargaDeLotes', 'Habilitar carga de lotes'], 28234 ['informaIndice', 'Informa Ãndice'], ['mostrarComitentes', 'Mostrar c
28234omitentes'], ['closeRemate', 'Cerrar remate'], 28235 ].filter(([key]) => key !== 'closeRemate' || !!s.rosganRemateEditingId).map(([key, label]) => { const on = !!form[key]; return { 28236 label, toggle: () => this.toggleRosganRemateField(key), track: on ?'#16846e' : '#aebdca', knob: on ?'15px' : '0px', 28237 bg: on ?'#f0faf7' : '#f7f9fb', border: on ?'#b9dfd4' : '#dde6ef', 28238 }; }); 28239 const message = s.rosganRemateMessage; 28240 return { 28241 showRosganRemates: true, 28242 canAdminRosganRemates, 28243 rematesIntro: canAdminRosganRemates ?'Creá y administrá los próximos remates y consultá el historial de Rosgan.' : 'Próximos remates e historial de Rosgan.', 28244 openRosganRemateCreate: () => this.chooseRemateCreationType(), 28245 rosganRematePageVisible: s.route === 'rosganRemateCreate' && s.rosganRemateFormOpen, 28246 showRemateArchive: s.route === 'remates' && !!s.remateArchiveOpen, 28247 showRemateArchiveToggle: s.route === 'remates', 28248 remateArchiveOpen: !!s.remateArchiveOpen, 28249 remateArchiveToggleLabel: s.remateArchiveOpen ?'Ocultar remates pasados' : 'Ver remates pasados', 28250 toggleRemateArchive: () => { 28251 const open = !this.state.remateArchiveOpen; 28252 this.setState({ remateArchiveOpen: open }); 28253 if (open && !Array.isArray(this.state.remates) && !this.state.rematesLoading) this.fetchRemates(this.state.remateYear); 28254 }, 28255 refreshRosganRemates: () => { this.loadRosganRemateAdmin(true); this.fetchCabanas(true); }, 28256 rosganRematesLoading: s.rosganRematesLoading || s.cabanasLoading, 28257 rosganRematesInitialLoading: (!!s.rosganRematesLoading && !Array.isArray(s.rosganRemates)) || (!!s.cabanasLoading && !Array.isArray(s.cabanas)), 28258 rosganRematesHasError: !!(s.rosganRematesError || s.cabanasError) && remates.length + activeCabanas.length === 0, 28259 rosganRematesError: [s.rosganRematesError, s.cabanasError].filter(
28259Boolean).join(' · '), 28260 hasRosganRemates: remates.length + activeCabanas.length > 0, 28261 rosganRematesEmpty: !s.rosganRematesLoading && !s.cabanasLoading && !s.rosganRematesError && !s.cabanasError && s.rosganRemates !== null && s.cabanasCollectionReady && remates.length + activeCabanas.length === 0, 28262 rosganRemateCards: [ 28263 ...remates.map((item) => ({ item, date: item?.fechaHoraRemate ?new Date(item.fechaHoraRemate) : null })), 28264 ...activeCabanas.map((item) => ({ item: { ...item, __cabanaAuction: true }, date: this.cabanaAuctionStartAt(item) })), 28265 ].sort((a, b) => (a.date || 0) - (b.date || 0)).map(({ item }) => { 28266 if (item.__cabanaAuction) { 28267 const participant = this.cabanaRemateParticipants(item)[0] || {}; 28268 const mode = this.cabanaMode(item); 28269 const consignataria = this.cabanaConsignatariaName(item, rawConsignatarias); 28270 const consignatariaLogo = this.consWhiteLogo(consignataria) || this.consLogo(consignataria); 28271 const logo = consignatariaLogo; 28272 return { 28273 ...this.remateListCatalog(item, 'cabana'), prebidsActive: window.CabanaPrebid.isOpen(item), 28274 title: item.nombre_remate || item.title || item.titulo || 'Remate de cabaña', date: dateLabel(item), 28275 showFact: true, factLabel: 'CABAÃA', factValue: participant.name || 'Cabaña principal', 28276 consignataria, showConsignataria: false, consignatariaLogo, consignatariaLogoBg: this.consBrandColor(consignataria), hasConsignatariaLogo: !!consignatariaLogo, showHeaderConsignatariaLogo: false, 28277 location: item.ubicacion_remate || item.lugar || item.localidad || item.provincia || item?.location?.lugar || item?.location?.localidad || 'Pendiente', 28278 typeDetail: 'Reproductores', ...this.remateListPlace(item), status: mode === 'live' ?'EN VIVO · CABAÃAS' : 'ACTIVO · CABAÃAS', logo, hasLogo: !!logo, noLogo: !logo, 28279 logoBg: this.consWhiteLogo(consignataria) ?this.consBrandColor(consignataria) : '#fff', initial: consignataria?.trim().charAt(0).toUpperCase() || 'R', 28280 open: () => this.openCabana(item), 28281 }; 28282 } 28283 const id = String(item?.id || ''); 28284 const cons = consById[String(item?.consignatariaId || '')]; 28285 const consName = item?.consignatariaNombre || cons?.label || 'Consignataria pendiente'; 28286 const whiteLogo = this.consWhiteLogo(consName); 28287 const logo = whiteLogo || this.consLogo(consName); 28288 return { 28289 ...this.remateListCatalog(item), 28290 title: item?.titulo || 'Remate Rosgan', date: dateLabel(item), showFact: Number(item?.cabezas) > 0, factLabel: 'CABEZAS', factValue: Number(item?.cabezas) > 0 ? Number(item.cabezas).toLocaleString('es-AR') : '', 28291 location: [item?.localidad, item?.provincia].filter(Boolean).join(', ') || item?.lugar || 'Pendiente', consignataria: consName, 28292 typeDetail: 'Invernada, Faena y CrÃa', ...this.remateListPlace(item), status: item?.closeRemate ?'CERRADO' : item?.activo === false ?'INACTIVO' : 'ACTIVO', logo, hasLogo: !!logo, noLogo: !logo, 28293 logoBg: whiteLogo ?this.consBrandColor(consName) : '#eef6fd', initial: consName.trim().charAt(0).toUpperCase() || 'R', 28294 open: () => this.openRosganActiveRemate(item), 28295 }; 28296 }), 28297 rosganRemateFormOpen: s.rosganRemateFormOpen, 28298 rosganRemateFormEyebrow: s.rosganRemateEditingId ?'EDITAR REMATE' : 'NUEVO REMATE', 28299 rosganRemateFormTitle: s.rosganRemateEditingId ?'Editar remate Rosgan' : 'Crear remate Rosgan', 28300 closeRosganRemateForm: () => this.closeRosganRemateForm(), 28301 saveRosganRemate: () => this.saveRosganRemate(), rosganRemateSaving: s.rosganRemateSaving, 28302 rosganRemateSaveLabel: s.rosganRemateSaving ?'Guardandoâ¦' : (s.rosganRemateEditingId ?'Guardar cambios' : 'Crear remate'), 28303 rrfTitle: form.titulo, setRrfTitle: (e) => this.setRosganRemateField('titulo', e.target.value), 28304 rrfDate: form.fecha, setRrfDate: (e) => this.setRosganRemateField('fecha', e.target.value), 28305 rrfTime: form.hora, setRrfTime: (e) => this.setRosganRemateField('hora', e.target.value), 28306 rrfHeads: form.cabezas, setRrfHeads: (e) => this.setRosganRemateField('cabezas', e.target.value), 28307 rrfType: form.tipoRemate, setRrfType: (e) => this.setRosganRemateField('tipoRemate', e.target.value), 28308 rosganRemateTypeOptions: activeTypes.map((item) => ({ value: item.id, label: item.name })), 28309 rrfTypeDisabled: s.rosganRemateTypesLoading || !activeTypes.length, 28310 rrfTypePlaceholder: s.rosganRemateTypesLoading ?'Cargando tiposâ¦' : 'Seleccionar tipoâ¦', 28311 rrfTypesMessageVisible: !!s.rosganRemateTypesError, 28312 rrfTypesMessage: s.rosganRemateTypesError || '', 28313 rrfMode: form.modoConsignataria === 'Todos' && typeAllowsAll ?'Todos' : 'Indiv
28313idual', 28314 setRrfMode: (e) => this.setRosganRemateField('modoConsignataria', e.target.value), 28315 rosganRemateModeOptions: typeAllowsAll ?[{ value: 'Individual', label: 'Individual' }, { value: 'Todos', label: 'Todos' }] : [{ value: 'Individual', label: 'Individual' }], 28316 rrfModeLocked: !typeAllowsAll, 28317 rrfShowConsignataria: form.modoConsignataria !== 'Todos' || !typeAllowsAll, 28318 rrfAllConsignatarias: form.modoConsignataria === 'Todos' && typeAllowsAll, 28319 rrfConsignataria: form.consignatariaId, setRrfConsignataria: (e) => this.setRosganRemateField('consignatariaId', e.target.value), 28320 rrfConsignatariaLocked: !this.canManageUsers(s.apiUser), 28321 rosganRemateConsignatarias: this.canManageUsers(s.apiUser) ?consignatarias : (assigned ?consignatarias.filter((item) => item.value === String(assigned.id)) : []), 28322 rrfProvince: form.provincia, setRrfProvince: (e) => this.setRosganRemateField('provincia', e.target.value), 28323 rosganRemateProvinces: s.geoProvinces || [], 28324 rrfLocality: form.localidad, setRrfLocality: (e) => this.onRosganRematePlaceInput(e.target.value), 28325 rrfLocalityDisabled: !form.provincia, 28326 rrfLocalityPlaceholder: !form.provincia ?'Elegà una provincia primero' : 'Escribà al menos 3 letras', 28327 rrfPlacesExpanded: String(!!s.rrfPlaceOpen && !!s.rrfPlaceResults?.length), 28328 rrfPlacesVisible: !!s.rrfPlaceOpen && !!s.rrfPlaceResults?.length, 28329 rrfPlaceActiveId: s.rrfPlaceIndex >= 0 ? 'rrf-place-' + s.rrfPlaceIndex : '', 28330 rrfPlaceStatus: s.rrfPlaceLoading ? 'Buscando localidadesâ¦' : s.rrfPlaceError || '', 28331 rrfPlaces: (s.rrfPlaceResults || []).map((item,index)=>({...item,id:'rrf-place-'+index,source:item.sourceLabel || (item.source === 'google'?'Google Places':'Localidad argentina'),selected:String(index === s.rrfPlaceIndex),bg:index === s.rrfPlaceIndex?'#edf5fc':'#fff',pickPointer:event=>{event.preventDefault();this.selectRosganRematePlace(item);},pick:()=>this.selectRosganRematePlace(item)})), 28332 rrfPlaceKey: event => this.rosganRematePlaceKey(event), 28333 rrfPlaceMouseDown: event => event.preventDefault(), 28334 rrfPlaceFocus: () => { clearTimeout(this._rrfPlaceBlur); if (s.rrfPlaceResults?.length) this.setState({rrfPlaceOpen:true}); }, 28335 rrfPlaceBlur: () => { this._rrfPlaceBlur=setTimeout(()=>this.resetRosganRematePlaces(),150); }, 28336 rosganRemateLocalities: (s.geoLocs?.[form.provincia] || []).map((item) => item.nombre), 28337 rrfPlace: form.lugar, setRrfPlace: (e) => this.setRosganRemateField('lugar', e.target.value), 28338 rosganRemateToggles: toggles, 28339 setRosganRemateFlyer: (e) => this.setRosganRemateFlyer(e.target.files?.[0] || null), 28340 rrfHasFlyer: !!s.rosganRemateFlyerPreview, rrfNoFlyer: !s.rosganRemateFlyerPreview, rrfFlyerPreview: s.rosganRemateFlyerPreview, 28341 rosganRemateHasMessage: !!message, rosganRemateMessageText: message?.text || '', 28342 rosganRemateMessageBg: message?.ok ?'#effaf6' : '#fff4f4', rosganRemateMessageBorder: message?.ok ?'#b9e2d4' : '#efc4c8', rosganRemateMessageColor: message?.ok ?'#176b58' : '#a32934', 28343 }; 28344 })(), 28345 ...(() => { 28346 const all = this.unifiedArchiveRemates(s.remates, s.rosganRemates, s.cabanas, s.remateYear); 28347 const withType = all.map((r) => ({ ...r, _t: r.__cabanaAuction ?this.REMATE_TYPES.cabanas : this.remateType(r) })); 28348 const counts = {}; 28349 withType.forEach((r) => { const k = r._t[0]; counts[k] = (counts[k] || 0) + 1; }); 28350 const filtered = s.remateType ?withType.filter((r) => r._t[0] === s.remateType) : withType; 28351 const itemDate = (item) => item.__cabanaAuction ?this.cabanaAuctionStartAt(item) : this.remateDate(item); 28352 const sorted = [...filtered].sort((a, b) => (itemDate(b) || 0) - (itemDate(a) || 0)); 28353 const typeKeys = [...new Set(withType.map((r) => r._t[0]))]; 28354 const yearNow = new Date().getFullYear(); 28355 const dateLabel = (r) => { const d = itemDate(r); if (!d) return 'Fecha pendiente'; const dl = d.toLocaleDateString('es-AR', { day: '2-digit', month: 'short', year: 'numeric' }); const t = String(r.hora_remate || r.hora || r?.schedule?.hora || '').slice(0, 5); return t ?dl + ' · ' + t + ' h' : dl; }; 28356 const mapCard = (r) => { 28357 if (r.__cabanaAuction) { 28358 const participant = this.cabanaRemateParticipants(r)[0] || {}; 28359 const color = this.cabanaColor(r); 28360 const breed = this.cabanaBreedOf(r) || 'Reproductores'; 28361 const consignataria = this.cabanaConsignatariaName(r, rawConsignatarias); 28362 const consignatariaLogo = this.consWhiteLogo(consignataria) || this.consLogo(consignataria); 28363 const logo = consignatariaLogo; 28364 return { 28365 ...this.remateListCatalog(r, 'cabana'), prebidsActive: window.CabanaPrebid.isOpen(r), 28366 open: () =>
28366 this.openCabana(r), color, type: 'CABAÃAS', typeDetail: 'Reproductores', initial: consignataria?.trim().charAt(0).toUpperCase() || 'R', 28367 logo, hasLogo: !!logo, noLogo: !logo, logoBg: this.consWhiteLogo(consignataria) ?this.consBrandColor(consignataria) : '#fff', logoBorder: 'transparent', 28368 title: r.nombre_remate || r.title || r.titulo || 'Remate de cabaña', date: dateLabel(r), 28369 location: r.ubicacion_remate || r.lugar || r.localidad || r.provincia || r?.location?.lugar || r?.location?.localidad || 'Localidad pendiente', 28370 ...this.remateListPlace(r), metaLabel: 'CABAÃA · ' + breed.toUpperCase(), metaValue: participant.name || 'Cabaña principal', showMeta: true, 28371 consignataria, showConsignataria: false, consignatariaLogo, consignatariaLogoBg: this.consBrandColor(consignataria), hasConsignatariaLogo: !!consignatariaLogo, showHeaderConsignatariaLogo: false, 28372 }; 28373 } 28374 const consignataria = this.consDisplayName(r.nombre_cuenta || r.nombre_consignatario) || 'Rosgan'; 28375 const whiteLogo = this.consWhiteLogo(consignataria); 28376 const logo = whiteLogo || this.consLogo(consignataria); 28377 const consignatariaLogo = this.consWhiteLogo(consignataria) || this.consLogo(consignataria); 28378 return { 28379 ...this.remateListCatalog(r, r.__rosganClosedArchive || r.__rosganScheduled ? 'active' : 'historical'), 28380 open: () => r.__rosganClosedArchive || r.__rosganScheduled ?this.openRosganActiveRemate(r) : this.openRemate(r), 28381 color: r._t[1], type: r._t[0].toUpperCase(), typeDetail: /feria/i.test(r._t[0]) ? 'FÃsico' : /cabaña/i.test(r._t[0]) ? 'Reproductores' : 'Invernada, Faena y CrÃa', initial: consignataria.trim()[0].toUpperCase(), 28382 logo, hasLogo: !!logo, noLogo: !logo, 28383 logoBg: whiteLogo ?this.consBrandColor(consignataria) : '#f4f7fb', logoBorder: whiteLogo ?'transparent' : '#e6edf5', 28384 title: r.nombre_remate || 'Remate Rosgan', date: dateLabel(r), 28385 location: r.ubicacion_remate || [r.localidad, r.provincia].filter(Boolean).join(', ') || 'Localidad pendiente', 28386 ...this.remateListPlace(r), metaLabel: 'CONSIGNATARIA', metaValue: consignataria, showMeta: this.consKey(consignataria) !== 'rosgan', 28387 consignatariaLogo, consignatariaLogoBg: this.consBrandColor(consignataria), hasConsignatariaLogo: !!consignatariaLogo, 28388 }; 28389 }; 28390 const scheduledCabanas = (s.cabanas || []).filter((item) => this.cabanaListingGroup(item) === 'upcoming' && !this.isExcludedCabana(item)) 28391 .map((item) => ({ ...item, __cabanaAuction: true, _t: this.REMATE_TYPES.cabanas })); 28392 const scheduled = [...this.rosganScheduledRemates(s.monthRemates, s.rosganRemates).map((r) => ({ ...r, _t: this.remateType(r) })), ...scheduledCabanas] 28393 .sort((a,b) => (itemDate(a)?.getTime() || Infinity) - (itemDate(b)?.getTime() || Infinity)); 28394 const latestFinished = this.unifiedLatestFinishedRemates(s.latestFinished, s.rosganRemates, s.cabanas).map((r) => ({ ...r, _t: r.__cabanaAuction ?this.REMATE_TYPES.cabanas : this.remateType(r) })); 28395 return { 28396 scheduledRemateCards: scheduled.map(mapCard), 28397 latestFinishedCards: latestFinished.map(mapCard), 28398 hasLatestFinished: latestFinished.length > 0, 28399 latestFinishedLoading: !!s.latestFinishedLoading || !!s.cabanasLoading, 28400 latestFinishedError: [s.latestFinishedError, s.cabanasError].filter(
28400Boolean).join(' · '), 28401 retryLatestFinished: () => { this.fetchLatestFinishedRemates(); this.fetchCabanas(true); }, 28402 hasScheduledRemates: scheduled.length > 0, 28403 scheduledRematesLoading: s.monthRemates === null, 28404 remateArchiveHasMore: sorted.length > s.remateArchiveLimit, 28405 moreRemateArchive: () => this.setState({ remateArchiveLimit: this.state.remateArchiveLimit + 12 }), 28406 remateYear: String(s.remateYear), onRemateYear: (e) => { const y = Number(e.target.value); this.setState({ remateYear: y, remates: null, remateType: '', remateArchiveLimit: 12 }); this.fetchRemates(y); }, 28407 remateYears: (s.remateYears && s.remateYears.length ?s.remateYears : Array.from({ length: 8 }, (_, i) => String(yearNow - i))), 28408 remateTypeChips: [{ label: 'Todos', key: '', dot: '#002e5f', count: withType.length }, ...typeKeys.map((k) => ({ label: k, key: k, dot: (Object.values(this.REMATE_TYPES).find((v) => v[0] === k) || ['', '#002e5f'])[1], count: counts[k] }))].map((c) => ({ 28409 label: c.label, count: c.count, dot: c.dot, go: () => this.setState({ remateType: c.key }), 28410 bg: s.remateType === c.key ?'#002e5f' : '#fff', color: s.remateType === c.key ?'#fff' : '#3d4c5f', border: s.remateType === c.key ?'#002e5f' : '#e3eaf2', 28411 })), 28412 rematesLoadingVisible: s.rematesLoading || s.remates === null || s.cabanasLoading, 28413 rematesReady: !s.rematesLoading && s.remates !== null && !s.cabanasLoading, 28414 remateFiltersVisible: withType.length > 0, 28415 hasRemates: sorted.length > 0, rematesEmpty: !s.rematesLoading && !!s.remates && sorted.length === 0, 28416 remateCards: s.remateArchiveOpen ?sorted.slice(0, s.remateArchiveLimit).map(mapCard) : [], 28417 }; 28418 })(), 28419 ...(() => { 28420 const rm = s.currentRemate || {}; 28421 const t = this.remateType(rm); 28422 const raw = s.route === 'remateDetail' ? this.publicRemateLots(s.remateLots, rm) : (s.remateLots || []); 28423 const orderDefined = !rm.__rosganActive || (s.catalogPublication?.key === this.catalogPublicationKey(rm) && s.catalogPublication.salesOrderDefined === true); 28424 const lotNumber = (l) => rm.__rosganActive 28425 ?(orderDefined ? l.lotNumber : null) 28426 :(l.orden_final ?? l.orden_venta ?? l.lotNumber); 28427 const lotNum = (l) => { const n = Number(lotNumber(l)); return Number.isFinite(n) && n > 0 ?'Lote ' + n : ''; }; 28428 const heads = (l) => Number(l.cantidad) || 0; 28429 const wt = (l) => Number(l.peso_promedio) || 0; 28430 const loc = (l) => [l.nombre_localidad, l.nombre_provincia].filter(Boolean).join(', '); 28431 const q = s.rdQuery.trim().toLowerCase(); 28432 let list = raw.filter((l) => !q || [lotNum(l), l.nro_certificado, l.nombre_categoria, l.raza, loc(l)].join(' ').toLowerCase().includes(q)); 28433 const filterId=this.remateRouteId(rm); 28434 const selectedFilters=s.rdLotFilters?.[filterId]||{}; 28435 const filterGroups=window.RemateFilters.facets(list,selectedFilters).filter(group=>group.alwaysVisible || group.options.length>1 || group.options.some(option=>option.active)); 28436 const toggleCategory = value => { 28437 const latest = this.state.rdLotFilters?.[filterId] || {}; 28438 const values = latest.category || []; 28439 this.setState({ rdLotFilters: { ...this.state.rdLotFilters, [filterId]: { ...latest, category: values.includes(value) ?values.filter(item=>item!==value) : [...values,value] } } }); 28440 }; 28441 list=window.RemateFilters.filterLots(list,selectedFilters); 28442 const filterCount=Object.values(selectedFilters).reduce((n,v)=>n+v.length,0); 28443 if (s.rdSort === 'peso-desc') list = [...list].sort((a, b) => wt(b) - wt(a)); 28444 if (s.rdSort === 'peso-asc') list = [...list].sort((a, b) => wt(a) - wt(b)); 28445 if (s.rdSort === 'cabezas') list = [...list].sort((a, b) => heads(b) - heads(a)); 28446 if (rm.__rosganActive || s.rdSort === 'orden') list = [...list].sort((a, b) => this.compareRosganSalesOrder(a,b)); 28447 const totalHeads = rm.__rosganActive ? (s.route === 'remateDetail' ? this.publicRemateHeads(raw, rm) : this.rosganRemateHeads(rm, raw)) : raw.reduce((a, l) => a + heads(l), 0); 28448 const categoryHeads = Object.values(raw.reduce((groups, lot) => { 28449 const label = String(lot.nombre_categoria || 'Sin categorÃa').trim() || 'Sin categorÃa';
28450 const key = label.toLocaleLowerCase('es-AR'); 28451 if (!groups[key]) groups[key] = { label, heads: 0 }; 28452 groups[key].heads += heads(lot); 28453 return groups; 28454 }, {})).filter((item) => item.heads > 0).sort((a, b) => b.heads - a.heads || a.label.localeCompare(b.label, 'es')); 28455 const rosganOrderLots = Array.isArray(s.rosganSalesOrderLots) ?s.rosganSalesOrderLots : []; 28456 const orderSchedule = this.normalizeRosganSalesOrderSchedule(s.rosganSalesOrderSchedule); 28457 const scheduleByCert = new Map(orderSchedule.entries.map((entry) => [entry.cert, entry])); 28458 const orderCategoryOf = (lot) => String(lot.nombre_categoria || lot.categoryName || 'Sin categorÃa').trim() || 'Sin categorÃa'; 28459 const orderConsignatariaOf = (lot) => this.consDisplayName(lot.nombre_consignatario || lot.consignatariaNombre || rm.nombre_cuenta || rm.nombre_consignatario) || 'Sin consignataria'; 28460 const orderStatusLabels = { cargado: 'Cargado', controlado: 'Controlado', publicado: 'Publicado', anulado: 'Anulado', cerrado: 'Cerrado', 'sin-estado': 'Sin estado' }; 28461 const orderQuery = this.consKey(s.rosganSalesOrderQuery || ''); 28462 const orderStatusFilter = String(s.rosganSalesOrderStatusFilter || ''); 28463 const orderCategoryFilter = String(s.rosganSalesOrderCategoryFilter || ''); 28464 const orderConsignatariaFilter = String(s.rosganSalesOrderConsignatariaFilter || ''); 28465 const orderRatingFilter = String(s.rosganSalesOrderRatingFilter || ''); 28466 28467 const ratingColors = ['#c7353f', '#e05a32', '#e58a19', '#b5a20b', '#56a044', '#16846e']; 28468 const rosganOrderRows = rosganOrderLots.map((lot, index) => { 28469 const cert = this.remateLotRouteId(lot); 28470 const scheduleEntry = scheduleByCert.get(cert); 28471 const category = orderCategoryOf(lot); 28472 const workflowStatus = this.rosganLotWorkflowStatus(lot); 28473 const rating = Number(s.rosganSalesOrderRatings?.[cert]) || 0; 28474 const ratingColor = rating ?ratingColors[rating - 1] : '#8a9aad'; 28475 const fallbackMedia = this.rosganSalesOrderMedia(lot); 28476 const videoUrl = lot.salesVideoUrl || fallbackMedia.url; 28477 const videoKind = lot.salesVideoKind || fallbackMedia.kind; 28478 const videoLabel = lot.salesVideoLabel || fallbackMedia.label; 28479 const consignataria = orderConsignatariaOf(lot); 28480 const whiteConsignatariaLogo = this.consWhiteLogo(consignataria); 28481 const consignatariaLogo = whiteConsignatariaLogo || this.consLogo(consignataria); 28482 const locality = [lot.nombre_localidad || lot.locality || lot.establishmentLocality, lot.nombre_provincia || lot.province || lot.establishmentProvince].filter(Boolean).join(', '); 28483 const paymentTerm = lot.plazo || lot.paymentTerm || lot.commercial?.paymentTerm || lot.sale?.paymentTerm || ''; 28484 const quantity = Number(lot.cantidad) || 0; 28485 const weight = Number(lot.peso_promedio) || 0; 28486 return { 28487 cert, position: index + 1, maxPosition: rosganOrderLots.length, 28488 title: 'Cert. ' + cert, 28489 isOutOfCatalog: lot.isOutOfCatalog === true, 28490 category, 28491 breed: lot.raza || '', consignataria, 28492 statusKey: workflowStatus, 28493 statusLabel: orderStatusLabels[workflowStatus] || String(workflowStatus).replace(/(^|-)(\w)/g, (_, separator, letter) => (separator ?' ' : '') + letter.toUpperCase()), 28494 statusClass: ['cargado', 'controlado', 'publicado', 'anulado'].includes(workflowStatus) ?'is-' + workflowStatus : 'is-otro', 28495 searchText: this.consKey([index + 1, cert, category, lot.raza, consignataria, locality, paymentTerm, scheduleEntry?.alias, orderStatusLabels[workflowStatus] || workflowStatus].filter(Boolean).join(' ')), 28496 consignatariaLogo, hasConsignatariaLogo: !!consignatariaLogo, noConsignatariaLogo: !consignatariaLogo, 28497 consignatariaLogoBg: whiteConsignatariaLogo ?this.consBrandColor(consignataria) : (consignatariaLogo ?'#fff' : this.consBrandColor(consignataria)), 28498 consignatariaInitial: String(consignataria).trim().charAt(0).toUpperCase() || 'R', 28499 facts: [ 28500 { label: 'CABEZAS', value: quantity ?quantity.toLocaleStr
28500ing('es-AR') : 'â' }, 28501 { label: 'PESO PROM.', value: weight ?weight.toLocaleString('es-AR') + ' kg' : 'â' }, 28502 { label: 'LOCALIDAD', value: locality || 'â' }, 28503 { label: 'PLAZO', value: paymentTerm || 'â' }, 28504 ], 28505 hasExcelSchedule: !!scheduleEntry, 28506 excelAlias: scheduleEntry?.alias || '', 28507 estimatedTime: window.RosganSalesOrderImport.clock(scheduleEntry?.estimatedStartSeconds), 28508 averageTime: window.RosganSalesOrderImport.duration(scheduleEntry?.averageSeconds) + ' min', 28509 filterRating: Number((s.rosganSalesOrderRatingSnapshot || s.rosganSalesOrderRatings)?.[cert]) || 0, 28510 rating, ratingLabel: rating ?'Nivel ' + rating + ' de 6' : 'Sin puntaje', ratingColor, 28511 ratingStars: Array.from({ length: 6 }, (_, star) => ({ 28512 value: star + 1, active: star < rating, color: star < rating ?ratingColor : '#c8d3df', 28513 pick: () => this.setRosganSalesOrderRating(lot, star + 1), 28514 label: 'Puntuar ' + (star + 1) + ' de 6 el lote ' + cert, 28515 })), 28516 videoUrl, hasVideo: !!videoUrl, noVideo: !videoUrl, videoLabel: videoLabel || 'VIDEO', videoIsRaw: videoKind === 'raw', 28517 videoBadgeClass: videoKind === 'raw' ?'is-raw' : 'is-final', videoOpen: s.rosganSalesOrderVideoCert === cert, 28518 videoButtonLabel: s.rosganSalesOrderVideoCert === cert ?'Cerrar video' : 'Ver video ' + (videoLabel || ''), 28519 videoPoster: lot.thumbnailUrl || lot.thumbnail || lot.imagen || '', toggleVideo: () => this.toggleRosganSalesOrderVideo(lot), 28520 playVideo: (event) => this.playRosganSalesOrderVideo(lot, event), 28521 videoDidPlay: (event) => this.markRosganSalesOrderVideoPlaying(event), 28522 videoDidPause: (event) => this.markRosganSalesOrderVideoPaused(event), 28523 videoDidError: (event) => this.markRosganSalesOrderVideoError(event), 28524 isFirst: index === 0, isLast: index === rosganOrderLots.length - 1, 28525 moveUp: () => this.moveRosganSalesOrder(index, -1), moveDown: () => this.moveRosganSalesOrder(index, 1), 28526 setPosition: (event) => this.setRosganSalesOrderPosition(index, event), 28527 }; 28528 }).filter((row) => (!orderQuery || row.searchText.includes(orderQuery)) 28529 && (!orderStatusFilter || row.statusKey === orderStatusFilter) 28530 && (!orderCategoryFilter || row.category === orderCategoryFilter) 28531 && (!orderConsignatariaFilter || row.consignataria === orderConsignatariaFilter) 28532 && (orderRatingFilter === 'unrated' ? !row.filterRating : orderRatingFilter === 'rated' ? !!row.filterRating : true)); 28533 const dd = this.remateDate(rm); 28534 const lb = s.route === 'remateDetail' && rm.__rosganActive && !this.isRosganLotPublished(s.rdLightbox) ? null : s.rdLightbox; 28535 const lbNeighbors = this.remateLotNeighbors(lb, raw); 28536 const isActiveRosgan = !!rm.__rosganActive; 28537 const remateId = this.remateRouteId(rm); 28538 const lotCert = this.remateLotRouteId(lb); 28539 const videoKey = isActiveRosgan && lotCert ?this.rosganLotVideoKey(remateId, lotCert) : ''; 28540 const videoState = videoKey ?(s.rosganLotVideoState?.[videoKey] || { loading: true, videos: [], uploads: [], error: '' }) : { loading: false, videos: [], uploads: [], error: '' }; 28541 const loadedPrimaryVideo = (videoState.videos || []).find((video) => video?.url || video?.publicUrl || video?.videoUrl || video?.downloadUrl || video?.signedUrl); 28542 const lbPrimaryVideoUrl = lb?.videoUrl || lb?.video_uri || loadedPrimaryVideo?.url || loadedPrimaryVideo?.publicUrl || loadedPrimaryVideo?.videoUrl || loadedPrimaryVideo?.downloadUrl || loadedPrimaryVideo?.signedUrl || ''; 28543 const canManageVideos = isActiveRosgan && this.canEditRosganLot(lb); 28544 const videoDeletingPrefix = videoKey + ':'; 28545 const hasLotValue = (value) => { 28546 if (value === null || value === undefined) return false; 28547 const normalized = String(value).trim().toLowerCase(); 28548 return !['', '0', '?', '-', 'â', 'n/a', 'null', 'undefined', 'sin dato', 's/d'].includes(normalized); 28549 }; 28550 const factsOf = (l) => [ 28551 { label: 'CABEZAS', value: heads(l) || '' }, { label: 'PESO PROM.', value: wt(l) ?wt(l) + ' kg' : '' }, 28552 { label: 'PLAZO', value: l.plazo || '' }, { label: 'LOCALIDAD', value: loc(l) }, 28553 ].filter((item) => hasLotValue(item.value)); 28554 const rosganFieldIconSlots = {
28555 'CABEZAS':1, 'MACHOS':2, 'HEMBRAS':3, 'MADRES':4, 'CRÃAS':5, 28556 'PESO PROM.':6, 'PESO MÃNIMO':7, 'PESO MÃXIMO':8, 28557 'CATEGORÃA':9, 'RAZA':10, 'TIPO':11, 'CLASE':12, 'EDAD':13, 28558 'ESTADO':14, 'DESCRIPCIÃN':15, 'DENTICIÃN':16, 28559 'ESTADO REPRODUCTIVO':17, 'DESTINO':18, 'PLAZO':19, 'PESAJE':20, 28560 'DESBASTE':21, 'ALIMENTACIÃN':22, 'SANIDAD':23, 'GARRAPATA':24, 28561 'MÃO MÃO':25, 'APTO CHINA':26, 'TÃRMINO':27, 'MARCA LÃQUIDA':28, 28562 'APTO VISEC':29, 'SERVICIO':30, 'PREÃEZ':31, 'PARICIÃN':32, 28563 'SUBCATEGORÃA':33, 'CONDICIÃN':34, 'ENTREGA':35, 28564 'TERMINACIÃN':36, 'RINDE ESTIMADO':37, 28565 }; 28566 const lotCards = (items) => items.filter((item) => hasLotValue(item.value)).map((item) => { 28567 const index = (rosganFieldIconSlots[item.label] || 15) - 1; 28568 return {...item, isBreed:item.label === 'RAZA', iconX:(index % 8) * 100 / 7, iconY:Math.floor(index / 8) * 25}; 28569 }); 28570 const kiloValue = (value) => hasLotValue(value) ?Number(value).toLocaleString('es-AR') + ' kg' : ''; 28571 const headValue = (value) => hasLotValue(value) ?Number(value).toLocaleString('es-AR') : ''; 28572 const booleanValue = (value) => typeof value === 'boolean' ?(value ?'SÃ' : 'No') : ''; 28573 const formTypeValue = (value) => ({ invernada: 'Invernada', vientres: 'Vientres', faena: 'Faena' }[String(value || '').toLowerCase()] || value || ''); 28574 const rdOwnCompany=s.userConsignataria || s.userConsignatarias?.[0]; 28575 const rdOwnCompanyRef=String(rdOwnCompany?.id || rdOwnCompany?.ref || rdOwnCompany?.name || ''); 28576 const rdOwnCompanyRecord=this.consRecord(rdOwnCompanyRef); 28577 const rdOwnCompanyName=rdOwnCompanyRef?(rdOwnCompanyRecord?.name || rdOwnCompanyRecord?.nombre || rdOwnCompanyRef.replace(/-s-r-l$/i,' S.R.L.').replace(/-s-a$/i,' S.A.').replace(/-/g,' ').replace(/\b\p{L}/gu,c=>c.toUpperCase())):''; 28578 const rdConsignataria = this.consDisplayName(rm.nombre_cuenta || rm.nombre_consignatario) || 'Rosgan'; 28579 const lbConsignataria = this.consDisplayName(lb?.nombre_consignatario || rdConsignataria) || 'Rosgan'; 28580 const lbConsignatariaLogo = this.consWhiteLogo(lbConsignataria) || this.consLogo(lbConsignataria); 28581 const lbConsignatariaColor = this.consBrandColor(lbConsignataria); 28582 const lbRepresentative = this.rosganLotRepresentative(lb); 28583 const lbRepresentativeAvatar = this.commercialAvatar(lbRepresentative, lbConsignataria, !!s.rosganRepresentativePhotoFailed?.[lbRepresentative?.uid]); 28584 const lbBreedIcons = this.rosganBreedIcons(lb?.raza); 28585 const descriptionParts = (Array.isArray(lb?.descripcion_lote) ? lb.descripcion_lote : [lb?.descripcion_lote || '']).flatMap(value => String(value).split(/\s*[·;\n]\s*/)); 28586 const descriptionAges = descriptionParts.map(value => value.match(/^Edad\s*:\s*(.+)$/i)?.[1]).filter(Boolean); 28587 const lbDescription = descriptionParts.filter(value => !/^Edad\s*:/i.test(value)).join(' · '); 28588 const lbAge = lb?.edad || [...new Set(descriptionAges)].join(' · '); 28589 const lbObservations = [lb?.observations, lb?.observaciones_certificado, lb?.observaciones].map(value => String(value ?? '').trim()).find(Boolean) || ''; 28590 const lbDetailGroups = lb ?[ 28591 { label: 'CANTIDAD Y PESO', cards: lotCards([ 28592 { label: 'CABEZAS', value: headValue(lb.cantidad) }, { label: 'MACHOS', value: headValue(lb.cantidad_machos) }, 28593 { label: 'HEMBRAS', value: headValue(lb.cantidad_hembras) }, { label: 'MADRES', value: headValue(lb.cantidad_madres) }, 28594 { label: 'CRÃAS', value: headValue(lb.cantidad_crias) }, { label: 'PESO PROM.', value: kiloValue(lb.peso_promedio) }, 28595 { label: 'PESO MÃNIMO', value: kiloValue(lb.peso_minimo) }, { label: 'PESO MÃXIMO', value: kiloValue(lb.peso_maximo) }, 28596 ]) }, 28597 { label: 'CARACTERÃSTICAS DEL LOTE', cards: lotCards([ 28598 { label: 'CATEGORÃA', value: lb.nombre_categoria }, { label: 'RAZA', value: lb.raza }, 28599 { label: 'CLASE', value: lb.clase }, 28600 { label: 'EDAD', value: lbAge }, { label: 'ESTADO', value: lb.estado },
28601 { label: 'DESCRIPCIÃN', value: lbDescription }, { label: 'DENTICIÃN', value: lb.denticion }, 28602 { label: 'ESTADO REPRODUCTIVO', value: lb.estado_reproductivo }, { label: 'DESTINO', value: lb.destino }, 28603 ]) }, 28604 { label: 'CONDICIONES', cards: lotCards([ 28605 { label: 'PLAZO', value: lb.plazo }, { label: 'PESAJE', value: lb.pesaje }, 28606 { label: 'DESBASTE', value: lb.desbaste }, { label: 'ALIMENTACIÃN', value: lb.pastoreo }, 28607 { label: 'SANIDAD', value: lb.sanidad }, { label: 'GARRAPATA', value: lb.garrapata }, 28608 { label: 'MÃO MÃO', value: booleanValue(lb.hasMioMio) }, { label: 'APTO CHINA', value: this.rosganTvIsAptoChina(lb) ? 'SÃ' : '' }, 28609 { label: 'TÃRMINO', value: [true, 1, 'true', '1', 'si', 'sÃ'].includes(typeof lb.isTermino === 'string' ? lb.isTermino.trim().toLowerCase() : lb.isTermino) ? 'SÃ' : '' }, { label: 'MARCA LÃQUIDA', value: booleanValue(lb.invernada?.hasLiquidBrand) }, 28610 28611 ]) }, 28612 { label: 'VIENTRES', cards: lotCards([ 28613 { label: 'SERVICIO', value: lb.servicio }, { label: 'PREÃEZ', value: lb.prenez }, { label: 'PARICIÃN', value: lb.paricion }, 28614 ]) }, 28615 { label: 'FAENA', cards: lotCards([ 28616 { label: 'SUBCATEGORÃA', value: lb.faena_subcategoria }, { label: 'CONDICIÃN', value: lb.faena_condicion }, 28617 { label: 'ENTREGA', value: lb.faena_entrega }, { label: 'TERMINACIÃN', value: lb.faena_terminacion }, 28618 { label: 'RINDE ESTIMADO', value: lb.faena_rinde }, 28619 ]) }, 28620 ].filter((group) => group.cards.length) : []; 28621 return { 28622 rdColor: t[1], rdType: t[0].toUpperCase(), rdInitial: rdConsignataria.trim()[0].toUpperCase(), 28623 ...(() => { 28624 const consignatariaRef = rdConsignataria; 28625 const whiteLogo = this.consWhiteLogo(consignatariaRef); 28626 const logo = whiteLogo || this.consLogo(consignatariaRef); 28627 return { rdLogo: logo, rdHasLogo: !!logo, rdNoLogo: !logo, rdLogoBg: whiteLogo ?this.consBrandColor(consignatariaRef) : '#fff', rdLogoBorder: whiteLogo ?'transparent' : 'rgba(255,255,255,.32)' }; 28628 })(), 28629 rdTitle: rm.nombre_remate || 'Remate histórico', 28630 rdDate: dd ?dd.toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric' }) + (rm.hora_remate ?' · ' + String(rm.hora_remate).slice(0, 5) + ' h' : '') : 'Fecha pendiente', 28631 rdLocation: [this.remateListPlace(rm).venue, this.remateListPlace(rm).location].filter(Boolean).join(' · '), 28632 rdConsignataria, 28633 ...window.RosganAuctionShare.prepare({id:remateId,title:rm.nombre_remate||'Remate Rosgan',date:dd?dd.toLocaleDateString('es-AR',{day:'numeric',month:'long',year:'numeric'})+(rm.hora_remate?' · '+String(rm.hora_remate).slice(0,5)+' h':''):'',location:rm.ubicacion_remate||[rm.localidad,rm.provincia].filter(Boolean).join(', '),consignataria:rdConsignataria,heads:this.remateManualShareHeads(rm),path:'/remates/'+encodeURIComponent(remateId)},()=>this.setState({remateShareReady:Date.now()})), 28634 rdCanAdminRemate: isActiveRosgan && this.canManageUsers(s.apiUser), 28635 editCurrentRosganRemate: () => this.openRosganRemateForm(rm), 28636 deleteCurrentRosganRemate: () => this.deleteRosganRemate(rm), 28637 rdRemateDeleting: s.rosganRemateDeleting === remateId, 28638 rdDeleteRemateLabel: s.rosganRemateDeleting === remateId ?'Eliminandoâ¦' : 'Eliminar remate', 28639 rdHasStats: raw.length > 0 || totalHeads > 0, 28640 rdStats: [ 28641 ...(raw.length ?[{ value: raw.length.toLocaleString('es-AR'), label: 'LOTES' }] : []), 28642 ...(totalHeads ?[{ value: totalHeads.toLocaleString('es-AR'), label: 'CABEZAS' }] : []), 28643 ], 28644 rdHasCategoryHeads: categoryHeads.length > 0, 28645 rdCategoryHeads: categoryHeads.map((item) => { 28646 const active = (selectedFilters.category || []).includes(item.label); 28647 return { label: item.label, value: item.heads.toLocaleString('es-AR'), active, bg: active ?'rgba(56,166,255,.32)' :'rgba(255,255,255,.08)', border: active ?'#80c8ff' :'rgba(255,255,255,.14)', pick: () => toggleCategory(item.label) }; 28648 }), 28649 rdCatalogPublished: s.catalogPublication?.key === this.catalogPublicationKey(rm) && s.catalogPublication?.published === true, 28650 rdCompanyCatalogAvailable: !!rdOwnCompanyRef && s.catalogPublication?.key === this.catalogPublicationKey(rm) && s.catalogPublication?.published === true, 28651 rdCompanyCatalogColor: this.consBrandColor(rdOwnCompanyName), 28652 openRdCompanyCatalog: () => { 28653 if(!rdOwnCompanyRef)return; 28654 const dialog=document.createElement('dialog');dialog.className='rd-company-dialog'; 28655 dialog.style.setProperty('--company-color',this.consBrandColor(rdOwnCompanyRef)); 28656 const make=(tag,cls,text)=>{const node=document.createElement(tag);node.className=cls;if(text)node.textContent=text;return node;}; 28657 const header=make('div','rd-company-dialog-header'); 28658 const brand=make('div','rd-company-dialog-brand'),logo=this.consWhiteLogo(rdOwnCompanyRef)||this.consLogo(rdOwnCompanyRef); 28659 if(logo){const img=make('img','');img.src=logo;img.alt='';brand.append(img);if(!this.consWhiteLogo(rdOwnCompanyRef))brand.style.background='#fff';}else brand.textContent=rdOwnCompanyName.slice(0,1); 28660 const identity=make('div','rd-company-dialog-identity');identity.append(make('span','rd-company-dialog-eyebrow','CATÃLOGO DE TU CONSIGNATARIA'),make('strong','',rdOwnCompanyName));header.append(brand,identity); 28661 const close=make('button','rd-company-dialog-dismiss','Ã');close.type='button';close.setAttribute('aria-label','Cerrar');close.onclick=()=>dialog.close();header.append(close); 28662 const body=make('div','rd-company-dialog-body'),heading=make('h2','','¿Qué versión querés descargar?');heading.id='rd-company-dialog-title';dialog.setAttribute('aria-labelledby',heading.id); 28663 body.append(heading,make('p','rd-company-dialog-intro','Los mismos lotes, con la información que necesitás compartir.')); 28664 const actions=make('div','rd-company-dialog-actions'); 28665 for(const [variant,text,description,icon] of [['con','Con comitentes','Incluye el nombre del c
28665omitente en cada lote.','users-round'],['sin','Sin comitentes','Muestra los datos del lote sin identificar al comitente.','file-text']]){ 28666 const button=make('button','rd-company-dialog-option');button.type='button'; 28667 const symbol=make('span','rd-company-dialog-symbol'),img=make('img','');img.src='/assets/icons/lucide/'+icon+'.svg';img.alt='';symbol.append(img); 28668 const download=make('span','rd-company-dialog-download','Descargar PDF'),downloadIcon=make('img','');downloadIcon.src='/assets/icons/lucide/download.svg';downloadIcon.alt='';downloadIcon.width=16;downloadIcon.height=16;download.style.display='flex';download.style.alignItems='center';download.style.gap='6px';download.append(downloadIcon); 28669 const copy=make('span','rd-company-dialog-option-copy');copy.append(make('strong','',text),make('span','',description),download);button.append(symbol,copy); 28670 button.onclick=()=>{const query=new URLSearchParams({remate:String(remateId),consignataria:rdOwnCompanyName,consignatariaId:rdOwnCompanyRef,version:variant});if(!isActiveRosgan)query.set('historico','1');window.location.href='/catalogo.html?'+query;};actions.append(button); 28671 } 28672 body.append(actions,make('div','rd-company-dialog-format','PDF · A4 horizontal · Un lote por página')); 28673 dialog.append(header,body);dialog.addEventListener('close',()=>dialog.remove(),{once:true});document.body.append(dialog);dialog.showModal();; 28674 }, 28675 openRdCatalog: () => { window.location.href = '/catalogo.html?remate=' + encodeURIComponent(remateId) + (isActiveRosgan ? '' : '&historico=1'); }, 28676 rdFiltersOpen:s.rdLotFiltersOpen===filterId,rdFilterButton:'Filtros'+(filterCount?' · '+filterCount:''), 28677 rdHasActiveFilters: filterCount > 0, 28678 rdActiveFilterCount: String(filterCount), 28679 toggleRdFilters:()=>this.setState({rdLotFiltersOpen:s.rdLotFiltersOpen===filterId?null:filterId}), 28680 clearRdFilters:()=>this.setState({rdLotFilters:{...s.rdLotFilters,[filterId]:{}}}), 28681 rdFilterGroups: filterGroups.map(group => ({ ...group, icon: ({ category: '/assets/icons/lucide/tag.svg', consignor: '/assets/icons/lucide/building-2.svg', breed: '/assets/icons/lucide/beef.svg', term: '/assets/icons/lucide/clock-3.svg', province: '/assets/icons/lucide/map.svg', locality: '/assets/icons/lucide/map-pin.svg', visec: '/assets/icons/visec-mark.svg', china: '/assets/icons/china-flag.svg' })[group.key], options: group.options.map(option => { 28682 const brand = group.key === 'consignor' ?this.lotDestinationLogos({ consignatariaId: option.value, consignatariaNombre: option.value })[0] : null; 28683 return { ...option, label: brand?.name || (group.alwaysVisible ? 'Solo aptos' : option.value), hasLogo: !!brand?.logo, logo: brand?.logo || '', logoBg: brand?.color || '#fff', 28684 bg: option.active ?'#003767' :'#fff', color: option.active ?'#fff' :'#375875', 28685 pick: () => { const latest = this.state.rdLotFilters?.[filterId] || {}; const values = latest[group.key] || []; 28686 this.setState({ rdLotFilters: { ...this.state.rdLotFilters, [filterId]: { ...latest, [group.key]: values.includes(option.value) ?values.filter(v=>v!==option.value) : [...values,option.value] } } }); 28687 } 28688 }; 28689 }) })), 28690 rdQuery: s.rdQuery, onRdQuery: (e) => this.setState({ rdQuery: e.target.value }), 28691 rdSort: s.rdSort, onRdSort: (e) => this.setState({ rdSort: e.target.value }), 28692 rdCanManageSalesOrder: isActiveRosgan && this.canManageUsers(s.apiUser), 28693 rdCanViewAuditor: isActiveRosgan && !!s.user && (this.canManageUsers(s.apiUser) || this.canManageSellerData(s.apiUser) || this.isRosganProfile(s.apiUser)), 28694 openRdSalesOrder: () => this.openRosganSalesOrder(), 28695 rdAuditorLabel: this.isRosganProfile() ? 'Panel Consignatario' : 'Panel Auditor', 28696 openRdAuditor: () => this.openRosganAuditor(rm), 28697 rdShowOperations: isActiveRosgan, 28698 rdCanManageOperations: isActiveRosgan && this.canOpenRosganOperation(rm, 'commercial'), 28699 rdCanViewStats: isActiveRosgan && this.canOpenRosganOperation(rm, 'stats'), 28700 openRdCommercial: () => this.openRosganOperations(rm, 'commercial'), 28701 openRdStats:()=>this.openCurrentRosganOps('stats'), 28702 openRdPrices: () => this.openRosganOperations(rm, 'prices'), 28703 openRdTv: () => this.openRosganOperations(rm, 'tv'), 28704 openRdTvConfig: () => this.openRosganOperations(rm, 'tvConfig'), 28705 setGridView: () => this.setState({ rdView: 'grid' }), setListView: () => this.setState({ rdView: 'list' }), 28706 gridViewBg: s.rdView === 'grid' ?'#002e5f' : 'transparent', gridViewColor: s.rdView === 'grid' ?'#fff' : '#8593a6', 28707 listViewBg: s.rdView === 'list' ?'#002e5f' : 'transparent', listViewColor: s.rdView === 'list' ?'#fff' : '#8593a6', 28708 rdCount: list.length, rdTotal: raw.length, 28709 rdSourceNote: '', 28710 rdLoading: !!s.remateLotsLoading, 28711 rdLoaded: !s.remateLotsLoading, 28712 rdShowGrid: !s.remateLotsLoading && s.rdView === 'grid' && list.length > 0, rdShowList: !s.remateLotsLoading && s.rdView === 'list' && list.length > 0, 28713 rdEmpty: !s.remateLotsLoading && !!s.remateLots && list.length === 0, 28714 rdEmptyTitle: raw.length === 0 ?'Este remate aún no tiene lotes publicados' : 'No encontramos lotes para esa búsqueda', 28715 rdEmptyText: raw.length === 0 ?'Los lotes se publican cuando el remate se aproxima.' : 'Probá con otro término.', 28716 rdLots: (s.route==='remateDetail'?list:[]).map((l, i) => { 28717 const consignataria = this.consDisplayName(l.nombre_consignatario || rdConsignataria) || 'Rosgan'; 28718 const whiteLogo = this.consWhiteLogo(consignataria); 28719 const logo = whiteLogo || this.consLogo(consignataria); 28720 const alertCert = this.remateLotRouteId(l); 28721 const alertKey = this.remateLotAlertKey(remateId, alertCert); 28722 const alert = s.remateLotAlerts?.[alertKey] || null; 28723 const alertBusy = s.remateLotAlertBusy === alertKey; 28724 const alertEnabled = alert?.enabled === true && alert?.
28724status !== 'notified'; 28725 const alertSent = alert?.status === 'notified'; 28726 const raza = hasLotValue(l.raza) ?String(l.raza).trim() : ''; 28727 const localidad = loc(l); 28728 const cantidad = heads(l); 28729 const pesoPromedio = wt(l); 28730 const cert = l.nro_certificado || l.id_certificado || ''; 28731 const share=window.RosganAuctionShare.prepare({kind:'lot',brandLogo:logo,brandColor:this.consBrandColor(consignataria),slogan:window.rosganConsignatariaSlogan?.(consignataria)||'',title:l.nombre_categoria||raza||'Lote',cert,breed:raza,heads:cantidad,weight:pesoPromedio,location:localidad,consignataria,auction:rm.nombre_remate||rm.titulo||'Remate Rosgan',details:{maleQuantity:l.cantidad_machos,femaleQuantity:l.cantidad_hembras,minWeight:l.peso_minimo,maxWeight:l.peso_maximo,formType:l.formType||(l.faena?'faena':l.vientres?'vientres':''),isVientres:!!l.vientres,age:l.edad||String(l.descripcion_lote||'').match(/(?:^|[·;\n])\s*Edad\s*:\s*([^·;\n]+)/i)?.[1]||'',dentition:l.denticion,reproductiveStatus:l.estado_reproductivo,service:l.servicio,pregnancy:l.prenez,calving:l.paricion,terms:l.plazo,destination:l.destino,condition:l.faena_condicion,delivery:l.faena_entrega},image:l.thumbnailUrl||l.thumbnail||l.imagen||'',video:l.videoUrl||l.video_uri||'',path:'/remates/'+encodeURIComponent(remateId)+'/'+encodeURIComponent(alertCert)},()=>this.setState({lotShareReady:Date.now()})); 28732 28733 return { 28734 shareLot:event=>{event?.stopPropagation();share.shareCurrentRemate();}, shareReady:share.rdShareReady, shareLabel:share.rdShareReady?'Compartir lote':'Preparando imagenâ¦', 28735 open: () => this.openRemateLot(l), hoverIn, hoverOut, 28736 thumb: l.thumbnailUrl || l.thumbnail || l.imagen || (isActiveRosgan ? '' : 'assets/angus-field.png'), 28737 thumbnailPending: isActiveRosgan && !(l.thumbnailUrl || l.thumbnail || l.imagen), 28738 thumbSize: 'cover', 28739 thumbBg: '#e4edf5', 28740 videoPreview: l.videoUrl || l.video_uri || '', 28741 number: lotNum(l), numberShort: lotNumber(l) ?? '', showNumber: !!lotNum(l), 28742 cert: cert || 's/c', categoria: l.nombre_categoria || raza || 'Lote', 28743 raza, showRaza: !!raza, localidad, cabezas: cantidad || '', showCabezas: cantidad > 0, 28744 peso: pesoPromedio ?pesoPromedio + ' kg' : '', showPeso: pesoPromedio > 0, 28745 listMeta: [cert ?`Cert. ${cert}` : '', raza, localidad].filter(hasLotValue).join(' · '), 28746 facts: factsOf(l), 28747 consignataria, consignatariaLogo: logo, hasConsignatariaLogo: !!logo, noConsignatariaLogo: !logo, 28748 consignatariaLogoBg: whiteLogo ?this.consBrandColor(consignataria) : (logo ?'#fff' : this.consBrandColor(consignataria)), 28749 consignatariaInitial: String(consignataria).trim().charAt(0).toUpperCase() || 'R', 28750 showAlert: isActiveRosgan, 28751 toggleAlert: (event) => this.toggleRemateLotAlert(rm, l, event), 28752 alertBusy, 28753 alertDisabled: alertBusy || alertSent, 28754 alertAriaPressed: alertEnabled ?'true' : 'false', 28755 alertLabel: alertBusy ?'Guardandoâ¦' : alertEnabled ?'Aviso activado' : alertSent ?'Aviso enviado' : 'Avisame cuando sale', 28756 alertIcon: 'assets/icons/lucide/' + (alertEnabled || alertSent ?'bell-ring' : 'bell') + '.svg', 28757 alertBg: alertEnabled ?'#e7f2ff' : alertSent ?'#eef2f6' : '#fff', 28758 alertColor: alertEnabled ?'#075da8' : alertSent ?'#718196' : '#003f78', 28759 alertBorder: alertEnabled ?'#75b8ee' : alertSent ?'#d8e0e8' : '#b8cfe2', 28760 }; 28761 }), 28762 rdLightbox: false, rdLotPage: !!lb, rdCatalogPage: !lb, 28763 lbHasPreviousLot: !!lbNeighbors.previous, lbHasNextLot: !!lbNeighbors.next, 28764 lbPreviousLot: () => { if (lbNeighbors.previous) this.openRemateLot(lbNeighbors.previous); }, 28765 lbNextLot: () => { if (lbNeighbors.next) this.openRemateLot(lbNeighbors.next); }, 28766 ...(() => {if(!lb)return {};const share=window.RosganAuctionShare.prepare({kind:'lot',brandLogo:lbConsignatariaLogo,brandColor:lbConsignatariaColor,slogan:window.rosganConsignatariaSlogan?.(lbConsignataria)||'',title:lb.nombre_categoria||lb.raza||'Lote',cert:lb.nro_certificado,breed:lb.raza,heads:heads(lb),weight:wt(lb),location:loc(lb),consignataria:lbConsignataria,auction:rm.nombre_remate||rm.titulo||'Remate Rosgan',details:{maleQuantity:lb.cantidad_machos,femaleQuantity:lb.cantidad_hembras,minWeight:lb.peso_minimo,maxWeight:lb.peso_maximo,formType:lb.formType||(lb.faena?'faena':lb.vientres?'vientres':''),isVientres:!!lb.vientres,age:lbAge,dentition:lb.denticion,reproductiveStatus:lb.estado_reproductivo,service:lb.servicio,pregnancy:lb.prenez,calving:lb.paricion,terms:lb.plazo,destination:lb.destino,condition:lb.faena_condicion,delivery:lb.faena_entrega},image:lb.thumbnailUrl||lb.thumbnail||lb.imagen||'',video:lbPrimaryVideoUrl,path:'/remates/'+encodeURIComponent(remateId)+'/'+encodeURIComponent(lb.nro_certificado)},()=>this.setState({lotShareReady:Date.now()}));return {shareCurrentLot:share.shareCurrentRemate,lbShareReady:share.rdShareReady,lbShareLabel:share.rdShareReady?'Compartir lote':'Preparando imagenâ¦'};})(), 28767 lbThumb: lb ?(lb.thumbnailUrl || lb.thumbnail || lb.imagen || 'assets/angus-field.png') : '', 28768 lbHasNumber: !!lb && !!lotNum(lb),
28769 lbNumber: lb ?lotNum(lb) : '', lbCert: lb?.nro_certificado || 's/c', lbCategoria: lb?.nombre_categoria || lb?.raza || 'Lote', lbRaza: lb?.raza || 'Sin raza', 28770 lbBreedIcons, lbHasBreedIcons: lbBreedIcons.length > 0, 28771 lbFacts: lb ?factsOf(lb) : [], 28772 lbLocation: lb ?loc(lb) : '', lbDetailGroups, 28773 lbShowVisec: !!lb && this.rosganTvIsAptoVisec(lb), lbShowChina: !!lb && this.rosganTvIsAptoChina(lb), 28774 rdLotDetailCols: s.compactNavigation ?'minmax(0,1fr)' : 'minmax(0,1.55fr) minmax(280px,.72fr)', rdLotAsidePosition: s.compactNavigation ?'static' : 'sticky', 28775 lbPrimaryVideoUrl, lbNoPrimaryVideo: !lbPrimaryVideoUrl, 28776 lbVideoPlay: (event) => this.syncCabanaVideoState(event?.currentTarget, true), 28777 lbVideoPause: (event) => this.syncCabanaVideoState(event?.currentTarget, true), 28778 lbVideoMeta: (event) => this.syncCabanaVideoState(event?.currentTarget, true), 28779 lbVideoTimeUpdate: (event) => this.syncCabanaVideoState(event?.currentTarget, false), 28780 lbVideoVolumeChange: (event) => { const video=event?.currentTarget; if(video){video.muted=true;video.defaultMuted=true;} this.syncCabanaVideoState(video, true); }, 28781 lbVideoEnded: (event) => { const video = event?.currentTarget; if (video) { video.currentTime = 0; video.play?.().catch(() => {}); this.syncCabanaVideoState(video, true); } }, 28782 lbTogglePlay: (event) => this.toggleCabanaVideoPlayback(event), lbSeekVideo: (event) => this.seekCabanaVideo(event), 28783 lbToggleMute: (event) => this.toggleCabanaVideoMute(event), lbSetVolume: (event) => this.setCabanaVideoVolume(event), 28784 lbFullscreen: (event) => this.openCabanaVideoFullscreen(event), 28785 lbPlayIcon: 'assets/icons/lucide/' + (s.cabanaLotVideoPlaying ?'pause' : 'play') + '.svg', 28786 lbPlayLabel: s.cabanaLotVideoPlaying ?'Pausar video' : 'Reproducir video', 28787 lbVolumeIcon: 'assets/icons/lucide/' + (s.cabanaLotVideoMuted ?'volume-x' : 'volume-2') + '.svg', 28788 lbMuteLabel: s.cabanaLotVideoMuted ?'Activar sonido' : 'Silenciar video', 28789 lbVideoProgress: Math.round(Number(s.cabanaLotVideoProgress || 0) * 10) / 10, 28790 lbVideoVolume: Math.round(Number(s.cabanaLotVideoVolume || 0) * 100) / 100, 28791 lbVideoTime: this.formatCabanaVideoTime((Number(s.cabanaLotVideoProgress || 0) / 100) * Number(s.cabanaLotVideoDuration || 0)) + ' / ' + this.formatCabanaVideoTime(s.cabanaLotVideoDuration), 28792 lbHasObservations: !!lbObservations, lbObservations, 28793 lbConsignataria, lbConsignatariaLogo, lbHasConsignatariaLogo: !!lbConsignatariaLogo, lbNoConsignatariaLogo: !lbConsignatariaLogo, 28794 lbConsignatariaInitial: lbConsignataria.trim().charAt(0).toUpperCase(), lbConsignatariaColor, lbConsignatariaShadow: lbConsignatariaColor + '35', 28795 lbHasRepresentative: !!lbRepresentative, 28796 lbRepresentativeName: lbRepresentative?.name || '', lbRepresentativePhone: lbRepresentative?.phone || '', 28797 lbRepresentativeHasPhone: !!lbRepresentative?.hasPhone, lbRepresentativeHasWhatsapp: !!lbRepresentative?.hasWhatsapp, 28798 lbRepresentativeHasContact: !!lbRepresentative && !lbRepresentative.noContact, lbRepresentativeNoContact: !!lbRepresentative?.noContact, 28799 lbRepresentativePhoneHref: lbRepresentative?.phoneHref || '', lbRepresentativeWhatsappHref: lbRepresentative?.whatsappHref || '', 28800 lbRepresentativePhotoClass: lbRepresentativeAvatar.photo ? 'dc-representative-photo' : '', 28801 lbRepresentativeAvatar: lbRepresentativeAvatar.avatar, lbRepresentativeHasAvatar: lbRepresentativeAvatar.hasAvatar, 28802 lbRepresentativeAvatarBg: lbRepresentativeAvatar.bg, lbRepresentativeAvatarSize: lbRepresentativeAvatar.photo ? '100%' : '30px', lbRepresentativeAvatarFit: lbRepresentativeAvatar.fit, 28803 lbRepresentativeAvatarError: () => { if (lbRepresentativeAvatar.photo && lbRepresentative?.uid) this.setState({ rosganRepresentativePhotoFailed: { ...this.state.rosganRepresentativePhotoFailed, [lbRepresentative.uid]: true } }); }, 28804 lbRosganVideoManager: isActiveRosgan && !!lb, 28805 lbCanManageVideos: canManageVideos, 28806 lbCanEditRosganLot: isActiveRosgan && this.canEditRosganLot(lb), editRosganLot: () => this.openRosganLotEditor(lb), 28807 uploadRosganLotVideos: (event) => { const files = event.target.files; this.uploadRosganLotVideos(files); event.target.value = ''; }, 28808 prepareFieldLot: () => this.prepareFieldLot(), 28809 lbVideoTitle: (videoState.videos || []).length ?(videoState.videos || []).length + ((videoState.videos || []).length === 1 ?' video cargado' : ' videos cargados') : 'Material audiovisual', 28810 lbVideoLoading: !!videoState.loading, 28811 lbVideoHasError: !!videoState.error, 28812 lbVideoError: videoState.error || '', 28813 lbVideoUploadBusy: !!videoState.uploading, 28814 lbVideoHasUploads: !!(videoState.uploads || []).length, 28815 lbVideoUploads: (videoState.uploads || []).map((upload) => ({ 28816 name: upload.name || 'Video', progress: Math.max(0, Math.min(100, Number(upload.progress) || 0)), size: this.formatFileSize(upload.sizeBytes), 28817 status: upload.status || 'En espera', color: upload.status === 'Error' ?'#b42332' : upload.status === 'Listo' ?'#16846e' : '#0a5ca8', 28818 bar: upload.status === 'Error' ?'#d94a58' : upload.status === 'Listo' ?'#16846e' : '#0a5ca8', 28819 error: upload.error || '', hasError: !!upload.error, canRetry: !!upload.canRetry, retryLabel: upload.retryLabel, retry: () => this.retryRosganVideoUpload(upload.id), 28820 })), 28821 lbVideoHasItems: !videoState.loading && !!(videoState.videos || []).length, 28822 lbVideoEmpty: !videoState.loading && !(videoState.videos || []).length && !(videoState.uploads || []).some((upload) => upload.status !== 'Error'), 28823 lbVideos: (videoState.videos || []).map((video, index) => { 28824 const id = String(video?.id || video?.videoId || video?.videoID || ''); 28825 const url = video?.url || video?.publicUrl || video?.videoUrl || video?.downloadUrl || video?.signedUrl || ''; 28826 const name = video?.fileName || video?.name || video?.title || ('Video ' + (index + 1)); 28827 return { 28828 id, url, hasUrl: !!url, noUrl: !url, name, size: this.formatFileSize(video?.sizeBytes || video?.fileSize || video?.size), 28829 canDelete: canManageVideos && !!id && String(video?.videoType || video?.type || '').toLowerCase() === 'raw', deleting: s.rosganLotVideoDeleting === videoDeletingPrefix + id, 28830 remove: () => this.deleteRosganLotVideo(video), 28831 }; 28832 }), 28833 closeLightbox: () => this.closeRemateLot(), stopProp: (e) => e.stopPropagation(), 28834 rgOrderCommandApply:()=>this.applyRosganOrderCommand(), 28835 rgOrderCommandEnter:event=>{if(event.key==='Enter'&&!event.isComposing&&!event.repeat){event.preventDefault();this.applyRosganOrderCommand();}}, 28836 rgOrderCommandMessage:s.rosganOrderCommandMessage||'', 28837 rgCanToggleLotLoading: this.canManageUsers(s.apiUser), 28838 rgLotLoadingEnabled: rm.cargaDeLotes !== false, 28839 rgLotLoadingBusy: !!s.remateLotLoadingBusy, 28840 rgLotLoadingLabel: s.remateLotLoadingBusy ? 'Guardandoâ¦' : rm.cargaDeLotes !== false ? 'Carga de lotes abierta' : 'Carga de lotes cerrada', 28841 toggleRgLotLoading: () => this.setRemateLotLoading(rm, rm.cargaDeLotes === false).catch(e => this.showToast(e.message)), 28842 rgSalesOrderTitle: rm.nombre_remate || 'Remate Rosgan', 28843 rgSalesOrderCount: rosganOrderLots.length, 28844 rgSalesOrderUnratedCount: rosganOrderLots.filter(lot => !(Number(s.rosganSalesOrderRatings?.[this.remateLotRouteId(lot)]) > 0)).length, 28845 rgSalesOrderVisibleCount: rosganOrderRows.length, 28846 rgSalesOrderRows: rosganOrderRows, 28847 rgSalesOrderLoading: !!s.remateLotsLoading, 28848 rgSalesOrderEmpty: !s.remateLotsLoading && rosganOrderLots.length === 0, 28849 rgSalesOrderNoResults: !s.remateLotsLoading && rosganOrderLots.length > 0 && rosganOrderRows.length === 0, 28850 rgSalesOrderRatingFilter: orderRatingFilter, 28851 setRgSalesOrderRatingFilter: event => this.setState({ rosganSalesOrderRatingFilter: event.target.value, rosganSalesOrderRatingSnapshot: { ...s.rosganSalesOrderRatings } }), 28852 rgSalesOrderQuery: s.rosganSalesOrderQuery || '', 28853 setRgSalesOrderQuery: (event) => this.setState({ rosganSalesOrderQuery: event.target.value }), 28854 rgSalesOrderStatusFilter: orderStatusFilter, 28855 setRgSalesOrderStatusFilter: (event) => this.setState({ rosganSalesOrderStatusFilter: event.target.value }), 28856 rgSalesOrderCategoryFilter: orderCategoryFilter, 28857 setRgSalesOrderCategoryFilter: (event) => this.setState({ rosganSalesOrderCategoryFilter: event.target.value }), 28858 rgSalesOrderConsignatariaFilter: orderConsignatariaFilter, 28859 setRgSalesOrderConsignatariaFilter: (event) => this.setState({ rosganSalesOrderConsignatariaFilter: event.target.value }), 28860 rgSalesOrderStatusOptions: [{ value: '', label: 'Todos los estados' }, ...Array.from(new Set(rosganOrderLots.map((lot) => this.rosganLotWorkflowStatus(lot)))).sort().map((status) => ({ value: status, label: orderStatusLabels[status] || status }))], 28861 rgSalesOrderCategoryOptions: [{ value: '', label: 'Todas las categorÃas' }, ...Array.from(new Set(rosganOrderLots.map(orderCategoryOf))).sort((a, b) => a.localeCompare(b, 'es')).map((value) => ({ value, label: value }))], 28862 rgSalesOrderConsignatariaOptions: [{ value: '', label: 'Todas las consignatarias' }, ...Array.from(new Set(rosganOrderLots.map(orderConsignatariaOf))).sort((a, b) => a.localeCompare(b, 'es')).map((value) => ({ value, label: value }))], 28863 rgSalesOrderHasError: !!s.rosganSalesOrderError, 28864 rgSalesOrderError: s.rosganSalesOrderError || '', 28865 rgSalesOrderSaved: !!s.rosganSalesOrderSaved, 28866 rgSalesOrderSaveLabel: s.rosganSalesOrderSaving ?'Guardandoâ¦' : 'Guardar orden', 28867 rgCatalogPublished: s.catalogPublication?.key === this.catalogPublicationKey() && s.catalogPublication?.published === true, 28868 rgCatalogBusy: !s.catalogPublication?.loaded || !!s.catalogPublication?.busy, 28869 rgCatalogOpacity: !s.catalogPublication?.loaded || s.catalogPublication?.busy ? '.55' : '1', 28870 rgCatalogSwitchColor: s.catalogPublication?.published ? '#008D46' : '#aab9c9', 28871 rgCatalogSwitchOffset: s.catalogPublication?.published ? 14 : 0, 28872 toggleRgCatalog: () => this.toggleCatalogPublication(), 28873 rgSalesOrderSaveOpacity: s.rosganSalesOrderSaving ?'.62' : '1', 28874 rgPreorderDisabled: !!s.rosganPreorderExporting || !!s.remateLotsLoading || !rosganOrderLots.length, 28875 rgPreorderLabel: s.rosganPreorderExporting ? 'Generando preordenâ¦' : 'Obtener preorden', 28876 exportRgPreorderExcel: () => this.exportRosganPreorderExcel(), 28877 rgSalesOrderImportLabel: s.rosganSalesOrderImporting ?'Leyendo Excelâ¦' : 'Cargar Excel', 28878 rgSalesOrderImportClass: s.rosganSalesOrderImporting ?'is-busy' : '', 28879 importRgSalesOrderExcel: (event) => this.importRosganSalesOrderExcel(event), 28880 rgSalesOrderHasImport: orderSchedule.startSeconds != null && orderSchedule.entries.length > 0, 28881 rgSalesOrderImportFile: orderSchedule.fileName || 'Orden importado desde Excel', 28882 rgSalesOrderImportStart: window.RosganSalesOrderImport.clock(orderSchedule.startSeconds), 28883 rgSalesOrderImportEnd: window.RosganSalesOrderImport.clock(orderSchedule.endSeconds), 28884 rgSalesOrderImportGroups: new Set(orderSchedule.entries.map((entry) =>
28884 entry.alias)).size, 28885 closeRgSalesOrder: () => this.closeRosganSalesOrder(), 28886 uploadRgCatalogCover: async event => { 28887 const file = event.target.files?.[0]; event.target.value = ''; 28888 if (!file || !this.canManageUsers(this.state.apiUser)) return; 28889 try { await window.RosganCatalogCover.preview(file,{remateId:this.remateRouteId(this.state.currentRemate),apiFetch:(url,options)=>this.apiFetch(url,options),onSaved:()=>this.setState({rosganOrderCommandMessage:'Tapa del catálogo guardada.'})}); } 28890 catch(error) { this.setState({rosganSalesOrderError:error.message || 'No se pudo cargar la tapa.'}); } 28891 }, 28892 configureRgCatalogNews: () => window.RosganCatalogCover.news({remateId:this.remateRouteId(this.state.currentRemate),apiFetch:(url,options)=>this.apiFetch(url,options)}), 28893 uploadRgCatalogBackCover: async event => { 28894 const file = event.target.files?.[0]; event.target.value = ''; 28895 if (!file || !this.canManageUsers(this.state.apiUser)) return; 28896 try { await window.RosganCatalogCover.preview(file,{backCover:true,remateId:this.remateRouteId(this.state.currentRemate),apiFetch:(url,options)=>this.apiFetch(url,options),onSaved:()=>this.setState({rosganOrderCommandMessage:'Contratapa del catálogo guardada.'})}); } 28897 catch(error) { this.setState({rosganSalesOrderError:error.message || 'No se pudo cargar la contratapa.'}); } 28898 }, 28899 saveRgSalesOrder: () => this.saveRosganSalesOrder(), 28900 }; 28901 })(), 28902 ...(() => { 28903 const remate = s.currentRemate || {}; 28904 const lots = (Array.isArray(s.remateLots) ?s.remateLots : []).slice().sort((a, b) => this.compareRosganSalesOrder(a,b)); 28905 const state = this.normalizeRosganOpsState({ commercial: s.rosganOpsDrafts, tv: s.rosganOpsConfig }, remate); 28906 const money = (value) => { 28907 const number = Number(String(value ?? '').replace(/[^0-9,.-]/g, '').replace(',', '.')) || 0; 28908 return number ?'$ ' + Math.round(number).toLocaleString('es-AR') : 'â'; 28909 }; 28910 const numberText = (value) => { 28911 const number = Number(value) || 0; 28912 return number ?number.toLocaleString('es-AR') : 'â'; 28913 }; 28914 const priceOf = (draft = {}) => Number(draft.priceMode === 'kilo' ?draft.precioKilo : draft.precioCabeza) || 0; 28915 const rowFor = (lot) => { 28916 const cert = this.remateLotRouteId(lot); 28917 const draft = {...state.commercial[cert],...this._rosganPriceEdits?.get(this.remateRouteId(remate)+'/'+cert)?.patch}; 28918 const status = String(draft.status || 'pendiente').toLowerCase(); 28919 const consignataria = this.consDisplayName(lot.nombre_consignatario || remate.nombre_cuenta || remate.nombre_consignatario) || 'Rosgan'; 28920 const temporaryTvConsignataria = this.rosganTvTemporaryConsignataria(remate, lot); 28921 const priceMode = window.RosganPriceInput.mode(lot,draft); 28922 const quantity = Number(lot.cantidad) || 0; 28923 const weight = Number(lot.peso_promedio) || 0; 28924 const locality = [lot.nombre_localidad, lot.nombre_provincia].filter(Boolean).join(', '); 28925 const plazo = lot.plazo || ''; 28926 return { 28927 cert, 28928 saleOrder: lot.saleOrder ?? lot.lotNumber ?? lot.orden_venta ?? lot.orden_final, 28929 lotNumber: lot.lotNumber ?? lot.orden_final ?? lot.orden_venta, 28930 order: String(lot.saleOrder ?? lot.orden_final ?? lot.orden_venta ?? ''), 28931 lotLabel: lot.saleOrder || lot.orden_final || lot.orden_venta ?'Lote ' + String(lot.saleOrder || lot.orden_final || lot.orden_venta) : '', 28932 category: lot.nombre_categoria || 'Sin categorÃa', 28933 formType: lot.formType || lot.tipo || '', 28934 condition: lot.faena_condicion || '', 28935 subcategory: lot.faena_subcategoria || '', 28936 breed: lot.raza || '', 28937 buyerValue: draft.divisions?.length?'Por división':draft.comprador || '', 28938 setBuyer: event => this.editRosganCommercialBuyer(lot,event), 28939 hasDivisions:!!draft.divisions?.length, 28940 divisionLabel:draft.divisions?.length?draft.divisions.length+' partes · '+draft.divisions.reduce((sum,p)=>sum+(Number(p.price)>0?Number(p.quantity):0),0)+' / '+quantity+' vendidas':'Dividir venta', 28941 openDivisions:()=>this.openRosganLotDivisions(lot), 28942 consignataria, 28943 tvConsignataria: temporaryTvConsignataria?.name || '', 28944 tvConsignatariaLogo: temporaryTvConsignataria?.logo || '', 28945 tvConsignatariaColor: temporaryTvConsignataria?.color || '', 28946 quantity, 28947 weight, 28948 plazo, 28949 locality, 28950 dataSummary: [quantity ?quantity + ' cab.' : '', weight ?weight + ' kg' : ''].filter(
28950Boolean).join(' · ') || 'Sin datos de cantidad y peso', 28951 detailSummary: [locality, plazo].filter(Boolean).join(' · '), 28952 status, 28953 statusLabel: status === 'vendido' ?'Vendido' : status === 'levantado' ?'Levantado' : status === 'no-vendido' ?'No vendido' : draft.divisions?.some(p=>Number(p.price)>0)?'Venta parcial':'Pendiente', 28954 statusClass: 'rosgan-ops-status is-' + status, 28955 priceMode, 28956 priceValue: draft.divisions?.length?'Ver divisiones':window.RosganPriceInput.format(priceMode === 'kilo' ?(draft.precioKilo ?? '') : (draft.precioCabeza ?? '')), 28957 priceDisplay: money(priceOf(draft)), 28958 modeLabel: priceMode === 'kilo' ?'$ / kg' : '$ / cabeza', 28959 setPending: () => this.updateRosganCommercial(lot, { status: 'pendiente' }), 28960 setSold: () => this.updateRosganCommercial(lot, { status: 'vendido' }), 28961 setUnsold: () => this.updateRosganCommercial(lot, { status: 'no-vendido' }), 28962 setLifted: () => this.updateRosganCommercial(lot, { status: 'levantado' }), 28963 setKilo: () => this.updateRosganCommercial(lot, { priceMode: 'kilo' }), 28964 setHead: () => this.updateRosganCommercial(lot, { priceMode: 'cabeza' }), 28965 setPrice: (event) => this.editRosganCommercialPrice(lot,priceMode,event), 28966 commitPrice:()=>this.flushRosganCommercialPrice(this.remateRouteId(remate)+'/'+cert), 28967 commitPriceEnter:event=>this.commitRosganCommercialPrice(lot,priceMode,event), 28968 kiloClass: priceMode === 'kilo' ?'is-active' : '', 28969 headClass: priceMode === 'cabeza' ?'is-active' : '', 28970 }; 28971 }; 28972 const rows = lots.map(rowFor); 28973 const airtime=window.RosganAirtime.summarize(rows,s.rosganAirtime?.remateId===String(this.remateRouteId(remate))?s.rosganAirtime:null,Date.now(),remate.closeRemate===true); 28974 const progress=window.RosganCommercialProgress.progress(rows); 28975 const commercialFilter = s.rosganCommercialFilter || 'todos'; 28976 const commercialQuery = this.consKey(s.rosganCommercialQuery || ''); 28977 const commercialRows = rows.filter(row => (commercialFilter === 'todos' || row.status === commercialFilter) && this.consKey([row.order,row.cert,row.category,row.breed,row.consignataria,row.locality].join(' ')).includes(commercialQuery)); 28978 const soldRows = rows.flatMap(row=>window.RosganSaleDivisions.sales({cantidad:row.quantity},state.commercial[row.cert]||{},row.priceMode).map(part=>({...row,quantity:part.quantity,salePrice:part.price}))); 28979 const priceCategoryOrder = ['Terneros','Novillitos','Novillos','Terneros/as','Terneras','Vaquillonas','Vacas de invernada','Terneros Holando','Novillos Holando','Vaq. c/gtÃa. de preñez','Vacas c/gtÃa. de preñez','Vacas con crÃa al pie']; 28980 const priceCategoryFor = row => String(row.formType).toLowerCase() === 'faena' ? [String(row.category).replace(/\s*\([^)]*\)$/, ''),row.subcategory].filter(Boolean).join(' ') : this.rosganPriceCategory(row.category,row.breed); 28981 const localPriceGroups = window.RosganPriceBoard.summarize(soldRows,priceCategoryFor) 28982 .map(group=>({...group,max:money(group.maxValue),average:group.avgValue==null?'â':money(group.avgValue)})) 28983 .sort((a,b)=>priceCategoryOrder.indexOf(a.category)-priceCategoryOrder.indexOf(b.category)||a.unit.localeCompare(b.unit)); 28984 const officialPriceState=s.rosganOfficialPrices?.[this.remateRouteId(remate)]||{}; 28985 const priceGroups=window.RosganPriceBoard.merge(officialPriceState.groups?.length?officialPriceState.groups:localPriceGroups) 28986 .map(group=>({...group,max:money(group.maxValue),average:group.avgValue==null?'â':money(group.avgValue)})); 28987 const priceStoryGroups = priceGroups; 28988 const priceBrands = [...new Set(rows.map(row=>row.consignataria).filter(Boolean))].map(name=>({name,logo:this.consWhiteLogo(name)||'',color:this.consBrandColor(name)})); 28989 const priceBoardModel = {title:remate.nombre_remate||remate.titulo||'Remate Rosgan', 28990 date:this.remateDate(remate)?.toLocaleDateString('es-AR',{weekday:'long',day:'numeric',month:'long',year:'numeric',timeZone:'America/Argentina/Buenos_Aires'})||'', 28991 location:remate.ubicacion_remate||remate.lugar||'', heads:rows.reduce((n,row)=>n+row.quantity,0), brands:priceBrands, 28992 logo:'/assets/rosgan-logo-blue.png',favicon:'/favicon.png',background:'/assets/lacassina-story-background.png', 28993 mode:s.rosganPriceBoardMode||'category',groups:priceGroups,tables:window.RosganPriceBoard.weightTables(soldRows,priceCategoryFor)}; 28994 const selectedCert = this.rosganTvSelectedCert(state.tv, lots); 28995 const tvSlots = this.rosganTvSlots(state.tv, lots); 28996 const onAirCert = s.route === 'rosganTvLower' ?this._rosganTvDecks?.onAirCert : ''; 28997 const selectedLot = lots.find((lot) => this.remateLotRouteId(lot) === (onAirCert || selectedCert)) || lots[0] || null; 28998 const selected = s.route === 'rosganTvLower' && selectedLot ?this.rosganTvPreviewLot(selectedLot) : selectedLot; 28999 const extraFields = this.rosganTvExtraFields(selected); 29000 const extraIndex = (s.rosganTvFieldStep || 0) % Math.max(1, extraFields.length); 29001 const tvObservations = String(selected?.observaciones_certificado || selected?.observaciones || selected?.observations || '').replace(/\s+/g, ' ').trim(); 29002 const selectedRow = selected ?rowFor(selected) : null; 29003 const temporarySelectedConsignataria = this.rosganTvTemporaryConsignataria(remate, selected); 29004 const selectedConsignataria = temporarySelectedConsignataria?.name || selectedRow?.consignataria || this.consDisplayName(remate.nombre_cuenta || remate.nombre_consignatario) || 'Rosgan'; 29005 const consContactKey = this.consKey(selectedConsignataria); 29006 const consRecord = (s.consignatarias || []).find((item) => this.consKey(item.name || item.nombre) === consContactKey); 29007 const rawConsContacts = consRecord?.telefonos || consRecord?.phones || consRecord?.telefono || consRecord?.phone || ''; 29008 const consContacts = state.tv.contactsByConsignataria?.[consContactKey] ?? (Array.isArray(rawConsContacts) ?rawConsContacts.map((v) => typeof v === 'string' ?v : v.phone || v.telefono || '').filter(
29008Boolean).join(' | ') : String(rawConsContacts)); 29009 const editorEntry = this._rosganTvEdits?.[this.remateRouteId(remate) + '/' + selectedCert]; 29010 const consWhiteLogo = temporarySelectedConsignataria?.logo || this.consWhiteLogo(selectedConsignataria); 29011 const consLogo = consWhiteLogo || this.consLogo(selectedConsignataria); 29012 const videoUrl = selected?.videoUrl || selected?.video_uri || ''; 29013 const poster = selected?.thumbnailUrl || selected?.thumbnail || selected?.imagen || 'assets/angus-field.png'; 29014 const conditions = [selected?.garrapata ?('Garrapata: ' + selected.garrapata) : '', selected?.plazo ?('Plazo: ' + selected.plazo) : ''].filter(Boolean).join(' | '); 29015 return { 29016 rosCanCloseRemate: this.canManageUsers(s.apiUser) && !!s.currentRemate?.__rosganActive, rosRemateClosingBusy: !!s.remateClosingBusy, rosRemateClosedLabel: s.currentRemate?.closeRemate ?'Reabrir remate':'Cerrar remate', rosToggleRemateClosed: () => this.setRosganRemateClosed(s.currentRemate, !s.currentRemate?.closeRemate), 29017 rosOpsTitle: remate.nombre_remate || remate.titulo || 'Remate Rosgan', 29018 rosOpsLoading: !!s.rosganOpsLoading || !!s.remateLotsLoading, 29019 rosOpsRows: rows, 29020 rosAirtimeResetAll:()=>{if(window.confirm('¿Reiniciar todos los tiempos de este remate? Los lotes al aire seguirán contando desde cero.'))this.updateRosganAirtime({action:'reset-all'});}, 29021 rosProgressSegments:progress, 29022 rosProgressTotal:rows.length?rows.length+' lotes · 100%':'Sin lotes', 29023 rosProgressDescription:progress.map(item=>item.label+': '+item.percent+' ('+item.count+' lotes)').join(', '), 29024 rosAirtimeStatus:airtime.status, 29025 rosAirtimeGroups:airtime.groups.map(group=>{const logo=this.consWhiteLogo(group.name)||this.consLogo(group.name);return {...group,logo,hasLogo:!!logo,noLogo:!logo,initial:group.name.charAt(0),color:this.consBrandColor(group.name)};}), 29026 rosAirtimeMetrics:[{label:'Tiempo acumulado de lotes',value:airtime.total},{label:'Promedio por lote',value:airtime.average},{label:'Lotes cronometrados',value:airtime.count},{label:airtime.finished?'Remate finalizado':'Fin estimado · '+airtime.pending+' pendientes',value:airtime.eta}], 29027 rosExportAirtime:async()=>{ 29028 if(this._rosganAirtimeExportBusy)return; 29029 this._rosganAirtimeExportBusy=true; 29030 try{ 29031 const id=String(this.remateRouteId(remate)); 29032 const result=await this.apiFetch('/api/remate-operations/'+encodeURIComponent(id)+'/airtime'); 29033 if(!result?.data?.totals)throw new Error('No se pudieron obtener los tiempos registrados.'); 29034 const snapshot={...result.data,receivedAt:Date.now()}; 29035 const capturedAt=Date.now(); 29036 const meta={id,title:remate.nombre_remate||remate.titulo,closed:remate.closeRemate===true,date:this.remateDate(remate)?.toLocaleDateString('es-AR'),colors:Object.fromEntries([...new Set(rows.map(row=>row.consignataria))].map(name=>[name,this.consBrandColor(name)]))}; 29037 const buffer=await window.RosganAirtime.excel(rows,snapshot,meta,await this.ensureCabanaExcelJs(),capturedAt); 29038 this.downloadCabanaBlob(new Blob([buffer],{type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}),'tiempos-remate-'+id+'.xlsx'); 29039 this.showToast('Informe ejecutivo de tiempos descargado.'); 29040 }catch(error){this.showToast(error.message||'No se pudieron exportar los tiempos.');} 29041 finally{this._rosganAirtimeExportBusy=false;} 29042 }, 29043 rosCommercialRows: s.route === 'rosganCommercial' ?commercialRows.map(row => { 29044 const logo=this.consWhiteLogo(row.consignataria)||this.consLogo(row.consignataria); 29045 return {...row, 29046 interestLabel:s.rosganLotInterest?.remateId===String(this.remateRouteId(remate))?((s.rosganLotInterest.counts[row.cert]||0)+' '+((s.rosganLotInterest.counts[row.cert]||0)===1?'interesado':'interesados')):'Interesados: â', 29047 editingTime:s.rosganTimeEdit?.id===this.remateRouteId(remate)&&s.rosganTimeEdit?.cert===row.cert, 29048 timeDraft:s.rosganTimeEdit?.value||'', 29049 editTime:()=>this.setState({rosganTimeEdit:{id:this.remateRouteId(remate),cert:row.cert,value:window.RosganAirtime.duration(airtime.totals[row.cert])}}), 29050 changeTime:event=>this.setState({rosganTimeEdit:{...this.state.rosganTimeEdit,value:event.target.value}}), 29051 saveTime:()=>this.saveRosganManualTime(row.cert),cancelTime:()=>this.setState({rosganTimeEdit:null}), 29052 resetTime:()=>this.updateRosganAirtime({action:'set',cert:row.cert,milliseconds:0}), 29053 airtimeLabel:window.RosganAirtime.duration(airtime.totals[row.cert]),airtimeState:airtime.running.includes(row.cert)?'â Al aire':'Tiempo al aire',consLogo:logo,hasConsLogo:!!logo,noConsLogo:!logo,consInitial:row.
29053consignataria.charAt(0),consColor:this.consBrandColor(row.consignataria), 29054 commercialClass:row.status==='vendido'?'is-sold':row.status==='levantado'?'is-lifted':'', 29055 pendingClass:row.status==='pendiente'?'is-pendiente':'',soldClass:row.status==='vendido'?'is-vendido':'',liftedClass:row.status==='levantado'?'is-levantado':'', 29056 unsoldClass:row.status==='no-vendido'?'is-no-vendido':'',isUnsold:row.status==='no-vendido', 29057 isPending:row.status==='pendiente',isSold:row.status==='vendido',isLifted:row.status==='levantado',priceLabel:'Precio del lote '+row.order+' · '+row.modeLabel}; 29058 }) :[], 29059 rosCommercialResultCount: commercialRows.length, 29060 rosCommercialEmpty: !s.remateLotsLoading && !s.rosganOpsLoading && commercialRows.length === 0, 29061 rosCommercialQuery: s.rosganCommercialQuery || '', 29062 rosCommercialSearch: event => this.setState({rosganCommercialQuery:event.target.value}), 29063 rosCommercialFilters: [['todos','Todos'],['pendiente','Pendientes'],['vendido','Vendidos'],['no-vendido','No vendidos'],['levantado','Levantados']].map(([key,label]) => ({label,count:key==='todos'?rows.length:rows.filter(row=>row.status===key).length,active:commercialFilter===key,className:'commercial-filter'+(commercialFilter===key?' is-active':''),select:()=>this.setState({rosganCommercialFilter:key})})), 29064 rosCommercialMetrics: [ 29065 {label:'Lotes',value:rows.length,icon:'list-ordered',bg:'#e8f2fc'}, 29066 {label:'Cabezas',value:numberText(lots.reduce((sum,lot)=>sum+(Number(lot.cantidad)||0),0)),icon:'layers-2',bg:'#edf2fa'}, 29067 {label:'Vendidos',value:rows.filter(row=>row.status==='vendido').length,icon:'circle-check-big',bg:'#e7f5ee'}, 29068 {label:'Pendientes / levantados',value:rows.filter(row=>row.status==='pendiente').length+' / '+rows.filter(row=>row.status==='levantado').length,icon:'clock-3',bg:'#fff6df'}, 29069 ].map(metric=>({...metric,icon:'assets/icons/lucide/'+metric.icon+'.svg'})), 29070 rosOpsLotCount: rows.length, 29071 rosOpsHasRows: rows.length > 0, 29072 rosOpsEmpty: !s.remateLotsLoading && rows.length === 0, 29073 rosOpsSoldCount: rows.filter(row=>row.status==='vendido').length, 29074 rosOpsPendingCount: rows.filter((row) => row.status === 'pendiente').length, 29075 rosOpsLiftedCount: rows.filter((row) => row.status === 'levantado').length, 29076 rosOpsTotalHeads: numberText(lots.reduce((sum, lot) => sum + (Number(lot.cantidad) || 0), 0)), 29077 rosOpsPriceGroups: priceGroups, 29078 rosPriceSource:s.rosganPriceBoardMode==='weight'?'Rangos calculados desde ventas con peso · mesa comercial':officialPriceState.groups?.length?'Resultados oficiales · RosganNet'+(officialPriceState.rosganNetRemateId?' #'+officialPriceState.rosganNetRemateId:''):officialPriceState.loading?'Consultando resultados oficialesâ¦':'Cálculo provisorio · mesa comercial', 29079 rosPriceDescription:(remate.nombre_remate||remate.titulo||'Remate Rosgan')+(officialPriceState.groups?.length?'. Máximos y promedios oficiales publicados por RosganNet.':'. Resultados provisorios calculados desde la mesa comercial.'), 29080 rosPriceConsCount:new Set(rows.map(row=>row.consignataria)).size, 29081 rosPriceExportBusy:!!s.rosganPriceStoryBusy, 29082 rosPriceExportLabel:s.rosganPriceStoryBusy?'Generando Storyâ¦':'Descargar Story 9:16', 29083 rosPriceExport:()=>this.downloadRosganPriceStory(priceStoryGroups,priceBoardModel), 29084 rosPriceExportMessage:s.rosganPriceExportMessage||'', 29085 rosPriceBoardData:JSON.stringify(priceBoardModel), 29086 rosPriceCategoryView:()=>this.setState({rosganPriceBoardMode:'category'}), 29087 rosPriceWeightView:()=>this.setState({rosganPriceBoardMode:'weight'}), 29088 rosPriceCategorySelected:priceBoardModel.mode==='category', 29089 rosPriceWeightSelected:priceBoardModel.mode==='weight', 29090 rosOpsHasPrices: priceGroups.length > 0, 29091 rosOpsNoPrices: !s.remateLotsLoading && priceGroups.length === 0, 29092 rosOpsBack: () => this.backToRosganCatalog(), 29093 rosOpsCanAdmin: this.canOpenRosganOperation(s.currentRemate, 'commercial'), 29094 rosOpsOpenCommercial: () => this.openCurrentRosganOps('commercial'), 29095 rosOpsOpenPrices: () => this.openCurrentRosganOps('prices'), 29096 rosBillingExcel:()=>this.downloadRosganBilling('xlsx'), 29097 rosBillingPdf:()=>this.downloadRosganBilling('pdf'), 29098 rosOpsOpenTv: () => this.openCurrentRosganOps('tv'), 29099 rosOpsOpenTvConfig: () => this.openCurrentRosganOps('tvConfig'), 29100 rosTvOptions: lots.filter(lot => String(state.commercial[this.remateLotRouteId(lot)]?.status || '').toLowerCase() !== 'levantado').map((lot) => ({ value: this.remateLotRouteId(lot), label: 'Lote ' + String(lot.orden_final || lot.orden_venta || 'â') + ' · ' + (lot.nombre_categoria || 'Sin categorÃa') })), 29101 rosTvSelectedCert: selectedCert, 29102 rosTvMulti: tvSlots.length > 1, 29103 rosTvOutputClass: (state.tv.videoVisible===false?'is-video-off ':'')+(this._rosganTvAirSlots?.length?'is-multi ':'')+((state.tv.nameStraps||[]).some(p=>p.id===state.tv.activeNameStrap)&&state.tv.visible!==false?'has-name-strap':''), 29104 rosNameDraft:s.rosNameDraft||'',rosRoleDraft:s.rosRoleDraft||'',rosNameChange:e=>this.setRosganNameDraft('rosNameDraft',e.target.value),rosRoleChange:e=>this.setRosganNameDraft('rosRoleDraft',e.target.value), 29105 rosNameSave:()=>{const name=String(this.state.rosNameDraft||'').trim();if(!name){this.showToast('Ingresá un nombre.');return;}const tv=this.loadRosganOpsState().tv;const rows=tv.nameStraps||[];const id=this.state.rosNameEditId||crypto.randomUUID();if(rows.length>=50&&!rows.some(p=>p.id===id)){this.showToast('Podés guardar hasta 50 nombres.');return;}const person={id,name:name.slice(0,100),role:String(this.state.rosRoleDraft||'').trim().slice(0,120)};this.setRosganTvConfig({nameStraps:rows.some(p=>p.id===id)?rows.map(p=>p.id===id?person:p):[...rows,person]});this.setState({rosNameDraft:'',rosRoleDraft:'',rosNameEditId:''});}, 29106 rosNames:(state.tv.nameStraps||[]).map(p=>({...p,active:p.id===state.tv.activeNameStrap,action:p.id===state.tv.activeNameStrap?'Quitar del aire':'Poner al aire',toggle:()=>{const tv=this.loadRosganOpsState().tv;this.setRosganTvConfig({activeNameStrap:tv.activeNameStrap===p.id?'':p.id});},edit:()=>this.setState({rosNameDraft:p.name,rosRoleDraft:p.role,rosNameEditId:p.id}),remove:()=>{const tv=this.loadRosganOpsState().tv;this.setRosganTvConfig({nameStraps:(tv.nameStraps||[]).filter(r=>r.id!==p.id),activeNameStrap:tv.activeNameStrap===p.id?'':tv.activeNameStrap});}})), 29107 rosWaitActive:state.tv.fullScreenPlate==='waiting',rosEndActive:state.tv.fullScreenPlate==='end',rosWaitClass:state.tv.fullScreenPlate==='waiting'?'is-on':'',rosEndClass:state.tv.fullScreenPlate==='end'?'is-on':'', 29108 rosWaitToggle:()=>{const tv=this.loadRosganOpsState().tv;this.setRosganTvConfig({fullScreenPlate:tv.fullScreenPlate==='waiting'?'':'waiting'});},rosEndToggle:()=>{const tv=this.loadRosganOpsState().tv;this.setRosganTvConfig({fullScreenPlate:tv.fullScreenPlate==='end'?'':'end'});}, 29109 rosPlateVisible:state.tv.visible!==false&&['waiting','end'].includes(state.tv.fullScreenPlate),rosPlateTitle:this.state.currentRemate?.titulo||this.state.currentRemate?.nombre_remate||'Remate Rosgan',rosPlateStart:this.remateDate(this.state.currentRemate)?.getTime()||0,rosPlateDate:this.remateDate(this.state.currentRemate)?.toLocaleStr
29109ing('es-AR',{dateStyle:'long',timeStyle:'short'})||'Fecha a confirmar',rosPlateLocation:this.state.currentRemate?.ubicacion_remate||this.state.currentRemate?.lugar||this.state.currentRemate?.localidad||'Lugar a confirmar',rosPlateLogos:this.lotDestinationLogos(this.state.currentRemate||{}), 29110 ...this.rosganNameStrapPresentation(state.tv), 29111 rosTvShowSingleOverlay: state.tv.videoVisible===false || (!!selected && !this._rosganTvAirSlots?.length && !!this._rosganTvDecks?.onAirCert), 29112 rosTvAirPanes: this.rosganTvAirPanes(state.tv), 29113 rosTvLayoutLabel: ({ 2:'Doble · 2 lotes', 3:'Triple · 3 lotes', 4:'Cuádruple · 4 lotes' })[tvSlots.length] || 'Simple · 1 lote', 29114 rosTvClearSlots: () => this.setRosganTvConfig({ extraSlots:['','',''] }), 29115 rosTvLotList: s.route==='rosganTvConfig'?this.rosganTvCatalogRows(rows,state.tv,selectedCert,s.rosganTvQuery||''):[], 29116 rosTvQuery: s.rosganTvQuery || '', 29117 rosTvSearch: (event) => this.setState({ rosganTvQuery: event.target.value }), 29118 rosTvEditorFields: s.route === 'rosganTvConfig' ?this.rosganTvEditorFields(selected) : [], 29119 rosTvEditStatus: editorEntry?.status || 'Editá los datos y presioná guardar para actualizar TV.', 29120 rosTvSaveDisabled: !editorEntry || editorEntry.saving || !Object.keys(editorEntry.changes).length, 29121 rosTvSave: () => editorEntry && this.saveRosganTvLotEdits(editorEntry), 29122 rosTvVmixHost: s.rosganVmixDraft?.id===this.remateRouteId(remate)?s.rosganVmixDraft.host:(state.tv.vmixHost||'192.168.122.28:8088'), 29123 rosTvVmixInput: s.rosganVmixDraft?.id===this.remateRouteId(remate)?s.rosganVmixDraft.input:(state.tv.vmixBrowserInputNumber||''), 29124 rosTvVmixHostChange:event=>this.editRosganVmixConnection('host',event.target.value), 29125 rosTvVmixInputChange:event=>this.editRosganVmixConnection('input',event.target.value), 29126 rosTvVmixSave:()=>this.saveRosganVmixConnection(), 29127 rosTvVmixCopyUrl:()=>this.copyRosganVmixUrl(), 29128 rosTvVmixNumber: Number(state.tv.vmixBrowserInputNumber) || 'Sin vincular', 29129 rosTvSelect: (event) => this.selectRosganTvLot(event), 29130 rosTvPrev: () => this.stepRosganTvLot(-1), 29131 rosTvNext: () => this.stepRosganTvLot(1), 29132 rosTvVisible: state.tv.visible !== false, 29133 rosTvVisibleClass: state.tv.visible !== false ?'is-on' : '', 29134 rosTvVideoClass:state.tv.videoVisible!==false?'is-on':'',rosTvVideoVisible:state.tv.videoVisible!==false,rosTvToggleVideo:()=>{const tv=this.loadRosganOpsState().tv;this.setRosganTvConfig({videoVisible:tv.videoVisible===false});}, 29135 rosTvToggleVisible: () => this.setRosganTvConfig({ visible: state.tv.visible === false }), 29136 rosTvLogosVisible: state.tv.logosVisible !== false, 29137 rosTvLogosClass: state.tv.logosVisible !== false ?'is-on' : '', 29138 rosTvToggleLogos: () => this.setRosganTvConfig({ logosVisible: state.tv.logosVisible === false }), 29139 rosTvLowerVisible: state.tv.lowerThirdVisible !== false, 29140 rosTvLowerClass: state.tv.lowerThirdVisible !== false ?'is-on' : '', 29141 rosTvToggleLower: () => this.setRosganTvConfig({ lowerThirdVisible: state.tv.lowerThirdVisible === false }), 29142 rosTvContactsVisible: state.tv.contactsVisible !== false, 29143 rosTvContactsClass: state.tv.contactsVisible !== false ?'is-on' : '', 29144 rosTvToggleContacts: () => this.setRosganTvConfig({ contactsVisible: state.tv.contactsVisible === false }), 29145 rosTvContacts: consContacts, 29146 rosTvContactRows: s.route === 'rosganTvConfig' ?Array.from(new Map([ 29147 ...rows.map(row => row.consignataria), 29148 ...(remate.consignatariasParticipantes || []).map(participant => { 29149 const record=(s.consignatarias || []).find(item => String(item.id) === String(participant.consignatariaId || participant.id)); 29150 return this.consDisplayName(participant.consignatariaNombre || participant.nombre || record?.name || record?.nombre || ''); 29151 }),
29152 ].filter(Boolean).map(name => [this.consKey(name),name])).entries()).map(([key,name]) => { 29153 const record=(s.consignatarias || []).find(item => this.consKey(item.name || item.nombre) === key); 29154 const raw=record?.telefonos || record?.phones || record?.telefono || record?.phone || ''; 29155 const value=state.tv.contactsByConsignataria?.[key] ?? (Array.isArray(raw) ?raw.map(item => typeof item === 'string' ?item :item.phone || item.telefono || '').filter(Boolean).join(' | ') :String(raw)); 29156 const logo=this.consWhiteLogo(name) || this.consLogo(name); 29157 return {key,name,value,logo,hasLogo:!!logo,noLogo:!logo,initial:name.charAt(0),color:this.consBrandColor(name),label:'Contactos de ' + name, 29158 change:event => this.setRosganTvConfig({contactsByConsignataria:{...this.loadRosganOpsState().tv.contactsByConsignataria,[key]:event.target.value}})}; 29159 }) :[], 29160 rosTvOutputUrl: this.remateSectionPath(remate, 'tv'), 29161 rosTvOpenOutput: () => window.open(this.remateSectionPath(remate, 'tv'), '_blank', 'noopener'), 29162 rosTvHasLot: !!selected, 29163 rosTvNoLot: !selected && !s.remateLotsLoading, 29164 rosTvVideoUrl: videoUrl, 29165 rosTvHasVideo: !!videoUrl, 29166 rosTvNoVideo: !!selected && !videoUrl, 29167 rosTvPoster: poster, 29168 rosTvPlay: (event) => this.playRosganTvVideo(event), 29169 rosTvLotNumber: this.rosganTvLotNumber(selected), 29170 rosTvCert: selected ?String(selected.nro_certificado || selected.id_certificado || 'â') : '', 29171 rosTvCategory: this.rosganTvCategory(selected), 29172 rosTvPregnancy: this.rosganTvFixedPregnancy(selected), 29173 rosTvShowVisec: state.tv.visible!==false&&state.tv.lowerThirdVisible!==false&&this.rosganTvIsAptoVisec(selected), 29174 rosTvHasExtraFields: extraFields.length > 0, 29175 rosTvExtraLabel: extraFields[extraIndex]?.label || '', 29176 rosTvExtraValue: extraFields[extraIndex]?.value || '', 29177 rosTvObservations: tvObservations, 29178 rosTvShowChina:state.tv.visible!==false&&state.tv.lowerThirdVisible!==false&&this.rosganTvIsAptoChina(selected), 29179 rosTvHasObservations: !!tvObservations && !/^(?:\?|[-ââ]|null|sin datos?)$/i.test(tvObservations), 29180 rosTvObservationsDuration: Math.max(18, tvObservations.length / 8), 29181 rosTvBreed: selected?.raza || selected?.nombre_raza || '', 29182 rosTvHasQuantity: !!this.rosganTvQuantity(selected), 29183 rosTvHasWeight: Number(selected?.peso_promedio) > 0, 29184 rosTvPayment: selected?.plazo || '', 29185 rosTvTick: selected?.garrapata || '', 29186 rosTvQuantity: this.rosganTvQuantity(selected), 29187 rosTvQuantityFirst: this.rosganTvQuantityBreakdown(selected).split('\n')[0], 29188 rosTvQuantitySecond: this.rosganTvQuantityBreakdown(selected).split('\n')[1] || '', 29189 rosTvWeight: selected && Number(selected.peso_promedio) ?numberText(selected.peso_promedio) + ' kg' : '', 29190 rosTvLocation: selectedRow?.locality || '', 29191 rosTvConditions: conditions || 'Condiciones a confirmar', 29192 rosTvConsignataria: selectedConsignataria, 29193 rosTvConsLogo: consLogo, 29194 rosTvConsFullLogo: this.consFullHeroLogo(selectedConsignataria), 29195 rosTvHasConsLogo: !!consLogo, 29196 rosTvNoConsLogo: !consLogo, 29197 rosTvConsInitial: selectedConsignataria.charAt(0).toUpperCase(), 29198 rosTvConsColor: temporarySelectedConsignataria?.color || this.consBrandColor(selectedConsignataria), 29199 rosTvShowBrandBug: state.tv.visible !== false && state.tv.logosVisible !== false, 29200 rosTvShowLower: state.tv.visible !== false && state.tv.lowerThirdVisible !== false, 29201 rosTvShowContacts: state.tv.visible !== false && state.tv.lowerThirdVisible !== false && state.tv.contactsVisible !== false && !!consContacts, 29202 }; 29203 })(), 29204 ...(() => { 29205 const cabanas = Array.isArray(s.cabanas) ?s.cabanas : []; 29206 const breeds = [...new Set(cabanas.map((item) =>
29206 this.cabanaBreedOf(item)).filter(Boolean))]; 29207 const visibleCabanas = s.cabanaBreed ?cabanas.filter((item) => this.cabanaBreedOf(item) === s.cabanaBreed) : cabanas; 29208 const dateLabel = (item) => { 29209 const date = this.cabanaAuctionStartAt(item); 29210 return date ? date.toLocaleDateString('es-AR', {timeZone:'America/Argentina/Buenos_Aires',weekday:'short',day:'2-digit',month:'short',year:'numeric'}) : 'Fecha a confirmar'; 29211 }; 29212 const timeLabel = (item) => { 29213 const date = this.cabanaAuctionStartAt(item); 29214 return date ? date.toLocaleTimeString('es-AR', {timeZone:'America/Argentina/Buenos_Aires',hour:'2-digit',minute:'2-digit',hourCycle:'h23'}) + ' h' : ''; 29215 }; 29216 const mediaReady = (event) => { 29217 if (event?.currentTarget) event.currentTarget.dataset.dcLoaded = 'true'; 29218 }; 29219 const mediaError = (event) => { 29220 if (event?.currentTarget) event.currentTarget.style.display = 'none'; 29221 }; 29222 const cabanaCards = [...visibleCabanas].sort((a,b) => this.compareCabanaListing(a,b)).map((item) => { 29223 const mode = this.cabanaMode(item); 29224 const media = this.cabanaMedia(item); 29225 const participants = this.cabanaRemateParticipants(item); 29226 const mainCabana = participants.find((cabana) => cabana.type === 'principal') || participants[0] || {}; 29227 const participantLogos = [...participants].sort((a,b) => Number(b.type === 'principal') - Number(a.type === 'principal')).map(cabana => { 29228 const src = cabana.logoDataUrl || ''; 29229 const hasLogo = !!src && !s.cabanaCardLogoFailed?.[src]; 29230 return { src, name: cabana.name, hasLogo, noLogo: !hasLogo, kind: cabana.type === 'principal' ? 'is-principal' : 'is-guest', 29231 onError: () => this.setState({cabanaCardLogoFailed:{...this.state.cabanaCardLogoFailed,[src]:true}}) }; 29232 }); 29233 const consRef = item?.consignatariaId || item?.consignataria || item?.nombre_cuenta || ''; 29234 const consignatariaRecord = rawConsignatarias.find((candidate) => [candidate?.id, candidate?.consignatariaId, candidate?.slug, candidate?.name, candidate?.nombre].some((value) => this.consKey(value) && this.consKey(value) === this.consKey(consRef))); 29235 const consignatariaName = (consignatariaRecord ?this.consName(consignatariaRecord) : '') || (String(consRef).includes('-') ?this.humanizeSlug(consRef) : consRef) || 'Rosgan'; 29236 const whiteConsIcon = this.consWhiteLogo(consRef || consignatariaName); 29237 const consIcon = whiteConsIcon || this.consLogo(consRef || consignatariaName) || this.consLogo(consignatariaName); 29238 const mainLogo = mainCabana.logoDataUrl || ''; 29239 const listFallbackLogo = mainLogo || participants.find(c => c.logoDataUrl)?.logoDataUrl || this.consLogo(consRef || consignatariaName) || this.consLogo(consignatariaName) || consIcon || 'assets/rosgan-logo-blue.png'; 29240 const participantNames = participants.map(c => c.name).filter(Boolean).join(' · '); 29241 const participantRoll = participants.length > 3 || participantNames.length > 85; 29242 const cardId = this.cabanaRemateId(item); 29243 const cardMedia = s.cabanaCardMedia?.[cardId] || { ready: false, frames: [] }; 29244 const heroFrames = Array.isArray(cardMedia.frames) ?cardMedia.frames : []; 29245 return { 29246 createLot: (event) => { event?.stopPropagation?.(); this.openCabanaLotCreate(item); }, canCreateLot: this.canCreateCabanaLot(item),
29247 edit: (event) => { event?.stopPropagation?.(); this.openCabanaRemateEdit(item); }, canEdit: this.canEditCabanaRemate(item), 29248 salesOrder: (event) => { event?.stopPropagation?.(); this.openCabana(item, { openSalesOrder: true }); }, canManageSalesOrder: this.canManageUsers(), 29249 openRow: event => { if (event?.target?.closest?.('button, a, input, select, textarea, [role=button]')) return; this.openCabana(item); }, 29250 open: () => this.openCabana(item), title: item.nombre_remate || item.title || item.titulo || 'Remate de cabaña', 29251 breed: this.cabanaBreedOf(item) || 'Reproductores', color: this.cabanaColor(item), brandDark: this.cabanaBrandDark(item), 29252 media, hasMedia: !!media, noMedia: !media, mediaReady, mediaError, initial: String(item.nombre_remate || item.title || item.titulo || 'C').replace(/^\d+[^A-Za-z0-9]*/i, '').trim().charAt(0).toUpperCase() || 'C', 29253 listThumb: heroFrames[0]?.src || media || listFallbackLogo, listThumbClass: heroFrames[0]?.src || media ? 'has-photo' : '', participantNames, participantRoll, participantRollClass: participantRoll ? 'is-rolling' : '', participantRollDuration: Math.max(22, participantNames.length * .22) + 's', 29254 toggleListRoll: event => { const button=event.currentTarget; const paused=button.closest('.cabana-list-participants').classList.toggle('is-paused'); button.setAttribute('aria-pressed', String(paused)); button.setAttribute('aria-label', paused ? 'Reanudar desplazamiento de participantes' : 'Pausar desplazamiento de participantes'); button.textContent=paused ? 'â·' : 'â ¡'; }, 29255 listThumbError: event => { const img=event.currentTarget; if(img.dataset.fallback)return; img.dataset.fallback='true';img.src=listFallbackLogo;img.closest('button')?.classList.remove('has-photo'); }, 29256 heroFrames, heroBase: heroFrames[0]?.src || '', hasHeroMedia: heroFrames.length > 0, noHeroMedia: heroFrames.length === 0, fallbackClass: cardMedia.ready ?'' : 'is-loading', fallbackLogo: mainLogo || 'assets/rosgan-logo-blue.png', 29257 prebidsActive: window.CabanaPrebid.isOpen(item), 29258 badge: mode === 'live' ?'EN VIVO' : mode === 'upcoming' ?'PRÃXIMO' : 'FINALIZADO', 29259 badgeBg: mode === 'live' ?'#e63946' : mode === 'upcoming' ?'#0a5ca8' : '#65758a', 29260 location: item.ubicacion_remate || item.lugar || item.localidad || item.provincia || item?.location?.lugar || item?.location?.localidad || item?.location?.provinciaNombre || 'Ubicación a confirmar', 29261 rollParticipants: participantLogos.length > 5, participantsRollClass: participantLogos.length > 5 ?'is-rolling' : '', participantsTabindex: participantLogos.length > 5 ?'0' : '-1', participantsDuration: Math.max(24, participantLogos.length * 4) + 's', 29262 toggleParticipantsRoll: event => { event.stopPropagation(); const button = event.currentTarget; const paused = button.closest('.cabana-card-brand-strip').classList.toggle('is-paused'); button.textContent = paused ?'Reanudar' : 'Pausar'; button.setAttribute('aria-label', paused ?'Reanudar movimiento de participantes' : 'Pausar movimiento de participantes'); }, 29263 mainLogo, hasMainLogo: !!mainLogo, mainLogoAlt: mainCabana.name || 'Cabaña principal', participantLogos, hasLogos: participantLogos.length > 0, 29264 consignataria: consignatariaName, consBg: this.consBrandColor(consRef || consignatariaName), consIcon, hasConsIcon: !!consIcon, consIconFilter: whiteConsIcon ?'none' : 'brightness(0) invert(1)', 29265 date: dateLabel(item), time: timeLabel(item), dateTime: [dateLabel(item), timeLabel(item)].filter(Boolean).join(' · '), 29266 lots: Number(item.cantidad_lotes || item.lotsCount || item.lotCount || 0), 29267 }; 29268 }); 29269 const current = s.currentCabana; 29270 const currentHeroColor = current ?this.cabanaColor(current) : '#0a5ca8'; 29271 const currentHeroColorMatch = currentHeroColor.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i); 29272 const currentHeroIsDark = !currentHeroColorMatch || ((parseInt(currentHeroColorMatch[1], 16) * .299) + (parseInt(currentHeroColorMatch[2], 16) * .587) + (parseInt(currentHeroColorMatch[3], 16) * .114)) < 158; 29273 const mapBrandLogo = (cabana) => { 29274 const whiteLogo = cabana?.logoWhiteDataUrl || ''; 29275 const colorLogo = cabana?.logoDataUrl || ''; 29276 const candidate = currentHeroIsDark ?(whiteLogo || colorLogo) : (colorLogo || whiteLogo); 29277 const logo = s.cabanaHeroLogoFailed?.[candidate] ? '' : candidate; 29278 return { onError: () => this.setState({cabanaHeroLogoFailed:{...this.state.cabanaHeroLogoFailed,[candidate]:true}}), name: cabana?.nombre || cabana?.name || 'Cabaña', logo, whiteLogo, filter: currentHeroIsDark && !whiteLogo && colorLogo ?'brightness(0) invert(1)' : 'none', hasLogo: !!logo, noLogo: !logo }; 29279 }; 29280 const mapGuestLogo = (cabana) => { 29281 const whiteLogo = cabana?.logoWhiteDataUrl || ''; 29282 const colorLogo = cabana?.logoDataUrl || ''; 29283 const candidate = currentHeroIsDark ?(whiteLogo || colorLogo) : (colorLogo || whiteLogo); 29284 const logo = s.cabanaHeroLogoFailed?.[candidate] ? '' : candidate; 29285 return { onError: () => this.setState({cabanaHeroLogoFailed:{...this.state.cabanaHeroLogoFailed,[candidate]:true}}), name: cabana?.nombre || cabana?.name || 'Cabaña', logo, whiteLogo, filter: currentHeroIsDark && !whiteLogo && colorLogo ?'brightness(0) invert(1)' : 'none', hasLogo: !!logo, noLogo: !logo }; 29286 }; 29287 const participants = this.cabanaRemateParticipants(current || {}); 29288 const mainBrandLogos = participants.filter((cabana) => cabana.type === 'principal').map(mapBrandLogo); 29289 const guestBrandLogos = participants.filter((cabana) => cabana.type !== 'principal').map(mapGuestLogo); 29290 const primaryWhiteLogo = participants.find((cabana) => cabana.type === 'principal')?.logoWhiteDataUrl || ''; 29291 const currentConsignatariaRecord = current ?rawConsignatarias.find((candidate) => [candidate?.id, candidate?.consignatariaId, candidate?.slug, candidate?.name, candidate?.nombre].some((value) => this.consKey(value) && this.consKey(value) === this.consKey(current?.consignatariaId || current?.consignataria || current?.nombre_cuenta))) : null; 29292 const currentConsignatariaRaw = current ?(current.consignataria || current.nombre_cuenta || current?.consignatariaId || '') : ''; 29293 const currentConsignatariaName = current ?((currentConsignatariaRecord ?this.consName(currentConsignatariaRecord) : '') || (String(currentConsignatariaRaw).includes('-') ?this.humanizeSlug(currentConsignatariaRaw) : currentConsignatariaRaw) || 'Rosgan') : ''; 29294 const currentConsignatariaLogo = current ?(this.consLogo(currentConsignatariaName) || this.consLogo(current?.consignatariaId) || (currentConsignatariaRecord && (currentConsignatariaRecord.logoUrl || currentConsignatariaRecord.logo || currentConsignatariaRecord.imageUrl))) : ''; 29295 const currentConsignatariaWhiteLogo = current ?this.consWhiteLogo(currentConsignatariaRaw || currentConsignatariaName) : ''; 29296 const currentConsignatariaColor = current ?this.consBrandColor(currentConsignatariaRaw || currentConsignatariaName) : '#002e5f'; 29297 const currentContact = this.cabanaContactData(current); 29298 const prebidPanelData = s.cabanaPrebidPanelData || {}; 29299 const prebidPanelQuery = String(s.cabanaPrebidPanelQuery || '').trim().toLowerCase(); 29300 const prebidPanelStatus = String(s.cabanaPrebidPanelStatus || 'all'); 29301 const prebidPanelRawItems = Array.isArray(prebidPanelData.items) ?prebidPanelData.items : []; 29302 const prebidPanelLotBase = this.cabanaPrebidPanelLots(prebidPanelData); 29303 const requestedPanelLot = String(s.cabanaPrebidPanelLot || ''); 29304 const prebidPanelSelectedLot = prebidPanelLotBase.find((lot) => lot.key === requestedPanelLot) || prebidPanelLotBase[0] || null; 29305 const prebidPanelSelectedLotKey = String(prebidPanelSelectedLot?.key || ''); 29306 const prebidPanelScopedItems = prebidPanelRawItems.filter((item) => String(item?.lotCert || '') === prebidPanelSelectedLotKey); 29307 const prebidPanelLots = prebidPanelLotBase.map((lot) => ({ 29308 ...lot, 29309 className: 'cb-prebid-lot-option' + (lot.key === prebidPanelSelectedLotKey ?' is-active' : ''), 29310 go: () => this.selectCabanaPrebidPanelLot(lot.key), 29311 })); 29312 const prebidPanelItems = prebidPanelScopedItems.filter((item) => { 29313 const normalizedStatus = item?.isCurrent ?'current' : (/pendiente|pe
29313nding/i.test(String(item?.status || '')) ?'pending' : 'outbid'); 29314 if (prebidPanelStatus !== 'all' && normalizedStatus !== prebidPanelStatus) return false; 29315 if (!prebidPanelQuery) return true; 29316 const animalText = (Array.isArray(item?.animals) ?item.animals : []).flatMap((animal) => [animal?.cert, ...(Array.isArray(animal?.fields) ?animal.fields.flatMap((field) => [field?.label, field?.value]) : [])]); 29317 return [item?.buyer?.name, item?.buyer?.email, item?.buyer?.cuitCuil, item?.lotLabel, item?.lotCert, item?.category, item?.cabanaName, item?.enComisionPara, ...animalText].filter(Boolean).join(' ').toLowerCase().includes(prebidPanelQuery); 29318 }); 29319 const prebidPanelRows = prebidPanelItems.map((item) => { 29320 const created = item?.createdAt ?new Date(item.createdAt) : null; 29321 const statusKey = item?.isCurrent ?'current' : (/pendiente|pending/i.test(String(item?.status || '')) ?'pending' : 'outbid'); 29322 const animals = (Array.isArray(item?.animals) ?item.animals : []).map((animal) => { 29323 const fields = (Array.isArray(animal?.fields) ?animal.fields : []).filter((field) => field?.value != null && String(field.value).trim()); 29324 return { 29325 cert: animal?.cert ?(animal.identifierName || 'Cert.')+' '+animal.cert : '', 29326 detail: fields.map((field) => field.label + ': ' + field.value).join(' · ') || 'Sin identificación adicional', 29327 }; 29328 }); 29329 return { 29330 id: item?.id || [item?.lotCert, item?.amount, item?.createdAt].join('-'), 29331 amount: this.fmtARS(Number(item?.amount || 0)), 29332 date: created && !Number.isNaN(created.getTime()) ?created.toLocaleString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'Sin fecha', 29333 status: item.status || 'Sin estado', 29334 canApprove: /pendienteaprobacion/i.test(item.status), 29335 busy: !!s.cabanaPrebidApprovalBusy, 29336 approve: () => this.resolveCabanaPrebid(item,'approve'), 29337 reject: () => this.resolveCabanaPrebid(item,'reject'), 29338 canDelete:this.canDeleteCabanaPrebid(),deleteOffer:()=>
29338this.deleteCabanaPrebid(item),deleteBusy:!!s.cabanaPrebidDeleting, 29339 statusClass: 'cb-prebid-admin-status is-' + statusKey, 29340 buyerName: item?.buyer?.name || 'Usuario sin nombre', 29341 buyerEmail: item?.buyer?.email || 'Sin email', 29342 buyerHref: item?.buyer?.email ?'mailto:' + item.buyer.email : '', 29343 buyerCuit: item?.buyer?.cuitCuil || 'CUIT/CUIL no informado', 29344 lot: item?.lotLabel || ('Lote ' + (item?.lotCert || '')), 29345 lotMeta: [item?.category, item?.cabanaName].filter(Boolean).join(' · '), 29346 commission: item?.enComisionPara ?'En comisión para ' + item.enComisionPara : '', 29347 hasCommission: !!item?.enComisionPara, 29348 animals, 29349 hasAnimals: animals.length > 0, 29350 }; 29351 }); 29352 const prebidPanelBidderKeys = new Set(prebidPanelScopedItems.map((item) => item?.buyer?.uid || item?.buyer?.email).filter(Boolean)); 29353 const prebidPanelCurrentItem = prebidPanelScopedItems.filter(item=>window.CabanaPrebid.accepted(item)).sort((a,b)=>Number(b.amount)-Number(a.amount))[0] || null; 29354 const prebidPanelLastDate = prebidPanelScopedItems.reduce((latest, item) => { 29355 const value = item?.createdAt ?new Date(item.createdAt).getTime() : 0; 29356 return Number.isFinite(value) && value > latest ?value : latest; 29357 }, 0); 29358 const prebidPanelMetrics = [ 29359 { label: 'Preofertas del lote', value: prebidPanelScopedItems.length.toLocaleString('es-AR') }, 29360 { label: 'Ofertantes', value: prebidPanelBidderKeys.size.toLocaleString('es-AR') }, 29361 { label: 'Oferta actual', value: prebidPanelCurrentItem ?this.fmtARS(Number(prebidPanelCurrentItem.amount || 0)) : 'Sin ofertas' }, 29362 { label: 'Ãltima actividad', value: prebidPanelLastDate ?new Date(prebidPanelLastDate).toLocaleString('es-AR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : 'Sin actividad' }, 29363 ]; 29364 const prebidPanelUpdatedDate = prebidPanelData.updatedAt ?new Date(prebidPanelData.updatedAt) : null; 29365 const prebidPanelUpdated = prebidPanelUpdatedDate && !Number.isNaN(prebidPanelUpdatedDate.getTime()) ?'Actualizado ' + prebidPanelUpdatedDate.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : 'Actualización automática'; 29366 const lots = this.cabanaSortLotsByCategoryOrder(s.cabanaLots || []); 29367 const lotText = (lot, keys, fallback = '') => { 29368 for (const key of keys) { 29369 const value = lot?.[key] || lot?.values?.[key] || lot?.subformValues?.datos_principales?.[key]; 29370 if (value !== undefined && value !== null && value !== '') return value; 29371 } 29372 return fallback; 29373 }; 29374 const lotCategory = (lot) => String(lotText(lot, ['categoriaNombre', 'nombre_categoria', 'categoria', 'categoryName'], 'Reproductor')); 29375 const auditorRows = this.canAuditCabana(current) && s.route === 'cabanaAuditor' ? [...new Map(lots.map(lot => [this.cabanaPrebidCert(lot), lot])).values()].filter(lot => this.canEditCabanaLot(current, lot) && [this.cabanaLotNumber(lot), lotText(lot, ['rp', 'RP']), lotText(lot, ['nombre', 'lotName']), lotCategory(lot)].join(' ').toLowerCase().includes(String(s.cabanaAuditorQuery || '').trim().toLowerCase())).map(lot => { 29376 const key = this.cabanaRemateId(current) + ':' + this.cabanaPrebidCert(lot); 29377 this._cabanaAuditorDrafts ||= {}; 29378 const draft = this._cabanaAuditorDrafts[key] || {base:lot.prebidBaseOverride ?? '', increment:lot.prebidIncrementOverride ?? ''}; 29379 const pricing = window.CabanaPrebid.pricing(lot,current); 29380 const upload = s.cabanaCardVideoUploads?.[this.cabanaLotId(lot)] || {}; 29381 return {title:'Lote ' + this.cabanaLotNumber(lot) + ' · RP ' + lotText(lot,['rp','RP'],'â'), subtitle:lotText(lot,['nombre','lotName']) + ' · ' + lotCategory(lot), base:draft.base, increment:draft.increment, 29382 effective:'Vigente: base ' + (pricing.base == null ? 'Sin configurar' : this.fmtARS(pricing.base)) + ' · incremento ' + (pricing.increment == null ? 'Sin configurar' : this.fmtARS(pricing.increment)), 29383 setBase:event => {this._cabanaAuditorDrafts[key] = {...draft, ...this._cabanaAuditorDrafts[key], base:event.target.value};}, 29384 setIncrement:event => {this._cabanaAuditorDrafts[key] = {...draft, ...this._cabanaAuditorDrafts[key], increment:event.target.value};}, 29385 saving:!!s.cabanaAuditorSaving?.[key], saveLabel:s.cabanaAuditorSaving?.[key] ? 'Guardandoâ¦' : 'Guardar', save:event =>
29385 this.saveCabanaAuditorPricing(current,lot,event), 29386 edit:() => this.openCabanaLotEdit(current,lot), canUpload:this.canUploadCabanaLotMedia(current,lot), canFinal:this.canUploadCabanaFinal(current), uploading:!!upload.busy, upload:event => this.quickUploadCabanaLotVideo(event,lot), status:upload.busy ? (upload.stage || 'Subiendoâ¦') + ' ' + Math.round(upload.progress || 0) + '%' : ''}; 29387 }) : []; 29388 const isToro = (lot) => /toro|macho|mej/i.test(lotCategory(lot)); 29389 const query = String(s.cbQuery || '').trim().toLowerCase(); 29390 const filteredLots = lots.filter((lot) => (!query || [lotCategory(lot), this.cabanaLotNumber(lot), lotText(lot, ['rp', 'RP', 'numero_rp', 'caravana', 'lote']), lotText(lot, ['nombre', 'lotName', 'nombre_lote']), lotText(lot, ['raza', 'razaNombre']), lot?.cabanaNombre].join(' ').toLowerCase().includes(query))); 29391 const groupLots = (source) => { 29392 const groups = new Map(); 29393 (Array.isArray(source) ?source : []).forEach((lot, index) => { 29394 const key = this.cabanaLotGroupKey(lot, index); 29395 if (!groups.has(key)) groups.set(key, { 29396 key, 29397 categoryName: lotCategory(lot), 29398 categoryOrder: this.cabanaCategoryDisplayOrder(lot), 29399 lotNumber: this.cabanaLotNumber(lot), 29400 variants: [], 29401 }); 29402 groups.get(key).variants.push(lot); 29403 }); 29404 return Array.from(groups.values()) 29405 .map((group) => ({ 29406 ...group, 29407 variants: group.variants.sort((a, b) => 29408 this.cabanaLotSortNumber(a?.itemDisplayOrder, 0) - 29409 this.cabanaLotSortNumber(b?.itemDisplayOrder, 0) 29410 ), 29411 })) 29412 .sort((a, b) => { 29413 return String(a.lotNumber).localeCompare(String(b.lotNumber), 'es', { 29414 sensitivity: 'base', 29415 numeric: true, 29416 }); 29417 }); 29418 }; 29419 const allLotGroups = groupLots(lots); 29420 const filteredGroups = groupLots(filteredLots); 29421 const analyticsRoot = s.cabanaAnalyticsSummary?.data || s.cabanaAnalyticsSummary || {}; 29422 const analyticsLots = this.listFrom(analyticsRoot, ['lots', 'items', 'data']).filter(Boolean); 29423 const analyticsRps = this.listFrom(analyticsRoot, ['rps', 'items', 'data']).filter(Boolean); 29424 const analyticsScore = (row) => Number(row?.rpViews || row?.views || row?.viewCount || 0) + Number(row?.videoPlays || row?.plays || 0) + Number(row?.imageViews || 0) + Number(row?.contactClicks || row?.contacts || 0) + Number(row?.shares || row?.shareClicks || row?.event_share_click || 0); 29425 const topRpRankByKey = new Map(); 29426 analyticsRps 29427 .map((row) => ({ row, score: analyticsScore(row), rp: String(row?.rp || row?.rpId || row?.itemId || '').trim() })) 29428 .filter((item) => item.rp && item.score > 0) 29429 .sort((a, b) => b.score - a.score) 29430 .slice(0, 3) 29431 .forEach((item, rankIndex) => { 29432 topRpRankByKey.set(this.consKey(item.rp), { rank: rankIndex + 1, score: item.score }); 29433 }); 29434 const rankBadgeForRp = (rpValue) => { 29435 const hit = topRpRankByKey.get(this.consKey(rpValue)); 29436 if (!hit) return null; 29437 if (hit.rank === 1) return { 29438 label: 'Mas visto', 29439 bg: 'rgba(250,204,21,.94)', 29440 color: '#3b2500', 29441 border: 'rgba(255,244,178,.9)', 29442 iconUrl: 'assets/icons/lucide/flame.svg', 29443 title: `${hit.score.toLocaleString('es-AR')} senales registradas`, 29444 }; 29445 if (hit.rank === 2) return { 29446 label: 'Segundo mas visto', 29447 bg: 'rgba(236,242,248,.94)', 29448 color: '#243548', 29449 border: 'rgba(255,255,255,.82)', 29450 iconUrl: 'assets/icons/lucide/medal.svg', 29451 title: `${hit.score.toLocaleString('es-AR')} senales registradas`, 29452 }; 29453 return { 29454 label: 'Tercero mas visto', 29455 bg: 'rgba(190,126,74,.94)', 29456 color: '#fffaf2', 29457 border: 'rgba(255,222,186,.72)', 29458 iconUrl: 'assets/icons/lucide/medal.svg',
29459 title: `${hit.score.toLocaleString('es-AR')} senales registradas`, 29460 }; 29461 }; 29462 const prebidWindow = window.CabanaPrebid.windowState(current); 29463 const prebidActive = !!(current?.prebid?.habilitarPreofertas || current?.prebid?.habilitada || current?.prebid?.enabled || current?.prebid?.isActive || String(current?.preoferta || '').toLowerCase() === 'activa'); 29464 const isEmptyValue = (value) => { 29465 const text = String(value || '').trim(); 29466 return !text || text === '-' || /^[-\u2013\u2014]+$/.test(text) || text.toLowerCase() === 'null' || text.toLowerCase() === 'undefined'; 29467 }; 29468 const cleanLabel = (value) => String(value || '').replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim(); 29469 const displayValue = (value) => { 29470 if (Array.isArray(value)) return value.map(displayValue).filter((item) => !isEmptyValue(item)).join(', '); 29471 if (value && typeof value === 'object') return value.name || value.nombre || value.label || value.value || ''; 29472 if (typeof value === 'boolean') return value ?'SÃ' : 'No'; 29473 return value === undefined || value === null ?'' : String(value); 29474 }; 29475 const cardFactsFromSchema = (lot) => this.cabanaLotOnCardFacts(lot, current, 4); 29476 const compactCardFact = (fact) => { 29477 const title = String(fact?.value || '').trim(); 29478 if (title.length <= 48) return { ...fact, value: title, title }; 29479 const dates = title.match(/\d{1,2}\/\d{1,2}\/\d{4}/g) || []; 29480 if (dates.length > 2) return { ...fact, value: dates.slice(0, 2).join(' · ') + ' +' + (dates.length - 2), title }; 29481 const labelKey = this.consKey(fact?.label || ''); 29482 const rpParts = title.split('-').map((part) => part.trim()).filter(Boolean); 29483 if (labelKey === 'rp' && rpParts.length > 2 && rpParts.every((part) => /^[a-z0-9]+$/i.test(part))) { 29484 return { ...fact, value: rpParts.slice(0, 2).join(' · ') + ' +' + (rpParts.length - 2), title }; 29485 } 29486 return { ...fact, value: title.slice(0, 45).replace(/[\s-]+$/g, '') + 'â¦', title }; 29487 }; 29488 const mapLot = (lot, index) => { 29489 const videos = this.cabanaLotVideos(lot); 29490 const itemCoverUrl = this.cabanaLotItemCoverUrl(lot); 29491 const firstVideo = videos[0] || {}; 29492 const generatedVideoThumb = firstVideo.thumb || s.videoThumbByUrl[firstVideo.url] || ''; 29493 const media = itemCoverUrl || this.cabanaLotFallbackCoverUrl(lot) || generatedVideoThumb || ''; 29494 const videoPreview = media ?'' : (firstVideo.url || ''); 29495 const cardUploadState = s.cabanaCardVideoUploads?.[this.cabanaLotId(lot)] || {}; 29496 const rp = String(lotText(lot, ['rp', 'RP', 'numero_rp', 'lotNumber', 'orden_final'], index + 1)); 29497 const identity = window.CabanaLotIdentity.resolve(lot, current); 29498 const lotNumber = identity.number; 29499 const category = lotCategory(lot); 29500 const statusRaw = String(lotText(lot, ['estado', 'auctionStatus', 'status'], 'Disponible')).toLowerCase(); 29501 const sold = /vend|sold|closed/.test(statusRaw); 29502 const explicitName = String(lotText(lot, ['nombre', 'lotName', 'nombre_lote'], '')).trim(); 29503 const generatedName = (lot?.cabanaNombre ?lot.cabanaNombre + ' - ' : '') + category + ' RP ' + rp; 29504 const duplicateName = explicitName && (this.consKey(explicitName) === this.consKey(generatedName) || (lot?.cabanaNombre && this.consKey(explicitName).includes(this.consKey(lot.cabanaNombre)) && this.consKey(explicitName).includes(this.consKey(category)) && this.consKey(explicitName).includes(this.consKey(rp)))); 29505 const name = explicitName && !duplicateName ?explicitName : ''; 29506 const breed = String(lotText(lot, ['razaNombre', 'raza', 'breedName'], this.cabanaBreedOf(current) || '')); 29507 const weight = Number(lotText(lot, ['peso', 'pesoPromedio', 'peso_promedio'], 0)); 29508 const born = lotText(lot, ['nacimiento', 'anio_nacimiento', 'fecha_nacimiento'], '-'); 29509 const base = window.CabanaPrebid.pricing(lot, current).base; 29510 const priceMap=s.cabanaCatalogPrices?.id===this.cabanaRemateId(current)?s.cabanaCatalogPrices.prices:{}; 29511 const priceEntry=priceMap?.[this.cabanaPrebidCert(lot)]; 29512 const offered=lot.itemId ? priceEntry?.items?.[String(lot.itemId)] : priceEntry?.amount; 29513 const hasOffer=offered!=null && Number.isFinite(Number(offered)); 29514 const displayPrice=hasOffer?Number(offered):base; 29515 const hasBase = Number.isFinite(displayPrice) && displayPrice >= 0; 29516 const lotCabana = participants.find((cabana) => (lot?.cabanaKey && cabana.key === lot.cabanaKey) || (lot?.cabanaId && String(cabana.id || '')
29516=== String(lot.cabanaId)) || (lot?.cabanaNombre && cabana.name === lot.cabanaNombre)) || participants[0] || {}; 29517 const backgroundLogo = ''; 29518 const fallbackFacts = [{ label: 'NACIMIENTO', value: String(born) }, { label: 'PESO', value: weight ?weight.toLocaleString('es-AR') + ' kg' : '-' }, { label: isToro(lot) ?'C. ESCROTAL' : 'REGISTRO', value: String(lotText(lot, isToro(lot) ?['ce', 'circunferenciaEscrotal'] : ['registro', 'hba'], '-')) }, { label: 'DEP DESTETE', value: String(lotText(lot, ['dep', 'depDestete'], '-')) }].filter((fact) => !isEmptyValue(fact.value)); 29519 const facts = cardFactsFromSchema(lot); 29520 const rankBadge = rankBadgeForRp(rp); 29521 const rpParts = rp.split('-').map((part) => part.trim()).filter(Boolean); 29522 const isMultiRp = rpParts.length > 2 && rpParts.every((part) => /^[a-z0-9]+$/i.test(part)); 29523 const rpLabel = identity.type === 'corral' && (!lot?.values?.rp && !lot?.values?.RP || isMultiRp) ? '' : 'RP ' + rp; 29524 return { raw: lot, open: () => this.openCabanaLot(lot), rp: rpLabel, rpTitle: 'RP ' + rp, lotLabel: identity.label, lotBadge: identity.label.toUpperCase() + ' ' + lotNumber, lotNumber: String(lotNumber), hasLotNumber: !isEmptyValue(lotNumber), name, showName: false, category, breed, media, hasMedia: !!media, videoPreview, hasVideoPreview: !!videoPreview, mediaReady, mediaError, 29525 canUploadVideo: !media && !videoPreview && !cardUploadState.busy && this.canUploadCabanaLotMedia(current, lot), uploadVideo: (event) => this.quickUploadCabanaLotVideo(event, lot), stopUploadClick: (event) => event.stopPropagation(), videoUploadBusy: !!cardUploadState.busy, videoUploadProgress: Math.max(0, Math.min(100, Number(cardUploadState.progress || 0))), videoUploadStage: cardUploadState.stage || 'Preparandoâ¦', 29526 hasRp: !!rpLabel, backgroundLogo, hasBackgroundLogo: false, backgroundLogoFilter: 'none', 29527 status: sold ?'Vendido' : 'Disponible', statusBg: sold ?'#65758a' : '#1f7a63', opacity: sold ?0.72 : 1, 29528 facts: (facts.length ?facts : fallbackFacts).map(compactCardFact), 29529 hasRankBadge: !!rankBadge, rankLabel: rankBadge?.label || '', rankIconUrl: rankBadge?.iconUrl || '', rankBg: rankBadge?.bg || '', rankColor: rankBadge?.color || '', rankBorder: rankBadge?.border || '', rankTitle: rankBadge?.title || '', 29530 base: hasBase ?this.fmtARS(displayPrice) : '', showBase: hasBase, priceLabel:hasOffer?'OFERTA ACTUAL':'BASE', priceColor:hasOffer?'#08765a':'#0b63af', priceWeight:hasOffer?'800':'400', videos, 29531 }; 29532 }; 29533 const carouselStep = Number(s.cbCarouselStep || 0); 29534 const carouselOffsets = s.cbCarouselOffsets || {}; 29535 this._cabanaActiveCardByGroup = this._cabanaActiveCardByGroup || {}; 29536 this._cabanaCarouselPreloadedMedia = this._cabanaCarouselPreloadedMedia || new Set(); 29537 const lotCards = filteredGroups.map((group, index) => { 29538 const count = group.variants.length; 29539 const activeIndex = count ?((carouselStep + Number(carouselOffsets[group.key] || 0)) % count + count) % count : 0; 29540 const activeLot = group.variants[activeIndex]; 29541 const variantCards = group.variants.map((variant, variantIndex) => mapLot(variant, variantIndex)); 29542 variantCards.forEach((candidate) => { 29543 if (!candidate.hasMedia || !candidate.media || this._cabanaCarouselPreloadedMedia.has(candidate.media)) return; 29544 this._cabanaCarouselPreloadedMedia.add(candidate.media); 29545 const image = new Image(); 29546 image.src = candidate.media; 29547 if (typeof image.decode === 'function') image.decode().catch(() => {}); 29548 }); 29549 const card = variantCards[activeIndex] || mapLot(activeLot, index); 29550 const previousCard = (s.cbCarouselPreviousMedia || {})[group.key] || null; 29551 const isCrossfading = count > 1 && s.cbCarouselFading && (s.cbCarouselFadingKey === '*' || s.cbCarouselFadingKey === group.key) && !!previousCard; 29552 const cardMediaReady = (event) => { 29553 mediaReady(event); 29554 if (isCrossfading) this.revealCabanaCarouselCrossfade(); 29555 }; 29556 this._cabanaActiveCardByGroup[group.key] = card; 29557 return { 29558 ...card, 29559 mediaReady: cardMediaReady, 29560 mediaOpacity: isCrossfading && !s.cbCarouselCrossfadeActive ?0 : 1, 29561 hasPreviousMediaLayer: isCrossfading && !!(previousCard?.hasMedia || previousCard?.hasBackgroun
29561dLogo), 29562 // Keep the previous image fully opaque while the next one fades in. 29563 // Fading both layers simultaneously exposes the blue fallback between them. 29564 previousMediaOpacity: 1, 29565 previousMedia: previousCard?.media || '', 29566 previousHasMedia: !!previousCard?.hasMedia, 29567 previousBackgroundLogo: previousCard?.backgroundLogo || '', 29568 previousHasBackgroundLogo: !!previousCard?.hasBackgroundLogo, 29569 previousBackgroundLogoFilter: previousCard?.backgroundLogoFilter || 'none', 29570 open: () => this.openCabanaLot(group.variants[0], group.variants), 29571 hasVariants: count > 1, 29572 hasCardMetaRow: count > 1 || !!card.hasRankBadge, 29573 variantPosition: (activeIndex + 1) + ' / ' + count, 29574 dots: group.variants.map((variant, dotIndex) => ({ 29575 label: 'RP ' + String(variant?.rp || variant?.values?.rp || dotIndex + 1), 29576 select: (event) => this.selectCabanaCarouselSlide(group.key, dotIndex, event), 29577 bg: dotIndex === activeIndex ?(current ?this.cabanaColor(current) : '#0a5ca8') : '#c4d1df', 29578 width: dotIndex === activeIndex ?'25px' : '7px', 29579 })), 29580 }; 29581 }); 29582 const totalLotGroups = allLotGroups.length; 29583 const corralGroups = allLotGroups.filter(group => window.CabanaLotIdentity.resolve(group.variants[0], current).type === 'corral').length; 29584 const groupUnit = corralGroups && corralGroups === totalLotGroups ? 'corrales' : corralGroups ? 'lotes / corrales' : 'lotes'; 29585 const selected = s.currentCabanaLot; 29586 const selectedMapped = selected ?mapLot(selected, 0) : null; 29587 const selectedPrebidWinner = this.cabanaWinningPrebid(s.cabanaPrebids); 29588 const selectedPrebidIncrement = this.cabanaPrebidIncrement(current); 29589 const selectedPrebidBase = this.cabanaPrebidBase(selected); 29590 const selectedInstallments=window.CabanaInstallments.resolve(current?.installmentConfig,s.currentCabanaLot); 29591 const selectedPrebidSuggested = this.cabanaPrebidMinimum(); 29592 const selectedPrebidEndRaw = current?.prebid?.fechaHoraFinPreoferta || current?.prebid?.fin || current?.prebid?.endAt || current?.prebid?.endDate || ''; 29593 const selectedPrebidEnd = selectedPrebidEndRaw ?new Date(selectedPrebidEndRaw) : null; 29594 const selectedOwnWinner = s.user?.uid && window.CabanaPrebid.ownWinner(s.cabanaPrebids,selected?.itemId,this.cabanaPrebidIdentifier(selected).value); 29595 const selectedPrebidWindowOpen = prebidActive && (!selectedPrebidEnd || Number.isNaN(selectedPrebidEnd.getTime()) || selectedPrebidEnd.getTime() > Date.now()); 29596 const selectedPrebidForm = s.cabanaPrebidForm || { cuitCuil: '', amount: '', selectedIdentifiers: [], enComision: false, enComisionPara: '' }; 29597 const selectedPrebidCerts = this.cabanaPrebidAvailableCerts(); 29598 const selectedPrebidGrouped = this.cabanaPrebidGrouped(current); 29599 const selectedPrebidHistory = (Array.isArray(s.cabanaPrebids) ?[...s.cabanaPrebids] : []) 29600 .filter(prebid => !!s.user?.uid && prebid.isOwn === true) 29601 .sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0)) 29602 .slice(0, 10) 29603 .map((prebid) => { 29604 const createdAt = prebid.createdAt ?new Date(prebid.createdAt) : null; 29605 const hasValidDate = createdAt && !Number.isNaN(createdAt.getTime());
29606 const pending = /pendiente|pending/i.test(prebid.status); 29607 const liveWinner = prebid.isWinner && selectedPrebidWindowOpen; 29608 const ownOutbid = !!prebid.isOwn && prebid.status === 'Superada' && selectedPrebidWindowOpen; 29609 const isBaseOffer = !prebid.isWinner && prebid.status === 'Superada' && Number(prebid.amount) === Number(selectedPrebidBase); 29610 const ownWinningAnimal = !!s.user?.uid && !!window.CabanaPrebid.ownWinner([prebid],selected?.itemId,this.cabanaPrebidIdentifier(selected).value); 29611 const displayStatus = ownWinningAnimal 29612 ?(selectedPrebidWindowOpen ?'Estás ganando' : 'Ganada') 29613 :isBaseOffer ?'Base' :((prebid.isWinner || ownOutbid) ?'Superada' : prebid.status); 29614 const statusIsOutbid = ownOutbid && !isBaseOffer; 29615 const statusIsWinning = ownWinningAnimal; 29616 const statusIsCurrent = false; 29617 return { 29618 id: prebid.id, 29619 canDelete:this.canDeleteCabanaPrebid(),deleteOffer:()=>this.deleteCabanaPrebid(prebid),deleteBusy:!!s.cabanaPrebidDeleting, 29620 amount: this.fmtARS(prebid.amount), 29621 status: displayStatus, 29622 date: hasValidDate ?createdAt.toLocaleString('es-AR', { dateStyle: 'short', timeStyle: 'short' }) : 'Fecha no informada', 29623 certs: prebid.targets?.length ?prebid.targets.map(t => (t.characteristicIdentifierName || 'Cert.') + ' ' + (t.characteristicIdentifierValue ?? t.cert) + (t.isWinning ?' · Ganadora':' · ' + (prebid.status === 'Ganadora' ?'Superada':prebid.status))).join(' / ') : prebid.certs.join(', '), 29624 commission: prebid.enComisionPara ?('En comisión para ' + prebid.enComisionPara) : '', 29625 hasCommission: false, 29626 statusBg: statusIsOutbid ?'#ffe9ec' : (statusIsWinning ?'#e6f6ef' : (statusIsCurrent ?'#0b5fae' : (pending ?'#fff5dc' : '#eef4fa'))), 29627 statusColor: statusIsOutbid ?'#c6283d' : (statusIsWinning ?'#087653' : (statusIsCurrent ?'#ffffff' : (pending ?'#9a6500' : '#31526f'))), 29628 }; 29629 }); 29630 const selectedVariants = Array.isArray(s.currentCabanaLotVariants) && s.currentCabanaLotVariants.length ?s.currentCabanaLotVariants : (selected ?[selected] : []); 29631 const selectedVariantId = this.cabanaLotVariantId(selected); 29632 const clRpVariants = selectedVariants.map((variant, index) => { 29633 const active = this.cabanaLotVariantId(variant) === selectedVariantId; 29634 const mapped = mapLot(variant, index); 29635 const rpValue = String(variant?.rp || variant?.values?.rp || mapped.rp || index + 1).replace(/^RP\s+/i, ''); 29636 return { 29637 label: 'RP ' + rpValue, 29638 shortLabel: rpValue, 29639 thumb: this.cabanaLotItemCoverUrl(variant), 29640 hasThumb: !!this.cabanaLotItemCoverUrl(variant), 29641 noThumb: !this.cabanaLotItemCoverUrl(variant), 29642 active, 29643 className: 'dc-rp-variant-button' + (active ?' is-active' : ''), 29644 title: active ?('RP ' + rpValue + ' seleccionado') : ('Ver RP ' + rpValue), 29645 select: (event) => this.selectCabanaLotVariant(variant, event), 29646 border: active ?(current ?this.cabanaColor(current) : '#0a5ca8') : '#cad7e5', 29647 shadow: active ?'0 0 0 2px rgba(10,92,168,.18)' : 'none', 29648 }; 29649 }); 29650 const selectedVideos = selected ?this.cabanaLotVideos(selected) : []; 29651 const selectedCoverUrl = selected ?this.cabanaLotSelectedCoverUrl(selected) : ''; 29652 const selectedItemImages = selected ?this.cabanaLotImages(selected) : []; 29653 const selectedImages = [ 29654 ...(selectedCoverUrl ?[{ url: selectedCoverUrl, fileName: 'Portada del RP' }] : []), 29655 ...selectedItemImages.filter((image) => image.url !== selectedCoverUrl), 29656 ]; 29657 const selectedMediaItems = [ 29658 ...selectedVideos.map((video, index) => ({ ...video, type: 'video', url: video.url, thumb: video.thumb || selectedCoverUrl || '', label: 'Video ' + (index + 1), icon: 'Video' })), 29659 ...selectedImages.map((image, index) => ({ type: 'image', url: image.url, thumb: image.url, label: image.fileName || ('Imagen ' + (index + 1)), icon: 'Foto' })), 29660 ]; 29661 const selectedMediaIndex = Math.min(Number(s.cbLotVideo) || 0, Math.max(selectedMediaItems.length - 1, 0)); 29662 const selectedMedia = selectedMediaItems[selectedMediaIndex] || null; 29663 const selectedVideo = selectedMedia?.type === 'video' ?selectedMedia : null; 29664 const selectedImage = selectedMedia?.type === 'image' ?selectedMedia.url : selectedCoverUrl; 29665 const selectedMediaLoading = !!selected && (s.cabanaLotVideosLoading || selected.__videosLoaded !== true); 29666 const selectedMediaFailed = !!s.cabanaLotMediaFailed; 29667 const selectedHasVideo = !selectedMediaLoading && !!selectedVideo?.url; 29668 const selectedHasImage = !selectedMediaLoading && !selectedMediaFailed && !selectedVideo?.url && !!selectedImage; 29669 const selectedAssetLoading = !selectedMediaFailed && (selectedHasVideo || selectedHasImage) && !s.cabanaLotMediaReady; 29670 const dataCard = (label, keys, suffix = '') => { 29671 const rawValue = lotText(selected, keys, ''); 29672 return { label, value: String(rawValue) + (!isEmptyValue(rawValue) && suffix ?suffix : ''), show: !isEmptyValue(rawValue) }; 29673 }; 29674 const fieldValue = (field, form) => { 29675 const installmentText=this.cabanaInstallmentField(selected,field);if(installmentText!==null)return installmentText; 29676 const names = [field?.key, field?.name, this.consKey(field?.key), this.consKey(field?.name), cleanLabel(field?.name).toLowerCase().replace(/\s+/g, '_')].filter(
29676Boolean); 29677 const formKeys = [form?.key, form?.formKey, form?.name, this.consKey(form?.key), this.consKey(form?.name)].filter(Boolean); 29678 const buckets = [selected?.values, selected]; 29679 formKeys.forEach((formKey) => { 29680 const bucket = selected?.subformValues?.[formKey] || selected?.subformValues?.[this.consKey(formKey)]; 29681 if (bucket) buckets.unshift(bucket); 29682 }); 29683 for (const bucket of buckets) { 29684 if (!bucket || typeof bucket !== 'object') continue; 29685 for (const name of names) { 29686 if (bucket[name] !== undefined && bucket[name] !== null && bucket[name] !== '') return bucket[name]; 29687 const hit = Object.keys(bucket).find((key) => this.consKey(key) === this.consKey(name)); 29688 if (hit && bucket[hit] !== undefined && bucket[hit] !== null && bucket[hit] !== '') return bucket[hit]; 29689 } 29690 } 29691 return ''; 29692 }; 29693 const selectedCategorySchema = selected ?(Array.isArray(current?.formsByCategory) ?current.formsByCategory : []).find((category) => { 29694 const selectedCategoryKey = selected?.categoryId || selected?.categoryName || selected?.categoriaNombre || selected?.nombre_categoria || selected?.categoria || lotCategory(selected); 29695 return this.consKey(category?.categoryId || category?.categoryName) === this.consKey(selectedCategoryKey); 29696 }) : null; 29697 const schemaForms = selectedCategorySchema ?[ 29698 ...((selectedCategorySchema.fields || []).length ?[{ name: 'Datos principales', displayOrder: 0, isActive: true, fields: selectedCategorySchema.fields }] : []), 29699 ...(selectedCategorySchema.forms || []), 29700 ...(selectedCategorySchema.subforms || []), 29701 ] : []; 29702 const storedSubforms = Object.entries(selected?.subformValues || {}).map(([key, values], index) => ({ 29703 name: cleanLabel(key) || 'Datos del lote', 29704 displayOrder: index + 1, 29705 isActive: true, 29706 fields: Object.keys(values || {}).map((fieldKey, fieldIndex) => ({ key: fieldKey, name: cleanLabel(fieldKey), order: fieldIndex + 1 })), 29707 })); 29708 const uniqueForms = (forms) => { 29709 const byKey = new Map(); 29710 (forms || []).forEach((form, index) => { 29711 const key = this.consKey(form?.key || form?.formKey || form?.name) || ('form' + index); 29712 const previous = byKey.get(key); 29713 if (!previous) { 29714 byKey.set(key, form); 29715 return; 29716 } 29717 const previousFieldCount = (previous.fields || []).length; 29718 const nextFieldCount = (form.fields || []).length; 29719 if (nextFieldCount > previousFieldCount) { 29720 byKey.set(key, { 29721 ...form, 29722 displayOrder: Math.min(Number(previous.displayOrder || form.displayOrder || index + 1), Number(form.displayOrder || previous.displayOrder || index + 1)), 29723 }); 29724 } 29725 }); 29726 return Array.from(byKey.values()); 29727 }; 29728 const dynamicForms = uniqueForms(schemaForms.length ?schemaForms : storedSubforms); 29729 const mainFormKey = (form) => this.consKey(form?.key || form?.formKey || form?.name).replace(/-/g, '_'); 29730 const isMainForm = (form) => ['datos_principales', 'datosprincipales', 'principal', 'datos_del_lote', 'datoslote'].includes(mainFormKey(form)); 29731 const mainValueSource = selected?.values || {}; 29732 const explicitMainForms = dynamicForms.filter(isMainForm); 29733 const hasStructuredForms = !!(selectedCategorySchema && ((selectedCategorySchema.forms || []).length || (selectedCategorySchema.subforms || []).length)); 29734 const mainSchemaFields = [ 29735 ...(!hasStructuredForms ?(selectedCategorySchema?.fields || []) : []), 29736 ...explicitMainForms.flatMap((form) => form?.fields || []), 29737 ]; 29738 const usedDetailKeys = new Set(); 29739 const readMainValue = (field) => { 29740 const names = [field?.key, field?.name, this.consKey(field?.key), this.consKey(field?.name), cleanLabel(field?.name).toLowerCase().replace(/\s+/g, '_')].filter(
29740Boolean); 29741 for (const name of names) { 29742 if (mainValueSource[name] !== undefined && mainValueSource[name] !== null && mainValueSource[name] !== '') return mainValueSource[name]; 29743 const hit = Object.keys(mainValueSource).find((key) => this.consKey(key) === this.consKey(name)); 29744 if (hit && mainValueSource[hit] !== undefined && mainValueSource[hit] !== null && mainValueSource[hit] !== '') return mainValueSource[hit]; 29745 } 29746 return ''; 29747 }; 29748 const mainCardsFromSchema = mainSchemaFields.sort((a, b) => Number(a.order || 0) - Number(b.order || 0)).filter((field) => { 29749 const key = this.consKey(field?.key || field?.name); 29750 if (!key || usedDetailKeys.has(key)) return false; 29751 usedDetailKeys.add(key); 29752 return true; 29753 }).map((field) => { 29754 const value = displayValue(readMainValue(field)); 29755 [field?.key, field?.name, this.consKey(field?.key), this.consKey(field?.name)].filter(Boolean).forEach((key) => usedDetailKeys.add(this.consKey(key))); 29756 return { label: String(field?.name || 'Campo').toUpperCase(), value, show: !isEmptyValue(value) }; 29757 }); 29758 const mainCardsFromValues = mainSchemaFields.length ?[] : Object.entries(mainValueSource).filter(([key]) => !usedDetailKeys.has(this.consKey(key))).map(([key, value]) => { 29759 usedDetailKeys.add(this.consKey(key)); 29760 return { label: cleanLabel(key).toUpperCase(), value: displayValue(value), show: !isEmptyValue(displayValue(value)) }; 29761 }); 29762 const mainCards = [...mainCardsFromSchema, ...mainCardsFromValues].filter((card) => card.show); 29763 const formIdentityKey = (form) => this.consKey(form?.key || form?.formKey || form?.name); 29764 const isIdentityForm = (form) => ['identificacion', 'identification'].includes(formIdentityKey(form)); 29765 const isDepForm = (form) => { 29766 const key = formIdentityKey(form); 29767 return key === 'deps' || key === 'dep' || key === 'depes' || key.includes('deps'); 29768 }; 29769 const isPrecisionForm = (form) => { 29770 const key = formIdentityKey(form); 29771 return key === 'precision' || key === 'precisiones' || key.includes('precision'); 29772 }; 29773 const depPrecisionForms = dynamicForms 29774 .filter((form) => form?.isActive !== false && !isMainForm(form) && (isDepForm(form) || isPrecisionForm(form))) 29775 .sort((a, b) => { 29776 const formOrder = (isDepForm(a) ?0 : 1) - (isDepForm(b) ?0 : 1); 29777 return formOrder || Number(a.displayOrder || 0) - Number(b.displayOrder || 0); 29778 }); 29779 const depPrecisionKeys = new Set(depPrecisionForms.map((form) => formIdentityKey(form))); 29780 const depPrecisionColumns = []; 29781 depPrecisionForms.forEach((form) => { 29782 (form?.fields || []).sort((a, b) => Number(a.order || 0) - Number(b.order || 0)).forEach((field) => { 29783 const key = this.consKey(field?.key || field?.name); 29784 if (!key || depPrecisionColumns.some((column) => column.key === key)) return; 29785 depPrecisionColumns.push({ key, label: String(field?.name || cleanLabel(key)).toUpperCase() }); 29786 }); 29787 }); 29788 const detailTables = depPrecisionForms.length && depPrecisionColumns.length ?[{ 29789 title: "DEP'S Y PRECISIÃN", 29790 columns: depPrecisionColumns, 29791 rows: depPrecisionForms.map((form) => ({ 29792 label: isDepForm(form) ?"DEP'S" : 'PRECISIÃN', 29793 mobileLabel: isDepForm(form) ?"DEP'S" : 'PREC.', 29794 cells: depPrecisionColumns.map((column) => { 29795 const field = (form?.fields || []).find((candidate) => this.consKey(candidate?.key || candidate?.name) === column.key) || { key: column.key, name: column.label }; 29796 const value = displayValue(fieldValue(field, form)); 29797 [field?.key, field?.name, column.key].filter(Boolean).forEach((key) => usedDetailKeys.add(this.consKey(key))); 29798 return { label: column.label, value: isEmptyValue(value) ?'â' : value, show: !isEmptyValue(value) }; 29799 }), 29800 })).filter((row) => row.cells.some((cell) => cell.show)), 29801 }].filter((table) => table.rows.length > 0) : []; 29802 const detailGroups = selected ?[ 29803 ...(mainCards.length ?[{ label: 'DATOS PRINCIPALES', cards: mainCards }] : []), 29804 ...dynamicForms.filter((form) => form?.isActive !== false && !isMainForm(form) && !isIdentityForm(form) && !depPrecisionKeys.has(formIdentityKey(form))).sort((a, b) => Number(a.displayOrder || 0) - Number(b.displayOrder || 0)).map((form) => ({ 29805 label: String(form?.name || 'DATOS DEL LOTE').toUpperCase(), 29806 cards: (form?.fields || []).sort((a, b) => Number(a.order || 0) - Number(b.order || 0)).map((field) => { 29807 const value = displayValue(fieldValue(field, form)); 29808 [field?.key, field?.name, this.consKey(field?.key), this.consKey(field?.name)].filter(
29808Boolean).forEach((key) => usedDetailKeys.add(this.consKey(key))); 29809 return { label: String(field?.name || 'Campo').toUpperCase(), value, show: !isEmptyValue(value) }; 29810 }), 29811 })), 29812 ].map((group) => ({ ...group, cards: group.cards.filter((card) => card.show) })).filter((group) => group.cards.length > 0) : []; 29813 const extraCards = selected ?Object.entries(selected?.values || {}).filter(([key, value]) => !usedDetailKeys.has(this.consKey(key)) && !['rp', 'categoriaNombre', 'categoryName'].some((known) => this.consKey(known) === this.consKey(key))).map(([key, value]) => ({ label: cleanLabel(key).toUpperCase(), value: displayValue(value), show: !isEmptyValue(displayValue(value)) })).filter((card) => card.show) : []; 29814 if (extraCards.length) detailGroups.push({ label: 'OTROS DATOS', cards: extraCards }); 29815 const isObservationCard = (card) => ['observaciones', 'observacion', 'observations', 'descripcion', 'lotdescription'].includes(this.consKey(card.label)); 29816 const observationCards = detailGroups.flatMap((group) => group.cards).filter(isObservationCard); 29817 const observations = selected ?String(lotText(selected, ['observaciones', 'descripcion', 'lotDescription'], '') || observationCards.map((card) => card.value).filter(Boolean).join('\n\n')) : ''; 29818 const withoutObservations = (groups) => groups.map((group) => ({ ...group, cards: group.cards.filter((card) => !isObservationCard(card)) })).filter((group) => group.cards.length); 29819 const primaryDetailGroups = withoutObservations(mainCards.length ?[{ label: 'DATOS PRINCIPALES', cards: mainCards }] : []); 29820 const secondaryDetailGroups = withoutObservations(detailGroups.filter((group) => this.consKey(group.label) !== 'datosprincipales')); 29821 const selectedCabanaForDetail = selected ?(participants.find((cabana) => (selected?.cabanaKey && cabana.key === selected.cabanaKey) || (selected?.cabanaId && String(cabana.id || '') === String(selected.cabanaId)) || (selected?.cabanaNombre && cabana.name === selected.cabanaNombre)) || {}) : {}; 29822 const selectedCabanaLogo = selected ?window.resolveCabanaMediaUrl(selected.cabanaLogoDataUrl || selectedCabanaForDetail.logoDataUrl) : ''; 29823 const selectedCabanaName = selected ?(selected.cabanaNombre || selected.cabanaName || selectedCabanaForDetail.name || '') : ''; 29824 const detailStacked = s.isMobile || (typeof window !== 'undefined' && window.innerWidth < 1120); 29825 const cabanasInitialLoading = !!s.cabanasLoading && (!Array.isArray(s.cabanas) || !s.cabanasCollectionReady); 29826 const cabanaLotsInitialLoading = !!s.cabanaLotsLoading && !Array.isArray(s.cabanaLots); 29827 const cabanaCountsReady = Array.isArray(s.cabanaLots) && !cabanaLotsInitialLoading; 29828 const salesOrderLots = Array.isArray(s.cabanaSalesOrderLots) ?s.cabanaSalesOrderLots : []; 29829 const makeSalesOrderRow = (lot, index, scopeIndex = index, scopeCount = salesOrderLots.length) => { 29830 const cert = this.cabanaLotId(lot); 29831 const values = lot?.values || {}; 29832 const identifier = values.lote || lot?.lotNumber || values.rp || lot?.rp || cert; 29833 const rp = values.rp || lot?.rp || ''; 29834 return { 29835 cert, 29836 title: 'Lote ' + identifier + (rp && String(rp) !== String(identifier) ?' · RP ' + rp : ''), 29837 category: this.cabanaSalesCategoryName(lot), 29838 cabana: lot?.cabanaNombre || lot?.cabanaName || 'Cabaña sin asignar', 29839 position: index + 1, 29840 groupPosition: scopeIndex + 1, 29841 positions: Array.from({ length: scopeCount }, (_, position) => ({ value: position + 1, label: position + 1 })), 29842 isFirst: scopeIndex === 0, 29843 isLast: scopeIndex === scopeCount - 1, 29844 moveUp: () => this.setCabanaSalesLotPosition(cert, scopeIndex), 29845 moveDown: () => this.setCabanaSalesLotPosition(cert, scopeIndex + 2), 29846 setPosition: (event) => this.setCabanaSalesLotPosition(cert, event.target.value), 29847 }; 29848 }; 29849 const salesOrderRows = salesOrderLots.map(makeSalesOrderRow); 29850 const salesOrderGroupsRaw = []; 29851 const salesOrderGroupMap = new Map();
29852 salesOrderLots.forEach((lot, index) => { 29853 const key = this.cabanaSalesCategoryKey(lot); 29854 if (!salesOrderGroupMap.has(key)) { 29855 const group = { key, title: this.cabanaSalesCategoryName(lot), lots: [] }; 29856 salesOrderGroupMap.set(key, group); 29857 salesOrderGroupsRaw.push(group); 29858 } 29859 salesOrderGroupMap.get(key).lots.push({ lot, index }); 29860 }); 29861 const salesOrderGroups = salesOrderGroupsRaw.map((group, groupIndex) => ({ 29862 ...group, 29863 count: group.lots.length, 29864 lots: group.lots.map((entry, scopeIndex) => makeSalesOrderRow(entry.lot, entry.index, scopeIndex, group.lots.length)), 29865 position: groupIndex + 1, 29866 positions: Array.from({ length: salesOrderGroupsRaw.length }, (_, position) => ({ value: position + 1, label: position + 1 })), 29867 isFirst: groupIndex === 0, 29868 isLast: groupIndex === salesOrderGroupsRaw.length - 1, 29869 moveUp: () => this.setCabanaSalesCategoryPosition(group.key, groupIndex), 29870 moveDown: () => this.setCabanaSalesCategoryPosition(group.key, groupIndex + 2), 29871 setPosition: (event) => this.setCabanaSalesCategoryPosition(group.key, event.target.value), 29872 })); 29873 const salesOrderAccent = current ?this.cabanaColor(current) : '#0a5ca8'; 29874 const auctionTitle = current?.nombre_remate || current?.title || current?.titulo || 'La Cassina'; 29875 const absolutePublicUrl = (path) => { 29876 try { return new URL(path, window.location.origin).href; } 29877 catch (_) { return String(path || ''); } 29878 }; 29879 const auctionPublicUrl = this.state.publicAdvanceMode === 'lacassina' ?absolutePublicUrl('/lacassina') : absolutePublicUrl(window.location.pathname || '/'); 29880 const selectedPublicUrl = selected && this.state.publicAdvanceMode === 'lacassina' ?absolutePublicUrl(this.cabanaPublicLotPath(selected)) : auctionPublicUrl; 29881 const selectedRpRaw = String(selectedMapped?.rp || '').trim(); 29882 const selectedRpCode = selectedRpRaw.replace(/^rp\s*/i, '').trim(); 29883 const selectedRpText = selectedRpCode ?'el RP ' + selectedRpCode : 'el RP seleccionado'; 29884 const contactGreeting = currentContact.name ?String(currentContact.name).split(/\s+/)[0] : ''; 29885 const generalWhatsappText = 'Hola' + (contactGreeting ?' ' + contactGreeting : '') + ', quisiera consultar por los lotes del remate ' + auctionTitle + '. Enlace del remate: ' + auctionPublicUrl; 29886 const lotWhatsappText = 'Hola' + (contactGreeting ?' ' + contactGreeting : '') + ', quisiera consultar por ' + selectedRpText + ' del remate ' + auctionTitle + '. Enlace del RP: ' + selectedPublicUrl; 29887 const analyticsMetric = (row, keys) => { 29888 for (const key of keys) { 29889 const value = Number(row?.[key] || 0); 29890 if (value) return value; 29891 } 29892 return 0; 29893 }; 29894 const selectedLotIdKey = this.consKey(this.cabanaLotId(selected)); 29895 const selectedRpIdKey = this.consKey(this.cabanaAnalyticsRpId(selected)); 29896 const analyticsRowMatchesLot = (row) => { 29897 const keys = [row?.lotId, row?.lotCert, row?.cert, row?.id].filter(Boolean).map((value) => this.consKey(value)); 29898 return selectedLotIdKey && keys.includes(selectedLotIdKey); 29899 }; 29900 const analyticsRowMatchesRp = (row) => { 29901 const keys = [row?.rp, row?.rpId, row?.itemId, row?.itemKey].filter(Boolean).map((value) => this.consKey(value)); 29902 return selectedRpIdKey && keys.includes(selectedRpIdKey); 29903 }; 29904 const selectedAnalyticsRpRow = selected ?(analyticsRps.find((row) => analyticsRowMatchesRp(row) && (!row?.lotId || analyticsRowMatchesLot(row))) || analyticsRps.find(analyticsRowMatchesRp)) : null; 29905 const selectedAnalyticsLotRow = selected ?analyticsLots.find(analyticsRowMatchesLot) : null; 29906 const selectedSocialKey = selected ?this.cabanaSocialKey(selected) : ''; 29907 const selectedSocialBump = (s.cabanaSocialBumps || {})[selectedSocialKey] || {}; 29908 const selectedShareCount = (analyticsMetric(selectedAnalyticsRpRow, ['shares','shareClicks','sharedCount','event_share_click']) || analyticsMetric(selectedAnalyticsLotRow, ['shares','shareClicks','sharedCount','event_share_click'])) + Number(selectedSocialBump.shares || 0); 29909 const commercialDrafts = s.cabanaCommercialDrafts || {}; 29910 const commercialStatusMeta = { 29911 vendido: { label: 'Vendido', className: 'is-vendido', icon: 'check-circle-2', color: '#17704d', bg: '#e7f5ee' }, 29912 pendiente: { label: 'Pendiente', className: 'is-pendiente', icon: 'clock-3', color: '#9a6700', bg: '#fff6df' }, 29913 'no-vendido': { label: 'No vendido', className: 'is-no-vendido', icon: 'x-circle', color: '#c03434', bg: '#fff0f0' }, 29914 levantado: { label: 'Levantado', className: 'is-levantado', icon: 'circle-off', color: '#4e6279', bg: '#eef2f6' }, 29915 }; 29916 const commercialStatusKeys = ['vendido', 'pendiente', 'no-vendido', 'levantado']; 29917 const commercialDefaultText = (value, fallback = '') => { 29918 const text = String(value ?? '').trim(); 29919 return text ?text : fallback; 29920 }; 29921 const commercialIdentifierCount = (value) => String(value ?? '') 29922 .split(/\s*(?:-|,|;|\||\r?\n)\s*/) 29923 .map((identifier) => identifier.trim()) 29924 .filter(Boolean) 29925 .length; 29926 const commercialPriceDigits = (value) => String(value ?? '').replace(/\D/g, ''); 29927 const commercialPriceInputFormat = new Intl.NumberFormat('es-AR', { maximumFractionDigits: 0 }); 29928 const commercialNumberText = (value) => { 29929 const digits = commercialPriceDigits(value); 29930 if (!digits) return ''; 29931 const parsed = Number(digits); 29932 return Number.isFinite(parsed) ?commercialPriceInputFormat.format(parsed) : digits; 29933 }; 29934 const commercialParsePrice = (value) => { 29935 const digits = commercialPriceDigits(value); 29936 if (!digits) return null; 29937 const parsed = Number(digits); 29938 return Number.isFinite(parsed) && parsed > 0 ?parsed : null; 29939 }; 29940 const commercialCabanaForLot = (lot) => participants.find((cabana) => 29941 (lot?.cabanaKey && cabana.key === lot.cabanaKey) ||
29942 (lot?.cabanaId && String(cabana.id || '') === String(lot.cabanaId)) || 29943 (lot?.cabanaNombre && (cabana.name === lot.cabanaNombre || cabana.nombre === lot.cabanaNombre)) || 29944 (lot?.cabanaName && (cabana.name === lot.cabanaName || cabana.nombre === lot.cabanaName)) 29945 ) || participants.find((cabana) => cabana.type === 'principal') || participants[0] || {}; 29946 const commercialAllRows = lots.map((lot, index) => { 29947 const key = this.cabanaCommercialRowKey(lot) || String(index); 29948 const draft = commercialDrafts[key] || {}; 29949 const status = commercialStatusKeys.includes(draft.status) ?draft.status : 'pendiente'; 29950 const installments=window.CabanaInstallments.resolve(current?.installmentConfig,lot); 29951 const priceMode = installments.enabled?'cabeza':draft.priceMode === 'kilo' ?'kilo' : 'cabeza'; 29952 const faenaEnabled = !!draft.faenaEnabled; 29953 const commercialIdentity = window.CabanaLotIdentity.resolve(lot, current); 29954 const lotNumber = commercialIdentity.number || this.cabanaLotNumber(lot) || lotText(lot, ['lote', 'lotNumber', 'numero_lote'], ''); 29955 const rpRaw = String(lotText(lot, ['rp', 'RP', 'numero_rp', 'identifier'], index + 1)).replace(/^RP\s+/i, ''); 29956 const paintNumber = lotText(lot, ['n', 'N', 'nro', 'numero', 'numero_pintura', 'numeroPintura', 'nro_pintura', 'pintura', 'paint', 'paintNumber'], ''); 29957 const explicitQuantity = commercialDefaultText(draft.cantidad, commercialDefaultText(lotText(lot, ['cantidad', 'cant', 'cantidad_animales'], ''), '')); 29958 const corralQuantity = Math.max(Number(lot?.groupItemCount) || 0, commercialIdentifierCount(rpRaw)); 29959 const quantity = explicitQuantity || (commercialIdentity.type === 'corral' && corralQuantity ?String(corralQuantity) : ''); 29960 const weight = commercialDefaultText(draft.pesoProm, commercialDefaultText(lotText(lot, ['pesoPromedio', 'peso_promedio', 'peso', 'peso_actual'], ''), '')); 29961 const faenaPercent = commercialDefaultText(draft.faenaPercent, commercialDefaultText(lotText(lot, ['faena', 'porcentaje_faena', 'porcentajeFaena'], ''), '')); 29962 const loadedPrice = priceMode === 'kilo' ?commercialParsePrice(draft.precioKilo) : commercialParsePrice(draft.precioCabeza); 29963 const quantityNumeric = Number(commercialPriceDigits(quantity)) || (installments.enabled?1:0); 29964 const divisions=draft.divisions||[]; 29965 const saleParts=window.RosganSaleDivisions.sales({cantidad:quantityNumeric},draft,priceMode).map(p=>({...p,price:window.CabanaInstallments.total(p.price,installments)})); 29966 const totalNumeric = saleParts.reduce((n,p)=>n+p.price*p.quantity*(priceMode==='kilo'?Number(weight)||0:1),0)||null; 29967 const commercialCabana = commercialCabanaForLot(lot); 29968 return { 29969 raw: lot, 29970 key, 29971 order: commercialDefaultText(lot?.groupDisplayOrder || lot?.displayOrder, String(index + 1)), 29972 groupLabel: commercialIdentity.label || 'Lote', 29973 groupType: commercialIdentity.type || 'lote', 29974 lotNumber: commercialDefaultText(lotNumber, '-'), 29975 rp: commercialDefaultText(rpRaw, '-'), 29976 paintNumber: commercialDefaultText(paintNumber, '-'), 29977 category: lotCategory(lot), 29978 cabana: commercialDefaultText(lot?.cabanaNombre || lot?.cabanaName || lot?.cabana?.nombre || lot?.cabana?.name || commercialCabana.name || commercialCabana.nombre, '-'), 29979 consignataria: currentConsignatariaName || 'Rosgan', 29980 categoryDisplayOrder: this.cabanaCategoryDisplayOrder(lot, index + 1), 29981 hasConsLogo: !!currentConsignatariaWhiteLogo, 29982 noConsLogo: !currentConsignatariaWhiteLogo, 29983 consLogo: currentConsignatariaWhiteLogo, 29984 consInitial: (currentConsignatariaName || 'C').charAt(0).toUpperCase(), 29985 consColor: currentConsignatariaColor, 29986 status, 29987 statusLabel: status==='pendiente'&&saleParts.length?'Venta parcial':commercialStatusMeta[status]?.label || 'Pendiente', 29988 divisions,saleParts,hasDivisions:divisions.length>0, 29989 divideSale:()=>this.openCabanaSaleDivisions(lot,quantity,lotNumber), 29990 divisionLabel:divisions.length?divisions.length+' partes · '+saleParts.reduce((n,p)=>n+p.quantity,0)+' / '+quantity+' vendidas':'Dividir venta', 29991 statusBg: commercialStatusMeta[status]?.bg || '#fff6df', 29992 statusBorder: commercialStatusMeta[status]?.color || '#9a6700', 29993 statusColor: commercialStatusMeta[status]?.color || '#9a6700', 29994 statusOptions: commercialStatusKeys.map((keyName) => ({ 29995 key: keyName, 29996 label: commercialStatusMeta[keyName].label, 29997 className: keyName === status ?commercialStatusMeta[keyName].className : '', 29998 set: () => { if(divisions.length&&['vendido','pendiente'].includes(keyName)){this.openCabanaSaleDivisions(lot,quantity,lotNumber);return;}this.updateCabanaCommercialRow(lot, { status: keyName }); }, 29999 })), 30000 priceMode, 30001 installmentCount:installments.count, 30002 headPriceLabel:installments.enabled?'$ / Cuota':'$ / Cabeza', 30003 installmentLabel:installments.enabled?installments.count+' cuotas'+(installments.inherited?' · General':' · Este lote'):'Cuotas del lote', 30004 installmentTotal:installments.enabled&&loadedPrice?'Total por cabeza: '+this.fmtARS(window.CabanaInstallments.total(loadedPrice,installments)):'', 30005 editInstallments:()=>this.editCabanaInstallments(lot), 30006 kiloModeClass: priceMode === 'kilo' ?'is-active' : '', 30007 cabezaModeClass: priceMode === 'cabeza' ?'is-active' : '', 30008 priceDisabled: status !== 'pendiente'||divisions.length>0, 30009 setKilo: () => { if(installments.enabled){this.showToast('La modalidad por cuota se calcula por cabeza.');return;} if (status === 'pendiente'&&!divisions.length) this.updateCabanaCommercialRow(lot, { priceMode: 'kilo' }); }, 30010 setCabeza: () => { if (status === 'pendiente'&&!divisions.length) this.updateCabanaCommercialRow(lot, { priceMode: 'cabeza' }); }, 30011 pricePlaceholder: installments.enabled?'$ / Cuota':priceMode === 'kilo' ?'$ / Kilo' : '$ / Cabeza', 30012 priceValue: divisions.length?'Ver divisiones':priceMode === 'kilo' ?commercialNumberText(draft.precioKilo) : commercialNumberText(draft.precioCabeza), 30013 priceNumeric: loadedPrice, 30014 priceUnit: priceMode === 'kilo' ?'$/kg' : '$/cab.', 30015 priceUnitLabel: installments.enabled?'Cuota à '+installments.count:priceMode === 'kilo' ?'Por kilo' : 'Por cabeza', 30016 priceDisplay: loadedPrice ?'$ ' + commercialPriceInputFormat.format(loadedPrice) : '-', 30017 totalNumeric, 30018 totalDisplay: totalNumeric ?'$ ' + commercialPriceInputFormat.format(totalNumeric) : '-', 30019 commercialUpdatedAt: draft.updatedAt || '', 30020 setPrice: (event) => { 30021 if (status !== 'pendiente') return; 30022 this.formatMoneyInputEvent(event, (raw) => this.queueCabanaCommercialPriceInput(lot, priceMode, raw)); 30023 }, 30024 commitPrice: (event) => { 30025 if (status !== 'pendiente') return; 30026 this.flushCabanaCommercialPriceInput(lot, priceMode, commercialPriceDigits(event.target.value)); 30027 }, 30028 commitPriceEnter: (event) => { 30029 if (event.key !== 'Enter') return; 30030 event.preventDefault(); 30031 const price = commercialParsePrice(event.target.value); 30032 this.flushCabanaCommercialPriceInput(lot, priceMode, commercialPriceDigits(event.target.value), { status: price ?'vendido' : 'no-vendido' }); 30033 setTimeout(() => { 30034 try { 30035 const inputs = Array.from(document.querySelectorAll('.commercial-board .commercial-price input:not([disabled])'));
30036 const index = inputs.indexOf(event.target); 30037 const next = inputs[index + 1]; 30038 if (next) { next.focus(); next.select?.(); } 30039 else event.target.blur(); 30040 } catch {} 30041 }, 40); 30042 }, 30043 faenaEnabled, 30044 faenaSwitchClass: faenaEnabled ?'commercial-switch is-on' : 'commercial-switch', 30045 toggleFaena: () => this.updateCabanaCommercialRow(lot, { faenaEnabled: !faenaEnabled }), 30046 faenaPercent, 30047 setFaenaPercent: (event) => this.updateCabanaCommercialRow(lot, { faenaPercent: event.target.value, faenaEnabled: true }), 30048 cantidad: quantity, 30049 setCantidad: (event) => this.updateCabanaCommercialRow(lot, { cantidad: event.target.value }), 30050 pesoProm: weight, 30051 setPesoProm: (event) => this.updateCabanaCommercialRow(lot, { pesoProm: event.target.value }), 30052 plazo: commercialDefaultText(draft.plazo, ''), 30053 setPlazo: (event) => this.updateCabanaCommercialRow(lot, { plazo: event.target.value }), 30054 comprador: commercialDefaultText(draft.comprador, ''), 30055 setComprador: (event) => this.updateCabanaCommercialRow(lot, { comprador: event.target.value }), 30056 moreOpen: s.cabanaCommercialOpenRow === key, 30057 moreClassName: s.cabanaCommercialOpenRow === key ?'commercial-more is-open' : 'commercial-more', 30058 moreIcon: 'assets/icons/lucide/' + (s.cabanaCommercialOpenRow === key ?'chevron-up' : 'chevron-down') + '.svg', 30059 toggleMore: () => this.toggleCabanaCommercialMore(lot), 30060 }; 30061 }); 30062 const commercialStats = commercialStatusKeys.reduce((acc, key) => ({ ...acc, [key]: commercialAllRows.filter((row) => row.status === key).length }), {}); 30063 const commercialTotal = commercialAllRows.length; 30064 const commercialPct = (value) => commercialTotal ?Math.round((value / commercialTotal) * 100) + '%' : '0%'; 30065 const commercialFilter = s.cabanaCommercialFilter || 'todos'; 30066 const commercialFilteredRows = commercialFilter === 'todos' ?commercialAllRows : commercialAllRows.filter((row) => row.status === commercialFilter); 30067 let commercialLastCategory = ''; 30068 const commercialRows = commercialFilteredRows.map((row) => { 30069 const categoryKey = this.consKey(row.category || 'Sin categoria'); 30070 const showCategoryDivider = categoryKey !== commercialLastCategory; 30071 commercialLastCategory = categoryKey; 30072 return { ...row, showCategoryDivider, categoryDividerTitle: row.category || 'Sin categoria' }; 30073 }); 30074 const commercialReportRows = commercialFilteredRows.flatMap(row=>row.divisions?.length?row.divisions.map((part,i)=>{ 30075 const sold=part.price>0&&!['no-vendido','levantado'].includes(row.status),total=sold?part.price*row.installmentCount*part.quantity*(row.priceMode==='kilo'?Number(row.pesoProm)||0:1):0; 30076 return {...row,rp:row.rp+' · Parte '+(i+1),cantidad:part.quantity,comprador:part.buyer||'',status:sold?'vendido':row.status==='vendido'?'pendiente':row.status,statusLabel:sold?'Vendido':row.status==='no-vendido'?'No vendido':row.status==='levantado'?'Levantado':'Pendiente',priceNumeric:sold?part.price:0,priceDisplay:sold?'$ '+commercialPriceInputFormat.format(part.price):'-',totalNumeric:total,totalDisplay:total?'$ '+commercialPriceInputFormat.format(total):'-'}; 30077 }):[row]); 30078 const commercialView = s.cabanaCommercialView === 'lista' ?'lista' : 'carga'; 30079 const commercialMetricCards = [ 30080 { label: 'Total RPs', value: commercialTotal, pct: '', icon: 'list-checks', color: '#0a5ca8', bg: '#e8f2fc' }, 30081 { label: 'Vendidos', value: commercialStats.vendido || 0, pct: commercialPct(commercialStats.vendido || 0), icon: 'circle-check-big', color: '#17704d', bg: '#e7f5ee' }, 30082 { label: 'Pendientes', value: commercialStats.pendiente || 0, pct: commercialPct(commercialStats.pendiente || 0), icon: 'clock-3', color: '#9a6700', bg: '#fff6df' }, 30083 { label: 'No vendidos', value: commercialStats['no-vendido'] || 0, pct: commercialPct(commercialStats['no-vendido'] || 0), icon: 'circle-x', color: '#c03434', bg: '#fff0f0' }, 30084 ].map((item) => ({ ...item, iconUrl: 'assets/icons/lucide/' + item.icon + '.svg' })); 30085 const commercialFilters = [ 30086 ['todos', 'Todos', commercialTotal], 30087 ['pendiente', 'Pendientes', commercialStats.pendiente || 0], 30088 ['vendido', 'Vendidos', commercialStats.vendido || 0], 30089 ['no-vendido', 'No vendidos', commercialStats['no-vendido'] || 0], 30090 ['levantado', 'Levantados', commercialStats.levantado || 0], 30091 ].map(([key, label, count]) => { 30092 const meta = key === 'todos' 30093 ?{ color: '#0a5ca8', bg: '#e8f2fc', activeBg: '#002e5f', activeColor: '#fff', border: '#cfe0f5' } 30094 : commercialStatusMeta[key] || commercialStatusMeta.pendiente; 30095 const active = key === commercialFilter; 30096 return { 30097 key, label, count, 30098 className: active ?'commercial-filter is-active' : 'commercial-filter', 30099 bg: active ?(meta.activeBg || meta.color) : (meta.bg || '#fff'), 30100 color: active ?(meta.activeColor || '#fff') : (meta.color || '#31526f'), 30101 border: active ?(meta.activeBg || meta.color) : (meta.color || meta.border || '#d8e5f1'), 30102 pillBg: active ?'rgba(255,255,255,.2)' : (meta.bg || 'rgba(10,92,168,.1)'), 30103 set: () => this.setCabanaCommercialFilter(key), 30104 }; 30105 }); 30106 const priceBoardAnimalCountCategories = new Set([ 30107 'vaquillonascgtiadeprenezanguspc', 30108 'vaquillonasccriabrangus', 30109 'vaquillonascgtiadeprenezangusmas', 30110 ]); 30111 const priceLoadedGroups = new Map(); 30112 commercialAllRows.forEach((row) => { 30113 if (!row.saleParts.length) return;
30114 const key = this.consKey(row.category || 'Sin categoria'); 30115 if (!priceLoadedGroups.has(key)) { 30116 priceLoadedGroups.set(key, { 30117 category: row.category || 'Sin categoria', 30118 categoryDisplayOrder: row.categoryDisplayOrder || 9999, 30119 countByAnimals: priceBoardAnimalCountCategories.has(key), 30120 entries: [], 30121 }); 30122 } 30123 const group = priceLoadedGroups.get(key); 30124 group.categoryDisplayOrder = Math.min(group.categoryDisplayOrder || 9999, row.categoryDisplayOrder || 9999); 30125 row.saleParts.forEach(part=>group.entries.push({value:part.price,quantity:part.quantity,updatedAt:row.commercialUpdatedAt||''})); 30126 }); 30127 const priceBoardRows = Array.from(priceLoadedGroups.values()).map((group) => { 30128 const values = group.entries.map((entry) => entry.value); 30129 const count = group.countByAnimals 30130 ?group.entries.reduce((sum, entry) => sum + entry.quantity, 0) 30131 : values.length; 30132 const max = Math.max(...values); 30133 const min = Math.min(...values); 30134 const avg = group.entries.reduce((sum,entry)=>sum+entry.value*entry.quantity,0)/group.entries.reduce((sum,entry)=>sum+entry.quantity,0); 30135 const orderedEntries = [...group.entries].sort((a, b) => String(a.updatedAt || '').localeCompare(String(b.updatedAt || ''))); 30136 let runningSum = 0, runningQuantity = 0; 30137 const trendHistory = orderedEntries.map((entry, index) => { 30138 runningSum += entry.value*entry.quantity;runningQuantity+=entry.quantity; 30139 return { time: entry.updatedAt || String(index), value: runningSum / runningQuantity }; 30140 }); 30141 return { 30142 category: group.category, 30143 categoryDisplayOrder: group.categoryDisplayOrder || 9999, 30144 count, 30145 maxValue: max, 30146 avgValue: avg, 30147 trendHistory, 30148 countRoll: this.rollInteger(count), 30149 maxRoll: this.rollNumber(max), 30150 minRoll: this.rollNumber(min), 30151 avgRoll: this.rollNumber(avg), 30152 }; 30153 }).sort((a, b) => a.categoryDisplayOrder - b.categoryDisplayOrder || a.category.localeCompare(b.category)); 30154 const priceTrendColors = ['#0a5ca8', '#22b4bd', '#d91549', '#17704d', '#9a6700', '#6d5dfc']; 30155 this._cabanaPriceTrendPulse = this._cabanaPriceTrendPulse || {}; 30156 priceBoardRows.forEach((row) => { 30157 const key = this.consKey(row.category); 30158 const signature = (row.trendHistory || []).map((point) => Math.round(point.value)).join('|'); 30159 if ((this._cabanaPriceTrendSignatures || {})[key] !== signature) { 30160 this._cabanaPriceTrendSignatures = { ...(this._cabanaPriceTrendSignatures || {}), [key]: signature }; 30161 this._cabanaPriceTrendPulse[key] = (Number(this._cabanaPriceTrendPulse[key] || 0) + 1) % 2; 30162 } 30163 }); 30164 const priceBoardRowsWithTrend = priceBoardRows.map((row, index) => { 30165 const history = row.trendHistory || []; 30166 const visibleHistory = history.length > 1 ?history : [history[0], history[0]].filter(Boolean); 30167 const trendValues = visibleHistory.map((point) => point.value).filter((value) => value > 0); 30168 const trendMin = trendValues.length ?Math.min(...trendValues) : 0; 30169 const trendMax = trendValues.length ?Math.max(...trendValues) : 1; 30170 const trendRange = Math.max(1, trendMax - trendMin); 30171 const trendY = (value) => trendMax === trendMin ?35 : 58 - ((value - trendMin) / trendRange) * 42; 30172 const points = visibleHistory.map((point, pointIndex, arr) => { 30173 const x = arr.length <= 1 ?0 : 12 + (pointIndex / (arr.length - 1)) * 236; 30174 const y = trendY(point.value); 30175 return Math.round(x) + ',' + Math.round(y); 30176 }).join(' '); 30177 const lastRaw = visibleHistory.slice(-1)[0] || { value: 0 }; 30178 const lastX = visibleHistory.length > 1 ?248 : 12; 30179 const lastY = trendY(lastRaw.value); 30180 return { ...row, trendColor: priceTrendColors[index % priceTrendColors.length], trendClass: 'price-category-spark ' + ((this._cabanaPriceTrendPulse[this.consKey(row.category)] || 0) ?'is-a' : 'is-b'), trendPoints: points, trendLastX: Math.round(lastX), trendLastY: Math.round(lastY) }; 30181 }); 30182 const priceBoardUpdatedAt = Object.values(commercialDrafts || {}).map((row) =>
30182 row?.updatedAt).filter(Boolean).sort().pop(); 30183 const cabanaPriceModel={title:current?.nombre_remate||current?.titulo||current?.title||'Remate de cabaña', 30184 date:(current?this.cabanaAuctionStartAt(current):null)?.toLocaleDateString('es-AR',{day:'numeric',month:'long',year:'numeric'})||'', 30185 location:current?.location?.lugar||'',logo:'/assets/rosgan-logo-blue.png',favicon:'/favicon.png',background:'/assets/lacassina-story-background.png', 30186 hideRosganLogo:true, 30187 topBrands:participants.filter(b=>b.type==='principal').map(b=>({logo:b.logoDataUrl||'',name:b.nombre||b.name||'Cabaña',plain:true})), 30188 brands:[{name:currentConsignatariaName,logo:currentConsignatariaWhiteLogo||this.consWhiteLogo(currentConsignatariaName),color:this.consBrandColor(currentConsignatariaName)}],mode:'category', 30189 // Keep roll-number components and their host references out of the serializable board model. 30190 groups:priceBoardRowsWithTrend.filter(r=>r.count>0).map(r=>({category:r.category,count:r.count,maxValue:r.maxValue,avgValue:r.avgValue,section:'Reproductores',unit:'$ / cabeza'}))}; 30191 const priceBoardUpdatedLabel = priceBoardUpdatedAt ?('Actualizado ' + new Date(priceBoardUpdatedAt).toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' })) : 'Esperando precios cargados'; 30192 const tvGroups = allLotGroups; 30193 const tvGroupUnitLabel = (group) => window.CabanaLotIdentity.resolve(group?.variants?.[0], current).type === 'corral' ?'Corral' : 'Lote'; 30194 const tvGroupLabel = (group) => group ?(tvGroupUnitLabel(group) + ' ' + group.lotNumber) : ''; 30195 const storedTvKey = s.cabanaTvLotKey && tvGroups.some((group) => group.key === s.cabanaTvLotKey) ?s.cabanaTvLotKey : (tvGroups[0]?.key || ''); 30196 const tvSelectedGroup = tvGroups.find((group) => group.key === storedTvKey) || tvGroups[0] || null; 30197 const tvSelectedCabana = tvSelectedGroup?.variants?.length 30198 ?commercialCabanaForLot(tvSelectedGroup.variants[0]) 30199 :(participants.find((cabana) => cabana.type === 'principal') || participants[0] || {}); 30200 const tvSelectedCabanaColor = /^#[0-9a-f]{6}$/i.test(String(tvSelectedCabana?.brandColor || '')) ?tvSelectedCabana.brandColor : (current ?this.cabanaColor(current) : '#0a5ca8'); 30201 const tvSelectedCabanaWhiteLogo = String(tvSelectedCabana?.logoWhiteDataUrl || ''); 30202 const tvSelectedCabanaColorLogo = String(tvSelectedCabana?.logoDataUrl || ''); 30203 const tvSelectedCabanaLogo = [tvSelectedCabanaWhiteLogo,tvSelectedCabanaColorLogo].find(url=>url&&!s.cabanaTvLogoFailed?.[url]) || ''; 30204 const tvSelectedLogoIsWhite = !!tvSelectedCabanaLogo && tvSelectedCabanaLogo === tvSelectedCabanaWhiteLogo; 30205 const tvSelectedCabanaName = String(tvSelectedCabana?.name || tvSelectedCabana?.nombre || current?.nombre_remate || current?.titulo || current?.title || 'Cabaña'); 30206 const tvValueFromLot = (lot, keys, fallback = '-') => { 30207 const value = lot ?commercialDefaultText(lotText(lot, keys, ''), '') : ''; 30208 return value || fallback; 30209 }; 30210 const tvFieldWidth = (label) => { 30211 const key = this.consKey(label); 30212 if (/^(ce|c escrotal|circunferencia)/.test(key)) return '.5fr'; 30213 if (/^(pa|peso)/.test(key)) return '.62fr'; 30214 if (/rp/.test(key)) return '.66fr'; 30215 if (/padre|madre|abuelo|indice/.test(key)) return '1.08fr'; 30216 return '.82fr'; 30217 }; 30218 const tvSchema = this.cabanaRemateSchema(current); 30219 const tvCategorySchemaByKey = new Map(); 30220 tvSchema.forEach((category) => { 30221 const keys = [category?.categoryId, category?.categoryName, category?.categoria, category?.name].map((key) => this.consKey(key)).filter(Boolean); 30222 keys.forEach((key) => tvCategorySchemaByKey.set(key, category)); 30223 }); 30224 const tvFormKey = (form, fallback = 'main') => this.consKey(form?.key || form?.formKey || form?.templateId || form?.id || form?.name || fallback); 30225 const isTvDepForm = (form) => {
30226 const key = tvFormKey(form); 30227 return key === 'deps' || key === 'dep' || key === 'depes' || key.includes('deps'); 30228 }; 30229 const isTvPrecisionForm = (form) => { 30230 const key = tvFormKey(form); 30231 return key === 'precision' || key === 'precisiones' || key.includes('precision'); 30232 }; 30233 const buildTvForms = (categoryName) => { 30234 const categoryKey = this.consKey(categoryName || ''); 30235 const tvSchemaCategory = tvCategorySchemaByKey.get(categoryKey); 30236 return tvSchemaCategory ?[ 30237 ...((tvSchemaCategory.fields || []).length ?[{ name: 'Datos principales', displayOrder: 0, isActive: true, fields: tvSchemaCategory.fields }] : []), 30238 ...(tvSchemaCategory.forms || []), 30239 ...(tvSchemaCategory.subforms || []), 30240 ].filter((form) => form?.isActive !== false).sort((a, b) => Number(a?.displayOrder || 0) - Number(b?.displayOrder || 0)) : []; 30241 }; 30242 const buildTvFieldGroups = (categoryName) => { 30243 const seen = new Set(['rp']); 30244 const groups = [{ key: 'identity', label: 'Identificacion', fields: [{ key: 'rp', label: 'RP', fr: '.66fr', value: (variant) => tvValueFromLot(variant, ['rp', 'RP', 'numero_rp'], this.cabanaAnalyticsRpId(variant) || '-') }] }]; 30245 const forms = buildTvForms(categoryName).filter((form) => !isTvDepForm(form) && !isTvPrecisionForm(form)); 30246 forms.forEach((form) => { 30247 const fields = []; 30248 (form.fields || []) 30249 .slice() 30250 .sort((a, b) => Number(a?.order || a?.displayOrder || 0) - Number(b?.order || b?.displayOrder || 0)) 30251 .forEach((field, fieldIndex) => { 30252 const label = String(field?.name || field?.nombre || field?.label || field?.key || '').trim(); 30253 if (!label) return; 30254 const key = 'field:' + tvFormKey(form, 'main') + ':' + this.consKey(field?.key || field?.name || field?.nombre || label || fieldIndex); 30255 if (seen.has(key) || key === 'field::') return; 30256 seen.add(key); 30257 fields.push({ 30258 key, 30259 label: this.cabanaStoryCleanLabel(label), 30260 fr: tvFieldWidth(label), 30261 value: (variant) => { 30262 const value = this.cabanaStoryDisplayValue(this.cabanaLotFieldValue(variant, field, form)); 30263 return value || '-'; 30264 }, 30265 }); 30266 }); 30267 if (fields.length) groups.push({ key: tvFormKey(form, groups.length), label: form?.name || form?.nombre || 'Subformulario', fields }); 30268 }); 30269 return groups; 30270 }; 30271 const buildTvFieldDefs = (categoryName) => { 30272 const defs = buildTvFieldGroups(categoryName).flatMap((group) => group.fields || []); 30273 if (defs.length === 1) { 30274 [ 30275 { key: 'ce', label: 'CE', fr: '.5fr', value: (variant) => tvValueFromLot(variant, ['ce', 'circunferencia_escrotal', 'c_escrotal'], '-') }, 30276 { key: 'pa', label: 'PA', fr: '.62fr', value: (variant) => tvValueFromLot(variant, ['pesoProm', 'peso_promedio', 'pesoPromedio', 'peso', 'peso_actual'], '-') }, 30277 { key: 'padre', label: 'Padre', fr: '1.08fr', value: (variant) => tvValueFromLot(variant, ['padre', 'father'], '-') }, 30278 ].forEach((field) => defs.push(field)); 30279 } 30280 return defs; 30281 }; 30282 const tvSelectedCategoryKey = this.consKey(tvSelectedGroup?.categoryName || ''); 30283 const tvFieldDefsByCategory = {}; 30284 const tvFieldGroupsByCategory = {}; 30285 const tvCategoryOptions = []; 30286 tvGroups.forEach((group) => { 30287 const key = this.consKey(group.categoryName || ''); 30288 if (!key || tvFieldDefsByCategory[key]) return; 30289 tvFieldDefsByCategory[key] = buildTvFieldDefs(group.categoryName); 30290 tvFieldGroupsByCategory[key] = buildTvFieldGroups(group.categoryName); 30291 tvCategoryOptions.push({ key, label: group.categoryName || 'Sin categoria', count: tvGroups.filter((item) => this.consKey(item.categoryName || '') === key).length }); 30292 }); 30293 const tvFieldDefs = tvFieldDefsByCategory[tvSelectedCategoryKey] || buildTvFieldDefs(tvSelectedGroup?.categoryName || ''); 30294 const tvSelectedFieldGroups = tvFieldGroupsByCategory[tvSelectedCategoryKey] || buildTvFieldGroups(tvSelectedGroup?.categoryName || ''); 30295 this._cabanaTvFieldKeysByCategory = Object.fromEntries(Object.entries(tvFieldDefsByCategory).map(([key, defs]) => [key, defs.map((field) => field.key)])); 30296 this._cabanaTvActiveCategoryKey = tvSelectedCategoryKey; 30297 this._cabanaTvFieldKeys = tvFieldDefs.map((field) => field.key); 30298 const tvConfig = this.normalizeCabanaTvConfig(s.cabanaTvConfig || this.loadCabanaTvConfig(), this._cabanaTvFieldKeys, tvSelectedCategoryKey); 30299 const tvSelectedFields = this.cabanaTvFieldsForCategory(tvConfig, tvSelectedCategoryKey, this._cabanaTvFieldKeys); 30300 const tvColumns = tvFieldDefs.filter((field) => tvSelectedFields.includes(field.key)); 30301 if (!tvFieldDefsByCategory[tvSelectedCategoryKey] && tvFieldDefs.length) { 30302 tvFieldDefsByCategory[tvSelectedCategoryKey] = tvFieldDefs; 30303 tvFieldGroupsByCategory[tvSelectedCategoryKey] = tvSelectedFieldGroups; 30304 this._cabanaTvFieldKeysByCategory[tvSelectedCategoryKey] = tvFieldDefs.map((field) => field.key); 30305 } 30306 const tvVariantCount = tvSelectedGroup?.variants?.length || 0; 30307 this._cabanaTvRollSigs = this._cabanaTvRollSigs || {}; 30308 this._cabanaTvRollPulses = this._cabanaTvRollPulses || {}; 30309 const tvRollClassFor = (id, value) => { 30310 const text = String(value ?? ''); 30311 if (this._cabanaTvRollSigs[id] !== text) { 30312 this._cabanaTvRollSigs[id] = text; 30313 this._cabanaTvRollPulses[id] = (Number(this._cabanaTvRollPulses[id] || 0) + 1) % 2; 30314 return 'tv-roll ' + (this._cabanaTvRollPulses[id] ?'is-a' : 'is-b'); 30315 } 30316 return 'tv-roll'; 30317 }; 30318 const formatTvSalePrice = (draft) => { 30319 if (!draft || typeof draft !== 'object') return ''; 30320 const mode = draft.priceMode === 'kilo' ?'kilo' : 'cabeza'; 30321 const raw = mode === 'kilo' ?draft.precioKilo : draft.precioCabeza; 30322 const digits = String(raw ?? '').replace(/\D/g, ''); 30323 if (!digits) return '';
30324 const price = '$ ' + Number(digits).toLocaleString('es-AR'); 30325 return mode === 'kilo' ?(price + ' / kg') : price; 30326 }; 30327 const tvCommercialForVariant = (variant) => { 30328 const key = this.cabanaCommercialRowKey(variant); 30329 const draft = (commercialDrafts || {})[key] || this.normalizeCabanaCommercialRecord(variant?.commercial); 30330 const status = String(draft?.status || 'pendiente').trim().toLowerCase(); 30331 const isSold = status === 'vendido' || status === 'sold'; 30332 const isLifted = status === 'levantado' || status === 'lifted'; 30333 const price = formatTvSalePrice(draft); 30334 const saleLabel = draft.divisions?.length&&!isLifted&&status!=='no-vendido' ? draft.divisions.filter(p=>p.price>0).map(p=>p.quantity+' à $ '+Number(p.price).toLocaleString('es-AR')).join(' · ') || 'A la venta' : isSold ?(price || 'Vendido') : (isLifted ?'Anulado' : 'A la venta'); 30335 return { status, isSold, isLifted, price, saleLabel, hasSignal: isSold || isLifted }; 30336 }; 30337 const tvMainVariants = (tvSelectedGroup?.variants || []).slice(0, 10); 30338 const tvVariantQuantity = (variant) => { 30339 const draft = (commercialDrafts || {})[this.cabanaCommercialRowKey(variant)] || this.normalizeCabanaCommercialRecord(variant?.commercial); 30340 const raw = commercialDefaultText(draft?.cantidad, commercialDefaultText(lotText(variant, ['cantidad', 'cantidadTotal', 'cantidad_animales', 'quantity', 'totalQuantity'], ''), '')); 30341 const quantity = Number(String(raw ?? '').trim().replace(',', '.')); 30342 return Number.isFinite(quantity) && quantity > 0 ?quantity : 1; 30343 }; 30344 const tvQuantityCount = tvMainVariants.reduce((total, variant) => total + tvVariantQuantity(variant), 0) || tvVariantCount; 30345 const tvCommercialSignals = tvMainVariants.map((variant) => tvCommercialForVariant(variant)); 30346 const tvSaleBadgesBase = tvMainVariants.map((variant, rowIndex) => { 30347 const commercial = tvCommercialSignals[rowIndex] || tvCommercialForVariant(variant); 30348 if (!commercial.hasSignal) return null; 30349 const rp = tvValueFromLot(variant, ['rp', 'RP', 'numero_rp'], this.cabanaAnalyticsRpId(variant) || '-'); 30350 if (commercial.isSold) { 30351 return { 30352 className: 'tv-lower-sale-badge is-sold', 30353 label: 'Vendido', 30354 text: 'RP ' + rp + (commercial.price ?(' · ' + commercial.price) : ' · Sin precio'), 30355 }; 30356 } 30357 return { 30358 className: 'tv-lower-sale-badge is-lifted', 30359 label: 'Anulado', 30360 text: 'RP ' + rp, 30361 }; 30362 }).filter(Boolean); 30363 const tvMainColumns = [ 30364 ...tvColumns, 30365 ...(tvConfig.saleColumnVisible !== false ?[{ key: 'sale', label: 'Venta', fr: '.72fr', isSale: true, value: () => 'A la venta' }] : []), 30366 ]; 30367 const tvRows = tvMainVariants.map((variant, rowIndex) => { 30368 const commercial = tvCommercialSignals[rowIndex] || tvCommercialForVariant(variant); 30369 const cells = tvMainColumns.map((column) => { 30370 const value = column.isSale ?commercial.saleLabel : column.value(variant); 30371 const saleClass = column.isSale ?('tv-lower-sale-cell ' + (commercial.isSold ?'is-sold' : (commercial.isLifted ?'is-lifted' : 'is-empty'))) : ''; 30372 const length = String(value ?? '').trim().length; 30373 const lengthClass = column.isSale ?'' : (length > 90 ?' is-very-long' : (length > 42 ?' is-long' : '')); 30374 return { key: column.key, value, className: saleClass + lengthClass, rollClass: tvRollClassFor('row-' + rowIndex + '-' + column.key, value) }; 30375 }); 30376 return { className: 'tv-lower-row tv-lower-body ' + (commercial.isSold ?'is-sold' : (commercial.isLifted ?'is-lifted' : '')), cells }; 30377 }); 30378 const tvDepForms = buildTvForms(tvSelectedGroup?.categoryName || '').filter((form) => isTvDepForm(form) || isTvPrecisionForm(form)); 30379 const tvDepFieldLabel = (field, fieldIndex = 0) => this.cabanaStoryCleanLabel(field?.name || field?.nombre || field?.label || field?.key || ('Dato ' + (fieldIndex + 1))); 30380 const tvDepFormType = (form) => isTvPrecisionForm(form) ?'precision' : 'deps'; 30381 const tvDepFormLabel = (form) => tvDepFormType(form) === 'precision' ?'Precision' : 'DEPs'; 30382 const tvDepRowsForVariant = (variant) => {
30383 const valuesByField = new Map(); 30384 tvDepForms.forEach((form) => { 30385 const type = tvDepFormType(form); 30386 (form.fields || []).slice() 30387 .sort((a, b) => Number(a?.order || a?.displayOrder || 0) - Number(b?.order || b?.displayOrder || 0)) 30388 .forEach((field, fieldIndex) => { 30389 const label = tvDepFieldLabel(field, fieldIndex); 30390 if (!label) return; 30391 const rawValue = this.cabanaStoryDisplayValue(this.cabanaLotFieldValue(variant, field, form)); 30392 if (isEmptyValue(rawValue)) return; 30393 const key = this.consKey(field?.key || field?.name || label || fieldIndex); 30394 if (!valuesByField.has(key)) valuesByField.set(key, { key, label, deps: '', precision: '' }); 30395 valuesByField.get(key)[type] = rawValue; 30396 }); 30397 }); 30398 return Array.from(valuesByField.values()).filter((item) => !isEmptyValue(item.deps) || !isEmptyValue(item.precision)); 30399 }; 30400 const tvDepCards = (tvSelectedGroup?.variants || []).slice(0, 10).map((variant, variantIndex) => { 30401 const fields = tvDepRowsForVariant(variant); 30402 return { 30403 variant, 30404 variantIndex, 30405 rp: tvValueFromLot(variant, ['rp', 'RP', 'numero_rp'], this.cabanaAnalyticsRpId(variant) || '-'), 30406 fields, 30407 }; 30408 }).filter((item) => item.fields.length); 30409 const tvClock = Number(s.cabanaTvClock || Date.now()); 30410 const tvCycleStartedAt = Number(s.cabanaTvCycleStartedAt || tvClock); 30411 const tvMainDurationMs = 10000; 30412 const tvRpDurationMs = 5000; 30413 const tvCycleMs = tvMainDurationMs + (tvDepCards.length * tvRpDurationMs); 30414 const tvPhaseMs = tvConfig.depsEnabled === true && tvCycleMs > tvMainDurationMs ?Math.max(0, tvClock - tvCycleStartedAt) % tvCycleMs : 0; 30415 const tvDepCardIndex = tvPhaseMs >= tvMainDurationMs ?Math.min(tvDepCards.length - 1, Math.floor((tvPhaseMs - tvMainDurationMs) / tvRpDurationMs)) : -1; 30416 const tvActiveDepCard = tvDepCardIndex >= 0 ?tvDepCards[tvDepCardIndex] : null; 30417 const tvShowingDeps = !!tvActiveDepCard; 30418 const tvGraphicsVisible = tvConfig.visible !== false; 30419 const tvAllVideoItems = this.cabanaTvVideoItems(tvSelectedGroup); 30420 const tvVideoMode = tvConfig.videoMode === 'grid' ?'grid' : 'single'; 30421 const tvFullScreenPlateActive = tvGraphicsVisible && (tvConfig.waitingVisible === true || tvConfig.endVisible === true); 30422 const tvPlaybackMode = tvFullScreenPlateActive ?'single' : tvVideoMode; 30423 const tvStatusVideoItems = tvAllVideoItems.map((item) => { 30424 const commercial = tvCommercialForVariant(item.variant); 30425 return { ...item, isSold: commercial.isSold, isLifted: commercial.isLifted }; 30426 }); 30427 const tvModeVideoItems = tvStatusVideoItems.filter((item) => !item.isLifted && (tvPlaybackMode === 'grid' || !item.isSold)); 30428 const tvVideosPending = tvModeVideoItems.some((item) => item.needsLoad); 30429 const tvPlaybackRequested = tvGraphicsVisible && (tvFullScreenPlateActive || tvConfig.videoVisible === true); 30430 if (tvVideosPending) { 30431 setTimeout(() => this.ensureCabanaTvVideos({ variants: tvModeVideoItems.map((item) => item.variant) }), 0); 30432 } 30433 const tvReadyVideoItems = tvModeVideoItems.filter((item) => item.src); 30434 const tvRequestedVideoIndex = tvReadyVideoItems.findIndex((item) => item.key === tvConfig.videoRpKey); 30435 const tvSelectedVideoIndex = tvRequestedVideoIndex >= 0 ?tvRequestedVideoIndex : 0; 30436 const tvSelectedVideoItem = tvReadyVideoItems[tvSelectedVideoIndex] || null; 30437 const tvSinglePlaylistItems = tvReadyVideoItems.length ?[ 30438 ...tvReadyVideoItems.slice(tvSelectedVideoIndex), 30439 ...tvReadyVideoItems.slice(0, tvSelectedVideoIndex), 30440 ] : []; 30441 const tvVideoItems = tvPlaybackMode === 'grid' ?tvReadyVideoItems.slice(0, 4) : tvSinglePlaylistItems; 30442 this._cabanaTvReadyVideoCount = tvReadyVideoItems.length; 30443 if (s.route === 'cabanaTvLower' && tvVideoItems.length) { 30444 setTimeout(() => this.syncCabanaTvVideoMount(tvVideoItems, tvPlaybackMode, { 30445 playing: tvPlaybackRequested && (tvFullScreenPlateActive || tvConfig.videoPlaying !== false), 30446 prewarming: !tvPlaybackRequested, 30447 restartToken: tvConfig.videoRestartToken, 30448 lowerThirdVisible: tvConfig.lowerThirdVisible !== false,
30449 waitingBackground: tvFullScreenPlateActive, 30450 }), 0); 30451 } else if (this._cabanaTvVideoMountSignature && s.route === 'cabanaTvLower' && !tvVideosPending) { 30452 setTimeout(() => this.destroyCabanaTvVideoMount(), 0); 30453 } 30454 const tvDepColumns = tvActiveDepCard ?[ 30455 { key: 'label', label: '', fr: '.98fr' }, 30456 ...tvActiveDepCard.fields.map((field) => ({ key: field.key, label: field.label, fr: tvFieldWidth(field.label) })), 30457 ] : []; 30458 const tvDepRows = tvActiveDepCard ?[ 30459 { key: 'field-names', cells: tvDepColumns.map((column, columnIndex) => ({ key: 'name-' + column.key, value: columnIndex === 0 ?'Campo' : column.label, className: '', rollClass: tvRollClassFor('dep-' + tvActiveDepCard.variantIndex + '-name-' + column.key, columnIndex === 0 ?'Campo' : column.label) })) }, 30460 { key: 'deps-values', cells: tvDepColumns.map((column, columnIndex) => { 30461 const field = columnIndex === 0 ?null : tvActiveDepCard.fields[columnIndex - 1]; 30462 const value = columnIndex === 0 ?'DEPs' : (field?.deps || '-'); 30463 return { key: 'deps-' + column.key, value, className: '', rollClass: tvRollClassFor('dep-' + tvActiveDepCard.variantIndex + '-deps-' + column.key, value) }; 30464 }) }, 30465 { key: 'precision-values', cells: tvDepColumns.map((column, columnIndex) => { 30466 const field = columnIndex === 0 ?null : tvActiveDepCard.fields[columnIndex - 1]; 30467 const value = columnIndex === 0 ?'Precision' : (field?.precision || '-'); 30468 return { key: 'precision-' + column.key, value, className: '', rollClass: tvRollClassFor('dep-' + tvActiveDepCard.variantIndex + '-precision-' + column.key, value) }; 30469 }) }, 30470 ].map((row) => ({ ...row, className: row.className || 'tv-lower-row tv-lower-body' })) : []; 30471 const tvActiveColumns = tvShowingDeps ?tvDepColumns : tvMainColumns; 30472 const tvActiveRows = tvShowingDeps ?tvDepRows : tvRows; 30473 const tvTextUnits = (value) => Array.from(String(value ?? '')).reduce((total, char) => { 30474 if (/\s/.test(char)) return total + .38; 30475 if (/[.,:;!|/\\()\-]/.test(char)) return total + .48; 30476 if (/\d/.test(char)) return total + .74; 30477 if (char === char.toUpperCase() && char !== char.toLowerCase()) return total + 1.06; 30478 return total + .9; 30479 }, 0); 30480 const tvMaxColumnWeight = tvActiveColumns.length <= 5 ?2 : (tvActiveColumns.length === 6 ?1.8 : (tvActiveColumns.length === 7 ?1.6 : 1.4)); 30481 const tvGridCols = tvActiveColumns.map((column, columnIndex) => { 30482 const configuredWeight = Math.max(.48, Number.parseFloat(column?.fr) || .82); 30483 const contentUnits = [column?.label, ...tvActiveRows.map((row) => row?.cells?.[columnIndex]?.value)] 30484 .reduce((longest, value) => Math.max(longest, tvTextUnits(value)), 0); 30485 let contentWeight = .38 + (contentUnits * .055); 30486 if (column?.isSale) contentWeight = Math.min(contentWeight, 1.02); 30487 const weight = Math.min(tvMaxColumnWeight, Math.max(configuredWeight, contentWeight)); 30488 return (Math.round(weight * 100) / 100) + 'fr'; 30489 }).join(' ') || '1fr'; 30490 const tvGroupsCount = tvGroups.length; 30491 const tvIndex = Math.max(0, tvGroups.findIndex((group) => group.key === storedTvKey)); 30492 const setTvByIndex = (index) => { 30493 if (!tvGroupsCount) return; 30494 const next = tvGroups[((index % tvGroupsCount) + tvGroupsCount) % tvGroupsCount]; 30495 if (next) this.setCabanaTvLotKey(next.key); 30496 }; 30497 const tvParams = new URLSearchParams(window.location.search || '');
30498 const tvClean = tvParams.get('clean') === '1'; 30499 const tvWeather = s.cabanaTvWeather || null; 30500 const tvWeatherIcons = new Set(['sun', 'cloud', 'cloud-sun', 'cloud-rain', 'cloud-drizzle', 'cloud-lightning', 'cloud-snow', 'cloud-fog', 'cloudy', 'wind']); 30501 const tvWeatherIconName = tvWeatherIcons.has(String(tvWeather?.icon || '')) ?String(tvWeather.icon) : 'cloud'; 30502 const tvWeatherIcon = tvWeather ?('assets/icons/lucide/' + tvWeatherIconName + '.svg') : ''; 30503 const tvWeatherUnit = String(tvWeather?.unit || '°C').replace(/^Ã/, ''); 30504 const tvWaitingStart = this.cabanaAuctionStartAt(current); 30505 const tvWaitingRemainingSeconds = tvWaitingStart ?Math.max(0, Math.ceil((tvWaitingStart.getTime() - tvClock) / 1000)) : 0; 30506 const tvWaitingCountdownActive = !!tvWaitingStart && tvWaitingRemainingSeconds > 0; 30507 const tvWaitingDays = Math.floor(tvWaitingRemainingSeconds / 86400); 30508 const tvWaitingHours = Math.floor((tvWaitingRemainingSeconds % 86400) / 3600); 30509 const tvWaitingMinutes = Math.floor((tvWaitingRemainingSeconds % 3600) / 60); 30510 const tvWaitingSeconds = tvWaitingRemainingSeconds % 60; 30511 const tvWaitingPad = (value) => String(Math.max(0, Number(value) || 0)).padStart(2, '0'); 30512 const tvWaitingLocation = current?.ubicacion_remate || current?.lugar || [current?.location?.lugar, current?.localidad || current?.location?.localidad, current?.provincia || current?.location?.provinciaNombre].filter(Boolean).filter((value, index, list) => list.indexOf(value) === index).join(', ') || 'Localidad a confirmar'; 30513 return { 30514 cabanasLoading: cabanasInitialLoading, cabanaSkeletonCards: Array.from({ length: 6 }, (_, index) => ({ index })), cabanasError: s.cabanasError, cabanasHasError: !!s.cabanasError && (!s.cabanasCollectionReady || cabanaCards.length === 0), 30515 hasCabanas: !cabanasInitialLoading && cabanaCards.length > 0, cabanasEmpty: !s.cabanasLoading && !!s.cabanasCollectionReady && Array.isArray(s.cabanas) && !s.cabanasError && cabanaCards.length === 0, 30516 cabanaBreedChips: [{ label: 'Todas', key: '', count: cabanas.length }, ...breeds.map((breed) => ({ label: breed, key: breed, count: cabanas.filter((item) => this.cabanaBreedOf(item) === breed).length }))].map((chip) => ({ ...chip, go: () => this.setState({ cabanaBreed: chip.key }), bg: s.cabanaBreed === chip.key ?'#002e5f' : '#fff', color: s.cabanaBreed === chip.key ?'#fff' : '#3d4c5f' })), 30517 cabanaCards, 30518 cbColor: currentHeroColor, cbBrandDark: current ?this.cabanaBrandDark(current) : '#002e5f', cbMedia: current ?this.cabanaMedia(current) : '', cbHasMedia: !!(current && this.cabanaMedia(current)), 30519 cbHeroText: currentHeroIsDark ?'#fff' : '#102033', cbHeroMuted: currentHeroIsDark ?'rgba(255,255,255,.7)' : 'rgba(16,32,51,.68)', cbHeroSurface: currentHeroIsDark ?'rgba(0,24,55,.2)' : 'rgba(255,255,255,.38)', cbHeroSurfaceStrong: currentHeroIsDark ?'rgba(255,255,255,.12)' : 'rgba(255,255,255,.58)', cbHeroBorder: currentHeroIsDark ?'rgba(255,255,255,.2)' : 'rgba(16,32,51,.18)', cbHeroIconFilter: currentHeroIsDark ?'brightness(0) invert(1)' : 'none', 30520 cbBrandPanelClass: mainBrandLogos.length > 4 ?'is-compact' : '', cbManyMainBrands: mainBrandLogos.length > 4, cbMainBrandCount: mainBrandLogos.length, cbFewMainBrands: mainBrandLogos.length <= 4, cbNameRollDuration: Math.max(32, mainBrandLogos.length * 5) + 's', cbNameRollPaused: !!s.cbNameRollPaused, cbNameRollPausedClass: s.cbNameRollPaused ?'is-paused':'', cbNameRollPauseLabel: s.cbNameRollPaused ?'Reanudar':'Pausar', cbToggleNameRoll: () => this.setState({cbNameRollPaused:!this.state.cbNameRollPaused}), cbMainLogos: mainBrandLogos, cbGuestLogos: guestBrandLogos, 30521 cbManyGuestBrands: guestBrandLogos.length > 4, cbFewGuestBrands: guestBrandLogos.length > 0 && guestBrandLogos.length <= 4, cbGuestBrandCount: guestBrandLogos.length, 30522 cbGuestRollDuration: Math.max(32, guestBrandLogos.length * 5) + 's', cbGuestRollPaused: !!s.cbGuestRollPaused, cbGuestRollPausedClass: s.cbGuestRollPaused ?'is-paused':'', cbGuestRollPauseLabel: s.cbGuestRollPaused ?'Reanudar':'Pausar', cbToggleGuestRoll: () => this.setState({cbGuestRollPaused:!this.state.cbGuestRollPaused}), cbHasBranding: mainBrandLogos.length > 0 || guestBrandLogos.length > 0, cbHasGuestLogos: guestBrandLogos.length > 0, cbPrimaryWhiteLogo: primaryWhiteLogo, cbHasPrimaryWhiteLogo: !!primaryWhiteLogo, cbHeroGrid: s.isMobile ?'1fr' : 'minmax(0,1.15fr) minmax(320px,.85fr)', cbMainLogoHeight: s.isMobile ?'62px' : '82px', 30523 cbTitle: current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabaña', cbBreed: current ?(this.cabanaBreedOf(current) || 'Reproductores') : '', cbDate: current ?dateLabel(current) : '', cbTime: current ?timeLabel(current) : '', 30524 cbLocation: current ?(current.ubicacion_remate || current.localidad || current.provincia || current?.location?.lugar || current?.location?.localidad || current?.location?.provinciaNombre || 'Ubicación a confirmar') : '', cbConsignataria: currentConsignatariaName, 30525 cbConsignatariaLogo: currentConsignatariaLogo, cbHasConsignatariaLogo: !!currentConsignatariaLogo, cbNoConsignatariaLogo: !currentConsignatariaLogo, cbConsignatariaInitial: (currentConsignatariaName || 'R').charAt(0).toUpperCase(), 30526 cbConsignatariaColor: currentConsignatariaColor, cbConsignatariaShadow: currentConsignatariaColor + '35', cbConsignatariaWhiteLogo: currentConsignatariaWhiteLogo, cbHasConsignatariaWhiteLogo: !!currentConsignatariaWhiteLogo, cbNoConsignatariaWhiteLogo: !currentConsignatariaWhiteLogo, 30527 cbContactName: currentContact.name, cbContactPhone: currentContact.phone, cbContactPhoneHref: currentContact.phoneHref, 30528 cbContactWhatsappHref: currentContact.whatsappBase + (currentContact.phoneDigits ?'?text=' + encodeURIComponent(generalWhatsappText) : ''), 30529 cbLoading: cabanaLotsInitialLoading, cbCountsLoading: !cabanaCountsReady, cbCountsReady: cabanaCountsReady, 30530 cbHasLots: lotCards.length > 0, cbLotsEmpty: !s.cabanaLotsLoading && Array.isArray(s.cabanaLots) && !s.cabanaLotsError && lotCards.length === 0, 30531 cbGroupUnit: groupUnit, cbGroupUnitUpper: groupUnit.toUpperCase(), cbCatalogTitle: groupUnit === 'corrales' ? 'Corrales disponibles' : groupUnit === 'lotes' ? 'Lotes disponibles' : 'Lotes y corrales disponibles', 30532 cbTotal: totalLotGroups, cbLots: lotCards, 30533 cbHasCatalogPdf: !!this.cabanaCatalogPdfUrl(current), cbCatalogPdfUrl: this.cabanaCatalogPdfUrl(current), 30534 cbCatalogLocked: this.cabanaCatalogLocked(current), cbCatalogUnlocked: !this.cabanaCatalogLocked(current), cbCatalogLogin: () => this.loginForCabanaCatalog(), 30535 cbQuery: s.cbQuery, onCbQuery: (event) => this.setState({ cbQuery: event.target.value }), 30536 cbPrebidSummaryAllowed: !!s.user?.uid && s.cabanaPrebidSummaryAccess?.uid === s.user.uid && s.cabanaPrebidSummaryAccess?.id === this.cabanaRemateId(current), 30537 cbOpenPrebidSummary: () => this.openCabanaPrebidSummary(), 30538 cbTermsVisible: this.canEditCabanaCommercialTerms(current) || (s.cabanaCommercialTerms?.id===this.cabanaRemateId(current) && !!s.cabanaCommercialTerms?.imageUrl), 30539 cbOpenTerms: () => this.openCabanaCommercialTerms(), 30540 cbPrebidPanelAllowed: !!s.cabanaPrebidPanelAllowed, 30541 cbPrebidPanelVisible: !!s.cabanaPrebidPanelAllowed && prebidPanelRawItems.length > 0, 30542 cbPrebidPanelOpen: !!s.cabanaPrebidPanelOpen, 30543 cbPrebidPanelLoading: !!s.cabanaPrebidPanelLoading, 30544 cbPrebidPanelHasError: !!s.cabanaPrebidPanelError, 30545 cbPrebidPanelError: s.cabanaPrebidPanelError || '', 30546 cbPrebidPanelRows: prebidPanelRows, 30547 cbPrebidPanelHasRows: prebidPanelRows.length > 0, 30548 cbPrebidPanelEmpty: !s.cabanaPrebidPanelLoading && prebidPanelRows.length === 0, 30549 cbPrebidPanelMetrics: prebidPanelMetrics, 30550 cbPrebidPanelLots: prebidPanelLots, 30551 cbPrebidPanelHasLots: prebidPanelLots.length > 0, 30552 cbPrebidPanelHasSelectedLot: !!prebidPanelSelectedLot, 30553 cbPrebidPanelSelectedLotLabel: prebidPanelSelectedLot?.label || '', 30554 cbPrebidPanelUpdated: prebidPanelUpdated,
30555 cbPrebidPanelQuery: s.cabanaPrebidPanelQuery || '', 30556 cbPrebidPanelStatus: s.cabanaPrebidPanelStatus || 'all', 30557 cbPrebidPanelStatusTabs: [['all','Todas'],['current','Actuales'],['pending','Pendientes'],['outbid','Superadas']].map(([key, label]) => ({ key, label, active: prebidPanelStatus === key, className: prebidPanelStatus === key ?'cb-prebid-admin-filter is-active' : 'cb-prebid-admin-filter', go: () => this.setState({ cabanaPrebidPanelStatus: key }) })), 30558 cbCanManagePrivatePages: this.canManageCabanaPrivatePages(s.apiUser), 30559 cbCanSeeStatsButton: this.canSeeCabanaStats(current, s.apiUser), 30560 cbCanAudit: this.canAuditCabana(current), 30561 cbAuditorOpen: this.canAuditCabana(current) && s.route === 'cabanaAuditor', 30562 toggleCbAuditor: () => this.openCabanaAuditor(current), 30563 closeCbAuditor: () => this.openCabana(current), 30564 cbAuditorName: current?.name || current?.nombre || current?.titulo || '', 30565 cbAuditorCount: auditorRows.length, 30566 cbAuditorLoading: !!s.cabanaLotsLoading, 30567 cbAuditorError: s.cabanasError || '', 30568 refreshCbAuditor: () => { if(this.canAuditCabana(current)) return this.fetchCabanaLots(current); },
30569 cbAuditorQuery: s.cabanaAuditorQuery || '', 30570 searchCbAuditor: event => this.setState({cabanaAuditorQuery:event.target.value}), 30571 cbAuditorRows: auditorRows, 30572 cbAuditorEmpty: !s.cabanaLotsLoading && !auditorRows.length, 30573 cbShowAuctionSubpages: this.canAuditCabana(current) || this.canSeeCabanaStats(current, s.apiUser) || this.canManageCabanaPrivatePages(s.apiUser), 30574 openCbPrebidPanel: () => this.openCabanaPrebidPanelPage(current), 30575 closeCbPrebidPanel: () => this.closeCabanaPrebidPanelPage(), 30576 toggleCbPrebidPanel: () => this.toggleCabanaPrebidPanel(), 30577 refreshCbPrebidPanel: () => this.fetchCabanaPrebidPanel(current), 30578 onCbPrebidPanelQuery: (event) => this.setState({ cabanaPrebidPanelQuery: event.target.value }), 30579 cbCanManageSalesOrder: !!current && this.canManageUsers(), 30580 cbCanExportCatalog: this.canExportCabanaCatalog(current, s.apiUser), 30581 openCbSalesOrder: () => this.openCabanaSalesOrder(), 30582 openCbStats: () => this.openCabanaStats(current), 30583 openCbCommercial: () => this.openCabanaCommercial(current), 30584 openCbPrices: () => this.openCabanaPriceBoard(current), 30585 openCbTv: () => this.openCabanaTvLower(current), 30586 openCbTvConfig: () => this.openCabanaTvConfig(current), 30587 downloadCbCatalogExcel: () => this.downloadCabanaCatalogExcel(), 30588 ...this.cabanaStatsData(current, s.cabanaLots), 30589 cbSalesOrderAuctionTitle: current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabaña', 30590 cbSalesOrderOpen: !!s.cabanaSalesOrderOpen, 30591 cbSalesOrderGrouped: !!s.cabanaSalesOrderGrouped, 30592 cbSalesOrderGroupLabel: s.cabanaSalesOrderGrouped ?'Vista por orden general' : 'Agrupar por categorÃa', 30593 cbSalesOrderSaved: !!s.cabanaSalesOrderSaved, 30594 cbSalesOrderFlat: !s.cabanaSalesOrderGrouped, 30595 cbSalesOrderRows: salesOrderRows, 30596 cbSalesOrderGroups: salesOrderGroups, 30597 cbSalesOrderCount: salesOrderLots.length, 30598 cbSalesOrderLoading: !!s.cabanaLotsLoading, 30599 cbSalesOrderEmpty: !s.cabanaLotsLoading && salesOrderLots.length === 0, 30600 cbSalesOrderHasError: !!s.cabanaSalesOrderError, 30601 cbSalesOrderError: s.cabanaSalesOrderError || '', 30602 cbSalesOrderSaving: !!s.cabanaSalesOrderSaving, 30603 cbSalesOrderSaveOpacity: s.cabanaSalesOrderSaving ?'.62' : '1', 30604 cbSalesOrderSaveLabel: s.cabanaSalesOrderSaving ?'Guardando...' : 'Guardar orden', 30605 cbSalesGroupBg: s.cabanaSalesOrderGrouped ?salesOrderAccent : '#fff', 30606 cbSalesGroupColor: s.cabanaSalesOrderGrouped ?'#fff' : '#3d4c5f', 30607 cbSalesGroupBorder: s.cabanaSalesOrderGrouped ?salesOrderAccent : '#cbd8e7', 30608 closeCbSalesOrder: () => this.closeCabanaSalesOrder(), 30609 toggleCbSalesOrderGrouped: () => this.toggleCabanaSalesOrderGrouped(), 30610 saveCbSalesOrder: () => this.saveCabanaSalesOrder(), 30611 cbInstallmentSummary:current?.installmentConfig?.enabled?'Se preoferta una cuota · '+current.installmentConfig.count+' cuotas generales. Cada lote puede tener su propia cantidad.':'Desactivada. Los importes se consideran totales.', 30612 cbEditInstallments:()=>this.editCabanaInstallments(), 30613 cbCommercialTitle: current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabana', 30614 cbCommercialGroupLabel: groupUnit === 'corrales' ?'Corral' : (groupUnit === 'lotes / corrales' ?'Lote / Corral' : 'Lote'), 30615 cbCommercialRows: commercialRows, 30616 cbCommercialReportRows: commercialReportRows, 30617 cbCommercialMetrics: commercialMetricCards, 30618 cbCommercialFilters: commercialFilters, 30619 cbCommercialHasRows: commercialRows.length > 0, 30620 cbCommercialEmpty: !s.cabanaLotsLoading && commercialRows.length === 0, 30621 cbCommercialHasReportRows: commercialReportRows.length > 0, 30622 cbCommercialReportEmpty: !s.cabanaLotsLoading && commercialReportRows.length === 0, 30623 cbCommercialReportCountLabel: commercialReportRows.length === commercialTotal ?commercialTotal + ' registros' : commercialReportRows.length + ' de ' + commercialTotal + ' registros', 30624 cbCommercialIsEntry: commercialView === 'carga', 30625 cbCommercialIsList: commercialView === 'lista', 30626 cbCommercialEntryTabClass: commercialView === 'carga' ?'commercial-view-tab is-active' : 'commercial-view-tab', 30627 cbCommercialListTabClass: commercialView === 'lista' ?'commercial-view-tab is-active' : 'commercial-view-tab', 30628 cbCommercialShowEntry: () => this.setState({ cabanaCommercialView: 'carga' }), 30629 cbCommercialShowList: () => this.setState({ cabanaCommercialView: 'lista', cabanaCommercialOpenRow: '' }), 30630 cbCommercialExportExcel: () => this.downloadCabanaCommercialExcel(commercialReportRows), 30631 cbCommercialLoading: !!s.cabanaLotsLoading || !!s.cabanaCommercialLoading, 30632 cbCommercialOpenPrices: () => this.openCabanaPriceBoard(current), 30633 cbCommercialBack: () => this.backToCabanaCatalog(), 30634 isCabanaPriceBoard: s.route === 'cabanaPriceBoard' && (!!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/precios')), 30635 pbTitle: current?.nombre_remate || current?.titulo || current?.title || 'La Cassina', 30636 pbRows: priceBoardRowsWithTrend, 30637 pbHasRows: priceBoardRowsWithTrend.length > 0, 30638 pbEmpty: !s.cabanaLotsLoading && priceBoardRowsWithTrend.length === 0, 30639 pbUpdatedLabel: priceBoardUpdatedLabel, 30640 pbMainLogo: (mainBrandLogos[0] && mainBrandLogos[0].logo) || '', 30641 pbHasMainLogo: !!(mainBrandLogos[0] && mainBrandLogos[0].logo), 30642 pbNoMainLogo: !(mainBrandLogos[0] && mainBrandLogos[0].logo), 30643 pbConsignataria: currentConsignatariaName, 30644 pbConsLogo: currentConsignatariaWhiteLogo || currentConsignatariaLogo, 30645 pbHasConsLogo: !!(currentConsignatariaWhiteLogo || currentConsignatariaLogo), 30646 pbNoConsLogo: !(currentConsignatariaWhiteLogo || currentConsignatariaLogo), 30647 pbConsInitial: (currentConsignatariaName || 'C').charAt(0).toUpperCase(), 30648 pbConsColor: currentConsignatariaColor, 30649 pbBackCommercial: () => this.openCabanaCommercial(current), 30650 pbBoardData:JSON.stringify(cabanaPriceModel), 30651 pbExportStory: () => this.downloadCabanaPriceBoardStory(priceBoardRowsWithTrend,cabanaPriceModel), 30652 pbStoryBusy: !!s.cabanaPriceStoryBusy, 30653 pbStoryLabel: s.cabanaPriceStoryBusy ?'Generando...' : 'Exportar Story', 30654 pbOpenTv: () => this.openCabanaTvConfig(current), 30655 pbBackCatalog: () => this.backToCabanaCatalog(), 30656 isCabanaTvLower: s.route === 'cabanaTvLower' && (!!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/tv')), 30657 isCabanaTvConfig: s.route === 'cabanaTvConfig' && (!!s.currentCabana || (s.publicAdvanceMode === 'lacassina' && window.__ROSGAN_PUBLIC_PATH__ === '/lacassina/tv/config')), 30658 tvShowControls: false, 30659 tvHasLot: !!tvSelectedGroup && tvGraphicsVisible, 30660 tvEmpty: !tvSelectedGroup, 30661 tvLogo: tvSelectedCabanaLogo, 30662 tvLogoError:()=>{if(tvSelectedCabanaLogo)this.setState({cabanaTvLogoFailed:{...this.state.cabanaTvLogoFailed,[tvSelectedCabanaLogo]:true}});}, 30663 tvHasLogo: !!tvSelectedCabanaLogo, 30664 tvNoLogo: !tvSelectedCabanaLogo, 30665 tvRosganLogo: '/assets/rosgan-logo-blue.png', 30666 tvKicker: current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabana', 30667 tvClockLabel: new Date(tvClock || Date.now()).toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' }), 30668 tvLogosVisible: tvConfig.logosVisible !== false, 30669 tvClockVisible: tvConfig.clockVisible !== false, 30670 tvKickerVisible: tvConfig.kickerVisible !== false, 30671 tvChannelBugVisible: tvConfig.logosVisible !== false || tvConfig.clockVisible !== false || tvConfig.kickerVisible !== false, 30672 tvLiveBugVisible: tvConfig.clockVisible !== false || tvConfig.kickerVisible !== false, 30673 tvWeatherVisible: !!tvWeather && tvConfig.weatherVisible !== false, 30674 tvWeatherIcon, 30675 tvWeatherTemp: tvWeather ?(String(tvWeather.temperature) + tvWeatherUnit) : '', 30676 tvWeatherLabel: tvWeather?.label || '', 30677 tvWeatherLocation: tvWeather?.location || s.cabanaTvWeatherLocation || '', 30678 tvWaitingVisible: tvGraphicsVisible && tvConfig.waitingVisible === true, 30679 tvEndVisible: tvGraphicsVisible && tvConfig.endVisible === true, 30680 tvWaitingTitle: current?.nombre_remate || current?.titulo || current?.title || 'Remate de cabaña', 30681 tvWaitingDate: tvWaitingStart ?tvWaitingStart.toLocaleDateString('es-AR', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' }) : 'Fecha a confirmar', 30682 tvWaitingTime: tvWaitingStart ?(tvWaitingStart.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', hour12: false }) + ' h') : 'Hora a confirmar', 30683 tvWaitingLocation, 30684 tvWaitingCountdownLabel: tvWaitingCountdownActive ?'El remate comienza en' : (tvWaitingStart ?'Estamos listos' : 'Próximamente'), 30685 tvWaitingCountdownActive, 30686 tvWaitingMessageVisible: !tvWaitingCountdownActive, 30687 tvWaitingMessage: tvWaitingStart ?'Enseguida comienza el remate' : 'Horario a confirmar', 30688 tvWaitingCountdownParts: [ 30689 { label: 'DÃas', value: tvWaitingPad(tvWaitingDays) }, 30690 { label: 'Horas', value: tvWaitingPad(tvWaitingHours) }, 30691 { label: 'Minutos', value: tvWaitingPad(tvWaitingMinutes) }, 30692 { label: 'Segundos', value: tvWaitingPad(tvWaitingSeconds) }, 30693 ], 30694 tvWaitingNow: new Date(tvClock || Date.now()).toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }), 30695 tvWaitingConsLogoFilter: currentConsignatariaWhiteLogo ?'none' : 'brightness(0) invert(1)', 30696 tvVideoVisible: tvGraphicsVisible && tvConfig.videoVisible === true && tvVideoItems.length > 0, 30697 tvLowerThirdVisible: tvConfig.lowerThirdVisible !== false, 30698 tvCabanaName: tvSelectedCabanaName, 30699 tvCabanaColor: tvSelectedCabanaColor, 30700 tvCabanaLogoFilter:'none',
30701 tvCabanaLogoBackground:tvSelectedLogoIsWhite?tvSelectedCabanaColor:'#ffffff', 30702 tvConsName: currentConsignatariaName, 30703 tvConsLogo: currentConsignatariaWhiteLogo || currentConsignatariaLogo, 30704 tvHasConsLogo: !!(currentConsignatariaWhiteLogo || currentConsignatariaLogo), 30705 tvNoConsLogo: !(currentConsignatariaWhiteLogo || currentConsignatariaLogo), 30706 tvConsInitial: (currentConsignatariaName || 'C').charAt(0).toUpperCase(), 30707 tvConsColor: currentConsignatariaColor, 30708 tvLotLabel: tvGroupLabel(tvSelectedGroup), 30709 tvActiveRpLabel: tvShowingDeps ?('RP ' + tvActiveDepCard.rp) : '', 30710 tvHasActiveRp: tvShowingDeps, 30711 tvActiveRpRollClass: tvRollClassFor('active-rp', tvShowingDeps ?('RP ' + tvActiveDepCard.rp) : ''), 30712 tvCategory: tvSelectedGroup?.categoryName || '', 30713 tvCount: String(tvQuantityCount), 30714 tvRows: tvActiveRows, 30715 tvColumns: tvActiveColumns, 30716 tvShowTableHead: !tvShowingDeps, 30717 tvGridCols, 30718 tvTableClass: 'tv-lower-table ' + (tvShowingDeps ?'is-deps is-single-dep ' : '') + (Math.max(tvActiveRows.length, tvActiveColumns.length) >= 8 ?'is-max' : (Math.max(tvActiveRows.length, tvActiveColumns.length) >= 5 ?'is-many' : '')), 30719 tvLotOptions: tvGroups.map((group) => ({ key: group.key, label: tvGroupLabel(group) + ' - ' + group.categoryName })), 30720 tvGroupListTitle: groupUnit === 'corrales' ?'Corrales' : (groupUnit === 'lotes' ?'Lotes' : 'Lotes y corrales'), 30721 tvSelectedKey: storedTvKey, 30722 tvVisibleLabel: tvConfig.visible === false ?'Gráfica oculta' : 'Gráfica visible', 30723 tvVisibleSwitchClass: tvConfig.visible === false ?'tv-config-switch' : 'tv-config-switch is-on', 30724 tvToggleVisible: () => this.toggleCabanaTvVisible(), 30725 tvContactsText: tvConfig.contacts || '', 30726 tvContactsChange: (event) => this.saveCabanaTvConfig({ contacts: event.target.value }), 30727 tvShowContacts: tvConfig.visible !== false && tvConfig.contactsVisible === true && !tvConfig.waitingVisible && !tvConfig.endVisible && !!String(tvConfig.contacts || '').trim(), 30728 tvScreenElements: [ 30729 ['contactsVisible', 'Ticker de teléfonos'], 30730 ['lowerThirdVisible', 'Zócalo'], 30731 ['saleColumnVisible', 'Columna Venta'], 30732 ['waitingVisible', 'Placa de espera'], 30733 ['endVisible', 'Fin del remate'], 30734 ['logosVisible', 'Mosca de logos'], 30735 ['weatherVisible', 'Clima'], 30736 ['clockVisible', 'Hora'], 30737 ['kickerVisible', 'Nombre del remate'], 30738 ['videoVisible', 'Videos'], 30739 ].map(([key, label]) => ({ key, label, className: tvConfig[key] === true ?'tv-config-screen-toggle is-on' : 'tv-config-screen-toggle', disabled: (tvConfig.waitingVisible === true || tvConfig.endVisible === true) && !['waitingVisible', 'endVisible'].includes(key), toggle: () => this.toggleCabanaTvElement(key) })), 30740 tvVideosConfigVisible: tvConfig.videoVisible === true, 30741 tvVideoModeOptions: [ 30742 ['single', '1 RP'], 30743 ['grid', '2-4 videos'], 30744 ].map(([mode, label]) => ({ mode, label, className: tvVideoMode === mode ?'is-active' : '', select: () => this.setCabanaTvVideoMode(mode) })), 30745 tvVideoTransportVisible: tvVideoMode === 'single', 30746 tvVideoTransportDisabled: tvReadyVideoItems.length === 0, 30747 tvVideoTransportRp: tvSelectedVideoItem?.rp || (tvVideosPending ?'Cargando videos...' : 'Sin videos disponibles'), 30748 tvVideoPlaybackStatus: tvReadyVideoItems.length ?(tvConfig.videoPlaying === false ?'Pausado' : 'Reproduciendo') : 'Sin señal', 30749 tvVideoPlaybackAction: tvConfig.videoPlaying === false ?'Reproducir' : 'Pausar', 30750 tvVideoSelectedKey: tvSelectedVideoItem?.key || '', 30751 tvVideoRpOptions: tvReadyVideoItems.map((item) => ({ key: item.key, label: item.rp || 'RP' })), 30752 tvSelectVideoRp: (event) => this.setCabanaTvVideoRpKey(event.target.value), 30753 tvPrevVideoRp: () => this.stepCabanaTvVideoRp(tvReadyVideoItems, -1), 30754 tvNextVideoRp: () => this.stepCabanaTvVideoRp(tvReadyVideoItems, 1), 30755 tvRestartVideo: () => this.restartCabanaTvVideo(tvSelectedVideoItem?.key || ''), 30756 tvToggleVideoPlayback: () => this.toggleCabanaTvVideoPlayback(tvSelectedVideoItem?.key || ''), 30757 tvDepsLabel: tvConfig.depsEnabled === true ?'DEPs activos' : 'DEPs ocultos',
30758 tvDepsSwitchClass: tvConfig.depsEnabled === true ?'tv-config-switch is-on' : 'tv-config-switch', 30759 tvToggleDeps: () => this.toggleCabanaTvDeps(), 30760 tvDepsModeLabel: tvShowingDeps ?('RP ' + tvActiveDepCard.rp) : '', 30761 tvShowingDeps, 30762 tvConfigBack: () => this.backToCabanaCatalog(), 30763 tvCategoryFieldGroups: tvCategoryOptions.map((category) => { 30764 const defs = tvFieldDefsByCategory[category.key] || []; 30765 const allowed = defs.map((field) => field.key); 30766 const selected = this.cabanaTvFieldsForCategory(tvConfig, category.key, allowed); 30767 return { 30768 key: category.key, 30769 label: category.label, 30770 subforms: (tvFieldGroupsByCategory[category.key] || []).map((group) => ({ 30771 key: group.key, 30772 label: group.label, 30773 fields: (group.fields || []).map((field) => ({ key: field.key, label: field.label, checked: selected.includes(field.key), toggle: () => this.toggleCabanaTvField(field.key, category.key) })), 30774 })), 30775 }; 30776 }), 30777 tvFieldOptions: tvFieldDefs.map((field) => ({ key: field.key, label: field.label, checked: tvSelectedFields.includes(field.key), toggle: () => this.toggleCabanaTvField(field.key, tvSelectedCategoryKey) })), 30778 tvLotRows: tvGroups.map((group) => { 30779 const variants = Array.isArray(group.variants) ?group.variants : []; 30780 const statuses = variants.map((variant) => tvCommercialForVariant(variant).status); 30781 const allLifted = statuses.length > 0 && statuses.every((status) => status === 'levantado' || status === 'lifted'); 30782 const allResolved = statuses.length > 0 && statuses.every((status) => status && status !== 'pendiente' && status !== 'pending'); 30783 const classes = ['tv-config-lot']; 30784 if (group.key === storedTvKey) classes.push('is-active'); 30785 if (allLifted) classes.push('is-all-lifted'); 30786 return { 30787 key: group.key, 30788 lotNumber: tvGroupLabel(group), 30789 category: group.categoryName, 30790 count: variants.length, 30791 className: classes.join(' '), 30792 disabled: allLifted, 30793 statusClass: 'tv-config-lot-status ' + (allLifted ?'is-lifted' : (allResolved ?'is-complete' : 'is-empty')), 30794 statusIsLifted: allLifted, 30795 statusIsResolved: !allLifted && allResolved, 30796 statusLabel: allLifted ?'Todos los RP levantados' : (allResolved ?'Todos los RP procesados' : 'Tiene RP pendientes'), 30797 select: () => { if (!allLifted) this.setCabanaTvLotKey(group.key); }, 30798 }; 30799 }), 30800 tvLowerClass: 'tv-lower-third', 30801 tvCategoryRollClass: tvRollClassFor('category', tvSelectedGroup?.categoryName || ''), 30802 tvCountRollClass: tvRollClassFor('count', String(tvQuantityCount)), 30803 tvLotRollClass: tvRollClassFor('lot', tvGroupLabel(tvSelectedGroup)), 30804 tvSelectLot: (event) => this.setCabanaTvLotKey(event.target.value), 30805 tvPrev: () => setTvByIndex(tvIndex - 1), 30806 tvNext: () => setTvByIndex(tvIndex + 1), 30807 goCabanas: () => this.go('cabanas'), goCabanaBack: () => { this.flushCabanaLotViewTimer('back_to_catalog'); if (this.state.publicAdvanceMode === 'lacassina') { this.writeAppHistory('/lacassina'); window.__ROSGAN_PUBLIC_PATH__ = '/lacassina'; window.__ROSGAN_PUBLIC_LOT_NUMBER__ = ''; window.__ROSGAN_PUBLIC_RP__ = ''; } else this.syncCabanaLocation(this.state.currentCabana); this.setState({ route: 'cabanaDetail', currentCabanaLot: null, currentCabanaLotVariants: [], cbLotVideo: 0, cabanaLotVideosLoading: false, cabanaLotMediaReady: false, cabanaLotMediaFailed: false, cabanaPublicDeepLoading: false }); }, 30808 clHasRpVariants: clRpVariants.length > 1, clRpVariants, 30809 clTitle: selectedMapped?.hasLotNumber ?(selectedMapped.lotLabel + ' ' + selectedMapped.lotNumber) : (selectedMapped?.name || selectedMapped?.lotLabel || 'Lote'), 30810 clSubtitle: selectedMapped?.category || '', clName: selectedMapped?.name || selectedMapped?.category || '', clRp: selectedMapped?.rp || '', clCategory: selectedMapped?.category || '', clBreed: selectedMapped?.breed || '', clStatus: selectedMapped?.status || '', clStatusBg: selectedMapped?.statusBg || '#65758a', clShowStatus: false, 30811 clBase: selectedMapped?.base || 'Consultar', clMediaLoading: selectedMediaLoading || selectedAssetLoading, 30812 clMediaLoaded: (event) => { 30813 mediaReady(event); 30814 const expectedUrl = selectedVideo?.url ?new URL(selectedVideo.url, window.location.href).href : ''; 30815 if (!selectedVideo?.url && selectedImage) this.trackCabanaAnalyticsOnce('image_view', selected, { mediaId: selectedImage, imageId: selectedImage, mediaType: 'image', metadata: { action: 'image_visible', mediaIndex: selectedMediaIndex } }); 30816 if (!expectedUrl || event?.currentTarget?.currentSrc === expectedUrl || event?.currentTarget?.src === expectedUrl) this.setState({ cabanaLotMediaReady: true, cabanaLotMediaFailed: false }); 30817 }, 30818 clMediaError: (event) => { 30819 const expectedUrl = selectedVideo?.url ?new URL(selectedVideo.url, window.location.href).href : ''; 30820 if (!expectedUrl || event?.currentTarget?.currentSrc === expectedUrl || event?.currentTarget?.src === expectedUrl) this.setState({ cabanaLotMediaReady: true, cabanaLotMediaFailed: true }); 30821 }, 30822 clVideoUrl: selectedVideo?.url || '', clVideoPoster: selectedVideo?.thumb || selectedImage || '', 30823 clVideoPlay: (event) => { this.syncCabanaVideoState(event?.currentTarget, true); this.trackCabanaVideoPlay(selected, selectedVideo?.url, selectedMediaIndex); }, 30824 clVideoPause: (event) => this.syncCabanaVideoState(event?.currentTarget, true), 30825 clVideoMeta: (event) => this.syncCabanaVideoState(event?.currentTarget, true), 30826 clVideoTimeUpdate: (event) => this.syncCabanaVideoState(event?.currentTarget, false), 30827 clVideoVolumeChange: (event) => this.syncCabanaVideoState(event?.currentTarget, true), 30828 clVideoEnded: (event) => { 30829 if (selectedVideo?.url) this.trackCabanaAnalytics('video_complete', selected, { mediaId: selectedVideo.url, videoId: selectedVideo.url, mediaType: 'video', metadata: { action: 'video_ended', mediaIndex: selectedMediaIndex } }); 30830 const video = event?.currentTarget; 30831 if (video) { 30832 video.currentTime = 0; 30833 video.play?.().catch(() => {}); 30834 this.syncCabanaVideoState(video, true); 30835 } 30836 }, 30837 clTogglePlay: (event) => this.toggleCabanaVideoPlayback(event), 30838 clSeekVideo: (event) => this.seekCabanaVideo(event), 30839 clToggleMute: (event) => this.toggleCabanaVideoMute(event), 30840 clSetVolume: (event) => this.setCabanaVideoVolume(event), 30841 clFullscreen: (event) => this.openCabanaVideoFullscreen(event), 30842 clPlayIcon: 'assets/icons/lucide/' + (s.cabanaLotVideoPlaying ?'pause' : 'play') + '.svg', 30843 clPlayLabel: s.cabanaLotVideoPlaying ?'Pausar video' : 'Reproducir video', 30844 clVolumeIcon: 'assets/icons/lucide/' + (s.cabanaLotVideoMuted ?'volume-x' : 'volume-2') + '.svg', 30845 clMuteLabel: s.cabanaLotVideoMuted ?'Activar sonido' : 'Silenciar video', 30846 clVideoProgress: Math.round(Number(s.cabanaLotVideoProgress || 0) * 10) / 10, 30847 clVideoVolume: Math.round(Number(s.cabanaLotVideoVolume || 0) * 100) / 100,
30848 clVideoTime: this.formatCabanaVideoTime((Number(s.cabanaLotVideoProgress || 0) / 100) * Number(s.cabanaLotVideoDuration || 0)) + ' / ' + this.formatCabanaVideoTime(s.cabanaLotVideoDuration), 30849 clHasVideo: selectedHasVideo, clImage: selectedImage, clShowImage: selectedHasImage, 30850 clEmptyMedia: !selectedMediaLoading && !selectedVideo?.url && !selectedImage, 30851 clPlaybackError: !selectedMediaLoading && selectedMediaFailed && !!(selectedVideo?.url || selectedImage), 30852 clRetryMedia: () => { 30853 this.setState({ cabanaLotMediaFailed: false, cabanaLotMediaReady: false }, () => this.syncCabanaLotVideo(this.cabanaLotId(this.state.currentCabanaLot))); 30854 }, 30855 clCanUploadMedia: this.canUploadCabanaLotMedia(current, selected), 30856 clUploadMedia: (event) => this.quickUploadCabanaLotVideo(event), 30857 clUploadMediaBusy: !!s.cabanaLotQuickVideoUploading, 30858 clCanSelectUpload: this.canUploadCabanaLotMedia(current, selected) && !s.cabanaLotQuickVideoUploading, 30859 clUploadProgress: Math.max(0, Math.min(100, Number(s.cabanaLotQuickVideoProgress || 0))), 30860 clUploadStage: s.cabanaLotQuickVideoStage || 'Preparando la cargaâ¦', 30861 clThumbs: selectedMediaItems.map((media, index) => { 30862 const thumbUrl = media.type === 'video' ?media.thumb : (media.thumb || media.url); 30863 return { thumb: thumbUrl, hasThumb: !!thumbUrl, label: media.label, icon: media.icon, mediaReady, mediaError, border: index === selectedMediaIndex ?(current ?this.cabanaColor(current) : '#0a5ca8') : '#dce5ef', select: () => { 30864 const activeLotId = this.cabanaLotId(this.state.currentCabanaLot); 30865 if (media.type === 'image') this.trackCabanaAnalyticsOnce('image_view', this.state.currentCabanaLot, { mediaId: media.url, imageId: media.url, mediaType: 'image', metadata: { action: 'select_image', mediaIndex: index } }); 30866 this.setState({ cbLotVideo: index, cabanaLotMediaReady: false, cabanaLotMediaFailed: false, cabanaLotVideoPlaying: false, cabanaLotVideoProgress: 0, cabanaLotVideoDuration: 0, cabanaLotVideoVolume: 0, cabanaLotVideoMuted: true }); 30867 this.syncCabanaLotVideo(activeLotId); 30868 } }; 30869 }), 30870 clHasThumbs: selectedMediaItems.length > 1, clCabana: current?.nombre_remate || current?.title || current?.titulo || '', clConsignataria: currentConsignatariaName || current?.nombre_cuenta || current?.consignataria || 'Rosgan', 30871 clConsignatariaColor: currentConsignatariaColor, clConsignatariaShadow: currentConsignatariaColor + '35', clConsignatariaWhiteLogo: currentConsignatariaWhiteLogo, 30872 clHasConsignatariaWhiteLogo: !!currentConsignatariaWhiteLogo, clNoConsignatariaWhiteLogo: !currentConsignatariaWhiteLogo, clConsignatariaInitial: (currentConsignatariaName || 'R').charAt(0).toUpperCase(), 30873 clContactName: currentContact.name, clContactPhone: currentContact.phone, clContactPhoneHref: currentContact.phoneHref, 30874 clContactWhatsappHref: currentContact.whatsappBase + (currentContact.phoneDigits ?'?text=' + encodeURIComponent(lotWhatsappText) : ''), 30875 clContactWhatsapp: () => this.trackCabanaAnalytics('contact_whatsapp_click', selected, { metadata: { action: 'contact_whatsapp', contactName: currentContact.name } }), 30876 clContactCall: () => this.trackCabanaAnalytics('contact_call_click', selected, { metadata: { action: 'contact_call', contactName: currentContact.name } }), 30877 clCanEdit: this.canEditCabanaLot(current, selected), 30878 clEditPricing: () => this.editCabanaPrebidPricing(), 30879 clEdit: () => this.openCabanaLotEdit(current, selected), 30880 clCanDelete: this.canDeleteCabanaLot(current, selected), 30881 clDelete: () => this.deleteCabanaLot(current, selected), 30882 clDeleting: !!selected && s.cabanaLotDeleting === (this.cabanaPrebidCert(selected) || this.cabanaLotId(selected)), 30883 clDeleteLabel: !!selected && s.cabanaLotDeleting === (this.cabanaPrebidCert(selected) || this.cabanaLotId(selected)) ?'Eliminando...' : 'Eliminar lote', 30884 clDeleteOpacity: s.cabanaLotDeleting ?'.6' : '1', clDeleteCursor: s.cabanaLotDeleting ?'wait' : 'pointer', 30885 clCanDeleteVideo: !!selectedVideo?.id && this.canExportCabanaCatalog(current, s.apiUser), 30886 clDeleteVideo: (event) =>
30886 this.deleteCabanaLotVideo(current, selected, selectedVideo, event), 30887 clVideoDeleting: !!selectedVideo?.id && s.cabanaLotVideoDeleting === selectedVideo.id, 30888 clDeleteVideoLabel: !!selectedVideo?.id && s.cabanaLotVideoDeleting === selectedVideo.id ?'Eliminando videoâ¦' : 'Eliminar video', 30889 clShare: () => this.shareCabanaLot(selected), 30890 clShareInstagramStory: () => this.shareCabanaLotStory(selected, { remate: current, contact: currentContact }), 30891 clShareWhatsappStatus: () => this.shareCabanaLotStory(selected, { remate: current, contact: currentContact }), 30892 clInstagramStoryLabel: s.cabanaLotStoryBusy ?'Generando...' : 'Instagram Stories', 30893 clWhatsappStatusLabel: s.cabanaLotStoryBusy ?'Generando...' : 'WhatsApp Estado', 30894 clShareCount: selectedShareCount.toLocaleString('es-AR'), 30895 clCabanaName: selectedCabanaName, clCabanaLogo: selectedCabanaLogo, clHasCabanaLogo: !!selectedCabanaLogo, clHasCabanaName: !!selectedCabanaName, clShowCabanaNameCard: !!selectedCabanaName && !selectedCabanaLogo, 30896 clTables: detailTables, clPrimaryGroups: primaryDetailGroups, clSecondaryGroups: secondaryDetailGroups, clShowBaseCard: !!(prebidActive && (selectedPrebidWinner || (selectedMapped?.base && selectedMapped.base !== 'Consultar'))), 30897 clPrebidAmountLabel:selectedInstallments.enabled?'MONTO POR CUOTA':'MONTO', 30898 clInstallmentsEnabled:selectedInstallments.enabled, 30899 clInstallmentPriceTotal:selectedInstallments.count+' cuotas · Total: '+this.fmtARS(window.CabanaInstallments.total(selectedPrebidWinner?.amount??selectedPrebidBase,selectedInstallments)), 30900 clInstallmentOfferTotal:selectedInstallments.count+' cuotas de '+this.fmtARS(this.cabanaPrebidNumber(selectedPrebidForm.amount||selectedPrebidSuggested))+' · Total por cabeza: '+this.fmtARS(window.CabanaInstallments.total(this.cabanaPrebidNumber(selectedPrebidForm.amount||selectedPrebidSuggested),selectedInstallments)), 30901 clOwnWinning:!!selectedOwnWinner, clOwnWinningLabel:selectedPrebidWindowOpen?'Estás ganando':'Ganaste la preoferta', 30902 clPriceBackground:selectedOwnWinner?'linear-gradient(145deg,#075d46,#119b70)':'linear-gradient(145deg,#002e5f,'+this.cabanaColor(current)+')', 30903 clPriceLabel: (selectedPrebidWinner ?'OFERTA ACTUAL' : 'PRECIO BASE')+(selectedInstallments.enabled?' / CUOTA':''), 30904 clPriceAmount: selectedPrebidWinner ?this.fmtARS(selectedPrebidWinner.amount) : (selectedMapped?.base || 'Consultar'), 30905 clObservations: observations, clHasObservations: !isEmptyValue(observations), 30906 clPrebidEnabled: prebidActive, 30907 clPrebidOpen: prebidWindow.active, 30908 clPrebidUpcoming: prebidWindow.upcoming, 30909 clPrebidClosed: !prebidWindow.active && !prebidWindow.upcoming, 30910 clPrebidCardClass: prebidWindow.active ? '' : 'is-inactive', 30911 clPrebidStart: prebidWindow.start || 0, 30912 clPrebidStartLabel: prebidWindow.upcoming ? new Date(prebidWindow.start).toLocaleString('es-AR', {timeZone:'America/Argentina/Buenos_Aires',day:'numeric',month:'long',hour:'2-digit',minute:'2-digit',hourCycle:'h23'}) + ' h (Argentina)' : '', 30913 30914 clPrebidLoading: !!s.cabanaPrebidsLoading, 30915 clPrebidHasError: !!s.cabanaPrebidsError, 30916 clPrebidError: s.cabanaPrebidsError || '', 30917 clPrebidHasWinner: !!selectedPrebidWinner, 30918 clPrebidCurrentAmount: selectedPrebidWinner ?this.fmtARS(selectedPrebidWinner.amount) : 'Sin preofertas', 30919 clPrebidCurrentRoll: selectedPrebidWinner ?this.rollNumber(selectedPrebidWinner.amount) : 'Sin preofertas', 30920 clPrebidSuggestedAmount: this.fmtARS(selectedPrebidSuggested), 30921 clPrebidIncrement: this.fmtARS(selectedPrebidIncrement), 30922 clPrebidHistory: selectedPrebidHistory, 30923 clPrebidHasHistory: selectedPrebidHistory.length > 0, 30924 clPrebidEmpty: !s.cabanaPrebidsLoading && !s.cabanaPrebidsError && selectedPrebidHistory.length === 0, 30925 clOpenPrebid: () => this.openCabanaPrebid(), 30926 clViewPrebidHistory: () => { const history = document.getElementById('cabana-prebid-history'); if (!history) return; history.focus({ preventScroll:true }); history.scrollIntoView({ behavior:window.matchMedia('(prefers-reduced-motion: reduce)').matches ?'auto' : 'smooth', block:'start' }); }, 30927 clPrebidModalOpen: !!s.cabanaPrebidModalOpen, 30928 clPrebidSuccessOpen: !!s.cabanaPrebidSuccessFx, 30929 clPrebidSuccessEyebrow: s.cabanaPrebidSuccessFx?.eyebrow || '', 30930 clPrebidSuccessTitle: s.cabanaPrebidSuccessFx?.title || '', 30931 clPrebidSuccessAmount: s.cabanaPrebidSuccessFx?.amount || '', 30932 clPrebidSuccessMessage: s.cabanaPrebidSuccessFx?.message || '', 30933 clClosePrebid: () => this.closeCabanaPrebid(), 30934 clPrebidCuit: this.cuitInputDisplay(selectedPrebidForm.cuitCuil), 30935 clSetPrebidCuit: (event) => this.setCabanaPrebidField('cuitCuil', this.cuitInputRaw(event.target.value)), 30936 clPrebidAmount: this.moneyInputDisplay(selectedPrebidForm.amount || selectedPrebidSuggested), 30937 clPrebidAmountRoll: this.rollInteger(this.cabanaPrebidNumber(selectedPrebidForm.amount || selectedPrebidSuggested)), 30938 clPrebidRolling: !!s.cabanaPrebidRolling, 30939 clPrebidRollOpacity: s.cabanaPrebidRolling ?'1' : '0', 30940 clPrebidRollTransition: s.cabanaPrebidRolling ?'none' : 'opacity 120ms ease', 30941 clSetPrebidAmount: (event) => this.setCabanaPrebidField('amount', this.moneyInputRaw(event.target.value)), 30942 clPrebidMinus: () => this.adjustCabanaPrebidAmount(-1), 30943 clPrebidPlus: () => this.adjustCabanaPrebidAmount(1), 30944 clPrebidCommission: !!selectedPrebidForm.enComision, 30945 clPrebidCommissionBg: selectedPrebidForm.enComision ?'#1f8a67' : '#d7e1eb', 30946 clPrebidCommissionLeft: selectedPrebidForm.enComision ?'25px' : '4px', 30947 clTogglePrebidCommission: () => this.setCabanaPrebidField('enComision', !selectedPrebidForm.enComision), 30948 clPrebidCommissionFor: selectedPrebidForm.enComisionPara || '', 30949 clSetPrebidCommissionFor: (event) => this.setCabanaPrebidField('enComisionPara', event.target.value), 30950 clPrebidGrouped: selectedPrebidGrouped, 30951 clPrebidNeedsCuit: !s.cabanaPrebidProfile?.cuitCuil, 30952 clPrebidCertMode: current?.prebid?.preofertaIndividual === true ?'Elegà un animal.' : 'Elegà uno o más animales, consecutivos o no.', 30953 clPrebidCertOptions: selectedPrebidCerts.map((cert) => { 30954 const isSelected = (selectedPrebidForm.selectedIdentifiers || []).map(String).includes(String(cert)); 30955 return { 30956 cert, 30957 role: current?.prebid?.preofertaIndividual === true ?'radio':'checkbox', 30958 label: this.cabanaPrebidIdentifierLabel(cert), 30959 selected: isSelected, 30960 bg: isSelected ?'#e8f7ef' : '#fff', 30961 border: isSelected ?'#1f8a67' : '#cdd9e5', 30962 color: isSelected ?'#087653' : '#50647a', 30963 icon: isSelected ?'assets/icons/lucide/check.svg' : 'assets/icons/lucide/circle-dollar-sign.svg', 30964 cursor: 'pointer', 30965 toggle: () => this.toggleCabanaPrebidCert(String(cert)), 30966 }; 30967 }), 30968 clPrebidHasCerts: selectedPrebidCerts.length > 0, 30969 clPrebidHasMessage: !!s.cabanaPrebidMessage || !prebidWindow.active, 30970 clPrebidMessage: !prebidWindow.active ? (prebidWindow.upcoming ? 'Las preofertas todavÃa no comenzaron.' : 'Las preofertas están cerradas.') : window.CabanaPrebid.errorMessage(s.cabanaPrebidMessage?.text || '', ''), 30971 clPrebidMessageBg: s.cabanaPrebidMessage?.ok ?'#e7f7ef' : '#fff0f1', 30972 clPrebidMessageColor: s.cabanaPrebidMessage?.ok ?'#087653' : '#b4232f', 30973 clPrebidSubmitting: !!s.cabanaPrebidSubmitting || !this.cabanaPrebidCuitReady() || !prebidWindow.active, 30974 clCuitCheckVisible:!!s.cabanaPrebidCuitCheck, clCuitCheckText:s.cabanaPrebidCuitCheck?.text || '', 30975 clCuitCheckError:s.cabanaPrebidCuitCheck?.status === 'error', 30976 clCuitCheckColor:s.cabanaPrebidCuitCheck?.status === 'error' ? '#b4232f' : '#087653', 30977 clRetryCuit:()=>this.checkCabanaPrebidCuit(s.cabanaPrebidProfile?.cuitCuil || s.cabanaPrebidForm?.cuitCuil), 30978 clPrebidSubmitLabel: s.cabanaPrebidSubmitting ?'Enviando...' : s.cabanaPrebidCuitCheck?.status === 'loading' ? 'Validando CUITâ¦' : 'Confirmar preoferta', 30979 clPrebidSubmitOpacity: s.cabanaPrebidSubmitting || !this.cabanaPrebidCuitReady() || !prebidWindow.active ?'.58' : '1', 30980 clSubmitPrebid: () => this.submitCabanaPrebid(), 30981 cabanaDetailCols: detailStacked ?'1fr' : 'minmax(0, 1fr) minmax(280px, 360px)', cabanaSticky: detailStacked ?'static' : 'sticky', 30982 }; 30983 })(), 30984 ...(() => { 30985 const remate = s.cabanaLotCreateRemate; 30986 const schema = Array.isArray(remate?.formsByCategory) ?remate.formsByCategory : []; 30987 const form = s.cabanaLotCreateForm || { categoryId: '', values: {}, observations: '' }; 30988 const participantCabanas = remate ?this.cabanaRemateParticipants(remate) : []; 30989 const categoryKey = (category) => String(category?.categoryId || category?.categoryName || ''); 30990 const selectedCategory = schema.find((category) => categoryKey(category) === String(form.categoryId)) || schema[0]; 30991 const subforms = (selectedCategory?.forms || []).filter((subform) => subform.isActive !== false).sort((a, b) => Number(a.displayOrder || 0) - Number(b.displayOrder || 0)).map((subform, formIndex) => ({ 30992 number: formIndex + 1, name: subform.name || 'Datos del lote', 30993 fields: (subform.fields || []).sort((a, b) => Number(a.order || 0) - Number(b.order || 0)).map((field, fieldIndex) => {
30994 const key = categoryKey(selectedCategory) + ':' + formIndex + ':' + fieldIndex; 30995 const type = String(field.type || 'Texto').toLowerCase(); 30996 const value = form.values?.[key] ?? (type === 'booleano' ? false : ''); 30997 return { label: field.name || 'Campo', value, placeholder: 'Completar ' + String(field.name || 'campo').toLowerCase(), options: field.options || [], isPrimary: !!field.isPrimaryIdentifier, isText: type === 'texto', isNumber: type === 'numero' || type === 'número', isDate: type === 'fecha', isSelect: type === 'seleccion' || type === 'selección', isBoolean: type === 'booleano' || type === 'sà / no', on: (event) => this.setCabanaLotValue(key, event.target.value, type === 'texto' || type === 'numero' || type === 'número'), toggle: () => this.setCabanaLotValue(key, !value), track: value ?'#1f8a70' : '#cfd8e3', knob: value ?'15px' : '0px', booleanLabel: value ?'SÃ' : 'No' }; 30998 }), 30999 })); 31000 const editing = !!s.cabanaLotEditingCert; 31001 const loading = !!s.cabanaLotCreateLoading; 31002 const saving = !!s.cabanaLotCreateSaving; 31003 return { 31004 cabanaLotHasCabanas: participantCabanas.length > 0, cabanaLotNoCabanas: participantCabanas.length === 0, 31005 cabanaLotCabanas: participantCabanas.map((cabana) => { const active = cabana.key === String(form.cabanaKey || ''); const brand = cabana.brandColor || '#0a5ca8'; return { ...cabana, logo: cabana.logoDataUrl, hasLogo: !!cabana.logoDataUrl, noLogo: !cabana.logoDataUrl, initial: cabana.name.charAt(0).toUpperCase(), brand, select: () => this.setCabanaLotCabana(cabana.key), border: active ?brand : '#dce5ef', bg: active ?'#f4f9fd' : '#fff', shadow: active ?'0 0 0 2px ' + brand + '22' : 'none', checkBorder: active ?brand : '#bdcad8', checkBg: active ?brand : '#fff', check: active ?'?' : '' }; }), 31006 cabanaLotRemateTitle: remate?.nombre_remate || remate?.titulo || remate?.title || 'Remate de cabaña', 31007 cabanaLotPageTitle: editing ?'Editar lote' : 'Crear lote', cabanaLotModeLabel: editing ?('CERT. ' + s.cabanaLotEditingCert) : 'BORRADOR', cabanaLotBackLabel: editing ?'Volver al lote' : 'Volver a Cabañas', 31008 cabanaLotCreateLoading: loading, cabanaLotNoSchema: !loading && schema.length === 0, cabanaLotHasSchema: !loading && schema.length > 0, 31009 cabanaLotCategories: schema.map((category) => { const active = categoryKey(category) === categoryKey(selectedCategory); return { label: category.categoryName || 'CategorÃa', select: () => this.setCabanaLotCategory(categoryKey(category)), bg: active ?'#002e5f' : '#f5f8fb', color: active ?'#fff' : '#3d4c5f', border: active ?'#002e5f' : '#dce5ef' }; }), 31010 cabanaLotSubforms: subforms, cabanaLotObservations: form.observations || '', onCabanaLotObservations: (event) => this.setState({ cabanaLotCreateForm: { ...form, observations: event.target.value } }), 31011 cabanaLotImages: (s.cabanaLotImages || []).map((media) => ({ ...media, remove: () => this.removeCabanaLotMedia('image', media.id) })), cabanaLotHasImages: (s.cabanaLotImages || []).length > 0, cabanaLotImageCount: (s.cabanaLotImages || []).length, 31012 cabanaLotVideos: (s.cabanaLotVideosUpload || []).map((media) => ({ ...media, remove: () => this.removeCabanaLotMedia('video', media.id) })), cabanaLotHasVideos: (s.cabanaLotVideosUpload || []).length > 0, cabanaLotVideoCount: (s.cabanaLotVideosUpload || []).length, 31013 onCabanaLotImages: (event) => { this.addCabanaLotMedia('image', event.target.files); event.target.value = ''; }, onCabanaLotVideos: (event) => { this.addCabanaLotMedia('video', event.target.files); event.target.value = ''; }, saveCabanaLotDraft: () => this.saveCabanaLotDraft(), closeCabanaLotCreate: () => this.closeCabanaLotCreate(), 31014 cabanaLotCreateSaving: saving, cabanaLotSaveLabel: saving ?(editing ?'Guardando cambios...' : 'Guardando lote...') : (editing ?'Guardar cambios' : 'Guardar lote'), cabanaLotSaveOpacity: saving ?'.62' : '1', cabanaLotSaveCursor: saving ?'wait' : 'pointer', 31015 cabanaLotLayout: s.isMobile ?'1fr' : 'minmax(0,1.45fr) minmax(300px,.72fr)', cabanaLotMediaPosition: s.isMobile ?'static' : 'sticky', 31016 }; 31017 })(), 31018 navItems: navDefs.filter((n) => n.route !== 'profile').map((n) => ({ 31019 label: n.label, path: n.path, go: () => this.go(n.route), 31020 bg: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#fff' : 'transparent', color: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#002e5f' : '#c8d8ea', 31021 lightBg: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#002e5f' : 'transparent', lightColor: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#fff' : '#4a5c72', 31022 mobColor: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#e63946' : '#8593a6', mobWeight: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?3 : 2, weight: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?700 : 500, 31023 })), 31024 mobileNavItems: (s.publicAdvanceMode ?[] : navDefs.filter((n) => ['home', 'pool', 'remates', 'cabanas', 'profile'].includes(n.route))).map((n) => ({ 31025 label: n.label, go: () => this.go(n.route), 31026 current: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'page' : 'false', 31027 mobColor: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#002e5f' : '#718096', 31028 iconBg: n.route === (s.route === 'feriaConfig' ? 'feria' : activeNav) ?'#e7f0fa' : 'transparent', 31029 mobilePath: { 31030 home: 'M3 10.5 12 3l9 7.5M5 9v10a2 2 0 0 0 2 2h3v-7h4v7h3a2 2 0 0 0 2-2V9', 31031 pool: 'M4 3h7a2 2 0 0 1 1.4.6l8 8a2 2 0 0 1 0 2.8l-6 6a2 2 0 0 1-2.8 0l-8-8A2 2 0 0 1 3 11V4a1 1 0 0 1 1-1ZM7.5 7.5h.01',
31032 remates: 'm14 3 7 7-4 4-7-7 4-4ZM12 9l-8 8a2.1 2.1 0 0 0 3 3l8-8M14 21h8M16 18h4v3', 31033 cabanas: 'M8 6C5 6 4 4 4 2M16 6c3 0 4-2 4-4M7 8 2 6c0 3 2 5 5 5M17 8l5-2c0 3-2 5-5 5M7 8c0-4 10-4 10 0v6c0 4-2 7-5 7s-5-3-5-7V8ZM8 17c1-2 7-2 8 0M9.5 10.5v1M14.5 10.5v1M10 18h.01M14 18h.01', 31034 profile: 'M20 21v-2a5 5 0 0 0-5-5H9a5 5 0 0 0-5 5v2M16 6a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z', 31035 }[n.route], 31036 })), 31037 accessCards: [ 31038 { chipBg: '#002e5f', tint: '#edf3fa', icon: 'm14 4 6 6-4 4-6-6 4-4ZM13 3l8 8M9 7l8 8M12 10l-8 8a1.4 1.4 0 0 0 2 2l8-8M13 21h9M15 18h5v3', iconImage: 'assets/home-icons/remates-v2.png', title: 'Remates', text: 'La agenda del mercado', go: () => this.go('remates') }, 31039 { chipBg: '#c93248', tint: '#fdf0f2', icon: 'M4 4h7l9 9a2 2 0 0 1 0 3l-4 4a2 2 0 0 1-3 0l-9-9V4ZM8 7a1 1 0 1 0 0 2 1 1 0 0 0 0-2ZM12 12l4 4', iconImage: 'assets/home-icons/roslot-v2.png', title: 'Roslot', text: 'Explorá lotes y ofertas', go: () => this.go('pool') }, 31040 { chipBg: '#0a5ca8', tint: '#edf5fc', icon: 'M8 6C5 6 5 3 5 2m11 4c3 0 3-3 3-4M7 8 2 6c0 4 2 6 5 5m10-3 5-2c0 4-2 6-5 5M7 8c0-4 10-4 10 0v6c0 4-2 7-5 7s-5-3-5-7V8Zm2 9c1-2 5-2 6 0M9.5 10.5v1m5-1v1', iconImage: 'assets/home-icons/cabanas-v3.png', title: 'Cabañas', text: 'Genética y reproductores', go: () => this.go('cabanas') }, 31041 { id: 'feria', chipBg: '#1f7a63', tint: '#edf7f2', icon: 'M5 5.5h8a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-7a3 3 0 0 1 3-3ZM16 10l4.5-2.7a1 1 0 0 1 1.5.85v7.7a1 1 0 0 1-1.5.85L16 14M6.5 12h5M9 9.5v5', iconImage: 'assets/home-icons/feria-v2.png', title: 'Crear feria', text: 'Creá y gestioná tu transmisión', go: () => this.go('feria') }, 31042 ].filter((card) => card.title !== 'Roslot' || this.canAccessRoslot(s.apiUser)) 31043 .filter((card) => card.id !== 'feria' || this.canAccessFeria(s.apiUser)), 31044 agendaCards: upcoming.map((fa, i) => { 31045 const isToday = this.feriaDate(fa)?.toDateString() === todayKey; 31046 const consRef = this.remateConsignatariaRef(fa) || 'Rosgan'; 31047 const consName = this.consRecord(consRef)?.name || this.consRecord(consRef)?.nombre || consRef; 31048 const whiteConsLogo = this.consWhiteLogo(consRef); 31049 const consLogo = whiteConsLogo || this.consLogo(consRef); 31050 const background = this.consBackground(consRef); 31051 const brandColor = this.consBrandColor(consRef); 31052 return { 31053 title: fa.title || fa.nombre || 'Remate Rosgan', meta: this.feriaLabel(fa) + ' · ' + (fa.lugar || fa.consignataria || 'Rosgan'), 31054 href: fa.youtubeUrl || 'https://www.youtube.com/@rosgantv', 31055 thumb: background || (fa.youtubeUrl ?this.ytThumb(fa.youtubeUrl) : ['assets/feria.png', 'assets/cabanas.png', 'assets/angus-field.png'][i % 3]), 31056 consName, consLogo, hasConsLogo: !!consLogo, consLogoFilter: whiteConsLogo ?'none' : 'brightness(0) invert(1)', brandColor, thumbPosition: this.consBackgroundPosition(consRef, 'card'), 31057 thumbOverlay: this.rgba(this.shade(brandColor, -0.06), 0.82), 31058 badge: isToday ?'HOY' : (this.feriaDate(fa)?.toLocaleDateString('es-AR', { day: 'numeric', month: 'short' }) || 'PRÃXIMO'), 31059 badgeBg: isToday ?'#e63946' : '#fff', badgeColor: isToday ?'#fff' : '#0a5ca8', 31060 }; 31061 }), 31062 hasVideos: vids.length > 0, 31063 videoCards: vids.map((v) => { 31064 const vid = v.id || v.videoId || this.ytId(v.url); 31065 return { 31066 title: v.title || 'Rosgan TV', href: v.url || (vid ?'https://www.youtube.com/watch?v=' + vid : '#'), 31067 thumb: v.thumbnailUrl || v.thumbnail || (vid ?'https://i.ytimg.com/vi/' + vid + '/hqdefault.jpg' : 'assets/feria.png'), 31068 meta: v.publishedAt ?new Date(v.publishedAt).toLocaleDateString('es-AR', { day: 'numeric', month: 'long', year: 'numeric' }) : 'Rosgan TV', 31069 }; 31070 }), 31071 cafeText: s.cafe?.title || s.cafe?.description?.split('\n')[0] || 'Noticias del campo, entrevistas y actualidad ganadera todas las semanas.', 31072 cafeHref: s.cafe?.url || (s.cafe?.id ?'https://www.youtube.com/playlist?list=' + s.cafe.id : 'https://www.youtube.com/playlist?list=PLAa_PiRkH0KOCs4VtYFA7NpUeuKs_rt4f'), 31073 hasLoteNoticias: loteCards.length > 0, loteCards: loteCards.map((l) => ({ ...l, badgeBg: l.featured ?'#e63946' : 'rgba(255,255,255,0.16)' })), 31074 heroIndices: homeIndexCards, 31075 hasNoticias: !!noticiaLead, noticiaLead: noticiaLead || {}, noticiaCards, 31076 hasNotiCarousel: noticiaCards.length > 0, notiCurrent: notiCurrent || {}, notiKey: 'n' + notiIdx, notiDots, hasNotiDots: noticiaCards.length > 1, 31077 noticiasGrid: s.isMobile ?'1fr' : 'minmax(0, 1.5fr) minmax(300px, 1fr)', 31078 leadHoverIn: (e) => { const im = e.currentTarget.querySelector('[data-lead-img]'); if (im) im.style.transform = 'scale(1.06)'; }, 31079 leadHoverOut: (e) => { const im = e.currentTarget.querySelector('[data-lead-img]'); if (im) im.style.transform = 'scale(1)'; }, 31080 channelCards: consList.map((c, i) => { 31081 const name = c.name || c.nombre || c.slug || 'Canal'; 31082 const logo = this.consWhiteLogo(name) || this.consLogo(name) || c.logoUrl || c.logo || c.imageUrl || ''; 31083 const background = c.backgroundUrl || c.bannerUrl || this.consBackgroun
31083d(name); 31084 return { name, initial: name.trim()[0].toUpperCase(), chipBg: this.consBrandColor(name) || accents[i % accents.length], logo, hasLogo: !!logo, noLogo: !logo, background, hasBackground: !!background, color: this.consBrandColor(name), href: CONS_WEBS[name] }; 31085 }), 31086 toast: s.toast, toastBottom: s.isMobile ?'96px' : '28px', 31087 // ---- pool ---- 31088 poolBarTop: s.isMobile ?'0px' : '61px', 31089 showRoslotOperations: !!s.user && (roslotCanBuy || roslotCanSell), 31090 roslotMobilePanelClass: s.roslotMobilePanelOpen ?'is-mobile-open' : '', 31091 roslotMobileDockClass: s.roslotMobilePanelOpen ?'is-hidden' : '', 31092 openRoslotMobilePanel: () => this.openRoslotMobilePanel(), 31093 closeRoslotMobilePanel: () => this.closeRoslotMobilePanel(), 31094 roslotCanBuy, 31095 roslotCanSell, 31096 roslotKpis, 31097 roslotOperations, 31098 roslotHasOperations: roslotOperations.length > 0, 31099 roslotOperationsEmpty: roslotOperations.length === 0, 31100 roslotWhatsappToggleClass: [ 31101 s.whatsappNotificationsEnabled ?'is-on' : 'is-off', 31102 !s.whatsappNotificationsAvailable ?'is-unavailable' : '', 31103 (s.whatsappNotificationsSaving || s.whatsappNotificationsLoading) ?'is-busy' : '', 31104 ].filter(Boolean).join(' '), 31105 roslotWhatsappAriaChecked: s.whatsappNotificationsEnabled ?'true' : 'false', 31106 roslotWhatsappStatus: s.whatsappNotificationsSaving ?'Guardandoâ¦' 31107 : s.whatsappNotificationsLoading ?'Consultandoâ¦' 31108 : !s.whatsappNotificationsAvailable ?'Verificá tu teléfono para activarlas' 31109 : s.whatsappNotificationsEnabled ?'Activadas' : 'Desactivadas', 31110 roslotWhatsappTitle: s.whatsappNotificationsMessage || (!s.whatsappNotificationsAvailable 31111 ?'Verificá tu teléfono para recibir avisos por WhatsApp.' 31112 : s.whatsappNotificationsEnabled ?'Desactivar notificaciones por WhatsApp.' : 'Activar notificaciones por WhatsApp.'), 31113 roslotWhatsappBusy: s.whatsappNotificationsSaving || s.whatsappNotificationsLoading, 31114 toggleRoslotWhatsapp: () => this.toggleWhatsAppNotifications(), 31115 roslotDailyCategories, 31116 roslotHasDailyCategories: roslotDailyCategories.length > 0, 31117 roslotDailyCategoriesEmpty: roslotDailyCategories.length === 0, 31118 roslotDailyCategoriesLabel: roslotDailyCategories.length === 1 ?'1 categorÃa' : roslotDailyCategories.length + ' categorÃas', 31119 roslotActivity, 31120 roslotHasActivity: roslotActivity.length > 0, 31121 roslotActivityLoading: roslotActivity.length === 0 && !Array.isArray(s.notificationHistory), 31122 roslotActivityEmpty: roslotActivity.length === 0 && Array.isArray(s.notificationHistory), 31123 roslotRealtimeLabel: roslotActivity.length === 1 ?'1 evento de hoy' : roslotActivity.length + ' eventos de hoy', 31124 showRoslotLiveToast: s.route === 'pool' && s.isMobile && !!roslotLiveToastItem, 31125 roslotLiveToastTitle: roslotLiveToastItem?.title || '', 31126 roslotLiveToastBody: roslotLiveToastItem?.body || '', 31127 roslotLiveToastIcon: roslotLiveToastItem?.icon || 'assets/icons/lucide/bell-ring.svg', 31128 roslotLiveToastAria: [roslotLiveToastItem?.title, roslotLiveToastItem?.body].filter(Boolean).join('. '), 31129 openRoslotLiveToast: () => this.openRoslotLiveToast(), 31130 showPoolOfferTracker: false, 31131 poolOfferTrackerText: activeStandingCount 31132 ?(activeStandingCount === 1 ?'Tenés 1 posición activa en el pool.' : 'Tenés ' + activeStandingCount + ' posiciones activas en el pool.') 31133 : 'No tenés ofertas activas en los lotes abiertos.', 31134 poolQuery: s.poolQuery, onPoolQuery: (e) => this.bufferInputState('pool-query', { poolQuery: e.target.value }, 100), 31135 poolSort: s.poolSort, onSort: (e) => this.setState({ poolSort: e.target.value }), 31136 setPoolGridView: () => this.setState({ poolView: 'grid' }), 31137 setPoolListView: () => this.setState({ poolView: 'list' }), 31138 poolViewClass: s.poolView === 'list' ?'is-list' : 'is-grid', 31139 poolGridPressed: s.poolView !== 'list' ?'true' : 'false', 31140 poolListPressed: s.poolView === 'list' ?'true' : 'false', 31141 poolGridViewBg: s.poolView !== 'list' ?'#002e5f' : 'transparent', 31142 poolGridViewColor: s.poolView !== 'list' ?'#fff' : '#8593a6', 31143 poolListViewBg: s.poolView === 'list' ?'#002e5f' : 'transparent', 31144 poolListViewColor: s.poolView === 'list' ?'#fff' : '#8593a6', 31145 toggleFilters: () => this.setState({ filtersOpen: !s.filtersOpen }), 31146 filtersOpen: s.filtersOpen, activeFilterCount, 31147 filterSidebarClass: s.filtersOpen ?'is-mobile-open' : '', 31148 filterBtnBg: s.filtersOpen || activeFilterCount ?'#002e5f' : '#fff', 31149 filterBtnColor: s.filtersOpen || activeFilterCount ?'#fff' : '#112033', 31150 filterBtnBorder: s.filtersOpen || activeFilterCount ?'#002e5f' : '#e3eaf2', 31151 filterIconFilter: s.filtersOpen || activeFilterCount ?'none' : 'invert(40%) sepia(12%) saturate(800%) hue-rotate(175deg)', 31152 marketStatusLabel: marketStatus.label, 31153 marketStatusBg: marketStatus.bg, 31154 marketStatusColor: marketStatus.color, 31155 marketStatusBorder: marketStatus.border, 31156 marketStatusIcon: marketStatus.icon, 31157 marketCountdownLabel: s.lotsLoading ?'Sincronizando horariosâ¦' : (marketOpen ?(marketTarget ?'Cierra en ' + marketRemainingLabel(marketTarget) : 'Operando ahora') : (marketTarget ?'Abre en ' + marketRemainingLabel(marketTarget) : 'Sin subastas programadas')), 31158 marketScheduleLabel: s.lotsLoading ?'Consultando la próxima ventana del mercado' : (marketOpen ?(marketTarget ?'Cierre previsto: ' + marketTargetLabel : 'Mercado operando en este momento') : (marketTarget ?'Próxima apertura: ' + marketTargetLabel : 'Próxima apertura sin programar')), 31159 marketTimerColor: marketOpen ?'#176c58' : marketTarget ?'#07589d' : '#687a8f', 31160 marketProgressValue, 31161 marketProgress: marketProgressValue + '%', 31162 marketProgressBg: marketOpen ?'linear-gradient(90deg,#1f7a63,#43b48f)' : marketTarget ?'linear-gradient(90deg,#075da8,#28a7d8)' : '#aebdca', 31163 statusTabs: statusDefs.map((t) => { 31164 const count = poolEligibleLots.filter((lot) => isPoolDay(lot, t.key)).length; 31165 const active = s.poolStatus === t.key; 31166 return { label: t.label, count, live: !!t.live, go: () => this.setState({ poolStatus: t.key }), 31167 bg: active ?'#002e5f' : '#fff', color: active ?'#fff' : '#3d4c5f', border: active ?'#002e5f' : '#e3eaf2' }; 31168 }), 31169 filterGroups: [ 31170 { label: 'CATEGORÃA', options: mkOptions('category', filterS
31170ourceLots.map((l) => l.categoria)) }, 31171 { label: 'DESTINO', options: mkOptions('destination', filterSourceLots.map((l) => l.destino)) }, 31172 { label: 'ENGORDE', options: mkOptions('fattening', filterSourceLots.map((l) => l.engorde)) }, 31173 { label: 'PROVINCIA', options: mkOptions('province', filterSourceLots.map((l) => l.provincia)) }, 31174 ].filter((g) => g.options.length), 31175 toggleChina: () => this.setState({ poolFilters: { ...f, china: !f.china } }), 31176 chinaBg: f.china ?'#9d2235' : '#f2f6fb', chinaColor: f.china ?'#fff' : '#3d4c5f', chinaBorder: f.china ?'#9d2235' : '#dde7f2', 31177 clearFilters: () => this.setState({ poolFilters: { category: [], destination: [], fattening: [], province: [], china: false }, poolQuery: '' }), 31178 resultCount: filtered.length, resultLabel: filtered.length === 1 ?'lote encontrado' : 'lotes encontrados', 31179 lotsSourceNote: '', 31180 lotsLoading: s.lotsLoading, skeletons: Array.from({ length: 6 }, (_, i) => ({ i })), 31181 roslotPresenceBuyers: s.roslotPresenceError && !s.roslotPresence ?'â' :(s.roslotPresenceLoading && !s.roslotPresence ?'â¦' :String(s.roslotPresence?.buyers ?? 0)), 31182 roslotPresenceSellers: s.roslotPresenceError && !s.roslotPresence ?'â' :(s.roslotPresenceLoading && !s.roslotPresence ?'â¦' :String(s.roslotPresence?.sellers ?? 0)), 31183 roslotPresenceBuyerLabel: Number(s.roslotPresence?.buyers) === 1 ?'comprador' :'compradores', 31184 roslotPresenceSellerLabel: Number(s.roslotPresence?.sellers) === 1 ?'vendedor' :'vendedores', 31185 showLots: !s.lotsLoading && filtered.length > 0, showEmpty: !s.lotsLoading && !!s.lots && filtered.length === 0, 31186 poolEmptyTitle: s.poolStatus === 'upcoming' ?'No hay lotes en próximas subastas' : 'No hay lotes en la subasta de hoy', 31187 poolEmptyText: activeFilterCount || q ?'Probá limpiar algún filtro o cambiar la búsqueda.' : (s.poolStatus === 'upcoming' ?'Los próximos lotes aparecerán acá cuando sean publicados.' : 'El mercado no tiene lotes programados para hoy.'), 31188 lotCards: filtered.map((l) => ({ 31189 cert: l.cert, categoria: l.categoria, origen: l.origen, image: l.image, 31190 videoPreview: l.videoPreviewUrl, isLive: l.status === 'En vivo', isSold: l.status === 'Vendido' || l.auctionStatus === 'sold', 31191 base: this.fmtARS(l.baseNum), condicion: l.condicion, cabezas: String(l.cabezas), peso: l.peso, 31192 ...(() => { 31193 const offerCount = Math.max(Number(l.bidCount) || 0, (s.lotBids?.[l.cert] || []).length); 31194 const hasOffers = offerCount > 0 || !!l.currentBidId || !!l.currentBidBuyerUid; 31195 const isSold = l.status === 'Vendido' || l.auctionStatus === 'sold'; 31196 const current = Number(isSold ?l.soldAmount : l.currentBidAmount) || Number(l.currentBidAmount) || Number(l.baseNum) || 0; 31197 const base = Number(l.baseNum) || 0; 31198 const variation = base ?((current - base) / base) * 100 : 0; 31199 const direction = variation > 0.005 ?'up' : variation < -0.005 ?'down' : 'flat'; 31200 return { 31201 hasOffers, 31202 priceBadgeClass: hasOffers ?'has-offers' : 'is-base', 31203 priceLabel: isSold ?'PRECIO FINAL' : hasOffers ?'ÃLTIMA OFERTA' : 'OFERTA BASE', 31204 currentPrice: this.fmtARS(current), 31205 priceChange: direction === 'up' || direction === 'down' ?Math.abs(variation).toLocaleString('es-AR', { maximumFractionDigits: 1 }) + '%' : '0%', 31206 priceChangeUp: direction === 'up', priceChangeDown: direction === 'down', 31207 priceChangeColor: direction === 'up' ?'#bdf4de' : direction === 'down' ?'#ffd0d5' : '#dbe6f2', 31208 priceChangeBg: direction === 'up' ?'rgba(31,122,99,0.58)' : direction === 'down' ?'rgba(230,57,70,0.58)' : 'rgba(255,255,255,0.12)', 31209 }; 31210 })(), 31211 razas: (l.razas.length ?l.razas : ['Sin razas']).join(', '), 31212 destino: l.destino, destinoColor: l.destino === 'Faena' ?'#9d2235' : l.destino === 'Invernada' ?'#1f7a63' : '#0a5ca8', 31213 plazo: l.plazo, plazoDetalle: l.plazoOtro, showPlazoDetalle: l.plazoEsOtro && !!l.plazoOtro, aptoChina: l.aptoChina, 31214 open: () => this.openLot(l.cert), openBid: () => this.openLot(l.cert, true), 31215 hoverIn, hoverOut, 31216 standing: l.auctionStatus === 'open' && !!standingByCert[l.cert], standingLabel: standingByCert[l.cert]?.label || '', 31217 standingColor: standingByCert[l.cert]?.color || '#1f7a63', standingBg: standingByCert[l.cert]?.bg || 'rgba(31,122,99,0.14)', 31218 standingWinning: standingByCert[l.cert]?.key === 'winning', standingOutbid: standingByCert[l.cert]?.key === 'outbid', 31219 })), 31220 // ---- lot detail ---- 31221 lotTitle: lot?.categoria || '', lotCert: lot?.cert || '', lotOrigen: lot?.origen || '', 31222 lotStatus: lot?.status === 'Vendido' && lot.fechaDisponibleCarga && lot.fechaDisponibleCarga <= this.localIsoDate() ?'Listo para la carga' : (lot?.status || ''), 31223 lotStatusBg: lot?.status === 'Vendido' ?'#1f7a63' : lot?.status === 'En vivo' ?'#e63946' : lot?.status === 'Próximo' ?'#0a5ca8' : '#65758a', 31224 lotStatusIcon: 'assets/icons/lucide/' + (lot?.status === 'Vendido' ?'circle-check-big' : lot?.status === 'En vivo' ?'radio' : lot?.status === 'Próximo' ?'clock-3' : 'circle-x') + '.svg', 31225 lotLayoutCols: s.isMobile ?'1fr' : 'minmax(0, 1.55fr) minmax(320px, 1fr)', 31226 bidPanelPos: s.isMobile ?'static' : 'sticky', 31227 lotVideoUrl: selVideo?.url || '', lotNoVideo: !selVideo?.url, lotImage: lot?.image || 'assets/angus-field.png', 31228 lotVideoPlay: (event) => this.syncRoslotLotVideoState(event?.currentTarget, true), 31229 lotVideoPause: (event) => this.syncRoslotLotVideoState(event?.currentTarget, true), 31230 lotVideoMeta: (event) => this.syncRoslotLotVideoState(event?.currentTarget, true), 31231 lotVideoTimeUpdate: (event) => this.syncRoslotLotVideoState(event?.currentTarget, false), 31232 lotVideoVolumeChange: (event) => this.syncRoslotLotVideoState(event?.currentTarget, true), 31233 lotVideoEnded: (event) => { 31234 const video = event?.currentTarget; 31235 const count = lot?.videos?.length || 0; 31236 if (count > 1) { 31237 this.setState({ selectedVideo: (this.state.selectedVideo + 1) % count, lotVideoPlaying: false, lotVideoProgress: 0, lotVideoDuration: 0, lotVideoVolume: 0, lotVideoMuted: true }); 31238 } else if (video) { 31239 video.currentTime = 0; 31240 video.play?.().catch(() => {}); 31241 this.syncRoslotLotVideoState(video, true); 31242 } 31243 }, 31244 lotTogglePlay: (event) => this.toggleRoslotLotVideoPlayback(event), 31245 lotSeekVideo: (event) => this.seekRoslotLotVideo(event), 31246 lotToggleMute: (event) => this.toggleRoslotLotVideoMute(event), 31247 lotSetVolume: (event) => this.setRoslotLotVideoVolume(event), 31248 lotFullscreen: (event) => this.openRoslotLotVideoFullscreen(event), 31249 lotPlayIcon: 'assets/icons/lucide/' + (s.lotVideoPlaying ?'pause' : 'play') + '.svg', 31250 lotPlayLabel: s.lotVideoPlaying ?'Pausar video' : 'Reproducir video', 31251 lotVolumeIcon: 'assets/icons/lucide/' + (s.lotVideoMuted ?'volume-x' : 'volume-2') + '.svg', 31252 lotMuteLabel: s.lotVideoMuted ?'Activar sonido' : 'Silenciar video', 31253 lotVideoProgress: Math.round(Number(s.lotVideoProgress || 0) * 10) / 10, 31254 lotVideoVolume: Math.round(Number(s.lotVideoVolume || 0) * 100) / 100,
31255 lotVideoTime: this.formatCabanaVideoTime((Number(s.lotVideoProgress || 0) / 100) * Number(s.lotVideoDuration || 0)) + ' / ' + this.formatCabanaVideoTime(s.lotVideoDuration), 31256 hasVideoThumbs: (lot?.videos.length || 0) > 1, 31257 videoThumbs: (lot?.videos || []).map((v, i) => { 31258 const generatedThumb = v.thumb || s.videoThumbByUrl[v.url] || ''; 31259 const fallbackThumb = lot?.image || 'assets/angus-field.png'; 31260 const thumb = generatedThumb || fallbackThumb; 31261 return { 31262 n: i + 1, 31263 url: v.url, 31264 thumb, 31265 hasThumb: !!thumb, 31266 loading: !thumb && !s.videoThumbFailedByUrl[v.url], 31267 border: i === s.selectedVideo ?'#e63946' : 'transparent', 31268 select: () => this.setState({ selectedVideo: i, lotVideoPlaying: false, lotVideoProgress: 0, lotVideoDuration: 0, lotVideoVolume: 0, lotVideoMuted: true }), 31269 }; 31270 }), 31271 lotGroups: lot ? [ 31272 { label: 'CANTIDAD Y PESO', cards: [ 31273 { label: 'CABEZAS', value: String(lot.cabezas) }, { label: 'PESO PROM.', value: lot.peso }, 31274 { label: 'PESO MÃNIMO', value: lot.pesoMin }, { label: 'PESO MÃXIMO', value: lot.pesoMax } ] }, 31275 { label: 'COMERCIAL', cards: [ 31276 { label: 'PRECIO BASE', value: this.fmtARS(lot.baseNum) }, { label: 'CONDICIÃN', value: lot.condicion }, 31277 { label: 'PLAZO', value: lot.plazo }, ...(lot.plazoEsOtro && lot.plazoOtro ?[{ label: 'DETALLE DEL PLAZO', value: lot.plazoOtro }] : []), { label: 'PESAJE', value: lot.pesaje }, 31278 { label: 'DISPONIBLE PARA CARGA', value: lot.fechaDisponibleCarga ?this.formatBaDate(lot.fechaDisponibleCarga) : 'Sin fecha informada' } ] }, 31279 { label: 'OPERACIÃN Y SANIDAD', cards: [ 31280 { label: 'ENGORDE', value: lot.engorde }, { label: 'DESTINO', value: lot.destino }, 31281 { label: 'APTO CHINA', value: lot.aptoChina ?'SÃ' : 'No' }, { label: 'DESBASTE', value: lot.desbaste }, 31282 { label: 'RENDIMIENTO', value: lot.rendimiento }, { label: 'RAZAS', value: (lot.razas.length ?lot.razas : ['â']).join(', ') } ] }, 31283 ] : [], 31284 lotObs: lot?.obs || '', 31285 lotPublisherName, 31286 lotPublisherLogo, 31287 lotPublisherHasLogo: !!lotPublisherLogo, 31288 lotPublisherNoLogo: !lotPublisherLogo, 31289 lotPublisherInitials: lotHasConsignataria 31290 ?lotPublisherName.split(/\s+/).filter(Boolean).slice(0, 2).map((word) => word[0]?.toUpperCase() || '').join('') 31291 : (lotPublisherPending ?'â¦' : 'CAF'), 31292 lotPublisherAvatarBg: lotPublisherLogo && !lotPublisherUsesWhiteLogo ?'#fff' : '#002e5f', 31293 lotViews: lot ? (() => { 31294 const count = Object.prototype.hasOwnProperty.call(s.lotViewCounts, lot.cert) 31295 ? s.lotViewCounts[lot.cert] 31296 : lot.views; 31297 return anim ?this.rollInteger(count) : Number(count || 0).toLocaleString('es-AR'); 31298 })() : '', 31299 lotBase: lot ?this.fmtARS(lot.baseNum) : '', lotCondicion: lot?.condicion || '', 31300 lotBidSuccess: !!lot && s.roslotBidSuccessFx?.cert === String(lot.cert || ''), 31301 lotBidSuccessAmount: !!lot && s.roslotBidSuccessFx?.cert === String(lot.cert || '') ?this.fmtARS(s.roslotBidSuccessFx.amount) : '', 31302 ...(() => { 31303 const cert = lot?.cert || ''; 31304 const raw = (s.lotBids[cert] || []); 31305 const loading = !!s.lotBidsLoading[cert] && !raw.length; 31306 return { 31307 lotBidsLoading: loading, 31308 lotBidsHas: raw.length > 0, 31309 lotBidsCount: String(raw.length), 31310 lotBidRows: raw.slice(0, 8).map((b, i) => { 31311 const m = this.bidStatusMeta(b.status); 31312 return { amount: this.fmtARS(b.amount), bidder: 'Oferta reservada', date: b.date ?new Date(b.date).toLocaleDateString('es-AR', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) : '', status: m.label, statusBg: m.bg, statusColor: m.color, top: i === 0 }; 31313 }), 31314 }; 31315 })(), 31316 currentBidRoll: anim && lot ?this.rollNumber(curBid) : (lot ?this.fmtARS(curBid) : ''), 31317 bidTargetRoll: anim && lot ?this.rollNumber(target) : (lot ?this.fmtARS(target) : ''), 31318 ...(() => { 31319 const averageWeight = Number(lot?.pesoNum) || Number(String(lot?.peso || '').replace(/[^\d.,-]/g, '').replace(',', '.')) || 0; 31320 const quantity = Number(lot?.cabezas) || 0; 31321 const condition = String(lot?.condicion || ''); 31322 const isPerHead = /cabeza|head/i.test(condition); 31323 const isPerKg = /kg|kilo|kilogram/i.test(condition); 31324 const isPerMeat = !isPerHead && !isPerKg && /carne|gancho|rinde|rendimiento/i.test(condition); 31325 const yieldPercent = Number(lot?.raw?.rendimientoEstimado ?? lot?.raw?.estimatedYield ?? lot?.raw?.rinde) || 0; 31326 const unitCost = target > 0 && (isPerHead || averageWeight > 0) 31327 ? (isPerHead ? target : isPerMeat ? (yieldPercent > 0 ? target * averageWeight * yieldPercent / 100 : 0) : target * averageWeight) 31328 : 0; 31329 return { 31330 bidPriceUnitLabel: isPerHead ? 'por cabeza' : isPerMeat ? 'por kg carne' : 'por kg vivo', 31331 bidEstimatedUnitCost: unitCost > 0 ?(anim ?this.rollNumber(unitCost) : this.fmtARS(unitCost)) : 'â', 31332 bidEstimatedTotalCost: unitCost > 0 && quantity > 0 ?(anim ?this.rollNumber(unitCost * quantity) : this.fmtARS(unitCost * quantity)) : 'â', 31333 bidEstimatedNote: isPerMeat && yieldPercent <= 0 ?'Falta el rendimiento para estimar el costo. Importes + IVA.' : isPerMeat ?'Calculado con ' + yieldPercent.toLocaleStr
31333ing('es-AR') + '% de rendimiento. Importes + IVA.' : 'Valores estimados. Importes + IVA.', 31334 }; 31335 })(), 31336 bidOffsetLabel: adj.offset === 0 ?'Tu oferta (sin ajuste)' : 'Tu oferta (' + (adj.offset > 0 ?'+' : 'â') + '$' + Math.abs(adj.offset).toLocaleString('es-AR') + ')', 31337 bidPlus: () => lot && this.setBidAdj(lot.cert, { ...adj, offset: adj.offset + adj.increment }), 31338 bidMinus: () => lot && this.setBidAdj(lot.cert, { ...adj, offset: Math.max(adj.offset - adj.increment, -curBid) }), 31339 plusBg: 'rgba(31,122,99,0.75)', minusBg: 'rgba(230,57,70,0.55)', 31340 bidIncrements: this.INCREMENTS.map((inc) => ({ 31341 label: '+$' + inc, pick: () => lot && this.setBidAdj(lot.cert, { increment: inc, offset: adj.offset === adj.increment ?inc : adj.offset }), 31342 bg: adj.increment === inc ?'#fff' : 'rgba(255,255,255,0.08)', color: adj.increment === inc ?'#002e5f' : '#dbe6f2', 31343 border: adj.increment === inc ?'#fff' : 'rgba(255,255,255,0.22)', 31344 })), 31345 ...(() => { 31346 const isOpen = lot?.auctionStatus === 'open'; 31347 const isSold = lot?.auctionStatus === 'sold' || lot?.status === 'Vendido'; 31348 const isScheduled = lot?.auctionStatus === 'scheduled' || lot?.status === 'Próximo'; 31349 const isOwnLot = !!(lot?.sellerUid && s.user?.uid && lot.sellerUid === s.user.uid); 31350 const hasOffer = !!(lot?.currentBidId || lot?.currentBidBuyerUid || Number(lot?.bidCount) > 0 || (s.lotBids?.[lot?.cert] || []).length > 0); 31351 const isLeading = !!(lot && isOpen && !isOwnLot && ( 31352 (lot.currentBidBuyerUid && String(lot.currentBidBuyerUid) === String(s.user?.uid || '')) 31353 || standingByCert[lot.cert]?.key === 'winning' 31354 )); 31355 const closing = !!(lot?.cert && s.lotClosingCert === lot.cert); 31356 const canClose = !!(lot && isOpen && isOwnLot && hasOffer && !closing && s.lotsReal); 31357 const canBid = !!(lot && isOpen && !isOwnLot && !isLeading && !s.bidSubmitting && s.lotsReal); 31358 return { 31359 bidPriceTitle: isSold ?'PRECIO FINAL' : 'OFERTA VIGENTE', 31360 bidPriceSubtitle: isSold ?'Lote vendido · ' + lot.condicion : 'Base configurada: ' + this.fmtARS(lot?.baseNum || 0) + ' · ' + (lot?.condicion || ''), 31361 bidPrimaryAction: () => isOwnLot ?this.closeRoslotLot(lot?.cert) : this.submitBid(), 31362 bidDisabled: isOwnLot ?!canClose : !canBid, 31363 bidControlsVisible: canBid, 31364 bidControlsOpacity: canBid ?'1' : '0.42', 31365 bidControlsPointer: canBid ?'auto' : 'none', 31366 showSellerHammerPanel: isOwnLot && isOpen, 31367 sellerHammerMessage: hasOffer 31368 ? 'Si esta oferta ya te convence, podés bajar el martillo. El lote se cerrará y la oferta lÃder quedará adjudicada.' 31369 : 'Cuando el lote reciba una oferta válida, vas a poder bajar el martillo y adjudicarlo antes del cierre general.', 31370 showWinningNotice: isLeading, 31371 bidShowsHammer: isOwnLot && ((isOpen && hasOffer) || isSold), 31372 bidBtnLabel: closing 31373 ? 'Bajando el martilloâ¦' 31374 : s.bidSubmitting 31375 ? 'Enviando ofertaâ¦' 31376 : isSold 31377 ? 'Subasta cerrada' 31378 : isScheduled 31379 ? 'Subasta aún no abierta' 31380 : !isOpen 31381 ? 'Subasta cerrada' 31382 : isOwnLot 31383 ? (hasOffer ?'Bajar el martillo' : 'Esperando ofertas') 31384 : isLeading 31385 ? '¡Vas ganando!' 31386 : 'Ofertar ' + (lot ?this.fmtARS(target) : ''), 31387 bidBtnBg: isSold || isLeading ?'#1f7a63' : canClose ?'#b86d00' : canBid ?'#e63946' : isScheduled ?'#0a5ca8' : '#65758a',
31388 bidBtnCursor: canBid || canClose ?'pointer' : 'default', 31389 bidBtnOpacity: '1', 31390 bidBtnAnim: anim && (canBid || canClose) ?'bidPulse 2.4s ease-out infinite' : 'none', 31391 }; 31392 })(), 31393 bidMessage: s.bidMessage?.text || '', bidMsgBg: s.bidMessage?.ok ?'rgba(95,214,143,0.18)' : 'rgba(230,57,70,0.25)', 31394 bidMsgColor: s.bidMessage?.ok ?'#bff0d3' : '#ffd3d7', 31395 estRows: lot ? [ 31396 { label: 'Nombre', value: lot.establecimiento }, { label: 'Localidad', value: lot.localidad }, 31397 { label: 'Provincia', value: lot.provincia || 'Pendiente' }, { label: 'Comitente', value: lot.comitente }, 31398 ] : [], 31399 // ---- load wizard ---- 31400 lotDestinationOpen:!!s.lotDestinationOpen,lotDestinationLoading:!!s.lotDestinationLoading,lotDestinationError:s.lotDestinationError||'', 31401 lotDestinationRoslot:this.canAccessRoslot()&&canLoadLot, 31402 lotDestinationChoices:(s.lotDestinationItems||[]).filter(item=>this.lotDestinationAllowed(item)).map(item=>({title:item.titulo||item.nombre_remate||item.name||'Remate '+item.id,type:item.cabanasConfig?'Cabañas':'Remate Rosgan',logos:this.lotDestinationLogos(item),choose:()=>this.chooseLotDestination(item)})), 31403 lotDestinationEmpty:!s.lotDestinationLoading&&!s.lotDestinationError&&!(s.lotDestinationItems||[]).some(item=>this.lotDestinationAllowed(item)), 31404 goMobileLoad: () => ['remateDetail', 'cabanaDetail'].includes(s.route) ? this.openLotDestination() : this.beginNewLot(), 31405 goLotDestination:()=>this.openLotDestination(),closeLotDestination:()=>this.setState({lotDestinationOpen:false}), 31406 lotDestinationKey:event=>{if(event.key==='Escape')this.setState({lotDestinationOpen:false});}, 31407 chooseRoslotDestination:()=>{if(!this.canAccessRoslot()||!canLoadLot)return;this.setState({lotDestinationOpen:false});this.beginNewLot();}, 31408 goLoad: () => this.beginNewLot(), 31409 resetForm: () => this.resetLotWizard(), 31410 wizardBack: () => this.closeLotWizard(), 31411 wizardBackLabel: s.editLotCert ?'Volver a Mis lotes' : 'Volver al pool', 31412 wizardTitle: s.editLotCert ?'Editar lote · Cert. ' + s.editLotCert : 'Cargar nuevo lote', 31413 wizardSubtitle: s.editLotCert ?'Actualizá los datos y videos del lote antes de guardar.' : 'Cuatro etapas con previsualización en vivo antes de publicar.', 31414 wizardResetLabel: s.editLotCert ?'Restablecer cambios' : 'Limpiar formulario', 31415 loadLayoutCols: s.isMobile ?'1fr' : 'minmax(0, 1.6fr) minmax(280px, 1fr)', 31416 stepKey: 'step-' + s.loadStep, 31417 isStep0: s.loadStep === 0, isStep1: s.loadStep === 1, isStep2: s.loadStep === 2, isStep3: s.loadStep === 3, 31418 wizardSteps: this.WIZ_META.map((w, i) => { 31419 const done = this.loadStepComplete(i); const active = i === s.loadStep; 31420 return { title: w.title, text: w.text, go: () => this.setState({ loadStep: i, formMessage: null }), 31421 barBg: active ?'#e63946' : done ?'#1f7a63' : '#e3eaf2', 31422 dotBg: active ?'#002e5f' : done ?'#1f7a63' : '#fff', dotColor: (active || done) ?'#fff' : '#8593a6', 31423 dotBorder: active ?'#002e5f' : done ?'#1f7a63' : '#dde7f2', done: done && !active, numberVisible: !done || active, number: String(i + 1), 31424 titleColor: active ?'#112033' : '#4a5c72' }; 31425 }), 31426 estSelectorHas: (s.establishments || []).length > 0, 31427 estSelectOptions: (s.establishments || []).map((e) => ({ id: e.id, label: e.nombre + (e.localidad ?' · ' + e.localidad : '') })), 31428 selectedEstId: fm.estId || '', 31429 estIsSaved: !!fm.estId, 31430 pickEst: (e) => this.setEst(e.target.value), 31431 ...(() => { 31432 const ef = s.estForm || this.emptyEstForm(); 31433 const cf = s.comitenteForm || this.emptyComitenteForm(); 31434 return { 31435 wzEstOpen: s.estFormOpen, 31436 wzEstToggle: () => { if (!this.canManageComitentes(s.apiUser)) return; const opening = !s.estFormOpen; if (opening) this.fetchProvinces(); this.setState({ estFormOpen: opening, estForm: s.estFormOpen ?s.estForm : this.emptyEstForm(), estFormMessage: null }); }, 31437 wzNoComitentes: !s.comitentesLoading && !(s.comitentes || []).length, 31438 wzComitenteOpen: !!s.comitenteFormOpen, 31439 wzToggleComitente: () => { const opening = !s.comitenteFormOpen; this.setState({ comitenteFormOpen: opening, comitenteForm: opening ?this.emptyComitenteForm() : s.comitenteForm, comitenteFormMessage: null, comitenteLookupLoading: false }); }, 31440 wz_cf_alias: cf.alias || '', wz_set_cf_alias: e => this.setComitenteForm('alias', e.target.value), 31441 wz_cf_cuit: cf.cuitCuil || '', wz_set_cf_cuit: (e) => this.onComitenteCuit(e.target.value), 31442 wz_cf_razon: cf.nombreFiscal || '', wz_cf_provincia: cf.provincia || '', wz_cf_localidad: cf.localidad || '', 31443 wzComitenteLookup: !!s.comitenteLookupLoading, 31444 wzComitenteMsg: s.comitenteFormMessage?.text || '', 31445 wzComitenteMsgBg: s.comitenteFormMessage?.ok ?'#e8f7f1' : '#fff0f1', wzComitenteMsgColor: s.comitenteFormMessage?.ok ?'#17664f' : '#b42335', 31446 wzSaveComitente: () => this.saveComitente(), 31447 wzComitenteSaveDisabled: !!s.comitenteSaving || !!s.comitenteLookupLoading || !cf.source, 31448 wzComitenteSaveOpacity: (s.comitenteSaving || s.comitenteLookupLoading || !cf.source) ?'.52' : '1', 31449 wzComitenteSaveLabel: s.comitenteSaving ?'Creandoâ¦' : (s.comitenteLookupLoading ?'Consultandoâ¦' : 'Guardar comitente'), 31450 wz_ef_comitenteId: ef.comitenteId || '', wz_set_ef_comitenteId: (e) => this.setEstForm('comitenteId', e.target.value), 31451 wzEfComitenteOpts: (s.comitentes || []).map((item) => ({ id: item.id, label: (item.alias || item.nombreFiscal || 'Sin razón social') + ' · ' + (item.cuitCuil || 'Sin CUIT') })), 31452 wz_ef_nombre: ef.nombre, wz_set_ef_nombre: (e) => this.setEstForm('nombre', e.target.value), 31453 wz_ef_renspa: ef.renspa || '', wz_set_ef_renspa: (e) => this.setEstForm('renspa', e.target.value), 31454 wz_ef_provincia: ef.provincia, wz_set_ef_provincia: (e) => this.setEstForm('provincia', e.target.value), 31455 wz_ef_localidad: ef.localidad, wz_set_ef_localidad: (e) => this.onEstPlaceInput(e.target.value), 31456 wz_ef_latitud: ef.latitud, wz_set_ef_latitud: (e) => this.setEstForm('latitud', e.target.value), 31457 wz_ef_longitud: ef.longitud, wz_set_ef_longitud: (e) => this.setEstForm('longitud', e.target.value), 31458 wzEfProvinceOpts: s.geoProvinces || [], wzEfProvPlaceholder: s.geoLoadingProv ?'Cargando provinciasâ¦' : 'Seleccionarâ¦', 31459 wzEfLocalityOpts: (s.geoLocs[ef.provincia] || []).map((l) => l.nombre), 31460 wzEfLocDisabled: !ef.provincia, wzEfLocBg: !ef.provincia ?'#f2f6fb' : '#fff', 31461 wzEfLocPlaceholder: !ef.provincia ?'Elegà provincia primero' : 'Escribà una localidad', 31462 wzPlaceLoading: !!s.estPlaceLoading, 31463 wzPlaceResultsOpen: !!(s.estPlaceResults || []).length, 31464 wzPlaceResults: (s.estPlaceResults || []).map((item) => ({ label: item.label || 'Localidad', address: item.address || '', pick: () => this.selectEstPlace(item) })), 31465 wzOpenPlaceResults: () => { if (ef.provincia && String(ef.localidad || '').trim().length >= 3) this.searchEstPlaces(ef.localidad, ef.provincia); }, 31466 wzClosePlaceResults: () => this.closeEstPlaceResults(), 31467 wzPlaceError: s.estPlaceError || '', 31468 wzEfCoordsNote: (ef.latitud && ef.longitud) ?'Coordenadas autocompletadas desde la localidad' : 'Se completan al elegir la localidad', 31469 wzSaveEst: () => this.saveEstablishment(true), 31470 wzEstSaveLabel: s.estSaving ?'Guardandoâ¦' : 'Guardar y usar', 31471 wzEstSaveDisabled: s.estSaving, 31472 wzEstSaveCursor: s.estSaving ?'not-allowed' : 'pointer', 31473 wzEstSaveOpacity: s.estSaving ?'0.55' : '1', 31474 wzEstFormMsg: s.estFormMessage?.text || '', wzEstFormMsgBg: s.estFormMessage?.ok ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)', wzEstFormMsgColor: s.estFormMessage?.ok ?'#1f7a63' : '#c22', 31475 }; 31476 })(), 31477 f_estName: fm.estName, set_estName: (e) => this.setF('estName', e.target.value), 31478 f_comitente: fm.comitente, set_comitente: (e) => this.setF('comitente', e.target.value), 31479 f_lat: fm.lat, set_lat: (e) => this.setF('lat', e.target.value), 31480 f_lon: fm.lon, set_lon: (e) => this.setF('lon', e.target.value), 31481 f_fecha: fm.fecha, set_fecha: (e) => this.setF('fecha', e.target.value), 31482 f_prov: fm.prov, set_prov: (e) => this.setF('prov', e.target.value), 31483 f_loc: fm.loc, set_loc: (e) => this.setF('loc', e.target.value), 31484 f_cat: fm.cat, set_cat: (e) => { 31485 const categoryId = e.target.value; 31486 this.setState({ form: { ...this.form(), cat: categoryId, subcat: '', rinde: this.catYieldPct(categoryId) }, formMessage: null }); 31487 }, 31488 f_subcat: fm.subcat, set_subcat: (e) => this.setF('subcat', e.target.value), 31489 subcatOptions: this.subcatsFor(fm.cat).map(([id, label]) => ({ id, label })), 31490 subcatHas: this.subcatsFor(fm.cat).length > 0, 31491 f_cant: fm.cant, set_cant: (e) => this.setF('cant', e.target.value), 31492 catIsMixed: this.isMixedCat(fm.cat), catNotMixed: !this.isMixedCat(fm.cat), 31493 f_cantMachos: fm.cantMachos, set_cantMachos: (e) => this.setF('cantMachos', e.target.value), 31494 f_cantHembras: fm.cantHembras, set_cantHembras: (e) => this.setF('cantHembras', e.target.value), 31495 cantLabel: this.isMixedCat(fm.cat) ?'Cantidad total (opcional)' : 'Cantidad de cabezas', 31496 categoryOptions: this.CATALOGS.categories.map(([id, label]) => ({ id, label })), 31497 razasCount: fm.razas.length, 31498 breedOptions: this.CATALOGS.breeds.map(([id, label]) => ({ label, toggle: () => this.toggleRaza(id), 31499 bg: fm.razas.includes(id) ?'#002e5f' : '#f2f6fb', color: fm.razas.includes(id) ?'#fff' : '#3d4c5f', 31500 border: fm.razas.includes(id) ?'#002e5f' : '#dde7f2' })), 31501 fatteningOptions: opt('fatteningTypes', 'engorde'), 31502 showDiasEncierre: !!fm.engorde && fm.engorde !== 'a-campo', 31503 f_diasEncierre: fm.diasEncierre, set_diasEncierre: (e) => this.setF('diasEncierre', e.target.value), 31504 destinationOptions: opt('destinations', 'destino').filter((option) => !((fm.engorde === 'racion' || fm.engorde === 'feedlot') && option.id === 'exportacion-hilton')), 31505 hiltonRestricted: fm.engorde === 'racion' || fm.engorde === 'feedlot',
31506 chinaOptions: [['true', 'SÃ'], ['false', 'No']].map(([id, label]) => ({ label, pick: () => this.setF('china', id), 31507 bg: fm.china === id ?(id === 'true' ?'#9d2235' : '#002e5f') : '#f2f6fb', color: fm.china === id ?'#fff' : '#3d4c5f', 31508 border: fm.china === id ?(id === 'true' ?'#9d2235' : '#002e5f') : '#dde7f2' })), 31509 conditionOptions: opt('saleConditions', 'cond'), 31510 weighingOptions: opt('weighingOptions', 'pesaje'), 31511 termOptions: opt('termOptions', 'plazo'), 31512 showPlazoOtro: fm.plazo === 'otro', 31513 f_plazoOtro: fm.plazoOtro, set_plazoOtro: (e) => this.setF('plazoOtro', e.target.value), 31514 f_pesoProm: fm.pesoProm, set_pesoProm: (e) => this.setF('pesoProm', e.target.value), 31515 f_pesoMin: fm.pesoMin, set_pesoMin: (e) => this.setF('pesoMin', e.target.value), 31516 f_pesoMax: fm.pesoMax, set_pesoMax: (e) => this.setF('pesoMax', e.target.value), 31517 f_precio: this.moneyInputDisplay(fm.precio), set_precio: (e) => this.setF('precio', this.moneyInputRaw(e.target.value)), 31518 f_listoParaCargar: fm.listoParaCargar === true, 31519 f_listoLabel: fm.listoParaCargar === true ?'â Listo para cargar' : 'â Listo para cargar', 31520 f_listoBg: fm.listoParaCargar === true ?'#e6f5ef' : '#f5f8fc', f_listoBorder: fm.listoParaCargar === true ?'#1f7a63' : '#dce6f0', f_listoColor: fm.listoParaCargar === true ?'#17664f' : '#3d4c5f', 31521 toggleListoParaCargar: () => this.setF('listoParaCargar', fm.listoParaCargar !== true), 31522 f_showFechaCarga: fm.listoParaCargar !== true, 31523 f_dispoMin: this.loadDateOffset(fm.fecha, 1), f_dispoMax: this.loadDateOffset(fm.fecha, 7), 31524 f_dispo: fm.dispo, set_dispo: (e) => this.setF('dispo', e.target.value), 31525 f_desbaste: fm.desbaste, set_desbaste: (e) => { let v = String(e.target.value).replace(/\D/g, '').slice(0, 3); if (v !== '' && Number(v) > 100) v = '100'; this.setF('desbaste', v); }, 31526 f_rinde: fm.rinde, set_rinde: (e) => { const raw = String(e.target.value || '').replace(',', '.').replace(/[^\d.]/g, ''); const value = Number(raw) > 100 ?'100' : raw; this.setF('rinde', value); }, 31527 f_obs: fm.obs, set_obs: (e) => this.setF('obs', e.target.value), 31528 videoSlots: fm.videos.map((v, i) => { 31529 const uploading = s.vidProg[i] != null; 31530 const filled = !!v && !uploading; 31531 const prog = Math.min(100, Math.round(s.vidProg[i] || 0)); 31532 const isLotCover = fm.thumbIdx === i; 31533 const frames = v?.frames || []; 31534 const chosen = Math.max(0, frames.findIndex((frame) => frame === v?.selectedFrame || frame?.previewUrl === v?.selectedFrame?.previewUrl)); 31535 return { n: i + 1, filled, empty: !filled && !uploading, uploading, 31536 progress: prog, progressPct: prog + '%', poster: v?.selectedFrame?.previewUrl || frames[0]?.previewUrl || '', 31537 upload: () => this.startVideoUpload(i), 31538 remove: (e) => { e.stopPropagation(); this.removeVideo(i); }, 31539 isLotCover, coverLabel: isLotCover ?'Portada del lote' : 'Marcar como portada del lote', 31540 coverBg: isLotCover ?'#002e5f' : '#fff', coverColor: isLotCover ?'#fff' : '#0a5ca8', coverBorder: isLotCover ?'#002e5f' : '#cddbec', 31541 setLotCover: () => this.setF('thumbIdx', i), 31542 posterOpts: frames.map((frame, p) => ({ url: frame.previewUrl, selected: chosen === p, 31543 pick: () => this.setVideoPoster(i, p), 31544 ring: chosen === p ?'#0a5ca8' : 'transparent' })) }; 31545 }), 31546 uploadAllVideos: () => this.uploadAllVideos(), 31547 filmAssistedVideo: () => this.filmAssistedVideo(), 31548 videosAnyPending: fm.videos.filter(Boolean).length < 3 && Object.keys(s.vidProg || {}).length === 0, 31549 formMessage: s.formMessage?.text || '', 31550 formMsgBg: s.formMessage?.ok ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)', 31551 formMsgColor: s.formMessage?.ok ?'#1f7a63' : '#c22', 31552 prevStep: () => this.setState({ loadStep: Math.max(s.loadStep - 1, 0), formMessage: null }), 31553 nextStep: () => this.loadNext(), 31554 prevVisible: s.loadStep === 0 ?'hidden' : 'visible', 31555 nextBg: s.loadStep === 3 ?'#e63946' : '#002e5f', 31556 nextLabel: s.publishing ?(s.editLotCert ?'Guardandoâ¦' : 'Publicandoâ¦') : s.loadStep === 3 ?(s.editLotCert ?'Guardar cambios' : 'Publicar lote') : 'Continuar', 31557 nextShowsArrow: !s.publishing && s.loadStep < 3, 31558 showPreviewCard: !s.isMobile, 31559 pv_image: (wizFilledVideos.length ?(fm.videos[fm.thumbIdx] ?this.videoPoster(fm.thumbIdx) : this.videoPoster(fm.videos.findIndex(Boolean))) : 'assets/angus-field.png'), 31560 pv_cert: 'Nuevo', 31561 pv_cat: this.catName(fm.cat) || 'CategorÃa del lote', pv_origen: [fm.loc, fm.prov].filter(
31561Boolean).join(', ') || 'Origen del lote', 31562 pv_precio: fm.precio ?this.fmtARS(Number(fm.precio)) : '$ â', pv_cond: this.labelOf('saleConditions', fm.cond) || 'Condición', 31563 pv_cabezas: this.totalHeads(fm) || 'â', pv_peso: fm.pesoProm ?fm.pesoProm + ' kg' : 'â', 31564 pv_razas: fm.razas.length ?fm.razas.map((id) => this.labelOf('breeds', id)).join(', ') : 'â', 31565 pv_destino: this.labelOf('destinations', fm.destino) || 'â', 31566 pv_completeness: this.formCompleteness() + '%', pv_china: fm.china === 'true', 31567 goProfile: () => this.go('profile'), 31568 ...(() => { 31569 // Dashboard values always reflect current API state; animation must never zero live data. 31570 const P = 1; 31571 const all = s.lots || []; 31572 const userUid = String(s.user?.uid || ''); 31573 const userEmail = String(s.user?.email || '').toLowerCase(); 31574 const ownFromAll = all.filter((l) => (userUid && l.sellerUid && l.sellerUid === userUid) || (userEmail && String(l.sellerEmail || '').toLowerCase() === userEmail)); 31575 const mine = (Array.isArray(s.myLots) && s.myLots.length) ?s.myLots : ownFromAll; 31576 const fmtK = (n) => Math.round(n).toLocaleString('es-AR'); 31577 const totalCabezas = mine.reduce((a, l) => a + (Number(l.cabezas) || 0), 0); 31578 const bidFor = (l) => Math.max( 31579 Number(l.baseNum) || 0, 31580 Number(l.currentBidAmount) || 0, 31581 Number(l.soldAmount) || 0, 31582 Number(s.bidsPlaced[l.cert]) || 0, 31583 ...(l.bids || []).map((bid) => Number(bid?.amount || bid) || 0), 31584 0 31585 ); 31586 const cartera = mine.reduce((a, l) => a + bidFor(l) * (l.condicion && /cabeza/i.test(l.condicion) ?(Number(l.cabezas) || 1) : (Number(l.cabezas) || 1) * (l.pesoNum || 1)), 0); 31587 const placedEntries = Object.entries(s.bidsPlaced); 31588 const offerLots = placedEntries.length ?placedEntries.map(([cert, amt]) => ({ lot: all.find((l) => l.cert === cert), amt })).filter((o) => o.lot) : []; 31589 const fallbackImg = ''; 31590 const profileBidByCert = {}; 31591 (s.myBids || []).forEach((bid) => { 31592 if (!bid.cert) return; 31593 const current = profileBidByCert[bid.cert]; 31594 if (!current || Number(bid.amount) >= Number(current.amount)) profileBidByCert[bid.cert] = { ...bid }; 31595 else profileBidByCert[bid.cert] = { ...current, topAmount: Math.max(Number(current.topAmount) || 0, Number(bid.topAmount) || 0) }; 31596 }); 31597 const profileBids = Object.values(profileBidByCert); 31598 const offerData = profileBids.length 31599 ? profileBids.map((b) => { 31600 const lot = all.find((l) => l.cert === b.cert); const m = this.bidStatusMeta(b.status); 31601 const isOpen = lot?.auctionStatus === 'open'; 31602 const standing = isOpen && !failedBidPattern.test(b.status) ?standingByCert[b.cert] : null; 31603 const displayAmount = Number(b.amount) || 0; 31604 const topAmount = Number(standing?.topAmount || lot?.currentBidAmount || b.topAmount || displayAmount) || 0; 31605 const isSold = lot?.auctionStatus === 'sold'; 31606 const winnerUid = String(lot?.winnerBuyerUid || ''); 31607 const isWinner = isSold && (winnerUid ?winnerUid === String(s.user?.uid || '') : m.label === 'Aprobada'); 31608 return { open: () => this.openLot(b.cert), image: lot?.image || fallbackImg, categoria: lot?.categoria || b.lotName || ('Lote ' + b.cert), cert: b.cert, origen: lot?.origen || (b.date ?new Date(b.date).toLocaleDateString('es-AR') : ''), amount: this.fmtARS(displayAmount), amountNum: displayAmount, topAmount, best: this.fmtARS(topAmount), status: m.label, statusBg: m.bg, statusColor: m.color, auctionStatus: lot?.auctionStatus || '', isSold, isWinner, standing: !!standing, standKey: standing?.key || '', standLabel: standing?.label || '', standBg: standing?.bg || '', standColor: standing?.color || '' }; 31609 }) 31610 : offerLots.map(({ lot, amt }) => { 31611 const standing = standingByCert[lot.cert]; 31612 return { open: () =>
31612 this.openLot(lot.cert), image: lot.image, categoria: lot.categoria, cert: lot.cert, origen: lot.origen, amount: this.fmtARS(amt), amountNum: Number(amt) || 0, topAmount: Number(standing?.topAmount || amt) || 0, best: this.fmtARS(standing?.topAmount || amt), status: 'Activa', statusBg: 'rgba(10,92,168,0.14)', statusColor: '#0a5ca8', auctionStatus: lot.auctionStatus || '', isSold: lot.auctionStatus === 'sold', standing: !!standing, standKey: standing?.key || '', standLabel: standing?.label || '', standBg: standing?.bg || '', standColor: standing?.color || '' }; 31613 }); 31614 const liveOffers = offerData.filter((offer) => offer.standing).map((offer) => { 31615 const adjustment = this.bidAdj(offer.cert); 31616 const marketAmount = Math.max(offer.topAmount, offer.amountNum); 31617 const target = marketAmount + adjustment.offset; 31618 const winning = offer.standKey === 'winning'; 31619 const standLabel = winning ?'Vas ganando' : 'Te superaron'; 31620 const standColor = winning ?'#1f7a63' : '#c22'; 31621 const standBg = winning ?'rgba(31,122,99,0.14)' : 'rgba(230,57,70,0.14)'; 31622 const submitting = s.quickBidCert === offer.cert; 31623 return { 31624 cert: offer.cert, image: offer.image, categoria: offer.categoria, origen: offer.origen, 31625 myRoll: anim ?this.rollNumber(offer.amountNum) : this.fmtARS(offer.amountNum), 31626 marketRoll: anim ?this.rollNumber(marketAmount) : this.fmtARS(marketAmount), 31627 winning, outbid: !winning, showOutbidIcon: !winning, 31628 standLabel, standColor, standBg, 31629 open: offer.open, 31630 offsetLabel: '+$' + adjustment.offset.toLocaleString('es-AR'),
31631 targetRoll: anim ?this.rollNumber(target) : this.fmtARS(target), 31632 incOpts: this.INCREMENTS.map((increment) => ({ 31633 label: '+$' + increment, 31634 activeBg: adjustment.increment === increment ?'#002e5f' : '#fff', 31635 activeColor: adjustment.increment === increment ?'#fff' : '#3d4c5f', 31636 activeBorder: adjustment.increment === increment ?'#002e5f' : '#dde7f2', 31637 pick: () => this.setBidAdj(offer.cert, { increment, offset: Math.max(increment, adjustment.offset) }), 31638 })), 31639 plus: () => this.setBidAdj(offer.cert, { ...adjustment, offset: adjustment.offset + adjustment.increment }), 31640 minus: () => this.setBidAdj(offer.cert, { ...adjustment, offset: Math.max(adjustment.increment, adjustment.offset - adjustment.increment) }), 31641 confirm: (event) => { event.stopPropagation(); this.quickBid(offer.cert, marketAmount); }, 31642 submitting, submitOpacity: submitting ?'0.6' : '1', 31643 confirmLabel: submitting ?'Enviandoâ¦' : 'Ofertar ' + this.fmtARS(target), 31644 }; 31645 }); 31646 const completedOffers = offerData 31647 .filter((offer) => offer.isWinner) 31648 .map((offer) => ({ 31649 cert: offer.cert, 31650 image: offer.image, 31651 categoria: offer.categoria, 31652 origen: offer.origen, 31653 amount: offer.amount, 31654 amountNum: offer.amountNum, 31655 open: offer.open, 31656 })); 31657 const buyerClosedAllRows = offerData 31658 .filter((offer) => ['sold', 'closed_no_bids'].includes(offer.auctionStatus)) 31659 .map((offer) => ({ 31660 ...offer, 31661 resultLabel: offer.isWinner ?'Ganaste' : 'No adjudicada', 31662 resultBg: offer.isWinner ?'rgba(31,122,99,0.14)' : 'rgba(101,117,138,0.12)', 31663 resultColor: offer.isWinner ?'#1f7a63' : '#65758a', 31664 amountLabel: offer.isWinner ?'OFERTA ACEPTADA' : 'MI OFERTA FINAL', 31665 })); 31666 const buyerClosedPages = Math.max(1, Math.ceil(buyerClosedAllRows.length / 10)); 31667 const buyerClosedPage = Math.min(Math.max(1, Number(s.buyerClosedPage) || 1), buyerClosedPages); 31668 const buyerClosedOfferRows = buyerClosedAllRows.slice((buyerClosedPage - 1) * 10, buyerClosedPage * 10); 31669 const sellerOpenLots = mine.filter((lot) => lot.auctionStatus === 'open'); 31670 const sellerTrackedLots = mine.filter((lot) => ['open', 'sold'].includes(lot.auctionStatus)); 31671 const sellerOfferRows = sellerTrackedLots.map((lot) => { 31672 const bids = (s.lotBids[lot.cert] || []).slice().sort((a, b) => b.amount - a.amount); 31673 const isFinal = lot.auctionStatus === 'sold'; 31674 const targetBidId = isFinal ?lot.winnerBidId : lot.currentBidId; 31675 const targetBuyerUid = isFinal ?lot.winnerBuyerUid : lot.currentBidBuyerUid; 31676 const leader = bids.find((bid) => targetBidId && bid.id === targetBidId) 31677 || bids.find((bid) => targetBuyerUid && bid.buyerUid === targetBuyerUid) 31678 || bids[0] 31679 || null; 31680 const amount = isFinal 31681 ? Math.max(Number(lot.soldAmount) || 0, Number(leader?.amount) || 0) 31682 : Math.max(Number(lot.currentBidAmount) || 0, Number(leader?.amount) || 0); 31683 const base = Number(lot.baseNum) || 0; 31684 const variation = base ?((amount - base) / base) * 100 : 0; 31685 const direction = variation > 0.005 ?'up' : variation < -0.005 ?'down' : 'flat'; 31686 const count = Math.max(Number(lot.bidCount) || 0, bids.length); 31687 const buyer = String(leader?.bidder || (isFinal ?lot.raw?.winnerBuyerName : lot.raw?.currentBidBuyerName) || '').trim() || 'Sin nombre informado'; 31688 return { 31689 cert: lot.cert, 31690 image: lot.image, 31691 categoria: lot.categoria, 31692 origen: lot.origen, 31693 buyer, 31694 buyerLabel: isFinal ?'COMPRADOR GANADOR' : 'COMPRADOR LÃDER', 31695 base: this.fmtARS(base), 31696 amount: this.fmtARS(amount), 31697 amountLabel: isFinal ?'PRECIO FINAL' : 'OFERTA LÃDER', 31698 amountNum: amount, 31699 isFinal, 31700 variation: direction === 'up' || direction === 'down' ?Math.abs(variation).toLocaleStr
31700ing('es-AR', { maximumFractionDigits: 1 }) + '%' : '0%', 31701 variationUp: direction === 'up', variationDown: direction === 'down', 31702 variationColor: direction === 'up' ?'#1f7a63' : direction === 'down' ?'#c22' : '#65758a', 31703 variationBg: direction === 'up' ?'rgba(31,122,99,0.14)' : direction === 'down' ?'rgba(230,57,70,0.14)' : 'rgba(101,117,138,0.12)', 31704 count: count === 1 ?'1 oferta' : count + ' ofertas', 31705 countNum: count, 31706 open: () => this.openLot(lot.cert), 31707 }; 31708 }).filter((row) => row.amountNum > 0 || row.countNum > 0); 31709 const sellerOpenOfferRows = sellerOfferRows.filter((row) => !row.isFinal); 31710 const sellerClosedAllRows = sellerOfferRows.filter((row) => row.isFinal); 31711 const sellerClosedPages = Math.max(1, Math.ceil(sellerClosedAllRows.length / 10)); 31712 const sellerClosedPage = Math.min(Math.max(1, Number(s.sellerClosedPage) || 1), sellerClosedPages); 31713 const sellerClosedOfferRows = sellerClosedAllRows.slice((sellerClosedPage - 1) * 10, sellerClosedPage * 10); 31714 const pendData = (s.pendingBids || []).map((b) => { 31715 const lot = all.find((l) => l.cert === b.cert); 31716 return { id: b.id, cert: b.cert, image: lot?.image || fallbackImg, categoria: lot?.categoria || b.lotName || ('Lote ' + b.cert), origen: lot?.origen || '', amount: this.fmtARS(b.amount), bidder: 'Identidad reservada', approveLabel: s.bidActioning === b.id ?'Aprobandoâ¦' : 'Aprobar', disabled: !!s.bidActioning, open: () => this.openLot(b.cert), approve: () => this.actOnBid(b.cert, b.id, 'approve'), reject: () => this.actOnBid(b.cert, b.id, 'reject') }; 31717 }); 31718 const estList = s.establishments || []; 31719 const ef = s.estForm || this.emptyEstForm(); 31720 const comitenteForm = s.comitenteForm || this.emptyComitenteForm(); 31721 const comitenteDirectory = this.profileComitenteDirectory(); 31722 const comitenteData = comitenteDirectory.rows.map((item) => ({ 31723 editAlias: () => this.editComitenteAlias(item.id), id: item.id, fiscalName: item.nombreFiscal || 'Nombre fiscal no informado', razonSocial: item.alias || item.nombreFiscal || 'Sin razón social', 31724 cuitCuil: item.cuitCuil || 'Sin CUIT', meta: [item.localidad, item.provincia].filter(Boolean).join(', ') || 'Sin ubicación', 31725 })); 31726 const estData = estList.map((e) => ({ 31727 id: e.id, nombre: e.nombre, provincia: e.provincia || '?', localidad: e.localidad || '?', comitente: e.comitenteAlias || e.comitenteNombreFiscal || e.comitente || '', 31728 comitenteCuitCuil: e.comitenteCuitCuil || '', renspa: e.renspa || '', 31729 meta: [e.localidad, e.provincia].filter(Boolean).join(', ') || 'Sin ubicación', 31730 hasCoords: e.latitud != null && e.longitud != null, 31731 coords: (e.latitud != null && e.longitud != null) ?Number(e.latitud).toFixed(4) + ', ' + Number(e.longitud).toFixed(4) : '', 31732 deleting: s.estDeleting === e.id, disabled: !!s.estDeleting, 31733 remove: () => this.deleteEstablishment(e.id), 31734 })); 31735 const baForm = s.baForm || this.emptyBaForm(); 31736 const buyerProfile = this.canUseBuyerData(s.apiUser) && !this.canManageSellerData(s.apiUser) && !this.isCertifierOnly(s.apiUser); 31737 const buyerDirectoryAll = (s.buyerAccounts || []).map((account) => { 31738 const base = account.displayName || account.email || 'Comprador'; 31739 const initials = base.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join('').toUpperCase() || 'C'; 31740 const lastLogin = account.lastLoginAt ?new Date(account.lastLoginAt) : null; 31741 return { 31742 uid: account.uid, 31743 name: account.displayName || 'Comprador', 31744 email: account.email || 'Sin email', 31745 initials, 31746 lastAccess: lastLogin && !isNaN(lastLogin) ?'Ãltimo acceso: ' + lastLogin.toLocaleDateString('es-AR') : 'Sin accesos registrados', 31747 authorize: () => this.authorizeBuyerAccount(account), 31748 }; 31749 }); 31750 const buyerDirectoryQuery = String(s.buyerDirectorySearch || '').trim().toLowerCase(); 31751 const buyerDirectoryData = buyerDirectoryQuery 31752 ? buyerDirectoryAll.filter((account) => (account.name + ' ' + account.email).toLowerCase().includes(buyerDirectoryQuery)) 31753 : buyerDirectoryAll; 31754 const todayIso = this.localIsoDate(); 31755 const creditEndFor = (auth) => auth.creditEndDate || this.calculateCreditEndDate(auth.creditStartDate); 31756 const creditValidity = (auth) => { 31757 const start = auth.creditStartDate; 31758 const end = creditEndFor(auth);
31759 if (!start && !end) return 'Vigencia no informada'; 31760 return 'Vigencia: ' + (start ?this.formatBaDate(start) : 'â') + ' al ' + (end ?this.formatBaDate(end) : 'â'); 31761 }; 31762 const creditIsCurrent = (auth) => { 31763 if (auth?.isActive === false) return false; 31764 const start = auth.creditStartDate; 31765 const end = creditEndFor(auth); 31766 return (!start || start <= todayIso) && (!end || end >= todayIso); 31767 }; 31768 const sellerIdentity = (auth) => { 31769 const uid = String(auth?.sellerUid || ''); 31770 const email = String(auth?.sellerEmail || '').trim(); 31771 const resolvedSeller = String(s.sellerLabels?.[uid] || '').trim(); 31772 const authorizationId = String(auth?.summaryId || auth?.id || ''); 31773 const creditNotification = rawNotifications.find((notification) => { 31774 const tag = String(notification?.tag || ''); 31775 const eventKey = String(notification?.eventKey || ''); 31776 return authorizationId && (tag === 'roslot-credit-' + authorizationId || eventKey.startsWith(authorizationId + '|')); 31777 }); 31778 const notificationBody = String(creditNotification?.body || ''); 31779 const notifiedSeller = (notificationBody.match(/^(.+?)\s+(?:te autorizó|actualizó tu autorización|desactivó tu autorización)/i) || [])[1] || ''; 31780 const relatedLot = (s.lots || []).find((lot) => 31781 (uid && String(lot.sellerUid || '') === uid) 31782 || (email && String(lot.sellerEmail || '').toLowerCase() === email.toLowerCase()) 31783 ); 31784 const lotSeller = String(relatedLot?.seller || '').trim(); 31785 const lotConsignataria = String(relatedLot?.consignatariaName || '').trim(); 31786 const isGenericSeller = !lotSeller || /^productor rosgan$/i.test(lotSeller) || /^vendedor(?:\s|$)/i.test(lotSeller); 31787 const primary = String(auth?.sellerName || resolvedSeller || notifiedSeller || (!isGenericSeller ?lotSeller : '') || lotConsignataria || email || 'Vendedor autorizado').trim(); 31788 const detail = email && email.toLowerCase() !== primary.toLowerCase() ?email : ''; 31789 return { primary, detail }; 31790 }; 31791 const creditMovementView = (movement) => { 31792 const date = movement.date ?new Date(movement.date) : null; 31793 const status = String(movement.status || '').toLowerCase(); 31794 return { 31795 id: movement.id, 31796 lot: movement.lot || 'Lote', 31797 date: date && !isNaN(date) ?date.toLocaleString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : (movement.date || 'Fecha no informada'), 31798 statusLabel: status === 'won' ?'Ganado' : status === 'winning' ?'Oferta vigente' : (status ?status.replace(/[_-]+/g, ' ').replace(/^./, (char) => char.toUpperCase()) : 'Movimiento'), 31799 statusBg: status === 'won' ?'rgba(10,92,168,.12)' : status === 'winning' ?'rgba(31,122,99,.13)' : 'rgba(101,117,138,.12)', 31800 statusColor: status === 'won' ?'#075ca8' : status === 'winning' ?'#1f7a63' : '#536a80', 31801 bidAmount: this.formatBaCurrency(movement.bidAmount), 31802 consumedAmount: this.formatBaCurrency(movement.consumedAmount), 31803 }; 31804 }; 31805 const inlineCreditView = (authorization) => { 31806 const key = this.creditAuthorizationKey(authorization); 31807 const summary = s.creditSummaryCache?.[key] || null; 31808 const weekly = Number(summary?.weeklyCreditAmount ?? authorization.weeklyCreditAmount) || 0; 31809 const used = Number(summary?.usedCreditAmount) || 0; 31810 const available = Number(summary?.availableCreditAmount ?? weekly) || 0; 31811 const percentRaw = weekly > 0 ?(used / weekly) * 100 : 0; 31812 const movements = (summary?.movements || []).map(creditMovementView).slice(0, 3); 31813 return { 31814 inlineLoading: !!s.creditSummaryLoadingIds?.[key] && !summary, 31815 inlineError: s.creditSummaryErrors?.[key] || '', 31816 inlineHasError: !!s.creditSummaryErrors?.[key] && !summary, 31817 inlineReady: !!summary, 31818 inlineAuthorized: this.rollEl(this.formatBaCurrency(weekly)),
31819 inlineUsed: this.rollEl(this.formatBaCurrency(used)), 31820 inlineAvailable: this.rollEl(this.formatBaCurrency(available)), 31821 inlineProgress: Math.max(0, Math.min(100, percentRaw)).toFixed(2) + '%', 31822 inlineProgressLabel: Math.round(percentRaw).toLocaleString('es-AR') + '%', 31823 inlineMovements: movements, 31824 inlineHasMovements: movements.length > 0, 31825 inlineEmpty: !!summary && movements.length === 0, 31826 refreshInline: () => this.loadCreditSummaryIntoCache(authorization, { force: true }).catch(() => {}), 31827 }; 31828 }; 31829 const baData = (s.buyerAuths || []).filter((a) => a.isActive && creditIsCurrent(a)).map((a) => { 31830 const buyerLabel = a.buyerName || a.buyerEmail || '(sin email)'; 31831 const buyerDetail = a.buyerName && a.buyerEmail && a.buyerEmail.toLowerCase() !== a.buyerName.toLowerCase() ?a.buyerEmail : ''; 31832 const initials = buyerLabel.trim().split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join('').toUpperCase() || 'C'; 31833 return { 31834 id: a.id, buyer: buyerLabel, buyerDetail, hasBuyerDetail: !!buyerDetail, email: a.buyerEmail || '', uid: a.buyerUid || '', 31835 hasUid: !!a.buyerUid, initials, 31836 creditStartDate: a.creditStartDate, 31837 weeklyCreditAmount: this.moneyInputDisplay(a.weeklyCreditAmount), 31838 weeklyCreditLabel: this.formatBaCurrency(a.weeklyCreditAmount), 31839 validityLabel: creditValidity(a), 31840 isActive: a.isActive, busy: s.baActioning === a.id, 31841 statusLabel: a.isActive ?'Activo' : 'Inactivo', 31842 statusBg: a.isActive ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)', 31843 statusColor: a.isActive ?'#1f7a63' : '#c22', 31844 cardOpacity: a.isActive ?'1' : '0.62', 31845 avatarBg: a.isActive ?'linear-gradient(135deg,#0a5ca8,#1f7a63)' : 'linear-gradient(135deg,#8593a6,#647186)', 31846 knobX: a.isActive ?'20px' : '2px', switchBg: a.isActive ?'#1f7a63' : '#cdd6e2', 31847 toggle: () => this.toggleBaActive(a.id), 31848 setStartDate: (event) => this.updateBuyerAuthField(a.id, 'creditStartDate', event.target.value), 31849 setWeeklyCreditAmount: (event) => this.formatMoneyInputEvent(event, (raw) => this.updateBuyerAuthField(a.id, 'weeklyCreditAmount', raw)), 31850 save: () => this.pushBuyerAuth(a.id), 31851 saveLabel: s.baActioning === a.id ?'Guardandoâ¦' : 'Guardar', 31852 openHistory: () => this.openCreditHistory(a, 'seller'), 31853 summaryDisabled: !a.summaryId, 31854 summaryOpacity: a.summaryId ?'1' : '.45', 31855 remove: () => this.deleteBuyerAuth(a.id), 31856 ...inlineCreditView(a), 31857 }; 31858 }); 31859 const activeLots = mine.filter((l) => !['Caducado', 'Vendido'].includes(l.status)).length; 31860 const expiredLots = mine.length - activeLots; 31861 const offersMade = offerData.length; 31862 const wonCount = offerData.filter((o) => o.isWinner || o.standLabel === 'Vas ganando').length; 31863 const winRate = offersMade ?Math.round(wonCount / offersMade * 100) : 0; 31864 const buyerCreditRows = (s.buyerAccess || []) 31865 .filter((auth) => auth.isActive && creditIsCurrent(auth)) 31866 .slice() 31867 .sort((a, b) => { 31868 const sellerCompare = String(a.sellerName || a.sellerEmail || '').localeCompare(String(b.sellerName || b.sellerEmail || ''), 'es'); 31869 return sellerCompare || String(b.creditStartDate || '').localeCompare(String(a.creditStartDate || '')); 31870 }) 31871 .map((a) => { 31872 const identity = sellerIdentity(a); 31873 const sellerPrimary = identity.primary; 31874 const sellerDetail = identity.detail; 31875 return { 31876 id: a.id || a.sellerUid || a.sellerEmail || a.creditStartDate, 31877 seller: sellerPrimary, 31878 sellerDetail, 31879 hasSellerDetail: !!sellerDetail, 31880 validityLabel: creditValidity(a), 31881 periodLabel: (a.creditStartDate ?this.formatBaDate(a.creditStartDate) : 'â') + ' al ' + (creditEndFor(a) ?this.formatBaDate(creditEndFor(a)) : 'â'), 31882 weeklyCreditLabel: this.formatBaCurrency(a.weeklyCreditAmount), 31883 statusLabel: a.isActive ?'Activo' : 'Inactivo', 31884 statusBg: a.isActive ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)', 31885 statusColor: a.isActive ?'#1f7a63' : '#c22', 31886 openHistory: () => this.openCreditHistory(a, 'buyer'), 31887 summaryDisabled: !a.summaryId, 31888 summaryOpacity: a.summaryId ?'1' : '.45', 31889 ...inlineCreditView(a), 31890 }; 31891 }); 31892 const creditSummary = s.creditSummary || null; 31893 const creditSummaryAuth = s.creditSummaryAuthorization || {};
31894 const creditSummaryPerspective = s.creditSummaryPerspective === 'seller' ?'seller' : 'buyer'; 31895 const creditSummaryWeekly = Number(creditSummary?.weeklyCreditAmount) || 0; 31896 const creditSummaryUsed = Number(creditSummary?.usedCreditAmount) || 0; 31897 const creditSummaryAvailable = Number(creditSummary?.availableCreditAmount) || 0; 31898 const creditSummaryPercentRaw = creditSummaryWeekly > 0 ?(creditSummaryUsed / creditSummaryWeekly) * 100 : 0; 31899 const creditSummaryPercent = Math.max(0, Math.min(100, creditSummaryPercentRaw)); 31900 const creditMovementRows = (creditSummary?.movements || []).map(creditMovementView); 31901 const creditSummaryParty = creditSummaryPerspective === 'seller' 31902 ? (creditSummaryAuth.buyerName || creditSummaryAuth.buyerEmail || 'Comprador') 31903 : (creditSummaryAuth.sellerName || creditSummaryAuth.sellerEmail || 'Vendedor'); 31904 const creditSummaryPeriod = (creditSummaryAuth.creditStartDate ?this.formatBaDate(creditSummaryAuth.creditStartDate) : 'â') + ' al ' + (creditEndFor(creditSummaryAuth) ?this.formatBaDate(creditEndFor(creditSummaryAuth)) : 'â'); 31905 const creditHistoryPerspective = s.creditHistoryPerspective === 'seller' ?'seller' : 'buyer'; 31906 const creditHistoryAnchor = s.creditHistoryAuthorization || null; 31907 const creditHistorySource = creditHistoryPerspective === 'seller' ?(s.buyerAuths || []) : (s.buyerAccess || []); 31908 const sameCreditParty = (authorization) => { 31909 if (!creditHistoryAnchor) return true; 31910 if (creditHistoryPerspective === 'seller') { 31911 const anchorUid = String(creditHistoryAnchor.buyerUid || ''); 31912 const anchorEmail = String(creditHistoryAnchor.buyerEmail || '').toLowerCase(); 31913 return (anchorUid && String(authorization.buyerUid || '') === anchorUid) 31914 || (anchorEmail && String(authorization.buyerEmail || '').toLowerCase() === anchorEmail); 31915 } 31916 const anchorUid = String(creditHistoryAnchor.sellerUid || ''); 31917 const anchorEmail = String(creditHistoryAnchor.sellerEmail || '').toLowerCase(); 31918 return (anchorUid && String(authorization.sellerUid || '') === anchorUid) 31919 || (anchorEmail && String(authorization.sellerEmail || '').toLowerCase() === anchorEmail); 31920 }; 31921 const creditHistoryRows = creditHistorySource 31922 .filter((authorization) => sameCreditParty(authorization) && !creditIsCurrent(authorization)) 31923 .slice() 31924 .sort((a, b) => String(b.creditStartDate || '').localeCompare(String(a.creditStartDate || ''))) 31925 .map((authorization) => { 31926 const party = creditHistoryPerspective === 'seller' 31927 ? (authorization.buyerName || authorization.buyerEmail || 'Comprador') 31928 : sellerIdentity(authorization).primary; 31929 const isInactive = authorization.isActive === false; 31930 return { 31931 id: this.creditAuthorizationKey(authorization) || authorization.creditStartDate, 31932 party, 31933 period: (authorization.creditStartDate ?this.formatBaDate(authorization.creditStartDate) : 'â') + ' al ' + (creditEndFor(authorization) ?this.formatBaDate(creditEndFor(authorization)) : 'â'), 31934 amount: this.formatBaCurrency(authorization.weeklyCreditAmount), 31935 statusLabel: isInactive ?'Inactivo' : 'Finalizado', 31936 statusBg: isInactive ?'rgba(101,117,138,.12)' : 'rgba(10,92,168,.11)', 31937 statusColor: isInactive ?'#596b7e' : '#075ca8', 31938 openDetail: () => this.openCreditSummary(authorization, creditHistoryPerspective), 31939 detailDisabled: !this.creditAuthorizationKey(authorization), 31940 detailOpacity: this.creditAuthorizationKey(authorization) ?'1' : '.45', 31941 }; 31942 }); 31943 const creditHistoryParty = creditHistoryAnchor 31944 ? (creditHistoryPerspective === 'seller' 31945 ? (creditHistoryAnchor.buyerName || creditHistoryAnchor.buyerEmail || 'Comprador') 31946 : sellerIdentity(creditHistoryAnchor).primary) 31947 : 'Todos los créditos anteriores'; 31948 const winningOffers = liveOffers.filter((offer) => offer.winning).length; 31949 const outbidOffers = liveOffers.filter((offer) => offer.outbid).length; 31950 const adjudicatedAmount = completedOffers.reduce((sum, offer) => sum + (Number(offer.amountNum) || 0), 0); 31951 const receivedOffers = mine.reduce((sum, lot) => sum + (Number(lot.bidCount) || (lot.bids || []).length || 0), 0); 31952 const soldLots = mine.filter((lot) => lot.auctionStatus === 'sold'); 31953 const closedLots = mine.filter((lot) => ['sold', 'closed_no_bids'].includes(lot.auctionStatus)); 31954 const closedWithoutBids = mine.filter((lot) => lot.auctionStatus === 'closed_no_bids').length; 31955 const sellerClosedSummary = [soldLots.length ?soldLots.length + ' vendidos' : '', closedWithoutBids ?closedWithoutBids + ' sin ofertas' : ''].filter(
31955Boolean).join(' · ') || 'al dÃa'; 31956 const salesConversion = closedLots.length ?Math.round(soldLots.length / closedLots.length * 100) : 0; 31957 const series = (values) => { 31958 const source = values.length >= 2 ?values : values.length === 1 ?[0, values[0]] : [0, 0]; 31959 return this.linePath(source.map((value) => value * P), 120, 34, 3); 31960 }; 31961 const cumulative = (values) => { let total = 0; return values.map((value) => (total += value)); }; 31962 let offersSeen = 0, winsSeen = 0; 31963 const winProgress = offerData.map((offer) => { 31964 offersSeen += 1; 31965 if (offer.isWinner || offer.standLabel === 'Vas ganando') winsSeen += 1; 31966 return winsSeen / offersSeen * 100; 31967 }); 31968 const sellerKpis = [ 31969 { label: 'Lotes activos', value: fmtK(activeLots * P), delta: sellerClosedSummary, color: '#0a5ca8', spark: series(cumulative(mine.map((l) => !['Caducado', 'Vendido'].includes(l.status) ?1 : 0))) }, 31970 { label: 'Cabezas publicadas', value: fmtK(totalCabezas * P), delta: mine.length + ' lotes', color: '#1f7a63', spark: series(cumulative(mine.map((l) => Number(l.cabezas) || 0))) }, 31971 { label: 'Ofertas recibidas', value: fmtK(receivedOffers * P), delta: activeLots + ' lotes activos', color: '#e63946', spark: series(cumulative(mine.map((lot) => Number(lot.bidCount) || 0))) }, 31972 { label: 'Lotes vendidos', value: fmtK(soldLots.length * P), delta: soldLots.length + '/' + closedLots.length + ' cerrados', color: '#8a5a2b', spark: series(cumulative(mine.map((lot) => lot.auctionStatus === 'sold' ?1 : 0))) }, 31973 { label: 'Conversión de venta', value: Math.round(salesConversion * P) + '%', delta: closedLots.length ?soldLots.length + '/' + closedLots.length : 'sin cierres', color: '#5b3fa8', spark: series(cumulative(closedLots.map((lot) => lot.auctionStatus === 'sold' ?1 : 0))) }, 31974 ]; 31975 const buyerKpis = [ 31976 { label: 'Ofertas activas', value: fmtK(liveOffers.length * P), delta: offersMade + ' realizadas', color: '#0a5ca8', spark: series(cumulative(offerData.map(() => 1))) }, 31977 { label: 'Vas ganando', value: fmtK(winningOffers * P), delta: liveOffers.length ?winningOffers + '/' + liveOffers.length + ' activas' : 'sin subastas abiertas', color: '#1f7a63', spark: series(cumulative(liveOffers.map((offer) => offer.winning ?1 : 0))) }, 31978 { label: 'No liderás', value: fmtK(outbidOffers * P), delta: outbidOffers ?'requieren atención' : 'sin alertas', color: '#e63946', spark: series(cumulative(liveOffers.map((offer) => offer.outbid ?1 : 0))) }, 31979 { label: 'Lotes ganados', value: fmtK(completedOffers.length * P), delta: completedOffers.length ?'ventas cerradas' : 'sin adjudicaciones', color: '#5b3fa8', spark: series(cumulative(completedOffers.map(() => 1))) }, 31980 { label: 'Monto adjudicado', value: this.fmtARS(adjudicatedAmount * P), delta: completedOffers.length + ' lotes', color: '#8a5a2b', spark: series(cumulative(completedOffers.map((offer) => Number(offer.amountNum) || 0))) }, 31981 ]; 31982 const kpis = (buyerProfile ?buyerKpis : sellerKpis).map((k) => ({ ...k, deltaBg: 'rgba(101,117,138,0.12)', deltaColor: '#65758a' })); 31983 const myLotBidCounts = mine.map((lot) => Number(lot.bidCount) || (lot.bids || []).length || 0); 31984 const rawActivityPoints = buyerProfile ?offerData.map(() => 1) : myLotBidCounts; 31985 const activityPoints = rawActivityPoints.length >= 2 ?rawActivityPoints : rawActivityPoints.length === 1 ?[0, rawActivityPoints[0]] : [0, 0]; 31986 const mySeries = cumulative(activityPoints).map((v) => v * P); 31987 const marketOffersTotal = all.reduce((sum, lot) => sum + (Number(lot.bidCount) || (lot.bids || []).length || 0), 0); 31988 const marketPace = all.length ?marketOffersTotal / all.length : 0; 31989 const mktSeries = mySeries.map((_, index) => marketPace * (index + 1) * P); 31990 const scaleMax = Math.max(...mySeries, ...mktSeries, 1); 31991 const norm = (arr) => arr.map((v) => (v / scaleMax) * 9); 31992 const distributionItems = buyerProfile 31993 ? [...new Set(offerData.map((offer) =>
31993 offer.categoria).filter(Boolean))].map((categoria) => ({ 31994 label: categoria, 31995 count: offerData.filter((offer) => offer.categoria === categoria).length, 31996 })) 31997 : [...new Set(mine.map((lot) => lot.categoria).filter(Boolean))].map((categoria) => ({ 31998 label: categoria, 31999 count: mine.filter((lot) => lot.categoria === categoria).reduce((sum, lot) => sum + (Number(lot.cabezas) || 0), 0), 32000 })); 32001 const dColors = ['#0a5ca8', '#1f7a63', '#e63946', '#5b3fa8', '#d18a1d', '#27879b']; 32002 const dCounts = distributionItems.map((item) => item.count); 32003 const distributionTotal = dCounts.reduce((a, b) => a + b, 0); 32004 const dTot = distributionTotal || 1; 32005 let acc = 0; 32006 const stops = dCounts.map((c, i) => { const from = acc / dTot * 360; acc += c; const to = acc / dTot * 360; return `${dColors[i % dColors.length]} ${from.toFixed(1)}deg ${to.toFixed(1)}deg`; }); 32007 const isKgLot = (l) => /kg/i.test(l.condicion || '') && Number(l.baseNum) > 0 && Number(l.baseNum) < 100000; 32008 const average = (items) => items.length ?items.reduce((sum, item) => sum + Number(item.baseNum), 0) / items.length : 0; 32009 const priceCats = [['Novillo', /novill/i], ['Ternero', /tern/i], ['Vaca', /vaca/i], ['Vaquillona', /vaquill/i], ['Toro', /toro/i]].map(([label, pattern]) => { 32010 const market = average(all.filter((l) => isKgLot(l) && pattern.test(l.categoria))); 32011 const own = buyerProfile 32012 ?average(offerData.filter((offer) => pattern.test(offer.categoria)).map((offer) => ({ baseNum: offer.amountNum }))) 32013 : average(mine.filter((l) => isKgLot(l) && pattern.test(l.categoria))); 32014 return [label, Math.round(own || market), Math.round(market)]; 32015 }).filter((entry) => entry[2] > 0).slice(0, 5); 32016 const pMax = Math.max(...priceCats.flatMap((c) => [c[1], c[2]]), 1); 32017 const stateMap = (st) => st === 'En vivo' ?{ state: 'EN VIVO', stateBg: '#e63946', stateColor: '#fff' } : st === 'Próximo' ?{ state: 'PROGRAMADO', stateBg: '#0a5ca8', stateColor: '#fff' } : st === 'Caducado' ?{ state: 'CADUCADO', stateBg: 'rgba(101,117,138,0.92)', stateColor: '#fff' } : st === 'Listo para la carga' ?{ state: 'LISTO PARA LA CARGA', stateBg: '#146b68', stateColor: '#fff' } : st === 'Vendido' ?{ state: 'VENDIDO', stateBg: '#1f7a63', stateColor: '#fff' } : { state: 'PUBLICADO', stateBg: 'rgba(0,29,61,0.82)', stateColor: '#fff' }; 32018 const mineSorted = [...mine].sort((a, b) => (['Caducado', 'Vendido'].includes(a.status) ?-1 : 0) - (['Caducado', 'Vendido'].includes(b.status) ?-1 : 0)); 32019 const republishForm = s.republishForm || {}; 32020 const canManageBuyers = this.canManageBuyerAccounts(s.apiUser); 32021 const canManageAdmin = this.canManageUsers(s.apiUser); 32022 const canSell = this.canManageSellerData(s.apiUser); 32023 const canBuy = this.canUseBuyerData(s.apiUser); 32024 const launchAdminRows = (Array.isArray(s.launchInterestsAdmin) ?s.launchInterestsAdmin : []).map((interest) => { 32025 const dateValue = interest.updatedAt || interest.createdAt || ''; 32026 const date = dateValue ?new Date(dateValue) : null; 32027 return { 32028 name: interest.displayName || interest.email || 'Usuario Rosgan', 32029 email: interest.email || 'Sin email', 32030 phone: interest.phoneNumber || 'Sin teléfono', 32031 emailOptIn: interest.emailOptIn !== false, 32032 whatsappOptIn: interest.whatsappOptIn === true, 32033 date: date && !isNaN(date) ?'Anotado el ' + date.toLocaleString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'Fecha no disponible', 32034 }; 32035 }); 32036 const lotYieldRows = (Array.isArray(s.lotYieldCategories) ?s.lotYieldCategories : []).map((category) => { 32037 const saving = String(s.lotYieldSavingId || '') === String(category.id); 32038 return { 32039 id: category.id, 32040 name: category.name || category.id, 32041 code: category.code || 'SIN CÃDIGO', 32042 percent: category.uiPercent ?? '', 32043 saving, 32044 saveLabel: saving ?'Guardandoâ¦' : 'Guardar', 32045 saveOpacity: saving ?'.55' : '1', 32046 setPercent: (event) => this.setLotYieldPercent(category.id, event.target.value), 32047 save: () => this.saveLotYieldCategory(category.id), 32048 }; 32049 }); 32050 const profileErrors = [s.authSyncError, s.apiSessionError, s.myLotsError, s.myBidsError, s.pendingBidsError, s.establishmentsError, s.buyerAccountsError, s.buyerAuthsError, s.buyerAccessError].filter(
32050Boolean); 32051 const fetchInfo = s.profileFetchInfo || {}; 32052 const allProfileEmpty = !mine.length && !offerData.length && !pendData.length && !estData.length && !buyerDirectoryData.length && !baData.length && !buyerCreditRows.length && !s.myLotsLoading && !s.myBidsLoading && !s.pendingBidsLoading && !s.establishmentsLoading && !s.buyerAccountsLoading && !s.buyerAuthsLoading && !s.buyerAccessLoading; 32053 const fetchInfoText = ['lotes', 'establecimientos', 'ofertas', 'pendientes', 'cuentas comprador', 'autorizaciones', 'mi crédito'] 32054 .map((key) => { 32055 const info = fetchInfo[key] || {}; 32056 const value = info.error ?info.error : (info.count == null ?'sin respuesta' : String(info.count)); 32057 return key + ': ' + value; 32058 }) 32059 .join(' · '); 32060 const profileEmptyDebug = allProfileEmpty 32061 ?'No hay datos para esta sesión. Email: ' + (s.user?.email || 'sin email') + ' · UID: ' + (s.user?.uid || 'sin uid') + ' · API: ' + fetchInfoText 32062 : ''; 32063 const profileConsignatariaRef = s.userConsignataria?.id || s.userConsignataria?.slug || s.userConsignataria?.ref || s.userConsignataria?.name || ''; 32064 const profileConsignatariaLogo = this.consLogo(profileConsignatariaRef); 32065 const profileConsignatariaBadges = (s.userConsignatarias || []).map((item) => { 32066 const ref = item.id || item.slug || item.ref || item.name || ''; 32067 const match = (s.consignatarias || []).find((consignataria) => [consignataria.id, consignataria.slug, consignataria.name, consignataria.nombre].some((value) => this.consKey(value) === this.consKey(ref))); 32068 const originalIcon = this.consLogo(ref); 32069 const whiteIcon = this.consWhiteLogo(ref); 32070 const icon = whiteIcon || originalIcon; 32071 const brandColor = this.consBrandColor(ref); 32072 return icon ? { 32073 label: match?.name || match?.nombre || item.name || ref, 32074 icon, 32075 iconTitle: match?.name || match?.nombre || item.name || ref, 32076 iconPadding: '0', 32077 iconRadius: '0', 32078 iconBg: 'transparent', 32079 iconFilter: whiteIcon ?'none' : 'brightness(0) invert(1)', 32080 badgeBg: brandColor, 32081 badgeBorder: 'rgba(255,255,255,0.42)', 32082 badgeColor: '#fff', 32083 } : null; 32084 }).filter(Boolean); 32085 const lucideRole = (label, icon) => ({ label, icon, iconTitle: '', iconPadding: '0', iconRadius: '0', iconBg: 'transparent', iconFilter: 'brightness(0) invert(1)', badgeBg: 'rgba(255,255,255,0.14)', badgeBorder: 'rgba(255,255,255,0.22)', badgeColor: '#fff' }); 32086 const roleMeta = { 32087 user: lucideRole('Usuario', 'assets/icons/lucide/user-round-check.svg'), 32088 buyer: lucideRole('Comprador', 'assets/icons/lucide/shopping-cart.svg'), 32089 comprador: lucideRole('Comprador', 'assets/icons/lucide/shopping-cart.svg'), 32090 seller: lucideRole('Vendedor', 'assets/icons/lucide/store.svg'), 32091 vendedor: lucideRole('Vendedor', 'assets/icons/lucide/store.svg'), 32092 consignatario: lucideRole('Consignatario', 'assets/icons/lucide/landmark.svg'), 32093 certificador: lucideRole('Certificador', 'assets/icons/lucide/badge-check.svg'), 32094 admin: lucideRole('Administrador', 'assets/icons/lucide/shield-check.svg'), 32095 sysadmin: lucideRole('Administrador del sistema', 'assets/icons/lucide/settings.svg'), 32096 dev: lucideRole('Dev', 'assets/icons/lucide/code.svg'), 32097 }; 32098 const rawProfileRoles = this.apiRoleIds(s.apiUser); 32099 const inferredProfileRoles = rawProfileRoles.length ?rawProfileRoles : [...(canSell ?['seller'] : []), ...(canBuy ?['buyer'] : [])]; 32100 const profileRoleBadges = inferredProfileRoles 32101 .map((role) => roleMeta[role] || lucideRole(String(role).replace(/(^|\s)\S/g, (letter) => letter.toUpperCase()), 'assets/icons/lucide/user-round-check.svg')) 32102 .filter((role, index, roles) => roles.findIndex((candidate) => candidate.label === role.label) === index) 32103 .concat(profileConsignatariaBadges); 32104 const profileEditorForm = s.profileEditorForm || {}; 32105 const profileEditorAvatar = s.profilePhotoPreview || currentProfilePhoto || profileConsignatariaLogo || ''; 32106 const adminRoleDefs = [ 32107 ['user', 'Usuario', 'user-round-check.svg'], 32108 ['buyer', 'Comprador', 'shopping-cart.svg'], 32109 ['seller', 'Vendedor', 'store.svg'], 32110 ['consignatario', 'Consignatario', 'landmark.svg'], 32111 ['certificador', 'Certificador', 'badge-check.svg'], 32112 ['admin', 'Administrador', 'shield-check.svg'], 32113 ['sysadmin', 'Administrador del sistema', 'settings.svg'], 32114 ['dev', 'Dev', 'code.svg'], 32115 ]; 32116 const adminRoleLabel = (role) => adminRoleDefs.find((item) => item[0] === role)?.[1] || role; 32117 const directory = this.adminUserDirectory(); 32118 const directoryCompany = (s.adminConsignatarias || []).find(c =>
32118 String(c.id) === String(s.adminUsersConsignataria)); 32119 const directoryCompanyLogo = directoryCompany ? (this.consLogo(directoryCompany.name || directoryCompany.nombre || directoryCompany.id) || directoryCompany.logoUrl || directoryCompany.logo || '') : ''; 32120 const adminUsersList = directory.rows.map((account) => { 32121 const active = s.adminSelectedUser?.uid === account.uid; 32122 return { 32123 ...account, 32124 versionDevices: s.adminVersionsAvailable === false ? [{label:'No se pudo consultar la versión',detail:'Volvé a actualizar el listado',color:'#64748b',bg:'#f1f5f9'}] : window.RosganAppUpdate.describeDevices(account.appDevices, s.adminCurrentVersion), 32125 initials: (account.displayName || account.email || 'U').split(/[\s@.]+/).slice(0, 2).map((word) => word[0]?.toUpperCase() || '').join(''), 32126 hasPhoto: !!account.photoUrl, 32127 showsInitials: !account.photoUrl, 32128 roleSummary: (account.roles || []).map(adminRoleLabel).join(' · ') || 'Sin rol', 32129 companyBadges: account.roles.some(role => ['consignatario','certificador'].includes(role)) ? [...new Set(account.consignatariaIds || [])].map(id => { 32130 const company = (s.adminConsignatarias || []).find(c => String(c.id) === String(id)); 32131 const name = company?.name || company?.nombre || 'Consignataria no disponible'; 32132 const ref = company?.name || company?.nombre || id; 32133 const logo = this.consLogo(ref) || company?.logoUrl || company?.logo || ''; 32134 return {name,logo,hasLogo:!!logo,color:this.consBrandColor(ref)}; 32135 }) : [], 32136 roleBadges: (account.roles?.length ? account.roles : ['none']).map(role => ({label:role === 'none' ? 'Sin rol' : adminRoleLabel(role),icon:'assets/icons/lucide/' + (adminRoleDefs.find(def => def[0] === role)?.[2] || 'user-round-check.svg'),bg:['consignatario','certificador'].includes(role)?'#e7f5f1':'#edf3fa',color:['consignatario','certificador'].includes(role)?'#087561':'#174e7e'})), 32137 border: active ?'#0a5ca8' : '#dce6ef', 32138 bg: active ?'#edf6fd' : '#fff', 32139 select: () => this.selectAdminUser(account), 32140 }; 32141 }); 32142 const adminSelectedRoles = s.adminSelectedRoles || []; 32143 const adminRoleOptions = adminRoleDefs.map(([id, label, icon]) => { 32144 const active = adminSelectedRoles.includes(id); 32145 return { 32146 id, label, 32147 icon: 'assets/icons/lucide/' + icon, 32148 bg: active ?'#003b72' : '#f5f8fb', 32149 border: active ?'#003b72' : '#d6e1eb', 32150 color: active ?'#fff' : '#40536a', 32151 iconFilter: active ?'brightness(0) invert(1)' : 'none', 32152 toggle: () => this.toggleAdminRole(id), 32153 }; 32154 }); 32155 const selectedConsignatariaIds = new Set(s.adminSelectedConsignatariaIds || []); 32156 const adminConsignatariaOptions = (s.adminConsignatarias || []).map((cons) => { 32157 const id = String(cons.id); 32158 const active = selectedConsignatariaIds.has(id); 32159 return { 32160 id, 32161 selected: active ?'true' : 'false', 32162 name: cons.name || cons.nombre || id, 32163 logo: this.consLogo(id) || cons.logoUrl || cons.logo, 32164 border: active ?'#0a5ca8' : '#d9e4ee', 32165 bg: active ?'#eaf5fd' : '#fff', 32166 select: () => this.selectAdminConsignataria(id), 32167 }; 32168 }); 32169 const adminUserMessage = s.adminUserMessage || null; 32170 const mailingStatus = s.mailingStatus || {}; 32171 const mailingSelected = (s.mailingCampaigns || []).find((campaign) => campaign.id === s.mailingSelectedId) || null; 32172 const mailingDraft = s.mailingDraft || {}; 32173 const mailingStatusLabels = { draft: 'Guardada', queued: 'En cola', sending: 'Enviando', completed: 'Completada', failed: 'Con error', cancelled: 'Cancelada' }; 32174 const mailingCampaignList = (s.mailingCampaigns || []).map((campaign) => { 32175 const active = campaign.id === s.mailingSelectedId; 32176 const total = Number(campaign.total) || 0; 32177 const processed = (Number(campaign.sent) || 0) + (Number(campaign.failed) || 0); 32178 const sentTotal = (Number(campaign.sent) || 0) + (Number(campaign.testCount) || 0); 32179 const lastSendAt = campaign.completedAt || campaign.lastTestAt || campaign.startedAt || campaign.queuedAt || ''; 32180 const deleteDisabled = !!s.mailingBusy || ['queued', 'sending'].includes(campaign.status); 32181 return { 32182 ...campaign, 32183 name: campaign.name || campaign.subject || 'Sin nombre', 32184 statusLabel: mailingStatusLabels[campaign.status] || campaign.status || 'Guardada', 32185 audienceLabel: campaign.audience === 'launch_interest' ?'Lanzamiento' : 'Base general', 32186 progress: total ?processed + '/' + total : '',
32187 sendMeta: sentTotal ?sentTotal.toLocaleString('es-AR') + ' enviado' + (sentTotal === 1 ?'' : 's') + ' · ' + this.mailingReportDate(lastSendAt) : 'Sin envÃos', 32188 border: active ?'#0a5ca8' : '#dce6ef', bg: active ?'#edf6fd' : '#fff', 32189 select: () => this.selectMailingCampaign(campaign), 32190 remove: () => this.deleteMailingCampaign(campaign), 32191 deleteDisabled, 32192 deleteOpacity: deleteDisabled ?'.35' : '1', 32193 }; 32194 }); 32195 const mailingMessage = s.mailingMessage || null; 32196 const mailingLocked = !!mailingSelected && mailingSelected.status !== 'draft'; 32197 const mailingInsertType = s.mailingInsertDialog || ''; 32198 const mailingInsertIsImage = mailingInsertType === 'image'; 32199 const mailingInsertUrlValid = !!this.mailingNormalizeUrl(s.mailingInsertUrl || ''); 32200 const mailingImageSize = ['small', 'medium', 'large', 'full'].includes(s.mailingInsertImageSize) ?s.mailingInsertImageSize : 'medium'; 32201 const mailingImageSizeWidths = { small: '320px', medium: '480px', large: '640px', full: '100%' }; 32202 const mailingImageLinkRaw = String(s.mailingInsertImageLink || '').trim(); 32203 const mailingImageLinkValid = !mailingImageLinkRaw || /^https?:/i.test(this.mailingNormalizeUrl(mailingImageLinkRaw)); 32204 const mailingReport = s.mailingReport || null; 32205 const mailingReportSummary = mailingReport?.summary || {}; 32206 const mailingReportNumber = (value) => (Number(value) || 0).toLocaleString('es-AR'); 32207 const mailingReportRate = (value) => (Number(value) || 0).toLocaleString('es-AR', { minimumFractionDigits: 0, maximumFractionDigits: 2 }) + '%'; 32208 const mailingReportStatusMeta = { 32209 accepted: ['Aceptado', '#eaf3fb', '#276087'], delivered: ['Entregado', '#e7f6f0', '#17664f'], 32210 opened: ['Abierto', '#e8f2ff', '#075ca8'], clicked: ['Clic', '#eeeafd', '#5b3fa8'], 32211 temporary_fail: ['Rebote temporal', '#fff6df', '#946200'], permanent_fail: ['Rebote definitivo', '#fff0f1', '#b42335'], 32212 unsubscribed: ['Desuscripto', '#f1f3f5', '#586575'], complained: ['Queja', '#fff0f1', '#9e2637'], 32213 }; 32214 const mailingReportRowsAll = (mailingReport?.deliveries || []).map((delivery) => { 32215 const meta = mailingReportStatusMeta[delivery.status] || [delivery.status || 'Sin estado', '#f1f3f5', '#586575']; 32216 const isTest = delivery.kind === 'test' || delivery.test === true; 32217 return { 32218 ...delivery, 32219 name: delivery.name || '', 32220 kindLabel: isTest ?'Prueba' : 'Campaña', kindBg: isTest ?'#fff4df' : '#eaf3fb', kindColor: isTest ?'#8b5d00' : '#276087', 32221 statusLabel: meta[0], statusBg: meta[1], statusColor: meta[2], 32222 sentAt: this.mailingReportDate(delivery.sentAt), deliveredAt: this.mailingReportDate(delivery.deliveredAt), 32223 openedAt: this.mailingReportDate(delivery.lastOpenedAt || delivery.openedAt), clickedAt: this.mailingReportDate(delivery.lastClickedAt || delivery.clickedAt), 32224 openCount: Number(delivery.openCount) || 0, clickCount: Number(delivery.clickCount) || 0, 32225 error: delivery.error || 'â', 32226 }; 32227 }).sort((a, b) => String(a.email || '').localeCompare(String(b.email || ''), 'es', { sensitivity: 'base' })); 32228 const mailingReportSearch = String(s.mailingReportSearch || '').trim().toLocaleLowerCase('es'); 32229 const mailingReportStatusFilter = String(s.mailingReportStatusFilter || 'all'); 32230 const mailingReportLetter = String(s.mailingReportLetter || 'all').toUpperCase(); 32231 const mailingReportEmailLetter = (email) => { 32232 const first = String(email || '').trim().charAt(0).toUpperCase(); 32233 return /^[A-Z]$/.test(first) ?first : '#'; 32234 }; 32235 const mailingReportBaseRows = mailingReportRowsAll.filter((row) => { 32236 const matchesSearch = !mailingReportSearch 32237 || String(row.email || '').toLocaleLowerCase('es').includes(mailingReportSearch) 32238 || String(row.name || '').toLocaleLowerCase('es').includes(mailingReportSearch); 32239 const matchesStatus = mailingReportStatusFilter === 'all' || row.status === mailingReportStatusFilter; 32240 return matchesSearch && matchesStatus; 32241 }); 32242 const mailingReportFilteredRows = mailingReportBaseRows.filter((row) => mailingReportLetter === 'ALL' || mailingReportEmailLetter(row.email) === mailingReportLetter); 32243 const mailingReportPageSize = 50; 32244 const mailingReportPageCount = Math.max(1, Math.ceil(mailingReportFilteredRows.length / mailingReportPageSize)); 32245 const mailingReportPage = Math.min(mailingReportPageCount, Math.max(1, Number(s.mailingReportPage) || 1)); 32246 const mailingReportPageStart = (mailingReportPage - 1) * mailingReportPageSize; 32247 const mailingReportRows = mailingReportFilteredRows.slice(mailingReportPageStart, mailingReportPageStart + mailingReportPageSize); 32248 const mailingReportLetters = ['ALL', '#', ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'].map((letter) => { 32249 const count = letter === 'ALL' ?mailingReportBaseRows.length : mailingReportBaseRows.filter((row) => mailingReportEmailLetter(row.email) === letter).length; 32250 const active = mailingReportLetter === letter; 32251 return { 32252 letter, 32253 label: letter === 'ALL' ?'Todos' : letter, 32254 count, 32255 disabled: count === 0, 32256 bg: active ?'#075ca8' : '#fff', 32257 color: active ?'#fff' : count ?'#355069' : '#a6b2bd', 32258 border: active ?'#075ca8' : '#d6e2ec', 32259 opacity: count ?'1' : '.45', 32260 select: () => { if (count) this.setState({ mailingReportLetter: letter.toLowerCase(), mailingReportPage: 1 }); }, 32261 }; 32262 }); 32263 const mailingReportKpis = [ 32264 ['ENVIADOS', mailingReportSummary.sent, mailingReportNumber(mailingReportSummary.campaignAccepted) + ' campaña · ' + mailingReportNumber(mailingReportSummary.tests) + ' prueba' + ((Number(mailingReportSummary.tests) || 0) === 1 ?'' : 's'), '#eef5fb', '#cbddea', '#48677f'], 32265 ['ENTREGADOS', mailingReportSummary.delivered, mailingReportRate(mailingReportSummary.deliveryRate) + ' de entrega', '#edf9f5', '#b9e3d4', '#17664f'], 32266 ['APERTURAS ÃNICAS', mailingReportSummary.opened, mailingReportRate(mailingReportSummary.openRate) + ' de apertura', '#edf6ff', '#c6dcf2', '#075ca8'], 32267 ['CLICS ÃNICOS', mailingReportSummary.clicked, mailingReportRate(mailingReportSummary.clickRate) + ' de clics', '#f2effd', '#d9cef5', '#5b3fa8'], 32268 ['REBOTES', mailingReportSummary.bounced, mailingReportNumber(mailingReportSummary.permanentFailed) + ' definitivos · ' + mailingReportNumber(mailingReportSummary.temporaryFailed) + ' temporales', '#fff8e8', '#eed9a4', '#8b6300'], 32269 ['BAJAS / QUEJAS', (Number(mailingReportSummary.unsubscribed) || 0) + (Number(mailingReportSummary.complained) || 0), mailingReportNumber(mailingReportSummary.unsubscribed) + ' bajas · ' + mailingReportNumber(mailingReportSummary.complained) + ' quejas', '#fff1f2', '#efc9ce', '#9e2637'], 32270 ].map(([label, value, detail, bg, border, color]) => ({ label, value: mailingReportNumber(value), detail, bg, border, color })); 32271 const mailingReportRates = [ 32272 ['entrega', mailingReportSummary.deliveryRate], ['apertura', mailingReportSummary.openRate], 32273 ['clics', mailingReportSummary.clickRate], ['rebotes', mailingReportSummary.bounceRate], ['bajas', mailingReportSummary.unsubscribeRate], 32274 ].map(([label, value]) => ({ label, value: mailingReportRate(value) })); 32275 const mailingProductionReady = mailingStatus.provider === 'mailgun' 32276 && mailingStatus.domainState === 'active' 32277 && mailingStatus.sendingEnabled !== false; 32278 return { 32279 profileRoslotVisible: !this.isRosganProfile(), 32280 profileRosganVisible: this.isRosganProfile(), 32281 profileFeriasVisible: this.canViewProfileFerias(), 32282 profileFeriasTitle: this.canManageUsers(s.apiUser) ? 'Remates feria de todas las consignatarias' : 'Remates feria de tu consignataria', 32283 profileFeriaRows: this.profileFeriaRows(), 32284 profileFeriasLoading: !!s.profileFeriasLoading, feriaDeleting: !!s.feriaDeleting, 32285 profileFeriasError: s.profileFeriasError || '', 32286 profileFeriasEmpty: !s.profileFeriasLoading && !s.profileFeriasError && !this.profileFeriaRows().length, 32287 refreshProfileFerias: () => this.fetchProfileFerias(), 32288 profileShowRosganLots: this.isRosganProfile() && s.profileTab === 'resumen', 32289 profileKicker: this.isBasicUserProfile(s.apiUser) ? 'MI PERFIL' : this.isRosganProfile() ? 'ROSGAN · MI PANEL' : 'PANEL DE MERCADO · MI ACTIVIDAD', 32290 profileCompanyName: profileConsignatariaBadges.map((item) => item.label).join(' · ') || (s.userConsignataria ? s.userConsignataria.name : 'Sin consignataria asignada'), 32291 profileCompanyLogo: profileConsignatariaLogo, 32292 profileCompanyColor: this.consBrandColor(profileConsignatariaRef) || '#002e5f', 32293 profileRosganRows: this.profileRosganRows(), 32294 profileRosganLoading: !!s.profileRosganLoading, 32295 profileRosganError: s.profileRosganError || '', 32296 profileRosganEmpty: !s.profileRosganLoading && !s.profileRosganError && !(s.profileRosganLots || []).length, 32297 profileRosganEmptyText: (s.userConsignatarias || []).length ? 'No hay lotes de tu consignataria en próximos remates.' : 'Tu cuenta todavÃa no tiene una consignataria asignada.', 32298 refreshProfileRosgan: () => this.fetchProfileRosganLots(), 32299 profileName: currentProfileName || s.user?.displayName || (s.user?.email || '').split('@')[0] || 'Usuario Rosgan', 32300 profileEmptyDebug: this.isRosganProfile() ? '' : profileEmptyDebug, 32301 profileEmail: s.user?.email || '', 32302 profileRoleBadges, 32303 profileCanSell: canSell || this.isRosganProfile(), 32304 goLoad: () => this.isRosganProfile() ? this.go('rosganLotLoad') : this.beginNewLot(), 32305 profileCanManageUsers: this.canManageUsers(s.apiUser), 32306 mailingOpen: !s.publicAdvanceMode && s.route === 'mailingAdmin' && this.canManageUsers(s.apiUser), 32307 mailingLoading: !!s.mailingLoading, 32308 mailingBusy: !!s.mailingBusy, 32309 mailingBusyOpacity: s.mailingBusy ?'.55' : '1', 32310 mailingHasError: !!s.mailingError, 32311 mailingError: s.mailingError || '', 32312 mailingHasMessage: !!mailingMessage, 32313 mailingMessageText: mailingMessage?.text || '', 32314 mailingMessageBg: mailingMessage?.ok ?'#edf9f5' : '#fff0f1', 32315 mailingMessageBorder: mailingMessage?.ok ?'#b9e3d4' : '#f2c7cc', 32316 mailingMessageColor: mailingMessage?.ok ?'#17664f' : '#b42335', 32317 mailingStatusLabel: mailingProductionReady ?'Mailgun operativo' : (s.mailingStatus ?'Mailgun pendiente' : 'Mailgun sin consultar'), 32318 mailingStatusBg: mailingProductionReady ?'rgba(29,173,121,.35)' : 'rgba(238,166,35,.35)', 32319 mailingGridCols: s.isMobile ?'1fr' : 'minmax(250px,.72fr) minmax(0,1.28fr)', 32320 mailingContactCount: s.mailingContactsState === 'ready' ? (Number(s.mailingContactTotal) || 0).toLocaleStr
32320ing('es-AR') : 'â', 32321 mailingConsentConfirmed: !!s.mailingConsentConfirmed, 32322 mailingConsentBg: s.mailingConsentConfirmed ?'#edf9f5' : '#fff', 32323 mailingConsentBorder: s.mailingConsentConfirmed ?'#50a98a' : '#cbd8e5', 32324 mailingConsentCheckBg: s.mailingConsentConfirmed ?'#198c68' : '#fff', 32325 mailingConsentChecked: !!s.mailingConsentConfirmed, 32326 mailingImportDisabled: !!s.mailingBusy || !s.mailingConsentConfirmed, 32327 mailingImportOpacity: s.mailingBusy || !s.mailingConsentConfirmed ?'.45' : '1', 32328 mailingCampaignsEmpty: s.mailingCampaignsState === 'ready' && !mailingCampaignList.length, 32329 mailingCampaignsPending: ['idle', 'loading'].includes(s.mailingCampaignsState || 'idle'), 32330 mailingContactsLabel: s.mailingContactsState === 'ready' ? 'contactos cargados' : s.mailingContactsState === 'error' ? 'No se pudo consultar el total' : 'Consultando contactosâ¦', 32331 retryMailing: () => this.fetchMailing(true), 32332 mailingCampaignList, 32333 mailingLaunchInterestCount: (Number(s.mailingLaunchInterestTotal) || 0).toLocaleString('es-AR'),
32334 mailingLaunchAudienceCount: s.mailingLaunchState !== 'ready' ? (s.mailingLaunchState === 'error' ? 'No se pudo consultar' : 'Consultando interesadosâ¦') : (Number(s.mailingLaunchInterestTotal) || 0).toLocaleString('es-AR') + ' interesado' + ((Number(s.mailingLaunchInterestTotal) || 0) === 1 ?'' : 's') + ' actualmente', 32335 mailingComposerTitle: mailingSelected ?(mailingSelected.name || mailingSelected.subject || 'Campaña') : 'Nueva campaña', 32336 mailingDraftStatus: mailingSelected ?(mailingStatusLabels[mailingSelected.status] || mailingSelected.status) : 'Sin guardar', 32337 mailingDraftName: mailingDraft.name || '', 32338 mailingDraftSubject: mailingDraft.subject || '', 32339 mailingDraftPreview: mailingDraft.previewText || '', 32340 mailingDraftHtml: mailingDraft.html || '', 32341 mailingDraftText: mailingDraft.text || '', 32342 mailingAudienceLabel: mailingDraft.campaignType === 'launch' 32343 ?'Interesados en el lanzamiento (' + (Number(s.mailingLaunchInterestTotal) || 0).toLocaleString('es-AR') + ')' 32344 : 'Todos los contactos aptos', 32345 mailingAudienceBg: mailingDraft.campaignType === 'launch' ?'#eef9f7' : '#f6f9fb', 32346 mailingAudienceBorder: mailingDraft.campaignType === 'launch' ?'#a9ddd3' : '#d4e0e9', 32347 mailingAudienceColor: mailingDraft.campaignType === 'launch' ?'#126b58' : '#405a72', 32348 mailingAudienceHelp: mailingDraft.campaignType === 'launch' 32349 ?'Esta audiencia es dinámica: al enviar tomará únicamente quienes se hayan anotado y sigan suscriptos.' 32350 : 'Esta campaña utiliza la base general suscripta. La audiencia de lanzamiento es exclusiva de su campaña.', 32351 mailingDraftLocked: mailingLocked, 32352 mailingTestEmail: s.mailingTestEmail || s.user?.email || '', 32353 mailingTestDisabled: !!s.mailingBusy || !mailingSelected?.id, 32354 mailingTestOpacity: s.mailingBusy || !mailingSelected?.id ?'.5' : '1', 32355 mailingSaveDisabled: !!s.mailingBusy || mailingLocked, 32356 mailingSaveOpacity: s.mailingBusy || mailingLocked ?'.5' : '1', 32357 mailingQueueDisabled: !!s.mailingBusy || !mailingSelected?.id || !['draft', 'failed', 'cancelled'].includes(mailingSelected?.status) || !mailingProductionReady, 32358 mailingQueueOpacity: s.mailingBusy || !mailingSelected?.id || !['draft', 'failed', 'cancelled'].includes(mailingSelected?.status) || !mailingProductionReady ?'.5' : '1', 32359 mailingQueueLabel: ['failed', 'cancelled'].includes(mailingSelected?.status) 32360 ?'Reanudar pendientes' 32361 : mailingSelected?.audience === 'launch_interest' ?'Enviar a interesados' : 'Enviar a todos', 32362 mailingEditorShellClass: s.mailingEditorExpanded ?'mailing-editor-fullscreen' : '', 32363 mailingEditorExpanded: !!s.mailingEditorExpanded, 32364 mailingEditorSourceMode: !!s.mailingEditorSourceMode, 32365 mailingEditorVisualMode: !s.mailingEditorSourceMode, 32366 mailingEditorEditable: mailingLocked ?'false' : 'true', 32367 mailingEditorModeLabel: s.mailingEditorSourceMode ?'Editor visual' : 'Ver HTML', 32368 mailingEditorExpandLabel: s.mailingEditorExpanded ?'Reducir' : 'Ampliar', 32369 mailingInsertDialogOpen: !!mailingInsertType, 32370 mailingInsertDialogTitle: mailingInsertIsImage ?'Insertar imagen' : mailingInsertType === 'button' ?'Insertar botón' : 'Insertar enlace', 32371 mailingInsertIsImage, 32372 mailingInsertUrl: s.mailingInsertUrl || '', 32373 mailingInsertText: s.mailingInsertText || '', 32374 mailingInsertUrlLabel: mailingInsertIsImage ?'Imagen subida o URL pública' : 'Destino', 32375 mailingInsertUrlPlaceholder: mailingInsertIsImage ?'https://...' : 'https://sitio.com', 32376 mailingInsertTextLabel: mailingInsertIsImage ?'Texto alternativo' : 'Texto visible', 32377 mailingInsertTextPlaceholder: mailingInsertIsImage ?'Descripción de la imagen' : mailingInsertType === 'button' ?'Ver más' : 'Texto del enlace', 32378 mailingInsertImageSize: mailingImageSize, 32379 mailingInsertImageLink: s.mailingInsertImageLink || '', 32380 mailingImagePreviewMaxWidth: mailingImageSizeWidths[mailingImageSize], 32381 mailingImageLinkBorder: mailingImageLinkValid ?'#cfdbe8' : '#d74a5c',
32382 mailingImageLinkHintColor: mailingImageLinkValid ?'#7a8b9d' : '#b42335', 32383 mailingImageLinkHint: !mailingImageLinkRaw ?'Si lo dejás vacÃo, la imagen no tendrá enlace.' : mailingImageLinkValid ?'La imagen completa abrirá este destino.' : 'Ingresá una dirección web válida.', 32384 mailingImageUploading: !!s.mailingImageUploading, 32385 mailingImageUploadLabel: s.mailingImageUploading ?'Subiendo y optimizando imagen...' : 'Elegir imagen · JPG, PNG, WEBP o GIF · máximo 8 MB', 32386 mailingImageHasPreview: mailingInsertIsImage && /^https?:\/\//i.test(s.mailingInsertUrl || ''), 32387 mailingImageHasError: !!s.mailingImageError, 32388 mailingImageError: s.mailingImageError || '', 32389 mailingInsertDisabled: !!s.mailingImageUploading || !mailingInsertUrlValid || (mailingInsertIsImage && !mailingImageLinkValid), 32390 mailingInsertOpacity: s.mailingImageUploading || !mailingInsertUrlValid || (mailingInsertIsImage && !mailingImageLinkValid) ?'.5' : '1', 32391 mailingInsertConfirmLabel: mailingInsertIsImage ?'Insertar imagen' : mailingInsertType === 'button' ?'Insertar botón' : 'Insertar enlace', 32392 mailingReportVisible: !!mailingSelected?.id, 32393 mailingReportLoading: !!s.mailingReportLoading, 32394 mailingReportHasError: !!s.mailingReportError, 32395 mailingReportError: s.mailingReportError || '', 32396 mailingReportReady: !!mailingReport && !s.mailingReportLoading, 32397 mailingReportKpis, 32398 mailingReportRates, 32399 mailingReportRows, 32400 mailingReportRowsEmpty: mailingReportRows.length === 0, 32401 mailingReportSearch: s.mailingReportSearch || '', 32402 mailingReportStatusFilter, 32403 mailingReportLetters, 32404 mailingReportFilteredTotal: mailingReportNumber(mailingReportFilteredRows.length), 32405 mailingReportRange: mailingReportFilteredRows.length ?mailingReportNumber(mailingReportPageStart + 1) + 'â' + mailingReportNumber(mailingReportPageStart + mailingReportRows.length) : '0', 32406 mailingReportPageLabel: 'Página ' + mailingReportPage + ' de ' + mailingReportPageCount, 32407 mailingReportPrevDisabled: mailingReportPage <= 1, 32408 mailingReportNextDisabled: mailingReportPage >= mailingReportPageCount, 32409 mailingReportPrevOpacity: mailingReportPage <= 1 ?'.45' : '1', 32410 mailingReportNextOpacity: mailingReportPage >= mailingReportPageCount ?'.45' : '1', 32411 onMailingReportSearch: (event) => this.setState({ mailingReportSearch: event.target.value || '', mailingReportPage: 1 }), 32412 onMailingReportStatus: (event) => this.setState({ mailingReportStatusFilter: event.target.value || 'all', mailingReportPage: 1 }), 32413 mailingReportPrev: () => this.setState({ mailingReportPage: Math.max(1, mailingReportPage - 1) }), 32414 mailingReportNext: () => this.setState({ mailingReportPage: Math.min(mailingReportPageCount, mailingReportPage + 1) }), 32415 mailingReportTotal: mailingReportNumber(mailingReportSummary.accepted || mailingReportSummary.sent), 32416 mailingReportExportDisabled: !!s.mailingReportExporting || !mailingSelected?.id, 32417 mailingReportExportOpacity: s.mailingReportExporting || !mailingSelected?.id ?'.55' : '1', 32418 mailingReportExportLabel: s.mailingReportExporting ?'Exportando...' : 'Exportar CSV', 32419 toggleMailing: () => this.toggleMailing(), 32420 newMailingCampaign: () => this.newMailingCampaign(), 32421 newLaunchMailingCampaign: () => this.newLaunchMailingCampaign(), 32422 onMailingName: (event) => this.setMailingDraftField('name', event.target.value), 32423 onMailingSubject: (event) => this.setMailingDraftField('subject', event.target.value), 32424 onMailingPreview: (event) => this.setMailingDraftField('previewText', event.target.value), 32425 onMailingHtml: (event) => this.setMailingDraftField('html', event.target.value), 32426 onMailingHtmlSource: (event) => this.setMailingDraftField('html', this.mailingSanitizeHtml(event.target.value)), 32427 onMailingVisualInput: (event) => this.onMailingVisualInput(event), 32428 onMailingText: (event) => this.setMailingDraftField('text', event.target.value), 32429 onMailingTestEmail: (event) => this.setState({ mailingTestEmail: event.target.value || '' }), 32430 importMailingCsv: (event) => { const file = event.target.files?.[0]; event.target.value = ''; this.importMailingCsvFile(file); }, 32431 toggleMailingConsent: () => this.toggleMailingConsent(), 32432 saveMailingDraft: () => this.saveMailingDraft(), 32433 sendMailingTest: () => this.sendMailingTest(), 32434 deleteMailingCampaign: () => this.deleteMailingCampaign(mailingSelected), 32435 queueMailingCampaign: () => this.queueMailingCampaign(), 32436 saveMailingSelection: () => this.saveMailingEditorSelection(), 32437 mailingEditorUndo: (event) => this.execMailingEditorCommand(event, 'undo'), 32438 mailingEditorRedo: (event) => this.execMailingEditorCommand(event, 'redo'), 32439 mailingEditorFormat: (event) => this.execMailingEditorCommand(event, 'formatBlock', event.target.value), 32440 mailingEditorFont: (event) => this.execMailingEditorCommand(event, 'fontName', event.target.value), 32441 mailingEditorFontSize: (event) => this.execMailingEditorCommand(event, 'fontSize', event.target.value), 32442 mailingEditorBold: (event) => this.execMailingEditorCommand(event, 'bold'), 32443 mailingEditorItalic: (event) => this.execMailingEditorCommand(event, 'italic'), 32444 mailingEditorUnderline: (event) => this.execMailingEditorCommand(event, 'underline'), 32445 mailingEditorAlignLeft: (event) => this.execMailingEditorCommand(event, 'justifyLeft'), 32446 mailingEditorAlignCenter: (event) => this.execMailingEditorCommand(event, 'justifyCenter'), 32447 mailingEditorAlignRight: (event) => this.execMailingEditorCommand(event, 'justifyRight'), 32448 mailingEditorBullets: (event) => this.execMailingEditorCommand(event, 'insertUnorderedList'), 32449 mailingEditorNumbers: (event) => this.execMailingEditorCommand(event, 'insertOrderedList'), 32450 mailingEditorColor: (event) => this.execMailingEditorCommand(event, 'foreColor', event.target.value),
32451 mailingEditorBackground: (event) => this.execMailingEditorCommand(event, 'hiliteColor', event.target.value), 32452 mailingEditorDivider: (event) => { event?.preventDefault?.(); this.insertMailingEditorHtml('<hr style="border:0;border-top:1px solid #dce6ef;margin:24px 0">'); }, 32453 mailingEditorName: (event) => { event?.preventDefault?.(); this.insertMailingEditorHtml('<span>%recipient.name%</span>'); }, 32454 mailingEditorClear: (event) => this.execMailingEditorCommand(event, 'removeFormat'), 32455 openMailingLink: () => this.openMailingInsertDialog('link'), 32456 openMailingButton: () => this.openMailingInsertDialog('button'), 32457 openMailingImage: () => this.openMailingInsertDialog('image'), 32458 closeMailingInsert: () => this.closeMailingInsertDialog(), 32459 onMailingInsertUrl: (event) => this.setState({ mailingInsertUrl: event.target.value || '', mailingImageError: '' }), 32460 onMailingInsertText: (event) => this.setState({ mailingInsertText: event.target.value || '' }), 32461 onMailingImageSize: (event) => this.setState({ mailingInsertImageSize: event.target.value || 'medium' }), 32462 onMailingImageLink: (event) => this.setState({ mailingInsertImageLink: event.target.value || '' }), 32463 uploadMailingImage: (event) => { const file = event.target.files?.[0]; event.target.value = ''; this.uploadMailingEditorImage(file); }, 32464 confirmMailingInsert: () => this.confirmMailingInsert(), 32465 toggleMailingSource: () => this.toggleMailingEditorSource(), 32466 toggleMailingExpanded: () => this.toggleMailingEditorExpanded(), 32467 previewMailingHtml: () => this.previewMailingEditor(), 32468 refreshMailingReport: () => this.fetchMailingCampaignReport(), 32469 exportMailingReport: () => this.exportMailingCampaignReport(), 32470 profileUsersSelected: !s.publicAdvanceMode && s.adminUsersOpen ? 'is-selected' : '', 32471 profileMailingSelected: !s.publicAdvanceMode && s.route === 'mailingAdmin' ? 'is-selected' : '', 32472 profileEditorSelected: !s.publicAdvanceMode && s.profileEditorOpen ? 'is-selected' : '', 32473 adminUsersOpen: !s.publicAdvanceMode && !!s.adminUsersOpen, 32474 adminUsersGridCols: s.isMobile ?'1fr' : 'minmax(330px,1fr) minmax(0,1fr)', 32475 adminUsersDivider: s.isMobile ?'0' : '1px solid #e5edf5', 32476 adminUsersMobileDivider: s.isMobile ?'1px solid #e5edf5' : '0', 32477 adminUsersSearch: s.adminUsersSearch || '', 32478 adminUsersLoading: !!s.adminUsersLoading, 32479 adminUsersError: s.adminUsersError || '', 32480 adminUsersHasError: !!s.adminUsersError, 32481 adminUsersEmpty: !s.adminUsersLoading && !s.adminUsersError && Array.isArray(s.adminUsers) && adminUsersList.length === 0, 32482 adminUsersList, 32483 adminDirectoryFilters: directory.filters, 32484 adminDirectorySummary: directory.start + 'â' + directory.end + ' de ' + directory.total + ' usuarios', 32485 adminDirectoryGroup: s.adminUsersGroup || 'recent', 32486 adminDirectoryCompany: s.adminUsersConsignataria || '', 32487 adminDirectoryCompanyLogo: directoryCompanyLogo, 32488 adminDirectoryCompanyHasLogo: !!directoryCompanyLogo, 32489 adminDirectoryCompanyLabel: (() => { const company = directory.companies.find(c => c.id === (s.adminUsersConsignataria || '')); return company ? company.label + ' (' + company.count + ')' : 'Todas las consignatarias'; })(), 32490 adminCompanyMenuOpen: !!s.adminCompanyMenuOpen, 32491 adminCompanyMenuExpanded: String(!!s.adminCompanyMenuOpen), 32492 toggleAdminCompanyMenu: () => this.setState({adminCompanyMenuOpen:!s.adminCompanyMenuOpen}), 32493 onAdminCompanyKey: event => { if (event.key === 'Escape') this.setState({adminCompanyMenuOpen:false}); }, 32494 adminDirectoryCompanies: directory.companies.map(company => { 32495 const record = (s.adminConsignatarias || []).find(c => String(c.id) === company.id); 32496 const logo = record ? (this.consLogo(record.name || record.nombre || record.id) || record.logoUrl || record.logo || '') : ''; 32497 return {...company,logo,hasLogo:!!logo,selected:String(company.id === (s.adminUsersConsignataria || '')),bg:company.id === (s.adminUsersConsignataria || '')?'#edf5fc':'#fff',select:()=>this.setState({adminUsersConsignataria:company.id,adminUsersGroup:'auction',adminUsersPage:1,adminCompanyMenuOpen:false})}; 32498 }), 32499 onAdminDirectoryGroup: event => this.setState({adminUsersGroup:event.target.value,adminUsersConsignataria:'',adminUsersPage:1}), 32500 onAdminDirectoryCompany: event => this.setState({adminUsersConsignataria:event.target.value,adminUsersPage:1}), 32501 adminDirectoryCanSort: s.adminUsersGroup === 'all', 32502 adminDirectorySort: s.adminUsersSort || 'alphabetical', 32503 onAdminDirectorySort: event => this.setState({adminUsersSort:event.target.value,adminUsersPage:1}), 32504 adminDirectoryPaginated: directory.pages > 1, 32505 adminDirectoryFirstPage: directory.page === 1, 32506 adminDirectoryLastPage: directory.page === directory.pages, 32507 adminDirectoryPageLabel: directory.page + ' / ' + directory.pages, 32508 adminDirectoryPrevious: () => this.setState({adminUsersPage:Math.max(1,directory.page-1)}), 32509 adminDirectoryNext: () => this.setState({adminUsersPage:Math.min(directory.pages,directory.page+1)}), 32510 adminRecentMissingDates: s.adminUsersGroup === 'recent' && directory.missingDates > 0, 32511 adminMissingDateText: directory.missingDates + ' cuentas sin fecha de alta disponible no se incluyen en este orden.', 32512 refreshAdminDirectory: () => this.fetchAdminUsers(true),
32513 adminUserSelected: !!s.adminSelectedUser, 32514 adminUserNotSelected: !s.adminSelectedUser, 32515 adminSelectedName: s.adminSelectedUser?.displayName || '', 32516 adminSelectedEmail: s.adminSelectedUser?.email || '', 32517 adminRoleOptions, 32518 adminNeedsConsignataria: adminSelectedRoles.some((role) => ['consignatario', 'certificador'].includes(role)), 32519 adminConsignatariaOptions, 32520 adminRelationLoading: !!s.adminUserRelationLoading, 32521 adminUserHasMessage: !!adminUserMessage, 32522 adminUserMessageText: adminUserMessage?.text || '', 32523 adminUserMessageBg: adminUserMessage?.ok ?'#edf9f5' : '#fff0f1', 32524 adminUserMessageBorder: adminUserMessage?.ok ?'#b9e3d4' : '#f2c7cc', 32525 adminUserMessageColor: adminUserMessage?.ok ?'#17664f' : '#b42335', 32526 adminUserSaving: !!s.adminUserSaving || !!s.adminUserRelationLoading, 32527 adminUserSaveLabel: s.adminUserRetrying ?'Esperando al servidorâ¦' : s.adminUserSaving ?'Guardando...' : s.adminUserRelationLoading ?'Consultando...' : 'Guardar permisos', 32528 adminUserSaveOpacity: s.adminUserSaving || s.adminUserRelationLoading ?'.65' : '1', 32529 toggleAdminUsers: () => this.toggleAdminUsers(), 32530 onAdminUsersSearch: (event) => this.setAdminUsersSearch(event.target.value), 32531 saveAdminUser: () => this.saveAdminUserAccess(), 32532 profileEditorOpen: !s.publicAdvanceMode && !!s.profileEditorOpen, 32533 profileEditorSaving: !!s.profileEditorSaving, 32534 profileEditorError: s.profileEditorError || '', 32535 profileEditorHasError: !!s.profileEditorError, 32536 profileEditorFirstName: profileEditorForm.firstName || '', 32537 profileEditorLastName: profileEditorForm.lastName || '', 32538 profileEditorCuit: this.cuitInputDisplay(profileEditorForm.cuitCuil), 32539 onProfileCuit: e => this.setProfileEditorField('cuitCuil',this.cuitInputRaw(e.target.value)), 32540 profileEditorPhoneNumber: profileEditorForm.phoneNumber || '', 32541 profilePhoneReadOnly: !!s.user?.phoneNumber, 32542 profilePhoneBg: s.user?.phoneNumber ?'#eef7f4' : '#fff', 32543 profilePhoneVerified: !!s.user?.phoneNumber, 32544 profilePhoneStatusText: s.user?.phoneNumber ?'Verificado por SMS' : 'Teléfono de contacto.', 32545 profileEditorAvatar, 32546 profileEditorHasAvatar: !!profileEditorAvatar, 32547 profileEditorShowsInitials: !profileEditorAvatar, 32548 profileEditorInitials: (currentProfileName || s.user?.displayName || s.user?.email || 'U').split(/[\s@.]+/).slice(0, 2).map((word) => word[0]?.toUpperCase() || '').join(''), 32549 profileEditorSaveLabel: s.profileEditorSaving ?'Guardando...' : 'Guardar cambios', 32550 profileEditorSaveOpacity: s.profileEditorSaving ?'.65' : '1', 32551 profileCropOpen: !s.publicAdvanceMode && !!s.profileCropOpen, 32552 profileCropZoom: s.profileCropZoom || 1, 32553 profileCropX: s.profileCropX || 50, 32554 profileCropY: s.profileCropY || 50, 32555 openProfileEditor: () => this.openProfileEditor(), 32556 closeProfileEditor: () => this.closeProfileEditor(), 32557 saveProfileEditor: () => this.saveProfileEditor(), 32558 onProfileFirstName: (event) => this.setProfileEditorField('firstName', event.target.value), 32559 onProfileLastName: (event) => this.setProfileEditorField('lastName', event.target.value), 32560 onProfilePhoneNumber: (event) => this.setProfileEditorField('phoneNumber', event.target.value), 32561 onProfilePhoto: (event) => this.selectProfilePhoto(event.target.files?.[0]), 32562 onProfileCropZoom: (event) => this.setProfileCrop('profileCropZoom', event.target.value), 32563 onProfileCropX: (event) => this.setProfileCrop('profileCropX', event.target.value), 32564 onProfileCropY: (event) => this.setProfileCrop('profileCropY', event.target.value), 32565 cancelProfileCrop: () => this.cancelProfileCrop(), 32566 confirmProfileCrop: () => this.confirmProfileCrop(), 32567 notificationsLabel: s.notificationsStatus === 'enabled' ? 'Notificaciones activas' 32568 : s.notificationsStatus === 'loading' ? 'Activando...' 32569 : s.notificationsStatus === 'denied' ? 'Notificaciones bloqueadas' 32570 : s.notificationsStatus === 'unsupported' ? 'No compatible' 32571 : 'Activar notificaciones', 32572 notificationsTitle: s.notificationsMessage || (s.notificationsStatus === 'denied' 32573 ? 'Habilitalas desde la configuracion del navegador.' 32574 : 'Recibi avisos cuando superen tu oferta.'), 32575 notificationsBg: s.notificationsStatus === 'enabled' ? 'rgba(31,122,99,0.82)' 32576 : s.notificationsStatus === 'denied' || s.notificationsStatus === 'error' ? 'rgba(230,57,70,0.75)' 32577 : 'rgba(255,255,255,0.14)', 32578 notificationsBusy: ['enabled', 'loading', 'denied', 'unsupported'].includes(s.notificationsStatus), 32579 notificationsCursor: ['enabled', 'loading', 'denied', 'unsupported'].includes(s.notificationsStatus) ?'default' : 'pointer', 32580 notificationsOpacity: s.notificationsStatus === 'loading' ?0.65 : 1, 32581 enableNotifications: () => this.enableNotifications(), 32582 whatsappNotificationsLabel: s.whatsappNotificationsSaving ?'Guardando...' 32583 : s.whatsappNotificationsLoading ?'Consultando...' 32584 : s.whatsappNotificationsEnabled ?'WhatsApp activo' 32585 : 'Activar WhatsApp', 32586 whatsappNotificationsTitle: s.whatsappNotificationsMessage || (!s.whatsappNotificationsAvailable 32587 ? 'Verifica tu telefono para recibir alertas por WhatsApp.' 32588 : s.whatsappNotificationsEnabled ?'Desactivar alertas por WhatsApp.' : 'Recibi alertas transaccionales por WhatsApp.'), 32589 whatsappNotificationsBg: s.whatsappNotificationsEnabled ?'rgba(31,122,99,0.9)' : 'rgba(255,255,255,0.14)', 32590 whatsappNotificationsBusy: s.whatsappNotificationsSaving || s.whatsappNotificationsLoading, 32591 whatsappNotificationsCursor: s.whatsappNotificationsSaving || s.whatsappNotificationsLoading ?'default' : 'pointer', 32592 whatsappNotificationsOpacity: s.whatsappNotificationsSaving || s.whatsappNotificationsLoading ?0.65 : 1, 32593 toggleWhatsAppNotifications: () => this.toggleWhatsAppNotifications(), 32594 dashCols: s.isMobile ?'1fr' : 'minmax(0,1.7fr) minmax(240px,1fr)', 32595 kpis, 32596 showBuyerCreditPanel: false, 32597 creditGridCols: s.isMobile ?'1fr' : 'minmax(170px,0.8fr) repeat(2,minmax(190px,1fr)) auto', 32598 buyerCreditLoading: s.buyerAccessLoading && buyerCreditRows.length === 0, 32599 buyerCreditHas: buyerCreditRows.length > 0, 32600 buyerCreditEmpty: !s.buyerAccessLoading && buyerCreditRows.length === 0, 32601 buyerCreditRows, 32602 buyerCreditHistory: () => this.openCreditHistory(null, 'buyer'), 32603 sellerCreditHistory: () => this.openCreditHistory(null, 'seller'), 32604 creditHistoryVisible: !!s.creditHistoryOpen && creditHistoryPer
32604spective === 'seller' && canManageBuyers, 32605 creditHistoryTitle: creditHistoryParty, 32606 creditHistoryPartyLabel: creditHistoryPerspective === 'seller' ?'COMPRADOR' : 'VENDEDOR', 32607 creditHistoryRows, 32608 creditHistoryHas: creditHistoryRows.length > 0, 32609 creditHistoryEmpty: creditHistoryRows.length === 0, 32610 closeCreditHistory: () => this.closeCreditHistory(), 32611 closeCreditHistoryBackdrop: (event) => { if (event.target === event.currentTarget) this.closeCreditHistory(); }, 32612 stopCreditHistoryClick: (event) => event.stopPropagation(), 32613 creditSummaryVisible: !!s.creditSummaryOpen && creditSummaryPerspective === 'seller' && canManageBuyers, 32614 creditSummaryLoading: !!s.creditSummaryLoading, 32615 creditSummaryHasError: !!s.creditSummaryError && !s.creditSummaryLoading, 32616 creditSummaryError: s.creditSummaryError || '', 32617 creditSummaryReady: !!creditSummary && !s.creditSummaryLoading && !s.creditSummaryError, 32618 creditSummaryTitle: (creditSummaryPerspective === 'seller' ?'Crédito de ' : 'Crédito otorgado por ') + creditSummaryParty, 32619 creditSummarySubtitle: 'Vigencia: ' + creditSummaryPeriod, 32620 creditSummaryStatsCols: s.isMobile ?'1fr' : 'repeat(3,minmax(0,1fr))', 32621 creditSummaryStats: [ 32622 { label: 'CRÃDITO AUTORIZADO', value: this.formatBaCurrency(creditSummaryWeekly), bg: '#eef6fd', border: '#cfe1f1', color: '#075ca8' }, 32623 { label: 'CRÃDITO USADO', value: this.formatBaCurrency(creditSummaryUsed), bg: '#fff7ea', border: '#efd9ac', color: '#9b6513' }, 32624 { label: 'CRÃDITO DISPONIBLE', value: this.formatBaCurrency(creditSummaryAvailable), bg: '#edf9f4', border: '#c9e8db', color: '#1f7a63' }, 32625 ], 32626 creditSummaryProgress: creditSummaryPercent.toFixed(2) + '%', 32627 creditSummaryProgressLabel: Math.round(creditSummaryPercentRaw).toLocaleString('es-AR') + '%', 32628 creditSummaryHasMovements: creditMovementRows.length > 0, 32629 creditSummaryEmpty: creditMovementRows.length === 0, 32630 creditSummaryMovements: creditMovementRows, 32631 creditMovementCols: s.isMobile ?'1fr 1fr' : 'minmax(180px,1.35fr) minmax(105px,.65fr) minmax(130px,.8fr) minmax(130px,.8fr)', 32632 closeCreditSummary: () => this.closeCreditSummary(), 32633 closeCreditSummaryBackdrop: (event) => { if (event.target === event.currentTarget) this.closeCreditSummary(); }, 32634 stopCreditSummaryClick: (event) => event.stopPropagation(), 32635 refreshCreditSummary: () => this.refreshCreditSummary(), 32636 gridLines: [40, 90, 140, 190], 32637 activityEyebrow: buyerProfile ?'ACTIVIDAD DE OFERTAS' : 'DESEMPEÃO COMERCIAL', 32638 activityTitle: buyerProfile ?'Acumulado por lote' : 'Ofertas recibidas por lote', 32639 activityUnit: buyerProfile ?'ofertas registradas' : 'ofertas recibidas', 32640 activityPrimaryLegend: buyerProfile ?'Mis ofertas' : 'Mis lotes', 32641 activityMarketLegend: 'Promedio del mercado', 32642 activityTotal: fmtK(buyerProfile ?offersMade : receivedOffers), 32643 areaD: this.areaPath(norm(mySeries), 560, 200, 8), 32644 lineD: this.linePath(norm(mySeries), 560, 200, 8), 32645 lineD2: this.linePath(norm(mktSeries), 560, 200, 8), 32646 distributionEyebrow: buyerProfile ?'MIS INTERESES' : 'MI HACIENDA', 32647 distributionTitle: buyerProfile ?'CategorÃas ofertadas' : 'Por categorÃa', 32648 distributionUnit: buyerProfile ?'ofertas' : 'cabezas', 32649 donutGradient: distributionTotal ?'conic-gradient(' + stops.join(', ') + ')' : 'conic-gradient(#e8eff6 0deg 360deg)', 32650 donutTotal: fmtK(buyerProfile ?offersMade : totalCabezas), 32651 donutLegend: distributionItems.map((item, i) => ({ label: item.label, color: dColors[i % dColors.length], pct: Math.round(item.count / dTot * 100) + '%' })), 32652 showPriceStats: priceCats.length > 0, 32653 priceBars: priceCats.map(([label, mineV, mkt], i) => ({ 32654 label, mine: '$' + fmtK(mineV), market: '$' + fmtK(mkt), 32655 minePct: (mineV / pMax * 100).toFixed(0) + '%', marketPct: (mkt / pMax * 100).toFixed(0) + '%', 32656 delay: (i * 0.08 + 0.2).toFixed(2) + 's', delay2: (i * 0.08 + 0.35).toFixed(2) + 's', 32657 })), 32658 showProfileTabs: !buyerProfile, 32659 profileTabs: (() => { 32660 const defs = []; 32661 if (this.isRosganProfile()) defs.push(['resumen', 'Próximos lotes', (s.profileRosganLots || []).length]); 32662 if (canSell && !this.isRosganProfile()) defs.push(['resumen', 'Mis lotes', mine.length]); 32663 if (canSell || this.isCertifierOnly(s.apiUser)) defs.push(['comitentes', 'Comitentes', comitenteDirectory.total], ['establecimientos', 'Establecimientos', estData.length]); 32664 if (canBuy && !this.isRosganProfile()) defs.push(['ofertas', 'Mis ofertas', offerData.length]); 32665 if (canSell && !this.isRosganProfile() && (pendData.length || s.pendingBidsLoading)) defs.push(['aprobar', 'Por aprobar', pendData.length]); 32666 return defs.map(([key, label, count]) => ({ 32667 label, count, go: () => this.setState({ profileTab: key }), 32668 bg: s.profileTab === key ?'#002e5f' : '#fff', color: s.profileTab === key ?'#fff' : '#3d4c5f', border: s.profileTab === key ?'#002e5f' : '#e3eaf2', 32669 })); 32670 })(), 32671 profileShowLots: !this.isRosganProfile() && s.profileTab === 'resumen' && canSell, profileShowOffers: !buyerProfile && s.profileTab === 'ofertas' && canBuy, profileShowPending: s.profileTab === 'aprobar' && canSell, profileShowComitentes: s.profileTab === 'comitentes' && (canSell || this.isCertifierOnly(s.apiUser)), profileShowEst: s.profileTab === 'establecimientos' && (canSell || this.isCertifierOnly(s.apiUser)), 32672 profileCanEditRelations: this.canManageComitentes(s.apiUser), 32673 profileIsDev: this.userRoleIds(s.apiUser).includes('dev'), 32674 profileCurrentVersionLabel: s.devCurrentVersion ? (String(s.devCurrentVersion).startsWith('local-') ? 'Versión vigente · Entorno local · ' : 'Versión vigente · ') + String(s.devCurrentVersion).replace(/^rosganapp-build-/, '') : 'Consultando versión vigenteâ¦', 32675 openDevActivity: () => { if (this.userRoleIds(this.state.apiUser).includes('dev')) { window.RosganDevActivity.start(this); this.go('devActivity'); } }, 32676 openProfileComitenteMerge: () => this.openComitenteMerge(),
32677 profileShowBuyers: !this.isRosganProfile() && canManageBuyers, 32678 launchAdminVisible: canManageAdmin, 32679 launchAdminLoading: !!s.launchInterestsAdminLoading && s.launchInterestsAdmin === null, 32680 launchAdminHasError: !!s.launchInterestsAdminError, 32681 launchAdminError: s.launchInterestsAdminError || '', 32682 launchAdminHasRows: launchAdminRows.length > 0, 32683 launchAdminEmpty: !s.launchInterestsAdminLoading && !s.launchInterestsAdminError && launchAdminRows.length === 0, 32684 launchAdminRows, 32685 launchAdminCountLabel: (Number(s.launchInterestsAdminTotal) || launchAdminRows.length).toLocaleString('es-AR') + ' persona' + ((Number(s.launchInterestsAdminTotal) || launchAdminRows.length) === 1 ?'' : 's') + ' anotada' + ((Number(s.launchInterestsAdminTotal) || launchAdminRows.length) === 1 ?'' : 's'), 32686 launchAdminExportDisabled: !launchAdminRows.length, 32687 launchAdminExportOpacity: launchAdminRows.length ?'1' : '.45', 32688 exportLaunchInterests: () => this.exportLaunchInterests(), 32689 lotYieldAdminVisible: this.isSysadmin(s.apiUser), 32690 lotYieldLoading: !!s.lotYieldLoading && s.lotYieldCategories === null, 32691 lotYieldHasError: !!s.lotYieldError, 32692 lotYieldError: s.lotYieldError || '', 32693 lotYieldHasRows: lotYieldRows.length > 0, 32694 lotYieldEmpty: !s.lotYieldLoading && !s.lotYieldError && lotYieldRows.length === 0, 32695 lotYieldRows, 32696 lotYieldHasMessage: !!s.lotYieldMessage, 32697 lotYieldMessageText: s.lotYieldMessage?.text || '', 32698 lotYieldMessageBg: s.lotYieldMessage?.ok ?'#edf9f5' : '#fff0f1', 32699 lotYieldMessageBorder: s.lotYieldMessage?.ok ?'#b9e3d4' : '#f2c7cc', 32700 lotYieldMessageColor: s.lotYieldMessage?.ok ?'#17664f' : '#b42335', 32701 refreshLotYields: () => this.loadLotYieldCategories(true), 32702 buyerDirectorySearch: s.buyerDirectorySearch || '', 32703 setBuyerDirectorySearch: (event) => this.setState({ buyerDirectorySearch: event.target.value }), 32704 buyerDirectoryLoading: s.buyerAccountsLoading && !buyerDirectoryData.length, 32705 buyerDirectoryHas: buyerDirectoryData.length > 0, 32706 buyerDirectoryEmpty: !s.buyerAccountsLoading && buyerDirectoryData.length === 0, 32707 buyerDirectoryEmptyText: buyerDirectoryQuery ?'No hay compradores que coincidan con la búsqueda.' : 'No hay cuentas con rol comprador.', 32708 buyerDirectoryRows: buyerDirectoryData, 32709 baLoading: s.buyerAuthsLoading && s.buyerAuths === null, 32710 baHas: baData.length > 0, baEmpty: !s.buyerAuthsLoading && baData.length === 0, 32711 baFormOpen: s.baFormOpen, 32712 toggleBaForm: () => this.setState({ baFormOpen: !s.baFormOpen, baForm: s.baFormOpen ?s.baForm : this.emptyBaForm(), baFormMessage: null }), 32713 ba_selectedBuyer: baForm.buyerEmail || 'Seleccioná un comprador del directorio', 32714 ba_creditStartDate: baForm.creditStartDate, set_ba_creditStartDate: (e) => this.setBaForm('creditStartDate', e.target.value), 32715 ba_weeklyCreditAmount: this.moneyInputDisplay(baForm.weeklyCreditAmount), set_ba_weeklyCreditAmount: (e) => this.formatMoneyInputEvent(e, (raw) => this.setBaForm('weeklyCreditAmount', raw)), 32716 baSaving: s.baSaving, baSaveLabel: s.baSaving ?'Creando créditoâ¦' : 'Crear crédito', 32717 baFormMsg: s.baFormMessage?.text || '', baFormMsgBg: s.baFormMessage?.ok ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)', baFormMsgColor: s.baFormMessage?.ok ?'#1f7a63' : '#c22', 32718 saveBuyerAuth: () => this.saveBuyerAuth(), 32719 baRows: baData, 32720 profileLotsLoading: s.myLotsLoading && mine.length === 0, 32721 hasMyLots: mine.length > 0, myLotsEmpty: !s.myLotsLoading && mine.length === 0, 32722 myLotCards: mineSorted.map((l) => { 32723 const bid = bidFor(l); 32724 const prog = Math.min(100, Math.round(bid / (l.baseNum || bid || 1
32724) * 88 + 12)); 32725 const expired = l.status === 'Caducado'; 32726 const canRepublish = expired && l.canRepublish; 32727 const offerCount = Math.max(Number(l.bidCount) || 0, (l.bids || []).length, s.bidsPlaced[l.cert] ?1 : 0); 32728 const isSold = l.auctionStatus === 'sold' || l.status === 'Vendido'; 32729 const isClosedWithoutBids = l.auctionStatus === 'closed_no_bids'; 32730 const isOpen = l.auctionStatus === 'open' || l.status === 'En vivo'; 32731 const canEdit = !expired && !isSold && !isClosedWithoutBids; 32732 const bidTitle = isSold ?'PRECIO FINAL' : (isOpen && offerCount > 0 ?'OFERTA LÃDER' : 'PRECIO BASE'); 32733 const readyForLoading = isSold && !!l.fechaDisponibleCarga && l.fechaDisponibleCarga <= this.localIsoDate(); 32734 const progressLabel = isSold 32735 ?(offerCount === 1 ?'Venta cerrada · 1 oferta' : 'Venta cerrada · ' + offerCount + ' ofertas') 32736 : isClosedWithoutBids 32737 ? 'Cerrado sin ofertas' 32738 : isOpen && offerCount > 0 32739 ?(offerCount === 1 ?'1 oferta recibida' : offerCount + ' ofertas recibidas') 32740 : l.auctionStatus === 'scheduled' 32741 ? 'Subasta programada' 32742 : 'Esperando ofertas'; 32743 return { open: () => this.openLot(l.cert), image: l.image, categoria: l.categoria, cert: l.cert, origen: l.origen, 32744 bid: this.fmtARS(bid), bidTitle, cabezas: String(l.cabezas), progress: prog + '%', progressLabel, 32745 expired, notExpired: !expired, canRepublish, canEdit, 32746 edit: (event) => { event.stopPropagation(); this.startEditLot(l); }, 32747 republish: (event) => { event.stopPropagation(); this.openRepublish(l); }, 32748 ...stateMap(readyForLoading ?'Listo para la carga' : l.status) }; 32749 }), 32750 republishOpen: !!s.republishLot, 32751 republishTitle: s.republishLot ?(s.republishLot.categoria + ' · Cert. ' + s.republishLot.cert) : '', 32752 closeRepublish: () => this.closeRepublish(), 32753 rp_fecha: republishForm.fechaSubasta || '', set_rp_fecha: (e) => this.setRepublish('fechaSubasta', e.target.value), 32754 rp_listoParaCargar: republishForm.listoParaCargar === true, 32755 rp_listoLabel: republishForm.listoParaCargar === true ?'â Listo para cargar desde la subasta' : 'â Listo para cargar desde la subasta', 32756 rp_listoBg: republishForm.listoParaCargar === true ?'#e6f5ef' : '#f5f8fc', rp_listoBorder: republishForm.listoParaCargar === true ?'#1f7a63' : '#dce6f0', rp_listoColor: republishForm.listoParaCargar === true ?'#17664f' : '#3d4c5f', 32757 toggleRpListoParaCargar: () => this.setRepublish('listoParaCargar', republishForm.listoParaCargar !== true), 32758 rp_showFechaCarga: republishForm.listoParaCargar !== true, 32759 rp_fechaCargaMin: this.loadDateOffset(republishForm.fechaSubasta, 1), rp_fechaCargaMax: this.loadDateOffset(republishForm.fechaSubasta, 7), 32760 rp_fechaCarga: republishForm.fechaDisponibleCarga || '', set_rp_fechaCarga: (e) => this.setRepublish('fechaDisponibleCarga', e.target.value), 32761 rp_precio: this.moneyInputDisplay(republishForm.precioSugerido), set_rp_precio: (e) => this.setRepublish('precioSugerido', this.moneyInputRaw(e.target.value)), 32762 rp_plazo: republishForm.plazoId || '', set_rp_plazo: (e) => this.setRepublish('plazoId', e.target.value), 32763 rp_plazoOtro: republishForm.plazoOtro || '', set_rp_plazoOtro: (e) => this.setRepublish('plazoOtro', e.target.value), 32764 rpPlazoOpts: (this.CATALOGS.termOptions || []).map(([id, label]) => ({ id, label })), 32765 rpShowOtro: republishForm.plazoId === 'otro', 32766 rpSaving: s.republishSaving, rpSaveLabel: s.republishSaving ?'Republicandoâ¦' : 'Republicar lote', 32767 submitRepublish: () => this.submitRepublish(), 32768 rpMsg: s.republishMessage?.text || '', rpMsgBg: s.republishMessage?.ok ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)', rpMsgColor: s.republishMessage?.ok ?'#1f7a63' : '#c22', 32769 myBidsLoading: s.myBidsLoading && !offerData.length, 32770 hasOffers: offerData.length > 0, offersEmpty: !s.myBidsLoading && offerData.length === 0, 32771 offerRows: offerData, 32772 showLiveOffersPanel: buyerProfile, 32773 livePanelStatus: liveOffers.length ?'EN VIVO' : 'SIN OFERTAS ABIERTAS', 32774 livePanelStatusColor: liveOffers.length ?'#1f7a63' : '#65758a', 32775 livePanelStatusBg: liveOffers.length ?'rgba(31,122,99,0.1)' : 'rgba(101,117,138,0.1)', 32776 livePanelDotAnimation: liveOffers.length ?'pulseDot 1.2s infinite' : 'none', 32777 hasLiveOffers: liveOffers.length > 0, 32778 liveOffersLoading: canBuy && s.myBidsLoading && liveOffers.length === 0, 32779 liveOffersEmpty: !s.myBidsLoading && liveOffers.length === 0 && buyerClosedAllRows.length === 0, 32780 liveOffers, 32781 completedOffers, 32782 buyerClosedHas: buyerClosedAllRows.length > 0, 32783 buyerClosedCount: buyerClosedAllRows.length, 32784 buyerClosedOfferRows, 32785 buyerClosedPaginated: buyerClosedPages > 1, 32786 buyerClosedPageLabel: 'Página ' + buyerClosedPage + ' de ' + buyerClosedPages, 32787 buyerClosedPrevDisabled: buyerClosedPage <= 1, 32788 buyerClosedNextDisabled: buyerClosedPage >= buyerClosedPages, 32789 buyerClosedPrev: () => this.setState({ buyerClosedPage: Math.max(1, buyerClosedPage - 1) }), 32790 buyerClosedNext: () => this.setState({ buyerClosedPage: Math.min(buyerClosedPages, buyerClosedPage + 1) }), 32791 showSellerOffersPanel: canSell, 32792 sellerOffersStatus: (() => { 32793 const liveCount = sellerOpenOfferRows.length; 32794 const finalCount = sellerClosedAllRows.length; 32795 return [liveCount ?liveCount + ' en vivo' : '', finalCount ?finalCount + ' cerrados' : ''].filter(
32795Boolean).join(' · ') || 'SIN OFERTAS'; 32796 })(), 32797 sellerOffersStatusColor: sellerOpenOfferRows.length ?'#1f7a63' : '#65758a', 32798 sellerOffersStatusBg: sellerOpenOfferRows.length ?'rgba(31,122,99,0.1)' : 'rgba(101,117,138,0.1)', 32799 sellerOffersStatusAnimation: sellerOpenOfferRows.length ?'pulseDot 1.2s infinite' : 'none', 32800 sellerOffersLoading: sellerOpenLots.some((lot) => !!s.lotBidsLoading[lot.cert]) && sellerOfferRows.length === 0, 32801 sellerOpenOffersEmpty: !sellerOpenLots.some((lot) => !!s.lotBidsLoading[lot.cert]) && sellerOpenOfferRows.length === 0, 32802 sellerOpenOfferRows, 32803 sellerClosedHas: sellerClosedAllRows.length > 0, 32804 sellerClosedCount: sellerClosedAllRows.length, 32805 sellerClosedOfferRows, 32806 sellerClosedPaginated: sellerClosedPages > 1, 32807 sellerClosedPageLabel: 'Página ' + sellerClosedPage + ' de ' + sellerClosedPages, 32808 sellerClosedPrevDisabled: sellerClosedPage <= 1, 32809 sellerClosedNextDisabled: sellerClosedPage >= sellerClosedPages, 32810 sellerClosedPrev: () => this.setState({ sellerClosedPage: Math.max(1, sellerClosedPage - 1) }), 32811 sellerClosedNext: () => this.setState({ sellerClosedPage: Math.min(sellerClosedPages, sellerClosedPage + 1) }), 32812 pendingLoading: s.pendingBidsLoading && !pendData.length, 32813 hasPending: pendData.length > 0, pendingEmpty: !s.pendingBidsLoading && pendData.length === 0, 32814 pendingRows: pendData, 32815 comitentesLoading: s.comitentesLoading && !comitenteData.length, 32816 comitentesHas: comitenteData.length > 0, 32817 comitentesEmpty: !s.comitentesLoading && !comitenteData.length, 32818 comitenteRows: comitenteData, 32819 pcQuery: s.profileComitenteQuery || '', pcProvince: s.profileComitenteProvince || '', pcLocality: s.profileComitenteLocality || '', 32820 pcProvinces: comitenteDirectory.provinces.map(value => ({value})), pcLocalities: comitenteDirectory.localities.map(value => ({value})), 32821 pcSearch: e => this.setState({profileComitenteQuery:e.target.value,profileComitentePage:1}), 32822 pcSetProvince: e => this.setState({profileComitenteProvince:e.target.value,profileComitenteLocality:'',profileComitentePage:1}), 32823 pcSetLocality: e => this.setState({profileComitenteLocality:e.target.value,profileComitentePage:1}), 32824 pcClear: () => this.setState({profileComitenteQuery:'',profileComitenteProvince:'',profileComitenteLocality:'',profileComitentePage:1}), 32825 pcHasFilters: !!(s.profileComitenteQuery || s.profileComitenteProvince || s.profileComitenteLocality), 32826 pcCount: comitenteDirectory.count + ' de ' + comitenteDirectory.total + ' comitentes', 32827 pcEmptyLabel: comitenteDirectory.total ? 'No hay coincidencias con esta búsqueda' : 'TodavÃa no tenés comitentes', 32828 pcPaginated: comitenteDirectory.pages > 1, 32829 pcPageLabel: 'Página ' + comitenteDirectory.page + ' de ' + comitenteDirectory.pages, 32830 pcPrevDisabled: comitenteDirectory.page <= 1, pcNextDisabled: comitenteDirectory.page >= comitenteDirectory.pages, 32831 pcPrev: () => this.setState({profileComitentePage:comitenteDirectory.page-1}), 32832 pcNext: () => this.setState({profileComitentePage:comitenteDirectory.page+1}), 32833 comitenteFormOpen: !!s.comitenteFormOpen, 32834 toggleComitenteForm: () => { const opening = !s.comitenteFormOpen; if (opening) this.fetchProvinces(); this.setState({ comitenteFormOpen: opening, comitenteForm: opening ?this.emptyComitenteForm() : s.comitenteForm, comitenteFormMessage: null }); }, 32835 cf_cuitCuil: comitenteForm.cuitCuil || '', set_cf_cuitCuil: (event) => this.onComitenteCuit(event.target.value), 32836 cf_alias: comitenteForm.alias || '', set_cf_alias: e => this.setComitenteForm('alias', e.target.value), 32837 cf_nombreFiscal: comitenteForm.nombreFiscal || '', cf_provincia: comitenteForm.provincia || '', cf_localidad: comitenteForm.localidad || '', 32838 cfLookupStatus: s.comitenteLookupLoading ?'Consultando ARCAâ¦' : (comitenteForm.source ?'Datos verificados' : ''), 32839 comitenteSave: () => this.saveComitente(), 32840 comitenteSaveDisabled: !!s.comitenteSaving || !!s.comitenteLookupLoading || !String(comitenteForm.alias || '').trim(), 32841 comitenteSaveLabel: s.comitenteSaving ?'Creandoâ¦' : (s.comitenteLookupLoading ?'Consultandoâ¦' : 'Crear comitente'), 32842 comitenteFormMsg: s.comitenteFormMessage?.text || '', 32843 comitenteFormMsgBg: s.comitenteFormMessage?.ok ?'#edf9f5' : '#fff0f1', 32844 comitenteFormMsgColor: s.comitenteFormMessage?.ok ?'#17664f' : '#b42335', 32845 estLoading: s.establishmentsLoading && !estData.length, 32846 estHas: estData.length > 0, estEmpty: !s.establishmentsLoading && estData.length === 0, 32847 estRowsList: estData, 32848 estFormOpen: s.estFormOpen, 32849 toggleEstForm: () => { if (!this.canManageComitentes(s.apiUser)) return; const opening = !s.estFormOpen; if (opening) this.fetchProvinces(); this.setState({ estFormOpen: opening, estForm: s.estFormOpen ?s.estForm : this.emptyEstForm(), estFormMessage: null }); }, 32850 ef_nombre: ef.nombre, set_ef_nombre: (e) => this.setEstForm('nombre', e.target.value), 32851 ef_comitenteId: ef.comitenteId || '', set_ef_comitenteId: (e) => this.setEstForm('comitenteId', e.target.value), 32852 efComitenteOpts: comitenteData.map((item) => ({ id: item.id, label: item.razonSocial + ' · ' + item.cuitCuil })), 32853 ef_renspa: ef.renspa || '', set_ef_renspa: (e) => this.setEstForm('renspa', e.target.value), 32854 ef_provincia: ef.provincia, set_ef_provincia: (e) => this.setEstForm('provincia', e.target.value), 32855 ef_localidad: ef.localidad, set_ef_localidad: (e) => this.onEstPlaceInput(e.target.value), 32856 ef_latitud: ef.latitud, set_ef_latitud: (e) => this.setEstForm('latitud', e.target.value), 32857 ef_longitud: ef.longitud, set_ef_longitud: (e) => this.setEstForm('longitud', e.target.value), 32858 efProvinceOpts: s.geoProvinces || [], efProvPlaceholder: s.geoLoadingProv ?'Cargando provinciasâ¦' : 'Seleccionarâ¦', 32859 efLocalityOpts: [], 32860 efLocDisabled: !ef.provincia, efLocBg: !ef.provincia ?'#f2f6fb' : '#fff', 32861 efLocPlaceholder: !ef.provincia ?'Elegà provincia primero' : 'Escribà una localidad', 32862 efPlaceLoading: !!s.estPlaceLoading, 32863 efPlaceResultsOpen: !!(s.estPlaceResults || []).length, 32864 efPlaceResults: (s.estPlaceResults || []).map((item) => ({ label: item.label || 'Localidad', address: item.address || '', pick: () => this.selectEstPlace(item) })), 32865 openEfPlaces: () => { if (ef.provincia && String(ef.localidad || '').trim().length >= 3) this.searchEstPlaces(ef.localidad, ef.provincia); }, 32866 closeEfPlaces: () => this.closeEstPlaceResults(), 32867 efCoordsNote: (ef.latitud && ef.longitud) ?'Coordenadas autocompletadas desde la localidad' : 'Se completan al elegir la localidad', 32868 saveEst: () => this.saveEstablishment(), 32869 estSaveLabel: s.estSaving ?'Guardandoâ¦' : 'Guardar establecimiento', 32870 estSaveDisabled: s.estSaving, 32871 estSaveCursor: s.estSaving ?'not-allowed' : 'pointer', 32872 estSaveOpacity: s.estSaving ?'0.55' : '1', 32873 estFormMsg: s.estFormMessage?.text || '', estFormMsgBg: s.estFormMessage?.ok ?'rgba(31,122,99,0.12)' : 'rgba(230,57,70,0.12)', estFormMsgColor: s.estFormMessage?.ok ?'#1f7a63' : '#c22', 32874 }; 32875 })(), 32876 // ---- feria creator ---- 32877 goFeria: () => this.go('feria'), 32878 goFeriaConfig: () => this.go('feriaConfig'), 32879 ...(() => { 32880 const ff = s.feriaForm; 32881 const phase = s.feriaPhase; 32882 const canChooseConsignataria = this.canManageUsers(s.apiUser); 32883 const assignedConsignataria = this.feriaAssignedConsignataria(); 32884 const allConsignatarias = (s.consignatarias || []).map((c) => ({ ...c, name: this.consName(c) })).filter((c) => c.name).sort((a, b) => a.name.localeCompare(b.name, 'es')); 32885 const cons = canChooseConsignataria ?allConsignatarias : (assignedConsignataria ?[{ ...assignedConsignataria, name: this.consName(assignedConsignataria) || assignedConsignataria.name }] : []); 32886 const provs = s.geoProvinces; 32887 const res = s.feriaResult || {}; 32888 const resVid = this.ytId(res.youtubeUrl); 32889 const fields = ['consignataria', 'provincia', 'localidad', 'fecha', 'hora', 'cabezas'];
32890 const done = fields.filter((k) => String(ff[k] || '').length).length; 32891 const pvVid = this.ytId(ff.youtube) || (s.videos || [])[0]?.id; 32892 const dfmt = (iso) => { const d = this.feriaDate({fecha:iso}); return !d ?'' : d.toLocaleDateString('es-AR', { day: 'numeric', month: 'short', year: 'numeric' }); }; 32893 const fechaLabel = ff.fecha ?new Date(ff.fecha + 'T' + (ff.hora || '00:00')).toLocaleDateString('es-AR', { day: 'numeric', month: 'short' }) : 'â'; 32894 return { 32895 feriaDevicesAllowed: canChooseConsignataria, 32896 controlFeriaYoutube: () => window.RosganFeriaYoutube.list({apiFetch:(path,opts)=>this.apiFetch(path,opts),consLogo:name=>this.consLogo(name)}), 32897 feriaCols: s.isMobile ?'1fr' : 'minmax(0,1.35fr) minmax(300px,1fr)', 32898 feriaGrid2: s.isMobile ?'1fr' : '1fr 1fr', 32899 feriaGrid3: s.isMobile ?'1fr 1fr' : '1fr 1fr 1fr', 32900 feriaThumbnailData: JSON.stringify({ ...ff, nombreRemate: ff.nombre || (ff.localidad ? 'Feria ' + ff.localidad : 'Remate feria') }), 32901 feriaPreviewPos: s.isMobile ?'static' : 'sticky', 32902 feriaProcessing: phase !== 'idle', 32903 feriaBusy: phase === 'connecting' || phase === 'creating', 32904 feriaDone: phase === 'done' && this.feriaYoutubeReady(res), feriaFailed: phase === 'error' || phase === 'waiting', feriaError: s.feriaError, 32905 feriaCanRetry: !!res.id, feriaFailureTitle: res.id ? 'Feria guardada · transmisión pendiente' : 'No pudimos crear la feria', 32906 feriaRetryLabel: phase === 'error' && res.youtubeStatus === 'failed' ? 'Reintentar transmisión' : 'Consultar estado', 32907 retryFeriaYoutube: () => this.retryFeriaYoutube(), 32908 deleteFeria: () => this.deleteSavedFeria(res), feriaDeleting: !!s.feriaDeleting, feriaDeleteLabel: s.feriaDeleting ? 'Eliminandoâ¦' : 'Eliminar feria', 32909 feriaSteps: [ 32910 { label: 'Conectando', key: 'connecting' }, { label: 'Creando vivo', key: 'creating' }, { label: 'Portada', key: 'done' }, 32911 ].map((st, i) => { 32912 const order = { connecting: 0, creating: 1, done: 2 }; 32913 const cur = order[phase] || 0; const idx = i; 32914 const active = idx === cur; const complete = idx < cur; 32915 return { label: st.label, color: active || complete ?'#fff' : 'rgba(255,255,255,0.4)', 32916 dotBg: complete ?'#5fd68f' : active ?'#fff' : 'rgba(255,255,255,0.14)', dotColor: complete ?'#fff' : active ?'#002e5f' : '#fff', 32917 done: complete, numberVisible: !complete, number: String(idx + 1) }; 32918 }), 32919 feriaPhaseTitle: phase === 'creating' ?'Creando tu remate' : 'Conectando con Rosgan', 32920 feriaPhaseText: phase === 'creating' ?'Preparando el vivo, vinculando la transmisión y generando la portada en YouTube.' : 'Estableciendo conexión con los servidores de transmisión.', 32921 resultThumb: resVid ?'https://i.ytimg.com/vi/' + resVid + '/maxresdefault.jpg' : this.consLogo(res.consignataria) || 'assets/feria.png',
32922 resultLogo: this.consLogo(res.consignataria), resultHasLogo: !!this.consLogo(res.consignataria), 32923 resultTitle: res.nombreRemate || res.title || 'Remate feria', resultUrl: res.youtubeUrl || '', 32924 resultCons: res.consignataria || 'â', resultDate: dfmt(res.fecha) || 'A confirmar', resultDemo: false, 32925 copyResult: () => this.copyText(res.youtubeUrl || ''), 32926 backFromFeriaResult: () => { this.resetFeriaCreator(); this.go('profile'); this.refreshProfileData(true); }, 32927 resetCreator: () => this.resetFeriaCreator(), 32928 consChips: cons.map((c) => { 32929 const name = c.name || c.nombre || 'Consignataria'; 32930 const logo = this.consLogo(name) || c.logoUrl || c.logo || c.imageUrl || ''; 32931 return { name, pick: () => this.chooseFeriaConsignataria(name, logo), logo, whiteLogo: this.consWhiteLogo(name) || logo, logoBg: this.consBrandColor(name) || '#002e5f', 32932 cursor: canChooseConsignataria ?'pointer' : 'default', bg: ff.consignataria === name ?'rgba(31,122,99,0.08)' : '#f6f9fc', border: ff.consignataria === name ?'#1f7a63' : '#eef3f9' }; 32933 }), 32934 feriaConsScopeText: canChooseConsignataria ?'' : (assignedConsignataria ?'La consignataria está fijada por la asignación de tu usuario.' : 'Tu usuario no tiene una consignataria activa asignada.'), 32935 feriaConsScopeColor: assignedConsignataria || canChooseConsignataria ?'#65758a' : '#c62f3d', 32936 feriaHasConsignataria: !!ff.consignataria, 32937 feriaFavoriteColor: this.consBrandColor(ff.consignataria), 32938 feriaFavoriteLogo: ff.consignatariaLogo || this.consLogo(ff.consignataria) || '', 32939 feriaManualOpen: !!s.feriaManualOpen, 32940 feriaFavoritesHint: this.feriaFavoritesFor(ff.consignataria).length ?'Elegà dónde será tu próximo remate.' : 'Sin fer
32940ias favoritas. Tocá âUsar otra ubicaciónâ para cargar una.', 32941 feriaFavorites: this.feriaFavoritesFor(ff.consignataria).map(f => ({ ...f, selected:s.feriaFavoriteId === f.id, border:s.feriaFavoriteId === f.id ?'#1f7a63':'#e3eaf2', bg:s.feriaFavoriteId === f.id ?'#eef8f3':'#f6f9fc', pick:() => this.chooseFeriaFavorite(f.id) })), 32942 feriaManualLocation: () => this.clearFeriaFavorite(), 32943 provinceOpts: provs || [], provPlaceholder: s.geoLoadingProv ?'Cargando provinciasâ¦' : 'Seleccionarâ¦', 32944 locDisabled: !ff.provincia, locBg: !ff.provincia ?'#f2f6fb' : '#fff', 32945 locPlaceholder: !ff.provincia ?'Elegà provincia primero' : 'Escribà una localidad', 32946 ff_provincia: ff.provincia, ff_setProvincia: (e) => this.setFF('provincia', e.target.value), 32947 ff_localidad: ff.localidad, ff_setLocalidad: (e) => this.onFeriaPlaceInput(e.target.value), 32948 ff_openLocalidades: () => { if (ff.provincia && String(ff.localidad || '').trim().length >= 3) this.searchFeriaPlaces(ff.localidad, ff.provincia); }, 32949 ff_closeLocalidades: () => this.closeFeriaPlaceResults(), 32950 feriaPlaceLoading: !!s.feriaPlaceLoading, 32951 feriaPlaceResultsOpen: !!(s.feriaPlaceResults || []).length, 32952 feriaPlaceResults: (s.feriaPlaceResults || []).map((item) => ({ label: item.label || 'Localidad', address: item.address || '', pick: () => this.selectFeriaPlace(item) })), 32953 feriaPlaceError: s.feriaPlaceError || '', 32954 ff_nombre: ff.nombre, ff_setNombre: (e) => this.setFF('nombre', e.target.value), 32955 ff_fecha: ff.fecha, ff_setFecha: (e) => this.setFF('fecha', e.target.value), 32956 ff_hora: ff.hora, ff_setHora: (e) => this.setFF('hora', e.target.value), 32957 ff_cabezas: ff.cabezas, ff_setCabezas: (e) => this.setFF('cabezas', e.target.value), 32958 feriaFormMessage: s.feriaFormMessage?.text || '', 32959 feriaBtnBg: this.feriaValid() ?'#1f7a63' : '#9fb4ab', 32960 cancelFeriaEdit: () => { this.resetFeriaCreator(); this.go('profile'); }, feriaHeading: s.feriaEditingId ? 'Editar remate feria' : 'Crear remate feria', feriaSubmitLabel: s.feriaEditingId ? 'Guardar cambios' : 'Generar transmisión', feriaEditingId: s.feriaEditingId || null, submitFeria: () => this.submitFeria(), 32961 shareFeriaFlyer: () => this.shareFeriaFlyer(), 32962 pvBaseColor: this.consBrandColor(ff.consignataria), 32963 pvGradA: 'color-mix(in srgb, ' + this.consBrandColor(ff.consignataria) + ', white 18%)', 32964 pvGradB: 'color-mix(in srgb, ' + this.consBrandColor(ff.consignataria) + ', black 30%)', 32965 pvFeriaLogo: ff.consignatariaLogo || this.consLogo(ff.consignataria), pvFeriaHasLogo: !!(ff.consignatariaLogo || this.consLogo(ff.consignataria)), pvFeriaNoLogo: !(ff.consignatariaLogo || this.consLogo(ff.consignataria)), 32966 pvFeriaInitial: (ff.consignataria || 'R').trim()[0].toUpperCase(), 32967 pvFeriaTitle: ff.nombre || (ff.localidad ?'Feria ' + ff.localidad : 'Nuevo remate feria'), 32968 pvFeriaMeta: [ff.consignataria, [ff.localidad, ff.provincia].filter(Boolean).join(', ')].filter(Boolean).join(' · ') || 'Completá los datos', 32969 pvFeriaCabezas: ff.cabezas ?Number(ff.cabezas).toLocaleString('es-AR') : 'â', 32970 pvFeriaFecha: fechaLabel, 32971 feriaPct: Math.round(done / fields.length * 100) + '%', 32972 hasCreatedFerias: (s.feriaCreated || []).length > 0, 32973 createdFeriaList: (s.feriaCreated || []).slice(0, 4).map((cf) => ({ title: cf.nombreRemate || cf.title || 'Remate feria', href: cf.youtubeUrl || 'https://www.youtube.com/@rosgantv' })), 32974 }; 32975 })(), 32976 }; 32977 } 32978} 32979</script>
vendor: 1 bytes, line 32979
32979
32980<script id="lacassina-public-stats-v2-flag">window.__ROSGAN_PUBLIC_STATS_V2__ = true;</script>
vendor: 1 bytes, line 32980
32980
32981<script id="lacassina-public-stats-script"> 32982(function(){ 32983 if (window.__ROSGAN_PUBLIC_ROUTE__ !== 'lacassina-stats' || window.__ROSGAN_PUBLIC_STATS_V2__) return; 32984 32985 const AUCTION_ID = '2'; 32986 const SAME_ORIGIN_BASE = '/api/cabin-auctions'; 32987 const REMOTE_BASE = 'https://cabanasrosgan-3y52xraydq-uc.a.run.app/api/cabin-auctions'; 32988 32989 const byId = (id) => document.getElementById(id); 32990 const nf = new Intl.NumberFormat('es-AR'); 32991 32992 function escapeHtml(value){ 32993 return String(value ?? '') 32994 .replace(/&/g, '&') 32995 .replace(/</g, '<') 32996 .replace(/>/g, '>') 32997 .replace(/"/g, '"') 32998 .replace(/'/g, '''); 32999 } 33000 33001 function normalizeArray(payload){ 33002 if (Array.isArray(payload)) return payload; 33003 if (Array.isArray(payload?.items)) return payload.items; 33004 if (Array.isArray(payload?.data)) return payload.data; 33005 if (Array.isArray(payload?.results)) return payload.results; 33006 if (Array.isArray(payload?.$values)) return payload.$values; 33007 return []; 33008 } 33009 33010 async function fetchJson(path){ 33011 const targets = [SAME_ORIGIN_BASE + path, REMOTE_BASE + path]; 33012 let lastError = null; 33013 for (const url of targets){ 33014 try{ 33015 const controller = new AbortController(); 33016 const timer = setTimeout(() => controller.abort(), 12000); 33017 const response = await fetch(url, { 33018 headers:{ accept:'application/json' }, 33019 signal:controller.signal 33020 }); 33021 clearTimeout(timer); 33022 if (!response.ok) throw new Error(response.status + ' ' + response.statusText); 33023 return await response.json(); 33024 }catch(error){ 33025 lastError = error; 33026 } 33027 } 33028 throw lastError || new Error('No se pudo consultar el endpoint.'); 33029 } 33030 33031 function primaryCabana(auction){ 33032 const principales = normalizeArray(auction?.cabanasPrincipales); 33033 return principales[0] || normalizeArray(auction?.cabanas || auction?.cabins)[0] || null; 33034 } 33035 33036 function lotItems(lot){ 33037 const items = normalizeArray(lot?.items); 33038 return items.length ? items : [{ values:lot?.values || {}, displayOrder:lot?.displayOrder || lot?.order || 0 }]; 33039 } 33040 33041 function firstValues(lot){ 33042 return lotItems(lot)[0]?.values || {}; 33043 } 33044 33045 function fieldValue(values, keys){ 33046 for (const key of keys){ 33047 const value = values?.[key]; 33048 if (value !== undefined && value !== null && value !== '') return value; 33049 } 33050 return ''; 33051 } 33052 33053 function groupBy(list, getKey){ 33054 const map = new Map(); 33055 for (const item of list){ 33056 const key = getKey(item) || 'Sin dato'; 33057 map.set(key, (map.get(key) || 0) + 1); 33058 } 33059 return [...map.entries()] 33060 .map(([label,count]) => ({ label, count })) 33061 .sort((a,b) => b.count - a.count || a.label.localeCompare(b.label)); 33062 } 33063 33064 function sumItems(lots){ 33065 return lots.reduce((total, lot) => total + Math.max(1, lotItems(lot).length), 0); 33066 } 33067 33068 function completionForLot(lot){ 33069 const values = firstValues(lot); 33070 const entries = Object.values(values || {}); 33071 if (!entries.length) return 0; 33072 const filled = entries.filter((value) => value !== null && value !== undefined && value !== '').length; 33073 return filled / entries.length; 33074 } 33075 33076 function buildSummary(auction, lots){ 33077 const rpTotal = sumItems(lots); 33078 const withCover = lots.filter((lot) => !!(lot.coverImage?.url || lot.coverImageUrl || lot.imageUrl)).length; 33079 const avgCompletion = lots.length 33080 ? lots.reduce((sum, lot) => sum + completionForLot(lot), 0) / lots.length 33081 : 0; 33082 const categories = groupBy(lots, (lot) => lot.categoryName || firstValues(lot).categoria || firstValues(lot).categoryName); 33083 const cabanas = groupBy(lots, (lot) => lot.cabanaName || firstValues(lot).cabana || firstValues(lot).cabanaName); 33084 return { rpTotal, withCover, avgCompletion, categories, cabanas }; 33085 } 33086 33087 function renderKpis(summary, lots){ 33088 const categoryCount = summary.categories.length; 33089 const cabanaCount = summary.cabanas.filter((item) => item.label !== 'Sin dato').length; 33090 byId('lc-stats-kpis').innerHTML = [ 33091 ['Lotes', nf.format(lots.length), nf.format(summary.rpTotal) + ' RP cargados'], 33092 ['Toros / RP', nf.format(summary.rpTotal), 'Cabezas individuales del catalogo'], 33093 ['Categorias', nf.format(categoryCount), categoryCount === 1 ? 'Categoria disponible' : 'Categorias disponibles'], 33094 ['Cabanas', nf.format(cabanaCount), 'Participantes con lotes'] 33095 ].map(([label,value,detail]) => ` 33096 <article class="lc-stats-kpi">
33097 <span>${escapeHtml(label)}</span> 33098 <strong>${escapeHtml(value)}</strong> 33099 <em>${escapeHtml(detail)}</em> 33100 </article> 33101 `).join(''); 33102 } 33103 33104 function renderBars(targetId, groups, total){ 33105 const safeTotal = Math.max(1, total); 33106 const html = groups.length ? groups.map((item) => { 33107 const pct = Math.round(item.count / safeTotal * 100); 33108 return ` 33109 <div class="lc-stat-row"> 33110 <b>${escapeHtml(item.label)}</b> 33111 <div class="lc-stat-track"><div class="lc-stat-fill" style="width:${pct}%"></div></div> 33112 <strong>${escapeHtml(nf.format(item.count))} · ${pct}%</strong> 33113 </div> 33114 `; 33115 }).join('') : '<div class="lc-stats-mini"><b>Sin datos cargados</b><span>--</span></div>'; 33116 byId(targetId).innerHTML = html; 33117 } 33118 33119 function renderCabanaList(summary, total){ 33120 const rows = summary.cabanas.length ? summary.cabanas.map((item) => { 33121 const pct = Math.round(item.count / Math.max(1,total) * 100); 33122 return `<div class="lc-stats-mini"><b>${escapeHtml(item.label)}</b><span>${escapeHtml(nf.format(item.count))} lotes · ${pct}%</span></div>`; 33123 }).join('') : '<div class="lc-stats-mini"><b>Sin cabanas asociadas</b><span>--</span></div>'; 33124 byId('lc-stats-cabana-list').innerHTML = rows; 33125 } 33126 33127 function renderQuality(summary, lots){ 33128 const total = Math.max(1, lots.length); 33129 const rows = [ 33130 { label:'Lotes con imagen principal', count:summary.withCover }, 33131 { label:'Completitud promedio de datos', count:Math.round(summary.avgCompletion * 100), percentMode:true } 33132 ]; 33133 byId('lc-stats-quality-bars').innerHTML = rows.map((item) => { 33134 const pct = item.percentMode ? item.count : Math.round(item.count / total * 100); 33135 const label = item.percentMode ? pct + '%' : nf.format(item.count) + ' · ' + pct + '%'; 33136 return ` 33137 <div class="lc-stat-row"> 33138 <b>${escapeHtml(item.label)}</b> 33139 <div class="lc-stat-track"><div class="lc-stat-fill" style="width:${Math.max(2, pct)}%"></div></div> 33140 <strong>${escapeHtml(label)}</strong> 33141 </div> 33142 `; 33143 }).join(''); 33144 } 33145 33146 function renderTopLots(lots){ 33147 const rows = lots 33148 .slice() 33149 .sort((a,b) => Number(a.displayOrder || 9999) - Number(b.displayOrder || 9999)) 33150 .slice(0, 10) 33151 .map((lot, index) => { 33152 const values = firstValues(lot); 33153 const rp = fieldValue(values, ['rp','RP','caravana','lote']) || lot.lotCert || lot.id || '-'; 33154 return ` 33155 <tr> 33156 <td>${escapeHtml(lot.displayOrder || index + 1)}</td> 33157 <td>${escapeHtml(fieldValue(values, ['lote','Lote']) || lot.lotCert || lot.id || '-')}</td> 33158 <td>${escapeHtml(rp)}</td> 33159 <td>${escapeHtml(lot.categoryName || '-')}</td> 33160 <td>${escapeHtml(lot.cabanaName || '-')}</td> 33161 </tr> 33162 `; 33163 }).join(''); 33164 byId('lc-stats-top-lots').innerHTML = rows || '<tr><td colspan="5">No hay lotes cargados para este remate.</td></tr>'; 33165 } 33166 33167 function renderMedia(summary, lots, videoInfo){ 33168 const total = Math.max(1, lots.length); 33169 const videoLots = videoInfo?.lotsWithVideos ?? null; 33170 const videoText = videoLots === null 33171 ? 'No disponible' 33172 : nf.format(videoLots) + ' lotes · ' + Math.round(videoLots / total * 100) + '%'; 33173 byId('lc-stats-media-list').innerHTML = ` 33174 <div class="lc-stats-mini"><b>Imagen principal</b><span>${escapeHtml(nf.format(summary.withCover))} lotes · ${Math.round(summary.withCover / total * 100)}%</span></div> 33175 <div class="lc-stats-mini"><b>Videos cargados</b><span>${escapeHtml(videoText)}</span></div> 33176 <div class="lc-stats-mini"><b>Total de videos</b><span>${escapeHtml(videoInfo?.totalVideos ?? 'No disponible')}</span></div> 33177 `; 33178 } 33179 33180 async function getVideoInfo(lots){ 33181 const lotIds = lots.map((lot) => lot.id || lot.lotCert || lot.lotCertificado).filter(Boolean); 33182 if (!lotIds.length) return { lotsWithVideos:null, totalVideos:'No disponible' }; 33183 const limited = lotIds.slice(0, 80); 33184 const results = await Promise.allSettled(limited.map((id) => fetchJson(`/${AUCTION_ID}/lots/${encodeURIComponent(id)}/videos`))); 33185 let totalVideos = 0; 33186 let lotsWithVideos = 0; 33187 let ok = 0;
33188 results.forEach((result) => { 33189 if (result.status !== 'fulfilled') return; 33190 ok += 1; 33191 const videos = normalizeArray(result.value); 33192 totalVideos += videos.length; 33193 if (videos.length) lotsWithVideos += 1; 33194 }); 33195 if (!ok) return { lotsWithVideos:null, totalVideos:'No disponible' }; 33196 return { lotsWithVideos, totalVideos:nf.format(totalVideos) }; 33197 } 33198 33199 function renderLogo(auction){ 33200 const cabana = primaryCabana(auction); 33201 const logo = window.resolveCabanaMediaUrl(cabana?.logoUrl || cabana?.logoDataUrl || cabana?.whiteLogoUrl || auction?.logoUrl); 33202 const name = cabana?.nombre || cabana?.name || 'La Cassina'; 33203 byId('lc-stats-logo-card').innerHTML = logo 33204 ? `<img src="${escapeHtml(logo)}" alt="${escapeHtml(name)}">` 33205 : `<strong style="color:#0b315d;font-size:32px">${escapeHtml(name)}</strong>`; 33206 } 33207 33208 function renderAuction(auction, lots){ 33209 const summary = buildSummary(auction, lots); 33210 const title = auction?.title || auction?.nombre || 'Remate La Cassina'; 33211 byId('lc-stats-auction-title').textContent = title + ' · Tablero publico de actividad del catalogo.'; 33212 byId('lc-stats-status').textContent = 'Datos actualizados'; 33213 renderLogo(auction); 33214 renderKpis(summary, lots); 33215 renderBars('lc-stats-category-bars', summary.categories, lots.length); 33216 renderCabanaList(summary, lots.length); 33217 renderQuality(summary, lots); 33218 renderTopLots(lots); 33219 renderMedia(summary, lots, null); 33220 getVideoInfo(lots).then((videoInfo) => renderMedia(summary, lots, videoInfo)).catch(() => renderMedia(summary, lots, null)); 33221 } 33222 33223 function renderError(error){ 33224 byId('lc-stats-status').textContent = 'Error de carga'; 33225 byId('lc-stats-kpis').innerHTML = `<div class="lc-stats-error">No pudimos cargar las estadisticas del remate. ${escapeHtml(error?.message || '')}</div>`; 33226 byId('lc-stats-category-bars').innerHTML = ''; 33227 byId('lc-stats-cabana-list').innerHTML = ''; 33228 byId('lc-stats-quality-bars').innerHTML = ''; 33229 byId('lc-stats-media-list').innerHTML = ''; 33230 byId('lc-stats-top-lots').innerHTML = '<tr><td colspan="5">Sin datos disponibles.</td></tr>'; 33231 } 33232 33233 async function init(){ 33234 try{ 33235 const [auction, lotsPayload] = await Promise.all([ 33236 fetchJson(`/${AUCTION_ID}`), 33237 fetchJson(`/${AUCTION_ID}/lots/`) 33238 ]); 33239 renderAuction(auction, normalizeArray(lotsPayload)); 33240 }catch(error){ 33241 renderError(error); 33242 } 33243 } 33244 33245 if (document.readyState === 'loading'){ 33246 document.addEventListener('DOMContentLoaded', init, { once:true }); 33247 }else{ 33248 init(); 33249 } 33250})(); 33251</script>
vendor: 1 bytes, line 33251
33251
33252<script id="lacassina-public-stats-v2-script"> 33253(function(){ 33254 if (window.__ROSGAN_PUBLIC_ROUTE__ !== 'lacassina-stats') return; 33255 33256 const AUCTION_ID = '2'; 33257 const CABIN_BASES = ['/api/cabin-auctions', 'https://cabanasrosgan-3y52xraydq-uc.a.run.app/api/cabin-auctions']; 33258 const ANALYTICS_BASES = [ 33259 '/api/cabanas-analytics', 33260 'https://us-central1-rosganapp.cloudfunctions.net/cabanasAnalytics', 33261 'https://us-central1-rosganapp.cloudfunctions.net/cabanasAnalytics/api/cabanas-analytics' 33262 ]; 33263 const STATS_REFRESH_MS = 7000; 33264 const nf = new Intl.NumberFormat('es-AR'); 33265 const byId = (id) => document.getElementById(id); 33266 const clean = (value) => String(value ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); 33267 const consKey = (value) => String(value || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, ''); 33268 const normalize = (payload) => Array.isArray(payload) ? payload : Array.isArray(payload?.items) ? payload.items : Array.isArray(payload?.data) ? payload.data : Array.isArray(payload?.results) ? payload.results : Array.isArray(payload?.$values) ? payload.$values : []; 33269 const hasValue = (value) => value !== undefined && value !== null && String(value).trim() !== '' && String(value).trim() !== '-' && String(value).trim() !== '--'; 33270 const sum = (items, key) => items.reduce((acc, item) => acc + Number(item[key] || 0), 0); 33271 const pct = (value, total) => Math.round((Number(value || 0) / Math.max(1, Number(total || 0))) * 100); 33272 33273 function noCachePath(path){ 33274 const separator = String(path).includes('?') ? '&' : '?'; 33275 return `${path}${separator}_=${Date.now()}`; 33276 } 33277 33278 async function fetchFromBases(bases, path, options = {}){ 33279 let lastError = null; 33280 for (const base of bases){ 33281 let timer = null; 33282 try { 33283 const controller = new AbortController(); 33284 timer = setTimeout(() => controller.abort(), 14000); 33285 const response = await fetch(`${base}${options.noCache ? noCachePath(path) : path}`, { headers: { accept: 'application/json' }, signal: controller.signal, cache: 'no-store' }); 33286 if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); 33287 return await response.json(); 33288 } catch (error) { 33289 lastError = error; 33290 } finally { 33291 if (timer) clearTimeout(timer); 33292 } 33293 } 33294 throw lastError || new Error('No se pudo consultar el endpoint.'); 33295 } 33296 33297 function parseMetric(value){ 33298 if (typeof value === 'number' && Number.isFinite(value)) return value; 33299 if (typeof value === 'boolean') return value ? 1 : 0; 33300 if (!hasValue(value)) return null; 33301 const parsed = Number(String(value).replace(/\./g,'').replace(',','.').replace(/[^\d.+-]/g,'')); 33302 return Number.isFinite(parsed) ? parsed : null; 33303 } 33304 33305 function nestedMetric(row, key){ 33306 const sources = [row, row?.metrics, row?.totals, row?.counters, row?.data, row?.summary, row?.analytics]; 33307 for (const source of sources){ 33308 if (!source || typeof source !== 'object') continue; 33309 const parsed = parseMetric(source[key]); 33310 if (parsed !== null) return parsed; 33311 } 33312 return null; 33313 } 33314 33315 function metric(row, keys){ 33316 for (const key of keys){ 33317 const parsed = nestedMetric(row, key); 33318 if (parsed !== null) return parsed; 33319 } 33320 return 0; 33321 } 33322 33323 function dataRoot(payload){ 33324 const root = payload?.data || payload?.result || payload?.payload || payload || {}; 33325 return root?.data || root; 33326 } 33327 33328 function firstArray(data, keys){ 33329 for (const key of keys){ 33330 const value = data?.[key]; 33331 const arr = normalize(value); 33332 if (arr.length) return arr; 33333 const nested = normalize(value?.items || value?.data || value?.rows); 33334 if (nested.length) return nested; 33335 } 33336 return []; 33337 } 33338 33339 function firstObject(data, keys){ 33340 for (const key of keys){ 33341 const value = data?.[key]; 33342 if (value && typeof value === 'object' && !Array.isArray(value)) return value; 33343 } 33344 return {}; 33345 } 33346 33347 function totalFrom(summary, keys){ 33348 return metric(summary?.totals, keys) || metric(summary?.auction, keys) || metric(summary?.summary, keys); 33349 } 33350 33351 function displayMetric(value, state){
33352 const number = Number(value || 0); 33353 if (!state?.ok && !number) return '--'; 33354 return nf.format(number); 33355 } 33356 33357 function analyticsTotals(rows, summary){ 33358 const whatsappClicks = sum(rows, 'whatsappClicks') || totalFrom(summary, ['whatsappClicks','whatsapp','whatsAppClicks','whatsapp_clicks','contactWhatsapp']); 33359 const callClicks = sum(rows, 'callClicks') || totalFrom(summary, ['callClicks','calls','phoneClicks','call_clicks']); 33360 const contactClicks = sum(rows, 'contactClicks') || totalFrom(summary, ['contactClicks','contacts','contactCount']); 33361 const contacts = Math.max(contactClicks, whatsappClicks + callClicks); 33362 const shares = sum(rows, 'shares') || totalFrom(summary, ['shares','shareClicks','sharedCount','event_share_click']); 33363 const durationMs = rows.reduce((acc, row) => acc + row.durationMs, 0) || totalFrom(summary, ['viewDurationMsTotal','durationMsTotal','totalDurationMs','viewTimeMs','totalViewTimeMs']); 33364 const durationSamples = rows.reduce((acc, row) => acc + row.durationSamples, 0) || totalFrom(summary, ['viewDurationSamples','durationSamples','samples','durationCount']); 33365 return { 33366 lotViews: sum(rows, 'lotViews') || totalFrom(summary, ['lotViews','views','viewCount','totalViews','lot_view','lotViewsTotal']), 33367 rpViews: sum(rows, 'rpViews') || totalFrom(summary, ['rpViews','rpViewCount','rp_views','totalRpViews']), 33368 videoPlays: sum(rows, 'videoPlays') || totalFrom(summary, ['videoPlays','plays','playCount','totalPlays','video_play']), 33369 imageViews: sum(rows, 'imageViews') || totalFrom(summary, ['imageViews','imageViewCount','images','totalImageViews']), 33370 whatsappClicks, 33371 callClicks, 33372 contactClicks, 33373 contacts, 33374 shares, 33375 durationMs, 33376 durationSamples 33377 }; 33378 } 33379 33380 function summaryEventCount(summary){ 33381 return totalFrom(summary, ['events','eventCount','totalEvents','signals','count']) 33382 || summary.lots.length + summary.rps.length + summary.videos.length + summary.images.length + summary.daily.length; 33383 } 33384 33385 function durationLabel(ms){ 33386 const total = Math.round(Number(ms || 0) / 1000); 33387 if (!total) return 'Sin datos'; 33388 const min = Math.floor(total / 60); 33389 const sec = total % 60; 33390 return min ? `${min}m ${String(sec).padStart(2,'0')}s` : `${sec}s`; 33391 } 33392 33393 function empty(id, title, text = ''){ 33394 const node = byId(id); 33395 if (!node) return; 33396 node.innerHTML = `<div class="lc-stats-empty"><strong>${clean(title)}</strong>${text ? `<span>${clean(text)}</span>` : ''}</div>`; 33397 } 33398 33399 function itemRows(lot){ 33400 const items = normalize(lot?.items); 33401 return items.length ? items : [{ id: lot?.id, displayOrder: lot?.displayOrder || 0, values: lot?.values || {}, subforms: [] }]; 33402 } 33403 33404 function valueOf(obj, keys){ 33405 for (const key of keys){ 33406 if (hasValue(obj?.[key])) return obj[key]; 33407 } 33408 return ''; 33409 } 33410 33411 function lotMetaEntries(lots){ 33412 const entries = []; 33413 lots.forEach((lot) => { 33414 itemRows(lot).forEach((item) => { 33415 const values = item?.values || {}; 33416 const cert = String(valueOf(lot, ['lotCert','cert']) || valueOf(values, ['lote','lot']) || item?.id || lot?.id || '').trim(); 33417 const lotNumber = String(valueOf(values, ['lote','numero_lote','lotNumber','lot']) || valueOf(lot, ['lotNumber','numero_lote','lotCert','cert']) || item?.id || lot?.id || '').trim(); 33418 const rp = String(valueOf(values, ['rp','RP','caravana']) || cert).trim(); 33419 entries.push({ 33420 cert, 33421 lotNumber, 33422 rp, 33423 lot, 33424 item, 33425 title: [lot?.categoryName, rp ? `RP ${rp}` : ''].filter(Boolean).join(' · ') || lot?.categoryName || `Lote ${cert}`, 33426 category: lot?.categoryName || 'Sin categorÃa', 33427 cabana: lot?.cabanaName || 'Sin cabaña', 33428 cover: window.resolveCabanaMediaUrl(lot?.coverImage?.url || lot?.coverImageUrl) 33429 }); 33430 }); 33431 }); 33432 return entries; 33433 } 33434 33435 function buildMetaMap(lots){ 33436 const map = new Map(); 33437 lotMetaEntries(lots).forEach((meta) => { 33438 [meta.cert, meta.rp, meta.lot?.id, meta.item?.id].filter(hasValue).forEach((key) => map.set(String(key), meta)); 33439 }); 33440 return map; 33441 } 33442 33443 function matchMeta(row, map){
33444 const keys = [row?.lotCert, row?.cert, row?.lotId, row?.lotKey, row?.id, row?.rp, row?.rpKey, row?.itemId, row?.itemKey].filter(hasValue).map(String); 33445 for (const key of keys){ 33446 if (map.has(key)) return map.get(key); 33447 } 33448 return null; 33449 } 33450 33451 function normalizeSummary(payload){ 33452 const data = payload?.data || payload || {}; 33453 return { 33454 ok: data.ok !== false, 33455 lots: normalize(data.lots), 33456 rps: normalize(data.rps), 33457 videos: normalize(data.videos), 33458 images: normalize(data.images), 33459 daily: normalize(data.daily), 33460 hourly: normalize(data.hourly), 33461 source: data.source || '', 33462 totals: data.totals || null, 33463 summary: data.summary || null, 33464 auction: data.auction || null 33465 }; 33466 } 33467 33468 function aggregate(summary, lots, map){ 33469 const rows = new Map(); 33470 const ensure = (key, meta = {}) => { 33471 const id = String(key || '').trim(); 33472 if (!id) return null; 33473 if (!rows.has(id)){ 33474 rows.set(id, { 33475 key: id, 33476 cert: meta.cert || id, 33477 lotNumber: meta.lotNumber || meta.cert || id, 33478 rp: meta.rp || '', 33479 title: meta.title || `Lote ${id}`, 33480 category: meta.category || 'Sin categorÃa', 33481 cabana: meta.cabana || 'Sin cabaña', 33482 lotViews: 0, 33483 rpViews: 0, 33484 videoPlays: 0, 33485 imageViews: 0, 33486 whatsappClicks: 0, 33487 callClicks: 0, 33488 contactClicks: 0, 33489 shares: 0, 33490 durationMs: 0, 33491 durationSamples: 0 33492 }); 33493 } 33494 return rows.get(id); 33495 }; 33496 33497 lotMetaEntries(lots).forEach((meta) => ensure(meta.cert || meta.rp || meta.lot?.id, meta)); 33498 33499 const apply = (src, type) => { 33500 const meta = matchMeta(src, map) || {}; 33501 const row = ensure(meta.cert || src.lotCert || src.cert || src.lotId || src.id || src.rp || src.itemId, meta); 33502 if (!row) return; 33503 Object.assign(row, { 33504 title: meta.title || row.title, 33505 category: meta.category || row.category, 33506 cabana: meta.cabana || row.cabana, 33507 rp: meta.rp || row.rp, 33508 cert: meta.cert || row.cert, 33509 lotNumber: meta.lotNumber || row.lotNumber || meta.cert || row.cert 33510 }); 33511 if (type === 'lot') { 33512 row.lotViews += metric(src, ['lotViews','views','viewCount']); 33513 row.whatsappClicks += metric(src, ['whatsappClicks','whatsapp','whatsAppClicks']); 33514 row.callClicks += metric(src, ['callClicks','calls']); 33515 row.contactClicks += metric(src, ['contactClicks','contacts']); 33516 } else if (type === 'rp') { 33517 row.rpViews += metric(src, ['rpViews','views','viewCount']); 33518 row.shares += metric(src, ['shares','shareClicks','sharedCount','event_share_click']); 33519 row.durationMs += metric(src, ['viewDurationMsTotal','durationMsTotal','totalDurationMs','viewTimeMs']); 33520 row.durationSamples += metric(src, ['viewDurationSamples','durationSamples','samples']); 33521 } else if (type === 'video') { 33522 row.videoPlays += metric(src, ['videoPlays','plays','playCount','count']); 33523 } else if (type === 'image') { 33524 row.imageViews += metric(src, ['imageViews','views','viewCount','count']); 33525 } 33526 }; 33527 33528 summary.lots.forEach((x) => apply(x, 'lot')); 33529 summary.rps.forEach((x) => apply(x, 'rp')); 33530 summary.videos.forEach((x) => apply(x, 'video')); 33531 summary.images.forEach((x) => apply(x, 'image')); 33532 return Array.from(rows.values()); 33533 } 33534 33535 function renderHeader(auction, lots, summary, state){ 33536 const title = auction?.title || summary.auction?.title || 'La Cassina'; 33537 const cabins = normalize(auction?.cabins || auction?.cabanas); 33538 const configuredPrincipals = normalize(auction?.cabanasPrincipales); 33539 const configuredGuests = normalize(auction?.cabanasInvitadas); 33540 const principals = configuredPrincipals.length ? configuredPrincipals : cabins.filter((cabana) => String(cabana?.type || '').toLowerCase() === 'principal'); 33541 const guests = configuredGuests.length ? configuredGuests : cabins.filter((cabana) => String(cabana?.type || '').toLowerCase() !== 'principal');
33542 const principal = principals[0] || cabins[0] || {}; 33543 const logoFor = (cabana) => window.resolveCabanaMediaUrl(cabana?.logoUrl || cabana?.logoDataUrl || cabana?.logo || cabana?.imageUrl); 33544 const nameFor = (cabana) => cabana?.nombre || cabana?.name || cabana?.title || 'Cabana'; 33545 const logo = logoFor(principal); 33546 const titleNode = byId('lc-stats-auction-title'); 33547 if (titleNode) titleNode.textContent = title; 33548 const logoNode = byId('lc-stats-logo-card'); 33549 if (logoNode) { 33550 const guestLogos = guests 33551 .filter((cabana) => nameFor(cabana) || logoFor(cabana)) 33552 .slice(0, 6) 33553 .map((cabana) => { 33554 const guestLogo = logoFor(cabana); 33555 const guestName = nameFor(cabana); 33556 return `<div class="lc-stats-guest-logo">${guestLogo ? `<img src="${clean(guestLogo)}" alt="${clean(guestName)}">` : `<span>${clean(guestName)}</span>`}</div>`; 33557 }).join(''); 33558 logoNode.innerHTML = ` 33559 <span class="lc-stats-logo-label">Cabana principal</span> 33560 ${logo ? `<img class="lc-stats-main-logo" src="${clean(logo)}" alt="${clean(nameFor(principal))}">` : `<strong>${clean(nameFor(principal) || title)}</strong>`} 33561 ${guestLogos ? `<div class="lc-stats-guest-logos">${guestLogos}</div>` : ''} 33562 `; 33563 } 33564 const eventCount = summaryEventCount(summary); 33565 const statusNode = byId('lc-stats-status'); 33566 const updated = state.updatedAt ? new Intl.DateTimeFormat('es-AR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(state.updatedAt) : ''; 33567 if (statusNode) statusNode.innerHTML = `<span>${nf.format(lots.length)} lotes publicados</span><strong>${state.ok ? `${nf.format(eventCount)} señales registradas` : 'Analytics sin respuesta'}</strong>${updated ? `<small>Actualizado ${clean(updated)}</small>` : ''}${state.error ? `<small>${clean(state.error)}</small>` : ''}`; 33568 } 33569 33570 function renderKpis(rows, state, summary){ 33571 const totals = analyticsTotals(rows, summary); 33572 const lotViews = totals.lotViews; 33573 const rpViews = totals.rpViews; 33574 const videoPlays = totals.videoPlays; 33575 const imageViews = totals.imageViews; 33576 const contacts = totals.contacts; 33577 const cards = [ 33578 ['Visitas de lotes', lotViews, state.ok ? 'Aperturas de ficha y catálogo' : 'Esperando analytics'], 33579 ['Vistas por RP', rpViews, 'Interacciones con reproductores'], 33580 ['Plays de video', videoPlays, 'Reproducciones registradas'], 33581 ['Vistas de imagen', imageViews, 'GalerÃa y portadas'], 33582 ['Compartidos', totals.shares, 'Links compartidos o copiados'], 33583 ['Tiempo prom. RP', durationLabel(totals.durationSamples ? totals.durationMs / totals.durationSamples : 0), totals.durationSamples ? `${nf.format(totals.durationSamples)} visitas medidas` : 'Sin muestras'], 33584 ['Contactos', contacts, 'WhatsApp, llamada y contacto'] 33585 ]; 33586 const node = byId('lc-stats-kpis'); 33587 if (node) node.innerHTML = cards.map(([label, value, hint]) => `<article class="lc-stat-card"><span>${clean(label)}</span><strong>${typeof value === 'number' ? nf.format(value) : clean(value)}</strong><small>${clean(hint)}</small></article>`).join(''); 33588 } 33589 33590 function barRows(id, rows){ 33591 const node = byId(id); 33592 if (!node) return; 33593 const filtered = rows.filter(([, value]) => Number(value) > 0); 33594 if (!filtered.length){
33595 empty(id, 'Sin señales todavÃa', 'No hay eventos suficientes para este módulo.'); 33596 return; 33597 } 33598 const max = Math.max(1, ...filtered.map(([, value]) => Number(value))); 33599 node.innerHTML = filtered.map(([label, value]) => `<div class="lc-stats-bar-row"><span>${clean(label)}</span><div><i style="width:${pct(value, max)}%"></i></div><strong>${nf.format(value)}</strong></div>`).join(''); 33600 } 33601 33602 function groupBy(rows, key, valueFn, id){ 33603 const map = new Map(); 33604 rows.forEach((row) => { 33605 const label = row[key] || 'Sin dato'; 33606 map.set(label, (map.get(label) || 0) + valueFn(row)); 33607 }); 33608 barRows(id, Array.from(map.entries()).sort((a, b) => b[1] - a[1]).slice(0, 8)); 33609 } 33610 33611 function renderInsights(rows, lots, summary){ 33612 const totals = analyticsTotals(rows, summary); 33613 const totalViews = totals.lotViews + totals.rpViews; 33614 const contacts = totals.contacts; 33615 const active = rows.filter((row) => row.lotViews || row.rpViews || row.videoPlays || row.imageViews || row.shares).length; 33616 const top = active ? rows.slice().sort((a, b) => (b.lotViews + b.rpViews + b.videoPlays + b.shares) - (a.lotViews + a.rpViews + a.videoPlays + a.shares))[0] : null; 33617 const topLotLabel = top ? (top.lotNumber || top.cert || top.lotId || '').trim() : ''; 33618 const topRpLabel = top?.rp ? `RP ${top.rp}` : ''; 33619 const topCategory = top?.category || top?.title || ''; 33620 const topDetail = [topRpLabel, topCategory].filter(Boolean).join(' - '); 33621 const insights = byId('lc-stats-insights'); 33622 if (insights) insights.innerHTML = [ 33623 ['Lote destacado', topLotLabel ? `Lote ${topLotLabel}` : 'Sin lote lider aun', topDetail || 'Se completa con las primeras senales'], 33624 ['Cobertura activa', `${nf.format(active)} / ${nf.format(lots.length)}`, 'Lotes con visitas, videos o imagenes'], 33625 ['Contactos', contacts ? nf.format(contacts) : 'Sin datos', contacts ? 'Consultas registradas desde el catalogo' : 'Esperando WhatsApp o llamadas'] 33626 ].map(([label, value, detail]) => `<article class="lc-stats-insight"><span>${clean(label)}</span><strong>${clean(value)}</strong><em>${clean(detail)}</em></article>`).join(''); 33627 } 33628 33629 function renderFunnelCounts(rows, summary){ 33630 const totals = analyticsTotals(rows, summary); 33631 const cards = [ 33632 ['Vistas lote', totals.lotViews, 'Aperturas de ficha'], 33633 ['RPs vistos', totals.rpViews, 'RPs efectivamente abiertos'], 33634 ['Videos', totals.videoPlays, 'Play real del reproductor'], 33635 ['Compartidos', totals.shares, 'Links enviados o copiados'], 33636 ['Contactos', totals.contacts, 'WhatsApp y llamadas'] 33637 ]; 33638 const node = byId('lc-stats-funnel'); 33639 if (node) node.innerHTML = cards.map(([label, value, hint]) => `<article class="lc-stats-count-card"><span>${clean(label)}</span><strong>${nf.format(value)}</strong><small>${clean(hint)}</small></article>`).join(''); 33640 } 33641 33642 function timelineValue(row){ 33643 return metric(row, ['lotViews']) + metric(row, ['rpViews']) + metric(row, ['videoPlays']) + metric(row, ['imageViews']) + metric(row, ['contactClicks']) + metric(row, ['shares','shareClicks','event_share_click']); 33644 } 33645 33646 function timelineLabel(key, mode){ 33647 const text = String(key || ''); 33648 if (mode === 'semana') return text.slice(5, 10).replace('-', '/'); 33649 const hour = text.match(/T(\d{2})/)?.[1]; 33650 return hour ? `${hour}h` : text.slice(5, 10).replace('-', '/'); 33651 } 33652 33653 function renderTimeline(summary, mode = window.__lcStatsRange || 'dia'){ 33654 window.__lcStatsRange = mode; 33655 const tabs = byId('lc-stats-range-tabs'); 33656 if (tabs) { 33657 const options = [['dia', 'Dia'], ['semana', 'Semana'], ['horas', 'Ultimas horas']]; 33658 tabs.innerHTML = options.map(([key, label]) => `<button type="button" class="${key === mode ? 'is-active' : ''}" data-lc-range="${key}">${label}</button>`).join(''); 33659 tabs.querySelectorAll('button').forEach((button) => { 33660 button.addEventListener('click', () => renderTimeline(summary, button.dataset.lcRange)); 33661 }); 33662 } 33663 const hourly = normalize(summary.hourly); 33664 const daily = normalize(summary.daily); 33665 let rows = []; 33666 if (mode === 'semana') rows = daily.slice(-7).map((row) => ({ key: row.dayKey || row.date || row.day, row })); 33667 else rows = (hourly.length ? hourly : daily).slice(mode === 'horas' ? -12 : -24).map((row) => ({ key: row.hourKey || row.dayKey || row.date || row.day, row })); 33668 rows = rows.filter((item) => item.key); 33669 const node = byId('lc-stats-timeline'); 33670 if (!node) return; 33671 if (!rows.length){ 33672 empty('lc-stats-timeline', 'Sin serie temporal todavia', 'Los eventos nuevos ya van a guardarse por hora y por dia.'); 33673 return; 33674 } 33675 const values = rows.map((item) => timelineValue(item.row)); 33676 const max = Math.max(1, ...values); 33677 const width = 640; 33678 const height = 240; 33679 const padX = 42; 33680 const padTop = 32; 33681 const padBottom = 44; 33682 const plotW = width - padX * 2; 33683 const plotH = height - padTop - padBottom; 33684 const points = values.map((value, index) => { 33685 const x = rows.length === 1 ? width / 2 : padX + (plotW *
33685index / Math.max(1, rows.length - 1)); 33686 const y = padTop + plotH - (plotH * (value / max)); 33687 return { x, y, value, label: timelineLabel(rows[index].key, mode) }; 33688 }); 33689 const linePath = points.map((point, index) => `${index ? 'L' : 'M'} ${point.x.toFixed(1)} ${point.y.toFixed(1)}`).join(' '); 33690 const areaPath = points.length 33691 ? `${linePath} L ${points[points.length - 1].x.toFixed(1)} ${height - padBottom} L ${points[0].x.toFixed(1)} ${height - padBottom} Z` 33692 : ''; 33693 const totals = rows.reduce((acc, item) => { 33694 acc.events += timelineValue(item.row); 33695 acc.views += metric(item.row, ['lotViews']) + metric(item.row, ['rpViews']); 33696 acc.videos += metric(item.row, ['videoPlays']); 33697 acc.contacts += metric(item.row, ['contactClicks']); 33698 acc.shares += metric(item.row, ['shares','shareClicks','event_share_click']); 33699 return acc; 33700 }, { events: 0, views: 0, videos: 0, contacts: 0, shares: 0 }); 33701 node.innerHTML = ` 33702 <div class="lc-stats-chart"> 33703 <svg class="lc-stats-line-chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="Evolucion de señales"> 33704 <defs> 33705 <linearGradient id="lcStatsLineArea" x1="0" x2="0" y1="0" y2="1"> 33706 <stop offset="0%" stop-color="#18b8ad" stop-opacity=".30"></stop> 33707 <stop offset="100%" stop-color="#0b67b2" stop-opacity=".04"></stop> 33708 </linearGradient> 33709 </defs> 33710 <line class="lc-stats-line-grid" x1="${padX}" x2="${width - padX}" y1="${padTop}" y2="${padTop}"></line> 33711 <line class="lc-stats-line-grid" x1="${padX}" x2="${width - padX}" y1="${padTop + plotH / 2}" y2="${padTop + plotH / 2}"></line> 33712 <line class="lc-stats-line-grid" x1="${padX}" x2="${width - padX}" y1="${height - padBottom}" y2="${height - padBottom}"></line> 33713 ${areaPath ? `<path class="lc-stats-line-area" d="${areaPath}"></path>` : ''} 33714 ${linePath ? `<path class="lc-stats-line-path" d="${linePath}"></path>` : ''} 33715 ${points.map((point) => `<g><circle class="lc-stats-line-dot" cx="${point.x.toFixed(1)}" cy="${point.y.toFixed(1)}" r="5"></circle><text class="lc-stats-line-value" x="${point.x.toFixed(1)}" y="${Math.max(16, point.y - 12).toFixed(1)}">${nf.format(point.value)}</text><text class="lc-stats-line-label" x="${point.x.toFixed(1)}" y="${height - 16}">${clean(point.label)}</text></g>`).join('')} 33716 </svg> 33717 </div> 33718 <div class="lc-stats-chart-summary"> 33719 <div><span>Vistas</span><strong>${nf.format(totals.views)}</strong></div> 33720 <div><span>Videos</span><strong>${nf.format(totals.videos)}</strong></div> 33721 <div><span>Contactos</span><strong>${nf.format(totals.contacts)}</strong></div> 33722 <div><span>Compartidos</span><strong>${nf.format(totals.shares)}</strong></div> 33723 </div> 33724 `; 33725 } 33726 33727 function renderAlerts(rows, state, summary){ 33728 const totals = analyticsTotals(rows, summary); 33729 const contacts = totals.contacts; 33730 const videoPlays = totals.videoPlays; 33731 const imageViews = totals.imageViews; 33732 const alerts = []; 33733 if (!state.ok) alerts.push(['warning', 'Cloud Function sin respuesta', state.error || 'El panel queda preparado hasta que el endpoint responda.']); 33734 alerts.push([contacts ? 'good' : 'info', 'Contactos', contacts ? `${nf.format(contacts)} consultas registradas.` : 'Sin clics de WhatsApp o llamada todavÃa.']); 33735 alerts.push([videoPlays ? 'good' : 'info', 'Videos', videoPlays ? `${nf.format(videoPlays)} reproducciones acumuladas.` : 'Sin plays registrados todavÃa.']); 33736 alerts.push([imageViews ? 'good' : 'info', 'Imágenes', imageViews ? `${nf.format(imageViews)} visualizaciones de imagen.` : 'Sin vistas de imagen todavÃa.']); 33737 const node = byId('lc-stats-alerts'); 33738 if (node) node.innerHTML = alerts.map(([type, title, text]) => { 33739 const iconClass = type === 'good' ?'dc-lucide-check' : type === 'warning' ?'dc-lucide-warning' : 'dc-lucide-info'; 33740 return `<div class="lc-stats-alert ${type === 'good' ? 'is-good' : ''}"><i><span class="dc-lucide ${iconClass}" aria-hidden="true"></span></i><div><strong>${clean(title)}</strong><span>${clean(text)}</span></div></div>`; 33741 }).join(''); 33742 } 33743 33744 function renderCatalogQuality(lots){ 33745 const entries = lotMetaEntries(lots); 33746 if (!entries.length){ 33747 empty('lc-stats-quality-bars', 'Sin lotes publicados', 'La cobertura se calcula cuando existe catalogo.'); 33748 return; 33749 } 33750 const hasMedia = (meta) => { 33751 const lot = meta.lot || {}; 33752 const item = meta.item || {}; 33753 return Boolean( 33754 meta.cover || 33755 lot.coverImageUrl || 33756 lot.imageUrl || 33757 lot.thumbnailUrl || 33758 normalize(lot.images).length || 33759 normalize(lot.videos).length || 33760 normalize(item.images).length || 33761 normalize(item.videos).length 33762 ); 33763 }; 33764 const completion = (meta) => { 33765 const values = meta.item?.values || meta.lot?.values || {}; 33766 const fields = Object.entries(values).filter(([key]) => !['id','displayOrder'].includes(key)); 33767 if (!fields.length) return 0; 33768 return fields.filter(([, value]) => hasValue(value)).length / fields.length; 33769 }; 33770 const mediaCount = entries.filter(hasMedia).length; 33771 const withData = entries.filter((meta) => Object.values(meta.item?.values || meta.lot?.values || {}).some(hasValue)).length; 33772 const completeness = Math.round(entries.reduce((acc, meta) => acc + completion(meta), 0) / entries.length * 100); 33773 const rows = [ 33774 ['RPs con imagen/video', pct(mediaCount, entries.length), `${nf.format(mediaCount)} de ${nf.format(entries.length)}`], 33775 ['RPs con datos tecnicos', pct(withData, entries.length), `${nf.format(withData)} de ${nf.format(entries.length)}`], 33776 ['Completitud promedio', completeness, `${nf.format(completeness)}%`] 33777 ]; 33778 const node = byId('lc-stats-quality-bars'); 33779 if (node) node.innerHTML = rows.map(([label, value, detail]) => `<div class="lc-stats-bar-row"><span>${clean(label)}</span><div><i style="width:${Math.max(2, Math.min(100, value))}%"></i></div><strong>${clean(detail)}</strong></div>`).join(''); 33780 } 33781 33782 function renderCatalogAttributes(lots){ 33783 const entries = lotMetaEntries(lots); 33784 const node = byId('lc-stats-genetics-list'); 33785 if (!node) return; 33786 if (!entries.length){ 33787 empty('lc-stats-genetics-list', 'Sin datos de catalogo', 'No hay atributos publicados para leer.'); 33788 return; 33789 } 33790 const valueOf = (meta, keys) => { 33791 const values = meta.item?.values || meta.lot?.values || {}; 33792 for (const key of keys) { 33793 const hit = Object.keys(values).find((candidate) => consKey(candidate) === consKey(key)); 33794 const value = values[key] ?? (hit ? values[hit] : undefined); 33795 if (hasValue(value)) return value; 33796 } 33797 return ''; 33798 }; 33799 const toNumber = (value) => { 33800 const number = Number(String(value || '').replace(',', '.').replace(/[^\d.-]/g, '')); 33801 return Number.isFinite(number) ? number : null; 33802 }; 33803 const average = (keys) => { 33804 const values = entries.map((meta) => toNumber(valueOf(meta, keys))).filter((value) => value !== null); 33805 return values.length ? Math.round(values.reduce((a, b) => a + b, 0) / values.length) : null; 33806 }; 33807 const countBy = (keys) => { 33808 const map = new Map();
33809 entries.forEach((meta) => { 33810 const value = String(valueOf(meta, keys) || '').trim(); 33811 if (!value) return; 33812 map.set(value, (map.get(value) || 0) + 1); 33813 }); 33814 return [...map.entries()].sort((a, b) => b[1] - a[1])[0] || null; 33815 }; 33816 const topFather = countBy(['padre', 'father', 'sire']); 33817 const categories = new Set(entries.map((meta) => meta.category).filter(hasValue)); 33818 const avgWeight = average(['peso_actual', 'p_a', 'peso', 'pesoPromedio']); 33819 const avgCe = average(['ce', 'circunferencia_escrotal']); 33820 const cards = [ 33821 ['RP publicados', entries.length, 'Animales leidos del catalogo'], 33822 ['Peso promedio', avgWeight ? `${nf.format(avgWeight)} kg` : 'Sin dato', 'Sobre RPs con peso cargado'], 33823 ['CE promedio', avgCe ? `${nf.format(avgCe)} cm` : 'Sin dato', 'Sobre RPs con CE cargada'], 33824 ['Padre mas repetido', topFather ? topFather[0] : 'Sin dato', topFather ? `${nf.format(topFather[1])} RPs` : 'Sin padre cargado'], 33825 ['Categorias', categories.size, 'Renglones comerciales'] 33826 ]; 33827 node.innerHTML = `<div class="lc-stats-attribute-grid">${cards.map(([label, value, hint]) => `<article class="lc-stats-attribute-card"><span>${clean(label)}</span><strong>${typeof value === 'number' ? nf.format(value) : clean(value)}</strong><small>${clean(hint)}</small></article>`).join('')}</div>`; 33828 } 33829 33830 function renderMedia(summary, metaMap){ 33831 const list = [ 33832 ...summary.videos.map((row) => ['Video', row, metric(row, ['videoPlays','plays','count'])]), 33833 ...summary.images.map((row) => ['Imagen', row, metric(row, ['imageViews','views','count'])]) 33834 ].filter(([, , value]) => value > 0).sort((a, b) => b[2] - a[2]).slice(0, 10); 33835 if (!list.length){ 33836 const node = byId('lc-stats-media-list'); 33837 if (node) node.dataset.mediaSignature = ''; 33838 empty('lc-stats-media-list', 'Sin métricas multimedia', 'TodavÃa no hay plays o vistas de imágenes.'); 33839 return; 33840 } 33841 const node = byId('lc-stats-media-list'); 33842 const mediaUrl = (row, isVideo) => { 33843 const meta = matchMeta(row, metaMap) || {}; 33844 const raw = row.thumbnailUrl || row.posterUrl || row.thumb || row.imageUrl || row.imageId || row.videoId || row.mediaId || ''; 33845 const text = String(raw || ''); 33846 if (!isVideo && meta.cover && (!text || (text.includes('firebasestorage.googleapis.com') && !text.includes('alt=media')))) return meta.cover; 33847 return window.resolveCabanaMediaUrl(text || meta.cover); 33848 }; 33849 const mediaName = (row) => { 33850 const raw = row.fileName || row.lotTitle || row.rp || row.lotCert || row.videoId || row.mediaId || 'Material'; 33851 return String(raw).startsWith('http') ? decodeURIComponent(String(raw).split('/').pop() || 'Material') : raw; 33852 }; 33853 const mediaTarget = (row) => { 33854 const meta = matchMeta(row, metaMap) || {}; 33855 const rp = row.rp || meta.rp || ''; 33856 const lot = meta.lotNumber || meta.cert || row.lotCert || row.cert || ''; 33857 return lot && rp ? `/lacassina/${encodeURIComponent(String(lot).toLowerCase())}/${encodeURIComponent(String(rp).toLowerCase())}` : '/lacassina'; 33858 }; 33859 if (node) { 33860 node.classList.add('lc-stats-media-grid'); 33861 const items = list.map(([kind, row, value]) => { 33862 const isVideo = String(kind).toLowerCase() === 'video' || String(row.mediaType || '').toLowerCase() === 'video'; 33863 const url = mediaUrl(row, isVideo); 33864 const label = row.rp ? `RP ${row.rp}` : (row.lotCert ? `Lote ${row.lotCert}` : mediaName(row)); 33865 const thumb = url 33866 ? `<div class="lc-stats-media-thumb">${isVideo ? `<video src="${clean(url)}" muted playsinline preload="metadata"></video>` : `<img src="${clean(url)}" alt="${clean(label)}">`}</div>` 33867 : `<div class="lc-stats-media-thumb"></div>`; 33868 return { 33869 signature: [kind, url, label, row.categoryName || row.cabanaName || mediaName(row), mediaTarget(row)].join('|'), 33870 value, 33871 html: `<a class="lc-stats-media-card" href="${clean(mediaTarget(row))}">${thumb}<div class="lc-stats-media-copy"><span>${clean(kind)}</span><strong>${clean(label)}</strong><small>${clean(row.categoryName || row.cabanaName || mediaName(row))}</small></div><strong data-lc-media-value>${nf.format(value)}</strong></a>` 33872 }; 33873 }); 33874 const signature = items.map((item) => item.signature).join('||'); 33875 if (node.dataset.mediaSignature === signature) { 33876 node.querySelectorAll('[data-lc-media-value]').forEach((valueNode, index) => { 33877 valueNode.textContent = nf.format(items[index]?.value || 0
33877); 33878 }); 33879 return; 33880 } 33881 node.dataset.mediaSignature = signature; 33882 node.innerHTML = items.map((item) => item.html).join(''); 33883 } 33884 } 33885 33886 function renderDaily(summary){ 33887 const daily = summary.daily.slice().sort((a, b) => String(a.date || a.day || '').localeCompare(String(b.date || b.day || ''))).slice(-10); 33888 if (!daily.length){ 33889 empty('lc-stats-traffic-list', 'Sin serie diaria', 'La Cloud Function todavÃa no devolvió datos por dÃa.'); 33890 return; 33891 } 33892 barRows('lc-stats-traffic-list', daily.map((row) => [row.date || row.day || 'DÃa', metric(row, ['events','count','views','total'])])); 33893 } 33894 33895 function renderTopLots(rows){ 33896 const active = rows 33897 .map((row) => ({ ...row, total: row.lotViews + row.rpViews + row.videoPlays + row.imageViews + row.whatsappClicks + row.callClicks + row.contactClicks + row.shares })) 33898 .filter((row) => row.total > 0) 33899 .sort((a, b) => b.total - a.total) 33900 .slice(0, 12); 33901 if (!active.length){ 33902 const node = byId('lc-stats-top-lots'); 33903 if (node) node.innerHTML = '<tr><td colspan="8"><div class="lc-stats-empty"><strong>Sin ranking de lotes</strong><span>TodavÃa no se registraron visitas o contactos.</span></div></td></tr>'; 33904 return; 33905 } 33906 const node = byId('lc-stats-top-lots'); 33907 if (!node) return; 33908 node.innerHTML = active.map((row) => { 33909 const contacts = Math.max(row.contactClicks, row.whatsappClicks + row.callClicks); 33910 const avg = row.durationSamples ? row.durationMs / row.durationSamples : 0; 33911 const lotLabel = row.lotNumber || row.cert || '-'; 33912 const rpLabel = row.rp ? `RP ${row.rp}` : 'RP sin dato'; 33913 const durationCell = row.durationSamples 33914 ? `${clean(durationLabel(avg))}<br><small>${nf.format(row.durationSamples)} ${row.durationSamples === 1 ? 'visita medida' : 'visitas medidas'}</small>` 33915 : 'Sin datos'; 33916 return `<tr><td><strong>Lote ${clean(lotLabel)}</strong><br><small>${clean(rpLabel)} - ${clean(row.cabana)}</small></td><td>${clean(row.category)}</td><td>${nf.format(row.lotViews)}</td><td>${nf.format(row.rpViews)}</td><td>${nf.format(row.videoPlays)}</td><td>${nf.format(contacts)}</td><td>${nf.format(row.shares)}</td><td>${durationCell}</td></tr>`; 33917 }).join(''); 33918 } 33919 33920 let lcStatsRefreshInFlight = false; 33921 33922 async function boot(options = {}){ 33923 const silent = !!options.silent; 33924 if (lcStatsRefreshInFlight) return; 33925 lcStatsRefreshInFlight = true; 33926 try { 33927 if (!silent) { 33928 [ 33929 'lc-stats-kpis','lc-stats-insights','lc-stats-timeline','lc-stats-funnel', 33930 'lc-stats-category-bars','lc-stats-cabana-list', 33931 'lc-stats-media-list','lc-stats-genetics-list' 33932 ].forEach((id) => { 33933 const node = byId(id); 33934 if (node) node.innerHTML = '<div class="lc-stats-loader"><span></span><span></span><span></span></div>'; 33935 }); 33936 const top = byId('lc-stats-top-lots'); 33937 if (top) top.innerHTML = '<tr><td colspan="8"><div class="lc-stats-loader"><span></span><span></span><span></span></div></td></tr>'; 33938 } 33939 33940 let auction = null; 33941 let lots = []; 33942 let summary = { ok: false, lots: [], rps: [], videos: [], images: [], daily: [], hourly: [] }; 33943 const state = { ok: false, error: '', updatedAt: new Date() }; 33944 33945 try { 33946 auction = await fetchFromBases(CABIN_BASES, `/${AUCTION_ID}`, { noCache: true }); 33947 lots = normalize(await fetchFromBases(CABIN_BASES, `/${AUCTION_ID}/lots/`, { noCache: true })); 33948 } catch (error) { 33949 const status = byId('lc-stats-status'); 33950 if (status) status.innerHTML = `<strong>No se pudo cargar La Cassina</strong><small>${clean(error.message)}</small>`; 33951 return; 33952 } 33953 33954 try { 33955 summary = normalizeSummary(await fetchFromBases(ANALYTICS_BASES, `/summary?auctionId=${encodeURIComponent(AUCTION_ID)}&limit=250`, { noCache: true })); 33956 state.ok = true; 33957 } catch (error) { 33958 state.error = 'Analytics no disponible todavia'; 33959 } 33960 33961 const metaMap = buildMetaMap(lots); 33962 const rows = aggregate(summary, lots, metaMap); 33963 renderHeader(auction, lots, summary, state); 33964 renderKpis(rows, state, summary); 33965 renderInsights(rows, lots, summary); 33966 renderTimeline(summary);
33967 renderFunnelCounts(rows, summary); 33968 const hasSignals = rows.some((row) => row.lotViews || row.rpViews || row.videoPlays || row.imageViews || row.whatsappClicks || row.callClicks || row.contactClicks); 33969 const demandValue = (row) => row.lotViews + row.rpViews + row.videoPlays + row.imageViews; 33970 groupBy(rows, 'category', hasSignals ? demandValue : () => 1, 'lc-stats-category-bars'); 33971 groupBy(rows, 'cabana', hasSignals ? demandValue : () => 1, 'lc-stats-cabana-list'); 33972 renderMedia(summary, metaMap); 33973 renderCatalogAttributes(lots); 33974 renderTopLots(rows); 33975 } finally { 33976 lcStatsRefreshInFlight = false; 33977 } 33978 } 33979 33980 function scheduleStatsRealtime(){ 33981 if (window.__lcStatsRealtimeTimer) clearInterval(window.__lcStatsRealtimeTimer); 33982 window.__lcStatsRealtimeTimer = setInterval(() => { 33983 if (!document.hidden) boot({ silent: true }).catch((error) => console.warn('lacassina stats refresh failed', error)); 33984 }, STATS_REFRESH_MS); 33985 document.addEventListener('visibilitychange', () => { 33986 if (!document.hidden) boot({ silent: true }).catch((error) => console.warn('lacassina stats refresh failed', error)); 33987 }); 33988 } 33989 33990 boot().then(scheduleStatsRealtime).catch((error) => { 33991 console.error('lacassina stats failed', error); 33992 const status = byId('lc-stats-status'); 33993 if (status) status.innerHTML = `<strong>Error cargando estadÃsticas</strong><small>${clean(error.message)}</small>`; 33994 }); 33995})(); 33996</script>
vendor: 1 bytes, line 33996
33996
33997<script> 33998(function setupPrebidFollower(){ 33999 var frame = 0; 34000 34001 function sync(){ 34002 frame = 0; 34003 var topOffset = window.innerWidth <= 760 ? 72 : 68; 34004 var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 34005 var hasVisibleLoadFab = Array.prototype.some.call( 34006 document.querySelectorAll('.app-mobile-load-fab'), 34007 function(fab){ 34008 var style = window.getComputedStyle(fab); 34009 return fab.getClientRects().length > 0 && 34010 style.display !== 'none' && 34011 style.visibility !== 'hidden'; 34012 } 34013 ); 34014 document.querySelectorAll('.dc-prebid-side-card').forEach(function(card){ 34015 var button = card.querySelector('.dc-prebid-side-button'); 34016 if (!button) return; 34017 var visibleCard = card.getClientRects().length > 0 && window.getComputedStyle(card).display !== 'none'; 34018 var shouldFloat = false; 34019 if (visibleCard) { 34020 var rect = card.getBoundingClientRect(); 34021 shouldFloat = rect.bottom <= topOffset; 34022 } 34023 var wasFloating = button.classList.contains('is-scroll-floating'); 34024 var wasAboveLoadFab = button.classList.contains('is-above-load-fab'); 34025 var positionChanged = wasFloating !== shouldFloat || wasAboveLoadFab !== hasVisibleLoadFab; 34026 var oldRect = positionChanged ? button.getBoundingClientRect() : null; 34027 34028 if (positionChanged && button._prebidPositionAnimation) { 34029 button._prebidPositionAnimation.cancel(); 34030 button._prebidPositionAnimation = null; 34031 button.classList.remove('is-position-transitioning'); 34032 } 34033 button.classList.remove('is-mobile-floating'); 34034 if (positionChanged) button.classList.add('is-position-transitioning'); 34035 button.classList.toggle('is-scroll-floating', shouldFloat); 34036 button.classList.toggle('is-above-load-fab', hasVisibleLoadFab); 34037 34038 if (!positionChanged) return; 34039 var newRect = button.getBoundingClientRect(); 34040 if (reduceMotion || !button.animate) { 34041 button.classList.remove('is-position-transitioning'); 34042 return; 34043 } 34044 34045 var dx = oldRect.left - newRect.left; 34046 var dy = oldRect.top - newRect.top; 34047 var distance = Math.sqrt(dx * dx + dy * dy); 34048 var startScaleX = newRect.width ? oldRect.width / newRect.width : 1; 34049 var startScaleY = newRect.height ? oldRect.height / newRect.height : 1; 34050 var transitionDuration = Math.min(620, Math.max(400, 350 + distance * .28)); 34051 34052 var animation = button.animate([ 34053 { transform:'translate3d(' + dx + 'px,' + dy + 'px,0) scale(' + startScaleX + ',' + startScaleY + ')', opacity:.92 }, 34054 { transform:'translate3d(0,0,0) scale(1,1)', opacity:1 } 34055 ], { 34056 duration:transitionDuration, 34057 easing:'cubic-bezier(.22,1,.36,1)', 34058 fill:'both' 34059 }); 34060 button._prebidPositionAnimation = animation; 34061 animation.onfinish = animation.oncancel = function(){ 34062 if (button._prebidPositionAnimation !== animation) return; 34063 button._prebidPositionAnimation = null; 34064 button.classList.remove('is-position-transitioning'); 34065 }; 34066 }); 34067 } 34068 34069 function schedule(){ 34070 if (frame) return; 34071 frame = window.requestAnimationFrame(sync); 34072 } 34073 34074 window.addEventListener('scroll', schedule, { passive:true }); 34075 window.addEventListener('resize', schedule, { passive:true }); 34076 window.addEventListener('orientationchange', schedule, { passive:true }); 34077 new MutationObserver(schedule).observe(document.body, { childList:true, subtree:true }); 34078 schedule(); 34079})(); 34080</script>
34080 34081</body> 34082</html>
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.