mirror of
https://github.com/marcogll/ap_pos.git
synced 2026-01-13 13:15:16 +00:00
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,3 +16,4 @@ dev-tasks.md
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
devtastks
|
||||||
|
|||||||
85
app.js
85
app.js
@@ -14,7 +14,18 @@ function escapeHTML(str) {
|
|||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
const APP_VERSION = '1.0.0';
|
function formatDate(dateString) {
|
||||||
|
if (!dateString) return '';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const userTimezoneOffset = date.getTimezoneOffset() * 60000;
|
||||||
|
const adjustedDate = new Date(date.getTime() + userTimezoneOffset);
|
||||||
|
const day = String(adjustedDate.getDate()).padStart(2, '0');
|
||||||
|
const month = String(adjustedDate.getMonth() + 1).padStart(2, '0');
|
||||||
|
const year = adjustedDate.getFullYear();
|
||||||
|
return `${day}/${month}/${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP_VERSION = '1.3.0';
|
||||||
|
|
||||||
// --- ESTADO Y DATOS ---
|
// --- ESTADO Y DATOS ---
|
||||||
const DEFAULT_SETTINGS = {
|
const DEFAULT_SETTINGS = {
|
||||||
@@ -62,7 +73,6 @@ let isDashboardLoading = false;
|
|||||||
// --- LÓGICA DE NEGOCIO ---
|
// --- LÓGICA DE NEGOCIO ---
|
||||||
|
|
||||||
async function loadDashboardData() {
|
async function loadDashboardData() {
|
||||||
// Guardia para prevenir ejecuciones múltiples y re-entradas.
|
|
||||||
if (currentUser.role !== 'admin' || isDashboardLoading) {
|
if (currentUser.role !== 'admin' || isDashboardLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -76,45 +86,39 @@ async function loadDashboardData() {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error('Falló la carga de datos del dashboard');
|
throw new Error('Falló la carga de datos del dashboard');
|
||||||
}
|
}
|
||||||
return; // Salir aquí después de manejar el error
|
return;
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Antes de actualizar, verificar que el dashboard sigue activo.
|
|
||||||
const dashboardTab = document.getElementById('tab-dashboard');
|
const dashboardTab = document.getElementById('tab-dashboard');
|
||||||
if (!dashboardTab.classList.contains('active')) {
|
if (!dashboardTab.classList.contains('active')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar tarjetas de estadísticas
|
|
||||||
document.getElementById('stat-total-income').textContent = `${Number(data.totalIncome || 0).toFixed(2)}`;
|
document.getElementById('stat-total-income').textContent = `${Number(data.totalIncome || 0).toFixed(2)}`;
|
||||||
document.getElementById('stat-total-movements').textContent = data.totalMovements || 0;
|
document.getElementById('stat-total-movements').textContent = data.totalMovements || 0;
|
||||||
|
|
||||||
// Actualizar datos del gráfico de ingresos
|
|
||||||
if (incomeChart) {
|
if (incomeChart) {
|
||||||
incomeChart.data.labels = data.incomeByService.map(item => item.tipo);
|
incomeChart.data.labels = data.incomeByService.map(item => item.tipo);
|
||||||
incomeChart.data.datasets[0].data = data.incomeByService.map(item => item.total);
|
incomeChart.data.datasets[0].data = data.incomeByService.map(item => item.total);
|
||||||
incomeChart.update('none');
|
incomeChart.update('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar datos del gráfico de método de pago
|
|
||||||
if (paymentMethodChart) {
|
if (paymentMethodChart) {
|
||||||
paymentMethodChart.data.labels = data.incomeByPaymentMethod.map(item => item.metodo);
|
paymentMethodChart.data.labels = data.incomeByPaymentMethod.map(item => item.metodo);
|
||||||
paymentMethodChart.data.datasets[0].data = data.incomeByPaymentMethod.map(item => item.total);
|
paymentMethodChart.data.datasets[0].data = data.incomeByPaymentMethod.map(item => item.total);
|
||||||
paymentMethodChart.update('none');
|
paymentMethodChart.update('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renderizar próximas citas
|
|
||||||
if (appointmentsList) {
|
if (appointmentsList) {
|
||||||
appointmentsList.innerHTML = '';
|
appointmentsList.innerHTML = '';
|
||||||
if (data.upcomingAppointments.length > 0) {
|
if (data.upcomingAppointments.length > 0) {
|
||||||
data.upcomingAppointments.forEach(appt => {
|
data.upcomingAppointments.forEach(appt => {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'appointment-item';
|
item.className = 'appointment-item';
|
||||||
const fechaCita = new Date(appt.fechaCita + 'T00:00:00').toLocaleDateString('es-MX', { day: '2-digit', month: 'long' });
|
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<a href="#" data-id="${appt.id}" data-action="reprint">${appt.clienteNombre}</a>
|
<a href="#" data-id="${appt.id}" data-action="reprint">${appt.clienteNombre}</a>
|
||||||
<span class="date">${fechaCita} - ${appt.horaCita}</span>
|
<span class="date">${formatDate(appt.fechaCita)} - ${appt.horaCita}</span>
|
||||||
`;
|
`;
|
||||||
appointmentsList.appendChild(item);
|
appointmentsList.appendChild(item);
|
||||||
});
|
});
|
||||||
@@ -126,7 +130,6 @@ async function loadDashboardData() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al cargar el dashboard:', error);
|
console.error('Error al cargar el dashboard:', error);
|
||||||
} finally {
|
} finally {
|
||||||
// Asegurar que el bloqueo se libere sin importar el resultado.
|
|
||||||
isDashboardLoading = false;
|
isDashboardLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,12 +185,11 @@ async function saveClient(clientData) {
|
|||||||
|
|
||||||
await save('clients', { client: clientToSave });
|
await save('clients', { client: clientToSave });
|
||||||
|
|
||||||
// Optimización: en lugar de recargar, actualizamos el estado local.
|
|
||||||
if (isUpdate) {
|
if (isUpdate) {
|
||||||
const index = clients.findIndex(c => c.id === clientToSave.id);
|
const index = clients.findIndex(c => c.id === clientToSave.id);
|
||||||
if (index > -1) clients[index] = clientToSave;
|
if (index > -1) clients[index] = clientToSave;
|
||||||
} else {
|
} else {
|
||||||
clients.unshift(clientToSave); // Añadir al principio para que aparezca primero
|
clients.unshift(clientToSave);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderClientsTable();
|
renderClientsTable();
|
||||||
@@ -251,7 +253,6 @@ function renderTable() {
|
|||||||
const client = clients.find(c => c.id === mov.clienteId);
|
const client = clients.find(c => c.id === mov.clienteId);
|
||||||
const tr = tblMovesBody.insertRow();
|
const tr = tblMovesBody.insertRow();
|
||||||
|
|
||||||
const fechaCita = mov.fechaCita ? new Date(mov.fechaCita + 'T00:00:00').toLocaleDateString('es-MX') : '';
|
|
||||||
const tipoServicio = mov.subtipo ? `${escapeHTML(mov.tipo)} (${escapeHTML(mov.subtipo)})` : escapeHTML(mov.tipo);
|
const tipoServicio = mov.subtipo ? `${escapeHTML(mov.tipo)} (${escapeHTML(mov.subtipo)})` : escapeHTML(mov.tipo);
|
||||||
|
|
||||||
const folioCell = tr.insertCell();
|
const folioCell = tr.insertCell();
|
||||||
@@ -263,8 +264,8 @@ function renderTable() {
|
|||||||
folioLink.textContent = mov.folio;
|
folioLink.textContent = mov.folio;
|
||||||
folioCell.appendChild(folioLink);
|
folioCell.appendChild(folioLink);
|
||||||
|
|
||||||
tr.insertCell().textContent = new Date(mov.fechaISO).toLocaleDateString('es-MX');
|
tr.insertCell().textContent = formatDate(mov.fechaISO);
|
||||||
tr.insertCell().textContent = `${fechaCita} ${mov.horaCita || ''}`;
|
tr.insertCell().textContent = `${formatDate(mov.fechaCita)} ${mov.horaCita || ''}`.trim();
|
||||||
tr.insertCell().textContent = client ? escapeHTML(client.nombre) : 'Cliente Eliminado';
|
tr.insertCell().textContent = client ? escapeHTML(client.nombre) : 'Cliente Eliminado';
|
||||||
tr.insertCell().textContent = tipoServicio;
|
tr.insertCell().textContent = tipoServicio;
|
||||||
tr.insertCell().textContent = Number(mov.monto).toFixed(2);
|
tr.insertCell().textContent = Number(mov.monto).toFixed(2);
|
||||||
@@ -416,7 +417,7 @@ async function handleSaveCredentials(e) {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert('Credenciales actualizadas.');
|
alert('Credenciales actualizadas.');
|
||||||
currentUser.name = name; // Actualizar el nombre en el estado local
|
currentUser.name = name;
|
||||||
currentUser.username = username;
|
currentUser.username = username;
|
||||||
document.getElementById('s-password').value = '';
|
document.getElementById('s-password').value = '';
|
||||||
} else {
|
} else {
|
||||||
@@ -546,9 +547,6 @@ async function deleteProduct(id) {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
products = products.filter(p => p.id !== id);
|
products = products.filter(p => p.id !== id);
|
||||||
renderProductTables();
|
renderProductTables();
|
||||||
} else {
|
|
||||||
const error = await response.json();
|
|
||||||
alert(`Error: ${error.error}`);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Error de conexión al eliminar el producto.');
|
alert('Error de conexión al eliminar el producto.');
|
||||||
@@ -680,7 +678,7 @@ async function handleNewMovement(e) {
|
|||||||
monto: Number(monto.toFixed(2)),
|
monto: Number(monto.toFixed(2)),
|
||||||
metodo: document.getElementById('m-metodo').value,
|
metodo: document.getElementById('m-metodo').value,
|
||||||
concepto: document.getElementById('m-articulo').value,
|
concepto: document.getElementById('m-articulo').value,
|
||||||
staff: currentUser.name, // Usar el nombre del usuario actual
|
staff: currentUser.name,
|
||||||
notas: document.getElementById('m-notas').value,
|
notas: document.getElementById('m-notas').value,
|
||||||
fechaCita: document.getElementById('m-fecha-cita').value,
|
fechaCita: document.getElementById('m-fecha-cita').value,
|
||||||
horaCita: document.getElementById('m-hora-cita').value,
|
horaCita: document.getElementById('m-hora-cita').value,
|
||||||
@@ -696,12 +694,11 @@ async function handleNewMovement(e) {
|
|||||||
function exportClientHistoryCSV(client, history) {
|
function exportClientHistoryCSV(client, history) {
|
||||||
const headers = 'Folio,Fecha,Servicio,Monto';
|
const headers = 'Folio,Fecha,Servicio,Monto';
|
||||||
const rows = history.map(mov => {
|
const rows = history.map(mov => {
|
||||||
const fecha = new Date(mov.fechaISO).toLocaleDateString('es-MX');
|
|
||||||
const servicio = mov.subtipo ? `${mov.tipo} (${mov.subtipo})` : mov.tipo;
|
const servicio = mov.subtipo ? `${mov.tipo} (${mov.subtipo})` : mov.tipo;
|
||||||
return [
|
return [
|
||||||
mov.folio,
|
mov.folio,
|
||||||
fecha,
|
formatDate(mov.fechaISO),
|
||||||
`"${servicio}"`,
|
`"${servicio}"`,
|
||||||
Number(mov.monto).toFixed(2)
|
Number(mov.monto).toFixed(2)
|
||||||
].join(',');
|
].join(',');
|
||||||
});
|
});
|
||||||
@@ -732,11 +729,10 @@ async function showClientRecord(clientId) {
|
|||||||
const clientHistoryTableBody = document.getElementById('client-history-table').querySelector('tbody');
|
const clientHistoryTableBody = document.getElementById('client-history-table').querySelector('tbody');
|
||||||
const clientCoursesContainer = document.getElementById('client-courses-history-container');
|
const clientCoursesContainer = document.getElementById('client-courses-history-container');
|
||||||
|
|
||||||
// Sanitize client details before rendering
|
|
||||||
clientDetails.innerHTML = `
|
clientDetails.innerHTML = `
|
||||||
<p><strong>Nombre:</strong> ${escapeHTML(client.nombre)}</p>
|
<p><strong>Nombre:</strong> ${escapeHTML(client.nombre)}</p>
|
||||||
<p><strong>Teléfono:</strong> ${escapeHTML(client.telefono || 'N/A')}</p>
|
<p><strong>Teléfono:</strong> ${escapeHTML(client.telefono || 'N/A')}</p>
|
||||||
<p><strong>Cumpleaños:</strong> ${escapeHTML(client.cumpleaños ? new Date(client.cumpleaños + 'T00:00:00').toLocaleDateString('es-MX') : 'N/A')}</p>
|
<p><strong>Cumpleaños:</strong> ${escapeHTML(formatDate(client.cumpleaños) || 'N/A')}</p>
|
||||||
<p><strong>Género:</strong> ${escapeHTML(client.genero || 'N/A')}</p>
|
<p><strong>Género:</strong> ${escapeHTML(client.genero || 'N/A')}</p>
|
||||||
<p><strong>Oncológico:</strong> ${client.esOncologico ? 'Sí' : 'No'}</p>
|
<p><strong>Oncológico:</strong> ${client.esOncologico ? 'Sí' : 'No'}</p>
|
||||||
`;
|
`;
|
||||||
@@ -754,10 +750,9 @@ async function showClientRecord(clientId) {
|
|||||||
if (history.length > 0) {
|
if (history.length > 0) {
|
||||||
history.forEach(mov => {
|
history.forEach(mov => {
|
||||||
const tr = clientHistoryTableBody.insertRow();
|
const tr = clientHistoryTableBody.insertRow();
|
||||||
const fecha = new Date(mov.fechaISO).toLocaleDateString('es-MX');
|
|
||||||
const servicio = mov.subtipo ? `${escapeHTML(mov.tipo)} (${escapeHTML(mov.subtipo)})` : escapeHTML(mov.tipo);
|
const servicio = mov.subtipo ? `${escapeHTML(mov.tipo)} (${escapeHTML(mov.subtipo)})` : escapeHTML(mov.tipo);
|
||||||
tr.insertCell().textContent = mov.folio;
|
tr.insertCell().textContent = mov.folio;
|
||||||
tr.insertCell().textContent = fecha;
|
tr.insertCell().textContent = formatDate(mov.fechaISO);
|
||||||
tr.insertCell().textContent = servicio;
|
tr.insertCell().textContent = servicio;
|
||||||
tr.insertCell().textContent = Number(mov.monto).toFixed(2);
|
tr.insertCell().textContent = Number(mov.monto).toFixed(2);
|
||||||
});
|
});
|
||||||
@@ -784,7 +779,7 @@ async function showClientRecord(clientId) {
|
|||||||
${courses.map(course => `
|
${courses.map(course => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${escapeHTML(course.course_name)}</td>
|
<td>${escapeHTML(course.course_name)}</td>
|
||||||
<td>${escapeHTML(course.fecha_curso)}</td>
|
<td>${escapeHTML(formatDate(course.fecha_curso))}</td>
|
||||||
<td>${escapeHTML(course.score_general)}</td>
|
<td>${escapeHTML(course.score_general)}</td>
|
||||||
<td>${course.completo_presencial ? 'Sí' : 'No'}</td>
|
<td>${course.completo_presencial ? 'Sí' : 'No'}</td>
|
||||||
<td>${course.completo_online ? 'Sí' : 'No'}</td>
|
<td>${course.completo_online ? 'Sí' : 'No'}</td>
|
||||||
@@ -876,18 +871,15 @@ function handleTableClick(e) {
|
|||||||
async function handleClientForm(e) {
|
async function handleClientForm(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await saveClient();
|
await saveClient();
|
||||||
// Después de guardar, cambiar a la pestaña de consulta
|
|
||||||
activateClientSubTab('sub-tab-consult');
|
activateClientSubTab('sub-tab-consult');
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateClientSubTab(subTabId) {
|
function activateClientSubTab(subTabId) {
|
||||||
if (!subTabId) return;
|
if (!subTabId) return;
|
||||||
|
|
||||||
// Desactivar todas las sub-pestañas y contenidos de clientes
|
|
||||||
document.querySelectorAll('#tab-clients .sub-tab-link').forEach(tab => tab.classList.remove('active'));
|
document.querySelectorAll('#tab-clients .sub-tab-link').forEach(tab => tab.classList.remove('active'));
|
||||||
document.querySelectorAll('#tab-clients .sub-tab-content').forEach(content => content.classList.remove('active'));
|
document.querySelectorAll('#tab-clients .sub-tab-content').forEach(content => content.classList.remove('active'));
|
||||||
|
|
||||||
// Activar la sub-pestaña y el contenido correctos
|
|
||||||
const tabButton = document.querySelector(`[data-subtab="${subTabId}"]`);
|
const tabButton = document.querySelector(`[data-subtab="${subTabId}"]`);
|
||||||
const tabContent = document.getElementById(subTabId);
|
const tabContent = document.getElementById(subTabId);
|
||||||
|
|
||||||
@@ -910,11 +902,9 @@ function handleClientTabChange(e) {
|
|||||||
function activateTab(tabId) {
|
function activateTab(tabId) {
|
||||||
if (!tabId) return;
|
if (!tabId) return;
|
||||||
|
|
||||||
// Desactivar todas las pestañas y contenidos
|
|
||||||
document.querySelectorAll('.tab-link').forEach(tab => tab.classList.remove('active'));
|
document.querySelectorAll('.tab-link').forEach(tab => tab.classList.remove('active'));
|
||||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||||
|
|
||||||
// Activar la pestaña y el contenido correctos
|
|
||||||
const tabButton = document.querySelector(`[data-tab="${tabId}"]`);
|
const tabButton = document.querySelector(`[data-tab="${tabId}"]`);
|
||||||
const tabContent = document.getElementById(tabId);
|
const tabContent = document.getElementById(tabId);
|
||||||
|
|
||||||
@@ -925,9 +915,7 @@ function activateTab(tabId) {
|
|||||||
tabContent.classList.add('active');
|
tabContent.classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cargar datos dinámicos si es la pestaña del dashboard
|
|
||||||
if (tabId === 'tab-dashboard' && currentUser.role === 'admin') {
|
if (tabId === 'tab-dashboard' && currentUser.role === 'admin') {
|
||||||
// Si es la primera vez que se visita la pestaña, inicializar el gráfico
|
|
||||||
if (!incomeChart) {
|
if (!incomeChart) {
|
||||||
const ctx = document.getElementById('incomeChart').getContext('2d');
|
const ctx = document.getElementById('incomeChart').getContext('2d');
|
||||||
incomeChart = new Chart(ctx, {
|
incomeChart = new Chart(ctx, {
|
||||||
@@ -966,7 +954,6 @@ function activateTab(tabId) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Cargar (o recargar) los datos del dashboard
|
|
||||||
loadDashboardData();
|
loadDashboardData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -975,13 +962,11 @@ function handleTabChange(e) {
|
|||||||
const tabButton = e.target.closest('.tab-link');
|
const tabButton = e.target.closest('.tab-link');
|
||||||
if (!tabButton) return;
|
if (!tabButton) return;
|
||||||
|
|
||||||
// Solo prevenir el comportamiento por defecto si es un botón para cambiar de pestaña
|
|
||||||
if (tabButton.dataset.tab) {
|
if (tabButton.dataset.tab) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const tabId = tabButton.dataset.tab;
|
const tabId = tabButton.dataset.tab;
|
||||||
activateTab(tabId);
|
activateTab(tabId);
|
||||||
}
|
}
|
||||||
// Si no tiene data-tab (es un enlace normal como el de Clientes), no hacer nada y permitir la navegación.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTestTicket() {
|
function handleTestTicket() {
|
||||||
@@ -1011,11 +996,13 @@ function setupUIForRole(role) {
|
|||||||
const settingsTab = document.querySelector('[data-tab="settings"]');
|
const settingsTab = document.querySelector('[data-tab="settings"]');
|
||||||
const userManagementSection = document.getElementById('user-management-section');
|
const userManagementSection = document.getElementById('user-management-section');
|
||||||
const staffInput = document.getElementById('m-staff');
|
const staffInput = document.getElementById('m-staff');
|
||||||
|
const dbInfoIcon = document.getElementById('db-info-icon');
|
||||||
|
|
||||||
if (role === 'admin') {
|
if (role === 'admin') {
|
||||||
if (dashboardTab) dashboardTab.style.display = 'block';
|
if (dashboardTab) dashboardTab.style.display = 'block';
|
||||||
if (settingsTab) settingsTab.style.display = 'block';
|
if (settingsTab) settingsTab.style.display = 'block';
|
||||||
if (userManagementSection) userManagementSection.style.display = 'block';
|
if (userManagementSection) userManagementSection.style.display = 'block';
|
||||||
|
if (dbInfoIcon) dbInfoIcon.style.display = 'inline-block';
|
||||||
|
|
||||||
fetch('/api/users')
|
fetch('/api/users')
|
||||||
.then(res => {
|
.then(res => {
|
||||||
@@ -1031,6 +1018,7 @@ function setupUIForRole(role) {
|
|||||||
if (dashboardTab) dashboardTab.style.display = 'none';
|
if (dashboardTab) dashboardTab.style.display = 'none';
|
||||||
if (settingsTab) settingsTab.style.display = 'none';
|
if (settingsTab) settingsTab.style.display = 'none';
|
||||||
if (userManagementSection) userManagementSection.style.display = 'none';
|
if (userManagementSection) userManagementSection.style.display = 'none';
|
||||||
|
if (dbInfoIcon) dbInfoIcon.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (staffInput) {
|
if (staffInput) {
|
||||||
@@ -1043,7 +1031,7 @@ function populateFooter() {
|
|||||||
const versionElement = document.getElementById('footer-version');
|
const versionElement = document.getElementById('footer-version');
|
||||||
|
|
||||||
if (dateElement) {
|
if (dateElement) {
|
||||||
dateElement.textContent = new Date().toLocaleDateString('es-MX', { year: 'numeric', month: 'long', day: 'numeric' });
|
dateElement.textContent = formatDate(new Date().toISOString());
|
||||||
}
|
}
|
||||||
if (versionElement) {
|
if (versionElement) {
|
||||||
versionElement.textContent = `Versión ${APP_VERSION}`;
|
versionElement.textContent = `Versión ${APP_VERSION}`;
|
||||||
@@ -1054,17 +1042,14 @@ function populateFooter() {
|
|||||||
// --- INICIALIZACIÓN ---
|
// --- INICIALIZACIÓN ---
|
||||||
|
|
||||||
async function initializeApp() {
|
async function initializeApp() {
|
||||||
// 1. Verificar autenticación y obtener datos del usuario.
|
|
||||||
let userResponse;
|
let userResponse;
|
||||||
try {
|
try {
|
||||||
userResponse = await fetch('/api/user');
|
userResponse = await fetch('/api/user');
|
||||||
if (!userResponse.ok) {
|
if (!userResponse.ok) {
|
||||||
// Si la respuesta no es 2xx, el usuario no está autenticado o hay un error.
|
|
||||||
window.location.href = '/login.html';
|
window.location.href = '/login.html';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar que la respuesta sea JSON antes de procesarla.
|
|
||||||
const contentType = userResponse.headers.get('content-type');
|
const contentType = userResponse.headers.get('content-type');
|
||||||
if (!contentType || !contentType.includes('application/json')) {
|
if (!contentType || !contentType.includes('application/json')) {
|
||||||
console.error('La respuesta del servidor no es JSON. Redirigiendo al login.');
|
console.error('La respuesta del servidor no es JSON. Redirigiendo al login.');
|
||||||
@@ -1072,22 +1057,20 @@ async function initializeApp() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Procesar datos del usuario.
|
|
||||||
currentUser = await userResponse.json();
|
currentUser = await userResponse.json();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Si hay un error de red, es probable que el servidor no esté corriendo.
|
|
||||||
console.error('Error de conexión al verificar la autenticación. Redirigiendo al login.', error);
|
console.error('Error de conexión al verificar la autenticación. Redirigiendo al login.', error);
|
||||||
window.location.href = '/login.html';
|
window.location.href = '/login.html';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Añadir manejadores de eventos.
|
|
||||||
const tabs = document.querySelector('.tabs');
|
const tabs = document.querySelector('.tabs');
|
||||||
const btnLogout = document.getElementById('btnLogout');
|
const btnLogout = document.getElementById('btnLogout');
|
||||||
const btnCancelEditUser = document.getElementById('btnCancelEditUser');
|
const btnCancelEditUser = document.getElementById('btnCancelEditUser');
|
||||||
const tipoServicioSelect = document.getElementById('m-tipo');
|
const tipoServicioSelect = document.getElementById('m-tipo');
|
||||||
const clientSubTabs = document.querySelector('#tab-clients .sub-tabs');
|
const clientSubTabs = document.querySelector('#tab-clients .sub-tabs');
|
||||||
|
const dbInfoIcon = document.getElementById('db-info-icon');
|
||||||
|
|
||||||
formSettings?.addEventListener('submit', handleSaveSettings);
|
formSettings?.addEventListener('submit', handleSaveSettings);
|
||||||
formCredentials?.addEventListener('submit', handleSaveCredentials);
|
formCredentials?.addEventListener('submit', handleSaveCredentials);
|
||||||
@@ -1107,6 +1090,9 @@ async function initializeApp() {
|
|||||||
if (currentUser.role === 'admin') {
|
if (currentUser.role === 'admin') {
|
||||||
formAddUser?.addEventListener('submit', handleAddOrUpdateUser);
|
formAddUser?.addEventListener('submit', handleAddOrUpdateUser);
|
||||||
tblUsersBody?.addEventListener('click', handleTableClick);
|
tblUsersBody?.addEventListener('click', handleTableClick);
|
||||||
|
dbInfoIcon?.addEventListener('click', () => {
|
||||||
|
document.getElementById('db-instructions').classList.toggle('hidden');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
btnLogout?.addEventListener('click', async () => {
|
btnLogout?.addEventListener('click', async () => {
|
||||||
@@ -1193,7 +1179,6 @@ async function initializeApp() {
|
|||||||
showAddCourseModal(clientId);
|
showAddCourseModal(clientId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Cargar el resto de los datos de la aplicación.
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
load(KEY_SETTINGS, DEFAULT_SETTINGS),
|
load(KEY_SETTINGS, DEFAULT_SETTINGS),
|
||||||
load(KEY_DATA, []),
|
load(KEY_DATA, []),
|
||||||
@@ -1213,7 +1198,6 @@ async function initializeApp() {
|
|||||||
renderProductTables();
|
renderProductTables();
|
||||||
console.log('Updating client datalist...');
|
console.log('Updating client datalist...');
|
||||||
updateClientDatalist();
|
updateClientDatalist();
|
||||||
// Initial population of the articulo dropdown
|
|
||||||
populateArticuloDropdown(document.getElementById('m-categoria').value);
|
populateArticuloDropdown(document.getElementById('m-categoria').value);
|
||||||
|
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
@@ -1234,7 +1218,7 @@ async function initializeApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Activating client sub-tab...');
|
console.log('Activating client sub-tab...');
|
||||||
activateClientSubTab('sub-tab-consult');
|
activateClientSubTab('sub-tab-register');
|
||||||
console.log('Clearing client record...');
|
console.log('Clearing client record...');
|
||||||
clearClientRecord();
|
clearClientRecord();
|
||||||
console.log('Populating footer...');
|
console.log('Populating footer...');
|
||||||
@@ -1247,5 +1231,4 @@ async function initializeApp() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initializeApp);
|
||||||
document.addEventListener('DOMContentLoaded', initializeApp);
|
|
||||||
|
|||||||
45
clients.js
45
clients.js
@@ -2,6 +2,18 @@ import { load, save, remove, KEY_CLIENTS } from './storage.js';
|
|||||||
|
|
||||||
let clients = [];
|
let clients = [];
|
||||||
|
|
||||||
|
// --- UTILITIES ---
|
||||||
|
function formatDate(dateString) {
|
||||||
|
if (!dateString) return '';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const userTimezoneOffset = date.getTimezoneOffset() * 60000;
|
||||||
|
const adjustedDate = new Date(date.getTime() + userTimezoneOffset);
|
||||||
|
const day = String(adjustedDate.getDate()).padStart(2, '0');
|
||||||
|
const month = String(adjustedDate.getMonth() + 1).padStart(2, '0');
|
||||||
|
const year = adjustedDate.getFullYear();
|
||||||
|
return `${day}/${month}/${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
// --- DOM ELEMENTS ---
|
// --- DOM ELEMENTS ---
|
||||||
const formClient = document.getElementById('formClient');
|
const formClient = document.getElementById('formClient');
|
||||||
const tblClientsBody = document.getElementById('tblClients')?.querySelector('tbody');
|
const tblClientsBody = document.getElementById('tblClients')?.querySelector('tbody');
|
||||||
@@ -64,11 +76,10 @@ async function deleteClient(id) {
|
|||||||
function exportClientHistoryCSV(client, history) {
|
function exportClientHistoryCSV(client, history) {
|
||||||
const headers = 'Folio,Fecha,Servicio,Monto';
|
const headers = 'Folio,Fecha,Servicio,Monto';
|
||||||
const rows = history.map(mov => {
|
const rows = history.map(mov => {
|
||||||
const fecha = new Date(mov.fechaISO).toLocaleDateString('es-MX');
|
|
||||||
const servicio = mov.subtipo ? `${mov.tipo} (${mov.subtipo})` : mov.tipo;
|
const servicio = mov.subtipo ? `${mov.tipo} (${mov.subtipo})` : mov.tipo;
|
||||||
return [
|
return [
|
||||||
mov.folio,
|
mov.folio,
|
||||||
fecha,
|
formatDate(mov.fechaISO),
|
||||||
`"${servicio}"`, // Corrected: escaped inner quotes for CSV compatibility
|
`"${servicio}"`, // Corrected: escaped inner quotes for CSV compatibility
|
||||||
Number(mov.monto).toFixed(2)
|
Number(mov.monto).toFixed(2)
|
||||||
].join(',');
|
].join(',');
|
||||||
@@ -101,19 +112,18 @@ async function toggleClientHistory(row, client) {
|
|||||||
historyRow.id = historyRowId;
|
historyRow.id = historyRowId;
|
||||||
historyRow.className = 'client-history-row';
|
historyRow.className = 'client-history-row';
|
||||||
|
|
||||||
let historyHtml = '
|
let historyHtml = `
|
||||||
<div class="client-history-header">
|
<div class="client-history-header">
|
||||||
<h4>Historial de Servicios</h4>
|
<h4>Historial de Servicios</h4>
|
||||||
<button class="action-btn" id="btn-export-history-' + client˚.id + '">Exportar CSV</button>
|
<button class="action-btn" id="btn-export-history-${client.id}">Exportar CSV</button>
|
||||||
</div>
|
</div>
|
||||||
';
|
`;
|
||||||
|
|
||||||
if (history.length > 0) {
|
if (history.length > 0) {
|
||||||
historyHtml += '<table><thead><tr><th>Folio</th><th>Fecha</th><th>Servicio</th><th>Monto</th></tr></thead><tbody>';
|
historyHtml += '<table><thead><tr><th>Folio</th><th>Fecha</th><th>Servicio</th><th>Monto</th></tr></thead><tbody>';
|
||||||
history.forEach(mov => {
|
history.forEach(mov => {
|
||||||
const fecha = new Date(mov.fechaISO).toLocaleDateString('es-MX');
|
|
||||||
const servicio = mov.subtipo ? `${mov.tipo} (${mov.subtipo})` : mov.tipo;
|
const servicio = mov.subtipo ? `${mov.tipo} (${mov.subtipo})` : mov.tipo;
|
||||||
historyHtml += `<tr><td>${mov.folio}</td><td>${fecha}</td><td>${servicio}</td><td>${Number(mov.monto).toFixed(2)}</td></tr>`;
|
historyHtml += `<tr><td>${mov.folio}</td><td>${formatDate(mov.fechaISO)}</td><td>${servicio}</td><td>${Number(mov.monto).toFixed(2)}</td></tr>`;
|
||||||
});
|
});
|
||||||
historyHtml += '</tbody></table>';
|
historyHtml += '</tbody></table>';
|
||||||
} else {
|
} else {
|
||||||
@@ -145,16 +155,16 @@ function renderClientsTable(clientList = clients) {
|
|||||||
clientList.forEach(c => {
|
clientList.forEach(c => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.dataset.id = c.id;
|
tr.dataset.id = c.id;
|
||||||
tr.innerHTML = '
|
tr.innerHTML = `
|
||||||
<td>' + c.nombre + '</td>
|
<td>${c.nombre}</td>
|
||||||
<td>' + (c.telefono || '') + '</td>
|
<td>${c.telefono || ''}</td>
|
||||||
<td>' + (c.esOncologico ? 'Sí' : 'No') + '</td>
|
<td>${c.esOncologico ? 'Sí' : 'No'}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="action-btn" data-id="' + c.id + '" data-action="view-history">Historial</button>
|
<button class="action-btn" data-id="${c.id}" data-action="view-history">Historial</button>
|
||||||
<button class="action-btn" data-id="' + c.id + '" data-action="edit-client">Editar</button>
|
<button class="action-btn" data-id="${c.id}" data-action="edit-client">Editar</button>
|
||||||
<button class="action-btn" data-id="' + c.id + '" data-action="delete-client">Eliminar</button>
|
<button class="action-btn" data-id="${c.id}" data-action="delete-client">Eliminar</button>
|
||||||
</td>
|
</td>
|
||||||
';
|
`;
|
||||||
tblClientsBody.appendChild(tr);
|
tblClientsBody.appendChild(tr);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -209,7 +219,6 @@ async function handleClientForm(e) {
|
|||||||
// --- INICIALIZACIÓN ---
|
// --- INICIALIZACIÓN ---
|
||||||
|
|
||||||
async function initializeClientsPage() {
|
async function initializeClientsPage() {
|
||||||
// 1. Verificar autenticación
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/check-auth');
|
const response = await fetch('/api/check-auth');
|
||||||
const auth = await response.json();
|
const auth = await response.json();
|
||||||
@@ -223,11 +232,9 @@ async function initializeClientsPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Cargar clientes
|
|
||||||
clients = await load(KEY_CLIENTS, []);
|
clients = await load(KEY_CLIENTS, []);
|
||||||
renderClientsTable();
|
renderClientsTable();
|
||||||
|
|
||||||
// 3. Añadir manejadores de eventos
|
|
||||||
formClient?.addEventListener('submit', handleClientForm);
|
formClient?.addEventListener('submit', handleClientForm);
|
||||||
tblClientsBody?.addEventListener('click', handleTableClick);
|
tblClientsBody?.addEventListener('click', handleTableClick);
|
||||||
|
|
||||||
@@ -249,4 +256,4 @@ async function initializeClientsPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initializeClientsPage);
|
document.addEventListener('DOMContentLoaded', initializeClientsPage);
|
||||||
16
index.html
16
index.html
@@ -372,12 +372,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="section">
|
|
||||||
<h2>Ubicación de los Datos</h2>
|
|
||||||
<p class="data-location-info">
|
|
||||||
Toda la información de tu negocio (clientes, recibos y configuración) se guarda de forma segura en el archivo <strong>ap-pos.db</strong>, ubicado en la misma carpeta que la aplicación. Para hacer un respaldo, simplemente copia este archivo.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="section" id="credentials-section">
|
<div class="section" id="credentials-section">
|
||||||
<h2>Mis Credenciales</h2>
|
<h2>Mis Credenciales</h2>
|
||||||
<form id="formCredentials">
|
<form id="formCredentials">
|
||||||
@@ -439,6 +433,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="section" id="db-location-section">
|
||||||
|
<h2>Ubicación y Exportación de Datos
|
||||||
|
<span class="material-icons-outlined info-icon" id="db-info-icon" style="display: none;">help_outline</span>
|
||||||
|
</h2>
|
||||||
|
<div id="db-instructions" class="data-location-info hidden">
|
||||||
|
<p>Toda la información de tu negocio (clientes, recibos, configuración) se guarda localmente. Para respaldar tus datos, simplemente copia el archivo correspondiente.</p>
|
||||||
|
<p>Para usuarios avanzados, la base de datos (<code>ap-pos.db</code>) se encuentra dentro del contenedor de la aplicación. Puedes acceder a ella usando los comandos estándar de Docker para copiar el archivo.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
58
print.js
58
print.js
@@ -4,15 +4,28 @@
|
|||||||
* @returns {string} El string escapado.
|
* @returns {string} El string escapado.
|
||||||
*/
|
*/
|
||||||
function esc(str) {
|
function esc(str) {
|
||||||
return String(str || '').replace(/[&<>"']/g, c => ({
|
return String(str || '').replace(/[&<>"'/]/g, c => ({
|
||||||
"&": "&",
|
"&": "&",
|
||||||
"<": "<",
|
"<": "<",
|
||||||
">": ">",
|
">": ">",
|
||||||
"\"": """,
|
'"': '"',
|
||||||
"'": "'"
|
"'": "'"
|
||||||
}[c]));
|
}[c]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDate(dateString) {
|
||||||
|
if (!dateString) return '';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const userTimezoneOffset = date.getTimezoneOffset() * 60000;
|
||||||
|
const adjustedDate = new Date(date.getTime() + userTimezoneOffset);
|
||||||
|
const day = String(adjustedDate.getDate()).padStart(2, '0');
|
||||||
|
const month = String(adjustedDate.getMonth() + 1).padStart(2, '0');
|
||||||
|
const year = adjustedDate.getFullYear();
|
||||||
|
const hours = String(adjustedDate.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(adjustedDate.getMinutes()).padStart(2, '0');
|
||||||
|
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Genera el HTML para un ticket de movimiento.
|
* Genera el HTML para un ticket de movimiento.
|
||||||
* @param {object} mov El objeto del movimiento.
|
* @param {object} mov El objeto del movimiento.
|
||||||
@@ -20,8 +33,7 @@ function esc(str) {
|
|||||||
* @returns {string} El HTML del ticket.
|
* @returns {string} El HTML del ticket.
|
||||||
*/
|
*/
|
||||||
function templateTicket(mov, settings) {
|
function templateTicket(mov, settings) {
|
||||||
const dt = new Date(mov.fechaISO || Date.now());
|
const fechaLocal = formatDate(mov.fechaISO || Date.now());
|
||||||
const fechaLocal = dt.toLocaleString('es-MX', { dateStyle: 'medium', timeStyle: 'short' });
|
|
||||||
const montoFormateado = Number(mov.monto).toFixed(2);
|
const montoFormateado = Number(mov.monto).toFixed(2);
|
||||||
const tipoServicio = mov.subtipo === 'Retoque' ? `Retoque de ${mov.tipo}` : mov.tipo;
|
const tipoServicio = mov.subtipo === 'Retoque' ? `Retoque de ${mov.tipo}` : mov.tipo;
|
||||||
|
|
||||||
@@ -50,7 +62,6 @@ function templateTicket(mov, settings) {
|
|||||||
lines.push('<div class="t-divider"></div>');
|
lines.push('<div class="t-divider"></div>');
|
||||||
lines.push(`<div class="t-row t-bold"><span>Total</span><span>${montoFormateado}</span></div>`);
|
lines.push(`<div class="t-row t-bold"><span>Total</span><span>${montoFormateado}</span></div>`);
|
||||||
|
|
||||||
// Sección de consentimientos
|
|
||||||
if (mov.client && (mov.client.esOncologico || mov.client.consentimiento)) {
|
if (mov.client && (mov.client.esOncologico || mov.client.consentimiento)) {
|
||||||
lines.push('<div class="t-divider"></div>');
|
lines.push('<div class="t-divider"></div>');
|
||||||
if (mov.client.esOncologico) {
|
if (mov.client.esOncologico) {
|
||||||
@@ -66,7 +77,6 @@ function templateTicket(mov, settings) {
|
|||||||
|
|
||||||
if (settings.leyenda) lines.push(`<div class="t-footer t-center t-small">${esc(settings.leyenda)}</div>`);
|
if (settings.leyenda) lines.push(`<div class="t-footer t-center t-small">${esc(settings.leyenda)}</div>`);
|
||||||
|
|
||||||
// Sección de Encuesta con QR
|
|
||||||
lines.push('<div class="t-qr-section">');
|
lines.push('<div class="t-qr-section">');
|
||||||
lines.push('<div class="t-small t-bold">¡Tu opinión es muy importante!</div>');
|
lines.push('<div class="t-small t-bold">¡Tu opinión es muy importante!</div>');
|
||||||
lines.push('<div class="t-small">Escanea el código QR para darnos tu feedback.</div>');
|
lines.push('<div class="t-small">Escanea el código QR para darnos tu feedback.</div>');
|
||||||
@@ -91,24 +101,18 @@ export async function renderTicketAndPrint(mov, settings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Renderizar la estructura HTML del ticket
|
|
||||||
printArea.innerHTML = templateTicket(mov, settings);
|
printArea.innerHTML = templateTicket(mov, settings);
|
||||||
|
|
||||||
// 2. Encontrar el elemento canvas para el QR
|
|
||||||
const canvas = document.getElementById('qr-canvas');
|
const canvas = document.getElementById('qr-canvas');
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
console.error("El canvas del QR #qr-canvas no se encontró. Se imprimirá sin QR.");
|
console.error("El canvas del QR #qr-canvas no se encontró. Se imprimirá sin QR.");
|
||||||
window.print(); // Imprimir sin QR de inmediato
|
window.print();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Generar el código QR
|
|
||||||
const qrUrl = 'http://vanityexperience.mx/qr';
|
const qrUrl = 'http://vanityexperience.mx/qr';
|
||||||
await QRCode.toCanvas(canvas, qrUrl, { width: 140, margin: 1 });
|
await QRCode.toCanvas(canvas, qrUrl, { width: 140, margin: 1 });
|
||||||
|
|
||||||
// 4. Llamar a la impresión.
|
|
||||||
// El `await` anterior asegura que el QR ya está renderizado.
|
|
||||||
// Un pequeño timeout puede seguir siendo útil para asegurar que el navegador "pinte" el canvas en la pantalla.
|
|
||||||
requestAnimationFrame(() => window.print());
|
requestAnimationFrame(() => window.print());
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -116,3 +120,31 @@ export async function renderTicketAndPrint(mov, settings) {
|
|||||||
alert(`Ocurrió un error al preparar la impresión: ${error.message}. Revise la consola para más detalles.`);
|
alert(`Ocurrió un error al preparar la impresión: ${error.message}. Revise la consola para más detalles.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const btnTestTicket = document.getElementById('btnTestTicket');
|
||||||
|
if (btnTestTicket) {
|
||||||
|
btnTestTicket.addEventListener('click', () => {
|
||||||
|
const demoMovement = {
|
||||||
|
id: 'demo',
|
||||||
|
folio: 'DEMO-000001',
|
||||||
|
fechaISO: new Date().toISOString(),
|
||||||
|
client: {
|
||||||
|
nombre: 'Cliente de Prueba',
|
||||||
|
esOncologico: true,
|
||||||
|
nombreMedico: 'Dr. Juan Pérez',
|
||||||
|
telefonoMedico: '5512345678',
|
||||||
|
cedulaMedico: '1234567'
|
||||||
|
},
|
||||||
|
tipo: 'Pago',
|
||||||
|
monto: 123.45,
|
||||||
|
metodo: 'Efectivo',
|
||||||
|
concepto: 'Producto de demostración',
|
||||||
|
staff: 'Admin',
|
||||||
|
notas: 'Esta es una impresión de prueba.'
|
||||||
|
};
|
||||||
|
renderTicketAndPrint(demoMovement, window.settings || {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
44
styles.css
44
styles.css
@@ -154,39 +154,6 @@ button:hover {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tblMoves, #tblClients, #tblUsers {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tblMoves th, #tblMoves td,
|
|
||||||
#tblClients th, #tblClients td,
|
|
||||||
#tblUsers th, #tblUsers td {
|
|
||||||
border-bottom: 1px solid #dee2e6;
|
|
||||||
padding: 12px 15px;
|
|
||||||
text-align: left;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
#tblMoves td:last-child, #tblMoves th:last-child,
|
|
||||||
#tblClients td:last-child, #tblClients th:last-child,
|
|
||||||
#tblUsers td:last-child, #tblUsers th:last-child {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tblMoves th, #tblClients th, #tblUsers th {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
font-family: 'Montserrat', sans-serif;
|
|
||||||
font-weight: 500;
|
|
||||||
border-bottom-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tblMoves tbody tr:last-child td,
|
|
||||||
#tblClients tbody tr:last-child td,
|
|
||||||
#tblUsers tbody tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -530,6 +497,17 @@ table tbody tr:hover {
|
|||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #6c757d;
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.info-icon:hover {
|
||||||
|
color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
.sub-section {
|
.sub-section {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
|
|||||||
Reference in New Issue
Block a user