nova interface da tela de atendimento

This commit is contained in:
Eder Moraes 2026-04-27 02:31:05 -03:00
parent d12f38fbaf
commit c9c28e68c7
5 changed files with 742 additions and 82 deletions

View File

@ -15,34 +15,91 @@
</head> </head>
<body> <body>
<!-- TELA PRINCIPAL: Lista de fila + Card do chamado atual -->
<div id="list-view"> <div id="list-view">
<h1>Fila</h1> <div class="attendance-layout">
<h3><span id="queue-number"></span></h3> <!-- Lado esquerdo: tabela da fila -->
<ul id="item-list"> <div class="queue-panel">
<!-- Itens serão carregados aqui --> <div class="panel-header">
</ul> <h2>Fila de Espera</h2>
<button id="next-button" disabled> <span id="queue-count" class="queue-badge">0</span>
<span id="counter-start"></span> </div>
Iniciar atendimento <div class="queue-table-wrapper">
</button> <table class="queue-table">
<!-- <button id="sendto-button" disabled>Encaminhar</button> --> <thead>
<button id="logout-button">Trocar Colaborador</button> <tr>
<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> </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>
<!-- TELA DE OBSERVAÇÕES (atendimento em andamento) -->
<div id="obs-view" style="display: none;"> <div id="obs-view" style="display: none;">
<h1>Observações</h1> <div class="obs-layout">
<p>Atendendo: <span id="selected-item-name"></span></p> <div class="obs-header">
<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" cols="50" placeholder="Digite suas observações..."></textarea> <textarea id="observation-text" rows="10" placeholder="Digite suas observações sobre o atendimento..."></textarea>
<button id="save-button">Finalizar atendimento</button> <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="selected-item-name"></span></p> <p>Atendendo: <span id="enc-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="save-button">Salvar</button> <button id="enc-save-button">Salvar</button>
</div> </div>
<script src="res/js/socket.io.min.js"></script> <script src="res/js/socket.io.min.js"></script>

33
main.js
View File

@ -733,6 +733,39 @@ 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) {

View File

@ -14,6 +14,9 @@ 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'),

View File

@ -1,14 +1,24 @@
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');
@ -16,16 +26,22 @@ 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 // Se já estiver em atendimento, ignora atualizações da lista para evitar flickering no botão
if (localStorage.getItem('atendimentoAtual')) { if (localStorage.getItem('atendimentoAtual')) {
return; return;
} }
nextButton.disabled = true;
if (!data) { if (!data) {
return; return;
} }
populateList(data); populateQueueTable(data);
}); });
async function initializeSocket() { async function initializeSocket() {
@ -75,20 +91,22 @@ initializeSocket();
window.electronAPI.selectAtendID((data) => { window.electronAPI.selectAtendID((data) => {
nextButton.disabled = true; nextButton.disabled = true;
if (!data) { if (!data) {
queueNumber.innerHTML = 'Ninguem aguardando atendimento, fechando a janela em alguns segundos...'; showNoCurrentCard('Ninguém aguardando atendimento');
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();
// Garante que o item selecionado (ID e Nome) seja o que veio da chamada, sobrescrevendo o da lista. // Trava os dados do atendimento chamado para que atualizações da fila
// NÃO sobrescrevam quem foi chamado.
calledAtendimentoData = data;
selectedItemId = data._id ?? null; selectedItemId = data._id ?? null;
selectedItemName = data.clientName ?? ''; selectedItemName = data.clientName ?? '';
//data.senhaGen // Atualiza a tabela da fila (sem afetar o card atual)
queueNumber.innerHTML = data ? `NA VEZ: <u>${data.clientName.toUpperCase()}</u>` : 'Ninguem aguardando atendimento'; // populateQueueTable já NÃO mexe no selectedItemId quando calledAtendimentoData está travado
selectedItemNameSpan.innerHTML = data ? `<u> ${data.clientName.toUpperCase()} </u> <i style="float:right;">[ ${data.ticketNumber} ]</i>` : 'Ninguem aguardando atendimento'; showListView();
// Mostra o card do atendimento chamado
showCurrentCard(data);
}); });
window.electronAPI.showObservation(() => { window.electronAPI.showObservation(() => {
@ -96,15 +114,29 @@ window.electronAPI.showObservation(() => {
showObservationView(); // Muda para a tela de observação showObservationView(); // Muda para a tela de observação
}); });
// Função para popular a lista de itens // Função para popular a tabela da fila
function populateList(proximos) { function populateQueueTable(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) {
itemList.innerHTML = `<li>Atendimento com <strong>${(atendimentoEmAndamentoNome || '').toUpperCase()}</strong> em andamento.</li>`; // Em atendimento: mostra card de atendimento no painel direito
queueNumber.innerHTML = `EM ATENDIMENTO: <u>${(atendimentoEmAndamentoNome || '').toUpperCase()}</u>`; queueTableBody.innerHTML = '';
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;
} }
@ -114,36 +146,81 @@ function populateList(proximos) {
proximos = JSON.parse(datastorage || '[]'); proximos = JSON.parse(datastorage || '[]');
} }
itemList.innerHTML = ''; // Limpa e popula a tabela
queueTableBody.innerHTML = '';
setTimeout(() => { if (!proximos || proximos.length === 0) {
nextButton.disabled = !proximos || proximos.length === 0; queueEmpty.style.display = 'flex';
}, 1000); document.querySelector('.queue-table').style.display = 'none';
queueCount.textContent = '0';
// Seleciona o primeiro item por padrão (ou o próximo disponível) } else {
// Aqui, vamos apenas pegar o primeiro da lista atual queueEmpty.style.display = 'none';
const itemToProcess = proximos[0]; // Pega o primeiro item document.querySelector('.queue-table').style.display = 'table';
if (itemToProcess) { queueCount.textContent = proximos.length;
selectedItemId = itemToProcess._id;
selectedItemName = itemToProcess.clientName; proximos.forEach((item, index) => {
const li = document.createElement('li'); const tr = document.createElement('tr');
li.innerHTML = /*${itemToProcess.ticketNumber}: */ `<u>${itemToProcess.clientName.toUpperCase()}</u> - ${itemToProcess.ticketNumber}`; const isPreferencial = item.ticketNumber && item.ticketNumber.startsWith('P');
li.dataset.id = itemToProcess._id; // Armazena o ID no elemento
li.classList.add('selected'); // Marca como selecionado visualmente (precisa de CSS) tr.innerHTML = `
itemList.appendChild(li); <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 { } 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.
}
// Adiciona os outros itens apenas para visualização (opcional) // Mostra o card do atendimento atual (chamado)
proximos.slice(1).forEach(item => { function showCurrentCard(data) {
const li = document.createElement('li'); currentCard.style.display = 'flex';
li.textContent = `${item.clientName.toUpperCase()} - ${item.ticketNumber}`; noCurrentCard.style.display = 'none';
itemList.appendChild(li);
}); 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(() => {
nextButton.disabled = false;
recallButton.disabled = false;
}, 500);
}
function showNoCurrentCard(message) {
currentCard.style.display = 'none';
noCurrentCard.style.display = 'flex';
noCurrentCard.querySelector('p').textContent = message || 'Nenhum atendimento chamado';
} }
@ -178,23 +255,51 @@ function showObservationView() {
observationView.style.display = 'block'; observationView.style.display = 'block';
} }
// // Evento do botão "Iniciar atendimento" // // Evento do botão "Iniciar atendimento" (INICIAR)
nextButton.addEventListener('click', () => { nextButton.addEventListener('click', () => {
if (selectedItemId !== null) { // Usa calledAtendimentoData para garantir que estamos iniciando O ATENDIMENTO CORRETO
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', selectedItemId); localStorage.setItem('atendimentoAtual', idToStart);
localStorage.setItem('atendimentoAtualNome', selectedItemName); localStorage.setItem('atendimentoAtualNome', nameToStart);
// 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. // 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 'Próximo'"); console.warn("Nenhum item selecionado para 'Iniciar'");
} }
}); });
// 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', () => {
@ -211,6 +316,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();
} }

487
style.css
View File

@ -34,7 +34,7 @@
/* Estilos Gerais */ /* Estilos Gerais */
body { body {
font-family: sans-serif; font-family: 'Segoe UI', sans-serif;
color: #FFF; color: #FFF;
margin: 0; margin: 0;
padding: 10px; padding: 10px;
@ -125,16 +125,465 @@ body#floating{
} }
/* Janela Principal */ /* ================================================
#list-view, #obs-view { NOVO LAYOUT: Tela de Atendimento Redesenhada
================================================ */
#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); /* Fundo branco para a janela principal */ background-color: var(--secondary-color);
border-radius: var(--border-radius); border-radius: var(--border-radius);
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
height: 96vh; /* Ocupa a altura da viewport */ height: 96vh;
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;
@ -227,15 +676,6 @@ 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;
@ -364,3 +804,22 @@ 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);
}