Merge pull request #3 from JavMB/Rama_Alex

Rama alex
This commit is contained in:
Alex 2026-02-02 18:00:24 +01:00 committed by GitHub
commit 810dbd7781
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1212 additions and 11 deletions

0
.env Normal file → Executable file
View File

0
.gitignore vendored Normal file → Executable file
View File

0
index.html Normal file → Executable file
View File

0
package-lock.json generated Normal file → Executable file
View File

0
package.json Normal file → Executable file
View File

0
public/vite.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,76 @@
export function InvoiceItem(invoice) {
const item = document.createElement('div');
item.className = 'invoice-item';
// Formatear fecha
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString('es-ES');
};
// Formatear moneda
const formatCurrency = (amount) => {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: 'EUR'
}).format(amount);
};
// Obtener clase de estado
const getStatusClass = (status) => {
const statusMap = {
'draft': 'status-draft',
'validated': 'status-validated',
'paid': 'status-paid',
'unpaid': 'status-unpaid',
'canceled': 'status-canceled'
};
return statusMap[status] || 'status-default';
};
// Obtener texto de estado
const getStatusText = (status) => {
const statusTextMap = {
'draft': 'Borrador',
'validated': 'Validada',
'paid': 'Pagada',
'unpaid': 'Impagada',
'canceled': 'Cancelada'
};
return statusTextMap[status] || status;
};
item.innerHTML = /*html*/`
<div class="invoice-checkbox">
<input type="checkbox" id="invoice-${invoice.id}" />
</div>
<div class="invoice-number">${invoice.number}</div>
<div class="invoice-status">
<span class="status-badge ${getStatusClass(invoice.status)}">
${getStatusText(invoice.status)}
</span>
</div>
<div class="invoice-client">
<span class="client-icon">👤</span>
${invoice.clientName || 'Sin nombre'}
</div>
<div class="invoice-date">${formatDate(invoice.date)}</div>
<div class="invoice-total">${formatCurrency(invoice.total)}</div>
<div class="invoice-remain">${formatCurrency(invoice.remainToPay)}</div>
<div class="invoice-actions">
<button class="btn-view" data-invoice-id="${invoice.id}" title="Ver detalles">
👁️
</button>
</div>
`;
// Event listener para el botón de ver
const viewBtn = item.querySelector('.btn-view');
viewBtn.addEventListener('click', () => {
console.log('Ver factura:', invoice.id);
// Aquí puedes agregar la lógica para ver los detalles
alert(`Ver detalles de factura: ${invoice.number}`);
});
return item;
}

0
src/components/Login.js Normal file → Executable file
View File

0
src/components/Sidebar.js Normal file → Executable file
View File

0
src/counter.js Normal file → Executable file
View File

0
src/javascript.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 995 B

After

Width:  |  Height:  |  Size: 995 B

0
src/main.js Normal file → Executable file
View File

View File

@ -0,0 +1,297 @@
export function renderCreateInvoicePage() {
const container = document.createElement('div');
container.className = 'create-invoice-page';
container.innerHTML = /*html*/`
<div class="create-invoice-header">
<div>
<h1>Nuevo Borrador</h1>
<p class="subtitle">Crear un nuevo borrador</p>
</div>
<button class="btn-back" id="back-btn">← Volver a Facturas</button>
</div>
<form class="invoice-form" id="invoice-form">
<div class="form-section">
<h2>Información General</h2>
<div class="form-row">
<div class="form-group">
<label for="clientId">Cliente ID *</label>
<input type="number" id="clientId" name="clientId" required min="1" placeholder="ID del cliente" />
</div>
<div class="form-group">
<label for="reference">Referencia</label>
<input type="text" id="reference" name="reference" placeholder="Ej: FAC-2026-001" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="date">Fecha *</label>
<input type="date" id="date" name="date" required />
</div>
<div class="form-group">
<label for="expireDate">Fecha de vencimiento *</label>
<input type="date" id="expireDate" name="expireDate" required />
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label for="notePublic">Nota pública</label>
<textarea id="notePublic" name="notePublic" rows="3" placeholder="Nota visible para el cliente"></textarea>
</div>
</div>
<div class="form-row">
<div class="form-group full-width">
<label for="notePrivate">Nota privada</label>
<textarea id="notePrivate" name="notePrivate" rows="3" placeholder="Nota interna"></textarea>
</div>
</div>
</div>
<div class="form-section">
<div class="section-header">
<h2>Líneas de Borrador</h2>
<button type="button" class="btn-add-line" id="add-line-btn">+ Añadir Línea</button>
</div>
<div id="invoice-lines-container">
<!-- Las líneas se agregarán dinámicamente aquí -->
</div>
</div>
<div class="form-actions">
<button type="button" class="btn-cancel" id="cancel-btn">Cancelar</button>
<button type="submit" class="btn-submit">Crear Borrador</button>
</div>
</form>
`;
// Establecer fecha de hoy por defecto
const today = new Date().toISOString().split('T')[0];
container.querySelector('#date').value = today;
// Establecer fecha de vencimiento (30 días después)
const expireDate = new Date();
expireDate.setDate(expireDate.getDate() + 30);
container.querySelector('#expireDate').value = expireDate.toISOString().split('T')[0];
// Contador de líneas
let lineCounter = 0;
// Función para crear una nueva línea de borrador
function createInvoiceLine() {
lineCounter++;
const lineDiv = document.createElement('div');
lineDiv.className = 'invoice-line';
lineDiv.dataset.lineId = lineCounter;
lineDiv.innerHTML = /*html*/`
<div class="line-header">
<span class="line-number">Línea ${lineCounter}</span>
<button type="button" class="btn-remove-line" data-line-id="${lineCounter}">🗑️ Eliminar</button>
</div>
<div class="line-content">
<div class="form-group">
<label>Descripción *</label>
<input type="text" class="line-description" required placeholder="Descripción del producto/servicio" />
</div>
<div class="form-row-inline">
<div class="form-group">
<label>Cantidad *</label>
<input type="number" class="line-quantity" required min="0.01" step="0.01" value="1" />
</div>
<div class="form-group">
<label>Precio unitario (€) *</label>
<input type="number" class="line-unitPrice" required min="0" step="0.01" placeholder="0.00" />
</div>
<div class="form-group">
<label>IVA (%) *</label>
<input type="number" class="line-taxRate" required min="0" max="100" step="0.01" value="21" />
</div>
<div class="form-group">
<label>Subtotal</label>
<input type="text" class="line-subtotal" readonly value="0.00 €" />
</div>
</div>
</div>
`;
// Event listeners para calcular subtotal
const quantityInput = lineDiv.querySelector('.line-quantity');
const priceInput = lineDiv.querySelector('.line-unitPrice');
const subtotalInput = lineDiv.querySelector('.line-subtotal');
function updateSubtotal() {
const quantity = parseFloat(quantityInput.value) || 0;
const price = parseFloat(priceInput.value) || 0;
const subtotal = quantity * price;
subtotalInput.value = `${subtotal.toFixed(2)} €`;
updateTotal();
}
quantityInput.addEventListener('input', updateSubtotal);
priceInput.addEventListener('input', updateSubtotal);
// Event listener para eliminar línea
lineDiv.querySelector('.btn-remove-line').addEventListener('click', () => {
lineDiv.remove();
updateLineNumbers();
updateTotal();
});
return lineDiv;
}
// Función para actualizar números de línea
function updateLineNumbers() {
const lines = container.querySelectorAll('.invoice-line');
lines.forEach((line, index) => {
line.querySelector('.line-number').textContent = `Línea ${index + 1}`;
});
}
// Función para actualizar el total
function updateTotal() {
const lines = container.querySelectorAll('.invoice-line');
let total = 0;
lines.forEach(line => {
const quantity = parseFloat(line.querySelector('.line-quantity').value) || 0;
const price = parseFloat(line.querySelector('.line-unitPrice').value) || 0;
const taxRate = parseFloat(line.querySelector('.line-taxRate').value) || 0;
const subtotal = quantity * price;
const withTax = subtotal * (1 + taxRate / 100);
total += withTax;
});
// Mostrar total en algún lugar si lo deseas
console.log('Total borrador:', total.toFixed(2));
}
// Agregar primera línea por defecto
const linesContainer = container.querySelector('#invoice-lines-container');
linesContainer.appendChild(createInvoiceLine());
// Event listener para añadir línea
container.querySelector('#add-line-btn').addEventListener('click', () => {
linesContainer.appendChild(createInvoiceLine());
});
// Event listener para volver
container.querySelector('#back-btn').addEventListener('click', () => {
window.location.hash = '#invoices';
});
// Event listener para cancelar
container.querySelector('#cancel-btn').addEventListener('click', () => {
if (confirm('¿Estás seguro de que quieres cancelar? Se perderán los datos no guardados.')) {
window.location.hash = '#invoices';
}
});
// Event listener para enviar formulario
container.querySelector('#invoice-form').addEventListener('submit', async (e) => {
e.preventDefault();
// Recopilar datos del formulario
const clientIdValue = parseInt(container.querySelector('#clientId').value);
// Validar clientId
if (!clientIdValue || isNaN(clientIdValue) || clientIdValue < 1) {
alert('Por favor ingresa un ID de cliente válido');
return;
}
const formData = {
clientId: clientIdValue,
date: container.querySelector('#date').value + 'T00:00:00',
expireDate: container.querySelector('#expireDate').value + 'T00:00:00',
reference: container.querySelector('#reference').value || null,
notePublic: container.querySelector('#notePublic').value || null,
notePrivate: container.querySelector('#notePrivate').value || null,
lines: []
};
// Recopilar líneas
const lines = container.querySelectorAll('.invoice-line');
if (lines.length === 0) {
alert('Debes agregar al menos una línea al borrador');
return;
}
lines.forEach(line => {
const lineData = {
description: line.querySelector('.line-description').value,
quantity: parseFloat(line.querySelector('.line-quantity').value),
unitPrice: parseFloat(line.querySelector('.line-unitPrice').value),
taxRate: parseFloat(line.querySelector('.line-taxRate').value)
};
formData.lines.push(lineData);
});
// Validar que todos los campos requeridos estén completos
if (!formData.clientId || formData.lines.some(l => !l.description || !l.quantity || !l.unitPrice)) {
alert('Por favor completa todos los campos obligatorios');
return;
}
// Enviar a la API
try {
const submitBtn = container.querySelector('.btn-submit');
submitBtn.disabled = true;
submitBtn.textContent = 'Creando...';
const token = localStorage.getItem('token');
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/Invoices`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
const error = await response.text();
throw new Error(error || 'Error al crear el borrador');
}
const invoiceId = await response.json();
alert(`Borrador creado exitosamente con ID: ${invoiceId}`);
// Redirigir inmediatamente a la lista de facturas
window.location.hash = '#invoices';
} catch (error) {
console.error('Error al crear borrador:', error);
// Mensaje de error más específico
let errorMessage = 'Error al crear el borrador';
if (error.message.includes('foreign key constraint')) {
errorMessage = 'Error: El cliente con ese ID no existe. Por favor verifica el ID del cliente.';
} else if (error.message) {
errorMessage = `Error: ${error.message}`;
}
alert(errorMessage);
const submitBtn = container.querySelector('.btn-submit');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = 'Crear Borrador';
}
}
});
return container;
}

0
src/pages/DashboardPage.js Normal file → Executable file
View File

235
src/pages/Facturas.js Executable file
View File

@ -0,0 +1,235 @@
import { InvoiceItem } from '../components/InvoiceItem.js';
export function renderFacturasPage() {
const container = document.createElement('div');
container.className = 'facturas-page';
// Estado de paginación
let currentPage = 1;
let totalPages = 1;
const pageSize = 20;
let allInvoices = []; // Almacenar todas las facturas
let filteredInvoices = []; // Facturas filtradas
let currentFilter = ''; // Filtro actual de estado
let searchTerm = ''; // Término de búsqueda actual
container.innerHTML = /*html*/`
<div class="facturas-header">
<h1>Facturas</h1>
<button class="btn-new-invoice">+ Nueva Factura</button>
</div>
<div class="facturas-filters">
<input type="search" placeholder="Buscar facturas..." class="search-input" />
<select class="filter-status">
<option value="">Todos los estados</option>
<option value="draft">Borrador</option>
<option value="validated">Validada</option>
<option value="paid">Pagada</option>
<option value="unpaid">Impagada</option>
<option value="canceled">Cancelada</option>
</select>
</div>
<div class="invoices-table">
<div class="invoices-header">
<div class="invoice-checkbox">
<input type="checkbox" id="select-all" />
</div>
<div class="invoice-number">Número</div>
<div class="invoice-status">Estado</div>
<div class="invoice-client">Cliente</div>
<div class="invoice-date">Fecha</div>
<div class="invoice-total">Total</div>
<div class="invoice-remain">Pendiente</div>
<div class="invoice-actions">Acciones</div>
</div>
<div class="invoices-list">
<div class="loading">Cargando facturas...</div>
</div>
</div>
<div class="pagination">
<button class="btn-pagination btn-prev">← Anterior</button>
<div class="pagination-info">
<span class="current-page">1</span> / <span class="total-pages">1</span>
</div>
<button class="btn-pagination btn-next">Siguiente →</button>
</div>
`;
// Función para aplicar filtros combinados
function applyFilters() {
// Ya no filtra localmente, hace llamada a la API
loadAllInvoices();
}
// Función para renderizar la página actual
function renderPage(page = 1) {
const invoicesList = container.querySelector('.invoices-list');
currentPage = page;
totalPages = Math.ceil(filteredInvoices.length / pageSize);
// Calcular índices para la página
const startIndex = (page - 1) * pageSize;
const endIndex = Math.min(startIndex + pageSize, filteredInvoices.length);
const invoices = filteredInvoices.slice(startIndex, endIndex);
console.log(`Página ${currentPage}/${totalPages}, Facturas en esta página: ${invoices.length}, Total filtrado: ${filteredInvoices.length}`);
// Actualizar UI de paginación
updatePaginationUI();
// Limpiar lista
invoicesList.innerHTML = '';
// Si no hay facturas
if (invoices.length === 0) {
invoicesList.innerHTML = '<div class="no-invoices">No hay facturas disponibles</div>';
return;
}
// Renderizar cada factura
invoices.forEach(invoice => {
const invoiceItem = InvoiceItem(invoice);
invoicesList.appendChild(invoiceItem);
});
}
// Función para cargar todas las facturas
async function loadAllInvoices() {
const invoicesList = container.querySelector('.invoices-list');
try {
invoicesList.innerHTML = '<div class="loading">Cargando facturas...</div>';
const token = localStorage.getItem('token');
// Construir URL con parámetros
let url = `${import.meta.env.VITE_API_BASE_URL}/api/Invoices?limit=1000`;
if (currentFilter) {
url += `&status=${currentFilter}`;
}
if (searchTerm) {
url += `&search=${encodeURIComponent(searchTerm)}`;
}
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Error al cargar facturas');
}
const data = await response.json();
// Manejo de respuesta - obtener todas las facturas
allInvoices = Array.isArray(data) ? data : data.data || data.invoices || [];
filteredInvoices = [...allInvoices];
console.log(`Total de facturas cargadas: ${allInvoices.length}`);
// Renderizar la primera página
renderPage(1);
} catch (error) {
console.error('Error al cargar facturas:', error);
invoicesList.innerHTML = '<div class="error">Error al cargar las facturas. Por favor, intenta de nuevo.</div>';
}
}
// Función para actualizar la UI de paginación
function updatePaginationUI() {
const paginationDiv = container.querySelector('.pagination');
const currentPageSpan = container.querySelector('.current-page');
const totalPagesSpan = container.querySelector('.total-pages');
const btnPrev = container.querySelector('.btn-prev');
const btnNext = container.querySelector('.btn-next');
currentPageSpan.textContent = currentPage;
totalPagesSpan.textContent = totalPages;
// Mostrar u ocultar la paginación según el número de páginas
if (totalPages <= 1) {
paginationDiv.style.display = 'none';
} else {
paginationDiv.style.display = 'flex';
}
// Deshabilitar botones según corresponda
btnPrev.disabled = currentPage === 1;
btnNext.disabled = currentPage === totalPages;
}
// Event listeners
const newInvoiceBtn = container.querySelector('.btn-new-invoice');
newInvoiceBtn.addEventListener('click', () => {
window.location.hash = '#create-invoice';
});
const selectAllCheckbox = container.querySelector('#select-all');
selectAllCheckbox.addEventListener('change', (e) => {
const checkboxes = container.querySelectorAll('.invoice-item input[type="checkbox"]');
checkboxes.forEach(cb => cb.checked = e.target.checked);
});
const searchInput = container.querySelector('.search-input');
searchInput.addEventListener('input', (e) => {
const inputValue = e.target.value;
// Debounce para no hacer muchas llamadas
clearTimeout(searchInput.debounceTimer);
searchInput.debounceTimer = setTimeout(() => {
// Solo aplicar búsqueda si contiene al menos un número
const hasNumber = /\d/.test(inputValue);
if (hasNumber && inputValue !== 'I' && inputValue !== 'IN' && inputValue !== 'N') {
searchTerm = inputValue;
} else {
searchTerm = ''; // Si no tiene número, mostrar todas
}
applyFilters();
}, 500); // Espera 500ms después de que el usuario deja de escribir
});
const filterStatus = container.querySelector('.filter-status');
filterStatus.addEventListener('change', (e) => {
currentFilter = e.target.value;
console.log(`Filtro aplicado: ${currentFilter || 'Todos'}`);
applyFilters();
});
// Event listeners de paginación
const btnPrev = container.querySelector('.btn-prev');
const btnNext = container.querySelector('.btn-next');
btnPrev.addEventListener('click', () => {
if (currentPage > 1) {
renderPage(currentPage - 1);
scrollToTop();
}
});
btnNext.addEventListener('click', () => {
if (currentPage < totalPages) {
renderPage(currentPage + 1);
scrollToTop();
}
});
// Función para scroll al inicio
function scrollToTop() {
container.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// Cargar todas las facturas al montar el componente
loadAllInvoices();
return container;
}

0
src/pages/LoginPage.js Normal file → Executable file
View File

16
src/pages/pagesRegistry.js Normal file → Executable file
View File

@ -1,5 +1,7 @@
import { renderDashboard } from './DashboardPage.js'; import { renderDashboard } from './DashboardPage.js';
import { renderTestPage } from './test.js'; import { renderTestPage } from './test.js';
import { renderFacturasPage } from './Facturas.js';
import { renderCreateInvoicePage } from './CreateInvoicePage.js';
// Registry of all pages available in the application // Registry of all pages available in the application
export const pagesRegistry = [ export const pagesRegistry = [
@ -25,11 +27,15 @@ export const pagesRegistry = [
icon: '📄', icon: '📄',
requiresAuth: true, requiresAuth: true,
showInSidebar: true, showInSidebar: true,
render: () => { render: renderFacturasPage
const div = document.createElement('div'); },
div.innerHTML = '<h1>Facturas</h1><p>Página en construcción...</p>'; {
return div; route: 'create-invoice',
} name: 'Nueva Factura',
icon: '➕',
requiresAuth: true,
showInSidebar: false, // No mostrar en sidebar
render: renderCreateInvoicePage
}, },
{ {
route: 'clients', route: 'clients',

0
src/pages/test.js Normal file → Executable file
View File

0
src/router.js Normal file → Executable file
View File

0
src/services/auth.js Normal file → Executable file
View File

599
src/style.css Normal file → Executable file
View File

@ -229,22 +229,22 @@ button:focus-visible {
.status-pending { .status-pending {
background-color: #fef3c7; background-color: #fef3c7;
color: #d97706; color: #000000;
} }
.status-paid { .status-paid {
background-color: #d1fae5; background-color: #d1fae5;
color: #059669; color: #000000;
} }
.status-overdue { .status-overdue {
background-color: #fee2e2; background-color: #fee2e2;
color: #dc2626; color: #000000;
} }
.status-draft { .status-draft {
background-color: #f1f5f9; background-color: #f1f5f9;
color: #64748b; color: #000000;
} }
.amount-display { .amount-display {
@ -432,9 +432,350 @@ button:focus-visible {
} }
} }
/* Test Page Styles */ /* Facturas Page Styles */
.test-page { .facturas-page {
padding: 2rem; padding: 2rem;
max-width: 1600px;
margin: 0 auto;
}
.facturas-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.facturas-header h1 {
color: var(--text-primary);
font-size: 2rem;
margin: 0;
}
.btn-new-invoice {
background-color: var(--primary);
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: background-color 0.2s;
}
.btn-new-invoice:hover {
background-color: var(--primary-hover);
}
.facturas-filters {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
}
.search-input,
.filter-status {
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 0.95rem;
background: var(--card-bg);
color: #000000;
}
.search-input {
flex: 1;
max-width: 400px;
}
.filter-status {
min-width: 200px;
cursor: pointer;
}
.invoices-table {
background: var(--card-bg);
border-radius: 12px;
border: 1px solid var(--border-color);
overflow: hidden;
}
.invoices-header,
.invoice-item {
display: grid;
grid-template-columns: 40px 150px 120px 120px 120px 120px 120px 120px 80px;
gap: 1rem;
padding: 1rem 1.5rem;
align-items: center;
}
.invoices-header {
background: #f8fafc;
font-weight: 600;
color: var(--text-secondary);
font-size: 0.85rem;
text-transform: uppercase;
border-bottom: 1px solid var(--border-color);
}
.invoice-item {
border-bottom: 1px solid var(--border-color);
transition: background-color 0.2s;
}
.invoice-item:hover {
background-color: #f8fafc;
}
.invoice-item:last-child {
border-bottom: none;
}
.invoice-checkbox input {
width: 18px;
height: 18px;
cursor: pointer;
}
.invoice-number {
font-weight: 600;
color: var(--text-primary);
}
.status-badge {
display: inline-block;
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
text-align: center;
}
.status-draft {
background: #f3f4f6;
color: #000000;
}
.status-validated {
background: #dbeafe;
color: #000000;
}
.status-paid {
background: #d1fae5;
color: #000000;
}
.status-unpaid {
background: #fee2e2;
color: #000000;
}
.status-canceled {
background: #f3f4f6;
color: #000000;
text-decoration: line-through;
}
.invoice-client {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-secondary);
}
.client-icon {
font-size: 1rem;
}
.invoice-date,
.invoice-total,
.invoice-remain {
color: var(--text-primary);
}
.invoice-total,
.invoice-remain {
font-weight: 600;
}
.invoice-actions {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.btn-view {
background: transparent;
border: 1px solid var(--border-color);
padding: 0.5rem;
border-radius: 6px;
cursor: pointer;
font-size: 1.1rem;
transition: all 0.2s;
}
.btn-view:hover {
background: var(--primary);
border-color: var(--primary);
transform: scale(1.05);
}
.loading,
.error,
.no-invoices {
padding: 3rem;
text-align: center;
color: var(--text-secondary);
font-size: 1.1rem;
}
.error {
color: var(--danger);
}
/* Pagination Styles */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
margin-top: 2rem;
padding: 1.5rem;
background: var(--card-bg);
border-radius: 12px;
border: 1px solid var(--border-color);
}
.pagination-info {
font-weight: 600;
color: var(--text-primary);
font-size: 1rem;
display: flex;
gap: 0.5rem;
align-items: center;
}
.pagination-info .current-page,
.pagination-info .total-pages {
font-weight: 700;
color: var(--primary);
min-width: 30px;
text-align: center;
}
.btn-pagination {
padding: 0.75rem 1.5rem;
background-color: var(--primary);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn-pagination:hover:not(:disabled) {
background-color: var(--primary-hover);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
}
.btn-pagination:disabled {
background-color: #cbd5e1;
cursor: not-allowed;
opacity: 0.6;
}
.btn-pagination:active:not(:disabled) {
transform: translateY(0);
}
/* Responsive */
@media (max-width: 768px) {
.pagination {
flex-wrap: wrap;
gap: 1rem;
}
.btn-pagination {
flex: 1;
min-width: 120px;
}
.pagination-info {
order: 3;
width: 100%;
justify-content: center;
}
}
@media (max-width: 1400px) {
.invoices-header,
.invoice-item {
grid-template-columns: 40px 120px 100px 50px 130px 100px 100px 100px 70px;
font-size: 0.9rem;
}
}
@media (max-width: 1024px) {
.facturas-page {
padding: 1rem;
}
.invoices-header,
.invoice-item {
grid-template-columns: 40px 100px 90px 130px 90px 90px 60px;
gap: 0.5rem;
padding: 1rem;
}
.invoice-id,
.invoice-remain {
display: none;
}
}
@media (max-width: 768px) {
.facturas-filters {
flex-direction: column;
}
.search-input {
max-width: 100%;
}
.invoices-header {
display: none;
}
.invoice-item {
grid-template-columns: 1fr;
gap: 0.5rem;
padding: 1.5rem;
}
.invoice-item > div {
display: flex;
justify-content: space-between;
align-items: center;
}
.invoice-item > div::before {
content: attr(class);
font-weight: 600;
color: var(--text-secondary);
text-transform: capitalize;
}
.invoice-checkbox {
position: absolute;
top: 1rem;
right: 1rem;
}
} }
.test-page h1 { .test-page h1 {
@ -474,3 +815,249 @@ button:focus-visible {
min-height: 100vh; min-height: 100vh;
width: 100%; width: 100%;
} }
/* Create Invoice Page Styles */
.create-invoice-page {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.create-invoice-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.create-invoice-header h1 {
margin: 0;
font-size: 2rem;
color: var(--text-primary);
}
.create-invoice-header .subtitle {
margin: 0.5rem 0 0 0;
color: var(--text-secondary);
}
.btn-back {
background: var(--card-bg);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 0.75rem 1.5rem;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.btn-back:hover {
background: #f1f5f9;
}
.invoice-form {
background: var(--card-bg);
border-radius: 12px;
border: 1px solid var(--border-color);
padding: 2rem;
}
.form-section {
margin-bottom: 2rem;
}
.form-section h2 {
font-size: 1.5rem;
margin: 0 0 1.5rem 0;
color: var(--text-primary);
border-bottom: 2px solid var(--primary);
padding-bottom: 0.5rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.section-header h2 {
margin: 0;
border: none;
padding: 0;
}
.btn-add-line {
background: var(--success);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: background 0.2s;
}
.btn-add-line:hover {
background: var(--success-hover);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.form-row-inline {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group.full-width {
grid-column: 1 / -1;
}
.form-group label {
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.form-group input,
.form-group textarea,
.form-group select {
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 1rem;
color: #000000;
background: var(--card-bg);
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary);
}
.form-group input[readonly] {
background: #f1f5f9;
cursor: not-allowed;
}
.invoice-line {
background: #f8fafc;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1rem;
}
.line-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.line-number {
font-weight: 700;
color: var(--text-primary);
font-size: 1.1rem;
}
.btn-remove-line {
background: var(--danger);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: background 0.2s;
}
.btn-remove-line:hover {
background: var(--danger-hover);
}
.line-content {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid var(--border-color);
}
.btn-cancel {
background: var(--card-bg);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 0.75rem 2rem;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.btn-cancel:hover {
background: #f1f5f9;
}
.btn-submit {
background: var(--primary);
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: background 0.2s;
}
.btn-submit:hover {
background: var(--primary-hover);
}
.btn-submit:disabled {
background: #cbd5e1;
cursor: not-allowed;
}
@media (max-width: 768px) {
.create-invoice-page {
padding: 1rem;
}
.create-invoice-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.form-row,
.form-row-inline {
grid-template-columns: 1fr;
}
.invoice-form {
padding: 1rem;
}
}