feat: módulo banco, acciones masivas facturas, resumen trimestral IVA, colores importes y mejoras UI
This commit is contained in:
parent
696e97080d
commit
f9ad12197a
|
|
@ -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*/`
|
||||
<td class="invoice-select-col">
|
||||
<input type="checkbox" class="invoice-check" value="${invoice.id}" aria-label="Seleccionar factura ${escapeHtml(invoice.number)}" />
|
||||
</td>
|
||||
<td class="invoice-number">
|
||||
<button class="btn-invoice-num" title="Ver detalles">${escapeHtml(invoice.number)}</button>
|
||||
</td>
|
||||
<td class="invoice-status">
|
||||
<span class="badge ${getStatusClass(invoice.status)}">
|
||||
<span class="status-badge ${getStatusClass(invoice.status)}">
|
||||
${escapeHtml(getStatusText(invoice.status))}
|
||||
</span>
|
||||
</td>
|
||||
|
|
@ -59,8 +64,8 @@ export function InvoiceItem(invoice, onView) {
|
|||
${escapeHtml(invoice.clientName || 'Sin nombre')}
|
||||
</td>
|
||||
<td class="invoice-date">${formatDate(invoice.date)}</td>
|
||||
<td class="invoice-total">${formatCurrency(invoice.total)}</td>
|
||||
<td class="invoice-remain">${formatCurrency(invoice.remainToPay)}</td>
|
||||
<td class="invoice-total amount-positive">${formatCurrency(invoice.total)}</td>
|
||||
<td class="invoice-remain ${parseFloat(invoice.remainToPay) > 0.009 ? 'amount-pending' : 'amount-paid'}">${formatCurrency(invoice.remainToPay)}</td>
|
||||
<td class="invoice-actions">
|
||||
<button class="btn-action btn-view" data-invoice-id="${invoice.id}" title="Ver/Editar detalles">
|
||||
${icons.eye}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { getInvoiceById, updateInvoice, validateInvoice, addInvoiceLine, deleteInvoiceLine, addPayment, getPayments, downloadInvoicePdf } from '../services/invoices.js';
|
||||
import { getInvoiceById, updateInvoice, validateInvoice, addInvoiceLine, deleteInvoiceLine, updateInvoiceLine, addPayment, getPayments, downloadInvoicePdf } from '../services/invoices.js';
|
||||
import { showConfirmExitModal, FormChangeTracker } from './ConfirmExitModal.js';
|
||||
import { toast } from '../services/toast.js';
|
||||
import { getPaymentTypes } from '../services/setup.js';
|
||||
import { apiGet } from '../services/apiClient.js';
|
||||
|
||||
export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
||||
const modal = document.createElement('div');
|
||||
|
|
@ -8,6 +10,8 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
|
||||
let invoice = null;
|
||||
let payments = [];
|
||||
let paymentTypes = [];
|
||||
let bankAccounts = [];
|
||||
let isLoading = true;
|
||||
|
||||
// Tracker de cambios
|
||||
|
|
@ -220,8 +224,11 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
<td>${formatCurrency(line.total)}</td>
|
||||
${isDraft ? `
|
||||
<td class="line-actions">
|
||||
<button type="button" class="btn-edit-line" data-line-id="${line.id}" title="Editar línea">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||
</button>
|
||||
<button type="button" class="btn-delete-line" data-line-id="${line.id}" title="Eliminar línea">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
|
|
@ -290,8 +297,8 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Cantidad a pagar (€)</label>
|
||||
<input type="number" id="payment-amount"
|
||||
min="0" max="${invoice.remainToPay}" step="0.01"
|
||||
<input type="number" id="payment-amount"
|
||||
min="0" max="${invoice.remainToPay}" step="0.01"
|
||||
placeholder="Dejar vacío o 0 = pago total (${invoice.remainToPay?.toFixed(2)} €)" />
|
||||
<small class="payment-hint">Máximo: ${formatCurrency(invoice.remainToPay)}. Vacío o 0 = pago completo.</small>
|
||||
</div>
|
||||
|
|
@ -300,6 +307,24 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
<input type="date" id="payment-date" value="${new Date().toISOString().split('T')[0]}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Método de pago</label>
|
||||
<select id="payment-method">
|
||||
${paymentTypes.length
|
||||
? paymentTypes.map(t => `<option value="${t.id}">${t.label || t.code}</option>`).join('')
|
||||
: '<option value="4">Tarjeta bancaria</option>'}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Cuenta bancaria</label>
|
||||
<select id="payment-account">
|
||||
${bankAccounts.length
|
||||
? bankAccounts.map(a => `<option value="${a.id}">${a.label}${a.iban ? ' · ' + a.iban.slice(-4) : ''}</option>`).join('')
|
||||
: '<option value="1">Cuenta principal</option>'}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Nº Referencia (opcional)</label>
|
||||
|
|
@ -384,13 +409,14 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
if (form) form.style.display = 'none';
|
||||
});
|
||||
|
||||
// Botones editar línea
|
||||
modal.querySelectorAll('.btn-edit-line').forEach(btn => {
|
||||
btn.addEventListener('click', () => handleEditLine(btn.dataset.lineId));
|
||||
});
|
||||
|
||||
// Botones eliminar línea
|
||||
const deleteLineBtns = modal.querySelectorAll('.btn-delete-line');
|
||||
deleteLineBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const lineId = parseInt(btn.dataset.lineId);
|
||||
handleDeleteLine(lineId);
|
||||
});
|
||||
modal.querySelectorAll('.btn-delete-line').forEach(btn => {
|
||||
btn.addEventListener('click', () => handleDeleteLine(parseInt(btn.dataset.lineId)));
|
||||
});
|
||||
|
||||
// Botón registrar pago
|
||||
|
|
@ -688,6 +714,56 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
|
|||
}
|
||||
};
|
||||
|
||||
// Editar línea de factura inline
|
||||
const handleEditLine = (lineId) => {
|
||||
const line = invoice.lines?.find(l => String(l.id) === String(lineId));
|
||||
if (!line) return;
|
||||
const row = modal.querySelector(`tr[data-line-id="${lineId}"]`);
|
||||
if (!row) return;
|
||||
|
||||
row.innerHTML = `
|
||||
<td><input class="line-edit-input line-edit-desc" type="text" value="${(line.description || '').replace(/"/g, '"')}" /></td>
|
||||
<td><input class="line-edit-input line-edit-qty" type="number" value="${line.quantity ?? 1}" min="0.01" step="0.01" style="width:70px"/></td>
|
||||
<td><input class="line-edit-input line-edit-price" type="number" value="${line.unitPrice ?? 0}" min="0" step="0.01" style="width:90px"/></td>
|
||||
<td><input class="line-edit-input line-edit-tax" type="number" value="${line.taxRate ?? 21}" min="0" max="100" step="0.01" style="width:60px"/></td>
|
||||
<td>—</td>
|
||||
<td class="line-actions">
|
||||
<button type="button" class="btn-save-line-edit btn-small btn-success" title="Guardar">✓</button>
|
||||
<button type="button" class="btn-cancel-line-edit btn-small" title="Cancelar">✕</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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*/`
|
||||
<div class="banco-header">
|
||||
<h1>Banco</h1>
|
||||
<p class="banco-subtitle">Gestión de cuentas bancarias y movimientos</p>
|
||||
</div>
|
||||
|
||||
<div class="banco-summary-grid">
|
||||
<div class="banco-stat-card banco-stat-card--balance">
|
||||
<div class="banco-stat-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="banco-stat-body">
|
||||
<span class="banco-stat-label">Saldo total</span>
|
||||
<span class="banco-stat-value" id="stat-balance">—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banco-stat-card banco-stat-card--income">
|
||||
<div class="banco-stat-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/>
|
||||
<polyline points="17 6 23 6 23 12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="banco-stat-body">
|
||||
<span class="banco-stat-label">Ingresos</span>
|
||||
<span class="banco-stat-value" id="stat-income">—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banco-stat-card banco-stat-card--expense">
|
||||
<div class="banco-stat-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="23 18 13.5 8.5 8.5 13.5 1 6"/>
|
||||
<polyline points="17 18 23 18 23 12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="banco-stat-body">
|
||||
<span class="banco-stat-label">Gastos</span>
|
||||
<span class="banco-stat-value" id="stat-expense">—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banco-stat-card banco-stat-card--accounts">
|
||||
<div class="banco-stat-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
<polyline points="9 22 9 12 15 12 15 22"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="banco-stat-body">
|
||||
<span class="banco-stat-label">Cuentas</span>
|
||||
<span class="banco-stat-value" id="stat-accounts">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banco-content-grid">
|
||||
<div class="banco-card banco-card--accounts">
|
||||
<div class="banco-card-header">
|
||||
<h2>Cuentas bancarias</h2>
|
||||
<button class="btn-banco-new" id="btn-new-account">+ Nueva cuenta</button>
|
||||
</div>
|
||||
<div class="banco-accounts-list" id="banco-accounts">
|
||||
<div class="banco-loading">
|
||||
<div class="banco-skeleton"></div>
|
||||
<div class="banco-skeleton"></div>
|
||||
<div class="banco-skeleton"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banco-card banco-card--transactions">
|
||||
<div class="banco-card-header">
|
||||
<h2 id="tx-title">Últimos movimientos</h2>
|
||||
<button class="btn-banco-back" id="btn-all-movements" style="display:none">
|
||||
← Todos
|
||||
</button>
|
||||
</div>
|
||||
<div class="banco-transactions-list" id="banco-transactions">
|
||||
<div class="banco-loading">
|
||||
<div class="banco-skeleton"></div>
|
||||
<div class="banco-skeleton"></div>
|
||||
<div class="banco-skeleton"></div>
|
||||
<div class="banco-skeleton"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let allMovements = [];
|
||||
let selectedAccountId = null;
|
||||
|
||||
// ── Render helpers ────────────────────────────────────────────────
|
||||
|
||||
function renderAccounts(list) {
|
||||
const el = container.querySelector('#banco-accounts');
|
||||
if (!list || list.length === 0) {
|
||||
el.innerHTML = `<div class="banco-empty">
|
||||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" style="color:var(--gray-300)">
|
||||
<rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/>
|
||||
</svg>
|
||||
<p>No hay cuentas bancarias registradas</p>
|
||||
</div>`;
|
||||
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 = `
|
||||
<div class="banco-account-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="banco-account-info">
|
||||
<span class="banco-account-name">${acc.label || acc.ref || 'Cuenta sin nombre'}</span>
|
||||
<span class="banco-account-sub">${acc.iban || acc.accountNumber || acc.ref || '—'}</span>
|
||||
</div>
|
||||
<div class="banco-account-right">
|
||||
<span class="banco-account-balance ${parseFloat(acc.balance) < 0 ? 'banco-account-balance--negative' : ''}">${fmt(acc.balance)}</span>
|
||||
<button class="btn-account-edit" title="Editar cuenta" aria-label="Editar ${acc.label}">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="pointer-events:none">
|
||||
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="banco-account-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</div>
|
||||
`;
|
||||
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 = `<div class="banco-empty"><p>No hay movimientos${subtitle ? ' para esta cuenta' : ' recientes'}</p></div>`;
|
||||
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 `
|
||||
<div class="banco-tx-item">
|
||||
<div class="banco-tx-indicator ${amount >= 0 ? 'banco-tx-indicator--credit' : 'banco-tx-indicator--debit'}"></div>
|
||||
<div class="banco-tx-info">
|
||||
<span class="banco-tx-label">${cleanLabel(tx.label)}</span>
|
||||
<span class="banco-tx-date">${fmtDate(tx.dateValue || tx.date)}</span>
|
||||
</div>
|
||||
<span class="banco-tx-amount ${amount >= 0 ? 'banco-tx-amount--credit' : 'banco-tx-amount--debit'}">
|
||||
${amount >= 0 ? '+' : ''}${fmt(amount)}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}).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 = `<div class="banco-empty banco-empty--soon">
|
||||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" style="color:var(--gray-300)">
|
||||
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>
|
||||
<p>Módulo bancario no disponible aún</p>
|
||||
<span class="banco-soon-tag">Próximamente</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── 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 = `<div class="banco-loading"><div class="banco-skeleton"></div><div class="banco-skeleton"></div><div class="banco-skeleton"></div></div>`;
|
||||
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 = `<div class="banco-empty"><p>No se pudieron cargar los movimientos</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
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*/`
|
||||
<div class="banco-modal">
|
||||
<div class="banco-modal-header">
|
||||
<div class="banco-modal-title-group">
|
||||
<div class="banco-modal-icon banco-modal-icon--edit">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Editar cuenta bancaria</h2>
|
||||
<p class="banco-modal-subtitle">${acc.label || acc.ref || 'Cuenta sin nombre'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="banco-modal-close" type="button" aria-label="Cerrar">✕</button>
|
||||
</div>
|
||||
|
||||
<form class="banco-modal-form" id="edit-account-form" novalidate>
|
||||
<div class="banco-form-section">
|
||||
<span class="banco-form-section-label">Identificación</span>
|
||||
<div class="banco-form-grid">
|
||||
<div class="banco-form-field banco-form-field--full">
|
||||
<label class="banco-form-label" for="edit-label">Nombre de la cuenta</label>
|
||||
<input type="text" class="banco-form-input" id="edit-label" value="${acc.label || ''}" autocomplete="off" />
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="edit-ref">Código / Ref</label>
|
||||
<input type="text" class="banco-form-input" id="edit-ref" value="${acc.ref || ''}" autocomplete="off" />
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="edit-type">Tipo de cuenta</label>
|
||||
<select class="banco-form-input" id="edit-type">
|
||||
<option value="1">Cuenta corriente</option>
|
||||
<option value="0">Cuenta de ahorro</option>
|
||||
<option value="2">Caja</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="edit-currency">Divisa</label>
|
||||
<input type="text" class="banco-form-input" id="edit-currency" value="${acc.currencyCode || 'EUR'}" maxlength="3" autocomplete="off" />
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="edit-country">País</label>
|
||||
<select class="banco-form-input" id="edit-country">
|
||||
<option value="">Cargando...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banco-form-section">
|
||||
<span class="banco-form-section-label">Datos bancarios <span class="banco-form-section-optional">(opcional)</span></span>
|
||||
<div class="banco-form-grid">
|
||||
<div class="banco-form-field banco-form-field--full">
|
||||
<label class="banco-form-label" for="edit-bank">Entidad bancaria</label>
|
||||
<input type="text" class="banco-form-input" id="edit-bank" value="${acc.bank || ''}" autocomplete="off" />
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="edit-number">Número de cuenta</label>
|
||||
<input type="text" class="banco-form-input" id="edit-number" value="${acc.accountNumber || ''}" autocomplete="off" />
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="edit-iban">IBAN</label>
|
||||
<input type="text" class="banco-form-input" id="edit-iban" value="${acc.iban || ''}" autocomplete="off" />
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="edit-bic">BIC / SWIFT</label>
|
||||
<input type="text" class="banco-form-input" id="edit-bic" value="${acc.bic || ''}" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banco-modal-footer">
|
||||
<button type="button" class="btn-banco-cancel" id="cancel-edit">Cancelar</button>
|
||||
<button type="submit" class="btn-banco-submit">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
Guardar cambios
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = '<option value="">Sin especificar</option>';
|
||||
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 = `
|
||||
<option value="">Sin especificar</option>
|
||||
<option value="4">España</option>
|
||||
<option value="1">Francia</option>
|
||||
<option value="5">Alemania</option>
|
||||
<option value="25">Portugal</option>
|
||||
<option value="7">Reino Unido</option>
|
||||
<option value="11">Estados Unidos</option>`;
|
||||
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 = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="animation:spin 0.8s linear infinite"><circle cx="12" cy="12" r="10" stroke-opacity="0.25"/><path d="M12 2a10 10 0 0 1 10 10"/></svg> 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 = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> Guardar cambios';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Create account modal ──────────────────────────────────────────
|
||||
|
||||
function showCreateAccountModal() {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'banco-modal-overlay';
|
||||
overlay.innerHTML = /*html*/`
|
||||
<div class="banco-modal">
|
||||
<div class="banco-modal-header">
|
||||
<div class="banco-modal-title-group">
|
||||
<div class="banco-modal-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Nueva cuenta bancaria</h2>
|
||||
<p class="banco-modal-subtitle">Los campos marcados con * son obligatorios</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="banco-modal-close" type="button" aria-label="Cerrar">✕</button>
|
||||
</div>
|
||||
|
||||
<form class="banco-modal-form" id="create-account-form" novalidate>
|
||||
|
||||
<div class="banco-form-section">
|
||||
<span class="banco-form-section-label">Identificación</span>
|
||||
<div class="banco-form-grid">
|
||||
<div class="banco-form-field banco-form-field--full">
|
||||
<label class="banco-form-label" for="acc-label">Nombre de la cuenta <span class="required">*</span></label>
|
||||
<input type="text" class="banco-form-input" id="acc-label" placeholder="Ej: Cuenta corriente principal" autocomplete="off" required />
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="acc-ref">Código / Ref <span class="required">*</span></label>
|
||||
<input type="text" class="banco-form-input" id="acc-ref" placeholder="Ej: CAJA-01" autocomplete="off" required />
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="acc-type">Tipo de cuenta <span class="required">*</span></label>
|
||||
<select class="banco-form-input" id="acc-type">
|
||||
<option value="1">Cuenta corriente</option>
|
||||
<option value="0">Cuenta de ahorro</option>
|
||||
<option value="2">Caja</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="acc-currency">Divisa <span class="required">*</span></label>
|
||||
<input type="text" class="banco-form-input" id="acc-currency" value="EUR" maxlength="3" autocomplete="off" required />
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="acc-country">País <span class="required">*</span></label>
|
||||
<select class="banco-form-input" id="acc-country">
|
||||
<option value="">Cargando países...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banco-form-section">
|
||||
<span class="banco-form-section-label">Datos bancarios <span class="banco-form-section-optional">(opcional)</span></span>
|
||||
<div class="banco-form-grid">
|
||||
<div class="banco-form-field banco-form-field--full">
|
||||
<label class="banco-form-label" for="acc-bank">Entidad bancaria</label>
|
||||
<input type="text" class="banco-form-input" id="acc-bank" placeholder="Nombre del banco" autocomplete="off" />
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="acc-number">Número de cuenta</label>
|
||||
<input type="text" class="banco-form-input" id="acc-number" placeholder="ES12 3456 7890..." autocomplete="off" />
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="acc-iban">IBAN</label>
|
||||
<input type="text" class="banco-form-input" id="acc-iban" placeholder="ES00 0000 0000..." autocomplete="off" />
|
||||
</div>
|
||||
<div class="banco-form-field">
|
||||
<label class="banco-form-label" for="acc-bic">BIC / SWIFT</label>
|
||||
<input type="text" class="banco-form-input" id="acc-bic" placeholder="CAIXESBBXXX" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banco-modal-footer">
|
||||
<button type="button" class="btn-banco-cancel" id="cancel-account">Cancelar</button>
|
||||
<button type="submit" class="btn-banco-submit">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
Crear cuenta
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = '<option value="">Seleccionar país...</option>';
|
||||
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 = `
|
||||
<option value="">Seleccionar país...</option>
|
||||
<option value="4" selected>España</option>
|
||||
<option value="1">Francia</option>
|
||||
<option value="5">Alemania</option>
|
||||
<option value="25">Portugal</option>
|
||||
<option value="7">Reino Unido</option>
|
||||
<option value="11">Estados Unidos</option>`;
|
||||
}
|
||||
})();
|
||||
|
||||
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 = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="animation:spin 0.8s linear infinite"><circle cx="12" cy="12" r="10" stroke-opacity="0.25"/><path d="M12 2a10 10 0 0 1 10 10"/></svg> 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 = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> Crear cuenta';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
container.querySelector('#btn-new-account').addEventListener('click', showCreateAccountModal);
|
||||
|
||||
loadData();
|
||||
return container;
|
||||
}
|
||||
|
|
@ -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() {
|
|||
<div class="clientes-header">
|
||||
<h1>Clientes</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="clientes-filters">
|
||||
<div class="search-wrapper">
|
||||
<span class="search-icon">${icons.search}</span>
|
||||
<input type="search" placeholder="Buscar clientes..." class="search-input search-input--with-icon" />
|
||||
</div>
|
||||
<button class="btn-new-client">
|
||||
${icons.plus} Nuevo cliente
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clients-table-container">
|
||||
|
|
@ -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*/`
|
||||
<div class="client-modal create-client-modal">
|
||||
<div class="client-modal-header">
|
||||
<h2>Nuevo cliente</h2>
|
||||
<button class="btn-close-modal" type="button">${icons.close}</button>
|
||||
</div>
|
||||
<form class="create-client-form" novalidate>
|
||||
<div class="client-modal-body">
|
||||
<div class="create-client-section">
|
||||
<h3>Información básica</h3>
|
||||
<div class="create-client-grid">
|
||||
<div class="form-field form-field--full">
|
||||
<label class="form-label" for="cc-name">Nombre <span class="form-required">*</span></label>
|
||||
<input id="cc-name" name="name" type="text" class="form-input" placeholder="Nombre o razón social" required maxlength="200" />
|
||||
<span class="form-error" id="cc-name-error"></span>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="cc-email">Email</label>
|
||||
<input id="cc-email" name="email" type="email" class="form-input" placeholder="empresa@ejemplo.com" maxlength="200" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="cc-phone">Teléfono</label>
|
||||
<input id="cc-phone" name="phone" type="tel" class="form-input" placeholder="+34 600 000 000" maxlength="20" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="cc-vat">NIF / CIF / VAT</label>
|
||||
<input id="cc-vat" name="vatNumber" type="text" class="form-input" placeholder="B12345678" maxlength="50" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="cc-country">País (código)</label>
|
||||
<input id="cc-country" name="countryCode" type="text" class="form-input" placeholder="ES" maxlength="50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="create-client-section">
|
||||
<h3>Dirección</h3>
|
||||
<div class="create-client-grid">
|
||||
<div class="form-field form-field--full">
|
||||
<label class="form-label" for="cc-address">Dirección</label>
|
||||
<input id="cc-address" name="address" type="text" class="form-input" placeholder="Calle, número, piso..." maxlength="50" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="cc-zip">Código postal</label>
|
||||
<input id="cc-zip" name="zip" type="text" class="form-input" placeholder="46001" maxlength="50" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="cc-town">Ciudad</label>
|
||||
<input id="cc-town" name="town" type="text" class="form-input" placeholder="Valencia" maxlength="50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="create-client-section">
|
||||
<h3>Notas</h3>
|
||||
<div class="create-client-grid">
|
||||
<div class="form-field form-field--full">
|
||||
<label class="form-label" for="cc-note-pub">Nota pública</label>
|
||||
<textarea id="cc-note-pub" name="notePublic" class="form-input form-textarea" placeholder="Visible para el cliente..." maxlength="500" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="form-field form-field--full">
|
||||
<label class="form-label" for="cc-note-priv">Nota privada</label>
|
||||
<textarea id="cc-note-priv" name="notePrivate" class="form-input form-textarea" placeholder="Solo visible internamente..." maxlength="500" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="create-client-footer">
|
||||
<button type="button" class="btn-cancel-create">Cancelar</button>
|
||||
<button type="submit" class="btn-submit-create">
|
||||
<span class="btn-submit-text">Crear cliente</span>
|
||||
<span class="btn-submit-spinner" hidden>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="animation:spin .7s linear infinite;display:block"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field-compact full-width">
|
||||
<label>Condiciones de pago</label>
|
||||
<select id="paymentTerms" name="paymentTerms">
|
||||
<option value="">Sin condiciones específicas</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes-grid-compact">
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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*/`
|
||||
<div class="facturas-header">
|
||||
|
|
@ -33,13 +35,41 @@ export function renderFacturasPage() {
|
|||
<option value="unpaid">Pte. Pago</option>
|
||||
<option value="paid">Pagada</option>
|
||||
</select>
|
||||
<button class="btn-quarterly" id="btn-quarterly">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
|
||||
Resumen trimestral
|
||||
</button>
|
||||
<button class="btn-new-invoice">${icons.plus} <span>Nueva Factura</span></button>
|
||||
</div>
|
||||
|
||||
<div class="quarterly-view" id="quarterly-view" style="display:none">
|
||||
<div class="quarterly-table-wrap" id="quarterly-content">
|
||||
<p class="quarterly-loading">Calculando...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bulk-action-bar" id="bulk-bar" style="display:none">
|
||||
<span class="bulk-count" id="bulk-count">0 seleccionadas</span>
|
||||
<div class="bulk-actions">
|
||||
<button class="btn-bulk btn-bulk-duplicate" id="btn-duplicate">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
Duplicar
|
||||
</button>
|
||||
<button class="btn-bulk btn-bulk-delete" id="btn-bulk-delete">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||||
Eliminar
|
||||
</button>
|
||||
<button class="btn-bulk btn-bulk-cancel" id="btn-deselect">✕ Cancelar selección</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="invoices-table-container">
|
||||
<table class="invoices-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="invoice-select-col">
|
||||
<input type="checkbox" class="invoice-check-all" id="check-all" title="Seleccionar todo" />
|
||||
</th>
|
||||
<th class="invoice-number sortable" data-sort="number">
|
||||
Número <span class="sort-icon"></span>
|
||||
</th>
|
||||
|
|
@ -62,7 +92,7 @@ export function renderFacturasPage() {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody class="invoices-list">
|
||||
<tr><td colspan="7" class="loading">Cargando facturas...</td></tr>
|
||||
<tr><td colspan="8" class="loading">Cargando facturas...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -165,14 +195,20 @@ export function renderFacturasPage() {
|
|||
invoicesList.innerHTML = '';
|
||||
|
||||
if (invoices.length === 0) {
|
||||
invoicesList.innerHTML = `<tr><td colspan="7"><div class="empty-state">${icons.emptyInvoices}<h3>No hay facturas</h3><p>Crea tu primera factura para comenzar.</p></div></td></tr>`;
|
||||
invoicesList.innerHTML = `<tr><td colspan="8"><div class="empty-state">${icons.emptyInvoices}<h3>No hay facturas</h3><p>Crea tu primera factura para comenzar.</p></div></td></tr>`;
|
||||
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 = '<tr><td colspan="7" class="loading">Cargando facturas...</td></tr>';
|
||||
invoicesList.innerHTML = '<tr><td colspan="8" class="loading">Cargando facturas...</td></tr>';
|
||||
|
||||
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 = `<tr><td colspan="7"><div class="empty-state">${icons.error}<h3>Error al cargar</h3><p>No se pudieron cargar las facturas. Inténtalo de nuevo.</p></div></td></tr>`;
|
||||
invoicesList.innerHTML = `<tr><td colspan="8"><div class="empty-state">${icons.error}<h3>Error al cargar</h3><p>No se pudieron cargar las facturas. Inténtalo de nuevo.</p></div></td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg> 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 = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> 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 = '<p class="quarterly-empty">No hay facturas para mostrar</p>';
|
||||
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 += `
|
||||
<div class="quarterly-year-block">
|
||||
<div class="quarterly-year-header">
|
||||
<span class="quarterly-year-label">${year}</span>
|
||||
<span class="quarterly-year-total">${fmt(totals.total)} · ${totals.count} facturas</span>
|
||||
</div>
|
||||
<table class="quarterly-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Trimestre</th>
|
||||
<th>Facturas</th>
|
||||
<th>Base imponible</th>
|
||||
<th>IVA</th>
|
||||
<th>Total</th>
|
||||
<th>Cobrado</th>
|
||||
<th>Pendiente</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${yearRows.map(r => {
|
||||
const pending = r.total - r.paid;
|
||||
return `<tr>
|
||||
<td class="quarterly-q-label">T${r.q} · <span>${qLabel(r.q)}</span></td>
|
||||
<td class="quarterly-count">${r.count}</td>
|
||||
<td class="quarterly-num">${fmt(r.ht)}</td>
|
||||
<td class="quarterly-num quarterly-tax">${fmt(r.tax)}</td>
|
||||
<td class="quarterly-num ${r.total > 0 ? 'quarterly-total--pos' : ''}">${fmt(r.total)}</td>
|
||||
<td class="quarterly-num ${r.paid > 0 ? 'quarterly-paid' : 'quarterly-zero'}">${fmt(r.paid)}</td>
|
||||
<td class="quarterly-num ${pending > 0.01 ? 'quarterly-pending' : 'quarterly-zero'}">${fmt(pending)}</td>
|
||||
</tr>`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="quarterly-footer-row">
|
||||
<td>Total ${year}</td>
|
||||
<td>${totals.count}</td>
|
||||
<td>${fmt(totals.ht)}</td>
|
||||
<td>${fmt(totals.tax)}</td>
|
||||
<td class="${totals.total > 0 ? 'quarterly-total--pos' : ''}">${fmt(totals.total)}</td>
|
||||
<td class="${totals.paid > 0 ? 'quarterly-paid' : 'quarterly-zero'}">${fmt(totals.paid)}</td>
|
||||
<td class="${totals.total - totals.paid > 0.01 ? 'quarterly-pending' : 'quarterly-zero'}">${fmt(totals.total - totals.paid)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
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 = `<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> Cerrar resumen`;
|
||||
renderQuarterlySummary();
|
||||
} else {
|
||||
view.style.display = 'none';
|
||||
table.style.display = '';
|
||||
btn.classList.remove('btn-quarterly--active');
|
||||
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> Resumen trimestral`;
|
||||
updatePaginationUI();
|
||||
updateSummary(filteredInvoices);
|
||||
}
|
||||
}
|
||||
|
||||
container.querySelector('#btn-quarterly').addEventListener('click', toggleQuarterly);
|
||||
|
||||
container.querySelector('.btn-new-invoice').addEventListener('click', () => {
|
||||
window.location.hash = '#create-invoice';
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,5 +95,7 @@ export const icons = {
|
|||
|
||||
moon: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>`,
|
||||
|
||||
info: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`
|
||||
info: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
|
||||
|
||||
bank: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></svg>`
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,4 +4,10 @@ export default defineConfig({
|
|||
optimizeDeps: {
|
||||
exclude: ['@huggingface/transformers'],
|
||||
},
|
||||
server: {
|
||||
watch: {
|
||||
usePolling: true,
|
||||
interval: 300,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue