diff --git a/Relatório b/Relatório new file mode 100644 index 0000000..dc18836 --- /dev/null +++ b/Relatório @@ -0,0 +1,1349 @@ +<!DOCTYPE html> +<html lang="pt-BR"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Relatório Fotográfico Ambiental 3.1</title> + <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" /> + <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> + <script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.1/build/qrcode.min.js"></script> + <style> + body { + font-family: Arial, sans-serif; + margin: 20px; + line-height: 1.6; + } + .container { + max-width: 900px; + margin: 0 auto; + padding: 20px; + border: 1px solid #ddd; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0,0,0,0.1); + } + .header { + text-align: center; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 2px solid #006400; + } + h1, h2 { + color: #006400; + } + .form-section { + margin-bottom: 25px; + page-break-inside: avoid; + } + .form-group { + margin-bottom: 15px; + } + label { + display: block; + font-weight: bold; + margin-bottom: 5px; + } + input, textarea, select { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + box-sizing: border-box; + } + #map { + height: 400px; + width: 100%; + margin: 15px 0; + border: 1px solid #ddd; + border-radius: 4px; + } + .btn { + background-color: #006400; + color: white; + padding: 10px 15px; + border: none; + border-radius: 4px; + cursor: pointer; + margin-top: 10px; + } + .btn-secondary { + background-color: #6c757d; + } + .btn-danger { + background-color: #dc3545; + } + .coordinates { + display: flex; + gap: 15px; + } + .coord-input { + flex: 1; + } + .error { + color: red; + margin-top: 5px; + } + .photo-container { + display: flex; + flex-wrap: wrap; + gap: 15px; + margin-top: 15px; + page-break-inside: avoid; + } + .photo-item { + flex: 1 1 300px; + border: 1px solid #ddd; + padding: 10px; + border-radius: 5px; + position: relative; + page-break-inside: avoid; + } + .photo-number { + position: absolute; + top: 5px; + left: 5px; + background-color: rgba(0, 100, 0, 0.7); + color: white; + padding: 3px 8px; + border-radius: 3px; + font-weight: bold; + } + .photo-preview { + width: 100%; + height: 200px; + object-fit: cover; + background-color: #f5f5f5; + margin-bottom: 10px; + } + .datetime-group { + display: flex; + gap: 15px; + } + .datetime-input { + flex: 1; + } + .dms-coordinates { + font-family: monospace; + background-color: #f5f5f5; + padding: 5px; + border-radius: 3px; + } + .map-controls { + display: flex; + gap: 10px; + margin-bottom: 10px; + } + .map-type-btn { + padding: 5px 10px; + background-color: #f8f9fa; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + } + .map-type-btn.active { + background-color: #006400; + color: white; + } + .action-buttons { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; + } + .connection-status { + position: fixed; + top: 10px; + right: 10px; + padding: 5px 10px; + border-radius: 4px; + color: white; + font-weight: bold; + z-index: 1000; + } + .online { + background-color: #28a745; + } + .offline { + background-color: #dc3545; + } + .footer { + text-align: center; + margin-top: 30px; + padding-top: 15px; + border-top: 1px solid #ddd; + font-size: 0.9em; + color: #666; + } + .termo-item { + border: 1px solid #ddd; + padding: 10px; + margin-bottom: 10px; + border-radius: 4px; + page-break-inside: avoid; + } + .auto-item { + border: 1px solid #ddd; + padding: 10px; + margin-bottom: 10px; + border-radius: 4px; + page-break-inside: avoid; + } + .add-btn { + margin-bottom: 15px; + } + .signature-pad { + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; + } + .signature-clear { + margin-top: 10px; + } + #qrcode { + margin: 15px 0; + text-align: center; + } + #infrator-search { + margin-bottom: 15px; + } + #infrator-results { + max-height: 200px; + overflow-y: auto; + border: 1px solid #ddd; + border-radius: 4px; + padding: 10px; + margin-bottom: 15px; + display: none; + } + .infrator-item { + padding: 8px; + border-bottom: 1px solid #eee; + cursor: pointer; + } + .infrator-item:hover { + background-color: #f5f5f5; + } + .infrator-selected { + background-color: #e6f7e6; + border-left: 3px solid #006400; + padding-left: 5px; + } + @media print { + .no-print { + display: none; + } + body { + margin: 0; + padding: 0; + } + .container { + border: none; + box-shadow: none; + padding: 0; + } + .footer { + display: none; + } + .photo-item { + page-break-inside: avoid; + } + } + </style> +</head> +<body> + <div id="connectionStatus" class="connection-status offline">Offline</div> + + <div class="container" id="relatorio"> + <div class="header"> + <h1>Relatório Fotográfico Ambiental - Versão 3.1</h1> + <p>Instituto Chico Mendes de Conservação da Biodiversidade - ICMBio</p> + <div id="qrcode"></div> + </div> + + <div class="form-section"> + <h2>Dados do Relatório</h2> + + <div class="datetime-group"> + <div class="datetime-input"> + <label for="data">Data:</label> + <input type="date" id="data" required> + </div> + <div class="datetime-input"> + <label for="hora">Hora:</label> + <input type="time" id="hora" required> + </div> + </div> + + <div class="form-group"> + <label for="local">Local da Fiscalização:</label> + <input type="text" id="local" required> + </div> + + <div class="form-group"> + <label for="infrator-search">Buscar Infrator:</label> + <input type="text" id="infrator-search" placeholder="Digite nome, CPF/CNPJ ou número de auto anterior"> + <div id="infrator-results"></div> + </div> + + <div class="form-group"> + <label for="infrator_nome">Nome do Infrator:</label> + <input type="text" id="infrator_nome" required> + </div> + + <div class="form-group"> + <label for="infrator_documento">CPF/CNPJ:</label> + <input type="text" id="infrator_documento" required> + </div> + + <div class="form-group"> + <label>Coordenadas Geográficas (Graus Decimais):</label> + <div class="coordinates"> + <input type="text" id="latitude" class="coord-input" placeholder="Latitude" readonly> + <input type="text" id="longitude" class="coord-input" placeholder="Longitude" readonly> + </div> + <div id="coord-error" class="error"></div> + <div class="form-group"> + <input type="checkbox" id="rastreamento" checked> + <label for="rastreamento" style="display: inline;">Atualizar coordenadas automaticamente</label> + </div> + </div> + + <div class="form-group"> + <label>Coordenadas em Graus, Minutos e Segundos:</label> + <div id="dms-coordinates" class="dms-coordinates">Não disponível</div> + </div> + + <div class="buttons no-print"> + <button id="get-location" class="btn">Obter Localização Atual</button> + <button id="manual-coords" class="btn btn-secondary">Inserir Coordenadas Manualmente</button> + </div> + + <div id="map"></div> + </div> + + <div class="form-section"> + <h2>Autos de Infração</h2> + <div id="autos-container"> + <div class="auto-item"> + <div class="form-group"> + <label for="auto_numero_1">Número do Auto de Infração:</label> + <input type="text" id="auto_numero_1" class="auto-numero" required> + </div> + <div class="form-group"> + <label for="auto_descricao_1">Descrição da Infração:</label> + <textarea id="auto_descricao_1" class="auto-descricao" rows="3" required></textarea> + </div> + </div> + </div> + <button id="add-auto" class="btn btn-secondary add-btn no-print">Adicionar outro Auto</button> + </div> + + <div class="form-section"> + <h2>Termos</h2> + <div id="termos-container"> + <div class="termo-item"> + <div class="form-group"> + <label for="termo_tipo_1">Tipo de Termo:</label> + <select id="termo_tipo_1" class="termo-tipo" required> + <option value="">Selecione...</option> + <option value="notificacao">Termo de Notificação</option> + <option value="apreensao">Termo de Apreensão</option> + <option value="embargo">Termo de Embargo</option> + <option value="soltura">Termo de Soltura</option> + </select> + </div> + <div class="form-group"> + <label for="termo_numero_1">Número do Termo:</label> + <input type="text" id="termo_numero_1" class="termo-numero" required> + </div> + </div> + </div> + <button id="add-termo" class="btn btn-secondary add-btn no-print">Adicionar outro Termo</button> + </div> + + <div class="form-section"> + <h2>Descrição da Ação</h2> + <div class="form-group"> + <label for="descricao">Descrição detalhada do que foi realizado em campo:</label> + <textarea id="descricao" rows="6" required></textarea> + </div> + </div> + + <div class="form-section"> + <h2>Registro Fotográfico</h2> + + <div class="form-group no-print"> + <label for="photo-upload">Adicionar Fotos:</label> + <input type="file" id="photo-upload" accept="image/*" multiple> + <small>Selecione uma ou mais fotos para upload</small> + </div> + + <div id="photo-container" class="photo-container"> + <div class="photo-item" style="display: none;"> + <div class="photo-number">Figura 1</div> + <img class="photo-preview" src=""> + <div class="form-group"> + <label>Legenda:</label> + <input type="text" class="photo-caption" placeholder="Descreva a foto"> + </div> + <div class="form-group"> + <label>Coordenadas:</label> + <input type="text" class="photo-coords" readonly> + </div> + <button class="btn btn-secondary remove-photo no-print">Remover</button> + </div> + </div> + </div> + + <div class="form-section"> + <h2>Assinatura do Fiscal</h2> + <div class="form-group"> + <label for="fiscal_nome">Nome do Fiscal:</label> + <input type="text" id="fiscal_nome" required> + </div> + <div class="form-group"> + <label for="fiscal_matricula">Matrícula:</label> + <input type="text" id="fiscal_matricula" required> + </div> + <div class="form-group"> + <label>Assinatura:</label> + <canvas id="signature-pad" class="signature-pad" width="400" height="200"></canvas> + <button id="clear-signature" class="btn btn-secondary signature-clear no-print">Limpar Assinatura</button> + </div> + </div> + + <div class="action-buttons no-print"> + <button id="print-report" class="btn">Imprimir Relatório</button> + <button id="generate-pdf" class="btn btn-secondary">Gerar PDF</button> + <button id="save-data" class="btn btn-secondary">Salvar Dados</button> + <button id="load-data" class="btn btn-secondary">Carregar Dados</button> + </div> + + <div class="footer"> + <p>Todos os direitos reservados © <span id="current-year"></span> - Iram Mendes Jr - Analista Ambiental - ICMBio</p> + </div> + </div> + + <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/signature_pad/1.5.3/signature_pad.min.js"></script> + + <script> + // Código do Service Worker como Blob URL + function registerServiceWorker() { + if ('serviceWorker' in navigator) { + const swCode = ` + const CACHE_NAME = 'ambiental-report-v3'; + const OFFLINE_TILES = [ + '/offline-tiles/{z}/{x}/{y}.png', + '/offline-page' + ]; + + self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME) + .then((cache) => { + return cache.addAll(OFFLINE_TILES); + }) + ); + }); + + self.addEventListener('fetch', (event) => { + if (event.request.mode === 'navigate' || + (event.request.method === 'GET' && + event.request.headers.get('accept').includes('text/html'))) { + event.respondWith( + fetch(event.request) + .catch(() => { + return caches.match('/offline-page'); + }) + ); + } else if (event.request.url.includes('{z}/{x}/{y}')) { + event.respondWith( + caches.match(event.request) + .then((response) => { + return response || fetch(event.request) + .catch(() => { + return new Response('<svg>...</svg>', { + headers: { 'Content-Type': 'image/svg+xml' } + }); + }); + }) + ); + } else { + event.respondWith( + caches.match(event.request) + .then((response) => { + return response || fetch(event.request); + }) + ); + } + }); + `; + + const blob = new Blob([swCode], { type: 'application/javascript' }); + const swUrl = URL.createObjectURL(blob); + + navigator.serviceWorker.register(swUrl) + .then(registration => { + console.log('ServiceWorker registration successful'); + }) + .catch(err => { + console.log('ServiceWorker registration failed: ', err); + }); + } + } + + // Variáveis globais + let map; + let marker; + const coordError = document.getElementById('coord-error'); + let currentPhotos = []; + let baseLayers = {}; + let currentLayer; + let isOnline = navigator.onLine; + const connectionStatus = document.getElementById('connectionStatus'); + let autoCount = 1; + let termoCount = 1; + let photoCount = 0; + let signaturePad; + let watchPositionId = null; + let infratoresDB = []; + + // Inicialização + window.onload = function() { + const now = new Date(); + document.getElementById('data').value = now.toISOString().split('T')[0]; + document.getElementById('hora').value = now.toTimeString().substring(0, 5); + document.getElementById('current-year').textContent = new Date().getFullYear(); + + // Inicializa o Signature Pad + const canvas = document.getElementById('signature-pad'); + signaturePad = new SignaturePad(canvas); + + // Carrega banco de infratores (simulado) + loadInfratoresDB(); + + updateConnectionStatus(); + initMap(-15.788, -47.879); + registerServiceWorker(); + generateQRCode(); + + // Configura eventos de busca + document.getElementById('infrator-search').addEventListener('input', searchInfrator); + }; + + // Carrega banco de infratores (simulação) + function loadInfratoresDB() { + // Simulação de banco de dados + infratoresDB = [ + { nome: "João da Silva", documento: "123.456.789-09", autos: ["AI-2023-001", "AI-2022-045"] }, + { nome: "Empresa XYZ Ltda", documento: "12.345.678/0001-99", autos: ["AI-2023-078"] }, + { nome: "Maria Oliveira", documento: "987.654.321-00", autos: ["AI-2021-123", "AI-2020-056"] } + ]; + } + + // Busca infrator no banco de dados + function searchInfrator() { + const searchTerm = document.getElementById('infrator-search').value.toLowerCase(); + const resultsContainer = document.getElementById('infrator-results'); + + if (searchTerm.length < 3) { + resultsContainer.style.display = 'none'; + return; + } + + const results = infratoresDB.filter(infrator => + infrator.nome.toLowerCase().includes(searchTerm) || + infrator.documento.includes(searchTerm) || + infrator.autos.some(auto => auto.toLowerCase().includes(searchTerm)) + ); + + if (results.length > 0) { + resultsContainer.innerHTML = ''; + results.forEach(infrator => { + const div = document.createElement('div'); + div.className = 'infrator-item'; + div.innerHTML = ` + <strong>${infrator.nome}</strong><br> + ${infrator.documento}<br> + <small>Autos anteriores: ${infrator.autos.join(', ')}</small> + `; + div.addEventListener('click', () => selectInfrator(infrator)); + resultsContainer.appendChild(div); + }); + resultsContainer.style.display = 'block'; + } else { + resultsContainer.style.display = 'none'; + } + } + + // Seleciona um infrator dos resultados + function selectInfrator(infrator) { + document.getElementById('infrator_nome').value = infrator.nome; + document.getElementById('infrator_documento').value = infrator.documento; + document.getElementById('infrator-results').style.display = 'none'; + document.getElementById('infrator-search').value = ''; + } + + // Gera QR Code com informações do relatório + function generateQRCode() { + const qrElement = document.getElementById('qrcode'); + qrElement.innerHTML = ''; + + // Gera um ID único para o relatório + const reportId = 'REL-' + Date.now(); + + QRCode.toCanvas(qrElement, reportId, { width: 150 }, function(error) { + if (error) console.error(error); + }); + } + + // Atualiza o status da conexão + function updateConnectionStatus() { + isOnline = navigator.onLine; + if (isOnline) { + connectionStatus.textContent = 'Online'; + connectionStatus.className = 'connection-status online'; + initMapWithOnlineLayers(); + } else { + connectionStatus.textContent = 'Offline'; + connectionStatus.className = 'connection-status offline'; + initMapWithOfflineLayers(); + } + } + + // Inicializa o mapa com camadas online + function initMapWithOnlineLayers(lat, lng) { + if (!map) { + const defaultLat = lat || -15.788; + const defaultLng = lng || -47.879; + initMap(defaultLat, defaultLng); + return; + } + + map.eachLayer(layer => { + if (layer._url && layer._url.includes('offline-tiles')) { + map.removeLayer(layer); + } + }); + + baseLayers["Satélite"].addTo(map); + currentLayer = baseLayers["Satélite"]; + } + + // Inicializa o mapa com camadas offline + function initMapWithOfflineLayers(lat, lng) { + if (!map) { + const defaultLat = lat || -15.788; + const defaultLng = lng || -47.879; + initMap(defaultLat, defaultLng); + return; + } + + map.eachLayer(layer => { + if (layer._url && (layer._url.includes('openstreetmap') || layer._url.includes('google'))) { + map.removeLayer(layer); + } + }); + + try { + const offlineLayer = L.tileLayer('offline-tiles/{z}/{x}/{y}.png', { + attribution: 'Map data offline', + maxZoom: 18, + minZoom: 3 + }).addTo(map); + currentLayer = offlineLayer; + } catch (e) { + document.getElementById('map').innerHTML = + '<p style="text-align: center; padding-top: 180px;">Mapa offline não disponível. Conecte-se à internet para carregar mapas.</p>'; + } + } + + // Inicializa o mapa + function initMap(lat, lng) { + if (map) { + map.setView([lat, lng], 15); + if (marker) { + marker.setLatLng([lat, lng]); + } + return; + } + + map = L.map('map').setView([lat, lng], 15); + + const streetLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' + }); + + const satelliteLayer = L.tileLayer('https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', { + attribution: 'Imagens © Google Satellite' + }); + + const hybridLayer = L.tileLayer('https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', { + attribution: 'Imagens © Google Hybrid' + }); + + baseLayers = { + "Satélite": satelliteLayer, + "Ruas": streetLayer, + "Híbrido": hybridLayer + }; + + if (isOnline) { + satelliteLayer.addTo(map); + currentLayer = satelliteLayer; + } else { + initMapWithOfflineLayers(lat, lng); + } + + marker = L.marker([lat, lng], { + draggable: true + }).addTo(map); + + marker.on('dragend', function() { + const newLatLng = marker.getLatLng(); + updateCoordinates(newLatLng.lat, newLatLng.lng); + }); + + map.on('click', function(e) { + updateCoordinates(e.latlng.lat, e.latlng.lng); + if (!marker) { + marker = L.marker(e.latlng, { + draggable: true + }).addTo(map); + marker.on('dragend', function() { + const newLatLng = marker.getLatLng(); + updateCoordinates(newLatLng.lat, newLatLng.lng); + }); + } else { + marker.setLatLng(e.latlng); + } + }); + } + + // Converte graus decimais para DMS + function decimalToDMS(decimal, isLatitude) { + const absDecimal = Math.abs(decimal); + const degrees = Math.floor(absDecimal); + const minutesNotTruncated = (absDecimal - degrees) * 60; + const minutes = Math.floor(minutesNotTruncated); + const seconds = ((minutesNotTruncated - minutes) * 60).toFixed(2); + + const direction = isLatitude + ? (decimal >= 0 ? 'N' : 'S') + : (decimal >= 0 ? 'E' : 'W'); + + return `${degrees}° ${minutes}' ${seconds}" ${direction}`; + } + + // Atualiza as coordenadas DMS + function updateDMSCoordinates(lat, lng) { + const latDMS = decimalToDMS(lat, true); + const lngDMS = decimalToDMS(lng, false); + document.getElementById('dms-coordinates').textContent = + `Latitude: ${latDMS} | Longitude: ${lngDMS}`; + } + + // Atualiza os campos de coordenadas + function updateCoordinates(lat, lng) { + document.getElementById('latitude').value = lat.toFixed(6); + document.getElementById('longitude').value = lng.toFixed(6); + updateDMSCoordinates(lat, lng); + coordError.textContent = ''; + + if (map) { + map.setView([lat, lng]); + if (marker) { + marker.setLatLng([lat, lng]); + } else { + marker = L.marker([lat, lng], { + draggable: true + }).addTo(map); + marker.on('dragend', function() { + const newLatLng = marker.getLatLng(); + updateCoordinates(newLatLng.lat, newLatLng.lng); + }); + } + } + } + + // Obtém a localização atual + function getLocation() { + coordError.textContent = 'Obtendo localização...'; + + if (!navigator.geolocation) { + coordError.textContent = 'Geolocalização não é suportada pelo seu navegador'; + return; + } + + // Para qualquer rastreamento anterior + if (watchPositionId) { + navigator.geolocation.clearWatch(watchPositionId); + } + + // Verifica se o rastreamento está ativado + const rastreamentoAtivo = document.getElementById('rastreamento').checked; + + if (rastreamentoAtivo) { + // Inicia o rastreamento contínuo + watchPositionId = navigator.geolocation.watchPosition( + function(position) { + const lat = position.coords.latitude; + const lng = position.coords.longitude; + updateCoordinates(lat, lng); + initMap(lat, lng); + coordError.textContent = 'Rastreamento ativo - coordenadas atualizadas'; + }, + function(error) { + handleGeolocationError(error); + }, + { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0 + } + ); + } else { + // Obtém apenas uma posição + navigator.geolocation.getCurrentPosition( + function(position) { + const lat = position.coords.latitude; + const lng = position.coords.longitude; + updateCoordinates(lat, lng); + initMap(lat, lng); + }, + function(error) { + handleGeolocationError(error); + }, + { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0 + } + ); + } + } + + // Trata erros de geolocalização + function handleGeolocationError(error) { + let errorMessage; + switch(error.code) { + case error.PERMISSION_DENIED: + errorMessage = "Permissão de localização negada pelo usuário"; + break; + case error.POSITION_UNAVAILABLE: + errorMessage = "Informações de localização indisponíveis"; + break; + case error.TIMEOUT: + errorMessage = "Tempo limite para obter localização excedido"; + break; + case error.UNKNOWN_ERROR: + errorMessage = "Ocorreu um erro desconhecido"; + break; + } + coordError.textContent = "Erro: " + errorMessage; + } + + // Permite inserir coordenadas manualmente + function manualCoords() { + const lat = prompt("Digite a latitude (ex: -15.788):"); + const lng = prompt("Digite a longitude (ex: -47.879):"); + + if (lat && lng && !isNaN(lat) && !isNaN(lng)) { + updateCoordinates(parseFloat(lat), parseFloat(lng)); + initMap(parseFloat(lat), parseFloat(lng)); + } else { + alert("Coordenadas inválidas. Por favor, insira valores numéricos."); + } + } + + // Adiciona um novo auto de infração + function addAuto() { + autoCount++; + const newAuto = document.createElement('div'); + newAuto.className = 'auto-item'; + newAuto.innerHTML = ` + <div class="form-group"> + <label for="auto_numero_${autoCount}">Número do Auto de Infração:</label> + <input type="text" id="auto_numero_${autoCount}" class="auto-numero" required> + </div> + <div class="form-group"> + <label for="auto_descricao_${autoCount}">Descrição da Infração:</label> + <textarea id="auto_descricao_${autoCount}" class="auto-descricao" rows="3" required></textarea> + </div> + <button class="btn btn-secondary remove-auto no-print" data-id="${autoCount}">Remover Auto</button> + `; + document.getElementById('autos-container').appendChild(newAuto); + + // Adiciona evento para remover o auto + newAuto.querySelector('.remove-auto').addEventListener('click', function() { + if (autoCount > 1) { + document.getElementById('autos-container').removeChild(newAuto); + autoCount--; + } else { + alert("Pelo menos um auto de infração deve ser mantido."); + } + }); + } + + // Adiciona um novo termo + function addTermo() { + termoCount++; + const newTermo = document.createElement('div'); + newTermo.className = 'termo-item'; + newTermo.innerHTML = ` + <div class="form-group"> + <label for="termo_tipo_${termoCount}">Tipo de Termo:</label> + <select id="termo_tipo_${termoCount}" class="termo-tipo" required> + <option value="">Selecione...</option> + <option value="notificacao">Termo de Notificação</option> + <option value="apreensao">Termo de Apreensão</option> + <option value="embargo">Termo de Embargo</option> + <option value="soltura">Termo de Soltura</option> + </select> + </div> + <div class="form-group"> + <label for="termo_numero_${termoCount}">Número do Termo:</label> + <input type="text" id="termo_numero_${termoCount}" class="termo-numero" required> + </div> + <button class="btn btn-secondary remove-termo no-print" data-id="${termoCount}">Remover Termo</button> + `; + document.getElementById('termos-container').appendChild(newTermo); + + // Adiciona evento para remover o termo + newTermo.querySelector('.remove-termo').addEventListener('click', function() { + if (termoCount > 1) { + document.getElementById('termos-container').removeChild(newTermo); + termoCount--; + } else { + alert("Pelo menos um termo deve ser mantido."); + } + }); + } + + // Adiciona uma foto ao relatório + function addPhoto(file, coords) { + const reader = new FileReader(); + reader.onload = function(e) { + photoCount++; + const photoContainer = document.getElementById('photo-container'); + const template = photoContainer.querySelector('.photo-item'); + const newPhoto = template.cloneNode(true); + + newPhoto.style.display = 'block'; + newPhoto.querySelector('.photo-number').textContent = `Figura ${photoCount}`; + newPhoto.querySelector('.photo-preview').src = e.target.result; + newPhoto.querySelector('.photo-coords').value = + `${coords.lat.toFixed(6)}, ${coords.lng.toFixed(6)}`; + + newPhoto.querySelector('.remove-photo').addEventListener('click', function() { + photoContainer.removeChild(newPhoto); + currentPhotos = currentPhotos.filter(p => p.id !== newPhoto.dataset.id); + updatePhotoNumbers(); + }); + + const photoId = 'photo-' + Date.now(); + newPhoto.dataset.id = photoId; + photoContainer.appendChild(newPhoto); + + currentPhotos.push({ + id: photoId, + file: file, + coords: coords, + element: newPhoto + }); + }; + reader.readAsDataURL(file); + } + + // Atualiza a numeração das fotos + function updatePhotoNumbers() { + const photos = document.querySelectorAll('.photo-item:not([style*="display: none"])'); + photoCount = 0; + + photos.forEach((photo, index) => { + photoCount++; + photo.querySelector('.photo-number').textContent = `Figura ${photoCount}`; + }); + } + + // Imprime o relatório + function printReport() { + window.print(); + } + + // Gera PDF do relatório com controle de paginação + function generatePDF() { + const { jsPDF } = window.jspdf; + const doc = new jsPDF('p', 'pt', 'a4'); + const element = document.getElementById('relatorio'); + const padding = 40; + const pageWidth = doc.internal.pageSize.getWidth() - padding * 2; + + // Primeiro criamos um canvas temporário para calcular a altura total + html2canvas(element, { + scale: 1, + logging: false, + useCORS: true + }).then(tempCanvas => { + const tempImgData = tempCanvas.toDataURL('image/png'); + const tempImgWidth = pageWidth; + const tempImgHeight = (tempCanvas.height * tempImgWidth) / tempCanvas.width; + + // Calcula quantas páginas serão necessárias + const pageHeight = doc.internal.pageSize.getHeight() - padding; + const totalPages = Math.ceil(tempImgHeight / pageHeight); + + // Agora geramos o PDF com qualidade maior, controlando a paginação + html2canvas(element, { + scale: 2, // Maior qualidade + logging: false, + useCORS: true + }).then(finalCanvas => { + const imgData = finalCanvas.toDataURL('image/png'); + const imgWidth = pageWidth; + const imgHeight = (finalCanvas.height * imgWidth) / finalCanvas.width; + + let position = padding; + const imgHeightPerPage = pageHeight - padding; + + // Adiciona a primeira página com cabeçalho + doc.setFontSize(18); + doc.text('Relatório Fotográfico Ambiental', padding, padding - 20); + + // Adiciona a imagem em páginas segmentadas + for (let i = 0; i < totalPages; i++) { + if (i > 0) { + doc.addPage(); + } + + const cropY = position - padding; + const cropHeight = Math.min(imgHeightPerPage, imgHeight - cropY); + + doc.addImage(imgData, 'PNG', + padding, padding, + imgWidth, imgHeight, + undefined, undefined, 0, + cropY, imgWidth, cropHeight); + + position += imgHeightPerPage; + + // Adiciona rodapé com número de página + doc.setFontSize(10); + doc.text(`Página ${i + 1} de ${totalPages}`, + doc.internal.pageSize.getWidth() - padding - 50, + doc.internal.pageSize.getHeight() - 20); + } + + doc.save('relatorio_ambiental.pdf'); + }); + }); + } + + // Salva os dados no localStorage e IndexedDB + function saveData() { + const reportData = { + data: document.getElementById('data').value, + hora: document.getElementById('hora').value, + local: document.getElementById('local').value, + infrator_nome: document.getElementById('infrator_nome').value, + infrator_documento: document.getElementById('infrator_documento').value, + latitude: document.getElementById('latitude').value, + longitude: document.getElementById('longitude').value, + descricao: document.getElementById('descricao').value, + fiscal_nome: document.getElementById('fiscal_nome').value, + fiscal_matricula: document.getElementById('fiscal_matricula').value, + signature: signaturePad.isEmpty() ? null : signaturePad.toDataURL(), + autos: [], + termos: [], + photos: [] + }; + + // Coleta dados dos autos + document.querySelectorAll('.auto-item').forEach((auto, index) => { + const autoId = index + 1; + reportData.autos.push({ + numero: document.getElementById(`auto_numero_${autoId}`).value, + descricao: document.getElementById(`auto_descricao_${autoId}`).value + }); + }); + + // Coleta dados dos termos + document.querySelectorAll('.termo-item').forEach((termo, index) => { + const termoId = index + 1; + reportData.termos.push({ + tipo: document.getElementById(`termo_tipo_${termoId}`).value, + numero: document.getElementById(`termo_numero_${termoId}`).value + }); + }); + + // Coleta dados das fotos + document.querySelectorAll('.photo-item:not([style*="display: none"])').forEach(photo => { + reportData.photos.push({ + caption: photo.querySelector('.photo-caption').value, + coords: photo.querySelector('.photo-coords').value, + src: photo.querySelector('.photo-preview').src + }); + }); + + localStorage.setItem('ambientalReportData', JSON.stringify(reportData)); + + if ('indexedDB' in window) { + const request = indexedDB.open('AmbientalReportsDB', 1); + + request.onupgradeneeded = function(event) { + const db = event.target.result; + if (!db.objectStoreNames.contains('reports')) { + db.createObjectStore('reports', { keyPath: 'id' }); + } + if (!db.objectStoreNames.contains('infratores')) { + const store = db.createObjectStore('infratores', { keyPath: 'documento' }); + store.createIndex('nome', 'nome', { unique: false }); + } + }; + + request.onsuccess = function(event) { + const db = event.target.result; + const transaction = db.transaction(['reports', 'infratores'], 'readwrite'); + const reportStore = transaction.objectStore('reports'); + const infratorStore = transaction.objectStore('infratores'); + + // Salva o relatório + const reportToSave = { + id: Date.now(), + data: reportData, + createdAt: new Date() + }; + + reportStore.put(reportToSave); + + // Atualiza/insere o infrator no banco de dados + const infrator = { + nome: reportData.infrator_nome, + documento: reportData.infrator_documento, + autos: reportData.autos.map(auto => auto.numero), + updatedAt: new Date() + }; + + infratorStore.put(infrator); + + alert('Dados salvos com sucesso no banco de dados offline!'); + }; + + request.onerror = function(event) { + console.error('IndexedDB error:', event.target.error); + alert('Dados salvos apenas no localStorage. Erro ao acessar IndexedDB.'); + }; + } else { + alert('Dados salvos com sucesso no localStorage!'); + } + } + + // Carrega os dados do localStorage ou IndexedDB + function loadData() { + const savedData = localStorage.getItem('ambientalReportData'); + if (savedData) { + loadDataFromStorage(savedData); + return; + } + + if ('indexedDB' in window) { + const request = indexedDB.open('AmbientalReportsDB', 1); + + request.onsuccess = function(event) { + const db = event.target.result; + const transaction = db.transaction(['reports'], 'readonly'); + const store = transaction.objectStore('reports'); + const getAllRequest = store.getAll(); + + getAllRequest.onsuccess = function() { + if (getAllRequest.result.length > 0) { + const reports = getAllRequest.result; + reports.sort((a, b) => b.createdAt - a.createdAt); + loadDataFromStorage(JSON.stringify(reports[0].data)); + } else { + alert('Nenhum dado salvo encontrado.'); + } + }; + + getAllRequest.onerror = function() { + alert('Erro ao carregar dados do banco de dados offline.'); + }; + }; + + request.onerror = function(event) { + console.error('IndexedDB error:', event.target.error); + alert('Nenhum dado salvo encontrado.'); + }; + } else { + alert('Nenhum dado salvo encontrado.'); + } + } + + function loadDataFromStorage(savedData) { + const reportData = JSON.parse(savedData); + + // Limpa os containers antes de carregar novos dados + document.getElementById('autos-container').innerHTML = ''; + document.getElementById('termos-container').innerHTML = ''; + document.getElementById('photo-container').innerHTML = ''; + + // Define os contadores para zero + autoCount = 0; + termoCount = 0; + photoCount = 0; + + // Carrega dados básicos + document.getElementById('data').value = reportData.data || ''; + document.getElementById('hora').value = reportData.hora || ''; + document.getElementById('local').value = reportData.local || ''; + document.getElementById('infrator_nome').value = reportData.infrator_nome || ''; + document.getElementById('infrator_documento').value = reportData.infrator_documento || ''; + document.getElementById('latitude').value = reportData.latitude || ''; + document.getElementById('longitude').value = reportData.longitude || ''; + document.getElementById('descricao').value = reportData.descricao || ''; + document.getElementById('fiscal_nome').value = reportData.fiscal_nome || ''; + document.getElementById('fiscal_matricula').value = reportData.fiscal_matricula || ''; + + if (reportData.signature) { + signaturePad.fromDataURL(reportData.signature); + } + + if (reportData.latitude && reportData.longitude) { + updateDMSCoordinates( + parseFloat(reportData.latitude), + parseFloat(reportData.longitude) + ); + + initMap( + parseFloat(reportData.latitude), + parseFloat(reportData.longitude) + ); + } + + // Carrega autos de infração + if (reportData.autos && reportData.autos.length > 0) { + reportData.autos.forEach((auto, index) => { + if (index === 0) { + // Usa o primeiro auto que já existe + document.getElementById('auto_numero_1').value = auto.numero || ''; + document.getElementById('auto_descricao_1').value = auto.descricao || ''; + autoCount = 1; + } else { + // Adiciona novos autos para os demais + addAuto(); + const autoId = index + 1; + document.getElementById(`auto_numero_${autoId}`).value = auto.numero || ''; + document.getElementById(`auto_descricao_${autoId}`).value = auto.descricao || ''; + } + }); + } + + // Carrega termos + if (reportData.termos && reportData.termos.length > 0) { + reportData.termos.forEach((termo, index) => { + if (index === 0) { + // Usa o primeiro termo que já existe + document.getElementById('termo_tipo_1').value = termo.tipo || ''; + document.getElementById('termo_numero_1').value = termo.numero || ''; + termoCount = 1; + } else { + // Adiciona novos termos para os demais + addTermo(); + const termoId = index + 1; + document.getElementById(`termo_tipo_${termoId}`).value = termo.tipo || ''; + document.getElementById(`termo_numero_${termoId}`).value = termo.numero || ''; + } + }); + } + + // Carrega fotos (se existirem URLs salvas) + if (reportData.photos && reportData.photos.length > 0) { + reportData.photos.forEach(photo => { + if (photo.src) { + // Simula um objeto File para carregar a foto + const fakeFile = { + name: 'foto_carregada.png', + type: 'image/png' + }; + + // Cria um objeto de coordenadas + const coords = { + lat: parseFloat(photo.coords.split(',')[0]), + lng: parseFloat(photo.coords.split(',')[1]) + }; + + // Cria um elemento de foto manualmente + photoCount++; + const photoContainer = document.getElementById('photo-container'); + const template = photoContainer.querySelector('.photo-item'); + const newPhoto = template.cloneNode(true); + + newPhoto.style.display = 'block'; + newPhoto.querySelector('.photo-number').textContent = `Figura ${photoCount}`; + newPhoto.querySelector('.photo-preview').src = photo.src; + newPhoto.querySelector('.photo-caption').value = photo.caption || ''; + newPhoto.querySelector('.photo-coords').value = photo.coords || ''; + + newPhoto.querySelector('.remove-photo').addEventListener('click', function() { + photoContainer.removeChild(newPhoto); + currentPhotos = currentPhotos.filter(p => p.id !== newPhoto.dataset.id); + updatePhotoNumbers(); + }); + + const photoId = 'photo-loaded-' + Date.now(); + newPhoto.dataset.id = photoId; + photoContainer.appendChild(newPhoto); + + currentPhotos.push({ + id: photoId, + file: fakeFile, + coords: coords, + element: newPhoto + }); + } + }); + } + + alert('Dados carregados com sucesso!'); + } + + // Event Listeners + document.getElementById('get-location').addEventListener('click', getLocation); + document.getElementById('manual-coords').addEventListener('click', manualCoords); + document.getElementById('print-report').addEventListener('click', printReport); + document.getElementById('generate-pdf').addEventListener('click', generatePDF); + document.getElementById('save-data').addEventListener('click', saveData); + document.getElementById('load-data').addEventListener('click', loadData); + document.getElementById('add-auto').addEventListener('click', addAuto); + document.getElementById('add-termo').addEventListener('click', addTermo); + document.getElementById('clear-signature').addEventListener('click', function() { + signaturePad.clear(); + }); + + document.getElementById('photo-upload').addEventListener('change', function(e) { + const files = e.target.files; + const lat = parseFloat(document.getElementById('latitude').value); + const lng = parseFloat(document.getElementById('longitude').value); + + if (isNaN(lat) || isNaN(lng)) { + alert("Por favor, defina as coordenadas antes de adicionar fotos."); + return; + } + + for (let i = 0; i < files.length; i++) { + addPhoto(files[i], { lat, lng }); + } + + e.target.value = ''; + }); + + // Monitora mudanças na conexão + window.addEventListener('online', updateConnectionStatus); + window.addEventListener('offline', updateConnectionStatus); + + // Atualiza o QR Code quando os dados mudam + document.getElementById('auto_numero_1').addEventListener('change', generateQRCode); + </script> +</body> +</html>