Compare commits
No commits in common. "c9c28e68c758dd27c600df6995d2d96b2ec8bdd9" and "624b6a72685ce82caa4b3b42b2a26131df174283" have entirely different histories.
c9c28e68c7
...
624b6a7268
95
index.html
95
index.html
|
|
@ -7,7 +7,7 @@
|
||||||
default-src 'self';
|
default-src 'self';
|
||||||
script-src 'self' 'unsafe-inline';
|
script-src 'self' 'unsafe-inline';
|
||||||
style-src 'self' 'unsafe-inline';
|
style-src 'self' 'unsafe-inline';
|
||||||
connect-src 'self' ws://localhost:3000 http://localhost:3000 ws://autoatends.linco.work wss://autoatends.linco.work;
|
connect-src 'self' ws://localhost:3000 http://localhost:3000;
|
||||||
">
|
">
|
||||||
<title>Tela de Atendimento</title>
|
<title>Tela de Atendimento</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
|
|
@ -15,91 +15,34 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- TELA PRINCIPAL: Lista de fila + Card do chamado atual -->
|
|
||||||
<div id="list-view">
|
<div id="list-view">
|
||||||
<div class="attendance-layout">
|
<h1>Fila</h1>
|
||||||
<!-- Lado esquerdo: tabela da fila -->
|
<h3><span id="queue-number"></span></h3>
|
||||||
<div class="queue-panel">
|
<ul id="item-list">
|
||||||
<div class="panel-header">
|
<!-- Itens serão carregados aqui -->
|
||||||
<h2>Fila de Espera</h2>
|
</ul>
|
||||||
<span id="queue-count" class="queue-badge">0</span>
|
<button id="next-button" disabled>
|
||||||
</div>
|
<span id="counter-start"></span>
|
||||||
<div class="queue-table-wrapper">
|
Iniciar atendimento
|
||||||
<table class="queue-table">
|
</button>
|
||||||
<thead>
|
<!-- <button id="sendto-button" disabled>Encaminhar</button> -->
|
||||||
<tr>
|
<button id="logout-button">Trocar Colaborador</button>
|
||||||
<th>SENHA</th>
|
|
||||||
<th>CLIENTE</th>
|
|
||||||
<th>SERVIÇO</th>
|
|
||||||
<th>TIPO</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="queue-table-body">
|
|
||||||
<!-- Linhas da fila -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div id="queue-empty" class="queue-empty" style="display:none;">
|
|
||||||
<span class="empty-icon">📋</span>
|
|
||||||
<p>Nenhum cliente aguardando</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Lado direito: card do atendimento atual -->
|
|
||||||
<div class="current-card-panel">
|
|
||||||
<div id="current-card" class="current-card" style="display:none;">
|
|
||||||
<div class="card-header">
|
|
||||||
<span id="card-ticket" class="card-ticket">---</span>
|
|
||||||
<span id="card-status-badge" class="card-badge badge-chamado">Chamado</span>
|
|
||||||
</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="next-button" class="btn-start" disabled>
|
|
||||||
<span id="counter-start"></span>
|
|
||||||
INICIAR
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="no-current" class="no-current-card">
|
|
||||||
<span class="empty-icon">✅</span>
|
|
||||||
<p>Nenhum atendimento chamado</p>
|
|
||||||
<small>Clique no botão flutuante para chamar o próximo</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bottom-bar">
|
|
||||||
<button id="logout-button" class="btn-logout">Trocar Colaborador</button>
|
|
||||||
</div>
|
|
||||||
</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>
|
<input type="hidden" name="idAtend" id="idAtend">
|
||||||
<div class="obs-client-info">
|
<textarea id="observation-text" rows="10" cols="50" placeholder="Digite suas observações..."></textarea>
|
||||||
<span id="selected-item-name" class="obs-client-name"></span>
|
<button id="save-button">Finalizar atendimento</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="idAtend" id="idAtend">
|
|
||||||
<textarea id="observation-text" rows="10" placeholder="Digite suas observações sobre o atendimento..."></textarea>
|
|
||||||
<div class="obs-actions">
|
|
||||||
<button id="save-button" class="btn-finish">Finalizar atendimento</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="encaminhar-view" style="display: none;">
|
<div id="encaminhar-view" style="display: none;">
|
||||||
<h1>Observações</h1>
|
<h1>Observações</h1>
|
||||||
<p>Atendendo: <span id="enc-selected-item-name"></span></p>
|
<p>Atendendo: <span id="selected-item-name"></span></p>
|
||||||
<textarea id="enc-observation-text" rows="10" cols="50" placeholder="Digite suas observações..."></textarea>
|
<textarea id="enc-observation-text" rows="10" cols="50" placeholder="Digite suas observações..."></textarea>
|
||||||
<button id="enc-save-button">Salvar</button>
|
<button id="save-button">Salvar</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="res/js/socket.io.min.js"></script>
|
<script src="res/js/socket.io.min.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
340
main.js
340
main.js
|
|
@ -15,10 +15,10 @@ 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 settingsPath = path.join(app.getPath('userData'), 'settings.json'); // Caminho para as configurações
|
||||||
|
|
||||||
//! api
|
//! API
|
||||||
const apiUrl = 'https://aapi.linco.work/';
|
const apiUrl = 'http://localhost:3000/api/'; // Adaptado para NestJS
|
||||||
//! pusher
|
//! pusher
|
||||||
const pusherUrl = apiUrl;
|
const pusherUrl = 'http://localhost:3000'; // Agora aponta para o Socket.io (NestJS)
|
||||||
|
|
||||||
autoUpdater.autoDownload = false;
|
autoUpdater.autoDownload = false;
|
||||||
autoUpdater.autoInstallOnAppQuit = true;
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
|
@ -100,7 +100,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,80 +110,68 @@ async function fetchDataFromAPI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFirstData() {
|
async function getFirstData() {
|
||||||
|
|
||||||
const token = await getAuthToken();
|
const token = await getAuthToken();
|
||||||
const colabId = await getSelectedOperatorId();
|
const colabId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('idOperator')")
|
||||||
const tenantId = await getTenantId();
|
const tenantId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('tenantId')")
|
||||||
const url = apiUrl + 'attendance/next-in-line/' + colabId;
|
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-tenant-id': tenantId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//! busca pela resposta
|
||||||
|
request.on('response', (response) => {
|
||||||
|
let rawData = '';
|
||||||
|
response.on('data', (chunk) => { rawData += chunk; });
|
||||||
|
response.on('end', () => {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(rawData);
|
||||||
|
let proximos = Array.isArray(parsedData) ? parsedData : [];
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
floatingWin.webContents.executeJavaScript("localStorage.setItem('proximos','" + JSON.stringify(proximos) + "')");
|
||||||
|
let count = proximos.length;
|
||||||
|
floatingWin.webContents.send('update-count', count);
|
||||||
|
} else {
|
||||||
|
console.error(`Erro na requisição: Status code ${response.statusCode}`, parsedData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao analisar a resposta JSON:", error);
|
||||||
|
mainWin.webContents.send('api-error', {
|
||||||
|
message: `Erro ao processar resposta do servidor.`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
request.on('response', (response) => {
|
|
||||||
let rawData = '';
|
|
||||||
response.on('data', (chunk) => { rawData += chunk; });
|
|
||||||
response.on('end', async () => {
|
|
||||||
try {
|
|
||||||
const parsedData = JSON.parse(rawData);
|
|
||||||
let proximos = Array.isArray(parsedData) ? parsedData : [];
|
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
|
||||||
const proximosStr = JSON.stringify(proximos);
|
|
||||||
|
|
||||||
// Sincroniza o localStorage de todas as janelas importantes
|
|
||||||
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 {
|
|
||||||
console.error(`Erro API: ${response.statusCode}`);
|
|
||||||
resolve([]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro JSON:", error);
|
|
||||||
resolve([]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
request.on('error', (error) => {
|
|
||||||
console.error("Erro rede:", error);
|
|
||||||
resolve([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
request.end();
|
|
||||||
});
|
});
|
||||||
|
request.on('error', (error) => {
|
||||||
|
console.error("Erro na requisição:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 stored = await floatingWin.webContents.executeJavaScript("localStorage.getItem('proximos')");
|
||||||
|
const proximos = JSON.parse(stored || '[]');
|
||||||
|
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
|
||||||
|
|
@ -211,29 +199,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({
|
||||||
|
|
@ -515,12 +480,9 @@ 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_HOST = process.env.PUSHER_HOST || pusherUrl;
|
||||||
if (host && host.endsWith('/')) {
|
return { host: PUSHER_HOST };
|
||||||
host = host.slice(0, -1);
|
|
||||||
}
|
|
||||||
return { host: host };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('update_version', async (event, arg) => {
|
ipcMain.on('update_version', async (event, arg) => {
|
||||||
|
|
@ -547,13 +509,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')");
|
||||||
|
|
@ -576,16 +533,24 @@ 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 stored = await floatingWin.webContents.executeJavaScript("localStorage.getItem('proximos')");
|
||||||
|
const proximos = JSON.parse(stored || '[]');
|
||||||
|
return proximos.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestData = async () => {
|
const requestData = async () => {
|
||||||
|
|
||||||
const colabId = await getSelectedOperatorId();
|
const colabId = await getSelectedOperatorId();
|
||||||
const token = await getAuthToken();
|
const token = await getAuthToken();
|
||||||
const tenantId = await getTenantId();
|
const tenantId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('tenantId')")
|
||||||
const url = apiUrl + 'attendance/call-next/' + colabId;
|
const url = apiUrl + 'attendance/call-next/' + colabId;
|
||||||
|
|
||||||
const request = net.request({
|
const request = net.request({
|
||||||
|
|
@ -600,33 +565,36 @@ ipcMain.on('chamar-fila', async () => {
|
||||||
|
|
||||||
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 === 201 || response.statusCode === 200) {
|
||||||
if (parsedData) {
|
if (parsedData) {
|
||||||
mainWin.webContents.send('select-atend-id', parsedData);
|
mainWin.webContents.send('select-atend-id', parsedData);
|
||||||
if (parsedData.status === 'Fila' || parsedData.status === 'Chamado') {
|
if (parsedData.status === 'Fila' || parsedData.status === 'Chamado') { showMainWindow(); } else
|
||||||
showMainWindow();
|
if (parsedData.status === 'Atendendo') {
|
||||||
} 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.clientName + '), continue e finalize por favor!',
|
'type': 'error',
|
||||||
'type': 'error',
|
'noLink': true,
|
||||||
'buttons': ['Depois', 'Continuar'],
|
'buttons': ['Depois', 'Continuar'],
|
||||||
};
|
|
||||||
dialog.showMessageBox(floatingWin, options2).then(result => {
|
|
||||||
if (result.response) {
|
|
||||||
mainWin.webContents.send('show-observation');
|
|
||||||
showMainWindow();
|
|
||||||
} else {
|
|
||||||
mainWin.hide();
|
|
||||||
};
|
};
|
||||||
});
|
dialog.showMessageBox(floatingWin, options2).then(result => {
|
||||||
}
|
if (result.response) {
|
||||||
|
mainWin.webContents.send('show-observation');
|
||||||
|
showMainWindow();
|
||||||
|
} else {
|
||||||
|
mainWin.hide();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
mainWin.webContents.send('select-atend-id', null);
|
mainWin.webContents.send('select-atend-id', null);
|
||||||
}
|
}
|
||||||
|
|
@ -646,7 +614,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}`
|
||||||
|
|
@ -656,11 +623,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.',
|
||||||
|
|
@ -670,20 +633,17 @@ 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
|
||||||
|
|
@ -704,7 +664,7 @@ 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 tenantId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('tenantId')")
|
||||||
const url = apiUrl + 'attendance/' + itemId + '/start';
|
const url = apiUrl + 'attendance/' + itemId + '/start';
|
||||||
|
|
||||||
// envio de uma solicitação POST com o ID do item
|
// envio de uma solicitação POST com o ID do item
|
||||||
|
|
@ -733,39 +693,6 @@ ipcMain.on('iniciar-atendimento', async (event, itemId) => {
|
||||||
request.end();
|
request.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ouvir clique no botão "Rechamar" - chama novamente o mesmo atendimento
|
|
||||||
ipcMain.on('rechamar-atendimento', async (event, itemId) => {
|
|
||||||
|
|
||||||
const token = await getAuthToken();
|
|
||||||
const tenantId = await getTenantId();
|
|
||||||
const colabId = await getSelectedOperatorId();
|
|
||||||
const url = apiUrl + 'attendance/call-next/' + colabId;
|
|
||||||
|
|
||||||
const request = net.request({
|
|
||||||
method: 'POST',
|
|
||||||
url: url,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer ' + token,
|
|
||||||
'x-tenant-id': tenantId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ouve quando um atendimento é iniciado e notifica a janela flutuante
|
// Ouve quando um atendimento é iniciado e notifica a janela flutuante
|
||||||
ipcMain.on('atendimento-iniciado', (event, itemId) => {
|
ipcMain.on('atendimento-iniciado', (event, itemId) => {
|
||||||
if (floatingWin) {
|
if (floatingWin) {
|
||||||
|
|
@ -780,20 +707,13 @@ 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 }) => {
|
||||||
|
|
||||||
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 tenantId = await floatingWin.webContents.executeJavaScript("localStorage.getItem('tenantId')")
|
||||||
const url = apiUrl + 'attendance/' + itemId + '/finish';
|
const url = apiUrl + 'attendance/' + itemId + '/finish';
|
||||||
|
|
||||||
const fmData = JSON.stringify({
|
const fmData = JSON.stringify({
|
||||||
|
|
@ -841,6 +761,7 @@ ipcMain.on('logout', () => {
|
||||||
localStorage.removeItem("selectedOperator");
|
localStorage.removeItem("selectedOperator");
|
||||||
localStorage.removeItem("salaOperator");
|
localStorage.removeItem("salaOperator");
|
||||||
localStorage.removeItem("servicosOperator");
|
localStorage.removeItem("servicosOperator");
|
||||||
|
localStorage.removeItem("tenantId");
|
||||||
`).then(() => {
|
`).then(() => {
|
||||||
// Fecha a janela main e abre a janela de operador e inicia o aplicativo
|
// Fecha a janela main e abre a janela de operador e inicia o aplicativo
|
||||||
mainWin.hide();
|
mainWin.hide();
|
||||||
|
|
@ -875,15 +796,10 @@ ipcMain.on('login-attempt', async (event, credentials) => {
|
||||||
const data = JSON.parse(responseData);
|
const data = JSON.parse(responseData);
|
||||||
|
|
||||||
if (data.access_token) {
|
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}`);
|
|
||||||
|
|
||||||
// Login bem-sucedido
|
// Login bem-sucedido
|
||||||
loginWin.webContents.executeJavaScript(`
|
loginWin.webContents.executeJavaScript(`
|
||||||
localStorage.setItem("authToken", "${data.access_token}");
|
localStorage.setItem("authToken", "${data.access_token}");
|
||||||
localStorage.setItem("tenantId", "${tenantId || ''}");
|
localStorage.setItem("tenantId", "${data.user.tenantId}");
|
||||||
`).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 });
|
||||||
|
|
@ -914,11 +830,8 @@ ipcMain.on('login-attempt', async (event, credentials) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove máscara do login (CPF/CNPJ) antes de enviar para a API
|
|
||||||
const cleanLogin = credentials.login.replace(/[.\-\/]/g, '');
|
|
||||||
|
|
||||||
// Envia as credenciais
|
// Envia as credenciais
|
||||||
request.write(JSON.stringify({ login: cleanLogin, password: credentials.password }));
|
request.write(JSON.stringify({ login: credentials.login, password: credentials.password }));
|
||||||
request.end();
|
request.end();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
event.reply('login-response', {
|
event.reply('login-response', {
|
||||||
|
|
@ -957,9 +870,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 {
|
||||||
|
|
@ -971,44 +881,26 @@ ipcMain.handle('get-operators', async () => {
|
||||||
|
|
||||||
const route = apiUrl + 'collaborators';
|
const route = apiUrl + 'collaborators';
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
const headers = {
|
|
||||||
'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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
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 (Array.isArray(data)) {
|
||||||
const operators = data.map(colab => ({
|
const operators = data.map(colab => ({
|
||||||
|
|
@ -1023,14 +915,14 @@ 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: 'Erro ao obter a lista de colaboradores',
|
||||||
operators: []
|
operators: []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
resolve({
|
reject({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Erro ao processar resposta do servidor',
|
message: 'Erro ao processar resposta do servidor',
|
||||||
operators: []
|
operators: []
|
||||||
|
|
@ -1040,7 +932,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: []
|
||||||
|
|
|
||||||
|
|
@ -14,16 +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),
|
|
||||||
|
|
||||||
//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),
|
||||||
|
|
|
||||||
223
renderer.js
223
renderer.js
|
|
@ -1,24 +1,14 @@
|
||||||
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');
|
const encaminharView = document.getElementById('encaminhar-view');
|
||||||
|
const itemList = document.getElementById('item-list');
|
||||||
// Novos elementos do layout redesenhado
|
|
||||||
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 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');
|
||||||
|
|
||||||
|
|
@ -26,22 +16,13 @@ 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 initializeSocket() {
|
||||||
|
|
@ -70,10 +51,9 @@ async function initializeSocket() {
|
||||||
|
|
||||||
// Evento que substitui o bind do Pusher
|
// Evento que substitui o bind do Pusher
|
||||||
socket.on('queueUpdate', (data) => {
|
socket.on('queueUpdate', (data) => {
|
||||||
console.log('Notificação de fila recebida:', data);
|
console.log('Atualização de fila recebida:', data);
|
||||||
// Avisa o processo principal que houve mudança.
|
localStorage.setItem('proximos', JSON.stringify(data));
|
||||||
// O main vai buscar a lista completa e atualizar todas as janelas.
|
populateList(data);
|
||||||
window.electronAPI.updateQueue();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('error', (err) => {
|
socket.on('error', (err) => {
|
||||||
|
|
@ -91,22 +71,20 @@ initializeSocket();
|
||||||
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
|
||||||
|
populateList(data);
|
||||||
|
showListView();
|
||||||
|
|
||||||
// Trava os dados do atendimento chamado para que atualizações da fila
|
// Garante que o item selecionado (ID e Nome) seja o que veio da chamada, sobrescrevendo o da lista.
|
||||||
// NÃO sobrescrevam quem foi chamado.
|
|
||||||
calledAtendimentoData = data;
|
|
||||||
selectedItemId = data._id ?? null;
|
selectedItemId = data._id ?? null;
|
||||||
selectedItemName = data.clientName ?? '';
|
selectedItemName = data.clientName ?? '';
|
||||||
|
|
||||||
// Atualiza a tabela da fila (sem afetar o card atual)
|
//data.senhaGen
|
||||||
// populateQueueTable já NÃO mexe no selectedItemId quando calledAtendimentoData está travado
|
queueNumber.innerHTML = data ? `NA VEZ: <u>${data.clientName.toUpperCase()}</u>` : 'Ninguem aguardando atendimento';
|
||||||
showListView();
|
selectedItemNameSpan.innerHTML = data ? `<u> ${data.clientName.toUpperCase()} </u> <i style="float:right;">[ ${data.ticketNumber} ]</i>` : 'Ninguem aguardando atendimento';
|
||||||
|
|
||||||
// Mostra o card do atendimento chamado
|
|
||||||
showCurrentCard(data);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electronAPI.showObservation(() => {
|
window.electronAPI.showObservation(() => {
|
||||||
|
|
@ -114,29 +92,15 @@ 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(proximos) {
|
||||||
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;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,81 +110,36 @@ function populateQueueTable(proximos) {
|
||||||
proximos = JSON.parse(datastorage || '[]');
|
proximos = JSON.parse(datastorage || '[]');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limpa e popula a tabela
|
itemList.innerHTML = '';
|
||||||
queueTableBody.innerHTML = '';
|
|
||||||
|
|
||||||
if (!proximos || proximos.length === 0) {
|
|
||||||
queueEmpty.style.display = 'flex';
|
|
||||||
document.querySelector('.queue-table').style.display = 'none';
|
|
||||||
queueCount.textContent = '0';
|
|
||||||
} else {
|
|
||||||
queueEmpty.style.display = 'none';
|
|
||||||
document.querySelector('.queue-table').style.display = 'table';
|
|
||||||
queueCount.textContent = proximos.length;
|
|
||||||
|
|
||||||
proximos.forEach((item, index) => {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
const isPreferencial = item.ticketNumber && item.ticketNumber.startsWith('P');
|
|
||||||
|
|
||||||
tr.innerHTML = `
|
|
||||||
<td class="ticket-cell ${isPreferencial ? 'ticket-preferencial' : 'ticket-normal'}">${item.ticketNumber || '---'}</td>
|
|
||||||
<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 {
|
|
||||||
selectedItemId = null;
|
|
||||||
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)
|
|
||||||
function showCurrentCard(data) {
|
|
||||||
currentCard.style.display = 'flex';
|
|
||||||
noCurrentCard.style.display = 'none';
|
|
||||||
|
|
||||||
cardTicket.textContent = data.ticketNumber || '---';
|
|
||||||
|
|
||||||
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(() => {
|
setTimeout(() => {
|
||||||
nextButton.disabled = false;
|
nextButton.disabled = !proximos || proximos.length === 0;
|
||||||
recallButton.disabled = false;
|
}, 1000);
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showNoCurrentCard(message) {
|
// Seleciona o primeiro item por padrão (ou o próximo disponível)
|
||||||
currentCard.style.display = 'none';
|
// Aqui, vamos apenas pegar o primeiro da lista atual
|
||||||
noCurrentCard.style.display = 'flex';
|
const itemToProcess = proximos[0]; // Pega o primeiro item
|
||||||
noCurrentCard.querySelector('p').textContent = message || 'Nenhum atendimento chamado';
|
if (itemToProcess) {
|
||||||
|
selectedItemId = itemToProcess._id;
|
||||||
|
selectedItemName = itemToProcess.clientName;
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.innerHTML = /*${itemToProcess.ticketNumber}: */ `<u>${itemToProcess.clientName.toUpperCase()}</u> - ${itemToProcess.ticketNumber}`;
|
||||||
|
li.dataset.id = itemToProcess._id; // Armazena o ID no elemento
|
||||||
|
li.classList.add('selected'); // Marca como selecionado visualmente (precisa de CSS)
|
||||||
|
itemList.appendChild(li);
|
||||||
|
} else {
|
||||||
|
itemList.innerHTML = '<li>Fila vazia!</li>';
|
||||||
|
nextButton.disabled = true;
|
||||||
|
selectedItemId = null;
|
||||||
|
selectedItemName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adiciona os outros itens apenas para visualização (opcional)
|
||||||
|
proximos.slice(1).forEach(item => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = `${item.clientName.toUpperCase()} - ${item.ticketNumber}`;
|
||||||
|
itemList.appendChild(li);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -255,51 +174,22 @@ 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
logoutButton.addEventListener('click', () => {
|
logoutButton.addEventListener('click', () => {
|
||||||
|
|
@ -316,9 +206,6 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
487
style.css
487
style.css
|
|
@ -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 {
|
|
||||||
background: var(--danger-color);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ================================================
|
|
||||||
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;
|
||||||
|
|
@ -803,23 +363,4 @@ input:checked + .slider:before {
|
||||||
|
|
||||||
.slider.round:before {
|
.slider.round:before {
|
||||||
border-radius: 50%;
|
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);
|
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue