DeepSeek-Coder-V2/Relatório
2025-03-29 18:17:12 -03:00

1350 lines
55 KiB
Plaintext

<!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 &copy; <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: '&copy; <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>