(function() { 'use strict'; class LocutoresWidget extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.voices = []; this.categories = []; this.selectedCategory = 'all'; this.playingId = null; this.visibleCount = 48; this.audioElements = {}; this.loadError = false; this.baseUrl = 'https://sistema.offsbarato.com.br'; } connectedCallback() { this.render(); this.loadData(); this.addStyles(); } addStyles() { const style = document.createElement('style'); style.textContent = ` :host { display: block; width: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .container { width: 100%; padding: 64px 0; } .filters { margin-bottom: 32px; max-width: 1152px; margin-left: auto; margin-right: auto; padding: 0 24px; } .filter-buttons { display: flex; flex-wrap: wrap; gap: 12px; } .filter-btn { padding: 12px 24px; border-radius: 12px; font-weight: 500; transition: all 0.3s; cursor: pointer; border: 2px solid #fef3c7; background: white; color: #374151; } .filter-btn:hover { border-color: #fcd34d; } .filter-btn.active { background: linear-gradient(to right, #f59e0b, #d97706); color: white; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); } .filter-btn.duty-btn { background: #f0fdf4; color: #15803d; border-color: #bbf7d0; display: flex; align-items: center; gap: 8px; } .filter-btn.duty-btn:hover { border-color: #86efac; } .filter-btn.duty-btn.active { background: linear-gradient(to right, #16a34a, #15803d); color: white; } .duty-dot { width: 12px; height: 12px; border-radius: 50%; background: #10b981; position: relative; display: inline-block; } .duty-dot::before { content: ''; position: absolute; inset: 0; border-radius: 50%; background: #10b981; animation: pulse-dot 2s ease-in-out infinite; } @keyframes pulse-dot { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(1.2); } } .grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0; width: 100%; } @media (min-width: 768px) { .grid { grid-template-columns: repeat(3, 1fr); } } @media (min-width: 1024px) { .grid { grid-template-columns: repeat(6, 1fr); } } .voice-card { position: relative; aspect-ratio: 4/4.5; overflow: hidden; cursor: pointer; background-size: cover; background-position: center; } .voice-overlay { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.9), rgba(0,0,0,0.6), transparent); transition: all 0.3s; } .voice-card:hover .voice-overlay { background: linear-gradient(to top, rgba(0,0,0,0.8), rgba(0,0,0,0.6), transparent); } .voice-content { position: absolute; inset: 0; padding: 16px; display: flex; flex-direction: column; justify-content: space-between; z-index: 10; } .voice-header { display: flex; justify-content: space-between; align-items: start; } .play-btn { width: 56px; height: 56px; border-radius: 50%; background: #facc15; display: flex; align-items: center; justify-content: center; color: black; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); transition: all 0.3s; border: none; cursor: pointer; } .play-btn svg { width: 28px; height: 28px; } .play-btn:hover { background: #eab308; transform: scale(1.1); } .badge { background: rgba(245, 158, 11, 0.9); color: white; padding: 4px 10px; border-radius: 6px; font-size: 12px; font-weight: 600; } .voice-footer { display: flex; flex-direction: column; gap: 8px; } .voice-name { font-weight: bold; color: white; font-size: 14px; text-shadow: 0 2px 4px rgba(0,0,0,0.5); } .voice-status { display: flex; align-items: center; gap: 4px; font-weight: 500; font-size: 12px; } .status-dot { width: 6px; height: 6px; border-radius: 50%; animation: pulse 2s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .order-btn { width: 100%; background: linear-gradient(to right, #16a34a, #15803d); color: white; font-size: 12px; padding: 8px; border-radius: 6px; border: none; cursor: pointer; font-weight: bold; display: flex; align-items: center; justify-content: center; gap: 4px; transition: all 0.3s; line-height: 1.4; } .order-btn svg { width: 12px; height: 12px; flex-shrink: 0; } .order-btn:hover { background: linear-gradient(to right, #15803d, #166534); } .loading { display: flex; justify-content: center; padding: 80px 0; } .spinner { width: 64px; height: 64px; border: 2px solid #f59e0b; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .load-more { display: flex; justify-content: center; margin-top: 32px; max-width: 1152px; margin-left: auto; margin-right: auto; padding: 0 24px; } .load-more-btn { background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(4px); border: 2px solid #f59e0b; color: #78350f; padding: 24px 32px; font-size: 18px; border-radius: 12px; cursor: pointer; font-weight: 600; transition: all 0.3s; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); } .load-more-btn:hover { background: #f59e0b; color: white; } `; this.shadowRoot.appendChild(style); } async loadData() { try { const scriptElement = document.querySelector('script[src*="serveWidget"]'); if (scriptElement && scriptElement.src) { const scriptUrl = new URL(scriptElement.src); this.baseUrl = scriptUrl.origin; } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); const [voicesRes, categoriesRes] = await Promise.all([ fetch(`${this.baseUrl}/functions/getWidgetVoices`, { signal: controller.signal }), fetch(`${this.baseUrl}/functions/getWidgetCategories`, { signal: controller.signal }) ]); clearTimeout(timeoutId); if (!voicesRes.ok || !categoriesRes.ok) { throw new Error(`Erro ao carregar dados`); } const voicesData = await voicesRes.json(); const categoriesData = await categoriesRes.json(); // Smart Shuffle const grouped = {}; voicesData.forEach(voice => { if (!grouped[voice.name]) grouped[voice.name] = []; grouped[voice.name].push(voice); }); const shuffledVoices = []; const names = Object.keys(grouped).sort(() => Math.random() - 0.5); let maxCount = 0; Object.values(grouped).forEach(g => maxCount = Math.max(maxCount, g.length)); for (let i = 0; i < maxCount; i++) { names.forEach(name => { if (grouped[name][i]) { shuffledVoices.push(grouped[name][i]); } }); } // Ordenar: locutores gravando primeiro const now = new Date(); const currentTime = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', hour12: false }); const today = now.toISOString().split('T')[0]; const recordingUserIds = new Set(); const recordingVoiceIds = new Set(); shuffledVoices.forEach(voice => { if (this.isVoiceRecording(voice, now, currentTime, today, shuffledVoices)) { if (voice.user_id) { recordingUserIds.add(voice.user_id); } else { recordingVoiceIds.add(voice.id); } } }); const sorted = shuffledVoices.sort((a, b) => { const isARecording = (a.user_id && recordingUserIds.has(a.user_id)) || recordingVoiceIds.has(a.id); const isBRecording = (b.user_id && recordingUserIds.has(b.user_id)) || recordingVoiceIds.has(b.id); if (isARecording && !isBRecording) return -1; if (!isARecording && isBRecording) return 1; return 0; }); this.voices = sorted; this.categories = categoriesData; this.render(); } catch (error) { console.error('Widget: Erro ao carregar dados:', error); this.voices = []; this.categories = []; this.loadError = true; this.render(); } } isVoiceRecording(voice, now, currentTime, today, allVoices) { const voicesToCheck = voice.user_id ? allVoices.filter(v => v.user_id === voice.user_id) : [voice]; for (const v of voicesToCheck) { if (v.on_duty && v.duty_date === today && v.duty_end_time >= currentTime) { return true; } if (v.recording_schedule) { const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; const dayKey = days[now.getDay()]; const sched = v.recording_schedule[dayKey]; if (sched && sched.works && currentTime >= sched.start && currentTime <= sched.end) { return true; } } } return false; } getAvailabilityInfo(voice) { const now = new Date(); const currentTime = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', hour12: false }); const todayDate = now.toISOString().split('T')[0]; const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; const currentDayKey = daysOfWeek[now.getDay()]; // Manual On Duty const isManualDuty = voice.on_duty && voice.duty_date === todayDate && voice.duty_end_time >= currentTime; if (isManualDuty) { return { text: `Locutor de Plantão HOJE até as ${voice.duty_end_time}`, color: "#a7f3d0", dotColor: "#10b981" }; } // Check Schedule let schedule = voice.recording_schedule; if (!schedule && voice.user_id) { const sameUser = this.voices.find(v => v.user_id === voice.user_id && v.recording_schedule); if (sameUser) schedule = sameUser.recording_schedule; } if (schedule) { const todaySchedule = schedule[currentDayKey]; if (todaySchedule && todaySchedule.works && currentTime >= todaySchedule.start && currentTime <= todaySchedule.end) { return { text: `Gravando até as ${todaySchedule.end}`, color: "#a7f3d0", dotColor: "#10b981" }; } if (todaySchedule && todaySchedule.works && currentTime < todaySchedule.start) { return { text: `Indisponível no momento, retorna hoje às ${todaySchedule.start}`, color: "#fca5a5", dotColor: "#ef4444" }; } const daysPt = { monday: "Segunda", tuesday: "Terça", wednesday: "Quarta", thursday: "Quinta", friday: "Sexta", saturday: "Sábado", sunday: "Domingo" }; for (let i = 1; i <= 7; i++) { const nextIndex = (now.getDay() + i) % 7; const nextDayKey = daysOfWeek[nextIndex]; const nextSchedule = schedule[nextDayKey]; if (nextSchedule && nextSchedule.works) { const dayName = i === 1 ? "Amanhã" : daysPt[nextDayKey]; return { text: `Indisponível no momento. Retorna ${dayName} às ${nextSchedule.start}`, color: "#fca5a5", dotColor: "#ef4444" }; } } } return { text: "Indisponível no momento", color: "#fca5a5", dotColor: "#ef4444" }; } togglePlay(voiceId, audioUrl) { Object.values(this.audioElements).forEach(a => a.pause()); const audio = this.audioElements[voiceId]; if (!audio) { const newAudio = new Audio(audioUrl); this.audioElements[voiceId] = newAudio; newAudio.addEventListener('ended', () => { this.playingId = null; this.render(); }); newAudio.play(); this.playingId = voiceId; } else { if (this.playingId === voiceId) { audio.pause(); this.playingId = null; } else { audio.play(); this.playingId = voiceId; } } this.render(); } filterVoices() { if (this.selectedCategory === 'duty') { const now = new Date(); const today = now.toISOString().split('T')[0]; const currentTime = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', hour12: false }); const onDutyUserIds = new Set(); const onDutyVoiceIds = new Set(); this.voices.forEach(v => { let isOnDuty = false; // Plantão manual ativo if (v.on_duty && v.duty_date === today && v.duty_end_time >= currentTime) { isOnDuty = true; } // Horário automático de gravação if (!isOnDuty && v.recording_schedule) { const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; const dayKey = days[now.getDay()]; const sched = v.recording_schedule[dayKey]; if (sched && sched.works && currentTime >= sched.start && currentTime <= sched.end) { isOnDuty = true; } } if (isOnDuty) { if (v.user_id) { onDutyUserIds.add(v.user_id); } else { onDutyVoiceIds.add(v.id); } } }); return this.voices.filter(v => { if (v.user_id && onDutyUserIds.has(v.user_id)) return true; if (onDutyVoiceIds.has(v.id)) return true; return false; }); } if (this.selectedCategory === 'all') return this.voices; if (this.selectedCategory === 'male') return this.voices.filter(v => v.gender === 'Masculino'); if (this.selectedCategory === 'female') return this.voices.filter(v => v.gender === 'Feminino'); if (this.selectedCategory === 'child') return this.voices.filter(v => v.is_child_voice === true); if (this.selectedCategory === 'impacto') return this.voices.filter(v => v.category_name === 'Impacto'); return this.voices.filter(v => v.category_id === this.selectedCategory); } hasActiveOnDuty() { const now = new Date(); const today = now.toISOString().split('T')[0]; const currentTime = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', hour12: false }); return this.voices.some(v => { // Plantão manual ativo if (v.on_duty && v.duty_date === today && v.duty_end_time >= currentTime) { return true; } // Horário automático de gravação if (v.recording_schedule) { const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; const dayKey = days[now.getDay()]; const sched = v.recording_schedule[dayKey]; if (sched && sched.works && currentTime >= sched.start && currentTime <= sched.end) { return true; } } return false; }); } getOnDutyButtonText() { return "Gravando agora"; } handleOrderWithVoice(voiceId) { window.location.href = `${this.baseUrl}/OrderForm?voiceId=${voiceId}`; } render() { const filteredVoices = this.filterVoices(); const visibleVoices = filteredVoices.slice(0, this.visibleCount); this.shadowRoot.innerHTML = ''; this.addStyles(); const container = document.createElement('div'); container.className = 'container'; if (this.loadError) { container.innerHTML = `
Por favor, verifique o console para mais detalhes.