diff --git a/src/components/InvoiceItem.js b/src/components/InvoiceItem.js index d481d8a..f67e020 100644 --- a/src/components/InvoiceItem.js +++ b/src/components/InvoiceItem.js @@ -18,22 +18,24 @@ export function InvoiceItem(invoice, onView) { const getStatusClass = (status) => { const statusMap = { - 'draft': 'badge-draft', - 'validated': 'badge-unpaid', - 'paid': 'badge-paid', - 'unpaid': 'badge-unpaid', - 'canceled': 'badge-draft' + 'draft': 'status-draft', + 'validated': 'status-validated', + 'paid': 'status-paid', + 'unpaid': 'status-unpaid', + 'canceled': 'status-canceled', + 'pending': 'status-pending', }; - return statusMap[status] || 'badge-draft'; + return statusMap[status] || 'status-draft'; }; const getStatusText = (status) => { const statusTextMap = { - 'draft': 'Borrador', + 'draft': 'Borrador', 'validated': 'Pte. Pago', - 'paid': 'Pagada', - 'unpaid': 'Pte. Pago', - 'canceled': 'Cancelada' + 'paid': 'Pagada', + 'unpaid': 'Pte. Pago', + 'canceled': 'Cancelada', + 'pending': 'Pendiente', }; return statusTextMap[status] || status; }; @@ -46,11 +48,14 @@ export function InvoiceItem(invoice, onView) { } item.innerHTML = /*html*/` + + + - + ${escapeHtml(getStatusText(invoice.status))} @@ -59,8 +64,8 @@ export function InvoiceItem(invoice, onView) { ${escapeHtml(invoice.clientName || 'Sin nombre')} ${formatDate(invoice.date)} - ${formatCurrency(invoice.total)} - ${formatCurrency(invoice.remainToPay)} + ${formatCurrency(invoice.total)} + ${formatCurrency(invoice.remainToPay)} + + + `; + + row.querySelector('.btn-save-line-edit').addEventListener('click', () => saveLineEdit(lineId, row)); + row.querySelector('.btn-cancel-line-edit').addEventListener('click', () => loadInvoice()); + row.querySelector('.line-edit-desc').focus(); + }; + + const saveLineEdit = async (lineId, row) => { + const description = row.querySelector('.line-edit-desc').value.trim(); + const quantity = parseFloat(row.querySelector('.line-edit-qty').value); + const unitPrice = parseFloat(row.querySelector('.line-edit-price').value); + const taxRate = parseFloat(row.querySelector('.line-edit-tax').value); + + if (!description) { toast.warning('La descripción es obligatoria'); return; } + if (!quantity || quantity <= 0) { toast.warning('La cantidad debe ser mayor que 0'); return; } + + const saveBtn = row.querySelector('.btn-save-line-edit'); + saveBtn.disabled = true; + saveBtn.textContent = '…'; + + try { + await updateInvoiceLine(invoiceId, lineId, { description, quantity, unitPrice, taxRate }); + toast.success('Línea actualizada'); + if (onUpdate) onUpdate(); + await loadInvoice(); + changeTracker.markAsSaved(); + } catch (error) { + toast.error('Error al actualizar línea: ' + error.message); + saveBtn.disabled = false; + saveBtn.textContent = '✓'; + } + }; + // Eliminar línea de factura const handleDeleteLine = async (lineId) => { const ok = await showConfirmExitModal({ @@ -751,6 +827,8 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { } const paymentRef = refInput?.value?.trim() || undefined; + const paymentModeId = parseInt(modal.querySelector('#payment-method')?.value) || 4; + const accountId = parseInt(modal.querySelector('#payment-account')?.value) || 1; try { const payBtn = modal.querySelector('#btn-pay'); @@ -760,9 +838,9 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { await addPayment(invoiceId, { amount: amount, paymentDate: paymentDate, - paymentModeId: 4, + paymentModeId, closePaidInvoices: "yes", - accountId: 1, + accountId, numPayment: paymentRef }); @@ -787,18 +865,24 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) { } }; - // Cargar factura + // Cargar factura + datos auxiliares en paralelo const loadInvoice = async () => { try { isLoading = true; renderContent(); - invoice = await getInvoiceById(invoiceId); - try { - payments = await getPayments(invoiceId); - } catch (e) { - payments = []; - } + const [invoiceData, paymentsData, typesData, accountsData] = await Promise.allSettled([ + getInvoiceById(invoiceId), + getPayments(invoiceId), + getPaymentTypes(), + apiGet('/api/Bank/accounts'), + ]); + + invoice = invoiceData.status === 'fulfilled' ? invoiceData.value : null; + payments = paymentsData.status === 'fulfilled' ? paymentsData.value : []; + paymentTypes = typesData.status === 'fulfilled' ? typesData.value : []; + bankAccounts = accountsData.status === 'fulfilled' ? accountsData.value : []; + isLoading = false; renderContent(); } catch (error) { diff --git a/src/main.js b/src/main.js index a818d32..a211338 100755 --- a/src/main.js +++ b/src/main.js @@ -8,6 +8,7 @@ import './styles/create-invoice.css' import './styles/settings.css' import './styles/voice-assistant.css' import './styles/toast.css' +import './styles/banco.css' import { initRouter } from './router.js' import { initSessionManager } from './services/session.js' import { initTheme } from './services/theme.js' diff --git a/src/pages/BancoPage.js b/src/pages/BancoPage.js new file mode 100644 index 0000000..1300e2a --- /dev/null +++ b/src/pages/BancoPage.js @@ -0,0 +1,629 @@ +import { apiGet, apiPost, apiPut } from '../services/apiClient.js'; +import { getCountries } from '../services/setup.js'; +import { showToast } from '../services/toast.js'; + +const API_ACCOUNTS = '/api/Bank/accounts'; +const API_MOVEMENTS = '/api/Bank/movements'; + +const DOLIBARR_LABELS = { + '(InitialBankBalance)': 'Saldo inicial', + '(banktransfert)': 'Transferencia interna', + '(payment)': 'Pago', + '(withdraw)': 'Retirada', +}; + +function cleanLabel(raw) { + if (!raw) return 'Movimiento'; + return DOLIBARR_LABELS[raw] ?? raw; +} + +export function renderBancoPage() { + const container = document.createElement('div'); + container.className = 'banco-page'; + + const fmt = (n) => new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(n ?? 0); + const fmtDate = (d) => d ? new Date(d).toLocaleDateString('es-ES') : '—'; + + container.innerHTML = /*html*/` +
+

Banco

+

Gestión de cuentas bancarias y movimientos

+
+ +
+
+
+ + + +
+
+ Saldo total + — +
+
+ +
+
+ + + + +
+
+ Ingresos + — +
+
+ +
+
+ + + + +
+
+ Gastos + — +
+
+ +
+
+ + + + +
+
+ Cuentas + — +
+
+
+ +
+
+
+

Cuentas bancarias

+ +
+
+
+
+
+
+
+
+
+ +
+
+

Últimos movimientos

+ +
+
+
+
+
+
+
+
+
+
+
+ `; + + let allMovements = []; + let selectedAccountId = null; + + // ── Render helpers ──────────────────────────────────────────────── + + function renderAccounts(list) { + const el = container.querySelector('#banco-accounts'); + if (!list || list.length === 0) { + el.innerHTML = `
+ + + +

No hay cuentas bancarias registradas

+
`; + return; + } + + const totalBalance = list.reduce((s, a) => s + (parseFloat(a.balance) || 0), 0); + container.querySelector('#stat-balance').textContent = fmt(totalBalance); + container.querySelector('#stat-accounts').textContent = list.length; + + el.innerHTML = ''; + list.forEach(acc => { + const item = document.createElement('div'); + item.className = 'banco-account-item'; + item.dataset.id = acc.id; + item.innerHTML = ` +
+ + + +
+
+ + +
+
+ + + +
+ `; + item.querySelector('.btn-account-edit').addEventListener('click', (e) => { + e.stopPropagation(); + showEditAccountModal(acc); + }); + item.addEventListener('click', () => selectAccount(acc)); + el.appendChild(item); + }); + } + + function renderTransactions(list, subtitle = null) { + const el = container.querySelector('#banco-transactions'); + const title = container.querySelector('#tx-title'); + const back = container.querySelector('#btn-all-movements'); + + title.textContent = subtitle ? `Movimientos · ${subtitle}` : 'Últimos movimientos'; + back.style.display = subtitle ? 'inline-flex' : 'none'; + + if (!list || list.length === 0) { + el.innerHTML = `

No hay movimientos${subtitle ? ' para esta cuenta' : ' recientes'}

`; + return; + } + + let income = 0, expense = 0; + el.innerHTML = list.map(tx => { + const amount = parseFloat(tx.amount) || 0; + if (amount >= 0) income += amount; else expense += Math.abs(amount); + return ` +
+
+
+ ${cleanLabel(tx.label)} + ${fmtDate(tx.dateValue || tx.date)} +
+ + ${amount >= 0 ? '+' : ''}${fmt(amount)} + +
+ `; + }).join(''); + + if (!subtitle) { + container.querySelector('#stat-income').textContent = fmt(income); + container.querySelector('#stat-expense').textContent = fmt(expense); + } + } + + function renderUnavailable(section) { + const el = container.querySelector(section); + el.innerHTML = `
+ + + +

Módulo bancario no disponible aún

+ Próximamente +
`; + } + + // ── Account selection ───────────────────────────────────────────── + + async function selectAccount(acc) { + selectedAccountId = acc.id; + + // Highlight selected + container.querySelectorAll('.banco-account-item').forEach(el => { + el.classList.toggle('banco-account-item--active', String(el.dataset.id) === String(acc.id)); + }); + + const el = container.querySelector('#banco-transactions'); + el.innerHTML = `
`; + container.querySelector('#tx-title').textContent = `Movimientos · ${acc.label || acc.ref}`; + container.querySelector('#btn-all-movements').style.display = 'inline-flex'; + + try { + const raw = await apiGet(`${API_ACCOUNTS}/${acc.id}/lines`); + const lines = Array.isArray(raw) ? raw : raw?.data ?? []; + renderTransactions(lines, acc.label || acc.ref); + } catch { + el.innerHTML = `

No se pudieron cargar los movimientos

`; + } + } + + function showAllMovements() { + selectedAccountId = null; + container.querySelectorAll('.banco-account-item').forEach(el => el.classList.remove('banco-account-item--active')); + renderTransactions(allMovements); + } + + container.querySelector('#btn-all-movements').addEventListener('click', showAllMovements); + + // ── Load all data ───────────────────────────────────────────────── + + async function loadData() { + try { + const raw = await apiGet(API_ACCOUNTS); + const accounts = Array.isArray(raw) ? raw : raw?.data ?? []; + renderAccounts(accounts); + } catch { + renderUnavailable('#banco-accounts'); + container.querySelector('#stat-balance').textContent = '—'; + container.querySelector('#stat-accounts').textContent = '—'; + } + + try { + const raw = await apiGet(`${API_MOVEMENTS}?limit=50`); + allMovements = Array.isArray(raw) ? raw : raw?.data ?? []; + renderTransactions(allMovements); + } catch { + renderUnavailable('#banco-transactions'); + container.querySelector('#stat-income').textContent = '—'; + container.querySelector('#stat-expense').textContent = '—'; + } + } + + // ── Edit account modal ──────────────────────────────────────────── + + function showEditAccountModal(acc) { + const overlay = document.createElement('div'); + overlay.className = 'banco-modal-overlay'; + overlay.innerHTML = /*html*/` +
+
+
+
+ + + + +
+
+

Editar cuenta bancaria

+

${acc.label || acc.ref || 'Cuenta sin nombre'}

+
+
+ +
+ +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ `; + + document.body.appendChild(overlay); + + // Set type select value + const typeSelect = overlay.querySelector('#edit-type'); + if (acc.type != null) typeSelect.value = String(acc.type); + + // Load countries and pre-select current + (async () => { + const sel = overlay.querySelector('#edit-country'); + try { + const countries = await getCountries(); + const priority = ['ES','FR','DE','PT','GB','US']; + const first = countries.filter(c => priority.includes(c.code)); + const rest = countries.filter(c => !priority.includes(c.code)); + sel.innerHTML = ''; + first.forEach(c => { const o = document.createElement('option'); o.value = c.id; o.textContent = c.label; sel.appendChild(o); }); + if (rest.length) { + const sep = document.createElement('option'); sep.disabled = true; sep.textContent = '──────────'; sel.appendChild(sep); + rest.forEach(c => { const o = document.createElement('option'); o.value = c.id; o.textContent = c.label; sel.appendChild(o); }); + } + if (acc.countryId) sel.value = String(acc.countryId); + } catch { + sel.innerHTML = ` + + + + + + + `; + if (acc.countryId) sel.value = String(acc.countryId); + } + })(); + + const close = () => { if (overlay.isConnected) overlay.remove(); }; + overlay.querySelector('.banco-modal-close').addEventListener('click', close); + overlay.querySelector('#cancel-edit').addEventListener('click', close); + overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + document.addEventListener('keydown', function esc(e) { + if (e.key === 'Escape') { close(); document.removeEventListener('keydown', esc); } + }); + + overlay.querySelector('#edit-account-form').addEventListener('submit', async (e) => { + e.preventDefault(); + + const payload = {}; + const label = overlay.querySelector('#edit-label').value.trim(); + const ref = overlay.querySelector('#edit-ref').value.trim(); + const currency = overlay.querySelector('#edit-currency').value.trim(); + const bank = overlay.querySelector('#edit-bank').value.trim(); + const number = overlay.querySelector('#edit-number').value.trim(); + const iban = overlay.querySelector('#edit-iban').value.trim(); + const bic = overlay.querySelector('#edit-bic').value.trim(); + const countryId = parseInt(overlay.querySelector('#edit-country').value) || null; + const type = parseInt(overlay.querySelector('#edit-type').value); + + if (label) payload.label = label; + if (ref) payload.ref = ref; + if (currency) payload.currencyCode = currency; + if (bank) payload.bank = bank; + if (number) payload.accountNumber = number; + if (iban) payload.iban = iban; + if (bic) payload.bic = bic; + if (countryId) payload.countryId = countryId; + payload.type = type; + + if (Object.keys(payload).length === 1 && payload.type === acc.type) { + close(); return; + } + + const submitBtn = overlay.querySelector('.btn-banco-submit'); + submitBtn.disabled = true; + submitBtn.innerHTML = ' Guardando...'; + + try { + await apiPut(`/api/Bank/accounts/${acc.id}`, payload); + showToast('Cuenta actualizada', 'success'); + close(); + loadData(); + } catch (err) { + showToast(err.message || 'Error al actualizar la cuenta', 'error'); + submitBtn.disabled = false; + submitBtn.innerHTML = ' Guardar cambios'; + } + }); + } + + // ── Create account modal ────────────────────────────────────────── + + function showCreateAccountModal() { + const overlay = document.createElement('div'); + overlay.className = 'banco-modal-overlay'; + overlay.innerHTML = /*html*/` +
+
+
+
+ + + +
+
+

Nueva cuenta bancaria

+

Los campos marcados con * son obligatorios

+
+
+ +
+ +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ `; + + document.body.appendChild(overlay); + // Focus first input + setTimeout(() => overlay.querySelector('#acc-label')?.focus(), 50); + + // Load countries + (async () => { + const sel = overlay.querySelector('#acc-country'); + try { + const countries = await getCountries(); + const priority = ['ES','FR','DE','PT','GB','US']; + const first = countries.filter(c => priority.includes(c.code)); + const rest = countries.filter(c => !priority.includes(c.code)); + sel.innerHTML = ''; + first.forEach(c => { + const o = document.createElement('option'); + o.value = c.id; o.textContent = c.label; + if (c.code === 'ES') o.selected = true; + sel.appendChild(o); + }); + if (rest.length) { + const sep = document.createElement('option'); sep.disabled = true; sep.textContent = '──────────'; + sel.appendChild(sep); + rest.forEach(c => { const o = document.createElement('option'); o.value = c.id; o.textContent = c.label; sel.appendChild(o); }); + } + } catch { + sel.innerHTML = ` + + + + + + + `; + } + })(); + + const close = () => { if (overlay.isConnected) overlay.remove(); }; + overlay.querySelector('.banco-modal-close').addEventListener('click', close); + overlay.querySelector('#cancel-account').addEventListener('click', close); + overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + document.addEventListener('keydown', function esc(e) { + if (e.key === 'Escape') { close(); document.removeEventListener('keydown', esc); } + }); + + overlay.querySelector('#create-account-form').addEventListener('submit', async (e) => { + e.preventDefault(); + const label = overlay.querySelector('#acc-label').value.trim(); + const ref = overlay.querySelector('#acc-ref').value.trim(); + const countryId = parseInt(overlay.querySelector('#acc-country').value); + + if (!label) { showToast('El nombre es obligatorio', 'warning'); overlay.querySelector('#acc-label').focus(); return; } + if (!ref) { showToast('El código/ref es obligatorio', 'warning'); overlay.querySelector('#acc-ref').focus(); return; } + if (!countryId) { showToast('Selecciona un país', 'warning'); overlay.querySelector('#acc-country').focus(); return; } + + const submitBtn = overlay.querySelector('.btn-banco-submit'); + submitBtn.disabled = true; + submitBtn.innerHTML = ' Guardando...'; + + try { + await apiPost('/api/Bank/accounts', { + label, + ref, + type: parseInt(overlay.querySelector('#acc-type').value), + currencyCode: overlay.querySelector('#acc-currency').value.trim() || 'EUR', + countryId, + bank: overlay.querySelector('#acc-bank').value.trim() || null, + accountNumber: overlay.querySelector('#acc-number').value.trim() || null, + iban: overlay.querySelector('#acc-iban').value.trim() || null, + bic: overlay.querySelector('#acc-bic').value.trim() || null, + }); + showToast('Cuenta bancaria creada', 'success'); + close(); + loadData(); + } catch (err) { + showToast(err.message || 'Error al crear la cuenta', 'error'); + submitBtn.disabled = false; + submitBtn.innerHTML = ' Crear cuenta'; + } + }); + } + + container.querySelector('#btn-new-account').addEventListener('click', showCreateAccountModal); + + loadData(); + return container; +} diff --git a/src/pages/ClientesPage.js b/src/pages/ClientesPage.js index 4abd6f0..366a566 100644 --- a/src/pages/ClientesPage.js +++ b/src/pages/ClientesPage.js @@ -1,6 +1,7 @@ import { ClientItem } from '../components/ClientItem.js'; -import { getClients } from '../services/clients.js'; +import { createClient, getClients } from '../services/clients.js'; import { icons } from '../services/icons.js'; +import { toast } from '../services/toast.js'; export function renderClientesPage() { const container = document.createElement('div'); @@ -17,12 +18,15 @@ export function renderClientesPage() {

Clientes

- +
${icons.search}
+
@@ -199,6 +203,159 @@ export function renderClientesPage() { document.body.appendChild(modal); } + function showCreateClientModal() { + const overlay = document.createElement('div'); + overlay.className = 'client-modal-overlay'; + overlay.innerHTML = /*html*/` +
+
+

Nuevo cliente

+ +
+
+
+
+

Información básica

+
+
+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Dirección

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Notas

+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ `; + + const form = overlay.querySelector('.create-client-form'); + const nameInput = overlay.querySelector('#cc-name'); + const nameError = overlay.querySelector('#cc-name-error'); + const submitBtn = overlay.querySelector('.btn-submit-create'); + const submitText = overlay.querySelector('.btn-submit-text'); + const submitSpinner = overlay.querySelector('.btn-submit-spinner'); + + const close = () => overlay.remove(); + overlay.querySelector('.btn-close-modal').addEventListener('click', close); + overlay.querySelector('.btn-cancel-create').addEventListener('click', close); + overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + + const handleEscape = (e) => { + if (e.key !== 'Escape') return; + if (!overlay.isConnected) { document.removeEventListener('keydown', handleEscape); return; } + close(); + }; + document.addEventListener('keydown', handleEscape); + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + nameError.textContent = ''; + + const name = nameInput.value.trim(); + if (!name) { + nameError.textContent = 'El nombre es obligatorio.'; + nameInput.focus(); + return; + } + + const getData = (n) => { + const v = form.elements[n]?.value?.trim(); + return v || undefined; + }; + + const payload = { + Name: name, + Email: getData('email'), + Phone: getData('phone'), + VatNumber: getData('vatNumber'), + CountryCode: getData('countryCode'), + Address: getData('address'), + Zip: getData('zip'), + Town: getData('town'), + NotePublic: getData('notePublic'), + NotePrivate: getData('notePrivate'), + }; + + submitBtn.disabled = true; + submitText.hidden = true; + submitSpinner.hidden = false; + + try { + await createClient(payload); + toast.success(`Cliente "${name}" creado correctamente.`); + close(); + await loadAllClients(); + } catch (err) { + toast.error(err.message || 'Error al crear el cliente.'); + submitBtn.disabled = false; + submitText.hidden = false; + submitSpinner.hidden = true; + } + }); + + document.body.appendChild(overlay); + setTimeout(() => nameInput.focus(), 50); + } + async function loadAllClients() { const clientsList = container.querySelector('.clients-list'); @@ -237,6 +394,8 @@ export function renderClientesPage() { btnNext.disabled = currentPage === totalPages; } + container.querySelector('.btn-new-client').addEventListener('click', showCreateClientModal); + const searchInput = container.querySelector('.search-input'); searchInput.addEventListener('input', (e) => { clearTimeout(searchInput._debounceTimer); diff --git a/src/pages/CreateInvoicePage.js b/src/pages/CreateInvoicePage.js index f8ee1c8..f368058 100644 --- a/src/pages/CreateInvoicePage.js +++ b/src/pages/CreateInvoicePage.js @@ -3,6 +3,7 @@ import { navigationGuards } from '../router.js'; import { apiGet, apiPost } from '../services/apiClient.js'; import { icons } from '../services/icons.js'; import { showToast } from '../services/toast.js'; +import { getPaymentTerms } from '../services/setup.js'; export function renderCreateInvoicePage() { const container = document.createElement('div'); @@ -46,6 +47,13 @@ export function renderCreateInvoicePage() {
+ +
+ + +
@@ -101,11 +109,44 @@ export function renderCreateInvoicePage() { const today = new Date().toISOString().split('T')[0]; container.querySelector('#date').value = today; - + const expireDate = new Date(); expireDate.setDate(expireDate.getDate() + 30); container.querySelector('#expireDate').value = expireDate.toISOString().split('T')[0]; + // Condiciones de pago — calcula vencimiento automáticamente al seleccionar + async function loadPaymentTerms() { + try { + const terms = await getPaymentTerms(); + const select = container.querySelector('#paymentTerms'); + terms.forEach(t => { + if (!t.label && !t.code) return; + const opt = document.createElement('option'); + opt.value = t.nbjour ?? ''; + opt.textContent = t.label || t.code; + select.appendChild(opt); + }); + } catch { /* opcional — fallo silencioso */ } + } + loadPaymentTerms(); + + container.querySelector('#paymentTerms').addEventListener('change', (e) => { + const days = parseInt(e.target.value); + if (!days || days <= 0) return; + const base = new Date(container.querySelector('#date').value || today); + base.setDate(base.getDate() + days); + container.querySelector('#expireDate').value = base.toISOString().split('T')[0]; + }); + + // Si cambia la fecha base, recalcular vencimiento si hay condición seleccionada + container.querySelector('#date').addEventListener('change', (e) => { + const days = parseInt(container.querySelector('#paymentTerms').value); + if (!days || days <= 0) return; + const base = new Date(e.target.value); + base.setDate(base.getDate() + days); + container.querySelector('#expireDate').value = base.toISOString().split('T')[0]; + }); + // Event listeners para botones de calendario container.querySelectorAll('.date-picker-btn').forEach(btn => { btn.addEventListener('click', () => { diff --git a/src/pages/Facturas.js b/src/pages/Facturas.js index f1fb928..10af5ca 100755 --- a/src/pages/Facturas.js +++ b/src/pages/Facturas.js @@ -1,7 +1,8 @@ import { InvoiceItem } from '../components/InvoiceItem.js'; import { InvoiceModal } from '../components/InvoiceModal.js'; -import { apiGet } from '../services/apiClient.js'; +import { apiDelete, apiGet, apiPost } from '../services/apiClient.js'; import { icons } from '../services/icons.js'; +import { showToast } from '../services/toast.js'; export function renderFacturasPage() { const container = document.createElement('div'); @@ -16,6 +17,7 @@ export function renderFacturasPage() { let searchTerm = ''; let sortKey = null; let sortDir = 'asc'; + let selectedIds = new Set(); container.innerHTML = /*html*/`
@@ -33,13 +35,41 @@ export function renderFacturasPage() { +
+ + + +
+ @@ -62,7 +92,7 @@ export function renderFacturasPage() { - +
+ + Número
Cargando facturas...
Cargando facturas...
@@ -165,14 +195,20 @@ export function renderFacturasPage() { invoicesList.innerHTML = ''; if (invoices.length === 0) { - invoicesList.innerHTML = `
${icons.emptyInvoices}

No hay facturas

Crea tu primera factura para comenzar.

`; + invoicesList.innerHTML = `
${icons.emptyInvoices}

No hay facturas

Crea tu primera factura para comenzar.

`; return; } invoices.forEach(invoice => { const invoiceItem = InvoiceItem(invoice, handleViewInvoice); + // Restore checked state for already-selected rows + const chk = invoiceItem.querySelector('.invoice-check'); + if (chk && selectedIds.has(String(invoice.id))) chk.checked = true; invoicesList.appendChild(invoiceItem); }); + + // Sync select-all state + syncSelectAll(); } function handleViewInvoice(invoiceId) { @@ -184,9 +220,11 @@ export function renderFacturasPage() { async function loadAllInvoices() { const invoicesList = container.querySelector('.invoices-list'); + selectedIds.clear(); + updateBulkBar(); try { - invoicesList.innerHTML = 'Cargando facturas...'; + invoicesList.innerHTML = 'Cargando facturas...'; let url = '/api/Invoices?limit=1000'; if (currentFilter) url += `&status=${currentFilter}`; @@ -200,7 +238,7 @@ export function renderFacturasPage() { renderPage(1); } catch (error) { console.error('Error al cargar facturas:', error); - invoicesList.innerHTML = `
${icons.error}

Error al cargar

No se pudieron cargar las facturas. Inténtalo de nuevo.

`; + invoicesList.innerHTML = `
${icons.error}

Error al cargar

No se pudieron cargar las facturas. Inténtalo de nuevo.

`; } } @@ -237,6 +275,264 @@ export function renderFacturasPage() { }); }); + // ── Bulk selection ────────────────────────────────────────────────── + + function updateBulkBar() { + const bar = container.querySelector('#bulk-bar'); + const count = container.querySelector('#bulk-count'); + if (selectedIds.size > 0) { + bar.style.display = 'flex'; + count.textContent = `${selectedIds.size} seleccionada${selectedIds.size !== 1 ? 's' : ''}`; + } else { + bar.style.display = 'none'; + } + } + + function syncSelectAll() { + const checkAll = container.querySelector('#check-all'); + if (!checkAll) return; + const pageCheckboxes = [...container.querySelectorAll('.invoice-check')]; + if (pageCheckboxes.length === 0) { checkAll.checked = false; checkAll.indeterminate = false; return; } + const checkedCount = pageCheckboxes.filter(c => c.checked).length; + checkAll.checked = checkedCount === pageCheckboxes.length; + checkAll.indeterminate = checkedCount > 0 && checkedCount < pageCheckboxes.length; + } + + // Event delegation for row checkboxes + container.querySelector('.invoices-list').addEventListener('change', (e) => { + if (!e.target.classList.contains('invoice-check')) return; + const id = String(e.target.value); + if (e.target.checked) selectedIds.add(id); else selectedIds.delete(id); + syncSelectAll(); + updateBulkBar(); + }); + + container.querySelector('#check-all').addEventListener('change', (e) => { + const checkboxes = container.querySelectorAll('.invoice-check'); + checkboxes.forEach(chk => { + chk.checked = e.target.checked; + if (e.target.checked) selectedIds.add(String(chk.value)); else selectedIds.delete(String(chk.value)); + }); + updateBulkBar(); + }); + + container.querySelector('#btn-deselect').addEventListener('click', () => { + selectedIds.clear(); + container.querySelectorAll('.invoice-check').forEach(c => { c.checked = false; }); + syncSelectAll(); + updateBulkBar(); + }); + + container.querySelector('#btn-bulk-delete').addEventListener('click', async () => { + if (selectedIds.size === 0) return; + const confirmed = window.confirm(`¿Eliminar ${selectedIds.size} factura${selectedIds.size !== 1 ? 's' : ''}? Esta acción no se puede deshacer.`); + if (!confirmed) return; + + const btn = container.querySelector('#btn-bulk-delete'); + btn.disabled = true; + btn.textContent = 'Eliminando...'; + + let ok = 0, fail = 0; + await Promise.allSettled([...selectedIds].map(async id => { + try { + await apiDelete(`/api/Invoices/${id}`); + ok++; + } catch { + fail++; + } + })); + + btn.disabled = false; + btn.innerHTML = ` Eliminar`; + + if (ok > 0) showToast(`${ok} factura${ok !== 1 ? 's' : ''} eliminada${ok !== 1 ? 's' : ''}`, 'success'); + if (fail > 0) showToast(`${fail} factura${fail !== 1 ? 's no pudieron' : ' no pudo'} eliminarse`, 'error'); + loadAllInvoices(); + }); + + container.querySelector('#btn-duplicate').addEventListener('click', async () => { + if (selectedIds.size === 0) return; + const btn = container.querySelector('#btn-duplicate'); + btn.disabled = true; + btn.textContent = 'Duplicando...'; + + const today = new Date().toISOString().split('T')[0]; + const expire = new Date(); expire.setDate(expire.getDate() + 30); + const expireStr = expire.toISOString().split('T')[0]; + + let ok = 0, fail = 0; + await Promise.allSettled([...selectedIds].map(async id => { + try { + const inv = await apiGet(`/api/Invoices/${id}`); + const lines = (inv.lines || []).map(l => ({ + description: l.description, + quantity: l.quantity, + unitPrice: l.unitPrice, + taxRate: l.taxRate ?? l.tax ?? 0, + })); + await apiPost('/api/Invoices', { + clientId: inv.clientId, + date: today + 'T00:00:00', + expireDate: expireStr + 'T00:00:00', + notePublic: inv.notePublic || null, + notePrivate: inv.notePrivate || null, + lines, + }); + ok++; + } catch { + fail++; + } + })); + + btn.disabled = false; + btn.innerHTML = ` Duplicar`; + + if (ok > 0) showToast(`${ok} factura${ok !== 1 ? 's' : ''} duplicada${ok !== 1 ? 's' : ''}`, 'success'); + if (fail > 0) showToast(`${fail} factura${fail !== 1 ? 's no pudieron' : ' no pudo'} duplicarse`, 'error'); + loadAllInvoices(); + }); + + // ── Quarterly summary ───────────────────────────────────────────── + + let quarterlyVisible = false; + + function buildQuarterlySummary(invoices) { + const quarters = {}; + + invoices.forEach(inv => { + if (!inv.date) return; + const d = new Date(inv.date); + const year = d.getFullYear(); + const q = Math.floor(d.getMonth() / 3) + 1; + const key = `${year}-Q${q}`; + + if (!quarters[key]) quarters[key] = { year, q, ht: 0, tax: 0, total: 0, paid: 0, count: 0 }; + + const ht = parseFloat(inv.totalHt) || 0; + const tax = parseFloat(inv.totalTax) || 0; + const ttc = parseFloat(inv.total) || 0; + const remain = parseFloat(inv.remainToPay) || 0; + + quarters[key].ht += ht; + quarters[key].tax += tax; + quarters[key].total += ttc; + quarters[key].paid += (ttc - remain); + quarters[key].count += 1; + }); + + return Object.values(quarters).sort((a, b) => + b.year !== a.year ? b.year - a.year : b.q - a.q + ); + } + + function renderQuarterlySummary() { + const content = container.querySelector('#quarterly-content'); + const rows = buildQuarterlySummary(allInvoices); + + if (rows.length === 0) { + content.innerHTML = '

No hay facturas para mostrar

'; + return; + } + + // Group by year + const byYear = {}; + rows.forEach(r => { + if (!byYear[r.year]) byYear[r.year] = []; + byYear[r.year].push(r); + }); + + const fmt = (n) => new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(n); + const qLabel = (q) => [`1er`, `2º`, `3er`, `4º`][q - 1] + ` trimestre`; + + let html = ''; + Object.keys(byYear).sort((a,b) => b - a).forEach(year => { + const yearRows = byYear[year]; + const totals = yearRows.reduce((acc, r) => ({ + ht: acc.ht + r.ht, tax: acc.tax + r.tax, + total: acc.total + r.total, paid: acc.paid + r.paid, + count: acc.count + r.count + }), { ht: 0, tax: 0, total: 0, paid: 0, count: 0 }); + + html += ` +
+
+ ${year} + ${fmt(totals.total)} · ${totals.count} facturas +
+ + + + + + + + + + + + + + ${yearRows.map(r => { + const pending = r.total - r.paid; + return ` + + + + + + + + `; + }).join('')} + + + + + + + + + + + + +
TrimestreFacturasBase imponibleIVATotalCobradoPendiente
T${r.q} · ${qLabel(r.q)}${r.count}${fmt(r.ht)}${fmt(r.tax)}${fmt(r.total)}${fmt(r.paid)}${fmt(pending)}
+
`; + }); + + content.innerHTML = html; + } + + function toggleQuarterly() { + quarterlyVisible = !quarterlyVisible; + const view = container.querySelector('#quarterly-view'); + const table = container.querySelector('.invoices-table-container'); + const pag = container.querySelector('.pagination'); + const summary = container.querySelector('.invoices-summary'); + const btn = container.querySelector('#btn-quarterly'); + const bulk = container.querySelector('#bulk-bar'); + + if (quarterlyVisible) { + view.style.display = 'block'; + table.style.display = 'none'; + if (pag) pag.style.display = 'none'; + if (summary) summary.style.display = 'none'; + if (bulk) bulk.style.display = 'none'; + btn.classList.add('btn-quarterly--active'); + btn.innerHTML = ` Cerrar resumen`; + renderQuarterlySummary(); + } else { + view.style.display = 'none'; + table.style.display = ''; + btn.classList.remove('btn-quarterly--active'); + btn.innerHTML = ` Resumen trimestral`; + updatePaginationUI(); + updateSummary(filteredInvoices); + } + } + + container.querySelector('#btn-quarterly').addEventListener('click', toggleQuarterly); + container.querySelector('.btn-new-invoice').addEventListener('click', () => { window.location.hash = '#create-invoice'; }); diff --git a/src/pages/pagesRegistry.js b/src/pages/pagesRegistry.js index 2884d55..88649d4 100755 --- a/src/pages/pagesRegistry.js +++ b/src/pages/pagesRegistry.js @@ -3,6 +3,7 @@ import { renderFacturasPage } from './Facturas.js'; import { renderCreateInvoicePage } from './CreateInvoicePage.js'; import { renderClientesPage } from './ClientesPage.js'; import { renderSettingsPage } from './SettingsPage.js'; +import { renderBancoPage } from './BancoPage.js'; import { icons } from '../services/icons.js'; export const pagesRegistry = [ @@ -42,6 +43,15 @@ export const pagesRegistry = [ showInSidebar: true, render: renderClientesPage }, + { + route: 'banco', + name: 'Banco', + icon: icons.bank, + voicePatterns: ['banco', 'ir al banco', 'cuentas', 'cuentas bancarias', 'movimientos'], + requiresAuth: true, + showInSidebar: true, + render: renderBancoPage + }, { route: 'settings', name: 'Configuración', diff --git a/src/router.js b/src/router.js index bc6ef05..8f90a55 100755 --- a/src/router.js +++ b/src/router.js @@ -42,15 +42,22 @@ export function initRouter() { const hash = window.location.hash || (DEV_MODE ? '#dashboard' : '#login'); const route = hash.substring(1); + // Auth check first — a forced logout must never be blocked by the unsaved-changes guard + if (!DEV_MODE && !auth.checkAuth() && hash !== '#login') { + navigationGuards.unregister(); + window.location.hash = '#login'; + return; + } + // Si hay un guard activo y no estamos forzando navegación if (!forceNavigate && navigationGuards.hasUnsavedChanges()) { // Guardar el hash al que se quiere ir pendingHash = hash; - + // Volver al hash anterior temporalmente const previousHash = '#' + (document.querySelector('.main-content')?.dataset?.currentRoute || 'dashboard'); history.pushState(null, '', previousHash); - + // Mostrar modal de confirmación const confirmed = await showConfirmExitModal({ title: 'Cambios sin guardar', @@ -69,11 +76,6 @@ export function initRouter() { return; } - if (!DEV_MODE && !auth.checkAuth() && hash !== '#login') { - window.location.hash = '#login'; - return; - } - if (typeof currentPageCleanup === 'function') { currentPageCleanup(); currentPageCleanup = null; diff --git a/src/services/apiClient.js b/src/services/apiClient.js index 5aef6ab..0f8e95f 100644 --- a/src/services/apiClient.js +++ b/src/services/apiClient.js @@ -3,6 +3,10 @@ let reloginInProgress = false; function redirectToLogin() { if (window.location.hash !== '#login') { + // Close any open modals/overlays appended to body before leaving + document.querySelectorAll('body > [class*="overlay"], body > [class*="modal"]').forEach(el => { + if (!el.classList.contains('connection-lost-overlay')) el.remove(); + }); window.location.hash = '#login'; } } diff --git a/src/services/clients.js b/src/services/clients.js index a4acffc..e906e7d 100644 --- a/src/services/clients.js +++ b/src/services/clients.js @@ -1,4 +1,4 @@ -import { apiGet } from './apiClient.js'; +import { apiDelete, apiGet, apiPost, apiPut } from './apiClient.js'; export async function getClients(limit = 50, page = 1) { return apiGet(`/api/Clients?limit=${limit}&page=${page}`); @@ -7,3 +7,15 @@ export async function getClients(limit = 50, page = 1) { export async function getClientById(id) { return apiGet(`/api/Clients/${id}`); } + +export async function createClient(data) { + return apiPost('/api/Clients', data); +} + +export async function updateClient(id, data) { + return apiPut(`/api/Clients/${id}`, data); +} + +export async function deleteClient(id) { + return apiDelete(`/api/Clients/${id}`); +} diff --git a/src/services/icons.js b/src/services/icons.js index adcde84..6a8d89e 100644 --- a/src/services/icons.js +++ b/src/services/icons.js @@ -95,5 +95,7 @@ export const icons = { moon: ``, - info: `` + info: ``, + + bank: `` }; \ No newline at end of file diff --git a/src/services/invoices.js b/src/services/invoices.js index fbace3a..988f55d 100644 --- a/src/services/invoices.js +++ b/src/services/invoices.js @@ -28,6 +28,11 @@ export async function deleteInvoiceLine(invoiceId, lineId) { return true; } +export async function updateInvoiceLine(invoiceId, lineId, data) { + await apiPut(`/api/Invoices/${invoiceId}/lines/${lineId}`, data); + return true; +} + export async function addPayment(invoiceId, paymentData) { return apiPost(`/api/Invoices/${invoiceId}/payments`, paymentData); } diff --git a/src/services/setup.js b/src/services/setup.js new file mode 100644 index 0000000..e160584 --- /dev/null +++ b/src/services/setup.js @@ -0,0 +1,17 @@ +import { apiGet } from './apiClient.js'; + +export async function getPaymentTypes() { + return apiGet('/api/Setup/payment-types'); +} + +export async function getPaymentTerms() { + return apiGet('/api/Setup/payment-terms'); +} + +export async function getCompany() { + return apiGet('/api/Setup/company'); +} + +export async function getCountries() { + return apiGet('/api/Setup/countries'); +} diff --git a/src/style.css b/src/style.css index a389606..ce8047d 100755 --- a/src/style.css +++ b/src/style.css @@ -132,39 +132,39 @@ color-scheme: dark; } -:root[data-theme='dark'] .status-badge, -:root[data-theme='dark'] .status-badge-client { - border: 1px solid rgba(148, 163, 184, 0.2); -} - :root[data-theme='dark'] .status-paid, :root[data-theme='dark'] .status-badge-client.status-active { - background-color: rgba(34, 197, 94, 0.18); - color: #86efac; - border-color: rgba(34, 197, 94, 0.35); + background-color: #15803d; + color: #fff; } :root[data-theme='dark'] .status-overdue, :root[data-theme='dark'] .status-unpaid, -:root[data-theme='dark'] .status-pending, :root[data-theme='dark'] .status-badge-client.status-inactive { - background-color: rgba(248, 113, 113, 0.18); - color: #fca5a5; - border-color: rgba(248, 113, 113, 0.35); + background-color: #c2410c; + color: #fff; +} + +:root[data-theme='dark'] .status-pending { + background-color: #b45309; + color: #fff; } :root[data-theme='dark'] .status-draft, -:root[data-theme='dark'] .status-canceled, :root[data-theme='dark'] .status-badge-client.status-unknown { - background-color: rgba(148, 163, 184, 0.14); - color: #cbd5e1; - border-color: rgba(148, 163, 184, 0.24); + background-color: #475569; + color: #fff; +} + +:root[data-theme='dark'] .status-canceled { + background-color: #991b1b; + color: #fff; + opacity: 0.75; } :root[data-theme='dark'] .status-validated { - background-color: rgba(96, 165, 250, 0.18); - color: #93c5fd; - border-color: rgba(96, 165, 250, 0.35); + background-color: #1d4ed8; + color: #fff; } :root[data-theme='dark'] .invoices-table thead, @@ -271,44 +271,46 @@ button:disabled { .status-badge { display: inline-flex; align-items: center; - padding: 2px var(--space-2); - border-radius: var(--radius-sm); - font-size: 12px; - font-weight: 500; + padding: 3px 10px; + border-radius: 99px; + font-size: 11px; + font-weight: 600; line-height: 1.5; white-space: nowrap; + letter-spacing: 0.02em; + border: none; } .status-pending { - background-color: var(--amber-50); - color: var(--amber-600); + background-color: #d97706; + color: #fff; } .status-paid { - background-color: var(--green-50); - color: var(--green-600); + background-color: #16a34a; + color: #fff; } .status-overdue, .status-unpaid { - background-color: var(--red-50); - color: var(--red-600); + background-color: #ea580c; + color: #fff; } .status-draft { - background-color: var(--gray-100); - color: var(--gray-600); + background-color: #64748b; + color: #fff; } .status-validated { - background-color: var(--blue-50); - color: var(--blue-600); + background-color: #2563eb; + color: #fff; } .status-canceled { - background-color: var(--gray-100); - color: var(--gray-400); - text-decoration: line-through; + background-color: #dc2626; + color: #fff; + opacity: 0.7; } /* ============================================ diff --git a/src/styles/banco.css b/src/styles/banco.css new file mode 100644 index 0000000..65eff50 --- /dev/null +++ b/src/styles/banco.css @@ -0,0 +1,660 @@ +/* ============================================ + Banco Page + ============================================ */ + +.banco-page { + padding: var(--space-6); + max-width: 1200px; + margin: 0 auto; +} + +.banco-header { + margin-bottom: var(--space-5); +} + +.banco-header h1 { + font-size: 1.25rem; + font-weight: 600; + margin: 0 0 var(--space-1); +} + +.banco-subtitle { + margin: 0; + font-size: 13px; + color: var(--text-secondary); +} + +/* ── Summary cards ── */ +.banco-summary-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--space-3); + margin-bottom: var(--space-5); +} + +.banco-stat-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + padding: var(--space-4); + display: flex; + align-items: center; + gap: var(--space-3); + box-shadow: var(--shadow-xs); +} + +.banco-stat-icon { + width: 40px; + height: 40px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.banco-stat-card--balance .banco-stat-icon { background: var(--blue-50); color: var(--primary); } +.banco-stat-card--income .banco-stat-icon { background: var(--green-50); color: var(--success); } +.banco-stat-card--expense .banco-stat-icon { background: var(--red-50); color: var(--danger); } +.banco-stat-card--accounts .banco-stat-icon { background: var(--amber-50); color: var(--warning); } + +.banco-stat-body { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.banco-stat-label { + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.banco-stat-value { + font-size: 18px; + font-weight: 700; + color: var(--text-primary); + font-variant-numeric: tabular-nums; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ── Content grid ── */ +.banco-content-grid { + display: grid; + grid-template-columns: 1fr 1.6fr; + gap: var(--space-4); +} + +.banco-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xs); + overflow: hidden; +} + +.banco-card-header { + padding: var(--space-4) var(--space-5); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; +} + +.banco-card-header h2 { + margin: 0; + font-size: 14px; + font-weight: 600; +} + +.btn-banco-new { + background: var(--blue-600) !important; + color: white !important; + border: none !important; + border-radius: var(--radius-md); + padding: 5px 12px; + font-size: 12px; + font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: background 0.15s; +} +.btn-banco-new:hover { background: var(--blue-700) !important; } + +/* ── Accounts list ── */ +.banco-accounts-list { + padding: var(--space-2) 0; +} + +.banco-account-item { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-5); + cursor: pointer; + transition: background-color 0.15s, border-left-color 0.15s; + border-left: 3px solid transparent; +} + +.banco-account-item:hover { + background: var(--gray-50); +} + +.banco-account-item--active { + background: var(--blue-50) !important; + border-left-color: var(--primary); +} + +:root[data-theme='dark'] .banco-account-item:hover { background: rgba(30,58,110,0.2); } +:root[data-theme='dark'] .banco-account-item--active { background: rgba(37,99,235,0.12) !important; } + +.banco-account-icon { + width: 34px; + height: 34px; + border-radius: var(--radius-md); + background: var(--blue-50); + color: var(--primary); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background 0.15s; +} + +.banco-account-item--active .banco-account-icon { + background: var(--primary); + color: white; +} + +.banco-account-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.banco-account-name { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.banco-account-sub { + font-size: 11px; + color: var(--text-secondary); + font-family: ui-monospace, 'Cascadia Code', monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.banco-account-right { + display: flex; + align-items: center; + gap: var(--space-2); + flex-shrink: 0; +} + +.banco-account-balance { + font-size: 13px; + font-weight: 700; + color: var(--success); + font-variant-numeric: tabular-nums; +} + +.banco-account-balance--negative { color: var(--danger); } + +.banco-account-arrow { + color: var(--gray-300); + transition: color 0.15s, transform 0.15s; +} + +.banco-account-item:hover .banco-account-arrow, +.banco-account-item--active .banco-account-arrow { + color: var(--primary); + transform: translateX(2px); +} + +.btn-account-edit { + display: none; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border-radius: var(--radius-sm); + background: transparent !important; + border: 1px solid var(--border-color) !important; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; + flex-shrink: 0; + padding: 0 !important; +} + +.btn-account-edit svg { + stroke: var(--gray-500); + transition: stroke 0.15s; + display: block; +} + +.banco-account-item:hover .btn-account-edit, +.banco-account-item--active .btn-account-edit { + display: inline-flex; +} + +.btn-account-edit:hover { + background: var(--blue-50) !important; + border-color: var(--primary) !important; +} + +.btn-account-edit:hover svg { + stroke: var(--primary); +} + +:root[data-theme='dark'] .btn-account-edit svg { stroke: var(--gray-400); } +:root[data-theme='dark'] .btn-account-edit:hover { background: rgba(37,99,235,0.15) !important; } +:root[data-theme='dark'] .btn-account-edit:hover svg { stroke: var(--blue-400, #60a5fa); } + +/* ── Back button in tx header ── */ +.btn-banco-back { + display: inline-flex; + align-items: center; + gap: 4px; + background: none !important; + border: 1px solid var(--border-color) !important; + color: var(--text-secondary) !important; + border-radius: var(--radius-md); + padding: 3px 10px; + font-size: 12px; + font-family: inherit; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} +.btn-banco-back:hover { background: var(--gray-100) !important; color: var(--text-primary) !important; } +:root[data-theme='dark'] .btn-banco-back:hover { background: rgba(30,58,110,0.3) !important; } + +.banco-account-balance--negative { + color: var(--danger); +} + +/* ── Transactions list ── */ +.banco-transactions-list { + padding: var(--space-2) 0; +} + +.banco-tx-item { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-5); + transition: background-color var(--transition-fast); +} + +.banco-tx-item:hover { + background: var(--bg-subtle); +} + +.banco-tx-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.banco-tx-indicator--credit { background: var(--success); } +.banco-tx-indicator--debit { background: var(--danger); } + +.banco-tx-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.banco-tx-label { + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.banco-tx-date { + font-size: 11px; + color: var(--text-secondary); +} + +.banco-tx-amount { + font-size: 13px; + font-weight: 600; + font-variant-numeric: tabular-nums; + flex-shrink: 0; +} + +.banco-tx-amount--credit { color: var(--success); } +.banco-tx-amount--debit { color: var(--danger); } + +/* ── Loading skeleton ── */ +.banco-loading { + padding: var(--space-3) var(--space-5); + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.banco-skeleton { + height: 36px; + border-radius: var(--radius-md); + background: linear-gradient(90deg, var(--bg-subtle) 25%, var(--gray-100) 50%, var(--bg-subtle) 75%); + background-size: 200% 100%; + animation: banco-shimmer 1.4s ease infinite; +} + +@keyframes banco-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +/* ── Empty state ── */ +.banco-empty { + padding: var(--space-8) var(--space-5); + text-align: center; + color: var(--text-secondary); + font-size: 13px; + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-3); +} + +.banco-soon-tag { + display: inline-block; + background: var(--amber-50); + color: var(--warning); + border: 1px solid rgba(217, 119, 6, 0.2); + border-radius: 99px; + font-size: 11px; + font-weight: 600; + padding: 2px 10px; + letter-spacing: 0.04em; +} + +/* ── Dark mode ── */ +:root[data-theme='dark'] .banco-stat-card, +:root[data-theme='dark'] .banco-card { + background: #081226; + border-color: #1e2f4d; +} + +:root[data-theme='dark'] .banco-card-header { + border-color: #1e2f4d; +} + +:root[data-theme='dark'] .banco-account-icon { + background: rgba(30, 58, 110, 0.4); +} + +:root[data-theme='dark'] .banco-account-item--active .banco-account-icon { + background: var(--primary); +} + +:root[data-theme='dark'] .banco-stat-card--balance .banco-stat-icon { background: rgba(37, 99, 235, 0.15); } +:root[data-theme='dark'] .banco-stat-card--income .banco-stat-icon { background: rgba(22, 163, 74, 0.15); } +:root[data-theme='dark'] .banco-stat-card--expense .banco-stat-icon { background: rgba(220, 38, 38, 0.15); } +:root[data-theme='dark'] .banco-stat-card--accounts .banco-stat-icon { background: rgba(217, 119, 6, 0.15); } + +:root[data-theme='dark'] .banco-skeleton { + background: linear-gradient(90deg, rgba(15,23,42,0.8) 25%, rgba(30,47,77,0.8) 50%, rgba(15,23,42,0.8) 75%); + background-size: 200% 100%; +} + +:root[data-theme='dark'] .banco-soon-tag { + background: rgba(217, 119, 6, 0.12); + border-color: rgba(217, 119, 6, 0.3); + color: #fbbf24; +} + +/* ── Create account modal ── */ +.banco-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: var(--space-4); + backdrop-filter: blur(2px); +} + +.banco-modal { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius-xl); + width: 100%; + max-width: 520px; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 20px 60px rgba(0,0,0,0.18); + animation: fadeScaleIn 0.15s ease; +} + +@keyframes fadeScaleIn { + from { opacity: 0; transform: scale(0.97) translateY(4px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.banco-modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: var(--space-5) var(--space-5) var(--space-4); + border-bottom: 1px solid var(--border-color); + gap: var(--space-3); +} + +.banco-modal-title-group { + display: flex; + align-items: flex-start; + gap: var(--space-3); +} + +.banco-modal-icon { + width: 36px; + height: 36px; + border-radius: var(--radius-md); + background: var(--blue-50); + color: var(--primary); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-top: 1px; +} + +.banco-modal-icon--edit { + background: #f0fdf4; + color: #16a34a; +} + +:root[data-theme='dark'] .banco-modal-icon--edit { + background: rgba(22,163,74,0.12); + color: #4ade80; +} + +.banco-modal-header h2 { + margin: 0 0 2px; + font-size: 15px; + font-weight: 600; + line-height: 1.3; +} + +.banco-modal-subtitle { + margin: 0; + font-size: 12px; + color: var(--text-secondary); +} + +.banco-modal-close { + background: none !important; + border: none !important; + color: var(--text-tertiary, var(--gray-400)); + cursor: pointer; + font-size: 16px; + padding: 4px 8px; + border-radius: var(--radius-sm); + line-height: 1; + flex-shrink: 0; + transition: background 0.15s, color 0.15s; +} +.banco-modal-close:hover { background: var(--gray-100) !important; color: var(--text-primary); } + +.banco-modal-form { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-4); } + +/* ── Form sections ── */ +.banco-form-section { + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.banco-form-section-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-secondary); + display: flex; + align-items: center; + gap: var(--space-2); +} + +.banco-form-section-label::after { + content: ''; + flex: 1; + height: 1px; + background: var(--border-color); +} + +.banco-form-section-optional { + font-weight: 400; + text-transform: none; + letter-spacing: 0; + font-size: 11px; + color: var(--text-tertiary, var(--gray-400)); +} + +.banco-form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-3); +} + +.banco-form-field { display: flex; flex-direction: column; gap: 5px; } +.banco-form-field--full { grid-column: 1 / -1; } + +.banco-form-label { + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); +} +.banco-form-label .required { color: var(--danger); margin-left: 1px; } + +.banco-form-input { + padding: 8px var(--space-3); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + font-size: 13px; + font-family: inherit; + background: var(--card-bg); + color: var(--text-primary); + transition: border-color 0.15s, box-shadow 0.15s; + width: 100%; + box-sizing: border-box; +} +.banco-form-input:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(59,130,246,0.1); +} +.banco-form-input::placeholder { color: var(--gray-400); } + +select.banco-form-input { cursor: pointer; } + +.banco-modal-footer { + display: flex; + gap: var(--space-2); + justify-content: flex-end; + align-items: center; + padding-top: var(--space-4); + border-top: 1px solid var(--border-color); +} + +.btn-banco-cancel { + background: transparent !important; + color: var(--text-secondary) !important; + border: 1px solid var(--border-color) !important; + border-radius: var(--radius-md); + padding: 8px 18px; + font-size: 13px; + font-family: inherit; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} +.btn-banco-cancel:hover { background: var(--gray-50) !important; color: var(--text-primary) !important; } + +.btn-banco-submit { + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--blue-600) !important; + color: white !important; + border: none !important; + border-radius: var(--radius-md); + padding: 8px 18px; + font-size: 13px; + font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: background 0.15s, box-shadow 0.15s; +} +.btn-banco-submit:hover:not(:disabled) { + background: var(--blue-700) !important; + box-shadow: 0 4px 12px rgba(37,99,235,0.3); +} +.btn-banco-submit:disabled { opacity: 0.65; cursor: not-allowed; } + +:root[data-theme='dark'] .banco-modal { background: #0d1a2e; border-color: #1e2f4d; } +:root[data-theme='dark'] .banco-modal-header { border-color: #1e2f4d; } +:root[data-theme='dark'] .banco-modal-footer { border-color: #1e2f4d; } +:root[data-theme='dark'] .banco-modal-icon { background: rgba(37,99,235,0.15); } +:root[data-theme='dark'] .banco-form-section-label::after { background: #1e2f4d; } +:root[data-theme='dark'] .banco-form-input { background: #081226; border-color: #1e2f4d; } +:root[data-theme='dark'] .banco-form-input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(59,130,246,0.12); } +:root[data-theme='dark'] .banco-modal-close:hover { background: rgba(30,58,110,0.3) !important; } +:root[data-theme='dark'] .btn-banco-cancel:hover { background: rgba(30,47,77,0.5) !important; } + +/* ── Responsive ── */ +@media (max-width: 1024px) { + .banco-summary-grid { grid-template-columns: repeat(2, 1fr); } + .banco-content-grid { grid-template-columns: 1fr; } +} + +@media (max-width: 640px) { + .banco-page { padding: var(--space-3); } + .banco-summary-grid { grid-template-columns: 1fr 1fr; } +} diff --git a/src/styles/clients.css b/src/styles/clients.css index ef07b53..6111e83 100644 --- a/src/styles/clients.css +++ b/src/styles/clients.css @@ -521,3 +521,185 @@ :root[data-theme='dark'] .no-contacts { background: rgba(15, 23, 42, 0.9); } + +/* ── Botón nuevo cliente ── */ +.btn-new-client { + display: inline-flex; + align-items: center; + gap: var(--space-2); + background: var(--primary); + color: white; + border: none; + border-radius: var(--radius-md); + padding: var(--space-2) var(--space-4); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background-color var(--transition-fast); + white-space: nowrap; + flex-shrink: 0; +} + +.btn-new-client:hover { + background: var(--primary-hover); +} + +/* ── Modal crear cliente ── */ +.create-client-modal { + max-width: 620px; + max-height: 90vh; +} + +.create-client-section { + margin-bottom: var(--space-5); +} + +.create-client-section:last-child { + margin-bottom: 0; +} + +.create-client-section h3 { + margin: 0 0 var(--space-3) 0; + font-size: 11px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.create-client-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-3); +} + +.form-field { + display: flex; + flex-direction: column; + gap: 4px; +} + +.form-field--full { + grid-column: 1 / -1; +} + +.form-label { + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); +} + +.form-required { + color: var(--danger); +} + +.form-input { + width: 100%; + padding: var(--space-2) var(--space-3); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + font-size: 13px; + font-family: inherit; + background: var(--card-bg); + color: var(--text-primary); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); + outline: none; +} + +.form-input:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px var(--primary-subtle); +} + +.form-textarea { + resize: vertical; + min-height: 60px; +} + +.form-error { + font-size: 11px; + color: var(--danger); + min-height: 14px; +} + +.create-client-footer { + display: flex; + justify-content: flex-end; + gap: var(--space-3); + padding: var(--space-4) var(--space-5); + border-top: 1px solid var(--border-color); + background: var(--bg-subtle); +} + +.btn-cancel-create { + background: transparent; + border: 1px solid var(--border-color); + color: var(--text-secondary); + border-radius: var(--radius-md); + padding: var(--space-2) var(--space-4); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: border-color var(--transition-fast), color var(--transition-fast); +} + +.btn-cancel-create:hover { + border-color: var(--gray-400); + color: var(--text-primary); + background: transparent; +} + +.btn-submit-create { + display: inline-flex; + align-items: center; + gap: var(--space-2); + background: var(--primary); + color: white; + border: none; + border-radius: var(--radius-md); + padding: var(--space-2) var(--space-5); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background-color var(--transition-fast); + min-width: 120px; + justify-content: center; +} + +.btn-submit-create:hover:not(:disabled) { + background: var(--primary-hover); +} + +.btn-submit-create:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* dark mode — create client modal */ +:root[data-theme='dark'] .form-input { + background: #0b1220; + border-color: #1e2f4d; + color: var(--text-primary); +} + +:root[data-theme='dark'] .form-input:focus { + border-color: var(--primary); +} + +:root[data-theme='dark'] .create-client-footer { + background: rgba(15, 23, 42, 0.8); + border-color: #1e2f4d; +} + +@media (max-width: 580px) { + .create-client-grid { + grid-template-columns: 1fr; + } + .form-field--full { + grid-column: unset; + } +} diff --git a/src/styles/facturas.css b/src/styles/facturas.css index 2ae6e36..17c6882 100644 --- a/src/styles/facturas.css +++ b/src/styles/facturas.css @@ -2,6 +2,84 @@ Facturas + Tablas + Paginacion ============================================ */ +/* ── Checkbox column ── */ +.invoice-select-col { + width: 36px; + padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4) !important; + text-align: center; +} + +.invoice-check, +.invoice-check-all { + width: 15px; + height: 15px; + cursor: pointer; + accent-color: var(--primary); +} + +/* ── Bulk action bar ── */ +.bulk-action-bar { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-2) var(--space-4); + background: var(--blue-600); + color: white; + border-radius: var(--radius-lg); + margin-bottom: var(--space-3); + animation: slideDown 0.15s ease; +} + +@keyframes slideDown { + from { opacity: 0; transform: translateY(-6px); } + to { opacity: 1; transform: translateY(0); } +} + +.bulk-count { + font-size: 13px; + font-weight: 600; + white-space: nowrap; +} + +.bulk-actions { + display: flex; + gap: var(--space-2); + margin-left: auto; + align-items: center; +} + +.btn-bulk { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + border-radius: var(--radius-md); + border: 1px solid rgba(255,255,255,0.3); + background: rgba(255,255,255,0.12); + color: white; + font-size: 13px; + font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: background 0.15s; +} + +.btn-bulk:hover { background: rgba(255,255,255,0.22); } + +.btn-bulk-delete { border-color: rgba(248,113,113,0.5); } +.btn-bulk-delete:hover { background: rgba(239,68,68,0.25); } + +.btn-bulk-cancel { + background: transparent; + border-color: rgba(255,255,255,0.2); + font-size: 12px; +} +.btn-bulk-cancel:hover { background: rgba(255,255,255,0.1); } + +:root[data-theme='dark'] .bulk-action-bar { + background: #1d4ed8; +} + .facturas-page { padding: var(--space-6); max-width: 1400px; @@ -11,6 +89,168 @@ .facturas-header { margin-bottom: var(--space-5); } .facturas-header h1 { font-size: 1.25rem; font-weight: 600; } +/* ── Quarterly toggle button ── */ +.btn-quarterly { + display: inline-flex; + align-items: center; + gap: var(--space-1); + padding: var(--space-2) var(--space-3); + background: var(--card-bg) !important; + color: var(--text-secondary) !important; + border: 1px solid var(--border-color) !important; + border-radius: var(--radius-md); + cursor: pointer; + font-size: 13px; + font-weight: 500; + font-family: inherit; + height: 36px; + transition: all 0.2s ease; + white-space: nowrap; +} + +.btn-quarterly:hover { + background: var(--gray-50) !important; + color: var(--text-primary) !important; + border-color: var(--gray-300) !important; +} + +.btn-quarterly--active { + background: var(--blue-600) !important; + color: white !important; + border-color: var(--blue-600) !important; +} + +.btn-quarterly--active:hover { + background: var(--blue-700) !important; + border-color: var(--blue-700) !important; +} + +:root[data-theme='dark'] .btn-quarterly { + background: #081226 !important; + border-color: #1e2f4d !important; + color: var(--text-secondary) !important; +} + +:root[data-theme='dark'] .btn-quarterly:hover { + background: rgba(30,58,110,0.3) !important; + color: var(--text-primary) !important; +} + +/* ── Quarterly summary view ── */ +.quarterly-view { + margin-bottom: var(--space-4); +} + +.quarterly-year-block { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + overflow: hidden; + margin-bottom: var(--space-4); + box-shadow: var(--shadow-xs); +} + +.quarterly-year-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-3) var(--space-5); + background: var(--gray-50); + border-bottom: 1px solid var(--border-color); +} + +.quarterly-year-label { + font-size: 14px; + font-weight: 700; + color: var(--text-primary); +} + +.quarterly-year-total { + font-size: 12px; + color: var(--text-secondary); + font-variant-numeric: tabular-nums; +} + +.quarterly-table { + width: 100%; + border-collapse: collapse; +} + +.quarterly-table th { + padding: var(--space-2) var(--space-4); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-secondary); + text-align: right; + background: var(--gray-50); + border-bottom: 1px solid var(--border-color); +} + +.quarterly-table th:first-child { text-align: left; } + +.quarterly-table td { + padding: var(--space-3) var(--space-4); + font-size: 13px; + border-bottom: 1px solid var(--border-color); + text-align: right; + font-variant-numeric: tabular-nums; +} + +.quarterly-table tbody tr:last-child td { border-bottom: none; } +.quarterly-table tbody tr:hover { background: var(--gray-50); } + +.quarterly-q-label { + text-align: left !important; + font-weight: 600; + color: var(--text-primary); +} + +.quarterly-q-label span { + font-weight: 400; + color: var(--text-secondary); + font-size: 12px; +} + +.quarterly-count { color: var(--text-secondary); } +.quarterly-num { color: var(--text-primary); } +.quarterly-tax { color: var(--text-secondary); } +.quarterly-total--pos { color: var(--primary); font-weight: 600; } +.quarterly-paid { color: var(--success); font-weight: 500; } +.quarterly-pending { color: var(--danger); font-weight: 500; } +.quarterly-zero { color: var(--text-secondary); } + +.quarterly-footer-row td { + font-weight: 700; + background: var(--gray-50); + border-top: 2px solid var(--border-color); + border-bottom: none; + color: var(--text-primary); +} + +.quarterly-loading, .quarterly-empty { + padding: var(--space-8); + text-align: center; + color: var(--text-secondary); + font-size: 14px; +} + +:root[data-theme='dark'] .quarterly-year-block { + background: #081226; + border-color: #1e2f4d; +} + +:root[data-theme='dark'] .quarterly-year-header, +:root[data-theme='dark'] .quarterly-table th, +:root[data-theme='dark'] .quarterly-footer-row td { + background: rgba(15,23,42,0.95); + border-color: #1e2f4d; +} + +:root[data-theme='dark'] .quarterly-table td { border-color: #1e2f4d; } +:root[data-theme='dark'] .quarterly-table tbody tr:hover { background: rgba(30,58,110,0.2); } + .btn-new-invoice { background: linear-gradient(135deg, var(--blue-600), var(--blue-700)); color: white; @@ -207,8 +447,8 @@ font-variant-numeric: tabular-nums; } -.summary-item--paid .summary-value { color: var(--success); } -.summary-item--pending .summary-value { color: var(--primary); } +.summary-item--paid .summary-value { color: var(--success); } +.summary-item--pending .summary-value { color: var(--danger); } :root[data-theme='dark'] .invoices-summary { background: #081226; @@ -266,11 +506,15 @@ .invoice-total, .invoice-remain { - font-weight: 500; + font-weight: 600; font-variant-numeric: tabular-nums; - color: var(--text-primary); } +/* ── Amount colours (used everywhere: table, summary, quarterly) ── */ +.amount-positive { color: var(--primary); } +.amount-paid { color: var(--success); } +.amount-pending { color: var(--danger); } + .invoice-actions { display: flex; gap: var(--space-1); diff --git a/src/styles/settings.css b/src/styles/settings.css index 5395e01..86359c6 100644 --- a/src/styles/settings.css +++ b/src/styles/settings.css @@ -71,15 +71,20 @@ /* ── Toggle Switch ── */ .settings-theme-toggle { position: relative; - background: none; - border: none; + background: none !important; + border: none !important; padding: 0; cursor: pointer; - outline: none; + outline: none !important; + box-shadow: none !important; +} + +.settings-theme-toggle:hover { + background: none !important; } .settings-theme-toggle:focus-visible { - outline: 2px solid var(--primary); + outline: 2px solid var(--primary) !important; outline-offset: 4px; border-radius: 4px; } diff --git a/src/styles/voice-assistant.css b/src/styles/voice-assistant.css index b456049..fbec685 100644 --- a/src/styles/voice-assistant.css +++ b/src/styles/voice-assistant.css @@ -10,85 +10,91 @@ display: flex; flex-direction: column; align-items: flex-end; - gap: 10px; + gap: 12px; } /* ── FAB ─────────────────────────────────────────────────── */ .va-fab { - width: 50px; - height: 50px; + width: 48px; + height: 48px; border-radius: 50%; - background: var(--primary); + background: var(--primary) !important; color: #fff; - border: none; + border: none !important; cursor: pointer; - box-shadow: 0 4px 14px rgba(37, 99, 235, 0.45); + box-shadow: 0 4px 16px rgba(37, 99, 235, 0.4); display: flex; align-items: center; justify-content: center; - transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast); + transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform 0.15s ease; flex-shrink: 0; + outline: none !important; } .va-fab:hover { - background: var(--primary-hover); - box-shadow: 0 6px 18px rgba(37, 99, 235, 0.55); - transform: scale(1.06); + background: var(--primary-hover) !important; + box-shadow: 0 6px 20px rgba(37, 99, 235, 0.5); + transform: scale(1.07); border-color: transparent; } .va-fab--recording { - background: var(--red-500) !important; - box-shadow: 0 4px 14px rgba(239, 68, 68, 0.45) !important; - animation: va-pulse-fab 1.2s ease-in-out infinite; + background: #ef4444 !important; + box-shadow: 0 4px 16px rgba(239, 68, 68, 0.45) !important; + animation: va-pulse-fab 1.4s ease-in-out infinite; } .va-fab--processing { background: var(--amber-600) !important; - box-shadow: 0 4px 14px rgba(217, 119, 6, 0.4) !important; + box-shadow: 0 4px 16px rgba(217, 119, 6, 0.4) !important; } @keyframes va-pulse-fab { - 0%, 100% { box-shadow: 0 4px 14px rgba(239, 68, 68, 0.45); } - 50% { box-shadow: 0 4px 22px rgba(239, 68, 68, 0.75); transform: scale(1.06); } + 0%, 100% { box-shadow: 0 4px 16px rgba(239, 68, 68, 0.45); } + 50% { box-shadow: 0 4px 28px rgba(239, 68, 68, 0.7); transform: scale(1.08); } } /* ── Panel ───────────────────────────────────────────────── */ .va-panel { background: var(--card-bg); border: 1px solid var(--border-color); - border-radius: 14px; - width: 290px; - box-shadow: var(--shadow-lg); + border-radius: 16px; + width: 300px; + box-shadow: 0 16px 40px rgba(0,0,0,0.12), 0 4px 12px rgba(0,0,0,0.06); overflow: hidden; - animation: va-slide-up 0.18s ease; + animation: va-slide-up 0.2s cubic-bezier(0.34, 1.4, 0.64, 1); } @keyframes va-slide-up { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } + from { opacity: 0; transform: translateY(14px) scale(0.97); } + to { opacity: 1; transform: translateY(0) scale(1); } } +/* ── Header ──────────────────────────────────────────────── */ .va-panel-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px 10px; border-bottom: 1px solid var(--border-color); + background: var(--bg-subtle); } .va-panel-title { display: flex; align-items: center; - gap: 6px; - font-size: 13px; + gap: 7px; + font-size: 11px; font-weight: 600; - color: var(--text-primary); + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.07em; } .va-close-btn { - background: none; - border: none; + background: none !important; + border: none !important; + outline: none !important; cursor: pointer; color: var(--text-secondary); display: flex; @@ -102,7 +108,7 @@ } .va-close-btn:hover { - background: var(--gray-100); + background: var(--gray-200) !important; color: var(--text-primary); border-color: transparent; } @@ -112,41 +118,44 @@ display: flex; flex-direction: column; align-items: center; - padding: 20px 16px 16px; + padding: 22px 16px 16px; gap: 8px; } .va-status-icon { - width: 44px; - height: 44px; + width: 52px; + height: 52px; border-radius: 50%; - background: var(--gray-100); + background: var(--bg-subtle); + border: 2px solid var(--border-color); display: flex; align-items: center; justify-content: center; color: var(--text-secondary); - transition: background-color var(--transition-fast), color var(--transition-fast); + transition: all var(--transition-fast); } .va-status-icon--recording { - background: rgba(239, 68, 68, 0.12); - color: var(--red-500); - animation: va-pulse-icon 1.2s ease-in-out infinite; + background: rgba(239, 68, 68, 0.1); + border-color: rgba(239, 68, 68, 0.4); + color: #ef4444; + animation: va-pulse-icon 1.4s ease-in-out infinite; } .va-status-icon--processing { background: rgba(217, 119, 6, 0.1); + border-color: rgba(217, 119, 6, 0.35); color: var(--amber-600); } @keyframes va-pulse-icon { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.12); } + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.1); } } .va-status-text { margin: 0; - font-size: 13px; + font-size: 14px; font-weight: 600; color: var(--text-primary); text-align: center; @@ -158,12 +167,13 @@ color: var(--text-secondary); text-align: center; min-height: 14px; + line-height: 1.4; } /* ── Resultado ───────────────────────────────────────────── */ .va-result { margin: 0 12px 12px; - background: var(--gray-50); + background: var(--bg-subtle); border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 10px 12px; @@ -176,7 +186,7 @@ font-size: 10px; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.06em; + letter-spacing: 0.07em; color: var(--text-secondary); } @@ -184,31 +194,33 @@ font-size: 13px; font-weight: 500; color: var(--text-primary); + font-style: italic; } .va-result-action { font-size: 12px; - color: var(--green-600); - font-weight: 500; + color: var(--success); + font-weight: 600; } .va-result-action--unknown { color: var(--text-secondary); + font-weight: 500; } /* ── Lista de comandos ───────────────────────────────────── */ .va-commands { border-top: 1px solid var(--border-color); - padding: 10px 0 4px; + padding: 8px 0 6px; } .va-commands-title { - margin: 0 0 6px; + margin: 0 0 4px; padding: 0 14px; font-size: 10px; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.06em; + letter-spacing: 0.07em; color: var(--text-secondary); } @@ -228,13 +240,14 @@ } .va-cmd-item:hover { - background: var(--gray-50); + background: var(--bg-subtle); } .va-cmd-icon { - width: 26px; - height: 26px; + width: 28px; + height: 28px; background: var(--blue-50); + border: 1px solid var(--blue-100); border-radius: var(--radius-md); display: flex; align-items: center; @@ -252,7 +265,7 @@ .va-cmd-label { font-size: 13px; - font-weight: 500; + font-weight: 600; color: var(--text-primary); } @@ -266,13 +279,27 @@ /* ── Dark mode ───────────────────────────────────────────── */ :root[data-theme='dark'] .va-panel { - background: #081226; + background: #0d1b2e; + border-color: #1e2f4d; + box-shadow: 0 16px 40px rgba(0,0,0,0.4), 0 4px 12px rgba(0,0,0,0.2); +} + +:root[data-theme='dark'] .va-panel-header { + background: rgba(15, 23, 42, 0.8); border-color: #1e2f4d; } -:root[data-theme='dark'] .va-close-btn:hover, +:root[data-theme='dark'] .va-status-icon { + background: rgba(148, 163, 184, 0.08); + border-color: #1e2f4d; +} + +:root[data-theme='dark'] .va-close-btn:hover { + background: rgba(30, 47, 77, 0.7) !important; +} + :root[data-theme='dark'] .va-cmd-item:hover { - background: rgba(30, 58, 110, 0.3); + background: rgba(30, 47, 77, 0.5); } :root[data-theme='dark'] .va-result { @@ -280,12 +307,13 @@ border-color: #1e2f4d; } -:root[data-theme='dark'] .va-status-icon { - background: rgba(148, 163, 184, 0.1); +:root[data-theme='dark'] .va-cmd-icon { + background: rgba(37, 99, 235, 0.15); + border-color: rgba(37, 99, 235, 0.25); } -:root[data-theme='dark'] .va-cmd-icon { - background: rgba(30, 58, 110, 0.4); +:root[data-theme='dark'] .va-commands { + border-color: #1e2f4d; } /* ── Móvil ───────────────────────────────────────────────── */ @@ -297,6 +325,6 @@ .va-panel { width: calc(100vw - 32px); - max-width: 290px; + max-width: 300px; } } diff --git a/vite.config.js b/vite.config.js index c7f6054..8d74b46 100644 --- a/vite.config.js +++ b/vite.config.js @@ -4,4 +4,10 @@ export default defineConfig({ optimizeDeps: { exclude: ['@huggingface/transformers'], }, + server: { + watch: { + usePolling: true, + interval: 300, + }, + }, });