Mejora simple de UI/UX
This commit is contained in:
parent
b67d3a8454
commit
605e73e304
|
|
@ -41,7 +41,9 @@ export function InvoiceItem(invoice, onView) {
|
|||
};
|
||||
|
||||
item.innerHTML = /*html*/`
|
||||
<td class="invoice-number">${invoice.number}</td>
|
||||
<td class="invoice-number">
|
||||
<button class="btn-invoice-num" title="Ver detalles">${invoice.number}</button>
|
||||
</td>
|
||||
<td class="invoice-status">
|
||||
<span class="status-badge ${getStatusClass(invoice.status)}">
|
||||
${getStatusText(invoice.status)}
|
||||
|
|
@ -61,13 +63,12 @@ export function InvoiceItem(invoice, onView) {
|
|||
</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', () => {
|
||||
if (onView) {
|
||||
onView(invoice.id);
|
||||
}
|
||||
});
|
||||
viewBtn.addEventListener('click', () => onView?.(invoice.id));
|
||||
|
||||
return item;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { getInvoiceById, updateInvoice, validateInvoice, addInvoiceLine, deleteInvoiceLine, addPayment, getPayments, downloadInvoicePdf } from '../services/invoices.js';
|
||||
import { showConfirmExitModal, FormChangeTracker } from './ConfirmExitModal.js';
|
||||
import { toast } from '../services/toast.js';
|
||||
|
||||
export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
||||
const modal = document.createElement('div');
|
||||
|
|
@ -475,12 +476,12 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
savedSuccessfully = true;
|
||||
changeTracker.markAsSaved();
|
||||
|
||||
alert('Factura actualizada correctamente');
|
||||
toast.success('Factura actualizada correctamente');
|
||||
if (onUpdate) onUpdate();
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.error('Error al guardar:', error);
|
||||
alert('Error al guardar la factura: ' + error.message);
|
||||
toast.error('Error al guardar la factura: ' + error.message);
|
||||
} finally {
|
||||
const saveBtn = modal.querySelector('.btn-save');
|
||||
if (saveBtn) {
|
||||
|
|
@ -492,9 +493,13 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
|
||||
// Manejar validación
|
||||
const handleValidate = async () => {
|
||||
if (!confirm('¿Estás seguro de que quieres validar esta factura? No podrás editarla completamente después.')) {
|
||||
return;
|
||||
}
|
||||
const ok = await showConfirmExitModal({
|
||||
title: 'Validar factura',
|
||||
message: '¿Seguro que quieres validar esta factura? No podrás editarla completamente después.',
|
||||
confirmText: 'Validar',
|
||||
cancelText: 'Cancelar'
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
try {
|
||||
const validateBtn = modal.querySelector('.btn-validate');
|
||||
|
|
@ -507,7 +512,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
savedSuccessfully = true;
|
||||
changeTracker.markAsSaved();
|
||||
|
||||
alert('Factura validada correctamente');
|
||||
toast.success('Factura validada correctamente');
|
||||
if (onUpdate) onUpdate();
|
||||
|
||||
// Recargar la factura para mostrar el nuevo estado
|
||||
|
|
@ -517,7 +522,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
savedSuccessfully = false;
|
||||
} catch (error) {
|
||||
console.error('Error al validar:', error);
|
||||
alert('Error al validar la factura: ' + error.message);
|
||||
toast.error('Error al validar la factura: ' + error.message);
|
||||
|
||||
const validateBtn = modal.querySelector('.btn-validate');
|
||||
if (validateBtn) {
|
||||
|
|
@ -530,7 +535,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
// Manejar descarga del PDF
|
||||
const handleDownloadPdf = async () => {
|
||||
if (!invoice?.number) {
|
||||
alert('No se puede descargar la factura porque no tiene número.');
|
||||
toast.warning('No se puede descargar la factura porque no tiene número.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -554,7 +559,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
URL.revokeObjectURL(blobUrl);
|
||||
} catch (error) {
|
||||
console.error('Error al descargar factura:', error);
|
||||
alert('Error al descargar la factura: ' + error.message);
|
||||
toast.error('Error al descargar la factura: ' + error.message);
|
||||
} finally {
|
||||
if (downloadBtn) {
|
||||
downloadBtn.disabled = false;
|
||||
|
|
@ -566,7 +571,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
// Manejar vista previa del PDF
|
||||
const handlePreviewPdf = async () => {
|
||||
if (!invoice?.number) {
|
||||
alert('No se puede previsualizar la factura porque no tiene número.');
|
||||
toast.warning('No se puede previsualizar la factura porque no tiene número.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -575,7 +580,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
const previewWindow = window.open('', '_blank');
|
||||
|
||||
if (!previewWindow) {
|
||||
alert('El navegador bloqueó la ventana emergente. Permite popups para ver la vista previa.');
|
||||
toast.warning('El navegador bloqueó la ventana emergente. Permite popups para ver la vista previa.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -600,7 +605,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
} catch (error) {
|
||||
console.error('Error al previsualizar factura:', error);
|
||||
previewWindow.close();
|
||||
alert('Error al abrir la vista previa: ' + error.message);
|
||||
toast.error('Error al abrir la vista previa: ' + error.message);
|
||||
} finally {
|
||||
if (previewBtn) {
|
||||
previewBtn.disabled = false;
|
||||
|
|
@ -634,22 +639,22 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
const taxRate = parseFloat(modal.querySelector('#line-tax').value);
|
||||
|
||||
if (!description) {
|
||||
alert('La descripción es obligatoria');
|
||||
toast.warning('La descripción es obligatoria');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!quantity || quantity <= 0) {
|
||||
alert('La cantidad debe ser mayor que 0');
|
||||
toast.warning('La cantidad debe ser mayor que 0');
|
||||
return;
|
||||
}
|
||||
|
||||
if (unitPrice < 0) {
|
||||
alert('El precio no puede ser negativo');
|
||||
toast.warning('El precio no puede ser negativo');
|
||||
return;
|
||||
}
|
||||
|
||||
if (taxRate < 0 || taxRate > 100) {
|
||||
alert('El IVA debe estar entre 0 y 100');
|
||||
toast.warning('El IVA debe estar entre 0 y 100');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -665,7 +670,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
taxRate
|
||||
});
|
||||
|
||||
alert('Línea añadida correctamente');
|
||||
toast.success('Línea añadida correctamente');
|
||||
toggleAddLineForm();
|
||||
|
||||
// Recargar y recapturar estado inicial
|
||||
|
|
@ -673,7 +678,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
changeTracker.markAsSaved();
|
||||
} catch (error) {
|
||||
console.error('Error al añadir línea:', error);
|
||||
alert('Error al añadir línea: ' + error.message);
|
||||
toast.error('Error al añadir línea: ' + error.message);
|
||||
} finally {
|
||||
const saveBtn = modal.querySelector('.btn-save-line');
|
||||
if (saveBtn) {
|
||||
|
|
@ -685,9 +690,13 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
|
||||
// Eliminar línea de factura
|
||||
const handleDeleteLine = async (lineId) => {
|
||||
if (!confirm('¿Estás seguro de que quieres eliminar esta línea?')) {
|
||||
return;
|
||||
}
|
||||
const ok = await showConfirmExitModal({
|
||||
title: 'Eliminar línea',
|
||||
message: '¿Seguro que quieres eliminar esta línea de la factura?',
|
||||
confirmText: 'Eliminar',
|
||||
cancelText: 'Cancelar'
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
try {
|
||||
// Deshabilitar el botón mientras se elimina
|
||||
|
|
@ -704,7 +713,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
changeTracker.markAsSaved();
|
||||
} catch (error) {
|
||||
console.error('Error al eliminar línea:', error);
|
||||
alert('Error al eliminar línea: ' + error.message);
|
||||
toast.error('Error al eliminar línea: ' + error.message);
|
||||
|
||||
const btn = modal.querySelector(`.btn-delete-line[data-line-id="${lineId}"]`);
|
||||
if (btn) {
|
||||
|
|
@ -729,7 +738,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
let amount = null;
|
||||
if (!isNaN(rawAmount) && rawAmount > 0) {
|
||||
if (rawAmount > remainToPay) {
|
||||
alert(`La cantidad no puede superar el pendiente de pago (${remainToPay.toFixed(2)} €)`);
|
||||
toast.warning(`La cantidad no puede superar el pendiente de pago (${remainToPay.toFixed(2)} €)`);
|
||||
return;
|
||||
}
|
||||
amount = rawAmount;
|
||||
|
|
@ -737,7 +746,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
|
||||
const paymentDate = dateInput.value;
|
||||
if (!paymentDate) {
|
||||
alert('La fecha de pago es obligatoria');
|
||||
toast.warning('La fecha de pago es obligatoria');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -761,14 +770,14 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
changeTracker.markAsSaved();
|
||||
|
||||
const displayAmount = amount ? `${amount.toFixed(2)} €` : `${remainToPay.toFixed(2)} € (total)`;
|
||||
alert(`Pago de ${displayAmount} registrado correctamente`);
|
||||
toast.success(`Pago de ${displayAmount} registrado correctamente`);
|
||||
|
||||
if (onUpdate) onUpdate();
|
||||
await loadInvoice();
|
||||
savedSuccessfully = false;
|
||||
} catch (error) {
|
||||
console.error('Error al registrar pago:', error);
|
||||
alert('Error al registrar el pago: ' + error.message);
|
||||
toast.error('Error al registrar el pago: ' + error.message);
|
||||
} finally {
|
||||
const payBtn = modal.querySelector('#btn-pay');
|
||||
if (payBtn) {
|
||||
|
|
@ -806,6 +815,17 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
</div>
|
||||
`;
|
||||
|
||||
// Escape key closes modal
|
||||
const handleEscapeKey = (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (!modal.isConnected) {
|
||||
document.removeEventListener('keydown', handleEscapeKey);
|
||||
return;
|
||||
}
|
||||
handleClose();
|
||||
};
|
||||
document.addEventListener('keydown', handleEscapeKey);
|
||||
|
||||
// Cargar factura
|
||||
loadInvoice();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { auth } from '../services/auth.js';
|
||||
|
||||
export function createSidebar(pages, currentPage) {
|
||||
const sidebar = document.createElement('aside');
|
||||
sidebar.className = 'sidebar';
|
||||
|
|
@ -48,6 +50,34 @@ 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();
|
||||
|
||||
const sidebarFooter = document.createElement('div');
|
||||
sidebarFooter.className = 'sidebar-footer';
|
||||
sidebarFooter.innerHTML = /*html*/`
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-avatar">${initials}</div>
|
||||
<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>
|
||||
</button>
|
||||
`;
|
||||
|
||||
sidebarFooter.querySelector('.sidebar-logout-btn').addEventListener('click', () => {
|
||||
auth.logout();
|
||||
window.location.hash = '#login';
|
||||
});
|
||||
|
||||
sidebar.appendChild(sidebarFooter);
|
||||
|
||||
const toggleBtn = sidebarHeader.querySelector('#sidebar-toggle');
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
if (window.matchMedia('(max-width: 768px)').matches) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import './styles/clients.css'
|
|||
import './styles/create-invoice.css'
|
||||
import './styles/settings.css'
|
||||
import './styles/voice-assistant.css'
|
||||
import './styles/toast.css'
|
||||
import { initRouter } from './router.js'
|
||||
import { initSessionManager } from './services/session.js'
|
||||
import { initTheme } from './services/theme.js'
|
||||
|
|
|
|||
|
|
@ -10,10 +10,14 @@ export function renderFacturasPage() {
|
|||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
const pageSize = 20;
|
||||
let allInvoices = []; // Almacenar todas las facturas
|
||||
let filteredInvoices = []; // Facturas filtradas
|
||||
let currentFilter = ''; // Filtro actual de estado
|
||||
let searchTerm = ''; // Término de búsqueda actual
|
||||
let allInvoices = [];
|
||||
let filteredInvoices = [];
|
||||
let currentFilter = '';
|
||||
let searchTerm = '';
|
||||
|
||||
// Estado de ordenación
|
||||
let sortKey = null;
|
||||
let sortDir = 'asc';
|
||||
|
||||
container.innerHTML = /*html*/`
|
||||
<div class="facturas-header">
|
||||
|
|
@ -35,12 +39,24 @@ export function renderFacturasPage() {
|
|||
<table class="invoices-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="invoice-number">Número</th>
|
||||
<th class="invoice-status">Estado</th>
|
||||
<th class="invoice-client">Cliente</th>
|
||||
<th class="invoice-date">Fecha</th>
|
||||
<th class="invoice-total">Total</th>
|
||||
<th class="invoice-remain">Pendiente</th>
|
||||
<th class="invoice-number sortable" data-sort="number">
|
||||
Número <span class="sort-icon"></span>
|
||||
</th>
|
||||
<th class="invoice-status sortable" data-sort="status">
|
||||
Estado <span class="sort-icon"></span>
|
||||
</th>
|
||||
<th class="invoice-client sortable" data-sort="clientName">
|
||||
Cliente <span class="sort-icon"></span>
|
||||
</th>
|
||||
<th class="invoice-date sortable" data-sort="date">
|
||||
Fecha <span class="sort-icon"></span>
|
||||
</th>
|
||||
<th class="invoice-total sortable" data-sort="total">
|
||||
Total <span class="sort-icon"></span>
|
||||
</th>
|
||||
<th class="invoice-remain sortable" data-sort="remainToPay">
|
||||
Pendiente <span class="sort-icon"></span>
|
||||
</th>
|
||||
<th class="invoice-actions">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -52,6 +68,26 @@ export function renderFacturasPage() {
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<div class="invoices-summary" style="display:none">
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Facturas</span>
|
||||
<span class="summary-value" data-summary="count">—</span>
|
||||
</div>
|
||||
<div class="summary-divider"></div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">Total facturado</span>
|
||||
<span class="summary-value" data-summary="total">—</span>
|
||||
</div>
|
||||
<div class="summary-item summary-item--paid">
|
||||
<span class="summary-label">Pagado</span>
|
||||
<span class="summary-value" data-summary="paid">—</span>
|
||||
</div>
|
||||
<div class="summary-item summary-item--pending">
|
||||
<span class="summary-label">Pendiente</span>
|
||||
<span class="summary-value" data-summary="pending">—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<button class="btn-pagination btn-prev">← Anterior</button>
|
||||
<div class="pagination-info">
|
||||
|
|
@ -61,39 +97,82 @@ export function renderFacturasPage() {
|
|||
</div>
|
||||
`;
|
||||
|
||||
// Función para aplicar filtros combinados
|
||||
function applyFilters() {
|
||||
// Ya no filtra localmente, hace llamada a la API
|
||||
loadAllInvoices();
|
||||
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) => {
|
||||
let va = a[sortKey], vb = b[sortKey];
|
||||
if (sortKey === 'total' || sortKey === 'remainToPay') {
|
||||
va = parseFloat(va) || 0;
|
||||
vb = parseFloat(vb) || 0;
|
||||
} else if (sortKey === 'date') {
|
||||
va = new Date(va).getTime() || 0;
|
||||
vb = new Date(vb).getTime() || 0;
|
||||
} else {
|
||||
va = String(va ?? '').toLowerCase();
|
||||
vb = String(vb ?? '').toLowerCase();
|
||||
}
|
||||
if (va < vb) return sortDir === 'asc' ? -1 : 1;
|
||||
if (va > vb) return sortDir === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Función para renderizar la página actual
|
||||
function updateSortIcons() {
|
||||
container.querySelectorAll('th.sortable').forEach(th => {
|
||||
const key = th.dataset.sort;
|
||||
const icon = th.querySelector('.sort-icon');
|
||||
if (key === sortKey) {
|
||||
icon.textContent = sortDir === 'asc' ? ' ↑' : ' ↓';
|
||||
th.classList.add('sort-active');
|
||||
} else {
|
||||
icon.textContent = '';
|
||||
th.classList.remove('sort-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── 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 paid = total - pending;
|
||||
|
||||
container.querySelector('[data-summary="count"]').textContent = invoices.length;
|
||||
container.querySelector('[data-summary="total"]').textContent = formatCurrency(total);
|
||||
container.querySelector('[data-summary="paid"]').textContent = formatCurrency(paid);
|
||||
container.querySelector('[data-summary="pending"]').textContent = formatCurrency(pending);
|
||||
summaryEl.style.display = 'flex';
|
||||
}
|
||||
|
||||
// ── Render current page ──────────────────────────────────────
|
||||
function renderPage(page = 1) {
|
||||
const invoicesList = container.querySelector('.invoices-list');
|
||||
|
||||
currentPage = page;
|
||||
totalPages = Math.ceil(filteredInvoices.length / pageSize);
|
||||
const sorted = sortInvoices(filteredInvoices);
|
||||
totalPages = Math.ceil(sorted.length / pageSize);
|
||||
|
||||
// Calcular índices para la página
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = Math.min(startIndex + pageSize, filteredInvoices.length);
|
||||
const invoices = filteredInvoices.slice(startIndex, endIndex);
|
||||
const invoices = sorted.slice(startIndex, startIndex + pageSize);
|
||||
|
||||
console.log(`Página ${currentPage}/${totalPages}, Facturas en esta página: ${invoices.length}, Total filtrado: ${filteredInvoices.length}`);
|
||||
|
||||
// Actualizar UI de paginación
|
||||
updatePaginationUI();
|
||||
updateSortIcons();
|
||||
updateSummary(filteredInvoices);
|
||||
|
||||
// Limpiar lista
|
||||
invoicesList.innerHTML = '';
|
||||
|
||||
// Si no hay facturas
|
||||
if (invoices.length === 0) {
|
||||
invoicesList.innerHTML = '<tr><td colspan="7" class="no-invoices">No hay facturas disponibles</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Renderizar cada factura
|
||||
invoices.forEach(invoice => {
|
||||
const invoiceItem = InvoiceItem(invoice, handleViewInvoice);
|
||||
invoicesList.appendChild(invoiceItem);
|
||||
|
|
@ -103,7 +182,6 @@ export function renderFacturasPage() {
|
|||
// Manejar ver/editar factura
|
||||
function handleViewInvoice(invoiceId) {
|
||||
const modal = InvoiceModal(invoiceId, null, () => {
|
||||
// Callback cuando se actualiza la factura
|
||||
loadAllInvoices();
|
||||
});
|
||||
document.body.appendChild(modal);
|
||||
|
|
@ -116,34 +194,26 @@ export function renderFacturasPage() {
|
|||
try {
|
||||
invoicesList.innerHTML = '<tr><td colspan="7" class="loading">Cargando facturas...</td></tr>';
|
||||
|
||||
// Construir URL con parámetros
|
||||
let url = '/api/Invoices?limit=1000';
|
||||
|
||||
if (currentFilter) {
|
||||
url += `&status=${currentFilter}`;
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
url += `&search=${encodeURIComponent(searchTerm)}`;
|
||||
}
|
||||
if (currentFilter) url += `&status=${currentFilter}`;
|
||||
if (searchTerm) url += `&search=${encodeURIComponent(searchTerm)}`;
|
||||
|
||||
const data = await apiGet(url);
|
||||
|
||||
// Manejo de respuesta - obtener todas las facturas
|
||||
allInvoices = Array.isArray(data) ? data : data.data || data.invoices || [];
|
||||
filteredInvoices = [...allInvoices].reverse();
|
||||
|
||||
console.log(`Total de facturas cargadas: ${allInvoices.length}`);
|
||||
|
||||
// Renderizar la primera página
|
||||
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>';
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
loadAllInvoices();
|
||||
}
|
||||
|
||||
// Función para actualizar la UI de paginación
|
||||
function updatePaginationUI() {
|
||||
const paginationDiv = container.querySelector('.pagination');
|
||||
|
|
@ -155,72 +225,58 @@ export function renderFacturasPage() {
|
|||
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
|
||||
paginationDiv.style.display = totalPages <= 1 ? 'none' : 'flex';
|
||||
btnPrev.disabled = currentPage === 1;
|
||||
btnNext.disabled = currentPage === totalPages;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
const newInvoiceBtn = container.querySelector('.btn-new-invoice');
|
||||
newInvoiceBtn.addEventListener('click', () => {
|
||||
// ── Sort click listeners ─────────────────────────────────────
|
||||
container.querySelectorAll('th.sortable').forEach(th => {
|
||||
th.style.cursor = 'pointer';
|
||||
th.addEventListener('click', () => {
|
||||
const key = th.dataset.sort;
|
||||
if (sortKey === key) {
|
||||
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortKey = key;
|
||||
sortDir = 'asc';
|
||||
}
|
||||
renderPage(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Other event listeners ────────────────────────────────────
|
||||
container.querySelector('.btn-new-invoice').addEventListener('click', () => {
|
||||
window.location.hash = '#create-invoice';
|
||||
});
|
||||
|
||||
const searchInput = container.querySelector('.search-input');
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const inputValue = e.target.value.trim();
|
||||
// Debounce para no hacer muchas llamadas
|
||||
clearTimeout(searchInput.debounceTimer);
|
||||
searchInput.debounceTimer = setTimeout(() => {
|
||||
// Solo aplicar búsqueda si contiene al menos un número
|
||||
const hasNumber = /\d/.test(inputValue);
|
||||
if (hasNumber) {
|
||||
searchTerm = inputValue;
|
||||
} else {
|
||||
searchTerm = ''; // Si es muy corto, mostrar todas
|
||||
}
|
||||
searchTerm = /\d/.test(inputValue) ? inputValue : '';
|
||||
applyFilters();
|
||||
}, 500); // Espera 500ms después de que el usuario deja de escribir
|
||||
}, 500);
|
||||
});
|
||||
|
||||
const filterStatus = container.querySelector('.filter-status');
|
||||
filterStatus.addEventListener('change', (e) => {
|
||||
container.querySelector('.filter-status').addEventListener('change', (e) => {
|
||||
currentFilter = e.target.value;
|
||||
console.log(`Filtro aplicado: ${currentFilter || 'Todos'}`);
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
// 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 todas las facturas al montar el componente
|
||||
loadAllInvoices();
|
||||
|
||||
return container;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
let container = null;
|
||||
|
||||
function getContainer() {
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
const ICONS = {
|
||||
success: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
|
||||
error: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>`,
|
||||
warning: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
|
||||
info: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>`,
|
||||
};
|
||||
|
||||
export function showToast(message, type = 'info', duration = 3500) {
|
||||
const c = getContainer();
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast--${type}`;
|
||||
toast.innerHTML = `
|
||||
<span class="toast-icon">${ICONS[type] ?? ICONS.info}</span>
|
||||
<span class="toast-message">${message}</span>
|
||||
<button class="toast-close" aria-label="Cerrar">
|
||||
<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>
|
||||
</button>
|
||||
`;
|
||||
|
||||
c.appendChild(toast);
|
||||
requestAnimationFrame(() => toast.classList.add('toast--visible'));
|
||||
|
||||
const remove = () => {
|
||||
if (!toast.isConnected) return;
|
||||
toast.classList.remove('toast--visible');
|
||||
toast.addEventListener('transitionend', () => toast.remove(), { once: true });
|
||||
};
|
||||
|
||||
const timer = setTimeout(remove, duration);
|
||||
toast.querySelector('.toast-close').addEventListener('click', () => {
|
||||
clearTimeout(timer);
|
||||
remove();
|
||||
});
|
||||
|
||||
return remove;
|
||||
}
|
||||
|
||||
export const toast = {
|
||||
success: (msg, d) => showToast(msg, 'success', d),
|
||||
error: (msg, d) => showToast(msg, 'error', d),
|
||||
warning: (msg, d) => showToast(msg, 'warning', d),
|
||||
info: (msg, d) => showToast(msg, 'info', d),
|
||||
};
|
||||
|
|
@ -196,7 +196,7 @@
|
|||
grid-template-columns: 4fr 0.8fr 1.2fr 0.8fr 1.2fr auto;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
background: var(--gray-50);
|
||||
background: var(--bg-subtle);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
|
|
@ -210,7 +210,7 @@
|
|||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13px;
|
||||
background: white;
|
||||
background: var(--card-bg);
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
transition: border-color var(--transition-fast);
|
||||
|
|
@ -337,3 +337,37 @@
|
|||
padding: var(--space-3);
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .invoice-form-compact {
|
||||
background: #081226;
|
||||
border-color: #1e2f4d;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .line-row-compact {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border-color: #1e2f4d;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .line-desc-compact,
|
||||
:root[data-theme='dark'] .line-qty-compact,
|
||||
:root[data-theme='dark'] .line-price-compact,
|
||||
:root[data-theme='dark'] .line-tax-compact {
|
||||
background: #0b1220;
|
||||
border-color: #1e2f4d;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .btn-add-compact {
|
||||
background: #0b1220;
|
||||
border-color: #1e2f4d;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .btn-add-compact:hover {
|
||||
border-color: #2d4a7a;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .btn-submit-compact:disabled {
|
||||
background: #1e2f4d;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,101 @@
|
|||
.invoices-table td,
|
||||
.clients-table td { padding: var(--space-3) var(--space-4); font-size: 14px; }
|
||||
|
||||
/* Sortable column headers */
|
||||
.invoices-table th.sortable {
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.invoices-table th.sortable:hover {
|
||||
color: var(--text-primary);
|
||||
background-color: var(--gray-100);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .invoices-table th.sortable:hover {
|
||||
background-color: rgba(30, 58, 110, 0.3);
|
||||
}
|
||||
|
||||
.invoices-table th.sort-active {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.sort-icon {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Totals summary bar */
|
||||
.invoices-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-5);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
margin-top: var(--space-3);
|
||||
box-shadow: var(--shadow-xs);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.summary-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.summary-item--paid .summary-value { color: var(--success); }
|
||||
.summary-item--pending .summary-value { color: var(--primary); }
|
||||
|
||||
:root[data-theme='dark'] .invoices-summary {
|
||||
background: #081226;
|
||||
border-color: #1e2f4d;
|
||||
}
|
||||
|
||||
/* Clickable invoice number */
|
||||
.btn-invoice-num {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--primary);
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration-color: transparent;
|
||||
transition: text-decoration-color var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn-invoice-num:hover {
|
||||
text-decoration-color: var(--primary);
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.invoice-number { font-weight: 500; color: var(--text-primary); }
|
||||
|
||||
.invoice-client {
|
||||
|
|
|
|||
|
|
@ -804,6 +804,103 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Dark Mode Overrides
|
||||
============================================ */
|
||||
|
||||
:root[data-theme='dark'] .modal-content {
|
||||
background: #081226;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .modal-header,
|
||||
:root[data-theme='dark'] .modal-footer,
|
||||
:root[data-theme='dark'] .total-row.pending,
|
||||
:root[data-theme='dark'] .payments-history-section,
|
||||
:root[data-theme='dark'] .payment-section {
|
||||
border-color: #1e2f4d;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .form-group input,
|
||||
:root[data-theme='dark'] .form-group textarea,
|
||||
:root[data-theme='dark'] .form-group select {
|
||||
background: #0b1220;
|
||||
border-color: #1e2f4d;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .form-group input:disabled,
|
||||
:root[data-theme='dark'] .form-group textarea:disabled,
|
||||
:root[data-theme='dark'] .form-group input[readonly] {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .add-line-form {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border-color: #1e2f4d;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .lines-table thead {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .lines-table th,
|
||||
:root[data-theme='dark'] .lines-table td {
|
||||
border-color: #1e2f4d;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .lines-table tbody tr:hover {
|
||||
background: rgba(30, 58, 110, 0.2);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .invoice-totals {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .payments-history-table {
|
||||
border-color: #1e2f4d;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .payments-history-table thead th {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border-color: #1e2f4d;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .payments-history-table td,
|
||||
:root[data-theme='dark'] .payments-history-table th {
|
||||
border-color: #1e2f4d;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .payments-history-table tbody tr:hover {
|
||||
background: rgba(30, 58, 110, 0.2);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .payment-form input {
|
||||
background: #0b1220;
|
||||
border-color: #1e2f4d;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .payment-type-pill {
|
||||
background: rgba(30, 58, 110, 0.4);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .btn-delete:hover {
|
||||
background-color: rgba(220, 38, 38, 0.15);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .btn-cancel:hover {
|
||||
background-color: rgba(30, 58, 110, 0.2);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .confirm-exit-modal {
|
||||
background: #081226;
|
||||
border: 1px solid #1e2f4d;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .confirm-exit-icon {
|
||||
background: rgba(217, 119, 6, 0.15);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.blocked-nav-toast {
|
||||
left: 1rem;
|
||||
|
|
|
|||
|
|
@ -171,11 +171,97 @@
|
|||
padding: var(--space-2);
|
||||
}
|
||||
|
||||
/* ── Sidebar footer (user + logout) ── */
|
||||
.sidebar-footer {
|
||||
padding: var(--space-3) var(--space-3);
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.sidebar-username {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: opacity var(--transition-fast), width var(--transition-fast);
|
||||
}
|
||||
|
||||
.sidebar-logout-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
padding: var(--space-1);
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
transition: color var(--transition-fast), background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.sidebar-logout-btn:hover {
|
||||
color: var(--danger);
|
||||
background: var(--red-50);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-username {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-logout-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-footer {
|
||||
justify-content: center;
|
||||
padding: var(--space-3) var(--space-2);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background-color: var(--bg-page);
|
||||
min-width: 0;
|
||||
animation: pageIn 0.18s ease;
|
||||
}
|
||||
|
||||
@keyframes pageIn {
|
||||
from { opacity: 0; transform: translateY(5px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.main-content > div {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
/* ============================================
|
||||
Toast Notifications
|
||||
============================================ */
|
||||
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
width: max-content;
|
||||
max-width: min(420px, calc(100vw - 32px));
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
pointer-events: auto;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid transparent;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: opacity 0.22s ease, transform 0.22s ease;
|
||||
line-height: 1.4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.toast--visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toast--success {
|
||||
background: #f0fdf4;
|
||||
border-color: #bbf7d0;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.toast--error {
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.toast--warning {
|
||||
background: #fffbeb;
|
||||
border-color: #fde68a;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.toast--info {
|
||||
background: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
opacity: 0.55;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
transition: opacity 0.15s, background-color 0.15s;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
opacity: 1;
|
||||
background: rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
:root[data-theme='dark'] .toast--success {
|
||||
background: rgba(22, 163, 74, 0.15);
|
||||
border-color: rgba(22, 163, 74, 0.3);
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .toast--error {
|
||||
background: rgba(220, 38, 38, 0.15);
|
||||
border-color: rgba(220, 38, 38, 0.3);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .toast--warning {
|
||||
background: rgba(217, 119, 6, 0.15);
|
||||
border-color: rgba(217, 119, 6, 0.3);
|
||||
color: #fcd34d;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .toast--info {
|
||||
background: rgba(37, 99, 235, 0.15);
|
||||
border-color: rgba(37, 99, 235, 0.3);
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.toast-container {
|
||||
bottom: 16px;
|
||||
width: calc(100vw - 32px);
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue