// Mapa da Equipe — orcamento 310, item 19. // // O QUE O ORCAMENTO PEDE // "Adicionar mapa dentro da acao. Mostrar o local da acao e os registros de // check-in realizados. Nao e necessario acompanhar a localizacao do // profissional continuamente." // // A SEGUNDA FRASE E A PARTE IMPORTANTE // Rastreio continuo seria mais facil de vender e pior de usar: derruba a // bateria do celular de quem esta trabalhando e transforma a ferramenta em // vigilancia. O que o mapa mostra sao os pontos que a operacao ja gravava por // conta propria — chegada, foto, ocorrencia, saida. Cada ponto tem um motivo // para existir, e entre um e outro ninguem e seguido. // // POR QUE circleMarker E NAO O PIN PADRAO // O marcador padrao do Leaflet e uma imagem PNG resolvida por caminho // relativo. Servido de CDN, ele quebra calado e o mapa aparece sem pino // nenhum. circleMarker e SVG puro: nao depende de arquivo nenhum. const MAPA_LEAFLET_JS = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; const MAPA_LEAFLET_CSS = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; // Carregado sob demanda, e nao no index.html, porque o mapa vive numa aba que // a maioria das telas nunca abre — nao ha por que atrasar o login de todo mundo // por causa dele. let mapaLeafletPromise = null; function carregarLeaflet() { if (window.L) return Promise.resolve(window.L); if (mapaLeafletPromise) return mapaLeafletPromise; mapaLeafletPromise = new Promise((resolve, reject) => { const css = document.createElement('link'); css.rel = 'stylesheet'; css.href = MAPA_LEAFLET_CSS; document.head.appendChild(css); const js = document.createElement('script'); js.src = MAPA_LEAFLET_JS; js.onload = () => resolve(window.L); js.onerror = () => { mapaLeafletPromise = null; reject(new Error('Não consegui carregar o mapa.')); }; document.head.appendChild(js); }); return mapaLeafletPromise; } const MAPA_TIPOS = { local: { cor: '#F97316', rotulo: 'Local da ação' }, check_in: { cor: '#16A34A', rotulo: 'Check-in' }, check_out: { cor: '#2563EB', rotulo: 'Check-out' }, foto: { cor: '#8B5CF6', rotulo: 'Foto' }, ocorrencia: { cor: '#DC2626', rotulo: 'Ocorrência' }, }; const mapaNum = v => (v === null || v === undefined || v === '' ? null : Number(v)); const mapaHora = iso => iso ? new Date(iso).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : null; // Escapa o que vai para dentro do popup: nome de pessoa e descricao de // ocorrencia sao texto digitado, e o popup do Leaflet aceita HTML. function mapaEsc(t) { return String(t === null || t === undefined ? '' : t) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"'); } // Monta a lista de pontos a partir do que a operacao ja gravou. function pontosDaAcao(event, escalas, fotos, ocorrencias) { const pontos = []; const evLat = mapaNum(event && event.local_lat); const evLng = mapaNum(event && event.local_lng); if (evLat !== null && evLng !== null) { pontos.push({ tipo: 'local', lat: evLat, lng: evLng, titulo: event.name || 'Local da ação', linhas: [event.location || null], raio: mapaNum(event.raio_checkin_metros), }); } (escalas || []).forEach(e => { const nome = (e.freelancers && e.freelancers.name) || 'Profissional'; const ciLat = mapaNum(e.check_in_lat), ciLng = mapaNum(e.check_in_lng); if (ciLat !== null && ciLng !== null) { pontos.push({ tipo: 'check_in', lat: ciLat, lng: ciLng, titulo: nome, linhas: [ mapaHora(e.check_in_em) ? 'Chegou às ' + mapaHora(e.check_in_em) : null, e.check_in_distancia_m !== null && e.check_in_distancia_m !== undefined ? Math.round(e.check_in_distancia_m) + ' m do local' : null, e.check_in_endereco || null, ], }); } const coLat = mapaNum(e.check_out_lat), coLng = mapaNum(e.check_out_lng); if (coLat !== null && coLng !== null) { pontos.push({ tipo: 'check_out', lat: coLat, lng: coLng, titulo: nome, linhas: [ mapaHora(e.check_out_em) ? 'Saiu às ' + mapaHora(e.check_out_em) : null, e.check_out_endereco || null, ], }); } }); (fotos || []).forEach(f => { const lat = mapaNum(f.lat), lng = mapaNum(f.lng); if (lat === null || lng === null) return; pontos.push({ tipo: 'foto', lat, lng, titulo: f.categoria || 'Foto', // escala_fotos marca a hora em enviada_em; ocorrencia usa criada_em. linhas: [mapaHora(f.enviada_em), f.legenda || null, f.endereco || null], }); }); (ocorrencias || []).forEach(o => { const lat = mapaNum(o.lat), lng = mapaNum(o.lng); if (lat === null || lng === null) return; pontos.push({ tipo: 'ocorrencia', lat, lng, titulo: o.categoria || 'Ocorrência', linhas: [mapaHora(o.criada_em), o.descricao || null, o.resolvida_em ? 'Resolvida' : 'Em aberto'], }); }); return pontos; } function MapaLegenda({ presentes }) { return (