first commit

This commit is contained in:
Eder Moraes 2025-05-29 23:52:48 -03:00
commit 87eef0b2a8
25 changed files with 26643 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
npm-debug.log
.env
# Laravel 4 specific
bootstrap/compiled.php
app/storage/
# Laravel 5 & Lumen specific
public/storage
public/hot
public/.efi-cert
storage/*.key
.env.*.php
.env.php
Homestead.yaml
Homestead.json
# Rocketeer PHP task runner and deployment package. https://github.com/rocketeers/rocketeer
.rocketeer/
.idea/**
storage/framework/views/**
storage/framework/sessions/**
storage/framework/cache/**
storage/logs/**
public/public/uploads/**
public/mobile/platforms/**
public/mobile/node_modules/**
.DS_Store
node_modules/**
dist/**

1
data.json Normal file
View File

@ -0,0 +1 @@
[]

74
floating.html Normal file
View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src https://autoatend.linco.work">
<title>Floating Button</title>
<link rel="stylesheet" href="style.css">
<script src="js/jquery/jquery.js"></script>
</head>
<body>
<button id="float-button">
<span id="icon">🧑‍🦰</span> <!-- Ícone de exemplo -->
<span id="count">0</span>
</button>
<script src="floating.js"></script>
<script>
$(function(){
let apiUrl = 'https://autoatend.linco.work/api/v1/';
let token = localStorage.getItem('authToken');
let colabId = localStorage.getItem('idOperator');
function obtemProximos() {
if (!token && !colabId) {
console.warn("Token or colabId not found in localStorage. API requests will not be made.");
return; // Stop the function if token or colabId is missing
}
$.ajax(apiUrl + 'get-proximos/'+colabId, {
method: 'GET',
headers: { 'Authorization': 'Bearer '+token },
// data: formData,
processData: false,
contentType: false,
dataType: 'JSON',
success: function(response) {
console.log('Resposta:', response);
// Ensure the response is valid JSON before parsing
try {
let proximos = response;
localStorage.setItem('proximos', JSON.stringify(proximos));
let count = proximos.length;
$('#float-button span#count').text(count); // Atualiza o texto do botão. Use .text() instead of .html()
$('#float-button').removeClass(['without-items', 'has-items']).addClass( count > 0 ? 'has-items' : 'without-items');
} catch (e) {
console.error("Error parsing JSON response:", e);
console.error("Response text:", response); // Log the raw response for debugging
}
},
error: function(xhr, status, error) {
console.error('Erro na requisição:', status, error);
console.error('Response Text:', xhr.responseText); // Log the response text for debugging
// Optionally, handle different error codes:
if (xhr.status === 401) {
console.warn("Unauthorized. Token might be invalid.");
// You could redirect the user to a login page here.
} else if (xhr.status === 404) {
console.warn("Resource not found. Check the API endpoint.");
}
}
});
}
// Call obtemProximos initially
obtemProximos();
// Set interval to call obtemProximos every 10 seconds
setInterval(obtemProximos, 30000);
});
</script>
</body>
</html>

21
floating.js Normal file
View File

@ -0,0 +1,21 @@
const floatButton = document.getElementById('float-button');
const countSpan = document.getElementById('count');
// Atualiza a contagem e a cor do botão quando recebe do main process
window.electronAPI.onUpdateCount((value) => {
countSpan.textContent = value;
// Verifica a contagem para mudar a cor
if (value > 0) {
floatButton.classList.add('has-items'); // Adiciona a classe para cor avermelhada
} else {
floatButton.classList.remove('has-items'); // Remove a classe, volta para o azul padrão
}
});
// Mostra a janela principal ao clicar
floatButton.addEventListener('click', () => {
window.electronAPI.showMainWindow();
});
// Ajuste inicial do cursor
floatButton.style.cursor = 'pointer';

8
floating_preload.js Normal file
View File

@ -0,0 +1,8 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCount: (callback) => ipcRenderer.on('update-count', (_event, value) => callback(value)),
showMainWindow: () => ipcRenderer.send('show-main-window')
// Remova a linha abaixo:
// startDrag: (offset) => ipcRenderer.send('drag-float-window', offset)
});

140
index.html Normal file
View File

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src https://autoatend.linco.work">
<title>Aguardando atendimento</title>
<link rel="stylesheet" href="style.css">
<script src="js/jquery/jquery.js"></script>
<style>
body {
margin: 10px;
padding: 0;
font-family: Arial, sans-serif;
background-color: #f4f4f4;
}
</style>
</head>
<body>
<div id="list-view">
<h1>Fila</h1>
<ul id="item-list">
<!-- Itens serão carregados aqui -->
</ul>
<button id="next-button" disabled>Iniciar atendimento</button>
<button id="sendto-button" disabled>Encaminhar</button>
<button id="sair-button">Sair do app</button>
</div>
<div id="obs-view" style="display: none;">
<h1>Observações</h1>
<p>Item selecionado: <span id="selected-item-name"></span></p>
<textarea id="observation-text" rows="10" cols="50" placeholder="Digite suas observações..."></textarea>
<button id="save-button">Salvar</button>
</div>
<div id="encaminhar-view" style="display: none;">
<h1>Observações</h1>
<p>Item selecionado: <span id="selected-item-name"></span></p>
<textarea id="observation-text" rows="10" cols="50" placeholder="Digite suas observações..."></textarea>
<button id="save-button">Salvar</button>
</div>
<script src="renderer.js"></script>
<script>
$(function(){
let apiUrl = 'https://autoatend.linco.work/api/v1/';
let token = localStorage.getItem('authToken');
let colabId = localStorage.getItem('idOperator');
function obtemProximosLocalStorage(){
let datastorage = localStorage.getItem('proximos');
// Adiciona os outros itens apenas para visualização (opcional)
const proximos = JSON.parse(datastorage);
itemList.innerHTML = ''; // Limpa a lista anterior
if (!proximos || proximos.length === 0) {
itemList.innerHTML = '<li>Nenhum item encontrado.</li>';
nextButton.disabled = true;
return;
}
// Seleciona o primeiro item por padrão (ou o próximo disponível)
// Aqui, vamos apenas pegar o primeiro da lista atual
const itemToProcess = proximos[0]; // Pega o primeiro item
if (itemToProcess) {
selectedItemId = itemToProcess.id;
selectedItemName = itemToProcess.clientName;
const li = document.createElement('li');
li.textContent = `${itemToProcess.senhaGen}: ${itemToProcess.clientName.toUpperCase()} - ${itemToProcess.attendanceType.toUpperCase()} - ${itemToProcess.descricaoServico.toUpperCase()}`;
li.dataset.id = itemToProcess.id; // Armazena o ID no elemento
li.classList.add('selected'); // Marca como selecionado visualmente (precisa de CSS)
itemList.appendChild(li);
nextButton.disabled = false;
} else {
itemList.innerHTML = '<li>Nenhum item para processar.</li>';
nextButton.disabled = true;
selectedItemId = null;
selectedItemName = '';
}
//[{"id":57,"userToken":"1feb970af7708cb","colabId":null,"colabObs":null,"serviceId":2,"attendanceType":"Normal","clientName":"Augusto teles","whatsappNumber":null,"notificationType":"audio","Favorito":false,"Status":"Fila","inicioAtendimento":null,"fimAtendimento":null,"duracaoAtendimento":null,"obsAtendimento":null,"subServiceId":0,"senhaGen":"AWTT","descricaoServico":"ENTREGA DE DOCUMENTO"}]
// Adiciona os outros itens apenas para visualização (opcional)
proximos.slice(1).forEach(item => {
const li = document.createElement('li');
li.textContent = `${item.senhaGen}: ${item.clientName.toUpperCase()} - ${item.attendanceType.toUpperCase()} - ${item.descricaoServico.toUpperCase()}`;
itemList.appendChild(li);
});
}
//não vai ser usado timer pois é usado no floatbuttom, ele atualiza com timer
//essa função vai ser chamada apenas quando algum registro já estiver sido atendido
function updRemoteList() {
if (!token && !colabId) {
console.warn("Token or colabId not found in localStorage. API requests will not be made.");
return; // Stop the function if token or colabId is missing
}
$.ajax(apiUrl + 'get-proximos/'+colabId, {
method: 'GET',
headers: { 'Authorization': 'Bearer '+token },
processData: false,
contentType: false,
dataType: 'JSON',
success: function(response) {
console.log('Resposta:', response);
// Ensure the response is valid JSON before parsing
try {
localStorage.setItem('proximos', JSON.stringify(response));
} catch (e) {
console.error("Error parsing JSON response:", e);
console.error("Response text:", response); // Log the raw response for debugging
}
},
error: function(xhr, status, error) {
console.error('Erro na requisição:', status, error);
console.error('Response Text:', xhr.responseText); // Log the response text for debugging
// Optionally, handle different error codes:
if (xhr.status === 401) {
console.warn("Unauthorized. Token might be invalid.");
// You could redirect the user to a login page here.
} else if (xhr.status === 404) {
console.warn("Resource not found. Check the API endpoint.");
}
}
});
}
setTimeout(()=>{
obtemProximosLocalStorage();
},5000);
});
</script>
</body>
</html>

10716
js/jquery/jquery.js vendored Normal file

File diff suppressed because it is too large Load Diff

2
js/jquery/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
js/jquery/jquery.min.map Normal file

File diff suppressed because one or more lines are too long

8617
js/jquery/jquery.slim.js Normal file

File diff suppressed because it is too large Load Diff

2
js/jquery/jquery.slim.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

52
login.html Normal file
View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'">
<title>Login</title>
<link rel="stylesheet" href="style.css">
<script src="js/jquery/jquery.js"></script>
</head>
<body class="login-page">
<div class="login-container">
<h1>Login</h1>
<div id="error-message" class="error-message"></div>
<form id="login-form">
<div class="form-group">
<label for="username">Usuário - CPF/CNPJ:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Senha:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" id="login-button">Entrar</button>
</form>
</div>
<script src="login.js"></script>
<script>
$(function(){
//o campo username deve ser formatado como cpf ou cnpj
function formatarCampoCPFCNPJ() {
var campo = document.getElementById('username');
var valor = campo.value.replace(/\D/g, ''); // Remove todos os caracteres não numéricos
if (valor.length <= 11) {
// Formata como CPF
valor = valor.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
} else {
// Formata como CNPJ
valor = valor.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, '$1.$2.$3/$4-$5');
}
campo.value = valor;
}
$('#username').on('input', formatarCampoCPFCNPJ);
});
</script>
</body>
</html>

47
login.js Normal file
View File

@ -0,0 +1,47 @@
const loginForm = document.getElementById('login-form');
const errorMessage = document.getElementById('error-message');
// Verifica se já existe um token de autenticação
document.addEventListener('DOMContentLoaded', () => {
// Verifica se já existe um token no localStorage
const authToken = localStorage.getItem('authToken');
if (authToken) {
console.log('Token encontrado, redirecionando para seleção de operador');
// Informa ao processo principal que já existe um token
window.electronAPI.tokenExists();
}
});
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
const login = document.getElementById('username').value;
const password = document.getElementById('password').value;
// Limpa mensagens de erro anteriores
errorMessage.textContent = '';
errorMessage.style.display = 'none';
// Desabilita o botão durante o login
const loginButton = document.getElementById('login-button');
loginButton.disabled = true;
loginButton.textContent = 'Autenticando...';
// Envia credenciais para o processo principal
window.electronAPI.login({ login, password });
});
// Recebe resposta do processo de login
window.electronAPI.onLoginResponse((response) => {
const loginButton = document.getElementById('login-button');
loginButton.disabled = false;
loginButton.textContent = 'Entrar';
if (!response.success) {
// Exibe mensagem de erro
errorMessage.textContent = response.message;
errorMessage.style.display = 'block';
}
// Se for bem-sucedido, o processo principal fechará esta janela
});

7
login_preload.js Normal file
View File

@ -0,0 +1,7 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
login: (credentials) => ipcRenderer.send('login-attempt', credentials),
onLoginResponse: (callback) => ipcRenderer.on('login-response', (_event, response) => callback(response)),
tokenExists: () => ipcRenderer.send('token-exists')
});

619
main.js Normal file
View File

@ -0,0 +1,619 @@
const { app, BrowserWindow, ipcMain, screen, net } = require('electron');
const path = require('path');
const fs = require('fs');
const { autoUpdater } = require('electron-updater');
const pjson = require(path.join(__dirname,'','package.json'));
let floatingWin;
let mainWin;
let loginWin;
let operatorWin;
const dataPath = path.join(__dirname, 'data.json'); // Caminho para o JSON (backup local)
const apiUrl = 'https://autoatend.linco.work/api/v1/';
const updUrl = 'https://autoatend.linco.work/public/aa_upd/';
autoUpdater.setFeedURL(updUrl);
if(!pjson.isBuildNow){
require('electron-reload')(__dirname,{
electron: require(`${__dirname}/node_modules/electron`)
})
}
// Função modificada para buscar dados da API
async function readData() {
try {
// Verifica se existe token de autenticação
const token = await getAuthToken();
const colabId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('idOperator')")
if (!token || !colabId) {
console.log("Usuário não autenticado, retornando lista vazia");
return [];
}
// Tenta buscar dados da API
return await fetchDataFromAPI(token,colabId); //não usada aqui vai ser disparada via jquery dentro do modulo
} catch (error) {
console.error("Erro ao buscar dados da API:", error);
// Fallback: tenta ler do arquivo local em caso de falha na API
try {
const rawData = fs.readFileSync(dataPath, 'utf-8');
return JSON.parse(rawData);
} catch (localError) {
console.error("Erro ao ler data.json local:", localError);
return []; // Retorna array vazio em caso de erro
}
}
}
// Função para buscar dados da API
async function fetchDataFromAPI(token,colabId) {
//refactorado
}
// Função para verificar se o token existe no localStorage
async function getAuthToken() {
return new Promise((resolve) => {
if (loginWin && !loginWin.isDestroyed()) {
loginWin.webContents.executeJavaScript('localStorage.getItem("authToken");')
.then(token => resolve(token))
.catch(() => resolve(null));
} else if (mainWin && !mainWin.isDestroyed()) {
mainWin.webContents.executeJavaScript('localStorage.getItem("authToken");')
.then(token => resolve(token))
.catch(() => resolve(null));
} else if (floatingWin && !floatingWin.isDestroyed()) {
floatingWin.webContents.executeJavaScript('localStorage.getItem("authToken");')
.then(token => resolve(token))
.catch(() => resolve(null));
} else if (operatorWin && !operatorWin.isDestroyed()) {
operatorWin.webContents.executeJavaScript('localStorage.getItem("authToken");')
.then(token => resolve(token))
.catch(() => resolve(null));
} else {
resolve(null);
}
});
}
// Função para criar a janela de login
function createLoginWindow() {
loginWin = new BrowserWindow({
width: 500,
height: 500,
frame: true,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'login_preload.js'),
contextIsolation: true,
nodeIntegration: true,
},
});
loginWin.loadFile('login.html');
// loginWin.webContents.openDevTools();
loginWin.on('closed', () => {
loginWin = null;
});
}
// Função para criar a janela de seleção de operador
function createOperatorWindow() {
operatorWin = new BrowserWindow({
width: 1200,//500
height: 600,
frame: true,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'operator_preload.js'),
contextIsolation: true,
nodeIntegration: true,
},
});
// operatorWin.webContents.openDevTools();
operatorWin.loadFile('operator.html');
operatorWin.on('closed', () => {
operatorWin = null;
});
}
function createFloatingWindow() {
const primaryDisplay = screen.getPrimaryDisplay();
const { width: screenWidth, height: screenHeight } = primaryDisplay.workAreaSize;
const winWidth = 50;
const winHeight = 70;
floatingWin = new BrowserWindow({
width: winWidth,
height: winHeight,
x: screenWidth - winWidth + 0,
y: screenHeight - winHeight - 60,
frame: false,
transparent: true,
alwaysOnTop: true,
skipTaskbar: true,
resizable: false,
hasShadow: false,
webPreferences: {
preload: path.join(__dirname, 'floating_preload.js'),
contextIsolation: true,
nodeIntegration: true
},
backgroundColor: '#00000000',
titleBarStyle: 'hidden',
roundedCorners: false
});
floatingWin.loadFile('floating.html');
floatingWin.webContents.executeJavaScript('localStorage.setItem("version","'+app.getVersion()+'")');
// Envia a contagem inicial para a janela flutuante
const data = readData();
floatingWin.webContents.on('did-finish-load', () => {
floatingWin.webContents.send('update-count', data.length);
});
floatingWin.on('closed', () => {
floatingWin = null;
});
// floatingWin.webContents.openDevTools();
}
function createMainWindow() {
mainWin = new BrowserWindow({
width: 1024,
height: 600,
show: false, // Inicia oculta
frame: true, // Sem bordas, título, etc.
autoHideMenuBar: true, // Oculta a barra de menus
menuBarVisible: false, // Garante que a barra de menus começa
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: true,
},
});
mainWin.loadFile('index.html');
if(!pjson.isBuildNow) {
mainWin.webContents.openDevTools(); // Descomente para depurar
}
mainWin.webContents.send('current_version', pjson.version);
mainWin.on('close', (event) => {
// Em vez de fechar, apenas oculta a janela principal
if (!app.isQuitting) {
event.preventDefault();
mainWin.hide();
}
});
mainWin.on('closed', () => {
mainWin = null;
});
}
function verifyUpdates(){
autoUpdater.on('download-progress',(obj) => {
mainWin.webContents.send('update_version',`Estamos baixando uma nova atualização: ${obj.percent.toFixed(2)}%`);
});
autoUpdater.on('update-downloaded',(obj) => {
mainWin.webContents.send('update_version',`Atualização concluída: ${obj.percent.toFixed(2)}%`);
setTimeout(()=>{
autoUpdater.quitAndInstall();
},5000);
});
autoUpdater.on('update-available', () => {
mainWin.webContents.send('update_version',`Uma nova versão está dispinível.`);
})
autoUpdater.on('error',err => {
mainWin.webContents.send('error',err);
});
}
verifyUpdates();
// Inicialização do aplicativo modificada para verificar autenticação
app.whenReady().then(async () => {
// Verifica se o usuário já está autenticado
const token = await getAuthToken();
if (!token) {
// Se não estiver autenticado, mostra a tela de login
createLoginWindow();
} else {
// Se já estiver autenticado, verifica se tem operador selecionado
const operator = await getSelectedOperator();
if (!operator || operator === 'null' || operator === null || operator === undefined || operator === '') {
// Se não tiver operador selecionado, mostra a tela de seleção
createOperatorWindow();
} else {
// Se já tiver operador, inicia normalmente
createFloatingWindow();
createMainWindow();
}
}
app.on('activate', () => {
// No macOS é comum recriar uma janela no aplicativo quando o
// ícone do dock é clicado e não há outras janelas abertas.
if (BrowserWindow.getAllWindows().length === 0) {
// Poderia recriar a janela principal aqui se necessário,
// mas como temos a flutuante, talvez não precise.
if (!floatingWin) createFloatingWindow();
if (!mainWin) createMainWindow();
}
});
});
// Função para verificar se já existe um operador selecionado
async function getSelectedOperator() {
return new Promise((resolve) => {
if (loginWin && !loginWin.isDestroyed()) {
loginWin.webContents.executeJavaScript('localStorage.getItem("selectedOperator");')
.then(operator => resolve(operator))
.catch(() => resolve(null));
} else if (operatorWin && !operatorWin.isDestroyed()) {
operatorWin.webContents.executeJavaScript('localStorage.getItem("selectedOperator");')
.then(operator => resolve(operator))
.catch(() => resolve(null));
} else if (mainWin && !mainWin.isDestroyed()) {
mainWin.webContents.executeJavaScript('localStorage.getItem("selectedOperator");')
.then(operator => resolve(operator))
.catch(() => resolve(null));
} else if (floatingWin && !floatingWin.isDestroyed()) {
floatingWin.webContents.executeJavaScript('localStorage.getItem("selectedOperator");')
.then(operator => resolve(operator))
.catch(() => resolve(null));
} else {
resolve(null);
}
});
}
async function getSelectedOperatorId() {
return new Promise((resolve, reject) => {
if (mainWin && !mainWin.isDestroyed()) {
mainWin.webContents.executeJavaScript('localStorage.getItem("idOperator");')
.then(operatorId => resolve(operatorId))
.catch(() => resolve(null));
} else if (floatingWin && !floatingWin.isDestroyed()) {
floatingWin.webContents.executeJavaScript('localStorage.getItem("idOperator");')
.then(operatorId => resolve(operatorId))
.catch(() => resolve(null));
} else {
reject(new Error('Nenhum componente aberto'));
}
});
}
// Ouvir pedido para mostrar a janela principal
ipcMain.on('show-main-window', async () => {
if (mainWin) {
if (!mainWin.isVisible()) {
const data = readData();
mainWin.webContents.send('load-data', data); // Envia dados ao mostrar
mainWin.show();
mainWin.focus();
} else {
mainWin.focus();
}
} else {
createMainWindow(); // Cria se não existir
mainWin.webContents.on('did-finish-load', () => {
const data = readData();
mainWin.webContents.send('load-data', data);
mainWin.show();
mainWin.focus();
});
}
});
// Ouvir pedido para obter contagem (ex: se o JSON for atualizado)
ipcMain.handle('get-count', async () => {
const data = readData();
return data.length;
});
// Ouvir clique no botão "Próximo"
ipcMain.on('next-step', (event, itemId) => {
console.log('Botão Próximo clicado para o item ID:', itemId); // Log para depuração
const url = 'https://httpbin.org/post'; // URL de exemplo para enviar a solicitação
// Simula o envio de uma solicitação POST com o ID do item
const request = net.request({
method: 'POST',
url: url,
headers: {
'Content-Type': 'application/json'
}
});
request.on('response', (response) => {
console.log(`STATUS: ${response.statusCode}`);
console.log(`HEADERS: ${JSON.stringify(response.headers)}`);
response.on('data', (chunk) => {
console.log(`BODY: ${chunk}`);
});
response.on('end', () => {
console.log('Solicitação concluída.');
// Avisa a janela principal que a solicitação foi feita (opcional)
// mainWin.webContents.send('request-done', itemId);
});
});
request.on('error', (error) => {
console.error(`Erro na solicitação: ${error}`);
// Poderia notificar a UI sobre o erro
});
// Envia o ID como corpo da requisição (exemplo)
request.write(JSON.stringify({ id: itemId }));
request.end();
// Não precisamos esperar a resposta para mudar a UI na janela principal
// A janela principal já mudou a UI ao enviar o evento 'next-step'
});
// Ouvir clique no botão "Salvar"
ipcMain.on('save-observation', (event, { itemId, observation }) => {
console.log(`Salvando observação para item ${itemId}: ${observation}`);
// Aqui você implementaria a lógica para salvar a observação.
// Poderia ser:
// 1. Atualizar o arquivo data.json (cuidado com concorrência se houver muita escrita)
// 2. Salvar em outro arquivo
// 3. Enviar para outra API/banco de dados
// Exemplo simples (apenas log):
console.log("Observação 'salva' (apenas log por enquanto).");
// Opcional: Ler dados novamente e atualizar contagem na janela flutuante
const data = readData();
if (floatingWin) {
floatingWin.webContents.send('update-count', data.length);
}
// Opcional: Fechar ou resetar a janela principal após salvar
if (mainWin) {
mainWin.hide(); // Ou mainWin.webContents.send('reset-view');
}
});
// Permite que a janela flutuante seja arrastada
// REMOVA ou comente este listener inteiro:
/*
ipcMain.on('drag-float-window', (event, { offsetX, offsetY }) => {
if (floatingWin) {
const { x, y } = screen.getCursorScreenPoint();
floatingWin.setPosition(x - offsetX, y - offsetY);
}
});
*/
ipcMain.on('sair', ()=>{
app.exit();
});
// Handler para login
ipcMain.on('login-attempt', async (event, credentials) => {
try {
// Substitua pela URL real da sua API de autenticação
const route = apiUrl + 'login';
const request = net.request({
method: 'POST',
url: route,
headers: {
'Content-Type': 'application/json'
}
});
let responseData = '';
request.on('response', (response) => {
response.on('data', (chunk) => {
responseData += chunk.toString();
console.log("Resposta da API:", responseData); // Adiciona este log para ver a resposta crua
});
response.on('end', () => {
try {
const data = JSON.parse(responseData);
if (data.status === 'Authorized' && data.api_key) {
// Login bem-sucedido
loginWin.webContents.executeJavaScript(`
localStorage.setItem("authToken", "${data.api_key}");
`).then(() => {
// Fecha a janela de login e abre a de seleção de operador
event.reply('login-response', { success: true });
loginWin.close();
createOperatorWindow();
});
} else {
// Login falhou
event.reply('login-response', {
success: false,
message: data.message || 'Falha na autenticação'
});
}
} catch (error) {
console.log("Resposta da API:", error);
event.reply('login-response', {
success: false,
message: 'Erro ao processar resposta do servidor'
});
}
});
});
request.on('error', (error) => {
event.reply('login-response', {
success: false,
message: `Erro de conexão: ${error.message}`
});
});
// Envia as credenciais
request.write(JSON.stringify(credentials));
request.end();
} catch (error) {
event.reply('login-response', {
success: false,
message: `Erro: ${error.message}`
});
}
});
// Handler para seleção de operador
ipcMain.on('select-operator', async (event, operator) => {
try {
// Salva o operador selecionado
operatorWin.webContents.executeJavaScript(`
localStorage.setItem("idOperator", "${operator.id}");
localStorage.setItem("selectedOperator", "${operator.name}");
localStorage.setItem("salaOperator", "${operator.sala}");
localStorage.setItem("servicosOperator", "${operator.servicos}");
`).then(() => {
// Fecha a janela de operador e inicia o aplicativo
operatorWin.close();
createFloatingWindow();
createMainWindow();
});
} catch (error) {
event.reply('operator-response', {
success: false,
message: `Erro: ${error.message}`
});
}
});
// Handler para buscar lista de operadores
ipcMain.handle('get-operators', async () => {
try {
// Verifica se existe token de autenticação
const token = await getAuthToken();
if (!token) {
return {
success: false,
message: 'Não autenticado',
operators: []
};
}
// Aqui você pode fazer uma chamada à API para obter os operadores
// Por enquanto, vamos retornar alguns operadores de exemplo
// Substitua pela URL real da sua API de autenticação
const route = apiUrl + 'colabs/list';
return new Promise((resolve, reject) => {
const request = net.request({
method: 'GET',
url: route,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
let responseData = '';
request.on('response', (response) => {
response.on('data', (chunk) => {
responseData += chunk.toString();
console.log("Resposta da API:", responseData); // Adiciona este log para ver a resposta crua
});
response.on('end', () => {
try {
const data = JSON.parse(responseData);
if (data.colabs) {
const operators = data.colabs.map(colab => ({
id: colab.id,
name: colab.nome.toUpperCase(),
sala: colab.sala,
servicos: colab.servicos
}));
resolve({
success: true,
operators: operators
});
} else {
reject({
success: false,
message: data.message || 'Erro ao obter a lista de colaboradores',
operators: []
});
}
} catch (error) {
console.log("Erro ao processar resposta da API:", error);
reject({
success: false,
message: 'Erro ao processar resposta do servidor',
operators: []
});
}
});
});
request.on('error', (error) => {
reject({
success: false,
message: `Erro de conexão: ${error.message}`,
operators: []
});
});
request.end();
});
} catch (error) {
console.error('Erro ao obter operadores:', error);
return {
success: false,
message: `Erro: ${error.message}`,
operators: []
};
}
});
// Handler para verificar se o token já existe
ipcMain.on('token-exists', async () => {
// Checa se o operador já foi selecionado
const operator = await getSelectedOperator();
// Fecha a janela de login, independentemente do operador já ter sido selecionado
if (loginWin && !loginWin.isDestroyed()) {
loginWin.close();
loginWin = null; // Garante que a referência seja limpa
}
if (!operator || operator === 'null' || operator === null || operator === undefined || operator === '') {
// Se não tiver operador selecionado, mostra a tela de seleção
createOperatorWindow();
} else {
// Se já tiver operador, inicia normalmente
createFloatingWindow();
createMainWindow();
}
});

6
nodemon.json Normal file
View File

@ -0,0 +1,6 @@
{
"ignore": [
"assets/"
],
"ext": "js, json, html, htm"
}

36
operator.html Normal file
View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'">
<title>Selecionar Operador</title>
<link rel="stylesheet" href="style.css">
<script src="js/jquery/jquery.js"></script>
</head>
<body class="operator-page">
<div class="operator-container">
<h1>Selecione o Operador</h1>
<div id="error-message" class="error-message"></div>
<div class="form-group">
<label for="operator-select">Operador atual: <span class="op"></span></label>
<select id="operator-select">
<option value="">Selecione um operador</option>
<!-- Operadores serão carregados aqui -->
</select>
</div>
<button id="select-button" disabled>Continuar</button>
</div>
<script src="operator.js"></script>
<script>
$(function(){
let opse = localStorage.getItem('selectedOperator');
setTimeout(()=>{
$('span.op').text(opse);
},2000);
});
</script>
</body>
</html>

81
operator.js Normal file
View File

@ -0,0 +1,81 @@
const operatorSelect = document.getElementById('operator-select');
const selectButton = document.getElementById('select-button');
const errorMessage = document.getElementById('error-message');
// Carrega a lista de operadores ao iniciar
window.addEventListener('DOMContentLoaded', async () => {
try {
const response = await window.electronAPI.getOperators();
//colabs/list
if (response.success) {
// Preenche o select com os operadores
response.operators.forEach(operator => {
const option = document.createElement('option');
option.value = operator.name;
option.textContent = operator.name;
option.setAttribute('data-sala', operator.sala);
option.setAttribute('data-servicos', operator.servicos);
option.setAttribute('data-id', operator.id);
operatorSelect.appendChild(option);
});
} else {
// Exibe mensagem de erro
errorMessage.textContent = response.message;
errorMessage.style.display = 'block';
// Se houver operadores de fallback, preenche com eles
if (response.operators && response.operators.length > 0) {
response.operators.forEach(operator => {
const option = document.createElement('option');
option.value = operator.name;
option.textContent = operator.name;
operatorSelect.appendChild(option);
});
}
}
} catch (error) {
errorMessage.textContent = `Erro ao carregar operadores: ${error.message}`;
errorMessage.style.display = 'block';
}
});
// Habilita/desabilita o botão com base na seleção
operatorSelect.addEventListener('change', () => {
selectButton.disabled = !operatorSelect.value;
});
// Envia o operador selecionado
selectButton.addEventListener('click', () => {
const selectedOperatorValue = operatorSelect.value;
if (selectedOperatorValue) {
const selectedOption = operatorSelect.options[operatorSelect.selectedIndex];
const selectedOperator = {
value: selectedOperatorValue,
name: selectedOperatorValue, // Assuming name is the same as value
sala: selectedOption.getAttribute('data-sala'),
servicos: selectedOption.getAttribute('data-servicos'),
id: selectedOption.getAttribute('data-id'),
};
selectButton.disabled = true;
selectButton.textContent = 'Processando...';
window.electronAPI.selectOperator(selectedOperator);
}
});
// Recebe resposta do processo de seleção
window.electronAPI.onOperatorResponse((response) => {
if (!response.success) {
// Exibe mensagem de erro
errorMessage.textContent = response.message;
errorMessage.style.display = 'block';
selectButton.disabled = false;
selectButton.textContent = 'Continuar';
}
// Se for bem-sucedido, o processo principal fechará esta janela
});

7
operator_preload.js Normal file
View File

@ -0,0 +1,7 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
getOperators: () => ipcRenderer.invoke('get-operators'),
selectOperator: (operatorName) => ipcRenderer.send('select-operator', operatorName),
onOperatorResponse: (callback) => ipcRenderer.on('operator-response', (_event, response) => callback(response))
});

5809
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

49
package.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "autoatendcolab",
"version": "1.0.0",
"main": "main.js",
"isBuildNow": true,
"scripts": {
"start": "electron .",
"nodemon": "nodemon --exec electron .",
"test": "echo \"Error: no test specified\" && exit 1",
"build": "electron-builder build --win --publish never"
},
"repository": "https://github.com/edermcastro/autoatendcolab",
"publish": {
"provider": "github",
"releaseType": "release"
},
"build": {
"appId": "com.electronjs.autoatend",
"productName": "AutoAtendColab",
"directories": {
"output": "./dist/win-unpacked"
},
"files": [
"**/*"
],
"win": {
"asar": true,
"target": "nsis"
},
"nsis": {
"deleteAppDataOnUninstall": true
}
},
"keywords": [],
"author": "Eder Moraes",
"license": "ISC",
"description": "Sistema auxiliar para colaboradores de autoatendimento",
"dependencies": {
"@electron/remote": "^2.1.2",
"electron-updater": "^6.6.2",
"jquery": "^3.7.1"
},
"devDependencies": {
"electron": "^35.2.1",
"electron-builder": "^26.0.12",
"electron-reload": "^2.0.0-alpha.1",
"nodemon": "^3.1.10"
}
}

18
preload.js Normal file
View File

@ -0,0 +1,18 @@
const { app, contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
onLoadData: (callback) => ipcRenderer.on('load-data', (_event, value) => callback(value)),
sendNextStep: (itemId) => ipcRenderer.send('next-step', itemId),
quitApp : () => ipcRenderer.send('sair'),
saveObservation: (data) => ipcRenderer.send('save-observation', data), // data = { itemId, observation }
updAvailable: () => ipcRenderer.on('update-available',(event,arg)=>{
console.log(arg);
}),
currVersion: () => ipcRenderer.on('current_version',(event,arg)=>{
console.log('Versão atual = ' + arg);
}),
updError: () => ipcRenderer.on('error',(event,arg)=>{
console.log('error = ' + arg);
}),
});

119
renderer.js Normal file
View File

@ -0,0 +1,119 @@
const listView = document.getElementById('list-view');
const observationView = document.getElementById('obs-view');
const encaminharView = document.getElementById('encaminhar-view');
const itemList = document.getElementById('item-list');
const nextButton = document.getElementById('next-button');
const quitButton = document.getElementById('sair-button');
const observationText = document.getElementById('observation-text');
const saveButton = document.getElementById('save-button');
const selectedItemNameSpan = document.getElementById('selected-item-name');
let currentData = [];
let selectedItemId = null;
let selectedItemName = '';
window.electronAPI.onLoadData(() => {
populateList();
// Reseta a view para a lista sempre que os dados são carregados
showListView();
});
// Função para popular a lista de itens
function populateList() {
// let datastorage = localStorage.getItem('proximos');
//
// // Adiciona os outros itens apenas para visualização (opcional)
// const proximos = JSON.parse(datastorage);
//
// itemList.innerHTML = ''; // Limpa a lista anterior
// if (!proximos || proximos.length === 0) {
// itemList.innerHTML = '<li>Nenhum item encontrado.</li>';
// nextButton.disabled = true;
// return;
// }
//
// // Seleciona o primeiro item por padrão (ou o próximo disponível)
// // Aqui, vamos apenas pegar o primeiro da lista atual
// const itemToProcess = proximos[0]; // Pega o primeiro item
// if (itemToProcess) {
// selectedItemId = itemToProcess.id;
// selectedItemName = itemToProcess.clientName;
// const li = document.createElement('li');
// li.textContent = `${itemToProcess.senhaGen}: ${itemToProcess.clientName.toUpperCase()} - ${itemToProcess.attendanceType.toUpperCase()} - ${itemToProcess.descricaoServico.toUpperCase()}`;
// li.dataset.id = itemToProcess.id; // Armazena o ID no elemento
// li.classList.add('selected'); // Marca como selecionado visualmente (precisa de CSS)
// itemList.appendChild(li);
// nextButton.disabled = false;
// } else {
// itemList.innerHTML = '<li>Nenhum item para processar.</li>';
// nextButton.disabled = true;
// selectedItemId = null;
// selectedItemName = '';
// }
//
// // Adiciona os outros itens apenas para visualização (opcional)
// proximos.slice(1).forEach(item => {
// const li = document.createElement('li');
// li.textContent = `${item.senhaGen}: ${item.clientName.toUpperCase()} - ${item.attendanceType.toUpperCase()} - ${item.descricaoServico.toUpperCase()}`;
// itemList.appendChild(li);
// });
}
//mostra a tela de listagem e permite iniciar o atendimento
function showListView() {
populateList();
listView.style.display = 'block';
encaminharView.style.display = 'none';
observationView.style.display = 'none';
observationText.value = ''; // Limpa a textarea
window.electronAPI.currVersion(()=>{});
// nextButton.disabled = !selectedItemId; // Habilita/desabilita baseado na seleção
}
showListView();
//inicia o atendimento
function showObservationView() {
if (!selectedItemId) return; // Não muda se nada estiver selecionado
//obter o id do atendimento de modo asyncrono
selectedItemNameSpan.textContent = selectedItemName || `ID ${selectedItemId}`; // Mostra nome ou ID
listView.style.display = 'none';
observationView.style.display = 'block';
}
// // Evento do botão "Iniciar atendimento"
nextButton.addEventListener('click', () => {
if (selectedItemId !== null) {
console.log("Botão Próximo clicado, enviando ID:", selectedItemId);
window.electronAPI.sendNextStep(selectedItemId); // Envia o ID para o main process
showObservationView(); // Muda para a tela de observação
} else {
console.warn("Nenhum item selecionado para 'Próximo'");
}
});
quitButton.addEventListener('click',()=>{
window.electronAPI.quitApp();
});
//
// // Evento do botão "Salvar"
// saveButton.addEventListener('click', () => {
// const observation = observationText.value.trim();
// if (selectedItemId !== null) {
// window.electronAPI.saveObservation({ itemId: selectedItemId, observation: observation });
// window.location.reload();
// // A janela será escondida pelo main process após salvar (conforme main.js)
// // Se quiser resetar a view sem esconder, chame showListView() aqui.
// }
// });
// Inicialmente, mostra a view da lista (estará vazia até receber dados)

179
style.css Normal file
View File

@ -0,0 +1,179 @@
/* Estilos Gerais */
body {
font-family: sans-serif;
margin: 0;
padding: 0;
background-color: transparent; /* Para janela flutuante */
overflow: hidden; /* Evita barras de rolagem indesejadas na flutuante */
-webkit-app-region: drag;
}
/* Janela Flutuante */
#float-button {
width: 64px;
height: 64px;
border-radius: 50%;
/* Cor padrão (azul) quando não há itens */
background-color: rgba(0, 122, 255, 0.6);
border: transparent;
color: white;
font-size: 16px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: inset 0 4px 8px rgba(2, 2, 2, 0.8);
transition: background-color 0.3s ease; /* Suaviza a transição de cor */
position: absolute;
top: -6px;
left: 4px;
-webkit-app-region: no-drag;
cursor: pointer;
}
/* Nova classe para quando houver itens */
#float-button.has-items {
background-color: rgba(255, 69, 0, 0.7); /* Tom avermelhado/alaranjado com transparência */
box-shadow: inset 0 4px 8px rgba(139, 0, 0, 0.9); /* Sombra mais escura avermelhada */
}
#float-button:hover {
/* Ajusta o hover para ambas as cores base */
filter: brightness(1.2); /* Clareia um pouco no hover */
}
/* Remove a cor de hover específica anterior se existir */
/* #float-button:hover {
background-color: rgba(0, 100, 210, 0.9);
} */
#float-button #icon {
font-size: 24px;
margin-bottom: 5px;
}
/* Para permitir arrastar a janela pelo body (se o botão não ocupar tudo) */
/* body { -webkit-app-region: drag; } */
/* Janela Principal */
#list-view, #observation-view {
padding: 20px;
background-color: white; /* Fundo branco para a janela principal */
height: 100vh; /* Ocupa a altura da viewport */
box-sizing: border-box;
}
#item-list {
list-style: none;
padding: 0;
margin-bottom: 15px;
max-height: 400px; /* Altura máxima para a lista */
overflow-y: auto; /* Barra de rolagem se necessário */
border: 1px solid #ccc;
padding: 10px;
}
#item-list li {
padding: 8px;
border-bottom: 1px solid #eee;
}
#item-list li:last-child {
border-bottom: none;
}
#item-list li.selected {
background-color: #e0e0e0;
font-weight: bold;
}
button {
padding: 10px 15px;
font-size: 14px;
cursor: pointer;
margin-top: 10px;
-webkit-app-region: no-drag;
background: transparent;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
textarea {
width: 95%; /* Ajuste conforme necessário */
margin-top: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
padding: 5px;
}
/* Estilos para a página de login */
.login-page, .operator-page {
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
padding: 0;
}
.login-container, .operator-container {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 350px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input, .form-group select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.error-message {
background-color: #ffebee;
color: #c62828;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
display: none;
}
#login-button, #select-button {
background-color: #0077cc;
color: white;
border: none;
border-radius: 4px;
padding: 12px 20px;
font-size: 16px;
cursor: pointer;
width: 100%;
}
#login-button:hover, #select-button:hover {
background-color: #005fa3;
}
#login-button:disabled, #select-button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}