GRUPO BRASANITAS Facilities Services
D1 Ativo
. As funções abaixo substituem as antigas (em JavaScript, a última declaração com o mesmo nome vence). =================================================================== */ let ultimoErroSync = ""; function salvarFila(fila) { localStorage.setItem("epi_sync_queue", JSON.stringify(fila)); } function atualizarIndicadorFila() { const pend = obterFilaSincronizacao().length; if (pend > 0) atualizarStatusD1('offline', pend); else atualizarStatusD1(navigator.onLine ? 'online' : 'offline', 0); } function atualizarTudo() { atualizarSelectsFuncionarios(); renderizarTabelaFuncionarios(); renderizarTabelaEntregas(); atualizarCardFuncionario(funcionarioSelecionadoId); } async function lerJsonSeguro(res) { try { return await res.json(); } catch { return null; } } function salvarNaFilaSincronizacao(tipo, payload) { const fila = obterFilaSincronizacao(); fila.push({ id: 'sync_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5), tipo, payload: JSON.parse(JSON.stringify(payload)), criadoEm: new Date().toISOString() }); salvarFila(fila); atualizarStatusD1('offline', fila.length); } // true = concluído (sai da fila) | false = tentar de novo depois async function enviarItemFila(item, fila) { const p = item.payload; const h = { 'Content-Type': 'application/json' }; let res; switch (item.tipo) { case 'NOVO_FUNCIONARIO': res = await fetch(`${API_URL}/api/funcionarios`, { method: 'POST', headers: h, body: JSON.stringify(p) }); break; case 'EDITAR_FUNCIONARIO': res = await fetch(`${API_URL}/api/funcionarios`, { method: 'PUT', headers: h, body: JSON.stringify(p) }); break; case 'EXCLUIR_FUNCIONARIO': res = await fetch(`${API_URL}/api/funcionarios?id=${p.id}`, { method: 'DELETE' }); break; case 'NOVA_ENTREGA': res = await fetch(`${API_URL}/api/entregas`, { method: 'POST', headers: h, body: JSON.stringify(p) }); break; case 'DEVOLUCAO': res = await fetch(`${API_URL}/api/entregas`, { method: 'PUT', headers: h, body: JSON.stringify(p) }); break; default: return true; } if (res.ok) { const resp = await lerJsonSeguro(res); const realId = resp && ((resp.data && resp.data.id) || resp.id); if (item.tipo === 'NOVO_FUNCIONARIO' && realId && realId != p.id) { const tempId = p.id; funcionarios = funcionarios.map(f => f.id == tempId ? { ...f, id: realId } : f); entregas = entregas.map(e => e.funcionario_id == tempId ? { ...e, funcionario_id: realId } : e); if (funcionarioSelecionadoId == tempId) funcionarioSelecionadoId = realId; fila.forEach(q => { if (q === item) return; if (q.tipo === 'NOVA_ENTREGA' && q.payload.funcionario_id == tempId) q.payload.funcionario_id = realId; if ((q.tipo === 'EDITAR_FUNCIONARIO' || q.tipo === 'EXCLUIR_FUNCIONARIO') && q.payload.id == tempId) q.payload.id = realId; }); } if (item.tipo === 'NOVA_ENTREGA' && realId && realId != p.id) { const tempId = p.id; entregas = entregas.map(e => e.id == tempId ? { ...e, id: realId } : e); fila.forEach(q => { if (q.tipo === 'DEVOLUCAO' && q.payload.id == tempId) q.payload.id = realId; }); } return true; } // Já não existe no servidor: a exclusão está cumprida if (item.tipo === 'EXCLUIR_FUNCIONARIO' && res.status === 404) return true; // Já existe no servidor: não adianta reenviar if ((item.tipo === 'NOVO_FUNCIONARIO' || item.tipo === 'NOVA_ENTREGA') && res.status === 409) return true; let detalhe = ''; try { detalhe = (await res.text()).slice(0, 150); } catch {} ultimoErroSync = `${item.tipo}: HTTP ${res.status} ${detalhe}`; console.error("Falha no sincronismo:", ultimoErroSync); return false; } async function processarFilaSincronizacao(manual = false) { if (sincronizandoAgora) return; const fila = obterFilaSincronizacao(); if (!navigator.onLine) { atualizarStatusD1('offline', fila.length); if (manual) mostrarToast("Sem conexão à internet. Suas alterações continuam salvas no aparelho."); return; } // Só considera indisponível se der erro de rede ou erro 5xx. // (se /api/health não existir e retornar 404, o servidor está no ar) try { const ping = await fetch(`${API_URL}/api/health`, { method: 'GET' }); if (ping.status >= 500) throw new Error("D1 inacessível"); } catch { atualizarStatusD1('offline', fila.length); if (manual) mostrarToast("Servidor Cloudflare D1 indisponível. Dados salvos localmente."); return; } if (fila.length === 0) { atualizarStatusD1('online', 0); if (manual) { mostrarToast("Tudo sincronizado! Banco de dados Cloudflare D1 atualizado."); await carregarDadosRemotos(); } return; } sincronizandoAgora = true; atualizarStatusD1('sincronizando', fila.length); ultimoErroSync = ""; const idsProcessados = new Set(fila.map(i => i.id)); const restantes = []; for (const item of fila) { try { const ok = await enviarItemFila(item, fila); if (!ok) restantes.push(item); } catch (err) { ultimoErroSync = String(err); restantes.push(item); } } // Preserva itens que o usuário adicionou enquanto o envio rodava const novos = obterFilaSincronizacao().filter(i => !idsProcessados.has(i.id)); const filaFinal = restantes.concat(novos); salvarFila(filaFinal); localStorage.setItem("epi_funcionarios", JSON.stringify(funcionarios)); localStorage.setItem("epi_entregas", JSON.stringify(entregas)); sincronizandoAgora = false; if (filaFinal.length === 0) { atualizarStatusD1('online', 0); mostrarToast("Sincronismo concluído! Todas as alterações subiram para o D1."); await carregarDadosRemotos(); return; } atualizarStatusD1('offline', filaFinal.length); if (manual) { const msg = `Ainda há ${filaFinal.length} item(ns) que o servidor não aceitou.\n\n` + `Último erro: ${ultimoErroSync || 'desconhecido'}\n\n` + `Deseja DESCARTAR esses itens travados da fila? (eles não serão enviados ao D1)`; if (confirm(msg)) { salvarFila([]); atualizarStatusD1('online', 0); await carregarDadosRemotos(); } } else if (novos.length > 0) { setTimeout(() => processarFilaSincronizacao(), 500); } } async function carregarDadosRemotos() { // Nunca sobrescrever dados locais enquanto houver alterações pendentes if (obterFilaSincronizacao().length > 0) return; try { const [resFunc, resEntr] = await Promise.all([ fetch(`${API_URL}/api/funcionarios`), fetch(`${API_URL}/api/entregas`) ]); if (resFunc.ok && resEntr.ok) { const dadosFunc = await resFunc.json(); const dadosEntr = await resEntr.json(); if (dadosFunc.data && Array.isArray(dadosFunc.data)) { funcionarios = dadosFunc.data; localStorage.setItem("epi_funcionarios", JSON.stringify(funcionarios)); } if (dadosEntr.data && Array.isArray(dadosEntr.data)) { entregas = dadosEntr.data; localStorage.setItem("epi_entregas", JSON.stringify(entregas)); } atualizarStatusD1('online', 0); atualizarTudo(); } } catch (err) { console.warn("D1 inacessível no momento. Mantendo dados locais.", err); atualizarIndicadorFila(); } } // Toda ação grava local -> entra na fila -> sincroniza na hora (ordem garantida) async function finalizarAcaoSync(msgOk, msgPendente) { await processarFilaSincronizacao(); mostrarToast(obterFilaSincronizacao().length === 0 ? msgOk : msgPendente); atualizarTudo(); } async function salvarFuncionario(e) { e.preventDefault(); const nome = document.getElementById("func-nome").value.trim(); const re = document.getElementById("func-re").value.trim(); const cargo = document.getElementById("func-cargo").value.trim(); const setor = document.getElementById("func-setor").value.trim(); const admissao = document.getElementById("func-admissao").value; if (funcionarios.some(f => String(f.re || f.cpf || '').trim() === re)) { alert("Já existe um colaborador cadastrado com este RE."); return; } // "cpf" enviado junto por compatibilidade com tabelas D1 antigas const novo = { id: Date.now(), nome, re, cpf: re, cargo, setor, data_admissao: admissao, status: "ATIVO" }; funcionarios.push(novo); localStorage.setItem("epi_funcionarios", JSON.stringify(funcionarios)); salvarNaFilaSincronizacao('NOVO_FUNCIONARIO', novo); closeFuncionarioModal(); atualizarTudo(); await finalizarAcaoSync( "Colaborador cadastrado e sincronizado com o Cloudflare D1!", "Colaborador salvo no aparelho. Aguardando sincronismo com o D1." ); } async function salvarEdicaoFuncionario(e) { e.preventDefault(); const id = parseInt(document.getElementById("edit-func-id").value); const nome = document.getElementById("edit-func-nome").value.trim(); const re = document.getElementById("edit-func-re").value.trim(); const cargo = document.getElementById("edit-func-cargo").value.trim(); const setor = document.getElementById("edit-func-setor").value.trim(); const admissao = document.getElementById("edit-func-admissao").value; if (!nome || !re || !cargo || !admissao) { alert("Preencha todos os campos obrigatórios."); return; } if (funcionarios.some(f => f.id != id && String(f.re || f.cpf || '').trim() === re)) { alert("Já existe outro colaborador com este RE."); return; } const index = funcionarios.findIndex(f => f.id == id); if (index === -1) return; const atualizado = { ...funcionarios[index], nome, re, cpf: re, cargo, setor, data_admissao: admissao }; funcionarios[index] = atualizado; localStorage.setItem("epi_funcionarios", JSON.stringify(funcionarios)); salvarNaFilaSincronizacao('EDITAR_FUNCIONARIO', atualizado); closeEditarFuncionarioModal(); atualizarTudo(); await finalizarAcaoSync( "Colaborador atualizado com sucesso no D1!", "Alterações salvas no aparelho. Aguardando sincronismo com o D1." ); } async function alternarArquivarFuncionario(id) { const func = funcionarios.find(f => f.id == id); if (!func) return; const novoStatus = func.status === 'ARQUIVADO' ? 'ATIVO' : 'ARQUIVADO'; const acaoTexto = novoStatus === 'ARQUIVADO' ? 'arquivar (marcar como desligado)' : 'reativar'; if (!confirm(`Deseja realmente ${acaoTexto} o colaborador ${func.nome}?`)) return; func.status = novoStatus; localStorage.setItem("epi_funcionarios", JSON.stringify(funcionarios)); salvarNaFilaSincronizacao('EDITAR_FUNCIONARIO', func); atualizarTudo(); await finalizarAcaoSync( `Colaborador ${novoStatus === 'ARQUIVADO' ? 'arquivado' : 'reativado'} no D1!`, "Status alterado no aparelho. Aguardando sincronismo com o D1." ); } async function excluirFuncionario(id) { const func = funcionarios.find(f => f.id == id); if (!func) return; const entregasDele = entregas.filter(e => e.funcionario_id == id); let mensagem = `Tem certeza que deseja excluir permanentemente o colaborador ${func.nome} (RE: ${func.re || func.cpf || '-'})?`; if (entregasDele.length > 0) { mensagem += `\n\nAtenção: este colaborador possui ${entregasDele.length} entrega(s) de EPI que também serão removidas. Se ele apenas saiu da empresa, prefira ARQUIVAR para manter a conformidade com a NR-6.`; } if (!confirm(mensagem)) return; const idsEntregas = entregasDele.map(e => e.id); funcionarios = funcionarios.filter(f => f.id != id); entregas = entregas.filter(e => e.funcionario_id != id); localStorage.setItem("epi_funcionarios", JSON.stringify(funcionarios)); localStorage.setItem("epi_entregas", JSON.stringify(entregas)); // Limpa da fila tudo que se refere a este colaborador (evita itens "órfãos" travados) let fila = obterFilaSincronizacao(); const nuncaChegouAoServidor = fila.some(q => q.tipo === 'NOVO_FUNCIONARIO' && q.payload.id == id); fila = fila.filter(q => { if (['NOVO_FUNCIONARIO', 'EDITAR_FUNCIONARIO', 'EXCLUIR_FUNCIONARIO'].includes(q.tipo)) return q.payload.id != id; if (q.tipo === 'NOVA_ENTREGA') return q.payload.funcionario_id != id; if (q.tipo === 'DEVOLUCAO') return !idsEntregas.some(x => x == q.payload.id); return true; }); salvarFila(fila); // Se ele nunca foi enviado ao D1, não há nada para excluir lá if (!nuncaChegouAoServidor) salvarNaFilaSincronizacao('EXCLUIR_FUNCIONARIO', { id }); atualizarIndicadorFila(); if (funcionarioSelecionadoId == id) funcionarioSelecionadoId = null; atualizarTudo(); await finalizarAcaoSync( "Colaborador removido e sincronizado com o D1!", "Exclusão salva no aparelho. Aguardando sincronismo com o D1." ); } async function salvarEntrega(e) { e.preventDefault(); if (!temAssinatura) { alert("A assinatura digital do empregado na tela é obrigatória conforme a NR-6."); return; } const fId = parseInt(document.getElementById("entrega-funcionario").value); const epi = document.getElementById("entrega-epi").value.trim(); const ca = document.getElementById("entrega-ca").value.trim(); const qtd = parseInt(document.getElementById("entrega-qtd").value) || 1; const data = document.getElementById("entrega-data").value; const obs = document.getElementById("entrega-obs").value.trim(); const novaEntrega = { id: Date.now(), funcionario_id: fId, descricao_epi: epi, epi: epi, numero_ca: ca, ca: ca, quantidade: qtd, data_entrega: data, observacoes: obs, assinatura_base64: canvasAssinatura.toDataURL("image/png"), data_devolucao: null, motivo_devolucao: null }; entregas.push(novaEntrega); localStorage.setItem("epi_entregas", JSON.stringify(entregas)); salvarNaFilaSincronizacao('NOVA_ENTREGA', novaEntrega); closeEntregaModal(); atualizarTudo(); await finalizarAcaoSync( "Entrega de EPI salva e sincronizada no Cloudflare D1!", "Entrega salva no aparelho. Sincronizará com o D1 automaticamente." ); } async function salvarDevolucao(e) { e.preventDefault(); const entregaId = parseInt(document.getElementById("devolucao-entrega-id").value); const dataDev = document.getElementById("devolucao-data").value; const motivo = document.getElementById("devolucao-motivo").value; const index = entregas.findIndex(entr => entr.id == entregaId); if (index === -1) return; entregas[index].data_devolucao = dataDev; entregas[index].motivo_devolucao = motivo; localStorage.setItem("epi_entregas", JSON.stringify(entregas)); salvarNaFilaSincronizacao('DEVOLUCAO', { id: entregaId, data_devolucao: dataDev, motivo_devolucao: motivo }); closeDevolucaoModal(); atualizarTudo(); await finalizarAcaoSync( "Devolução registrada com sucesso no D1!", "Devolução salva no aparelho. Sincronizará com o D1 na reconexão." ); }