Compare commits

...

16 Commits
1.0.0 ... main

Author SHA1 Message Date
Eder Moraes 1598ccd22f adicionadas novas funcionalidades, agora o app mostra se tem um atendimento em progresso, antes de iniciar o chamado de outro cliente 2025-12-15 00:35:11 -03:00
Eder Moraes 8280bb2ff6 alteração da rota para o pusher e portas de comunicação 2025-12-14 23:53:04 -03:00
Eder Moraes 97b4d2c90e adicionados novas funcionalidades para filtro de chamados, e busca a se há algum na fila na inicialização 2025-11-10 01:20:11 -03:00
Eder Moraes 54cfba5cc4 adicionado menu de contexto com opções para chamar fila, trocar de colab e sair do app 2025-07-12 12:26:16 -03:00
Eder Moraes 4906da2bf0 alterações no comportamento do inicio do atendimento, janela carregada somente depois das opções selecionadas. 2025-07-09 21:03:42 -03:00
Eder Moraes 75d795bd92 correções para o modulo colabs, estava bugando a fila de atendimento 2025-07-04 22:03:11 -03:00
Eder Moraes 97fa71f231 evita abertura de multiplas instancias 2025-06-20 11:08:13 -03:00
Eder Moraes 8eed72c15b adicionado funcionalidade para checar se já possui um atendimento em andamento 2025-06-17 03:29:53 -03:00
Eder Moraes 69ad98af95 correções para o app electronjs 2025-06-16 09:13:37 -03:00
Eder Moraes 47c3b9877a recebe as informações agora via pusher ao invés de requests da api, pra evitar sobrecarga no servidor 2025-06-11 17:53:52 -03:00
Eder Moraes 16a178b585 adicionados recursos graficos, correções para o tempo de liberação para iniciar o atendimento 2025-06-05 20:18:16 -03:00
Eder Moraes b8487d9d4c adicionado animação e controle de trabalho, impede do colaborador iniciar o atendimento até que o cliente chegue ao guichê ou sala. 2025-06-02 23:19:13 -03:00
Eder Moraes e4f49e98b7 adicionado timer para impedir o colaborador iniciar o atendimento antes do cliente chegar na sala. 2025-06-02 22:53:59 -03:00
Eder Moraes 5705acbacc adicionado timer para impedir o colaborador iniciar o atendimento antes do cliente chegar na sala. 2025-06-02 22:51:38 -03:00
Eder Moraes 7999fbdd56 redução do tempo de carga 2025-06-02 13:39:20 -03:00
Eder Moraes 1ec035bc25 adicionado estilos para todas as telas, tema escuro 2025-06-01 11:16:26 -03:00
23 changed files with 751 additions and 281 deletions

View File

@ -5,14 +5,13 @@
<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>
<script src="res/js/jquery/jquery.js"></script>
</head>
<body>
<body id="floating">
<button id="float-button">
<span id="icon">🧑‍🦰</span> <!-- Ícone de exemplo -->
<span id="icon"><img src="res/img/client.png" width="22px"></span> <!-- Ícone de exemplo -->
<span id="count">0</span>
</button>
<script src="floating.js"></script>
</body>
</html>

View File

@ -3,6 +3,11 @@ const countSpan = document.getElementById('count');
// Atualiza a contagem e a cor do botão quando recebe do main process
window.electronAPI.onUpdateCount((value) => {
// Se estiver em atendimento, ignora qualquer atualização de contagem
if (floatButton.classList.contains('em-atendimento')) {
return;
}
countSpan.innerHTML = value ?? 0;
// Verifica a contagem para mudar a cor
if (value > 0) {
@ -12,10 +17,50 @@ window.electronAPI.onUpdateCount((value) => {
}
});
// Altera a cor do botão com base no status do atendimento
window.electronAPI.onAtendimentoStatusChanged((status) => {
if (status === 'iniciado') {
const nomeAtendimento = localStorage.getItem('atendimentoAtualNome');
floatButton.classList.remove('has-items');
floatButton.classList.add('em-atendimento');
countSpan.innerHTML = '-'; // Mostra o hífen
if (nomeAtendimento) {
floatButton.setAttribute('title', `Atendendo: ${nomeAtendimento}`);
}
} else { // 'finalizado'
floatButton.classList.remove('em-atendimento');
floatButton.setAttribute('title', 'Chamar próximo da fila');
// Solicita uma atualização imediata da contagem para refletir o estado atual da fila
window.electronAPI.refreshCount();
}
});
// Ao carregar, verifica se já existe um atendimento em andamento
document.addEventListener('DOMContentLoaded', () => {
const atendimentoAtual = localStorage.getItem('atendimentoAtual');
if (atendimentoAtual) {
const nomeAtendimento = localStorage.getItem('atendimentoAtualNome');
floatButton.classList.remove('has-items');
floatButton.classList.add('em-atendimento');
countSpan.innerHTML = '-'; // Mostra o hífen
if (nomeAtendimento) {
floatButton.setAttribute('title', `Atendendo: ${nomeAtendimento}`);
}
} else {
floatButton.setAttribute('title', 'Chamar próximo da fila');
}
});
// Mostra a janela principal ao clicar
floatButton.addEventListener('click', () => {
window.electronAPI.showMainWindow();
});
// Adiciona um listener para o evento de clique com o botão direito
floatButton.addEventListener('contextmenu', (e) => {
e.preventDefault(); // Impede o menu padrão do navegador
window.electronAPI.showMenu(); // Chama a função para mostrar o menu no processo principal
});
// Ajuste inicial do cursor
floatButton.style.cursor = 'pointer';
floatButton.style.cursor = 'pointer';

View File

@ -2,7 +2,8 @@ const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCount: (callback) => ipcRenderer.on('update-count', (_event, value) => callback(value)),
showMainWindow: () => ipcRenderer.send('chamar-fila')
// Remova a linha abaixo:
// startDrag: (offset) => ipcRenderer.send('drag-float-window', offset)
onAtendimentoStatusChanged: (callback) => ipcRenderer.on('atendimento-status-changed', (_event, status) => callback(status)),
refreshCount: () => ipcRenderer.send('refresh-count'),
showMainWindow: () => ipcRenderer.send('chamar-fila'),
showMenu: () => ipcRenderer.send('show-context-menu')
});

View File

@ -1,20 +1,30 @@
<!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">
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
connect-src 'self' ws://autoatend.linco.work:6001 ws://localhost:6001 wss://aa.linco.work:443;
">
<title>Tela de Atendimento</title>
<link rel="stylesheet" href="style.css">
<script src="js/jquery/jquery.js"></script>
<script src="res/js/jquery/jquery.js"></script>
</head>
<body>
<div id="list-view">
<h1>Fila</h1>
<h3><span id="queue-number" style="color: #3498db;"></span></h3>
<h3><span id="queue-number"></span></h3>
<ul id="item-list">
<!-- Itens serão carregados aqui -->
</ul>
<button id="next-button" disabled>Iniciar atendimento</button>
<button id="next-button" disabled>
<span id="counter-start"></span>
Iniciar atendimento
</button>
<!-- <button id="sendto-button" disabled>Encaminhar</button> -->
<button id="logout-button">Trocar Colaborador</button>
@ -25,7 +35,7 @@
<p>Atendendo: <span id="selected-item-name"></span></p>
<input type="hidden" name="idAtend" id="idAtend">
<textarea id="observation-text" rows="10" cols="50" placeholder="Digite suas observações..."></textarea>
<button id="save-button">Salvar</button>
<button id="save-button">Finalizar atendimento</button>
</div>
<div id="encaminhar-view" style="display: none;">
@ -35,7 +45,9 @@
<button id="save-button">Salvar</button>
</div>
<script src="res/js/pusher.min.js"></script>
<script src="renderer.js"></script>
</body>
</html>

View File

@ -5,7 +5,7 @@
<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>
<script src="res/js/jquery/jquery.js"></script>
</head>
<body class="login-page">
<div class="login-container">

578
main.js
View File

@ -1,9 +1,10 @@
const { app, BrowserWindow, ipcMain, screen, net } = require('electron');
const { app, BrowserWindow, ipcMain, screen, net, dialog, Menu } = require('electron');
// const { app: singleInstanceLock } = require('electron-single-instance');
const path = require('path');
const fs = require('fs');
const { autoUpdater, AppUpdater } = require('electron-updater');
const pjson = require(path.join(__dirname,'','package.json'));
const pjson = require(path.join(__dirname, '', 'package.json'));
let floatingWin;
let mainWin;
@ -13,18 +14,17 @@ let updateWin;
const dataPath = path.join(__dirname, 'data.json'); // Caminho para o JSON (backup local)
const apiUrl = 'https://autoatend.linco.work/api/v1/';
// const apiUrl = 'http://_lara10-autoatend.devel/api/v1/';
const pusherUrl = 'aa.linco.work';
// const pusherUrl = 'localhost';
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
//// carrega o electron-reload, buga ao salvar o main.js por conta do fetchDataFromAPI
// if(!pjson.isBuildNow){
// require('electron-reload')(__dirname,{
// electron: require(`${__dirname}/node_modules/electron`)
// })
// }
//impede que o app seja executado mais de uma vez
const gotTheLock = app.requestSingleInstanceLock();
// Função modificada para buscar dados da API
async function readData() {
@ -32,14 +32,14 @@ async function readData() {
// 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();
return await fetchDataFromAPI();
} catch (error) {
console.error("Erro ao buscar dados da API:", error);
@ -50,27 +50,31 @@ async function readData() {
async function fetchDataFromAPI() {
//executa uma vez e a cada 30 segundos
//TODO propicio para fazer um webhook nessas funções que repetem a chamada de requisições em busca de alterações
getAndUpdateDataStorage();
const timer = setInterval(()=>{
getAndUpdateDataStorage();
},30000);
//!primeira requisição é feita para API
getFirstData();
//! as outras é o websockt que solicita a chamada de requisições em busca de alterações
const updData = setInterval(() => {
getDataAndUpdateFloatingBtn();
}, 3000);
const updVersion = setTimeout(() => {
if (pjson.isBuildNow) {
autoUpdater.checkForUpdates();
}
}, 300000);
}
// Função para coletar a lista de atendimentos do servidor, vai ser chamada uma vez e a cada 30s
async function getAndUpdateDataStorage (){
async function getFirstData() {
const token = await getAuthToken();
const colabId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('idOperator')")
const url = apiUrl + 'get-proximos/'+colabId; // URL de exemplo para enviar a solicitação
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
}
const url = apiUrl + 'get-proximos/' + colabId;
//! checa se o token e o colabId existem
if (!token && !colabId) { console.warn("Token or colabId not found in localStorage. API requests will not be made."); return; }
//! faz o request
const request = net.request({
method: 'GET',
url: url,
@ -80,54 +84,47 @@ async function getAndUpdateDataStorage (){
}
});
//! busca pela resposta
request.on('response', (response) => {
let rawData = '';
response.on('data', (chunk) => {
rawData += chunk;
});
response.on('data', (chunk) => { rawData += chunk; });
response.on('end', () => {
try {
const parsedData = JSON.parse(rawData);
let proximos = parsedData;
if (response.statusCode === 200) {
if(!mainWin.isVisible){
//envia os dados para o loadData atualiza a tela somente se não estiver sendo exibida
mainWin.webContents.send('load-data', proximos);
}
//grava os registros no localstorage
floatingWin.webContents.executeJavaScript("localStorage.setItem('proximos','"+JSON.stringify(proximos)+"')");
floatingWin.webContents.executeJavaScript("localStorage.setItem('proximos','" + JSON.stringify(proximos) + "')");
let count = proximos.length;
//lista a contagem no botão flutuante
floatingWin.webContents.send('update-count', count);
console.log(`Dados carregados com sucesso!`);
} else {
console.error(`Erro na requisição: Status code ${response.statusCode}`, parsedData);
// Lidar com o erro adequadamente, talvez enviando uma mensagem para a janela principal
mainWin.webContents.send('api-error', {
message: `Erro ao chamar atendimentos: ${parsedData.message || 'Erro desconhecido'}`
});
}
} catch (error) {
console.error("Erro ao analisar a resposta JSON:", error);
mainWin.webContents.send('api-error', {
message: `Erro ao processar resposta do servidor.`
message: `Erro ao processar resposta do servidor.`
});
}
});
});
request.on('error', (error) => {
console.error("Erro na requisição:", error);
mainWin.webContents.send('api-error', {
message: `Erro ao chamar atendimento: ${error.message}`
});
});
request.end();
return JSON.parse(await floatingWin.webContents.executeJavaScript("localStorage.getItem('proximos')"));
}
// Função para coletar a lista de atendimentos do servidor, vai ser chamada uma vez e a cada 30s
async function getDataAndUpdateFloatingBtn() {
const proximos = JSON.parse(await floatingWin.webContents.executeJavaScript("localStorage.getItem('proximos')")) ?? [];
let count = proximos.length;
//lista a contagem no botão flutuante
floatingWin.webContents.send('update-count', count);
}
// Função para verificar se o token existe no localStorage
@ -163,7 +160,7 @@ function createLoginWindow() {
icon: "icon.ico",
frame: true,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'login_preload.js'),
contextIsolation: true,
@ -198,7 +195,7 @@ function createOperatorWindow() {
// operatorWin.webContents.openDevTools();
operatorWin.loadFile('operator.html');
operatorWin.on('closed', () => {
operatorWin = null;
});
@ -221,7 +218,7 @@ function createUpdateWindow() {
});
updateWin.loadFile('update.html');
updateWin.on('closed', () => {
updateWin = null;
});
@ -231,7 +228,7 @@ function createUpdateWindow() {
function createFloatingWindow() {
const primaryDisplay = screen.getPrimaryDisplay();
const { width: screenWidth, height: screenHeight } = primaryDisplay.workAreaSize;
const winWidth = 50;
const winWidth = 60;
const winHeight = 70;
floatingWin = new BrowserWindow({
@ -259,7 +256,7 @@ function createFloatingWindow() {
floatingWin.loadFile('floating.html');
floatingWin.webContents.executeJavaScript('localStorage.setItem("version","'+app.getVersion()+'")');
floatingWin.webContents.executeJavaScript('localStorage.setItem("version","' + app.getVersion() + '")');
// Envia a contagem inicial para a janela flutuante
const data = readData();
@ -270,7 +267,6 @@ function createFloatingWindow() {
floatingWin.on('closed', () => {
floatingWin = null;
});
// floatingWin.webContents.openDevTools();
}
function createMainWindow() {
@ -291,9 +287,9 @@ function createMainWindow() {
mainWin.loadFile('index.html');
if(!pjson.isBuildNow) {
mainWin.webContents.openDevTools(); // Descomente para depurar
}
// if(!pjson.isBuildNow) {
// mainWin.webContents.openDevTools(); // Descomente para depurar
// }
mainWin.webContents.send('current_version', pjson.version);
@ -311,71 +307,85 @@ function createMainWindow() {
}
if(pjson.isBuildNow){
if (pjson.isBuildNow) {
autoUpdater.on('update-available', () => {
let pth = autoUpdater.downloadUpdate();
updateWin.show(); updateWin.focus();
updateWin.webContents.send('update_message',`Uma nova versão está dispinível.`);
updateWin.webContents.send('update_percent',pth);
updateWin.webContents.send('update_message', `Uma nova versão está dispinível.`);
updateWin.webContents.send('update_percent', pth);
})
autoUpdater.on('download-progress',(obj) => {
updateWin.webContents.send('update_message',`Estamos baixando uma nova atualização.`);
autoUpdater.on('download-progress', (obj) => {
updateWin.webContents.send('update_message', `Estamos baixando uma nova atualização.`);
});
autoUpdater.on('update-downloaded',(obj) => {
updateWin.webContents.send('update_message',`Download concluído. Aguarde, vamos reiniciar para instalar!`);
setTimeout(()=>{
autoUpdater.on('update-downloaded', (obj) => {
updateWin.webContents.send('update_message', `Download concluído. Aguarde, vamos reiniciar para instalar!`);
setTimeout(() => {
autoUpdater.quitAndInstall();
},5000);
}, 5000);
});
autoUpdater.on('error',err => {
updateWin.webContents.send('update_message',err);
autoUpdater.on('error', err => {
updateWin.webContents.send('update_message', err);
});
}
// 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();
}
}
createUpdateWindow();
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();
if (!updateWin) createUpdateWindow();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', () => {
if (mainWin) {
if (mainWin.isMinimized()) {
mainWin.restore();
}
mainWin.focus();
}
});
if(pjson.isBuildNow){
autoUpdater.checkForUpdates();
}
// 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();
}
}
createUpdateWindow();
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();
if (!updateWin) createUpdateWindow();
}
});
if (pjson.isBuildNow) {
autoUpdater.checkForUpdates();
}
});
}
});
// Função para verificar se já existe um operador selecionado
async function getSelectedOperator() {
@ -418,20 +428,26 @@ async function getSelectedOperatorId() {
});
}
ipcMain.handle('get-pusher-config', async () => {
// Obtenha sua chave e host de forma segura aqui (ambiente, .env, etc.)
const PUSHER_APP_KEY = process.env.PUSHER_APP_KEY || '1feb970af7708cb';
const PUSHER_HOST = process.env.PUSHER_HOST || pusherUrl;
return { appKey: PUSHER_APP_KEY, host: PUSHER_HOST };
});
ipcMain.on('update_version', async (event, arg) => {
if(updateWin){
if(!updateWin.isVisible()){
if (updateWin) {
if (!updateWin.isVisible()) {
updateWin.show();
updateWin.focus();
}else{
} else {
updateWin.focus();
}
} else {
createUpdateWindow();
updateWin.webContents.on('did-finish-load', () => {
updateWin.show();
updateWin.focus();
updateWin.show();
updateWin.focus();
});
}
});
@ -446,76 +462,147 @@ ipcMain.handle('get-count', async () => {
// Ouvir pedido para mostrar a janela principal
ipcMain.on('chamar-fila', async () => {
if (mainWin) {
if (!mainWin.isVisible()) {
mainWin.show();
mainWin.focus();
// Primeiro, verifica se já existe um atendimento em andamento
const atendimentoAtualId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('atendimentoAtual')");
const showMainWindow = () => {
if (mainWin) {
if (!mainWin.isVisible()) {
mainWin.show();
mainWin.focus();
} else {
mainWin.focus();
}
} else {
mainWin.focus();
createMainWindow(); // Cria se não existir
mainWin.webContents.on('did-finish-load', () => {
mainWin.show();
mainWin.focus();
});
}
} else {
createMainWindow(); // Cria se não existir
mainWin.webContents.on('did-finish-load', () => {
mainWin.show();
mainWin.focus();
});
}
const colabId = await getSelectedOperatorId();
const token = await getAuthToken('token');
const url = apiUrl + 'chama-fila-app-colab/'+colabId; // URL de exemplo para enviar a solicitação
// Se um atendimento já estiver em andamento, apenas mostra a janela principal.
// A lógica em renderer.js cuidará de exibir a tela de observação.
if (atendimentoAtualId) {
showMainWindow();
return; // Interrompe a execução aqui
}
const request = net.request({
method: 'GET',
url: url,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
}
});
// Se não houver atendimento em andamento, continua com a lógica original.
const countFila = async () => {
const proximos = JSON.parse(await floatingWin.webContents.executeJavaScript("localStorage.getItem('proximos')")) ?? [];
return proximos.length;
}
request.on('response', (response) => {
let rawData = '';
const requestData = async () => {
response.on('data', (chunk) => {
rawData += chunk;
});
response.on('end', () => {
try {
const parsedData = JSON.parse(rawData);
if (response.statusCode === 200) {
mainWin.webContents.send('select-atend-id', parsedData.data);
console.log(parsedData);
} else {
console.error(`Erro na requisição: Status code ${response.statusCode}`, parsedData);
// Lidar com o erro adequadamente, talvez enviando uma mensagem para a janela principal
mainWin.webContents.send('api-error', {
message: `Erro ao chamar atendimento: ${parsedData.message || 'Erro desconhecido'}`
});
}
} catch (error) {
console.error("Erro ao analisar a resposta JSON:", error);
mainWin.webContents.send('api-error', {
message: `Erro ao processar resposta do servidor.`
});
const colabId = await getSelectedOperatorId();
const token = await getAuthToken('token');
const url = apiUrl + 'chama-fila-app-colab/' + colabId; // URL de exemplo para enviar a solicitação
const request = net.request({
method: 'GET',
url: url,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
}
});
});
request.on('error', (error) => {
console.error("Erro na requisição:", error);
mainWin.webContents.send('api-error', {
message: `Erro ao chamar atendimento: ${error.message}`
request.on('response', (response) => {
let rawData = '';
response.on('data', (chunk) => {
rawData += chunk;
});
response.on('end', () => {
try {
const parsedData = JSON.parse(rawData);
if (response.statusCode === 200) {
mainWin.webContents.send('select-atend-id', parsedData.data);
if (parsedData.data && (parsedData.data.Status === 'Fila' || parsedData.data.Status === 'Chamado')) { showMainWindow(); } else
if (parsedData.data && parsedData.data.Status === 'Atendendo') {
let options2 = {
'title': 'Precisa finalizar antes de chamar o próximo.',
'message': 'Em andamento',
'detail': 'Já possui um atendimento em andamento (Atendendo: ' + parsedData.data.clientName + '), continue e finalize por favor!',
'type': 'error',
'noLink': true,
'buttons': ['Depois', 'Continuar'],
};
dialog.showMessageBox(floatingWin, options2).then(result => {
if (result.response) {
mainWin.webContents.send('show-observation');
showMainWindow();
} else {
mainWin.hide();
};
});
}
// console.log(parsedData);
} else {
console.error(`Erro na requisição: Status code ${response.statusCode}`, parsedData);
// Lidar com o erro adequadamente, talvez enviando uma mensagem para a janela principal
mainWin.webContents.send('api-error', {
message: `Erro ao chamar atendimento: ${parsedData.message || 'Erro desconhecido'}`
});
}
} catch (error) {
console.error("Erro ao analisar a resposta JSON:", error);
mainWin.webContents.send('api-error', {
message: `Erro ao processar resposta do servidor.`
});
}
});
});
});
request.end();
request.on('error', (error) => {
console.error("Erro na requisição:", error);
mainWin.webContents.send('api-error', {
message: `Erro ao chamar atendimento: ${error.message}`
});
});
request.end();
};
let options = {
'title': 'Incie o atendimento quando o cliente chegar na sala.',
'message': 'Chamar um cliente da fila?',
'type': 'warning',
'noLink': true,
'buttons': ['Não', 'Sim'],
};
if (await countFila()) {
dialog.showMessageBox(floatingWin, options).then(result => {
if (result.response) {
requestData();
};
});
} else {
requestData();
}
});
ipcMain.on('select-atend-id',(itemId)=>{
// Ouve um pedido da janela flutuante para forçar a atualização da contagem
ipcMain.on('refresh-count', async () => {
if (floatingWin) {
const proximos = JSON.parse(await floatingWin.webContents.executeJavaScript("localStorage.getItem('proximos')")) ?? [];
floatingWin.webContents.send('update-count', proximos.length);
}
});
ipcMain.on('select-atend-id', (itemId) => {
selectedItemId = itemId;
console.log(selectedItemId);
});
@ -527,9 +614,9 @@ ipcMain.on('iniciar-atendimento', async (event, itemId) => {
const colabId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('idOperator')")
//TODO inicia o atendimento o id do atendimento deve ser requisitado do backend
const url = apiUrl + 'iniciar-atendimento/'+itemId; // URL de exemplo para enviar a solicitação
const url = apiUrl + 'iniciar-atendimento/' + itemId; // URL para enviar a solicitação
// Simula o envio de uma solicitação POST com o ID do item
// envio de uma solicitação POST com o ID do item
const request = net.request({
method: 'GET',
url: url,
@ -564,6 +651,20 @@ ipcMain.on('iniciar-atendimento', async (event, itemId) => {
// A janela principal já mudou a UI ao enviar o evento 'next-step'
});
// Ouve quando um atendimento é iniciado e notifica a janela flutuante
ipcMain.on('atendimento-iniciado', (event, itemId) => {
if (floatingWin) {
floatingWin.webContents.send('atendimento-status-changed', 'iniciado');
}
});
// Ouve quando um atendimento é finalizado e notifica a janela flutuante
ipcMain.on('atendimento-finalizado', () => {
if (floatingWin) {
floatingWin.webContents.send('atendimento-status-changed', 'finalizado');
}
});
// Ouvir clique no botão "Salvar"
ipcMain.on('save-observation', async (event, { itemId, observation }) => {
@ -575,7 +676,7 @@ ipcMain.on('save-observation', async (event, { itemId, observation }) => {
const colabId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('idOperator')")
//TODO inicia o atendimento o id do atendimento deve ser requisitado do backend
const url = apiUrl + 'finalizar-atendimento/'+itemId; // URL de exemplo para enviar a solicitação
const url = apiUrl + 'finalizar-atendimento/' + itemId; // URL de exemplo para enviar a solicitação
const fmData = JSON.stringify({
"colabId": colabId,
@ -631,7 +732,7 @@ ipcMain.on('drag-float-window', (event, { offsetX, offsetY }) => {
*/
ipcMain.on('logout', ()=>{
ipcMain.on('logout', () => {
mainWin.webContents.executeJavaScript(`
localStorage.removeItem("idOperator");
localStorage.removeItem("selectedOperator");
@ -651,7 +752,7 @@ 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,
@ -659,24 +760,25 @@ ipcMain.on('login-attempt', async (event, credentials) => {
'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}");
localStorage.setItem("channel", "${data.channel}");
`).then(() => {
// Fecha a janela de login e abre a de seleção de operador
event.reply('login-response', { success: true });
@ -686,35 +788,35 @@ ipcMain.on('login-attempt', async (event, credentials) => {
} else {
// Login falhou
event.reply('login-response', {
success: false,
message: data.message || 'Falha na autenticação'
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'
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}`
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}`
event.reply('login-response', {
success: false,
message: `Erro: ${error.message}`
});
}
});
@ -736,9 +838,9 @@ ipcMain.on('select-operator', async (event, operator) => {
createUpdateWindow();
});
} catch (error) {
event.reply('operator-response', {
success: false,
message: `Erro: ${error.message}`
event.reply('operator-response', {
success: false,
message: `Erro: ${error.message}`
});
}
});
@ -748,15 +850,15 @@ 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: []
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
@ -797,7 +899,7 @@ ipcMain.handle('get-operators', async () => {
operators: operators
});
} else {
reject({
reject({
success: false,
message: data.message || 'Erro ao obter a lista de colaboradores',
operators: []
@ -805,7 +907,7 @@ ipcMain.handle('get-operators', async () => {
}
} catch (error) {
console.log("Erro ao processar resposta da API:", error);
reject({
reject({
success: false,
message: 'Erro ao processar resposta do servidor',
operators: []
@ -826,10 +928,10 @@ ipcMain.handle('get-operators', async () => {
});
} catch (error) {
console.error('Erro ao obter operadores:', error);
return {
success: false,
message: `Erro: ${error.message}`,
operators: []
return {
success: false,
message: `Erro: ${error.message}`,
operators: []
};
}
});
@ -858,6 +960,66 @@ ipcMain.on('token-exists', async () => {
});
ipcMain.on('sair', ()=>{
app.exit();
ipcMain.on('sair', () => {
let exec = `
localStorage.removeItem("idOperator");
localStorage.removeItem("selectedOperator");
localStorage.removeItem("salaOperator");
localStorage.removeItem("servicosOperator");
`;
let options = {
'title': 'Deseja realmente sair?',
'message': 'Tem certeza que deseja fechar o Auto Atendimento?',
'type': 'question',
'buttons': ['Não', 'Sim'],
'defaultId': 0,
'cancelId': 0,
};
dialog.showMessageBox(floatingWin, options).then(result => {
if (result.response === 1) { // Se o usuário clicou em "Sim" (índice 1)
if (floatingWin) {
floatingWin.webContents.executeJavaScript(exec).then(() => { app.exit(); });
} else if (operatorWin) {
operatorWin.webContents.executeJavaScript(exec).then(() => { app.exit(); });
}
}
});
});
ipcMain.on('show-context-menu', (event) => {
const template = [
{
label: 'Chamar Fila',
click: () => {
// Lógica para chamar atendimento
if (floatingWin) {
ipcMain.emit('chamar-fila');
}
}
},
{
label: 'Trocar Colab',
click: () => {
// Lógica para chamar atendimento
if (floatingWin) {
ipcMain.emit('logout');
}
}
},
{
label: 'Sair',
click: () => {
// Lógica para fechar o aplicativo
if (floatingWin) {
ipcMain.emit('sair');
}
}
}
];
const menu = Menu.buildFromTemplate(template);
menu.popup({ window: floatingWin });
});

View File

@ -5,7 +5,7 @@
<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>
<script src="res/js/jquery/jquery.js"></script>
</head>
<body class="operator-page">
<div class="operator-container">
@ -29,8 +29,10 @@
<script>
$(function(){
let opse = localStorage.getItem('selectedOperator');
let vs = localStorage.getItem('version');
setTimeout(()=>{
$('span.op').text(opse);
$('#version').text(vs);
},2000);
});
</script>

View File

@ -2,6 +2,7 @@ const operatorSelect = document.getElementById('operator-select');
const selectButton = document.getElementById('select-button');
const errorMessage = document.getElementById('error-message');
const quitButton = document.getElementById('sair-button');
const verionSpan = document.getElementById('version');
// Carrega a lista de operadores ao iniciar
window.addEventListener('DOMContentLoaded', async () => {
@ -68,6 +69,12 @@ const selectedOperatorValue = operatorSelect.value;
}
});
window.electronAPI.showVersion((version) => {
verionSpan.textContent = `${version}`;
});
// Recebe resposta do processo de seleção
window.electronAPI.onOperatorResponse((response) => {
if (!response.success) {

View File

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

15
package-lock.json generated
View File

@ -1,15 +1,16 @@
{
"name": "electronjs",
"version": "1.0.0",
"name": "autoatendcolab",
"version": "1.0.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "electronjs",
"version": "1.0.0",
"name": "autoatendcolab",
"version": "1.0.7",
"license": "ISC",
"dependencies": {
"@electron/remote": "^2.1.2",
"electron-single-instance": "^0.0.2",
"electron-updater": "^6.6.2",
"jquery": "^3.7.1"
},
@ -2609,6 +2610,12 @@
"chokidar": "^3.5.2"
}
},
"node_modules/electron-single-instance": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/electron-single-instance/-/electron-single-instance-0.0.2.tgz",
"integrity": "sha512-HWZ0BsGpFRuUU+8Cpf0ecDHYOqptuUyjup62sYnkl19//DtysiTZzeIWO9YXdbow3JErti/Zyi2yHO2e0yn1ew==",
"license": "MIT"
},
"node_modules/electron-updater": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.6.2.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "autoatendcolab",
"version": "1.0.0",
"version": "1.1.3",
"main": "main.js",
"isBuildNow": true,
"scripts": {
@ -38,6 +38,7 @@
"description": "Sistema auxiliar para colaboradores de autoatendimento",
"dependencies": {
"@electron/remote": "^2.1.2",
"electron-single-instance": "^0.0.2",
"electron-updater": "^6.6.2",
"jquery": "^3.7.1"
},
@ -47,4 +48,4 @@
"electron-reload": "^2.0.0-alpha.1",
"nodemon": "^3.1.10"
}
}
}

View File

@ -4,12 +4,21 @@ contextBridge.exposeInMainWorld('electronAPI', {
//carrega todos os atendimentos na fila
onLoadData: (callback) => ipcRenderer.on('load-data', (_event, data) => callback(data)),
//carrega todos os atendimentoa via pusher
getPusherConfig: () => ipcRenderer.invoke('get-pusher-config'),
sendPusherEvent: (callback) => ipcRenderer.on('pusher-event-received', (_event, data) => callback(data)),
//seleciona o atendimento atual
selectAtendID: (callback) => ipcRenderer.on('select-atend-id', (_event, data) => callback(data)),
//inicia o atendimento atual
iniciaAtendimento: (itemId) => ipcRenderer.send('iniciar-atendimento', itemId),
//notifica sobre o status do atendimento
atendimentoIniciado: (itemId) => ipcRenderer.send('atendimento-iniciado', itemId),
atendimentoFinalizado: () => ipcRenderer.send('atendimento-finalizado'),
showObservation: (callback) => ipcRenderer.on('show-observation', (_event) => callback() ),
//salva a observação do atendimento
saveObservation: (data) => ipcRenderer.send('save-observation', data),

View File

@ -10,52 +10,123 @@ const saveButton = document.getElementById('save-button');
const selectedItemNameSpan = document.getElementById('selected-item-name');
const queueNumber = document.getElementById('queue-number');
const idAtend = document.getElementById('idAtend');
const counterStart = document.getElementById('counter-start');
let currentData = [];
let selectedItemId = null;
let selectedItemName = '';
window.electronAPI.onLoadData((data) => {
if(!data){
nextButton.disabled = true;
if (!data) {
return;
}
// Reseta a view para a lista sempre que os dados são carregados
populateList(data[0]);
showListView();
});
async function initializePusher() {
if (typeof window.electronAPI === 'undefined' || !window.electronAPI.getPusherConfig) {
console.error('electronAPI not available or getPusherConfig method missing.');
return;
}
const pusherConfig = await window.electronAPI.getPusherConfig();
const PUSHER_APP_KEY = pusherConfig.appKey;
let host = pusherConfig.host;
const channelLocal = localStorage.getItem('channel');
const colabId = localStorage.getItem('idOperator');
//! checa se ja tem o colabId
if (channelLocal && colabId && PUSHER_APP_KEY) {
Pusher.logToConsole = true;
var pusher = new Pusher(PUSHER_APP_KEY, {
wsHost: host,
wsPort: 80,
wssPort: 443,
forceTLS: true,
enableStats: false,
enabledTransports: ['wss', 'ws'],
cluster: 'mt1'
});
let channel;
if (pusher.connection.state === 'connected') {
pusher.unsubscribe('chat.' + channelLocal + '_' + colabId);
}
channel = pusher.subscribe('chat.' + channelLocal + '_' + colabId);
channel.bind('message-sent', function (r) {
let data = r.data.fila.original;
let count = data.length;
console.log(data);
localStorage.setItem('proximos', JSON.stringify(data));
populateList(r.data.currentData.original);
});
pusher.connection.bind('error', function (err) {
console.error('Pusher connection error: ', err);
});
console.log('Host de conexão: ', host);
} else {
console.warn('User not authenticated or Pusher APP_KEY not available. Private channel not subscribed.');
}
}
initializePusher();
//chama o proximo da fila ao abrir a janela de atendimentos
window.electronAPI.selectAtendID((data)=>{
if(!data){
window.electronAPI.selectAtendID((data) => {
nextButton.disabled = true;
if (!data) {
queueNumber.innerHTML = 'Ninguem aguardando atendimento, fechando a janela em alguns segundos...';
setTimeout(() => {
window.close();
},5000);
return;
}
// Reseta a view para a lista sempre que os dados são carregados ao clicar no botão para abrir a janela
populateList(data);
showListView();
selectedItemId = data.id ?? null;
//data.senhaGen
queueNumber.innerHTML = data ? `NA VEZ: <u style="color:orange;">${data.clientName.toUpperCase()}</u> - ${data.descricaoServico.toUpperCase()}` : 'Ninguem aguardando atendimento';
selectedItemNameSpan.innerHTML = data ? `<u> ${data.clientName.toUpperCase()} </u> <i style="float:right;">[ ${data.senhaGen} ]</i>` : 'Ninguem aguardando atendimento';
// Garante que o item selecionado (ID e Nome) seja o que veio da chamada, sobrescrevendo o da lista.
selectedItemId = data.id ?? null;
selectedItemName = data.clientName ?? '';
//data.senhaGen
queueNumber.innerHTML = data ? `NA VEZ: <u>${data.clientName.toUpperCase()}</u> - ${data.descricaoServico.toUpperCase()}` : 'Ninguem aguardando atendimento';
selectedItemNameSpan.innerHTML = data ? `<u> ${data.clientName.toUpperCase()} </u> <i style="float:right;">[ ${data.senhaGen} ]</i>` : 'Ninguem aguardando atendimento';
});
window.electronAPI.showObservation(() => {
window.electronAPI.iniciaAtendimento(selectedItemId);
showObservationView(); // Muda para a tela de observação
});
// Função para popular a lista de itens
function populateList(currentData) {
const atendimentoEmAndamentoId = localStorage.getItem('atendimentoAtual');
const atendimentoEmAndamentoNome = localStorage.getItem('atendimentoAtualNome');
if (atendimentoEmAndamentoId) {
itemList.innerHTML = `<li>Atendimento com <strong>${(atendimentoEmAndamentoNome || '').toUpperCase()}</strong> em andamento.</li>`;
queueNumber.innerHTML = `EM ATENDIMENTO: <u>${(atendimentoEmAndamentoNome || '').toUpperCase()}</u>`;
nextButton.disabled = true;
return;
}
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 || !currentData) {
itemList.innerHTML = '<li>Fila vazia!</li>';
itemList.innerHTML = '';
setTimeout(() => {
nextButton.disabled = !currentData;
return;
}
}, 5000);
// Seleciona o primeiro item por padrão (ou o próximo disponível)
// Aqui, vamos apenas pegar o primeiro da lista atual
@ -68,10 +139,9 @@ function populateList(currentData) {
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>Fila vazia!</li>';
nextButton.disabled = !currentData;
nextButton.disabled = true;
selectedItemId = null;
selectedItemName = '';
}
@ -86,13 +156,25 @@ function populateList(currentData) {
}
// Verifica o estado ao carregar a janela
document.addEventListener('DOMContentLoaded', () => {
const atendimentoEmAndamentoId = localStorage.getItem('atendimentoAtual');
if (atendimentoEmAndamentoId) {
// Se um atendimento está em andamento, a tela de observação deve ser mostrada
selectedItemId = atendimentoEmAndamentoId;
selectedItemName = localStorage.getItem('atendimentoAtualNome');
selectedItemNameSpan.innerHTML = `<u> ${(selectedItemName || '').toUpperCase()} </u>`;
showObservationView();
}
});
//mostra a tela de listagem e permite iniciar o atendimento
function showListView() {
listView.style.display = 'block';
encaminharView.style.display = 'none';
observationView.style.display = 'none';
observationText.value = ''; // Limpa a textarea
// nextButton.disabled = !selectedItemId; // Habilita/desabilita baseado na seleção
}
showListView();
@ -108,6 +190,12 @@ function showObservationView() {
// // Evento do botão "Iniciar atendimento"
nextButton.addEventListener('click', () => {
if (selectedItemId !== null) {
// Salva o estado de atendimento no localStorage
localStorage.setItem('atendimentoAtual', selectedItemId);
localStorage.setItem('atendimentoAtualNome', selectedItemName);
// Notifica o main process e muda a view
window.electronAPI.atendimentoIniciado(selectedItemId);
window.electronAPI.iniciaAtendimento(selectedItemId);
showObservationView(); // Muda para a tela de observação
} else {
@ -117,7 +205,7 @@ nextButton.addEventListener('click', () => {
logoutButton.addEventListener('click',()=>{
logoutButton.addEventListener('click', () => {
window.electronAPI.logoutApp();
});
@ -126,10 +214,13 @@ logoutButton.addEventListener('click',()=>{
saveButton.addEventListener('click', () => {
const observation = observationText.value;
if (selectedItemId !== null) {
// Limpa o estado de atendimento
localStorage.removeItem('atendimentoAtual');
localStorage.removeItem('atendimentoAtualNome');
window.electronAPI.atendimentoFinalizado();
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.
}
});

BIN
res/img/client.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

9
res/js/pusher.min.js vendored Normal file

File diff suppressed because one or more lines are too long

180
style.css
View File

@ -1,11 +1,49 @@
:root {
--primary-color: #001422;
--secondary-color: rgba(11, 44, 80, 0.6);
--tertiary-color: rgba(8, 20, 61, 0.6);
--lightorange: #f3845c;
--accent-gold: #c0965c; /* Dourado suave */
--medium-gold: #c0965c30; /* Dourado suave2 */
--white: #fff;
--black: #000;
--light-gray: #f5f5f5;
--medium-gray: #e0e0e0;
--dark-gray: #5a5a5a;
--text-color: #303030;
--light-success-color: #39be70;
--success-color: #27ae60;
--dark-success-color: #1e8549;
--light-danger-color: #e97669;
--danger-color: #e74c3c;
--dark-danger-color: #a8372a;
--light-warning-color: #f5b857;
--warning-color: #f39c12;
--dark-warning-color: #e95f2b;
--opac-warning-color: rgba(156, 79, 40, 0.8);
--light-info-color: #62afe2;
--info-color: #2980b9;
--dark-info-color: #2980b954;
--border-radius: 6px;
--box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
--box-shadow-inputs: 0 2px 10px rgba(0, 0, 0, 0.192);
--inset-box-shadow: inset 0 4px 8px rgba(2, 2, 2, 0.8);
--transition: all 0.3s ease;
}
/* Estilos Gerais */
body {
font-family: sans-serif;
color: #FFF;
margin: 0;
padding: 10px;
background-color: transparent; /* Para janela flutuante */
overflow: hidden; /* Evita barras de rolagem indesejadas na flutuante */
-webkit-app-region: drag;
background-color: #001422;
}
body#floating{
background-color: transparent !important;
}
/* Janela Flutuante */
@ -14,27 +52,55 @@ body {
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;
background-color: var(--dark-info-color);
border: 2px solid var(--dark-info-color);
color: var(--light-warning-color);
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);
box-shadow: var(--inset-box-shadow);
transition: background-color 0.3s ease; /* Suaviza a transição de cor */
position: absolute;
top: -6px;
left: 4px;
left: 8px;
-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 */
background-color: var(--opac-warning-color); /* Tom avermelhado/alaranjado com transparência */
box-shadow: var(--inset-box-shadow); /* Sombra mais escura avermelhada */
border: 4px solid var(--dark-warning-color);
animation: pulse-border .5s infinite;
}
/* Nova classe para quando estiver em atendimento */
#float-button.em-atendimento {
background-color: var(--danger-color); /* Vermelho */
border: 4px solid var(--dark-danger-color);
animation: pulse-border .2s infinite; /* Pulso mais rápido */
}
@keyframes pulse-border {
0% {
border-color: var(--medium-gold);
width: 64px;
height: 64px;
}
50% {
width: 66px;
height: 66px;
border-color: var(--accent-gold);
}
100% {
width: 64px;
height: 64px;
border-color: var(--medium-gold);
}
}
#float-button:hover {
@ -42,11 +108,6 @@ body {
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;
@ -56,12 +117,21 @@ body {
/* Para permitir arrastar a janela pelo body (se o botão não ocupar tudo) */
/* body { -webkit-app-region: drag; } */
#queue-number {
color: var(--info-color);
}
#queue-number u {
color: var(--light-info-color);
}
/* Janela Principal */
#list-view, #observation-view {
#list-view, #obs-view {
padding: 20px;
background-color: white; /* Fundo branco para a janela principal */
height: 100vh; /* Ocupa a altura da viewport */
background-color: var(--secondary-color); /* Fundo branco para a janela principal */
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
height: 96vh; /* Ocupa a altura da viewport */
box-sizing: border-box;
}
@ -71,13 +141,15 @@ body {
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;
border: 1px solid var(--tertiary-color);
border-radius: var(--border-radius);
background-color: var(--secondary-color);
padding: 10px;
}
#item-list li {
padding: 8px;
border-bottom: 1px solid #eee;
border-bottom: 1px solid var(--tertiary-color);
}
#item-list li:last-child {
@ -85,7 +157,8 @@ body {
}
#item-list li.selected {
background-color: #e0e0e0;
background-color: var(--secondary-color);
color: var(--light-info-color);
font-weight: bold;
}
@ -114,7 +187,6 @@ textarea {
/* Estilos para a página de login */
.login-page, .operator-page {
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
@ -124,7 +196,7 @@ textarea {
}
.login-container, .operator-container {
background-color: white;
background-color: rgba(11, 44, 80, 0.6);
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
@ -141,14 +213,29 @@ textarea {
font-weight: bold;
}
.form-group input:focus-visible, .form-group select:focus-visible, textarea:focus-visible{
box-shadow: var(--box-shadow-inputs);
outline: none;
}
.form-group input, .form-group select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
background-color: var(--secondary-color);
border: 1px solid var(--tertiary-color);
color: var(--medium-gray);
border-radius: 4px;
font-size: 16px;
}
textarea#observation-text{
width: calc(100% -20px) !important;
padding: 10px;
background-color: var(--secondary-color);
border: 1px solid var(--tertiary-color);
border-radius: 4px;
color: var(--medium-gray);
}
.error-message {
background-color: #ffebee;
color: #c62828;
@ -158,22 +245,59 @@ textarea {
display: none;
}
#login-button, #select-button {
background-color: #0077cc;
button{
color: white;
border: none;
border-radius: 4px;
padding: 12px 20px;
font-size: 16px;
cursor: pointer;
width: 100%;
}
#next-button{
background-color: var(--success-color);
}
#next-button:hover{
background-color: var(--light-success-color);
}
#logout-button{
background-color: var(--danger-color);
}
#logout-button:hover{
background-color: var(--dark-danger-color);
}
#login-button, #select-button {
background-color: var(--info-color);
width: 50%;
}
#login-button:hover, #select-button:hover {
background-color: #005fa3;
background-color: var(--light-info-color);
}
#login-button:disabled, #select-button:disabled {
background-color: #cccccc;
background-color: var(--dark-info-color);
cursor: not-allowed;
}
}
#sair-button{
background-color: #eb574d;
}
#sair-button:hover{
background-color: #e93f2c;
}
#save-button{
background-color: var(--light-warning-color);
}
#save-button:hover{
background-color: var(--warning-color);
}

View File

@ -5,7 +5,7 @@
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'">
<title>Update</title>
<link rel="stylesheet" href="style.css">
<script src="js/jquery/jquery.js"></script>
<script src="res/js/jquery/jquery.js"></script>
</head>
<body class="login-page">
<div class="login-container">