Enhance styles across various components
This commit is contained in:
parent
9e7b4b3562
commit
ee252a2648
11
index.html
11
index.html
|
|
@ -1,14 +1,17 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/doli-favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/doli-favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>doli-front</title>
|
||||
<meta name="theme-color" content="#2563eb" />
|
||||
<meta name="description" content="Doli — Gestión de facturación" />
|
||||
<title>Doli</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/src/styles/modal.css" />
|
||||
</head>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#3b82f6"/>
|
||||
<stop offset="100%" stop-color="#1d4ed8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="32" height="32" rx="8" fill="url(#g)"/>
|
||||
<path d="M10 7h7.5a3.5 3.5 0 0 1 2.47 6L14.5 16.5a3.5 3.5 0 0 1-2.47 6H10V7z" fill="#fff" opacity=".92"/>
|
||||
<path d="M10 17h5.3" stroke="#fff" stroke-width="1.2" stroke-linecap="round" opacity=".6"/>
|
||||
<path d="M10 21.5h5" stroke="#fff" stroke-width="1.2" stroke-linecap="round" opacity=".6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 611 B |
|
|
@ -1,10 +1,6 @@
|
|||
/**
|
||||
* Muestra un toast de aviso cuando se bloquea la navegación
|
||||
* @param {string} message - Mensaje a mostrar
|
||||
* @param {number} duration - Duración en ms (default: 3000)
|
||||
*/
|
||||
import { icons } from '../services/icons.js';
|
||||
|
||||
export function showBlockedNavigationToast(message = 'Guarda los cambios antes de salir', duration = 3000) {
|
||||
// Eliminar toast existente si hay uno
|
||||
const existingToast = document.querySelector('.blocked-nav-toast');
|
||||
if (existingToast) {
|
||||
existingToast.remove();
|
||||
|
|
@ -15,23 +11,17 @@ export function showBlockedNavigationToast(message = 'Guarda los cambios antes d
|
|||
|
||||
toast.innerHTML = /*html*/`
|
||||
<div class="blocked-nav-toast-content">
|
||||
<svg class="blocked-nav-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="12"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>
|
||||
${icons.info}
|
||||
<span class="blocked-nav-message">${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Animar entrada
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add('visible');
|
||||
});
|
||||
|
||||
// Auto-cerrar después del tiempo especificado
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('visible');
|
||||
toast.classList.add('hiding');
|
||||
|
|
@ -42,4 +32,4 @@ export function showBlockedNavigationToast(message = 'Guarda los cambios antes d
|
|||
}, duration);
|
||||
|
||||
return toast;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import { icons } from '../services/icons.js';
|
||||
|
||||
export function InvoiceItem(invoice, onView) {
|
||||
const item = document.createElement('tr');
|
||||
item.className = 'invoice-item';
|
||||
|
||||
// Formatear fecha
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('es-ES');
|
||||
};
|
||||
|
||||
// Formatear moneda
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('es-ES', {
|
||||
style: 'currency',
|
||||
|
|
@ -16,19 +16,17 @@ export function InvoiceItem(invoice, onView) {
|
|||
}).format(amount);
|
||||
};
|
||||
|
||||
// Obtener clase de estado
|
||||
const getStatusClass = (status) => {
|
||||
const statusMap = {
|
||||
'draft': 'status-draft',
|
||||
'validated': 'status-unpaid',
|
||||
'paid': 'status-paid',
|
||||
'unpaid': 'status-unpaid',
|
||||
'canceled': 'status-canceled'
|
||||
'draft': 'badge-draft',
|
||||
'validated': 'badge-unpaid',
|
||||
'paid': 'badge-paid',
|
||||
'unpaid': 'badge-unpaid',
|
||||
'canceled': 'badge-draft'
|
||||
};
|
||||
return statusMap[status] || 'status-default';
|
||||
return statusMap[status] || 'badge-draft';
|
||||
};
|
||||
|
||||
// Obtener texto de estado
|
||||
const getStatusText = (status) => {
|
||||
const statusTextMap = {
|
||||
'draft': 'Borrador',
|
||||
|
|
@ -40,35 +38,40 @@ export function InvoiceItem(invoice, onView) {
|
|||
return statusTextMap[status] || status;
|
||||
};
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (str == null) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = String(str);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
item.innerHTML = /*html*/`
|
||||
<td class="invoice-number">
|
||||
<button class="btn-invoice-num" title="Ver detalles">${invoice.number}</button>
|
||||
<button class="btn-invoice-num" title="Ver detalles">${escapeHtml(invoice.number)}</button>
|
||||
</td>
|
||||
<td class="invoice-status">
|
||||
<span class="status-badge ${getStatusClass(invoice.status)}">
|
||||
${getStatusText(invoice.status)}
|
||||
<span class="badge ${getStatusClass(invoice.status)}">
|
||||
${escapeHtml(getStatusText(invoice.status))}
|
||||
</span>
|
||||
</td>
|
||||
<td class="invoice-client">
|
||||
<span class="client-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></span>
|
||||
${invoice.clientName || 'Sin nombre'}
|
||||
<span class="client-icon">${icons.user}</span>
|
||||
${escapeHtml(invoice.clientName || 'Sin nombre')}
|
||||
</td>
|
||||
<td class="invoice-date">${formatDate(invoice.date)}</td>
|
||||
<td class="invoice-total">${formatCurrency(invoice.total)}</td>
|
||||
<td class="invoice-remain">${formatCurrency(invoice.remainToPay)}</td>
|
||||
<td class="invoice-actions">
|
||||
<button class="btn-action btn-view" data-invoice-id="${invoice.id}" title="Ver/Editar detalles">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
${icons.eye}
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
// Invoice number — also opens modal
|
||||
item.querySelector('.btn-invoice-num').addEventListener('click', () => onView?.(invoice.id));
|
||||
|
||||
// Event listener para el botón de ver
|
||||
const viewBtn = item.querySelector('.btn-view');
|
||||
viewBtn.addEventListener('click', () => onView?.(invoice.id));
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { doliLogo } from '../services/icons.js';
|
||||
|
||||
export function renderLogin(onLogin) {
|
||||
const loginContainer = document.createElement('div');
|
||||
loginContainer.className = 'login-container';
|
||||
|
|
@ -6,14 +8,8 @@ export function renderLogin(onLogin) {
|
|||
<div class="login-card">
|
||||
<div class="login-brand">
|
||||
<div class="login-logo-icon">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
</svg>
|
||||
${doliLogo(44)}
|
||||
</div>
|
||||
<span class="login-brand-name">Doli</span>
|
||||
</div>
|
||||
<h2 class="login-title">Iniciar sesión</h2>
|
||||
<p class="login-subtitle">Accede a tu cuenta para continuar</p>
|
||||
|
|
@ -40,4 +36,4 @@ export function renderLogin(onLogin) {
|
|||
});
|
||||
|
||||
return loginContainer;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { auth } from '../services/auth.js';
|
||||
import { doliLogo, icons } from '../services/icons.js';
|
||||
|
||||
export function createSidebar(pages, currentPage) {
|
||||
const sidebar = document.createElement('aside');
|
||||
|
|
@ -10,17 +11,12 @@ export function createSidebar(pages, currentPage) {
|
|||
sidebarHeader.innerHTML = /*html*/`
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-logo-icon">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
</svg>
|
||||
${doliLogo(26)}
|
||||
</div>
|
||||
<h2>Doli</h2>
|
||||
</div>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle" aria-label="Toggle sidebar">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||
<button class="sidebar-toggle" id="sidebar-toggle" aria-label="Alternar barra lateral">
|
||||
${icons.menu}
|
||||
</button>
|
||||
`;
|
||||
|
||||
|
|
@ -39,7 +35,7 @@ export function createSidebar(pages, currentPage) {
|
|||
link.classList.add('active');
|
||||
}
|
||||
link.innerHTML = /*html*/`
|
||||
<span class="sidebar-icon">${page.icon || '📄'}</span>
|
||||
<span class="sidebar-icon">${page.icon}</span>
|
||||
<span class="sidebar-text">${page.name}</span>
|
||||
`;
|
||||
listItem.appendChild(link);
|
||||
|
|
@ -50,7 +46,6 @@ export function createSidebar(pages, currentPage) {
|
|||
sidebar.appendChild(sidebarHeader);
|
||||
sidebar.appendChild(nav);
|
||||
|
||||
// User footer
|
||||
const user = auth.getUser();
|
||||
const identifier = user?.identifier || user?.name || user?.login || '?';
|
||||
const initials = identifier.slice(0, 2).toUpperCase();
|
||||
|
|
@ -63,11 +58,7 @@ export function createSidebar(pages, currentPage) {
|
|||
<span class="sidebar-username">${identifier}</span>
|
||||
</div>
|
||||
<button class="sidebar-logout-btn" title="Cerrar sesión">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
||||
<polyline points="16 17 21 12 16 7"/>
|
||||
<line x1="21" y1="12" x2="9" y2="12"/>
|
||||
</svg>
|
||||
${icons.logout}
|
||||
</button>
|
||||
`;
|
||||
|
||||
|
|
@ -88,4 +79,4 @@ export function createSidebar(pages, currentPage) {
|
|||
});
|
||||
|
||||
return sidebar;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { ClientItem } from '../components/ClientItem.js';
|
||||
import { getClients } from '../services/clients.js';
|
||||
import { icons } from '../services/icons.js';
|
||||
|
||||
export function renderClientesPage() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'clientes-page';
|
||||
container.className = 'clientes-page page-enter';
|
||||
|
||||
// Estado de paginación
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
const pageSize = 20;
|
||||
|
|
@ -19,7 +19,10 @@ export function renderClientesPage() {
|
|||
</div>
|
||||
|
||||
<div class="clientes-filters">
|
||||
<input type="search" placeholder="Buscar clientes..." class="search-input" />
|
||||
<div class="search-wrapper">
|
||||
<span class="search-icon">${icons.search}</span>
|
||||
<input type="search" placeholder="Buscar clientes..." class="search-input search-input--with-icon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clients-table-container">
|
||||
|
|
@ -36,9 +39,7 @@ export function renderClientesPage() {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody class="clients-list">
|
||||
<tr>
|
||||
<td colspan="7" class="loading">Cargando clientes...</td>
|
||||
</tr>
|
||||
<tr><td colspan="7" class="loading">Cargando clientes...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -52,7 +53,23 @@ export function renderClientesPage() {
|
|||
</div>
|
||||
`;
|
||||
|
||||
// Función para aplicar filtros
|
||||
function displayValue(value) {
|
||||
if (value === null || value === undefined || value === '' || value === 'null') return 'N/A';
|
||||
return value;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (str == null) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = String(str);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function getClientStatusText(status) {
|
||||
const statusMap = { '0': 'Inactivo', '1': 'Activo' };
|
||||
return statusMap[status] || 'N/A';
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
if (!searchTerm) {
|
||||
filteredClients = [...allClients];
|
||||
|
|
@ -74,40 +91,31 @@ export function renderClientesPage() {
|
|||
renderPage(1);
|
||||
}
|
||||
|
||||
// Función para renderizar la página actual
|
||||
function renderPage(page = 1) {
|
||||
const clientsList = container.querySelector('.clients-list');
|
||||
|
||||
currentPage = page;
|
||||
totalPages = Math.ceil(filteredClients.length / pageSize);
|
||||
|
||||
// Calcular índices para la página
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = Math.min(startIndex + pageSize, filteredClients.length);
|
||||
const clients = filteredClients.slice(startIndex, endIndex);
|
||||
|
||||
console.log(`Página ${currentPage}/${totalPages}, Clientes en esta página: ${clients.length}, Total filtrado: ${filteredClients.length}`);
|
||||
|
||||
// Actualizar UI de paginación
|
||||
updatePaginationUI();
|
||||
|
||||
// Limpiar lista
|
||||
clientsList.innerHTML = '';
|
||||
|
||||
// Si no hay clientes
|
||||
if (clients.length === 0) {
|
||||
clientsList.innerHTML = '<tr><td colspan="7" class="no-clients">No hay clientes disponibles</td></tr>';
|
||||
clientsList.innerHTML = `<tr><td colspan="7"><div class="empty-state">${icons.emptyClients}<h3>No hay clientes</h3><p>Los clientes aparecerán aquí una vez añadidos.</p></div></td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Renderizar cada cliente
|
||||
clients.forEach(client => {
|
||||
const clientItem = ClientItem(client, handleViewClient);
|
||||
clientsList.appendChild(clientItem);
|
||||
});
|
||||
}
|
||||
|
||||
// Manejar ver cliente
|
||||
function handleViewClient(clientId) {
|
||||
const client = allClients.find(c => c.id === clientId);
|
||||
if (client) {
|
||||
|
|
@ -115,44 +123,22 @@ export function renderClientesPage() {
|
|||
}
|
||||
}
|
||||
|
||||
// Función helper para mostrar N/A si el valor es nulo/vacío/inválido
|
||||
function displayValue(value) {
|
||||
if (value === null || value === undefined || value === '' || value === 'null') return 'N/A';
|
||||
return value;
|
||||
}
|
||||
|
||||
// Obtener texto de estado del cliente
|
||||
function getClientStatusText(status) {
|
||||
const statusMap = {
|
||||
'0': 'Inactivo',
|
||||
'1': 'Activo'
|
||||
};
|
||||
return statusMap[status] || 'N/A';
|
||||
}
|
||||
|
||||
// Modal simple para ver detalles del cliente
|
||||
function showClientModal(client) {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'client-modal-overlay';
|
||||
|
||||
const iconMail = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>`;
|
||||
const iconPhone = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12 19.79 19.79 0 0 1 1.61 3.38 2 2 0 0 1 3.6 1.22h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.82a16 16 0 0 0 6.27 6.27l.96-.96a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>`;
|
||||
const iconMobile = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>`;
|
||||
|
||||
const contactsHtml = client.contacts && client.contacts.length > 0
|
||||
? client.contacts.map(contact => `
|
||||
<div class="contact-card">
|
||||
<div class="contact-header">
|
||||
<div class="contact-avatar">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
</div>
|
||||
<span class="contact-full-name">${displayValue(contact.firstname)} ${displayValue(contact.lastname)}</span>
|
||||
<div class="contact-avatar">${icons.user}</div>
|
||||
<span class="contact-full-name">${escapeHtml(displayValue(contact.firstname))} ${escapeHtml(displayValue(contact.lastname))}</span>
|
||||
</div>
|
||||
<div class="contact-details">
|
||||
${contact.email ? `<div class="contact-row"><span class="contact-icon">${iconMail}</span><span>${displayValue(contact.email)}</span></div>` : ''}
|
||||
${contact.phoneMobile ? `<div class="contact-row"><span class="contact-icon">${iconMobile}</span><span>${displayValue(contact.phoneMobile)}</span></div>` : ''}
|
||||
${contact.phonePro ? `<div class="contact-row"><span class="contact-icon">${iconPhone}</span><span>${displayValue(contact.phonePro)}</span></div>` : ''}
|
||||
${contact.phonePerso ? `<div class="contact-row"><span class="contact-icon">${iconPhone}</span><span>${displayValue(contact.phonePerso)}</span></div>` : ''}
|
||||
${contact.email ? `<div class="contact-row"><span class="contact-icon">${icons.mail}</span><span>${escapeHtml(displayValue(contact.email))}</span></div>` : ''}
|
||||
${contact.phoneMobile ? `<div class="contact-row"><span class="contact-icon">${icons.mobile}</span><span>${escapeHtml(displayValue(contact.phoneMobile))}</span></div>` : ''}
|
||||
${contact.phonePro ? `<div class="contact-row"><span class="contact-icon">${icons.phone}</span><span>${escapeHtml(displayValue(contact.phonePro))}</span></div>` : ''}
|
||||
${contact.phonePerso ? `<div class="contact-row"><span class="contact-icon">${icons.phone}</span><span>${escapeHtml(displayValue(contact.phonePerso))}</span></div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')
|
||||
|
|
@ -161,8 +147,8 @@ export function renderClientesPage() {
|
|||
modal.innerHTML = /*html*/`
|
||||
<div class="client-modal">
|
||||
<div class="client-modal-header">
|
||||
<h2>${displayValue(client.name)}</h2>
|
||||
<button class="btn-close-modal">✕</button>
|
||||
<h2>${escapeHtml(displayValue(client.name))}</h2>
|
||||
<button class="btn-close-modal">${icons.close}</button>
|
||||
</div>
|
||||
<div class="client-modal-body">
|
||||
<div class="client-info-section">
|
||||
|
|
@ -170,27 +156,27 @@ export function renderClientesPage() {
|
|||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-label">ID</span>
|
||||
<span class="info-value">${client.id}</span>
|
||||
<span class="info-value">${escapeHtml(client.id)}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Código</span>
|
||||
<span class="info-value">${displayValue(client.codeClient)}</span>
|
||||
<span class="info-value">${escapeHtml(displayValue(client.codeClient))}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Tipo</span>
|
||||
<span class="info-value">${displayValue(client.typentCode)}</span>
|
||||
<span class="info-value">${escapeHtml(displayValue(client.typentCode))}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Estado</span>
|
||||
<span class="info-value">${getClientStatusText(client.status)}</span>
|
||||
<span class="info-value">${escapeHtml(getClientStatusText(client.status))}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Email</span>
|
||||
<span class="info-value">${displayValue(client.email)}</span>
|
||||
<span class="info-value">${escapeHtml(displayValue(client.email))}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Teléfono</span>
|
||||
<span class="info-value">${displayValue(client.phone)}</span>
|
||||
<span class="info-value">${escapeHtml(displayValue(client.phone))}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -204,7 +190,6 @@ export function renderClientesPage() {
|
|||
</div>
|
||||
`;
|
||||
|
||||
// Cerrar modal
|
||||
const closeBtn = modal.querySelector('.btn-close-modal');
|
||||
closeBtn.addEventListener('click', () => modal.remove());
|
||||
modal.addEventListener('click', (e) => {
|
||||
|
|
@ -214,7 +199,6 @@ export function renderClientesPage() {
|
|||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// Función para cargar todos los clientes
|
||||
async function loadAllClients() {
|
||||
const clientsList = container.querySelector('.clients-list');
|
||||
|
||||
|
|
@ -223,22 +207,16 @@ export function renderClientesPage() {
|
|||
|
||||
const data = await getClients(1000, 1);
|
||||
|
||||
// Manejo de respuesta
|
||||
allClients = Array.isArray(data) ? data : data.data || data.clients || [];
|
||||
filteredClients = [...allClients];
|
||||
|
||||
console.log(`Total de clientes cargados: ${allClients.length}`);
|
||||
|
||||
// Renderizar la primera página
|
||||
renderPage(1);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error al cargar clientes:', error);
|
||||
clientsList.innerHTML = '<tr><td colspan="7" class="error">Error al cargar los clientes. Por favor, intenta de nuevo.</td></tr>';
|
||||
clientsList.innerHTML = `<tr><td colspan="7"><div class="empty-state">${icons.error}<h3>Error al cargar</h3><p>No se pudieron cargar los clientes. Inténtalo de nuevo.</p></div></td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Función para actualizar la UI de paginación
|
||||
function updatePaginationUI() {
|
||||
const paginationDiv = container.querySelector('.pagination');
|
||||
const currentPageSpan = container.querySelector('.current-page');
|
||||
|
|
@ -249,53 +227,38 @@ export function renderClientesPage() {
|
|||
currentPageSpan.textContent = currentPage;
|
||||
totalPagesSpan.textContent = totalPages;
|
||||
|
||||
// Mostrar u ocultar la paginación según el número de páginas
|
||||
if (totalPages <= 1) {
|
||||
paginationDiv.style.display = 'none';
|
||||
} else {
|
||||
paginationDiv.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Deshabilitar botones según corresponda
|
||||
btnPrev.disabled = currentPage === 1;
|
||||
btnNext.disabled = currentPage === totalPages;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
const searchInput = container.querySelector('.search-input');
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
clearTimeout(searchInput.debounceTimer);
|
||||
searchInput.debounceTimer = setTimeout(() => {
|
||||
clearTimeout(searchInput._debounceTimer);
|
||||
searchInput._debounceTimer = setTimeout(() => {
|
||||
searchTerm = e.target.value.trim();
|
||||
applyFilters();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Event listeners de paginación
|
||||
const btnPrev = container.querySelector('.btn-prev');
|
||||
const btnNext = container.querySelector('.btn-next');
|
||||
|
||||
btnPrev.addEventListener('click', () => {
|
||||
if (currentPage > 1) {
|
||||
renderPage(currentPage - 1);
|
||||
scrollToTop();
|
||||
}
|
||||
container.querySelector('.btn-prev').addEventListener('click', () => {
|
||||
if (currentPage > 1) { renderPage(currentPage - 1); scrollToTop(); }
|
||||
});
|
||||
|
||||
btnNext.addEventListener('click', () => {
|
||||
if (currentPage < totalPages) {
|
||||
renderPage(currentPage + 1);
|
||||
scrollToTop();
|
||||
}
|
||||
container.querySelector('.btn-next').addEventListener('click', () => {
|
||||
if (currentPage < totalPages) { renderPage(currentPage + 1); scrollToTop(); }
|
||||
});
|
||||
|
||||
// Función para scroll al inicio
|
||||
function scrollToTop() {
|
||||
container.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
// Cargar todos los clientes al montar el componente
|
||||
loadAllClients();
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import { FormChangeTracker } from '../components/ConfirmExitModal.js';
|
||||
import { navigationGuards } from '../router.js';
|
||||
import { apiGet, apiPost } from '../services/apiClient.js';
|
||||
import { icons } from '../services/icons.js';
|
||||
import { showToast } from '../services/toast.js';
|
||||
|
||||
export function renderCreateInvoicePage() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'create-invoice-page';
|
||||
container.className = 'create-invoice-page page-enter';
|
||||
|
||||
// Tracker de cambios
|
||||
const changeTracker = new FormChangeTracker();
|
||||
|
|
@ -206,13 +208,13 @@ export function renderCreateInvoicePage() {
|
|||
const clientIdValue = parseInt(container.querySelector('#clientId').value);
|
||||
|
||||
if (!clientIdValue || isNaN(clientIdValue) || clientIdValue < 1) {
|
||||
alert('Selecciona un cliente');
|
||||
showToast('Selecciona un cliente', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = container.querySelectorAll('.line-row-compact');
|
||||
if (lines.length === 0) {
|
||||
alert('Añade al menos una línea');
|
||||
showToast('Añade al menos una línea', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -235,19 +237,19 @@ export function renderCreateInvoicePage() {
|
|||
}
|
||||
|
||||
if (!isValidDate(dateValue)) {
|
||||
alert('La fecha de factura no es válida. Introduce una fecha entre los años 2000 y 2100.');
|
||||
showToast('La fecha de factura no es válida.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidDate(expireDateValue)) {
|
||||
alert('La fecha de vencimiento no es válida. Introduce una fecha entre los años 2000 y 2100.');
|
||||
showToast('La fecha de vencimiento no es válida.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const dateObj = new Date(dateValue);
|
||||
const expireDateObj = new Date(expireDateValue);
|
||||
if (expireDateObj < dateObj) {
|
||||
alert('La fecha de vencimiento no puede ser anterior a la fecha de factura.');
|
||||
showToast('La fecha de vencimiento no puede ser anterior a la de factura.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -281,7 +283,7 @@ export function renderCreateInvoicePage() {
|
|||
});
|
||||
|
||||
if (hasEmptyLine) {
|
||||
alert('Completa todos los campos de las líneas');
|
||||
showToast('Completa todos los campos de las líneas', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -298,12 +300,12 @@ export function renderCreateInvoicePage() {
|
|||
navigationGuards.unregister();
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
alert(`Factura creada (ID: ${invoiceId})`);
|
||||
showToast(`Factura creada (ID: ${invoiceId})`, 'success');
|
||||
window.location.hash = '#invoices';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert(error.message || 'Error al crear la factura');
|
||||
showToast(error.message || 'Error al crear la factura', 'error');
|
||||
|
||||
const submitBtn = container.querySelector('.btn-submit-compact');
|
||||
if (submitBtn) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { auth } from '../services/auth.js';
|
||||
import { InvoiceModal } from '../components/InvoiceModal.js';
|
||||
import { apiGet } from '../services/apiClient.js';
|
||||
import { icons } from '../services/icons.js';
|
||||
import {
|
||||
Chart,
|
||||
BarController,
|
||||
|
|
@ -13,6 +14,13 @@ import {
|
|||
|
||||
Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend);
|
||||
|
||||
function getGreeting() {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) return 'Buenos días';
|
||||
if (hour < 20) return 'Buenas tardes';
|
||||
return 'Buenas noches';
|
||||
}
|
||||
|
||||
async function fetchAllInvoices() {
|
||||
const data = await apiGet('/api/Invoices?limit=500&page=1');
|
||||
return Array.isArray(data) ? data : data.data || data.invoices || [];
|
||||
|
|
@ -75,6 +83,13 @@ function statusDotClass(status) {
|
|||
return map[status] || 'status-draft';
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (str == null) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = String(str);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function openInvoiceModal(invoiceId) {
|
||||
const modal = InvoiceModal(invoiceId, null, () => { });
|
||||
document.body.appendChild(modal);
|
||||
|
|
@ -91,24 +106,20 @@ export function renderDashboard() {
|
|||
container.innerHTML = /*html*/`
|
||||
<div class="db-header">
|
||||
<div>
|
||||
<h1 class="db-greeting">Buenos dias, ${userName}</h1>
|
||||
<p class="db-subtitle">Resumen de facturacion · ${year}</p>
|
||||
<h1 class="db-greeting">${getGreeting()}, ${escapeHtml(userName)}</h1>
|
||||
<p class="db-subtitle">Resumen de facturación · ${year}</p>
|
||||
</div>
|
||||
<button id="logout-button" class="logout-button">Cerrar sesion</button>
|
||||
<button id="logout-button" class="logout-button">
|
||||
${icons.logout}
|
||||
<span>Cerrar sesión</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="db-kpi-row">
|
||||
<div class="db-kpi-card">
|
||||
<div class="db-kpi-top">
|
||||
<span class="db-kpi-label">Facturas emitidas</span>
|
||||
<div class="db-kpi-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="db-kpi-icon db-kpi-icon--blue">${icons.fileText}</div>
|
||||
</div>
|
||||
<div class="db-kpi-value" id="kpi-total"><span class="db-kpi-skeleton"></span></div>
|
||||
<div class="db-kpi-sub" id="kpi-total-sub"><span class="db-kpi-skeleton db-kpi-skeleton-sm"></span></div>
|
||||
|
|
@ -117,12 +128,7 @@ export function renderDashboard() {
|
|||
<div class="db-kpi-card">
|
||||
<div class="db-kpi-top">
|
||||
<span class="db-kpi-label">Total facturado</span>
|
||||
<div class="db-kpi-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="1" x2="12" y2="23"/>
|
||||
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="db-kpi-icon db-kpi-icon--green">${icons.dollarSign}</div>
|
||||
</div>
|
||||
<div class="db-kpi-value" id="kpi-billed"><span class="db-kpi-skeleton"></span></div>
|
||||
<div class="db-kpi-sub" id="kpi-billed-sub"><span class="db-kpi-skeleton db-kpi-skeleton-sm"></span></div>
|
||||
|
|
@ -131,12 +137,7 @@ export function renderDashboard() {
|
|||
<div class="db-kpi-card">
|
||||
<div class="db-kpi-top">
|
||||
<span class="db-kpi-label">Tasa de cobro</span>
|
||||
<div class="db-kpi-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="db-kpi-icon db-kpi-icon--emerald">${icons.checkCircle}</div>
|
||||
</div>
|
||||
<div class="db-kpi-value" id="kpi-rate"><span class="db-kpi-skeleton"></span></div>
|
||||
<div class="db-kpi-sub" id="kpi-rate-sub"><span class="db-kpi-skeleton db-kpi-skeleton-sm"></span></div>
|
||||
|
|
@ -145,12 +146,7 @@ export function renderDashboard() {
|
|||
<div class="db-kpi-card">
|
||||
<div class="db-kpi-top">
|
||||
<span class="db-kpi-label">Pendiente de cobro</span>
|
||||
<div class="db-kpi-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="db-kpi-icon db-kpi-icon--amber">${icons.clock}</div>
|
||||
</div>
|
||||
<div class="db-kpi-value" id="kpi-pending"><span class="db-kpi-skeleton"></span></div>
|
||||
<div class="db-kpi-sub" id="kpi-pending-sub"><span class="db-kpi-skeleton db-kpi-skeleton-sm"></span></div>
|
||||
|
|
@ -160,7 +156,7 @@ export function renderDashboard() {
|
|||
<div class="db-mid-row">
|
||||
<div class="db-chart-card">
|
||||
<div class="db-card-header">
|
||||
<span class="db-card-title">Facturacion trimestral</span>
|
||||
<span class="db-card-title">Facturación trimestral</span>
|
||||
<span class="db-card-year">${year}</span>
|
||||
</div>
|
||||
<div class="db-chart-body">
|
||||
|
|
@ -186,14 +182,14 @@ export function renderDashboard() {
|
|||
|
||||
<div class="db-table-card">
|
||||
<div class="db-card-header">
|
||||
<span class="db-card-title">Ultimas facturas</span>
|
||||
<span class="db-card-title">Últimas facturas</span>
|
||||
<a href="#invoices" class="db-see-all">Ver todas</a>
|
||||
</div>
|
||||
<div class="db-table-wrap">
|
||||
<table class="db-monitoring-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Numero</th>
|
||||
<th>Número</th>
|
||||
<th>Cliente</th>
|
||||
<th>Estado</th>
|
||||
<th>Fecha</th>
|
||||
|
|
@ -229,7 +225,6 @@ export function renderDashboard() {
|
|||
|
||||
const { total, totalBilled, paid, unpaid, draft, paidRate, pendingAmount } = calcKPIs(invoices);
|
||||
|
||||
// KPIs
|
||||
container.querySelector('#kpi-total').textContent = total.toLocaleString('es-ES');
|
||||
container.querySelector('#kpi-total-sub').innerHTML =
|
||||
`<span class="db-sub-green">${paid} pagadas</span> · ` +
|
||||
|
|
@ -248,10 +243,13 @@ export function renderDashboard() {
|
|||
? `<span class="db-sub-red">${unpaid} factura${unpaid !== 1 ? 's' : ''} sin cobrar</span>`
|
||||
: '<span class="db-sub-green">Sin importes pendientes</span>';
|
||||
|
||||
// Chart.js — quarterly bar chart
|
||||
const canvas = container.querySelector('#db-quarterly-chart');
|
||||
const q = calcQuarterly(invoices);
|
||||
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const chartGridColor = isDark ? 'rgba(148, 163, 184, 0.08)' : '#f3f4f6';
|
||||
const chartTickColor = isDark ? '#94a3b8' : '#9ca3af';
|
||||
|
||||
new Chart(canvas, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
|
|
@ -261,7 +259,7 @@ export function renderDashboard() {
|
|||
data: q,
|
||||
backgroundColor: 'rgba(37, 99, 235, 0.85)',
|
||||
hoverBackgroundColor: 'rgba(29, 78, 216, 1)',
|
||||
borderRadius: 4,
|
||||
borderRadius: 6,
|
||||
borderSkipped: false,
|
||||
barPercentage: 0.5,
|
||||
}]
|
||||
|
|
@ -273,9 +271,9 @@ export function renderDashboard() {
|
|||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: { label: ctx => ` ${formatCurrency(ctx.parsed.y)}` },
|
||||
backgroundColor: '#111827',
|
||||
backgroundColor: isDark ? '#1e293b' : '#111827',
|
||||
titleColor: '#f9fafb',
|
||||
bodyColor: '#d1d5db',
|
||||
bodyColor: isDark ? '#cbd5e1' : '#d1d5db',
|
||||
padding: 10,
|
||||
cornerRadius: 6,
|
||||
displayColors: false,
|
||||
|
|
@ -286,52 +284,48 @@ export function renderDashboard() {
|
|||
grid: { display: false },
|
||||
border: { display: false },
|
||||
ticks: {
|
||||
color: '#6b7280',
|
||||
color: chartTickColor,
|
||||
font: { size: 12, family: 'Inter, system-ui, sans-serif' }
|
||||
}
|
||||
},
|
||||
y: {
|
||||
grid: { color: '#f3f4f6' },
|
||||
grid: { color: chartGridColor },
|
||||
border: { display: false },
|
||||
ticks: {
|
||||
color: '#9ca3af',
|
||||
color: chartTickColor,
|
||||
font: { size: 11, family: 'Inter, system-ui, sans-serif' },
|
||||
callback: v => v >= 1000 ? `${(v / 1000).toFixed(0)}k €` : `${v} €`,
|
||||
maxTicksLimit: 5,
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: { duration: 500, easing: 'easeOutQuart' }
|
||||
animation: { duration: 600, easing: 'easeOutQuart' }
|
||||
}
|
||||
});
|
||||
|
||||
// Recent activity list
|
||||
const recent = [...invoices]
|
||||
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
.slice(0, 8);
|
||||
|
||||
const recentList = container.querySelector('#db-recent-list');
|
||||
if (recent.length === 0) {
|
||||
recentList.innerHTML = '<p class="db-no-data">No hay facturas recientes.</p>';
|
||||
recentList.innerHTML = `<div class="db-no-data">${icons.emptyInboxes || ''}No hay facturas recientes.</div>`;
|
||||
} else {
|
||||
recentList.innerHTML = recent.map(inv => `
|
||||
<div class="db-recent-item" data-id="${inv.id}" role="button" tabindex="0">
|
||||
<div class="db-recent-dot ${statusDotClass(inv.status)}">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
</svg>
|
||||
${icons.fileText}
|
||||
</div>
|
||||
<div class="db-recent-info">
|
||||
<span class="db-recent-num">${inv.number || `#${inv.id}`}</span>
|
||||
<span class="db-recent-client">${inv.clientName || '—'}</span>
|
||||
<span class="db-recent-num">${escapeHtml(inv.number || `#${inv.id}`)}</span>
|
||||
<span class="db-recent-client">${escapeHtml(inv.clientName || '—')}</span>
|
||||
</div>
|
||||
<div class="db-recent-right">
|
||||
<span class="db-recent-amount">${formatCurrency(inv.total)}</span>
|
||||
<span class="db-recent-date">${formatDate(inv.date)}</span>
|
||||
</div>
|
||||
<div class="db-recent-arrow">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
${icons.chevron}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
|
@ -343,7 +337,6 @@ export function renderDashboard() {
|
|||
});
|
||||
}
|
||||
|
||||
// Monitoring table – last 15
|
||||
const tableRows = [...invoices]
|
||||
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
.slice(0, 15);
|
||||
|
|
@ -354,8 +347,8 @@ export function renderDashboard() {
|
|||
} else {
|
||||
tbody.innerHTML = tableRows.map(inv => `
|
||||
<tr class="db-table-row" data-id="${inv.id}" role="button" tabindex="0">
|
||||
<td class="db-cell-num">${inv.number || `#${inv.id}`}</td>
|
||||
<td class="db-cell-client">${inv.clientName || '—'}</td>
|
||||
<td class="db-cell-num">${escapeHtml(inv.number || `#${inv.id}`)}</td>
|
||||
<td class="db-cell-client">${escapeHtml(inv.clientName || '—')}</td>
|
||||
<td>${getStatusBadge(inv.status)}</td>
|
||||
<td>${formatDate(inv.date)}</td>
|
||||
<td>${formatDate(inv.expireDate)}</td>
|
||||
|
|
@ -373,4 +366,4 @@ export function renderDashboard() {
|
|||
})();
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { InvoiceItem } from '../components/InvoiceItem.js';
|
||||
import { InvoiceModal } from '../components/InvoiceModal.js';
|
||||
import { apiGet } from '../services/apiClient.js';
|
||||
import { icons } from '../services/icons.js';
|
||||
|
||||
export function renderFacturasPage() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'facturas-page';
|
||||
container.className = 'facturas-page page-enter';
|
||||
|
||||
// Estado de paginación
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
const pageSize = 20;
|
||||
|
|
@ -14,8 +14,6 @@ export function renderFacturasPage() {
|
|||
let filteredInvoices = [];
|
||||
let currentFilter = '';
|
||||
let searchTerm = '';
|
||||
|
||||
// Estado de ordenación
|
||||
let sortKey = null;
|
||||
let sortDir = 'asc';
|
||||
|
||||
|
|
@ -25,14 +23,17 @@ export function renderFacturasPage() {
|
|||
</div>
|
||||
|
||||
<div class="facturas-filters">
|
||||
<input type="search" placeholder="Buscar facturas..." class="search-input" />
|
||||
<div class="search-wrapper">
|
||||
<span class="search-icon">${icons.search}</span>
|
||||
<input type="search" placeholder="Buscar facturas..." class="search-input search-input--with-icon" />
|
||||
</div>
|
||||
<select class="filter-status">
|
||||
<option value="">Todos los estados</option>
|
||||
<option value="draft">Borrador</option>
|
||||
<option value="unpaid">Pte. Pago</option>
|
||||
<option value="paid">Pagada</option>
|
||||
</select>
|
||||
<button class="btn-new-invoice">+ Nueva Factura</button>
|
||||
<button class="btn-new-invoice">${icons.plus} <span>Nueva Factura</span></button>
|
||||
</div>
|
||||
|
||||
<div class="invoices-table-container">
|
||||
|
|
@ -61,9 +62,7 @@ export function renderFacturasPage() {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody class="invoices-list">
|
||||
<tr>
|
||||
<td colspan="7" class="loading">Cargando facturas...</td>
|
||||
</tr>
|
||||
<tr><td colspan="7" class="loading">Cargando facturas...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -100,7 +99,6 @@ export function renderFacturasPage() {
|
|||
const formatCurrency = (n) =>
|
||||
new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(n || 0);
|
||||
|
||||
// ── Sort helpers ────────────────────────────────────────────
|
||||
function sortInvoices(invoices) {
|
||||
if (!sortKey) return invoices;
|
||||
return [...invoices].sort((a, b) => {
|
||||
|
|
@ -135,13 +133,12 @@ export function renderFacturasPage() {
|
|||
});
|
||||
}
|
||||
|
||||
// ── Summary ─────────────────────────────────────────────────
|
||||
function updateSummary(invoices) {
|
||||
const summaryEl = container.querySelector('.invoices-summary');
|
||||
if (!invoices.length) { summaryEl.style.display = 'none'; return; }
|
||||
|
||||
const total = invoices.reduce((s, i) => s + (parseFloat(i.total) || 0), 0);
|
||||
const pending = invoices.reduce((s, i) => s + (parseFloat(i.remainToPay) || 0), 0);
|
||||
const total = invoices.reduce((s, i) => s + (parseFloat(i.total) || 0), 0);
|
||||
const pending = invoices.reduce((s, i) => s + (parseFloat(i.remainToPay) || 0), 0);
|
||||
const paid = total - pending;
|
||||
|
||||
container.querySelector('[data-summary="count"]').textContent = invoices.length;
|
||||
|
|
@ -151,16 +148,15 @@ export function renderFacturasPage() {
|
|||
summaryEl.style.display = 'flex';
|
||||
}
|
||||
|
||||
// ── Render current page ──────────────────────────────────────
|
||||
function renderPage(page = 1) {
|
||||
const invoicesList = container.querySelector('.invoices-list');
|
||||
|
||||
currentPage = page;
|
||||
const sorted = sortInvoices(filteredInvoices);
|
||||
totalPages = Math.ceil(sorted.length / pageSize);
|
||||
totalPages = Math.ceil(sorted.length / pageSize);
|
||||
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const invoices = sorted.slice(startIndex, startIndex + pageSize);
|
||||
const invoices = sorted.slice(startIndex, startIndex + pageSize);
|
||||
|
||||
updatePaginationUI();
|
||||
updateSortIcons();
|
||||
|
|
@ -169,7 +165,7 @@ export function renderFacturasPage() {
|
|||
invoicesList.innerHTML = '';
|
||||
|
||||
if (invoices.length === 0) {
|
||||
invoicesList.innerHTML = '<tr><td colspan="7" class="no-invoices">No hay facturas disponibles</td></tr>';
|
||||
invoicesList.innerHTML = `<tr><td colspan="7"><div class="empty-state">${icons.emptyInvoices}<h3>No hay facturas</h3><p>Crea tu primera factura para comenzar.</p></div></td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +175,6 @@ export function renderFacturasPage() {
|
|||
});
|
||||
}
|
||||
|
||||
// Manejar ver/editar factura
|
||||
function handleViewInvoice(invoiceId) {
|
||||
const modal = InvoiceModal(invoiceId, null, () => {
|
||||
loadAllInvoices();
|
||||
|
|
@ -187,7 +182,6 @@ export function renderFacturasPage() {
|
|||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// Función para cargar todas las facturas
|
||||
async function loadAllInvoices() {
|
||||
const invoicesList = container.querySelector('.invoices-list');
|
||||
|
||||
|
|
@ -206,7 +200,7 @@ export function renderFacturasPage() {
|
|||
renderPage(1);
|
||||
} catch (error) {
|
||||
console.error('Error al cargar facturas:', error);
|
||||
invoicesList.innerHTML = '<tr><td colspan="7" class="error">Error al cargar las facturas. Por favor, intenta de nuevo.</td></tr>';
|
||||
invoicesList.innerHTML = `<tr><td colspan="7"><div class="empty-state">${icons.error}<h3>Error al cargar</h3><p>No se pudieron cargar las facturas. Inténtalo de nuevo.</p></div></td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -214,23 +208,21 @@ export function renderFacturasPage() {
|
|||
loadAllInvoices();
|
||||
}
|
||||
|
||||
// Función para actualizar la UI de paginación
|
||||
function updatePaginationUI() {
|
||||
const paginationDiv = container.querySelector('.pagination');
|
||||
const paginationDiv = container.querySelector('.pagination');
|
||||
const currentPageSpan = container.querySelector('.current-page');
|
||||
const totalPagesSpan = container.querySelector('.total-pages');
|
||||
const btnPrev = container.querySelector('.btn-prev');
|
||||
const btnNext = container.querySelector('.btn-next');
|
||||
|
||||
currentPageSpan.textContent = currentPage;
|
||||
totalPagesSpan.textContent = totalPages;
|
||||
totalPagesSpan.textContent = totalPages;
|
||||
|
||||
paginationDiv.style.display = totalPages <= 1 ? 'none' : 'flex';
|
||||
btnPrev.disabled = currentPage === 1;
|
||||
btnNext.disabled = currentPage === totalPages;
|
||||
}
|
||||
|
||||
// ── Sort click listeners ─────────────────────────────────────
|
||||
container.querySelectorAll('th.sortable').forEach(th => {
|
||||
th.style.cursor = 'pointer';
|
||||
th.addEventListener('click', () => {
|
||||
|
|
@ -245,7 +237,6 @@ export function renderFacturasPage() {
|
|||
});
|
||||
});
|
||||
|
||||
// ── Other event listeners ────────────────────────────────────
|
||||
container.querySelector('.btn-new-invoice').addEventListener('click', () => {
|
||||
window.location.hash = '#create-invoice';
|
||||
});
|
||||
|
|
@ -253,8 +244,8 @@ export function renderFacturasPage() {
|
|||
const searchInput = container.querySelector('.search-input');
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const inputValue = e.target.value.trim();
|
||||
clearTimeout(searchInput.debounceTimer);
|
||||
searchInput.debounceTimer = setTimeout(() => {
|
||||
clearTimeout(searchInput._debounceTimer);
|
||||
searchInput._debounceTimer = setTimeout(() => {
|
||||
searchTerm = /\d/.test(inputValue) ? inputValue : '';
|
||||
applyFilters();
|
||||
}, 500);
|
||||
|
|
@ -280,4 +271,4 @@ export function renderFacturasPage() {
|
|||
loadAllInvoices();
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { isDarkTheme, toggleTheme } from '../services/theme.js';
|
||||
import { icons } from '../services/icons.js';
|
||||
|
||||
function decodeTokenPayload(token) {
|
||||
try {
|
||||
|
|
@ -50,10 +51,12 @@ export function renderSettingsPage() {
|
|||
const container = document.createElement('div');
|
||||
container.className = 'settings-page';
|
||||
|
||||
const dark = isDarkTheme();
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="settings-header">
|
||||
<h1>Configuracion</h1>
|
||||
<p>Preferencias visuales y estado de sesion.</p>
|
||||
<h1>Configuración</h1>
|
||||
<p>Preferencias visuales y estado de sesión.</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-grid">
|
||||
|
|
@ -64,19 +67,33 @@ export function renderSettingsPage() {
|
|||
<p class="settings-label">Modo oscuro</p>
|
||||
<p class="settings-help">Cambia entre tema claro y oscuro.</p>
|
||||
</div>
|
||||
<button type="button" class="settings-theme-toggle" id="settings-theme-toggle"></button>
|
||||
<button type="button" class="settings-theme-toggle ${dark ? 'settings-theme-toggle--active' : ''}" id="settings-theme-toggle" aria-pressed="${dark ? 'true' : 'false'}">
|
||||
<span class="toggle-track">
|
||||
<span class="toggle-icons">
|
||||
<span class="toggle-icon-sun">${icons.sun}</span>
|
||||
<span class="toggle-icon-moon">${icons.moon}</span>
|
||||
</span>
|
||||
<span class="toggle-thumb"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-card">
|
||||
<h2>Sesion</h2>
|
||||
<section class="settings-card settings-card--session">
|
||||
<h2>Sesión</h2>
|
||||
<div class="settings-session-list">
|
||||
<div class="settings-session-item">
|
||||
<span class="settings-label">Caducidad del token</span>
|
||||
<div class="settings-session-item-label">
|
||||
<span class="settings-session-icon">${icons.info}</span>
|
||||
<span>Caducidad del token</span>
|
||||
</div>
|
||||
<strong id="token-expire-at">-</strong>
|
||||
</div>
|
||||
<div class="settings-session-item">
|
||||
<span class="settings-label">Tiempo restante</span>
|
||||
<div class="settings-session-item-label">
|
||||
<span class="settings-session-icon">${icons.clock}</span>
|
||||
<span>Tiempo restante</span>
|
||||
</div>
|
||||
<strong id="token-remaining">-</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -89,9 +106,9 @@ export function renderSettingsPage() {
|
|||
const remainingEl = container.querySelector('#token-remaining');
|
||||
|
||||
const renderThemeButton = () => {
|
||||
const dark = isDarkTheme();
|
||||
themeBtn.textContent = dark ? 'Activado' : 'Desactivado';
|
||||
themeBtn.setAttribute('aria-pressed', dark ? 'true' : 'false');
|
||||
const isDark = isDarkTheme();
|
||||
themeBtn.classList.toggle('settings-theme-toggle--active', isDark);
|
||||
themeBtn.setAttribute('aria-pressed', isDark ? 'true' : 'false');
|
||||
};
|
||||
|
||||
const renderTokenInfo = () => {
|
||||
|
|
@ -118,4 +135,4 @@ export function renderSettingsPage() {
|
|||
container.cleanup = () => clearInterval(intervalId);
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,13 +3,13 @@ import { renderFacturasPage } from './Facturas.js';
|
|||
import { renderCreateInvoicePage } from './CreateInvoicePage.js';
|
||||
import { renderClientesPage } from './ClientesPage.js';
|
||||
import { renderSettingsPage } from './SettingsPage.js';
|
||||
import { icons } from '../services/icons.js';
|
||||
|
||||
// Registry of all pages available in the application
|
||||
export const pagesRegistry = [
|
||||
{
|
||||
route: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>',
|
||||
icon: icons.dashboard,
|
||||
voicePatterns: ['dashboard', 'inicio', 'ir al inicio', 'ir al dashboard', 'resumen', 'home', 'panel', 'principal'],
|
||||
requiresAuth: true,
|
||||
showInSidebar: true,
|
||||
|
|
@ -18,7 +18,7 @@ export const pagesRegistry = [
|
|||
{
|
||||
route: 'invoices',
|
||||
name: 'Facturas',
|
||||
icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>',
|
||||
icon: icons.invoices,
|
||||
voicePatterns: ['facturas', 'ver facturas', 'ir a facturas', 'lista facturas', 'mis facturas', 'listado facturas'],
|
||||
requiresAuth: true,
|
||||
showInSidebar: true,
|
||||
|
|
@ -27,16 +27,16 @@ export const pagesRegistry = [
|
|||
{
|
||||
route: 'create-invoice',
|
||||
name: 'Nueva Factura',
|
||||
icon: '+',
|
||||
icon: icons.plus,
|
||||
voicePatterns: ['nueva factura', 'crear factura', 'factura nueva', 'añadir factura'],
|
||||
requiresAuth: true,
|
||||
showInSidebar: false, // No mostrar en sidebar
|
||||
showInSidebar: false,
|
||||
render: renderCreateInvoicePage
|
||||
},
|
||||
{
|
||||
route: 'clients',
|
||||
name: 'Clientes',
|
||||
icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
|
||||
icon: icons.clients,
|
||||
voicePatterns: ['clientes', 'ver clientes', 'ir a clientes', 'lista clientes', 'mis clientes'],
|
||||
requiresAuth: true,
|
||||
showInSidebar: true,
|
||||
|
|
@ -45,25 +45,24 @@ export const pagesRegistry = [
|
|||
{
|
||||
route: 'settings',
|
||||
name: 'Configuración',
|
||||
icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>', voicePatterns: ['configuración', 'configuracion', 'ajustes', 'settings', 'preferencias'], requiresAuth: true,
|
||||
icon: icons.settings,
|
||||
voicePatterns: ['configuración', 'configuracion', 'ajustes', 'settings', 'preferencias'],
|
||||
requiresAuth: true,
|
||||
showInSidebar: true,
|
||||
render: renderSettingsPage
|
||||
}
|
||||
];
|
||||
|
||||
// Get pages that the user can access
|
||||
export function getAvailablePages(isAuthenticated) {
|
||||
if (!isAuthenticated) {
|
||||
return [];
|
||||
}
|
||||
// Filter pages based on authentication requirement and sidebar visibility
|
||||
return pagesRegistry.filter(page => {
|
||||
if (page.requiresAuth && !isAuthenticated) return false;
|
||||
return page.showInSidebar;
|
||||
});
|
||||
}
|
||||
|
||||
// Get page by route
|
||||
export function getPageByRoute(route) {
|
||||
return pagesRegistry.find(page => page.route === route);
|
||||
}
|
||||
}
|
||||
|
|
@ -118,8 +118,9 @@ export function initRouter() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Render page content using registry
|
||||
const pageContent = page.render();
|
||||
// Render page content using registry (supports async render)
|
||||
const maybePromise = page.render();
|
||||
const pageContent = maybePromise instanceof Promise ? await maybePromise : maybePromise;
|
||||
|
||||
// Mobile sidebar: backdrop + topbar
|
||||
const sidebarBackdrop = document.createElement('div');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
export const doliLogo = (size = 28) => `
|
||||
<svg width="${size}" height="${size}" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="doli-logo-grad" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#3b82f6"/>
|
||||
<stop offset="100%" stop-color="#1d4ed8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="32" height="32" rx="8" fill="url(#doli-logo-grad)"/>
|
||||
<path d="M10 7h7.5a3.5 3.5 0 0 1 2.47 6L14.5 16.5a3.5 3.5 0 0 1-2.47 6H10V7z" fill="#fff" opacity=".92"/>
|
||||
<path d="M10 17h5.3" stroke="#fff" stroke-width="1.2" stroke-linecap="round" opacity=".55"/>
|
||||
<path d="M10 21.5h5" stroke="#fff" stroke-width="1.2" stroke-linecap="round" opacity=".55"/>
|
||||
</svg>`;
|
||||
|
||||
export const doliLogoFull = (height = 24) => `
|
||||
<svg width="${height * 2.2}" height="${height}" viewBox="0 0 88 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="doli-full-grad" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#3b82f6"/>
|
||||
<stop offset="100%" stop-color="#1d4ed8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="32" height="32" rx="8" fill="url(#doli-full-grad)"/>
|
||||
<path d="M10 7h7.5a3.5 3.5 0 0 1 2.47 6L14.5 16.5a3.5 3.5 0 0 1-2.47 6H10V7z" fill="#fff" opacity=".92"/>
|
||||
<path d="M10 17h5.3" stroke="#fff" stroke-width="1.2" stroke-linecap="round" opacity=".55"/>
|
||||
<path d="M10 21.5h5" stroke="#fff" stroke-width="1.2" stroke-linecap="round" opacity=".55"/>
|
||||
<text x="42" y="23" font-family="Inter, system-ui, sans-serif" font-weight="700" font-size="16" fill="var(--text-primary, #0f172a)">Doli</text>
|
||||
</svg>`;
|
||||
|
||||
export const icons = {
|
||||
dashboard: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="4" rx="1.5"/><rect x="14" y="11" width="7" height="10" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/></svg>`,
|
||||
|
||||
invoices: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="M9 13h6"/><path d="M9 17h4"/></svg>`,
|
||||
|
||||
clients: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="7" r="4"/><path d="M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/><path d="M21 21v-2a4 4 0 0 0-3-3.87"/></svg>`,
|
||||
|
||||
settings: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`,
|
||||
|
||||
logout: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>`,
|
||||
|
||||
menu: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>`,
|
||||
|
||||
chevron: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>`,
|
||||
|
||||
search: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
|
||||
|
||||
plus: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`,
|
||||
|
||||
close: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
|
||||
|
||||
dollarSign: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>`,
|
||||
|
||||
checkCircle: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>`,
|
||||
|
||||
clock: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`,
|
||||
|
||||
fileText: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="M9 13h6"/><path d="M9 17h4"/></svg>`,
|
||||
|
||||
mail: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>`,
|
||||
|
||||
phone: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12 19.79 19.79 0 0 1 1.61 3.38 2 2 0 0 1 3.6 1.22h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.82a16 16 0 0 0 6.27 6.27l.96-.96a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>`,
|
||||
|
||||
mobile: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>`,
|
||||
|
||||
user: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`,
|
||||
|
||||
calendar: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>`,
|
||||
|
||||
eye: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`,
|
||||
|
||||
lock: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
|
||||
|
||||
sort: `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="8 4 8 16"/><path d="M4 12l8 8 8-8"/></svg>`,
|
||||
|
||||
emptyInvoices: `<svg width="64" height="64" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="16" y="4" width="48" height="72" rx="6" fill="var(--blue-50, #eff6ff)" stroke="var(--blue-500, #3b82f6)" stroke-width="2"/>
|
||||
<line x1="26" y1="22" x2="54" y2="22" stroke="var(--blue-200, #bfdbfe)" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<line x1="26" y1="32" x2="48" y2="32" stroke="var(--blue-200, #bfdbfe)" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<line x1="26" y1="42" x2="44" y2="42" stroke="var(--blue-200, #bfdbfe)" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<circle cx="58" cy="60" r="14" fill="var(--card-bg, #fff)" stroke="var(--blue-500, #3b82f6)" stroke-width="2"/>
|
||||
<path d="M54 60h8" stroke="var(--blue-500, #3b82f6)" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M58 56v8" stroke="var(--blue-500, #3b82f6)" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>`,
|
||||
|
||||
emptyClients: `<svg width="64" height="64" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="32" cy="24" r="12" fill="var(--blue-50, #eff6ff)" stroke="var(--blue-500, #3b82f6)" stroke-width="2"/>
|
||||
<path d="M12 68v-4a20 20 0 0 1 40 0v4" stroke="var(--blue-500, #3b82f6)" stroke-width="2" fill="var(--blue-50, #eff6ff)"/>
|
||||
<circle cx="56" cy="28" r="10" fill="var(--card-bg, #fff)" stroke="var(--blue-200, #bfdbfe)" stroke-width="2" stroke-dasharray="4 3"/>
|
||||
<path d="M44 68v-2a14 14 0 0 1 8-12.5" stroke="var(--blue-200, #bfdbfe)" stroke-width="2" fill="none" stroke-dasharray="4 3"/>
|
||||
</svg>`,
|
||||
|
||||
error: `<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--danger, #ef4444)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>`,
|
||||
|
||||
sun: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>`,
|
||||
|
||||
moon: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>`,
|
||||
|
||||
info: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`
|
||||
};
|
||||
|
|
@ -41,19 +41,23 @@
|
|||
--red-600: #dc2626;
|
||||
--red-700: #b91c1c;
|
||||
|
||||
/* Semantic tokens */
|
||||
--primary: var(--blue-600);
|
||||
--primary-hover: var(--blue-700);
|
||||
--primary-light: rgba(37, 99, 235, 0.06);
|
||||
--primary-subtle: rgba(37, 99, 235, 0.08);
|
||||
--success: var(--green-600);
|
||||
--success-hover: var(--green-700);
|
||||
--warning: var(--amber-600);
|
||||
--warning-hover: #a16207;
|
||||
--danger: var(--red-500);
|
||||
--danger-hover: var(--red-600);
|
||||
|
||||
--card-bg: #ffffff;
|
||||
--border-color: var(--gray-200);
|
||||
--border-subtle: var(--gray-100);
|
||||
--text-primary: var(--gray-900);
|
||||
--text-secondary: var(--gray-500);
|
||||
--text-tertiary: var(--gray-400);
|
||||
--bg-page: var(--gray-50);
|
||||
--bg-subtle: var(--gray-100);
|
||||
|
||||
|
|
@ -92,6 +96,19 @@
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* ── Large / Presentation Displays ── */
|
||||
@media (min-width: 1600px) {
|
||||
:root {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2200px) {
|
||||
:root {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
--gray-50: #0f172a;
|
||||
--gray-100: #111827;
|
||||
|
|
@ -108,8 +125,10 @@
|
|||
--border-color: #1f2a3d;
|
||||
--text-primary: #f8fafc;
|
||||
--text-secondary: #94a3b8;
|
||||
--text-tertiary: #64748b;
|
||||
--bg-page: #070d18;
|
||||
--bg-subtle: #0f172a;
|
||||
--border-subtle: #1f2a3d;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -255,3 +255,96 @@ button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
|||
transition: background 0.15s ease;
|
||||
}
|
||||
.empty-state .btn-empty-action:hover { background: var(--primary-hover); }
|
||||
|
||||
/* ── Page entrance animation ── */
|
||||
.page-enter {
|
||||
animation: pageEnter 0.3s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
@keyframes pageEnter {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ── Stagger animation for KPI cards ── */
|
||||
.db-kpi-card { animation: fadeInUp 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; }
|
||||
.db-kpi-card:nth-child(1) { animation-delay: 0ms; }
|
||||
.db-kpi-card:nth-child(2) { animation-delay: 80ms; }
|
||||
.db-kpi-card:nth-child(3) { animation-delay: 160ms; }
|
||||
.db-kpi-card:nth-child(4) { animation-delay: 240ms; }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ── Hover lift for cards ── */
|
||||
.db-kpi-card,
|
||||
.settings-card,
|
||||
.db-chart-card,
|
||||
.db-recent-card,
|
||||
.db-table-card {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.db-kpi-card:hover,
|
||||
.db-chart-card:hover,
|
||||
.db-recent-card:hover,
|
||||
.db-table-card:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ── Status badge enhanced ── */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 99px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.badge::before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge-draft {
|
||||
background: var(--gray-100);
|
||||
color: var(--gray-600);
|
||||
}
|
||||
.badge-draft::before { background: var(--gray-400); }
|
||||
|
||||
.badge-unpaid {
|
||||
background: var(--amber-50);
|
||||
color: var(--amber-600);
|
||||
}
|
||||
.badge-unpaid::before { background: var(--amber-600); }
|
||||
|
||||
.badge-paid {
|
||||
background: var(--green-50);
|
||||
color: var(--green-600);
|
||||
}
|
||||
.badge-paid::before { background: var(--green-600); }
|
||||
|
||||
:root[data-theme='dark'] .badge-draft {
|
||||
background: rgba(148, 163, 184, 0.14);
|
||||
color: #cbd5e1;
|
||||
}
|
||||
:root[data-theme='dark'] .badge-draft::before { background: #64748b; }
|
||||
:root[data-theme='dark'] .badge-unpaid {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #fcd34d;
|
||||
}
|
||||
:root[data-theme='dark'] .badge-unpaid::before { background: #fcd34d; }
|
||||
:root[data-theme='dark'] .badge-paid {
|
||||
background: rgba(34, 197, 94, 0.18);
|
||||
color: #86efac;
|
||||
}
|
||||
:root[data-theme='dark'] .badge-paid::before { background: #86efac; }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,15 @@
|
|||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
.clientes-page { max-width: 1600px; }
|
||||
.clientes-header h1 { font-size: 1.5rem; }
|
||||
}
|
||||
|
||||
@media (min-width: 2200px) {
|
||||
.clientes-page { max-width: 2000px; }
|
||||
}
|
||||
|
||||
.clientes-header {
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
|
@ -29,6 +38,11 @@
|
|||
max-width: 400px;
|
||||
}
|
||||
|
||||
.clientes-filters .search-wrapper {
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.client-avatar-col {
|
||||
width: 48px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,20 @@
|
|||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
.create-invoice-page { max-width: 1100px; }
|
||||
.invoice-header-compact h1 { font-size: 1.5rem; }
|
||||
.form-field-compact input,
|
||||
.form-field-compact select,
|
||||
.form-field-compact textarea { height: 42px; font-size: 15px; }
|
||||
.btn-submit-compact,
|
||||
.btn-cancel-compact { height: 44px; font-size: 15px; }
|
||||
.line-desc-compact,
|
||||
.line-qty-compact,
|
||||
.line-price-compact,
|
||||
.line-tax-compact { height: 38px; font-size: 14px; }
|
||||
}
|
||||
|
||||
.invoice-header-compact {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@
|
|||
}
|
||||
|
||||
.db-kpi-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--gray-100);
|
||||
display: flex;
|
||||
|
|
@ -66,6 +66,26 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.db-kpi-icon--blue {
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
color: var(--blue-600);
|
||||
}
|
||||
|
||||
.db-kpi-icon--green {
|
||||
background: rgba(22, 163, 74, 0.1);
|
||||
color: var(--green-600);
|
||||
}
|
||||
|
||||
.db-kpi-icon--emerald {
|
||||
background: rgba(5, 150, 105, 0.1);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.db-kpi-icon--amber {
|
||||
background: rgba(217, 119, 6, 0.1);
|
||||
color: var(--amber-600);
|
||||
}
|
||||
|
||||
.db-kpi-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
|
|
@ -413,12 +433,69 @@
|
|||
.db-greeting { font-size: 1.1rem; }
|
||||
}
|
||||
|
||||
/* ── Large / Presentation Displays ── */
|
||||
@media (min-width: 1600px) {
|
||||
.db-kpi-row {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--space-6);
|
||||
}
|
||||
.db-kpi-value { font-size: 2rem; }
|
||||
.db-kpi-label { font-size: 12px; }
|
||||
.db-greeting { font-size: 1.75rem; }
|
||||
.db-subtitle { font-size: 15px; }
|
||||
.db-card-title { font-size: 15px; }
|
||||
.db-chart-body { height: 340px; }
|
||||
.db-recent-list { max-height: 400px; }
|
||||
.dashboard-subtitle { font-size: 15px; }
|
||||
}
|
||||
|
||||
@media (min-width: 2200px) {
|
||||
.db-kpi-value { font-size: 2.5rem; }
|
||||
.db-greeting { font-size: 2rem; }
|
||||
.db-kpi-card { padding: var(--space-6); }
|
||||
.db-card-header { padding: var(--space-5) var(--space-6); }
|
||||
.db-chart-body { height: 420px; }
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
margin: var(--space-1) 0 0 0;
|
||||
}
|
||||
|
||||
/* Logout button with icon */
|
||||
.logout-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
background-color: transparent;
|
||||
color: var(--text-secondary);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
transition: color var(--transition-fast), background-color var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
color: var(--danger);
|
||||
border-color: var(--danger);
|
||||
background-color: var(--red-50);
|
||||
}
|
||||
|
||||
.logout-button:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.logout-button svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Dark mode UX tune */
|
||||
:root[data-theme='dark'] .db-kpi-card,
|
||||
:root[data-theme='dark'] .db-chart-card,
|
||||
|
|
@ -458,3 +535,33 @@
|
|||
background: rgba(148, 163, 184, 0.18);
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .db-kpi-icon--blue {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .db-kpi-icon--green {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .db-kpi-icon--emerald {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .db-kpi-icon--amber {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #fcd34d;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .logout-button {
|
||||
background-color: transparent;
|
||||
border-color: rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .logout-button:hover {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
.facturas-header h1 { font-size: 1.25rem; font-weight: 600; }
|
||||
|
||||
.btn-new-invoice {
|
||||
background-color: var(--primary);
|
||||
background: linear-gradient(135deg, var(--blue-600), var(--blue-700));
|
||||
color: white;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border: none;
|
||||
|
|
@ -20,11 +20,25 @@
|
|||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
transition: background-color var(--transition-fast);
|
||||
font-family: inherit;
|
||||
transition: all 0.2s ease;
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.btn-new-invoice:hover { background-color: var(--primary-hover); }
|
||||
.btn-new-invoice:hover {
|
||||
background: linear-gradient(135deg, var(--blue-700), #1e3a8a);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
.btn-new-invoice:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.facturas-filters {
|
||||
display: flex;
|
||||
|
|
@ -56,6 +70,30 @@
|
|||
.search-input { flex: 1; max-width: 320px; }
|
||||
.filter-status { min-width: 180px; cursor: pointer; }
|
||||
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
max-width: 340px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
color: var(--text-tertiary, var(--gray-400));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-input--with-icon {
|
||||
padding-left: 34px !important;
|
||||
max-width: 100%;
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.invoices-table-container,
|
||||
.clients-table-container {
|
||||
background: var(--card-bg);
|
||||
|
|
@ -403,3 +441,17 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
.facturas-page { max-width: 1600px; }
|
||||
.invoices-table th,
|
||||
.invoices-table td { font-size: 14px; padding: var(--space-3) var(--space-5); }
|
||||
.facturas-header h1 { font-size: 1.5rem; }
|
||||
.filter-status { min-width: 220px; }
|
||||
}
|
||||
|
||||
@media (min-width: 2200px) {
|
||||
.facturas-page { max-width: 2000px; }
|
||||
.invoices-table th,
|
||||
.invoices-table td { padding: var(--space-4) var(--space-6); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,221 +1,355 @@
|
|||
/* ============================================
|
||||
Login + Session Notices
|
||||
============================================ */
|
||||
Login + Session Notices
|
||||
============================================ */
|
||||
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
background: linear-gradient(145deg, var(--bg-page) 0%, var(--blue-50) 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
background: linear-gradient(135deg, var(--bg-page) 0%, var(--blue-50) 50%, var(--bg-page) 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(37, 99, 235, 0.08) 0%, transparent 70%);
|
||||
top: -150px;
|
||||
right: -100px;
|
||||
pointer-events: none;
|
||||
animation: loginPulse 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.login-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(37, 99, 235, 0.05) 0%, transparent 70%);
|
||||
bottom: -120px;
|
||||
left: -80px;
|
||||
pointer-events: none;
|
||||
animation: loginPulse 10s ease-in-out infinite reverse;
|
||||
}
|
||||
|
||||
@keyframes loginPulse {
|
||||
0%, 100% { transform: scale(1); opacity: 0.7; }
|
||||
50% { transform: scale(1.1); opacity: 1; }
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: var(--card-bg);
|
||||
padding: var(--space-8);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-md);
|
||||
width: 100%;
|
||||
max-width: 390px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--card-bg);
|
||||
padding: var(--space-8);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 10px 30px -5px rgba(0, 0, 0, 0.08);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid var(--border-color);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
animation: loginCardIn 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
@keyframes loginCardIn {
|
||||
from { opacity: 0; transform: translateY(16px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.login-logo-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--primary);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: logoFloat 3s ease-in-out infinite;
|
||||
filter: drop-shadow(0 4px 12px rgba(37, 99, 235, 0.25));
|
||||
}
|
||||
|
||||
@keyframes logoFloat {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-3px); }
|
||||
}
|
||||
|
||||
.login-brand-name {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--space-1) 0;
|
||||
font-size: 1.125rem;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--space-1) 0;
|
||||
font-size: 1.25rem;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
margin: 0 0 var(--space-5) 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
margin: 0 0 var(--space-5) 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: var(--space-4);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-1);
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-1);
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--card-bg);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
transition: border-color var(--transition-fast);
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--card-bg);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.12);
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.form-group input:focus:not(:focus-visible) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-fast);
|
||||
height: 36px;
|
||||
margin-top: var(--space-2);
|
||||
width: 100%;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: linear-gradient(135deg, var(--blue-600), var(--blue-700));
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
height: 42px;
|
||||
margin-top: var(--space-2);
|
||||
letter-spacing: 0.01em;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-button::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.1), transparent);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
background-color: var(--primary-hover);
|
||||
border-color: transparent;
|
||||
background: linear-gradient(135deg, var(--blue-700), #1e3a8a);
|
||||
border-color: transparent;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
.login-button:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
.login-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
color: var(--danger);
|
||||
text-align: center;
|
||||
margin-top: var(--space-3);
|
||||
font-size: 13px;
|
||||
color: var(--danger);
|
||||
text-align: center;
|
||||
margin-top: var(--space-3);
|
||||
font-size: 13px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.session-warning-overlay,
|
||||
.connection-lost-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-4);
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.session-warning-overlay {
|
||||
background: rgba(17, 24, 39, 0.45);
|
||||
z-index: 3000;
|
||||
background: rgba(17, 24, 39, 0.45);
|
||||
z-index: 3000;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.connection-lost-overlay {
|
||||
background: rgba(17, 24, 39, 0.55);
|
||||
z-index: 4000;
|
||||
background: rgba(17, 24, 39, 0.55);
|
||||
z-index: 4000;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.session-warning-modal,
|
||||
.connection-lost-modal {
|
||||
width: 100%;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: var(--space-6);
|
||||
width: 100%;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.session-warning-modal { max-width: 430px; }
|
||||
.connection-lost-modal { max-width: 460px; }
|
||||
|
||||
.session-warning-modal h3 {
|
||||
margin: 0 0 var(--space-3) 0;
|
||||
font-size: 1.15rem;
|
||||
margin: 0 0 var(--space-3) 0;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.connection-lost-modal h3 {
|
||||
margin: 0 0 var(--space-2) 0;
|
||||
color: var(--danger);
|
||||
margin: 0 0 var(--space-2) 0;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.session-warning-modal p,
|
||||
.connection-lost-modal p {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.session-warning-actions {
|
||||
margin-top: var(--space-5);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-5);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.session-warning-logout {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.session-warning-logout:hover {
|
||||
background: var(--gray-100);
|
||||
background: var(--gray-100);
|
||||
}
|
||||
|
||||
.session-warning-continue {
|
||||
background: var(--primary);
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.connection-lost-sub {
|
||||
margin-top: var(--space-3) !important;
|
||||
font-size: 0.92rem;
|
||||
margin-top: var(--space-3) !important;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.connection-lost-login-btn {
|
||||
margin-top: var(--space-5);
|
||||
width: 100%;
|
||||
margin-top: var(--space-5);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.login-card {
|
||||
margin: var(--space-4);
|
||||
padding: var(--space-6);
|
||||
}
|
||||
.login-card {
|
||||
margin: var(--space-4);
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.session-warning-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
.session-warning-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.session-warning-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
.session-warning-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
.login-card {
|
||||
max-width: 440px;
|
||||
padding: var(--space-10);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.login-logo-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
height: 46px;
|
||||
font-size: 15px;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
|
||||
.login-button {
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .login-container {
|
||||
background: linear-gradient(145deg, var(--bg-page) 0%, rgba(30, 58, 110, 0.2) 100%);
|
||||
background: linear-gradient(135deg, var(--bg-page) 0%, rgba(30, 58, 110, 0.15) 50%, var(--bg-page) 100%);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .login-container::before {
|
||||
background: radial-gradient(circle, rgba(59, 130, 246, 0.06) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .login-container::after {
|
||||
background: radial-gradient(circle, rgba(59, 130, 246, 0.04) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .login-card {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 10px 30px -5px rgba(0, 0, 0, 0.2);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .login-logo-icon {
|
||||
filter: drop-shadow(0 4px 12px rgba(37, 99, 235, 0.2));
|
||||
}
|
||||
|
|
@ -8,18 +8,29 @@
|
|||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
.settings-page { max-width: 780px; }
|
||||
.settings-card { padding: var(--space-6); }
|
||||
.settings-header h1 { font-size: 1.5rem; }
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.settings-header h1 {
|
||||
margin: 0 0 var(--space-1) 0;
|
||||
}
|
||||
|
||||
.settings-header p {
|
||||
margin-top: var(--space-2);
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
|
|
@ -32,8 +43,9 @@
|
|||
}
|
||||
|
||||
.settings-card h2 {
|
||||
margin-bottom: var(--space-4);
|
||||
margin: 0 0 var(--space-4) 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-row {
|
||||
|
|
@ -56,14 +68,90 @@
|
|||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ── Toggle Switch ── */
|
||||
.settings-theme-toggle {
|
||||
min-width: 116px;
|
||||
position: relative;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.settings-theme-toggle:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.toggle-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 56px;
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
background: var(--gray-300);
|
||||
position: relative;
|
||||
transition: background-color 0.25s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-theme-toggle--active .toggle-track {
|
||||
background: var(--blue-600);
|
||||
}
|
||||
|
||||
.toggle-thumb {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.settings-theme-toggle--active .toggle-thumb {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.toggle-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 7px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toggle-icon-sun,
|
||||
.toggle-icon-moon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-icon-sun { opacity: 1; }
|
||||
.toggle-icon-moon { opacity: 0.6; }
|
||||
|
||||
.settings-theme-toggle--active .toggle-icon-sun { opacity: 0.6; }
|
||||
.settings-theme-toggle--active .toggle-icon-moon { opacity: 1; }
|
||||
|
||||
/* ── Session Section ── */
|
||||
.settings-card--session {
|
||||
background: var(--card-bg);
|
||||
}
|
||||
|
||||
.settings-session-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.settings-session-item {
|
||||
|
|
@ -71,7 +159,7 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding-bottom: var(--space-3);
|
||||
padding: var(--space-3) 0;
|
||||
border-bottom: 1px dashed var(--border-color);
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +168,47 @@
|
|||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.settings-token-expired {
|
||||
color: var(--danger);
|
||||
.settings-session-item-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.settings-session-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-tertiary, var(--text-secondary));
|
||||
}
|
||||
|
||||
.settings-token-expired {
|
||||
color: var(--danger) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Dark mode ── */
|
||||
:root[data-theme='dark'] .toggle-track {
|
||||
background: var(--gray-700);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .settings-theme-toggle--active .toggle-track {
|
||||
background: var(--blue-600);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .toggle-thumb {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
@media (max-width: 580px) {
|
||||
.settings-page {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.settings-session-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
}
|
||||
|
|
@ -38,15 +38,13 @@
|
|||
}
|
||||
|
||||
.sidebar-logo-icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background: var(--primary);
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
filter: drop-shadow(0 2px 4px rgba(37, 99, 235, 0.2));
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
|
|
@ -270,6 +268,25 @@
|
|||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
.sidebar { width: 250px; }
|
||||
.main-content > div {
|
||||
max-width: 1440px;
|
||||
padding: var(--space-8);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2200px) {
|
||||
.sidebar { width: 280px; }
|
||||
.sidebar-link { font-size: 15px; padding: var(--space-3) var(--space-4); }
|
||||
.sidebar-avatar { width: 32px; height: 32px; font-size: 13px; }
|
||||
.sidebar-username { font-size: 13px; }
|
||||
.main-content > div {
|
||||
max-width: 1800px;
|
||||
padding: var(--space-10);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Mobile topbar (hidden on desktop) ── */
|
||||
.mobile-topbar {
|
||||
display: none;
|
||||
|
|
|
|||
Loading…
Reference in New Issue