Compare commits

..

No commits in common. "nest-integration" and "main" have entirely different histories.

14 changed files with 333 additions and 1492 deletions

View File

@ -1,108 +1,51 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="pt-br"> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="Content-Security-Policy" content="
<title>Painel do Operador</title> 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"> <link rel="stylesheet" href="style.css">
<script src="res/js/jquery/jquery.js"></script>
</head> </head>
<body> <body>
<div id="list-view"> <div id="list-view">
<div class="main-container"> <h1>Fila</h1>
<!-- Sidebar/Lista de Espera --> <h3><span id="queue-number"></span></h3>
<div class="queue-sidebar"> <ul id="item-list">
<div class="sidebar-header"> <!-- Itens serão carregados aqui -->
<h2>Fila de Espera</h2> </ul>
<span id="queue-count" class="badge">0</span> <button id="next-button" disabled>
</div>
<div class="queue-list-container">
<table class="queue-table">
<thead>
<tr>
<th>Senha</th>
<th>Cliente</th>
</tr>
</thead>
<tbody id="queue-table-body">
<!-- Filas serão inseridas aqui -->
</tbody>
</table>
<div id="queue-empty" class="empty-state">
Ninguém na fila
</div>
</div>
</div>
<!-- Área Principal / Chamada -->
<div class="call-area">
<div id="no-current" class="no-current-card">
<div class="pulse-icon">🔔</div>
<h2>Aguardando Chamada</h2>
<p>Clique no botão abaixo para chamar o próximo cliente</p>
<button id="call-next-btn" class="btn-primary">CHAMAR PRÓXIMO</button>
</div>
<div id="current-card" class="current-card" style="display: none;">
<div class="card-header">
<span id="card-status-badge" class="status-badge">CHAMADO</span>
<h1 id="card-ticket">---</h1>
</div>
<div class="card-body">
<p id="card-client-name" class="card-client-name">---</p>
<p id="card-service" class="card-service">---</p>
<p id="card-type" class="card-type"></p>
</div>
<div class="card-actions">
<button id="recall-button" class="btn-recall" disabled>RECHAMAR</button>
<button id="forward-button" class="btn-forward" disabled>ENCAMINHAR</button>
<button id="next-button" class="btn-start" disabled>
<span id="counter-start"></span> <span id="counter-start"></span>
INICIAR Iniciar atendimento
</button> </button>
</div> <!-- <button id="sendto-button" disabled>Encaminhar</button> -->
</div> <button id="logout-button">Trocar Colaborador</button>
</div>
</div> </div>
<div class="bottom-bar">
<button id="logout-button" class="btn-logout">Trocar Colaborador</button>
</div>
</div>
<!-- TELA DE OBSERVAÇÕES (atendimento em andamento) -->
<div id="obs-view" style="display: none;"> <div id="obs-view" style="display: none;">
<div class="obs-layout"> <h1>Observações</h1>
<div class="obs-header"> <p>Atendendo: <span id="selected-item-name"></span></p>
<h2>Atendimento em Andamento</h2>
<div class="obs-client-info">
<span id="selected-item-name" class="obs-client-name"></span>
</div>
</div>
<input type="hidden" name="idAtend" id="idAtend"> <input type="hidden" name="idAtend" id="idAtend">
<textarea id="observation-text" rows="10" placeholder="Digite suas observações sobre o atendimento..."></textarea> <textarea id="observation-text" rows="10" cols="50" placeholder="Digite suas observações..."></textarea>
<div class="obs-actions"> <button id="save-button">Finalizar atendimento</button>
<button id="save-button" class="btn-finish">Finalizar atendimento</button>
</div>
</div>
</div> </div>
<!-- MODAL DE ENCAMINHAMENTO --> <div id="encaminhar-view" style="display: none;">
<div id="forward-view" class="modal-view" style="display: none;"> <h1>Observações</h1>
<div class="modal-content"> <p>Atendendo: <span id="selected-item-name"></span></p>
<div class="modal-header"> <textarea id="enc-observation-text" rows="10" cols="50" placeholder="Digite suas observações..."></textarea>
<h2>Encaminhar</h2> <button id="save-button">Salvar</button>
<button id="close-forward" class="btn-close">&times;</button>
</div>
<p style="color: #aaa; font-size: 14px; margin-bottom: 10px;">Selecione para quem encaminhar <span id="forward-ticket" class="highlight"></span>:</p>
<div id="operators-list" class="operators-grid">
<!-- Operadores serão carregados aqui -->
</div>
</div>
</div> </div>
<script src="res/js/socket.io.min.js"></script> <script src="res/js/pusher.min.js"></script>
<script src="renderer.js"></script> <script src="renderer.js"></script>
</body> </body>

View File

@ -34,14 +34,14 @@
//o campo username deve ser formatado como cpf ou cnpj //o campo username deve ser formatado como cpf ou cnpj
function formatarCampoCPFCNPJ() { function formatarCampoCPFCNPJ() {
var campo = document.getElementById('username'); var campo = document.getElementById('username');
var valor = campo.value.replace(/[^a-zA-Z0-9]/g, ''); // Remove apenas o que não for alfanumérico var valor = campo.value.replace(/\D/g, ''); // Remove todos os caracteres não numéricos
if (valor.length <= 11) { if (valor.length <= 11) {
// Formata como CPF // Formata como CPF
valor = valor.replace(/([a-zA-Z0-9]{3})([a-zA-Z0-9]{3})([a-zA-Z0-9]{3})([a-zA-Z0-9]{2})/, '$1.$2.$3-$4'); valor = valor.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
} else { } else {
// Formata como CNPJ // Formata como CNPJ
valor = valor.replace(/([a-zA-Z0-9]{2})([a-zA-Z0-9]{3})([a-zA-Z0-9]{3})([a-zA-Z0-9]{4})([a-zA-Z0-9]{2})/, '$1.$2.$3/$4-$5'); valor = valor.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, '$1.$2.$3/$4-$5');
} }
campo.value = valor; campo.value = valor;
} }

View File

@ -16,8 +16,8 @@ document.addEventListener('DOMContentLoaded', () => {
loginForm.addEventListener('submit', (e) => { loginForm.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
const login = document.getElementById('username').value.replace(/[./-]/g, ''); const login = document.getElementById('username').value;
const password = document.getElementById('password').value.replace(/[./-]/g, ''); const password = document.getElementById('password').value;
// Limpa mensagens de erro anteriores // Limpa mensagens de erro anteriores
errorMessage.textContent = ''; errorMessage.textContent = '';

483
main.js
View File

@ -13,63 +13,19 @@ let operatorWin;
let updateWin; let updateWin;
const dataPath = path.join(__dirname, 'data.json'); // Caminho para o JSON (backup local) const dataPath = path.join(__dirname, 'data.json'); // Caminho para o JSON (backup local)
const settingsPath = path.join(app.getPath('userData'), 'settings.json'); // Caminho para as configurações const apiUrl = 'https://autoatend.linco.work/api/v1/';
// const apiUrl = 'http://_lara10-autoatend.devel/api/v1/';
const pusherUrl = 'aa.linco.work';
// const pusherUrl = 'localhost';
//! api
const apiUrl = 'https://aapi.linco.work/';
//! pusher
const pusherUrl = apiUrl;
autoUpdater.autoDownload = false; autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true; autoUpdater.autoInstallOnAppQuit = true;
//? Função para ler as configurações
function getSettings() {
try {
if (fs.existsSync(settingsPath)) {
const settingsData = fs.readFileSync(settingsPath, 'utf-8');
return JSON.parse(settingsData);
}
} catch (error) {
console.error('Erro ao ler arquivo de configurações: ', error);
}
return {}; // Retorna objeto vazio se o arquivo não existir ou houver erro
}
//? Função para salvar as configurações
function saveSettings(settings) {
try {
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
} catch (error) {
console.error('Erro ao salvar arquivo de configurações:', error);
}
}
ipcMain.handle('get-setting', (event, key) => {
const settings = getSettings();
return settings[key];
});
ipcMain.on('set-setting', (event, key, value) => {
const settings = getSettings();
settings[key] = value;
saveSettings(settings);
app.relaunch();
app.exit();
});
//! final de configurações
//impede que o app seja executado mais de uma vez //impede que o app seja executado mais de uma vez
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();
// Função para configurar inicialização automática
function setAutoLaunch(start) {
app.setLoginItemSettings({
openAtLogin: start,
path: app.getPath('exe'), // Aponta para o executável do seu app
});
}
// Função modificada para buscar dados da API // Função modificada para buscar dados da API
async function readData() { async function readData() {
try { try {
@ -100,7 +56,7 @@ async function fetchDataFromAPI() {
//! as outras é o websockt que solicita a chamada de requisições em busca de alterações //! as outras é o websockt que solicita a chamada de requisições em busca de alterações
const updData = setInterval(() => { const updData = setInterval(() => {
getDataAndUpdateFloatingBtn(); getDataAndUpdateFloatingBtn();
}, 30000); }, 3000);
const updVersion = setTimeout(() => { const updVersion = setTimeout(() => {
if (pjson.isBuildNow) { if (pjson.isBuildNow) {
@ -110,82 +66,65 @@ async function fetchDataFromAPI() {
} }
async function getFirstData() { async function getFirstData() {
const token = await getAuthToken(); const token = await getAuthToken();
const colab = await getSelectedOperator(); const colabId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('idOperator')")
const colabId = await getSelectedOperatorId(); const url = apiUrl + 'get-proximos/' + colabId;
const tenantId = await getTenantId();
const url = apiUrl + 'attendance/next-in-line/' + colabId;
if (!token || !colabId) { //! checa se o token e o colabId existem
console.warn("Token or colabId not found. Skipping API request."); if (!token && !colabId) { console.warn("Token or colabId not found in localStorage. API requests will not be made."); return; }
return [];
}
return new Promise((resolve) => { //! faz o request
const request = net.request({ const request = net.request({
method: 'GET', method: 'GET',
url: url, url: url,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token, 'Authorization': 'Bearer ' + token
'x-tenant-id': tenantId,
'x-colab': colab
} }
}); });
//! busca pela resposta
request.on('response', (response) => { request.on('response', (response) => {
let rawData = ''; let rawData = '';
response.on('data', (chunk) => { rawData += chunk; }); response.on('data', (chunk) => { rawData += chunk; });
response.on('end', async () => { response.on('end', () => {
try { try {
const parsedData = JSON.parse(rawData); const parsedData = JSON.parse(rawData);
let proximos = Array.isArray(parsedData) ? parsedData : []; let proximos = parsedData;
if (response.statusCode === 200) { if (response.statusCode === 200) {
const proximosStr = JSON.stringify(proximos); floatingWin.webContents.executeJavaScript("localStorage.setItem('proximos','" + JSON.stringify(proximos) + "')");
let count = proximos.length;
// Sincroniza o localStorage de todas as janelas importantes floatingWin.webContents.send('update-count', count);
const updateStorageScript = `localStorage.setItem('proximos', ${JSON.stringify(proximosStr)})`;
if (floatingWin && !floatingWin.isDestroyed()) {
floatingWin.webContents.executeJavaScript(updateStorageScript);
floatingWin.webContents.send('update-count', proximos.length);
}
// SÓ atualiza a janela principal se NÃO houver atendimento em andamento
floatingWin.webContents.executeJavaScript("localStorage.getItem('atendimentoAtual')").then(atendimentoAtualId => {
if (!atendimentoAtualId && mainWin && !mainWin.isDestroyed()) {
mainWin.webContents.executeJavaScript(updateStorageScript);
mainWin.webContents.send('load-data', proximos);
}
resolve(proximos);
}).catch(err => {
console.error("Erro ao verificar atendimento atual:", err);
resolve(proximos);
});
} else { } else {
console.error(`Erro API: ${response.statusCode}`); console.error(`Erro na requisição: Status code ${response.statusCode}`, parsedData);
resolve([]);
} }
} catch (error) { } catch (error) {
console.error("Erro JSON:", error); console.error("Erro ao analisar a resposta JSON:", error);
resolve([]); mainWin.webContents.send('api-error', {
message: `Erro ao processar resposta do servidor.`
});
} }
}); });
}); });
request.on('error', (error) => { request.on('error', (error) => {
console.error("Erro rede:", error); console.error("Erro na requisição:", error);
resolve([]);
}); });
request.end(); request.end();
});
return JSON.parse(await floatingWin.webContents.executeJavaScript("localStorage.getItem('proximos')"));
} }
// Atualiza a função de monitoramento para realmente buscar dados a cada 30s (ou o que desejar) // Função para coletar a lista de atendimentos do servidor, vai ser chamada uma vez e a cada 30s
async function getDataAndUpdateFloatingBtn() { async function getDataAndUpdateFloatingBtn() {
await getFirstData();
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 // Função para verificar se o token existe no localStorage
@ -213,29 +152,6 @@ async function getAuthToken() {
}); });
} }
// Função para verificar o tenantId no localStorage
async function getTenantId() {
const checkWindow = async (win) => {
if (win && !win.isDestroyed()) {
try {
const val = await win.webContents.executeJavaScript('localStorage.getItem("tenantId");');
return (val && val !== 'null' && val !== 'undefined') ? val : null;
} catch (e) {
return null;
}
}
return null;
};
// Prioridade para janelas visíveis
let id = await checkWindow(operatorWin);
if (!id) id = await checkWindow(mainWin);
if (!id) id = await checkWindow(floatingWin);
if (!id) id = await checkWindow(loginWin);
return id;
}
// Função para criar a janela de login // Função para criar a janela de login
function createLoginWindow() { function createLoginWindow() {
loginWin = new BrowserWindow({ loginWin = new BrowserWindow({
@ -277,7 +193,6 @@ function createOperatorWindow() {
}); });
// operatorWin.webContents.openDevTools(); // operatorWin.webContents.openDevTools();
operatorWin.webContents.executeJavaScript('localStorage.setItem("version","' + app.getVersion() + '")');
operatorWin.loadFile('operator.html'); operatorWin.loadFile('operator.html');
@ -341,6 +256,8 @@ function createFloatingWindow() {
floatingWin.loadFile('floating.html'); floatingWin.loadFile('floating.html');
floatingWin.webContents.executeJavaScript('localStorage.setItem("version","' + app.getVersion() + '")');
// Envia a contagem inicial para a janela flutuante // Envia a contagem inicial para a janela flutuante
const data = readData(); const data = readData();
floatingWin.webContents.on('did-finish-load', () => { floatingWin.webContents.on('did-finish-load', () => {
@ -427,11 +344,6 @@ if (!gotTheLock) {
// Inicialização do aplicativo modificada para verificar autenticação // Inicialização do aplicativo modificada para verificar autenticação
app.whenReady().then(async () => { app.whenReady().then(async () => {
//define a inicialização automatica do aplicativo ao entrar no windows
const settings = getSettings();
const enableAutoStart = settings.autostart === undefined ? true : settings.autostart;
setAutoLaunch(enableAutoStart);
// Verifica se o usuário já está autenticado // Verifica se o usuário já está autenticado
const token = await getAuthToken(); const token = await getAuthToken();
@ -517,12 +429,10 @@ async function getSelectedOperatorId() {
} }
ipcMain.handle('get-pusher-config', async () => { ipcMain.handle('get-pusher-config', async () => {
// Garante que o host não termine com barra para o Socket.io // Obtenha sua chave e host de forma segura aqui (ambiente, .env, etc.)
let host = pusherUrl; const PUSHER_APP_KEY = process.env.PUSHER_APP_KEY || '1feb970af7708cb';
if (host && host.endsWith('/')) { const PUSHER_HOST = process.env.PUSHER_HOST || pusherUrl;
host = host.slice(0, -1); return { appKey: PUSHER_APP_KEY, host: PUSHER_HOST };
}
return { host: host };
}); });
ipcMain.on('update_version', async (event, arg) => { ipcMain.on('update_version', async (event, arg) => {
@ -549,13 +459,8 @@ ipcMain.handle('get-count', async () => {
return data.length; return data.length;
}); });
let isCallingNext = false;
// Ouvir pedido para mostrar a janela principal // Ouvir pedido para mostrar a janela principal
ipcMain.on('chamar-fila', async () => { ipcMain.on('chamar-fila', async () => {
// Evita múltiplas chamadas simultâneas
if (isCallingNext) return;
isCallingNext = true;
// Primeiro, verifica se já existe um atendimento em andamento // Primeiro, verifica se já existe um atendimento em andamento
const atendimentoAtualId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('atendimentoAtual')"); const atendimentoAtualId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('atendimentoAtual')");
@ -578,48 +483,56 @@ ipcMain.on('chamar-fila', async () => {
} }
// Se um atendimento já estiver em andamento, apenas mostra a janela principal. // 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) { if (atendimentoAtualId) {
showMainWindow(); showMainWindow();
isCallingNext = false; return; // Interrompe a execução aqui
return; }
// 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;
} }
const requestData = async () => { const requestData = async () => {
const colabId = await getSelectedOperatorId(); const colabId = await getSelectedOperatorId();
const colab = await getSelectedOperator(); const token = await getAuthToken('token');
const token = await getAuthToken(); const url = apiUrl + 'chama-fila-app-colab/' + colabId; // URL de exemplo para enviar a solicitação
const tenantId = await getTenantId();
const url = apiUrl + 'attendance/call-next/' + colabId;
const request = net.request({ const request = net.request({
method: 'POST', method: 'GET',
url: url, url: url,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token, 'Authorization': 'Bearer ' + token
'x-tenant-id': tenantId,
'x-colab': colab
} }
}); });
request.on('response', (response) => { request.on('response', (response) => {
let rawData = ''; let rawData = '';
response.on('data', (chunk) => { rawData += chunk; });
response.on('data', (chunk) => {
rawData += chunk;
});
response.on('end', () => { response.on('end', () => {
isCallingNext = false;
try { try {
const parsedData = JSON.parse(rawData); const parsedData = JSON.parse(rawData);
if (response.statusCode === 201 || response.statusCode === 200) { if (response.statusCode === 200) {
if (parsedData) { mainWin.webContents.send('select-atend-id', parsedData.data);
mainWin.webContents.send('select-atend-id', parsedData); if (parsedData.data && (parsedData.data.Status === 'Fila' || parsedData.data.Status === 'Chamado')) { showMainWindow(); } else
if (parsedData.status === 'Fila' || parsedData.status === 'Chamado') { if (parsedData.data && parsedData.data.Status === 'Atendendo') {
showMainWindow();
} else if (parsedData.status === 'Atendendo') {
let options2 = { let options2 = {
'title': 'Precisa finalizar antes de chamar o próximo.', 'title': 'Precisa finalizar antes de chamar o próximo.',
'message': 'Em andamento', 'message': 'Em andamento',
'detail': 'Já possui um atendimento em andamento (Atendendo: ' + parsedData.clientName + '), continue e finalize por favor!', 'detail': 'Já possui um atendimento em andamento (Atendendo: ' + parsedData.data.clientName + '), continue e finalize por favor!',
'type': 'error', 'type': 'error',
'noLink': true,
'buttons': ['Depois', 'Continuar'], 'buttons': ['Depois', 'Continuar'],
}; };
dialog.showMessageBox(floatingWin, options2).then(result => { dialog.showMessageBox(floatingWin, options2).then(result => {
@ -631,11 +544,10 @@ ipcMain.on('chamar-fila', async () => {
}; };
}); });
} }
} else { // console.log(parsedData);
mainWin.webContents.send('select-atend-id', null);
}
} else { } else {
console.error(`Erro na requisição: Status code ${response.statusCode}`, parsedData); 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', { mainWin.webContents.send('api-error', {
message: `Erro ao chamar atendimento: ${parsedData.message || 'Erro desconhecido'}` message: `Erro ao chamar atendimento: ${parsedData.message || 'Erro desconhecido'}`
}); });
@ -650,7 +562,6 @@ ipcMain.on('chamar-fila', async () => {
}); });
request.on('error', (error) => { request.on('error', (error) => {
isCallingNext = false;
console.error("Erro na requisição:", error); console.error("Erro na requisição:", error);
mainWin.webContents.send('api-error', { mainWin.webContents.send('api-error', {
message: `Erro ao chamar atendimento: ${error.message}` message: `Erro ao chamar atendimento: ${error.message}`
@ -660,11 +571,7 @@ ipcMain.on('chamar-fila', async () => {
request.end(); request.end();
}; };
const countFilaValue = async () => {
const stored = await floatingWin.webContents.executeJavaScript("localStorage.getItem('proximos')");
const proximos = JSON.parse(stored || '[]');
return proximos.length;
}
let options = { let options = {
'title': 'Incie o atendimento quando o cliente chegar na sala.', 'title': 'Incie o atendimento quando o cliente chegar na sala.',
@ -674,27 +581,23 @@ ipcMain.on('chamar-fila', async () => {
'buttons': ['Não', 'Sim'], 'buttons': ['Não', 'Sim'],
}; };
const count = await countFilaValue();
if (count > 0) { if (await countFila()) {
dialog.showMessageBox(floatingWin, options).then(result => { dialog.showMessageBox(floatingWin, options).then(result => {
if (result.response === 1) { // 1 é 'Sim' if (result.response) {
requestData(); requestData();
} else { };
isCallingNext = false;
}
}); });
} else { } else {
// Se a fila estiver vazia, apenas abre a janela principal para visualização requestData();
showMainWindow();
isCallingNext = false;
} }
}); });
// Ouve um pedido da janela flutuante para forçar a atualização da contagem // Ouve um pedido da janela flutuante para forçar a atualização da contagem
ipcMain.on('refresh-count', async () => { ipcMain.on('refresh-count', async () => {
if (floatingWin) { if (floatingWin) {
const stored = await floatingWin.webContents.executeJavaScript("localStorage.getItem('proximos')"); const proximos = JSON.parse(await floatingWin.webContents.executeJavaScript("localStorage.getItem('proximos')")) ?? [];
const proximos = JSON.parse(stored || '[]');
floatingWin.webContents.send('update-count', proximos.length); floatingWin.webContents.send('update-count', proximos.length);
} }
}); });
@ -708,114 +611,44 @@ ipcMain.on('select-atend-id', (itemId) => {
ipcMain.on('iniciar-atendimento', async (event, itemId) => { ipcMain.on('iniciar-atendimento', async (event, itemId) => {
const token = await getAuthToken(); const token = await getAuthToken();
const tenantId = await getTenantId(); const colabId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('idOperator')")
const colabId = await getSelectedOperatorId(); //TODO inicia o atendimento o id do atendimento deve ser requisitado do backend
const colab = await getSelectedOperator();
const url = apiUrl + 'attendance/' + itemId + '/start'; const url = apiUrl + 'iniciar-atendimento/' + itemId; // URL para enviar a solicitaçã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({ const request = net.request({
method: 'PATCH', method: 'GET',
url: url, url: url,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token, 'Authorization': 'Bearer ' + token
'x-tenant-id': tenantId,
'x-colab': colab
} }
}); });
request.on('response', (response) => { request.on('response', (response) => {
// console.log(`STATUS: ${response.statusCode}`);
// console.log(`HEADERS: ${JSON.stringify(response.headers)}`);
response.on('data', (chunk) => { response.on('data', (chunk) => {
console.log(`BODY: ${chunk}`); console.log(`BODY: ${chunk}`);
}); });
response.on('end', () => { response.on('end', () => {
console.log('Solicitação concluída.'); 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) => { request.on('error', (error) => {
console.error(`Erro na solicitação: ${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(); request.end();
});
// Ouvir clique no botão "Rechamar" - chama novamente o mesmo atendimento // Não precisamos esperar a resposta para mudar a UI na janela principal
ipcMain.on('rechamar-atendimento', async (event, itemId) => { // A janela principal já mudou a UI ao enviar o evento 'next-step'
const token = await getAuthToken();
const tenantId = await getTenantId();
const colabId = await getSelectedOperatorId();
const colab = await getSelectedOperator();
// Rota correta para atualizar o status do item específico e disparar o chamado na TV
const url = apiUrl + 'attendance/' + itemId + '/status';
const request = net.request({
method: 'PATCH',
url: url,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
'x-tenant-id': tenantId,
'x-colab': colab
}
});
// Enviar o corpo com o status "Chamado"
request.write(JSON.stringify({
status: 'Chamado',
collaboratorId: colabId
}));
request.on('response', (response) => {
response.on('data', (chunk) => {
console.log(`Rechamar BODY: ${chunk}`);
});
response.on('end', () => {
console.log('Rechamada concluída.');
});
});
request.on('error', (error) => {
console.error(`Erro na rechamada: ${error}`);
});
request.end();
});
// Encaminhar atendimento para outro colaborador
ipcMain.on('encaminhar-atendimento', async (event, { itemId, collaboratorId }) => {
const token = await getAuthToken();
const tenantId = await getTenantId();
const colabId = await getSelectedOperatorId();
const colab = await getSelectedOperator();
const url = apiUrl + 'attendance/' + itemId + '/forward';
const request = net.request({
method: 'PATCH',
url: url,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
'x-tenant-id': tenantId,
'x-colab': colab
}
});
request.write(JSON.stringify({ collaboratorId }));
request.on('response', (response) => {
response.on('data', (chunk) => {
console.log(`Encaminhar BODY: ${chunk}`);
});
response.on('end', () => {
console.log('Encaminhamento concluído.');
});
});
request.on('error', (error) => {
console.error(`Erro no encaminhamento: ${error}`);
});
request.end();
}); });
// Ouve quando um atendimento é iniciado e notifica a janela flutuante // Ouve quando um atendimento é iniciado e notifica a janela flutuante
@ -832,26 +665,22 @@ ipcMain.on('atendimento-finalizado', () => {
} }
}); });
// Ouve atualização da fila vinda do renderer (via socket)
ipcMain.on('update-queue', (event, data) => {
// Sempre busca dados frescos da API quando o socket notifica mudança,
// pois o socket pode enviar apenas o objeto da mudança e não a lista completa.
getFirstData();
});
// Ouvir clique no botão "Salvar" // Ouvir clique no botão "Salvar"
ipcMain.on('save-observation', async (event, { itemId, observation }) => { ipcMain.on('save-observation', async (event, { itemId, observation }) => {
//TODO salva a observação e finaliza o atendimento
console.log(`Salvando observação para item ${itemId}: ${observation}`); console.log(`Salvando observação para item ${itemId}: ${observation}`);
const token = await getAuthToken(); const token = await getAuthToken();
const tenantId = await getTenantId(); const colabId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('idOperator')")
const colabId = await getSelectedOperatorId(); //TODO inicia o atendimento o id do atendimento deve ser requisitado do backend
const colab = await getSelectedOperator();
const url = apiUrl + 'attendance/' + itemId + '/finish'; const url = apiUrl + 'finalizar-atendimento/' + itemId; // URL de exemplo para enviar a solicitação
const fmData = JSON.stringify({ const fmData = JSON.stringify({
"observation": observation "colabId": colabId,
"obsAtendimento": observation
}); });
@ -861,9 +690,7 @@ ipcMain.on('save-observation', async (event, { itemId, observation }) => {
url: url, url: url,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token, 'Authorization': 'Bearer ' + token
'x-tenant-id': tenantId,
'x-colab': colab
} }
}); });
@ -873,10 +700,13 @@ ipcMain.on('save-observation', async (event, { itemId, observation }) => {
}); });
response.on('end', () => { response.on('end', () => {
console.log('Solicitação concluída.'); 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) => { request.on('error', (error) => {
console.error(`Erro na solicitação: ${error}`); console.error(`Erro na solicitação: ${error}`);
// Poderia notificar a UI sobre o erro
}); });
// Envia o ID como corpo da requisição (exemplo) // Envia o ID como corpo da requisição (exemplo)
@ -886,10 +716,22 @@ ipcMain.on('save-observation', async (event, { itemId, observation }) => {
// Opcional: Fechar ou resetar a janela principal após salvar // Opcional: Fechar ou resetar a janela principal após salvar
if (mainWin) { if (mainWin) {
mainWin.hide(); 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('logout', () => { ipcMain.on('logout', () => {
mainWin.webContents.executeJavaScript(` mainWin.webContents.executeJavaScript(`
localStorage.removeItem("idOperator"); localStorage.removeItem("idOperator");
@ -908,7 +750,8 @@ ipcMain.on('logout', () => {
// Handler para login // Handler para login
ipcMain.on('login-attempt', async (event, credentials) => { ipcMain.on('login-attempt', async (event, credentials) => {
try { try {
const route = apiUrl + 'auth/login'; // Substitua pela URL real da sua API de autenticação
const route = apiUrl + 'login';
const request = net.request({ const request = net.request({
method: 'POST', method: 'POST',
@ -923,22 +766,19 @@ ipcMain.on('login-attempt', async (event, credentials) => {
request.on('response', (response) => { request.on('response', (response) => {
response.on('data', (chunk) => { response.on('data', (chunk) => {
responseData += chunk.toString(); responseData += chunk.toString();
console.log("Resposta da API:", responseData); // Adiciona este log para ver a resposta crua
}); });
response.on('end', () => { response.on('end', () => {
try { try {
const data = JSON.parse(responseData); const data = JSON.parse(responseData);
if (data.access_token) {
// Tenta encontrar o tenantId em diferentes locais possíveis da resposta
const tenantId = data.tenantId || (data.user && data.user.tenantId) || (data.user && data.user.tenant_id);
console.log(`Login bem-sucedido. Token presente. TenantId encontrado: ${tenantId}`);
if (data.status === 'Authorized' && data.api_key) {
// Login bem-sucedido // Login bem-sucedido
loginWin.webContents.executeJavaScript(` loginWin.webContents.executeJavaScript(`
localStorage.setItem("authToken", "${data.access_token}"); localStorage.setItem("authToken", "${data.api_key}");
localStorage.setItem("tenantId", "${tenantId || ''}"); localStorage.setItem("channel", "${data.channel}");
`).then(() => { `).then(() => {
// Fecha a janela de login e abre a de seleção de operador // Fecha a janela de login e abre a de seleção de operador
event.reply('login-response', { success: true }); event.reply('login-response', { success: true });
@ -954,6 +794,7 @@ ipcMain.on('login-attempt', async (event, credentials) => {
}); });
} }
} catch (error) { } catch (error) {
console.log("Resposta da API:", error);
event.reply('login-response', { event.reply('login-response', {
success: false, success: false,
message: 'Erro ao processar resposta do servidor' message: 'Erro ao processar resposta do servidor'
@ -969,12 +810,8 @@ ipcMain.on('login-attempt', async (event, credentials) => {
}); });
}); });
// Remove máscara do login (CPF/CNPJ) e da senha antes de enviar para a API
const cleanLogin = credentials.login.replace(/[.\-\/]/g, '');
const cleanPassword = credentials.password.replace(/[.\-\/]/g, '');
// Envia as credenciais // Envia as credenciais
request.write(JSON.stringify({ login: cleanLogin, password: cleanPassword })); request.write(JSON.stringify(credentials));
request.end(); request.end();
} catch (error) { } catch (error) {
event.reply('login-response', { event.reply('login-response', {
@ -1013,9 +850,6 @@ ipcMain.handle('get-operators', async () => {
try { try {
// Verifica se existe token de autenticação // Verifica se existe token de autenticação
const token = await getAuthToken(); const token = await getAuthToken();
const tenantId = await getTenantId();
console.log(`Buscando operadores - TenantId: ${tenantId}, Token presente: ${!!token}`);
if (!token) { if (!token) {
return { return {
@ -1025,59 +859,39 @@ ipcMain.handle('get-operators', async () => {
}; };
} }
const route = apiUrl + 'collaborators'; // Aqui você pode fazer uma chamada à API para obter os operadores
// Por enquanto, vamos retornar alguns operadores de exemplo
return new Promise(async (resolve) => { // Substitua pela URL real da sua API de autenticação
const headers = { const route = apiUrl + 'colabs/list';
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
};
// Garante que o tenantId é uma string válida e não "null" ou "undefined"
if (tenantId && tenantId !== 'null' && tenantId !== 'undefined') {
headers['x-tenant-id'] = tenantId;
}
// Adiciona x-colab se disponível
const colab = await getSelectedOperator().catch(() => null);
if (colab && colab !== 'null' && colab !== 'undefined') {
headers['x-colab'] = colab;
}
return new Promise((resolve, reject) => {
const request = net.request({ const request = net.request({
method: 'GET', method: 'GET',
url: route, url: route,
headers: headers headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
}); });
let responseData = ''; let responseData = '';
request.on('response', (response) => { request.on('response', (response) => {
let body = '';
response.on('data', (chunk) => { response.on('data', (chunk) => {
body += chunk.toString(); responseData += chunk.toString();
console.log("Resposta da API:", responseData); // Adiciona este log para ver a resposta crua
}); });
response.on('end', () => { response.on('end', () => {
if (response.statusCode !== 200) {
console.error(`Erro na API (${response.statusCode}):`, body);
resolve({
success: false,
message: `Erro ${response.statusCode}: ${body}`,
operators: []
});
return;
}
try { try {
const data = JSON.parse(body); const data = JSON.parse(responseData);
if (Array.isArray(data)) { if (data.colabs) {
const operators = data.map(colab => ({ const operators = data.colabs.map(colab => ({
id: colab._id, id: colab.id,
name: colab.name.toUpperCase(), name: colab.nome.toUpperCase(),
sala: colab.roomId?.name || 'Sala', sala: colab.sala,
servicos: colab.serviceIds?.map(s => s.name).join(',') || '' servicos: colab.servicos
})); }));
resolve({ resolve({
@ -1085,14 +899,15 @@ ipcMain.handle('get-operators', async () => {
operators: operators operators: operators
}); });
} else { } else {
resolve({ reject({
success: false, success: false,
message: 'Resposta da API não é uma lista válida', message: data.message || 'Erro ao obter a lista de colaboradores',
operators: [] operators: []
}); });
} }
} catch (error) { } catch (error) {
resolve({ console.log("Erro ao processar resposta da API:", error);
reject({
success: false, success: false,
message: 'Erro ao processar resposta do servidor', message: 'Erro ao processar resposta do servidor',
operators: [] operators: []
@ -1102,7 +917,7 @@ ipcMain.handle('get-operators', async () => {
}); });
request.on('error', (error) => { request.on('error', (error) => {
resolve({ reject({
success: false, success: false,
message: `Erro de conexão: ${error.message}`, message: `Erro de conexão: ${error.message}`,
operators: [] operators: []

View File

@ -19,14 +19,6 @@
<option value="">Selecione um operador</option> <option value="">Selecione um operador</option>
<!-- Operadores serão carregados aqui --> <!-- Operadores serão carregados aqui -->
</select> </select>
&nbsp;
<p>
Iniciar automaticamente com o sistema
</p>
<label class="switch">
<input type="checkbox" id="active-switch" checked>
<span class="slider round"></span>
</label>
</div> </div>
<button id="select-button" disabled>Continuar</button> <button id="select-button" disabled>Continuar</button>
@ -37,11 +29,11 @@
<script> <script>
$(function(){ $(function(){
let opse = localStorage.getItem('selectedOperator'); let opse = localStorage.getItem('selectedOperator');
setTimeout(()=>{
let vs = localStorage.getItem('version'); let vs = localStorage.getItem('version');
setTimeout(()=>{
$('span.op').text(opse); $('span.op').text(opse);
$('#version').text(vs); $('#version').text(vs);
},1000); },2000);
}); });
</script> </script>
</body> </body>

View File

@ -3,17 +3,9 @@ const selectButton = document.getElementById('select-button');
const errorMessage = document.getElementById('error-message'); const errorMessage = document.getElementById('error-message');
const quitButton = document.getElementById('sair-button'); const quitButton = document.getElementById('sair-button');
const verionSpan = document.getElementById('version'); const verionSpan = document.getElementById('version');
const autoStartSwitch = document.getElementById('active-switch');
// Carrega a lista de operadores ao iniciar // Carrega a lista de operadores ao iniciar
window.addEventListener('DOMContentLoaded', async () => { window.addEventListener('DOMContentLoaded', async () => {
//... (código existente)
// Carrega o estado do autostart
const autostart = await window.electronAPI.getSetting('autostart');
autoStartSwitch.checked = autostart === undefined ? true : autostart;
try { try {
const response = await window.electronAPI.getOperators(); const response = await window.electronAPI.getOperators();
@ -96,10 +88,7 @@ window.electronAPI.onOperatorResponse((response) => {
// Se for bem-sucedido, o processo principal fechará esta janela // Se for bem-sucedido, o processo principal fechará esta janela
}); });
autoStartSwitch.addEventListener('change', () => {
const isEnabled = autoStartSwitch.checked;
window.electronAPI.setSetting('autostart', isEnabled);
});
quitButton.addEventListener('click',()=>{ quitButton.addEventListener('click',()=>{
window.electronAPI.quitApp(); window.electronAPI.quitApp();

View File

@ -6,6 +6,4 @@ contextBridge.exposeInMainWorld('electronAPI', {
showVersion: (version) => ipcRenderer.on('show-version', version), showVersion: (version) => ipcRenderer.on('show-version', version),
onOperatorResponse: (callback) => ipcRenderer.on('operator-response', (_event, response) => callback(response)), onOperatorResponse: (callback) => ipcRenderer.on('operator-response', (_event, response) => callback(response)),
quitApp : () => ipcRenderer.send('sair'), quitApp : () => ipcRenderer.send('sair'),
getSetting: (key) => ipcRenderer.invoke('get-setting', key),
setSetting: (key, value) => ipcRenderer.send('set-setting', key, value),
}); });

99
package-lock.json generated
View File

@ -1,19 +1,18 @@
{ {
"name": "autoatendcolab", "name": "autoatendcolab",
"version": "1.1.6", "version": "1.0.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "autoatendcolab", "name": "autoatendcolab",
"version": "1.1.6", "version": "1.0.7",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@electron/remote": "^2.1.2", "@electron/remote": "^2.1.2",
"electron-single-instance": "^0.0.2", "electron-single-instance": "^0.0.2",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
"jquery": "^3.7.1", "jquery": "^3.7.1"
"socket.io-client": "^4.8.3"
}, },
"devDependencies": { "devDependencies": {
"electron": "^35.2.1", "electron": "^35.2.1",
@ -953,12 +952,6 @@
"url": "https://github.com/sindresorhus/is?sponsor=1" "url": "https://github.com/sindresorhus/is?sponsor=1"
} }
}, },
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@szmarczak/http-timer": { "node_modules/@szmarczak/http-timer": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@ -2048,10 +2041,9 @@
} }
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
}, },
@ -2751,28 +2743,6 @@
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"node_modules/engine.io-client": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
"integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.18.3",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/env-paths": { "node_modules/env-paths": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
@ -5059,34 +5029,6 @@
"npm": ">= 3.0.0" "npm": ">= 3.0.0"
} }
}, },
"node_modules/socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socks": { "node_modules/socks": {
"version": "2.8.4", "version": "2.8.4",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
@ -5792,27 +5734,6 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}, },
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlbuilder": { "node_modules/xmlbuilder": {
"version": "15.1.1", "version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
@ -5823,14 +5744,6 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "autoatendcolab", "name": "autoatendcolab",
"version": "1.1.9", "version": "1.1.3",
"main": "main.js", "main": "main.js",
"isBuildNow": true, "isBuildNow": true,
"scripts": { "scripts": {
@ -40,8 +40,7 @@
"@electron/remote": "^2.1.2", "@electron/remote": "^2.1.2",
"electron-single-instance": "^0.0.2", "electron-single-instance": "^0.0.2",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
"jquery": "^3.7.1", "jquery": "^3.7.1"
"socket.io-client": "^4.8.3"
}, },
"devDependencies": { "devDependencies": {
"electron": "^35.2.1", "electron": "^35.2.1",

View File

@ -14,20 +14,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
//inicia o atendimento atual //inicia o atendimento atual
iniciaAtendimento: (itemId) => ipcRenderer.send('iniciar-atendimento', itemId), iniciaAtendimento: (itemId) => ipcRenderer.send('iniciar-atendimento', itemId),
//rechama o atendimento atual
rechamarAtendimento: (itemId) => ipcRenderer.send('rechamar-atendimento', itemId),
//encaminha o atendimento atual
getOperators: () => ipcRenderer.invoke('get-operators'),
encaminharAtendimento: (data) => ipcRenderer.send('encaminhar-atendimento', data),
//notifica sobre o status do atendimento //notifica sobre o status do atendimento
atendimentoIniciado: (itemId) => ipcRenderer.send('atendimento-iniciado', itemId), atendimentoIniciado: (itemId) => ipcRenderer.send('atendimento-iniciado', itemId),
atendimentoFinalizado: () => ipcRenderer.send('atendimento-finalizado'), atendimentoFinalizado: () => ipcRenderer.send('atendimento-finalizado'),
//notifica o main process sobre atualização na fila vinda do socket
updateQueue: (data) => ipcRenderer.send('update-queue', data),
showObservation: (callback) => ipcRenderer.on('show-observation', (_event) => callback() ), showObservation: (callback) => ipcRenderer.on('show-observation', (_event) => callback() ),
//salva a observação do atendimento //salva a observação do atendimento
saveObservation: (data) => ipcRenderer.send('save-observation', data), saveObservation: (data) => ipcRenderer.send('save-observation', data),

View File

@ -1,122 +1,104 @@
const listView = document.getElementById('list-view'); const listView = document.getElementById('list-view');
const observationView = document.getElementById('obs-view'); const observationView = document.getElementById('obs-view');
const encaminharView = document.getElementById('encaminhar-view');
// Novos elementos do layout redesenhado const itemList = document.getElementById('item-list');
const queueTableBody = document.getElementById('queue-table-body');
const queueEmpty = document.getElementById('queue-empty');
const queueCount = document.getElementById('queue-count');
const currentCard = document.getElementById('current-card');
const noCurrentCard = document.getElementById('no-current');
const cardTicket = document.getElementById('card-ticket');
const cardStatusBadge = document.getElementById('card-status-badge');
const cardClientName = document.getElementById('card-client-name');
const cardService = document.getElementById('card-service');
const cardType = document.getElementById('card-type');
const nextButton = document.getElementById('next-button'); const nextButton = document.getElementById('next-button');
const recallButton = document.getElementById('recall-button');
const forwardButton = document.getElementById('forward-button');
const logoutButton = document.getElementById('logout-button'); const logoutButton = document.getElementById('logout-button');
const observationText = document.getElementById('observation-text'); const observationText = document.getElementById('observation-text');
const encObservationText = document.getElementById('enc-observation-text');
const saveButton = document.getElementById('save-button'); const saveButton = document.getElementById('save-button');
const selectedItemNameSpan = document.getElementById('selected-item-name'); const selectedItemNameSpan = document.getElementById('selected-item-name');
const queueNumber = document.getElementById('queue-number');
const idAtend = document.getElementById('idAtend'); const idAtend = document.getElementById('idAtend');
const counterStart = document.getElementById('counter-start'); const counterStart = document.getElementById('counter-start');
// Elementos do Modal de Encaminhamento
const forwardView = document.getElementById('forward-view');
const operatorsList = document.getElementById('operators-list');
const closeForward = document.getElementById('close-forward');
const forwardTicketSpan = document.getElementById('forward-ticket');
let currentData = []; let currentData = [];
let selectedItemId = null; let selectedItemId = null;
let selectedItemName = ''; let selectedItemName = '';
// ===========================
// FLAG que trava o ID do atendimento chamado.
// Quando o call-next retorna e define o atendimento atual,
// este flag impede que atualizações da fila sobrescrevam o selectedItemId.
// ===========================
let calledAtendimentoData = null; // Guarda os dados completos do atendimento chamado
window.electronAPI.onLoadData((data) => { window.electronAPI.onLoadData((data) => {
// Se já estiver em atendimento, ignora atualizações da lista para evitar flickering no botão nextButton.disabled = true;
if (localStorage.getItem('atendimentoAtual')) {
return;
}
if (!data) { if (!data) {
return; return;
} }
populateQueueTable(data); // Reseta a view para a lista sempre que os dados são carregados
populateList(data[0]);
}); });
async function initializeSocket() { async function initializePusher() {
if (typeof window.electronAPI === 'undefined' || !window.electronAPI.getPusherConfig) { if (typeof window.electronAPI === 'undefined' || !window.electronAPI.getPusherConfig) {
console.error('electronAPI not available or getPusherConfig method missing.'); console.error('electronAPI not available or getPusherConfig method missing.');
return; return;
} }
const config = await window.electronAPI.getPusherConfig(); const pusherConfig = await window.electronAPI.getPusherConfig();
let host = config.host; const PUSHER_APP_KEY = pusherConfig.appKey;
let host = pusherConfig.host;
const tenantId = localStorage.getItem('tenantId'); const channelLocal = localStorage.getItem('channel');
const token = localStorage.getItem('authToken'); const colabId = localStorage.getItem('idOperator');
const colab = localStorage.getItem('selectedOperator');
//! checa se ja tem o colabId e o tenantId //! checa se ja tem o colabId
if (tenantId && token) { if (channelLocal && colabId && PUSHER_APP_KEY) {
// Conexão Socket.io adaptada do Pusher Pusher.logToConsole = true;
const socket = io(host, {
auth: { token }, var pusher = new Pusher(PUSHER_APP_KEY, {
extraHeaders: { wsHost: host,
'x-tenant-id': tenantId, wsPort: 80,
'x-colab': colab || 'N/A' 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);
}); });
socket.on('connect', () => { pusher.connection.bind('error', function (err) {
console.log('Socket.io conectado ao host: ', host); console.error('Pusher connection error: ', err);
});
// Evento que substitui o bind do Pusher
socket.on('queueUpdate', (data) => {
console.log('Notificação de fila recebida:', data);
// Avisa o processo principal que houve mudança.
// O main vai buscar a lista completa e atualizar todas as janelas.
window.electronAPI.updateQueue();
});
socket.on('error', (err) => {
console.error('Socket.io connection error: ', err);
}); });
console.log('Host de conexão: ', host);
} else { } else {
console.warn('User not authenticated or tenantId not available. WebSocket not connected.'); console.warn('User not authenticated or Pusher APP_KEY not available. Private channel not subscribed.');
} }
} }
initializeSocket(); initializePusher();
//chama o proximo da fila ao abrir a janela de atendimentos //chama o proximo da fila ao abrir a janela de atendimentos
window.electronAPI.selectAtendID((data) => { window.electronAPI.selectAtendID((data) => {
nextButton.disabled = true; nextButton.disabled = true;
if (!data) { if (!data) {
showNoCurrentCard('Ninguém aguardando atendimento'); queueNumber.innerHTML = 'Ninguem aguardando atendimento, fechando a janela em alguns segundos...';
return; return;
} }
// Reseta a view para a lista sempre que os dados são carregados ao clicar no botão para abrir a janela
// Trava os dados do atendimento chamado para que atualizações da fila populateList(data);
// NÃO sobrescrevam quem foi chamado.
calledAtendimentoData = data;
selectedItemId = data._id ?? null;
selectedItemName = data.clientName ?? '';
// Atualiza a tabela da fila (sem afetar o card atual)
// populateQueueTable já NÃO mexe no selectedItemId quando calledAtendimentoData está travado
showListView(); showListView();
// Mostra o card do atendimento chamado // Garante que o item selecionado (ID e Nome) seja o que veio da chamada, sobrescrevendo o da lista.
showCurrentCard(data); 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.showObservation(() => {
@ -124,115 +106,53 @@ window.electronAPI.showObservation(() => {
showObservationView(); // Muda para a tela de observação showObservationView(); // Muda para a tela de observação
}); });
// Função para popular a tabela da fila // Função para popular a lista de itens
function populateQueueTable(proximos) { function populateList(currentData) {
const atendimentoEmAndamentoId = localStorage.getItem('atendimentoAtual'); const atendimentoEmAndamentoId = localStorage.getItem('atendimentoAtual');
const atendimentoEmAndamentoNome = localStorage.getItem('atendimentoAtualNome'); const atendimentoEmAndamentoNome = localStorage.getItem('atendimentoAtualNome');
if (atendimentoEmAndamentoId) { if (atendimentoEmAndamentoId) {
// Em atendimento: mostra card de atendimento no painel direito itemList.innerHTML = `<li>Atendimento com <strong>${(atendimentoEmAndamentoNome || '').toUpperCase()}</strong> em andamento.</li>`;
queueTableBody.innerHTML = ''; queueNumber.innerHTML = `EM ATENDIMENTO: <u>${(atendimentoEmAndamentoNome || '').toUpperCase()}</u>`;
queueCount.textContent = '0';
queueEmpty.style.display = 'flex';
document.querySelector('.queue-table').style.display = 'none';
// Mostra card como "Atendendo"
currentCard.style.display = 'flex';
noCurrentCard.style.display = 'none';
cardTicket.textContent = '';
cardStatusBadge.textContent = 'Atendendo';
cardStatusBadge.className = 'card-badge badge-atendendo';
cardClientName.textContent = (atendimentoEmAndamentoNome || '').toUpperCase();
cardService.textContent = '';
cardType.textContent = '';
nextButton.disabled = true; nextButton.disabled = true;
recallButton.disabled = true;
forwardButton.disabled = true;
return; return;
} }
// Se não vier dados por parâmetro, tenta pegar do localStorage (fallback)
if (!Array.isArray(proximos)) {
let datastorage = localStorage.getItem('proximos'); let datastorage = localStorage.getItem('proximos');
proximos = JSON.parse(datastorage || '[]');
}
// Limpa e popula a tabela // Adiciona os outros itens apenas para visualização (opcional)
queueTableBody.innerHTML = ''; const proximos = JSON.parse(datastorage);
itemList.innerHTML = '';
if (!proximos || proximos.length === 0) { setTimeout(() => {
queueEmpty.style.display = 'flex'; nextButton.disabled = !currentData;
document.querySelector('.queue-table').style.display = 'none'; }, 5000);
queueCount.textContent = '0';
} else { // Seleciona o primeiro item por padrão (ou o próximo disponível)
queueEmpty.style.display = 'none'; // Aqui, vamos apenas pegar o primeiro da lista atual
document.querySelector('.queue-table').style.display = 'table'; const itemToProcess = proximos[0]; // Pega o primeiro item
queueCount.textContent = proximos.length; if (itemToProcess) {
selectedItemId = itemToProcess.id;
proximos.forEach((item, index) => { selectedItemName = itemToProcess.clientName;
const tr = document.createElement('tr'); const li = document.createElement('li');
const isPreferencial = item.ticketNumber && item.ticketNumber.startsWith('P'); li.innerHTML = /*${itemToProcess.senhaGen}: */ `<u>${itemToProcess.clientName.toUpperCase()}</u> - ${itemToProcess.attendanceType.toUpperCase()} - ${itemToProcess.descricaoServico.toUpperCase()}`;
li.dataset.id = itemToProcess.id; // Armazena o ID no elemento
tr.innerHTML = ` li.classList.add('selected'); // Marca como selecionado visualmente (precisa de CSS)
<td class="ticket-cell ${isPreferencial ? 'ticket-preferencial' : 'ticket-normal'}">${item.ticketNumber || '---'}</td> itemList.appendChild(li);
<td>${(item.clientName || '---').toUpperCase()}</td>
<td>${item.serviceName || 'Atendimento'}</td>
<td class="${isPreferencial ? 'type-preferencial' : 'type-normal'}">${isPreferencial ? 'PREFERENCIAL' : 'NORMAL'}</td>
`;
queueTableBody.appendChild(tr);
});
}
// CORREÇÃO DO BUG: Só atualiza selectedItemId se NÃO houver um atendimento já chamado (travado)
if (!calledAtendimentoData) {
// Nenhum atendimento foi chamado ainda, pode selecionar o primeiro
const firstItem = proximos && proximos[0];
if (firstItem) {
selectedItemId = firstItem._id;
selectedItemName = firstItem.clientName;
} else { } else {
itemList.innerHTML = '<li>Fila vazia!</li>';
nextButton.disabled = true;
selectedItemId = null; selectedItemId = null;
selectedItemName = ''; selectedItemName = '';
} }
// Sem atendimento chamado: não mostra card
if (!currentCard.style.display || currentCard.style.display === 'none') {
noCurrentCard.style.display = 'flex';
}
}
// Se calledAtendimentoData está definido, NÃO toca no selectedItemId — mantém o que foi chamado.
}
// Mostra o card do atendimento atual (chamado) // Adiciona os outros itens apenas para visualização (opcional)
function showCurrentCard(data) { proximos.slice(1).forEach(item => {
currentCard.style.display = 'flex'; const li = document.createElement('li');
noCurrentCard.style.display = 'none'; //${item.senhaGen}:
li.textContent = `${item.clientName.toUpperCase()} - ${item.attendanceType.toUpperCase()} - ${item.descricaoServico.toUpperCase()}`;
cardTicket.textContent = data.ticketNumber || '---'; itemList.appendChild(li);
});
const statusText = data.status === 'Atendendo' ? 'Atendendo' : 'Chamado';
cardStatusBadge.textContent = statusText;
cardStatusBadge.className = `card-badge ${data.status === 'Atendendo' ? 'badge-atendendo' : 'badge-chamado'}`;
cardClientName.textContent = (data.clientName || '---').toUpperCase();
cardService.textContent = (data.serviceName || 'ATENDIMENTO').toUpperCase();
const isPreferencial = data.ticketNumber && data.ticketNumber.startsWith('P');
cardType.textContent = isPreferencial ? 'PREFERENCIAL' : 'NORMAL';
cardType.className = `card-type ${isPreferencial ? 'type-preferencial' : ''}`;
// Habilita os botões
setTimeout(() => {
nextButton.disabled = false;
recallButton.disabled = false;
forwardButton.disabled = false;
}, 500);
}
function showNoCurrentCard(message) {
currentCard.style.display = 'none';
noCurrentCard.style.display = 'flex';
noCurrentCard.querySelector('p').textContent = message || 'Nenhum atendimento chamado';
} }
@ -252,7 +172,7 @@ document.addEventListener('DOMContentLoaded', () => {
//mostra a tela de listagem e permite iniciar o atendimento //mostra a tela de listagem e permite iniciar o atendimento
function showListView() { function showListView() {
listView.style.display = 'block'; listView.style.display = 'block';
forwardView.style.display = 'none'; encaminharView.style.display = 'none';
observationView.style.display = 'none'; observationView.style.display = 'none';
observationText.value = ''; // Limpa a textarea observationText.value = ''; // Limpa a textarea
} }
@ -267,100 +187,23 @@ function showObservationView() {
observationView.style.display = 'block'; observationView.style.display = 'block';
} }
// // Evento do botão "Iniciar atendimento" (INICIAR) // // Evento do botão "Iniciar atendimento"
nextButton.addEventListener('click', () => { nextButton.addEventListener('click', () => {
// Usa calledAtendimentoData para garantir que estamos iniciando O ATENDIMENTO CORRETO if (selectedItemId !== null) {
const idToStart = calledAtendimentoData ? calledAtendimentoData._id : selectedItemId;
const nameToStart = calledAtendimentoData ? calledAtendimentoData.clientName : selectedItemName;
if (idToStart !== null) {
// Salva o estado de atendimento no localStorage // Salva o estado de atendimento no localStorage
localStorage.setItem('atendimentoAtual', idToStart); localStorage.setItem('atendimentoAtual', selectedItemId);
localStorage.setItem('atendimentoAtualNome', nameToStart); localStorage.setItem('atendimentoAtualNome', selectedItemName);
// Atualiza o span da tela de observação
selectedItemId = idToStart;
selectedItemName = nameToStart;
selectedItemNameSpan.innerHTML = `<u> ${(nameToStart || '').toUpperCase()} </u>`;
// Notifica o main process e muda a view // Notifica o main process e muda a view
// IMPORTANTE: iniciaAtendimento deve apenas mudar o status na API, não chamar o próximo. window.electronAPI.atendimentoIniciado(selectedItemId);
window.electronAPI.atendimentoIniciado(idToStart); window.electronAPI.iniciaAtendimento(selectedItemId);
window.electronAPI.iniciaAtendimento(idToStart);
// Limpa o travamento — atendimento foi iniciado
calledAtendimentoData = null;
showObservationView(); // Muda para a tela de observação showObservationView(); // Muda para a tela de observação
} else { } else {
console.warn("Nenhum item selecionado para 'Iniciar'"); console.warn("Nenhum item selecionado para 'Próximo'");
} }
}); });
// Evento do botão RECHAMAR
recallButton.addEventListener('click', () => {
if (calledAtendimentoData) {
// Re-chama o mesmo atendimento (pode tocar som, exibir no painel, etc.)
window.electronAPI.rechamarAtendimento(calledAtendimentoData._id);
// Feedback visual
recallButton.textContent = 'RECHAMANDO...';
recallButton.disabled = true;
setTimeout(() => {
recallButton.textContent = 'RECHAMAR';
recallButton.disabled = false;
}, 2000);
}
});
// Evento do botão ENCAMINHAR
forwardButton.addEventListener('click', async () => {
if (calledAtendimentoData) {
forwardTicketSpan.textContent = calledAtendimentoData.ticketNumber;
forwardView.style.display = 'flex';
// Carrega operadores
operatorsList.innerHTML = '<p style="color: #aaa; padding: 20px; text-align: center;">Carregando atendentes...</p>';
const response = await window.electronAPI.getOperators();
if (response.success && response.operators) {
operatorsList.innerHTML = '';
response.operators.forEach(op => {
const item = document.createElement('div');
item.className = 'operator-item';
item.textContent = op.name;
item.onclick = () => {
if (confirm(`Encaminhar para ${op.name}?`)) {
window.electronAPI.encaminharAtendimento({
itemId: calledAtendimentoData._id,
collaboratorId: op._id
});
forwardView.style.display = 'none';
// Limpa o atendimento atual pois foi encaminhado
localStorage.removeItem('atendimentoAtual');
localStorage.removeItem('atendimentoAtualNome');
calledAtendimentoData = null;
selectedItemId = null;
showListView();
}
};
operatorsList.appendChild(item);
});
if (response.operators.length === 0) {
operatorsList.innerHTML = '<p style="color: #aaa; padding: 20px; text-align: center;">Nenhum outro atendente disponível.</p>';
}
} else {
operatorsList.innerHTML = `<p style="color: #e74c3c; padding: 20px; text-align: center;">${response.message || 'Erro ao carregar atendentes.'}</p>`;
}
}
});
closeForward.addEventListener('click', () => {
forwardView.style.display = 'none';
});
logoutButton.addEventListener('click', () => { logoutButton.addEventListener('click', () => {
window.electronAPI.logoutApp(); window.electronAPI.logoutApp();
@ -376,10 +219,9 @@ saveButton.addEventListener('click', () => {
localStorage.removeItem('atendimentoAtualNome'); localStorage.removeItem('atendimentoAtualNome');
window.electronAPI.atendimentoFinalizado(); window.electronAPI.atendimentoFinalizado();
// Limpa o travamento
calledAtendimentoData = null;
window.electronAPI.saveObservation({ itemId: selectedItemId, observation: observation }); window.electronAPI.saveObservation({ itemId: selectedItemId, observation: observation });
window.location.reload(); window.location.reload();
} }
}); });
// Inicialmente, mostra a view da lista (estará vazia até receber dados)

File diff suppressed because one or more lines are too long

View File

@ -1,3 +0,0 @@
{
"autostart": false
}

658
style.css
View File

@ -34,7 +34,7 @@
/* Estilos Gerais */ /* Estilos Gerais */
body { body {
font-family: 'Segoe UI', sans-serif; font-family: sans-serif;
color: #FFF; color: #FFF;
margin: 0; margin: 0;
padding: 10px; padding: 10px;
@ -125,465 +125,16 @@ body#floating{
} }
/* ================================================ /* Janela Principal */
NOVO LAYOUT: Tela de Atendimento Redesenhada #list-view, #obs-view {
================================================ */
#list-view {
padding: 16px;
background-color: var(--primary-color);
height: 96vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
#obs-view {
padding: 20px; padding: 20px;
background-color: var(--secondary-color); background-color: var(--secondary-color); /* Fundo branco para a janela principal */
border-radius: var(--border-radius); border-radius: var(--border-radius);
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
height: 96vh; height: 96vh; /* Ocupa a altura da viewport */
box-sizing: border-box; box-sizing: border-box;
} }
.attendance-layout {
display: flex;
gap: 16px;
flex: 1;
min-height: 0;
}
/* PAINEL ESQUERDO: Tabela da fila */
.queue-panel {
flex: 1.6;
background-color: rgba(11, 44, 80, 0.4);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.06);
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.panel-header h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: rgba(255, 255, 255, 0.85);
letter-spacing: 0.3px;
}
.queue-badge {
background: var(--info-color);
color: white;
padding: 2px 10px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
min-width: 20px;
text-align: center;
}
.queue-table-wrapper {
flex: 1;
overflow-y: auto;
padding: 0;
}
/* Tabela da fila */
.queue-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.queue-table thead th {
padding: 10px 20px;
text-align: left;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: rgba(255, 255, 255, 0.4);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
position: sticky;
top: 0;
background: rgba(11, 44, 80, 0.95);
z-index: 1;
}
.queue-table tbody tr {
transition: background-color 0.2s ease;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.queue-table tbody tr:hover {
background-color: rgba(255, 255, 255, 0.03);
}
.queue-table tbody td {
padding: 12px 20px;
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
}
.ticket-cell {
font-weight: 700;
font-size: 14px !important;
}
.ticket-normal {
color: var(--light-info-color) !important;
}
.ticket-preferencial {
color: var(--warning-color) !important;
}
.type-normal {
color: rgba(255, 255, 255, 0.5) !important;
font-size: 11px !important;
font-weight: 600;
letter-spacing: 0.5px;
}
.type-preferencial {
color: var(--warning-color) !important;
font-size: 11px !important;
font-weight: 600;
letter-spacing: 0.5px;
}
.queue-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
color: rgba(255, 255, 255, 0.3);
gap: 8px;
}
.queue-empty .empty-icon {
font-size: 36px;
opacity: 0.5;
}
.queue-empty p {
margin: 0;
font-size: 14px;
}
/* PAINEL DIREITO: Card do atendimento atual */
.current-card-panel {
flex: 0.8;
display: flex;
align-items: stretch;
}
.current-card {
background: linear-gradient(145deg, rgba(11, 44, 80, 0.6), rgba(8, 30, 60, 0.8));
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
flex-direction: column;
padding: 24px;
width: 100%;
box-sizing: border-box;
animation: cardFadeIn 0.3s ease-out;
}
@keyframes cardFadeIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.card-ticket {
font-size: 32px;
font-weight: 800;
color: #fff;
letter-spacing: 1px;
}
.card-badge {
padding: 4px 14px;
border-radius: 16px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-chamado {
background: var(--warning-color);
color: #fff;
}
.badge-atendendo {
background: var(--success-color);
color: #fff;
animation: pulse-badge 1.5s infinite;
}
@keyframes pulse-badge {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.card-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 20px;
}
.card-client-name {
font-size: 18px;
font-weight: 700;
color: #fff;
margin: 0;
line-height: 1.3;
}
.card-service {
font-size: 13px;
color: rgba(255, 255, 255, 0.5);
margin: 0;
font-weight: 500;
letter-spacing: 0.3px;
}
.card-type {
font-size: 12px;
color: rgba(255, 255, 255, 0.35);
margin: 4px 0 0 0;
font-weight: 600;
letter-spacing: 0.5px;
}
.card-type.type-preferencial {
color: var(--warning-color);
}
.card-actions {
display: flex;
gap: 10px;
margin-top: auto;
}
.btn-recall {
flex: 1;
padding: 12px 16px;
border: 2px solid var(--warning-color);
background: transparent;
color: var(--warning-color);
border-radius: 8px;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
letter-spacing: 0.5px;
-webkit-app-region: no-drag;
}
.btn-recall:hover:not(:disabled) {
background: var(--warning-color);
color: #fff;
}
.btn-recall:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.btn-start {
flex: 1;
padding: 12px 16px;
border: none;
background: var(--info-color);
color: #fff;
border-radius: 8px;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
letter-spacing: 0.5px;
-webkit-app-region: no-drag;
}
.btn-start:hover:not(:disabled) {
background: var(--light-info-color);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(41, 128, 185, 0.4);
}
.btn-start:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.no-current-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
background: rgba(11, 44, 80, 0.2);
border-radius: 12px;
border: 1px dashed rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.3);
gap: 8px;
padding: 20px;
box-sizing: border-box;
}
.no-current-card .empty-icon {
font-size: 36px;
opacity: 0.4;
}
.no-current-card p {
margin: 0;
font-size: 14px;
font-weight: 500;
}
.no-current-card small {
font-size: 12px;
opacity: 0.6;
}
/* Barra inferior */
.bottom-bar {
padding: 10px 0 0 0;
display: flex;
justify-content: flex-start;
gap: 10px;
}
.btn-logout {
padding: 8px 20px;
background: transparent;
border: 1px solid var(--danger-color);
color: var(--danger-color);
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
-webkit-app-region: no-drag;
}
.btn-logout:hover {
color: var(--white);
background: var(--danger-color);
}
/* ================================================
TELA DE OBSERVAÇÕES (Atendimento em andamento)
================================================ */
.obs-layout {
display: flex;
flex-direction: column;
height: 100%;
}
.obs-header {
margin-bottom: 20px;
}
.obs-header h2 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
.obs-client-info {
display: flex;
align-items: center;
gap: 12px;
}
.obs-client-name {
font-size: 16px;
color: var(--light-info-color);
font-weight: 600;
}
#observation-text {
width: calc(100% - 22px) !important;
padding: 10px;
background-color: var(--secondary-color);
border: 1px solid var(--tertiary-color);
border-radius: 6px;
color: var(--medium-gray);
font-size: 14px;
flex: 1;
resize: vertical;
min-height: 120px;
}
#observation-text:focus-visible {
box-shadow: var(--box-shadow-inputs);
outline: none;
}
.obs-actions {
display: flex;
justify-content: flex-end;
padding-top: 12px;
}
.btn-finish {
padding: 12px 28px;
background: var(--warning-color);
border: none;
color: #fff;
border-radius: 8px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
-webkit-app-region: no-drag;
}
.btn-finish:hover {
background: var(--light-warning-color);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(243, 156, 18, 0.3);
}
/* ================================================
ESTILOS LEGADOS (mantidos para outras telas)
================================================ */
#item-list { #item-list {
list-style: none; list-style: none;
padding: 0; padding: 0;
@ -676,6 +227,15 @@ textarea {
font-size: 16px; 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 { .error-message {
background-color: #ffebee; background-color: #ffebee;
color: #c62828; color: #c62828;
@ -703,12 +263,10 @@ button{
} }
#logout-button{ #logout-button{
color: var(--white);
background-color: var(--danger-color); background-color: var(--danger-color);
} }
#logout-button:hover{ #logout-button:hover{
color: var(--white);
background-color: var(--dark-danger-color); background-color: var(--dark-danger-color);
} }
@ -728,12 +286,10 @@ button{
#sair-button{ #sair-button{
color: var(--white);
background-color: #eb574d; background-color: #eb574d;
} }
#sair-button:hover{ #sair-button:hover{
color: var(--white);
background-color: #e93f2c; background-color: #e93f2c;
} }
@ -745,189 +301,3 @@ button{
#save-button:hover{ #save-button:hover{
background-color: var(--warning-color); background-color: var(--warning-color);
} }
/* Estilo do Switch */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
z-index: 1; /* Garante que o switch esteja acima de outros elementos */
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: var(--success-color);
}
input:focus + .slider {
box-shadow: 0 0 1px var(--success-color);
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
/* width: 60px; */
}
.slider.round:before {
border-radius: 50%;
}
/* Scrollbar personalizada */
.queue-table-wrapper::-webkit-scrollbar {
width: 6px;
}
.queue-table-wrapper::-webkit-scrollbar-track {
background: transparent;
}
.queue-table-wrapper::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 3px;
}
.queue-table-wrapper::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
}
.btn-forward {
flex: 1;
padding: 12px 16px;
border: 2px solid #8e44ad;
background: transparent;
color: #8e44ad;
border-radius: 8px;
font-weight: 700;
font-size: 13px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-forward:hover:not(:disabled) {
background: #8e44ad;
color: #fff;
}
.btn-forward:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Modal Forward styles */
.modal-view {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(5px);
}
.modal-content {
background: var(--primary-color);
width: 90%;
max-width: 500px;
border-radius: 16px;
border: 1px solid var(--secondary-color);
padding: 24px;
box-shadow: 0 20px 40px rgba(0,0,0,0.5);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-header h2 {
color: var(--white);
margin: 0;
font-size: 1.5rem;
}
.btn-close {
background: transparent;
border: none;
color: var(--dark-gray);
font-size: 24px;
cursor: pointer;
}
.operators-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
max-height: 300px;
overflow-y: auto;
margin-top: 15px;
padding-right: 5px;
}
.operator-item {
background: var(--secondary-color);
border: 1px solid var(--tertiary-color);
padding: 15px;
border-radius: 10px;
color: var(--white);
cursor: pointer;
transition: all 0.2s;
text-align: left;
font-weight: 500;
}
.operator-item:hover {
background: var(--medium-gold);
border-color: var(--accent-gold);
}
.highlight {
color: var(--accent-gold);
font-weight: bold;
}