diff --git a/src/components/InvoiceItem.js b/src/components/InvoiceItem.js
index 26cf6cb..5bc89d2 100644
--- a/src/components/InvoiceItem.js
+++ b/src/components/InvoiceItem.js
@@ -52,7 +52,7 @@ export function InvoiceItem(invoice) {
+
+
+
+
+
+
+
+ `;
+
+ // 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;
+}
diff --git a/src/pages/Facturas.js b/src/pages/Facturas.js
index 879d5a1..e211206 100755
--- a/src/pages/Facturas.js
+++ b/src/pages/Facturas.js
@@ -9,6 +9,9 @@ export function renderFacturasPage() {
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*/`
`;
+ // 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(allInvoices.length / pageSize);
+ totalPages = Math.ceil(filteredInvoices.length / pageSize);
// Calcular índices para la página
const startIndex = (page - 1) * pageSize;
- const endIndex = Math.min(startIndex + pageSize, allInvoices.length);
- const invoices = allInvoices.slice(startIndex, endIndex);
+ 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: ${allInvoices.length}`);
+ console.log(`Página ${currentPage}/${totalPages}, Facturas en esta página: ${invoices.length}, Total filtrado: ${filteredInvoices.length}`);
// Actualizar UI de paginación
updatePaginationUI();
@@ -96,16 +105,25 @@ export function renderFacturasPage() {
invoicesList.innerHTML = 'Cargando facturas...
';
const token = localStorage.getItem('token');
- const response = await fetch(
- `${import.meta.env.VITE_API_BASE_URL}/api/Invoices`,
- {
- method: 'GET',
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json'
- }
+
+ // 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');
@@ -115,6 +133,7 @@ export function renderFacturasPage() {
// 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}`);
@@ -129,6 +148,7 @@ export function renderFacturasPage() {
// 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');
@@ -137,6 +157,13 @@ export function renderFacturasPage() {
currentPageSpan.textContent = currentPage;
totalPagesSpan.textContent = totalPages;
+ // Mostrar u ocultar la paginación según el número de páginas
+ if (totalPages <= 1) {
+ paginationDiv.style.display = 'none';
+ } else {
+ paginationDiv.style.display = 'flex';
+ }
+
// Deshabilitar botones según corresponda
btnPrev.disabled = currentPage === 1;
btnNext.disabled = currentPage === totalPages;
@@ -145,8 +172,7 @@ export function renderFacturasPage() {
// Event listeners
const newInvoiceBtn = container.querySelector('.btn-new-invoice');
newInvoiceBtn.addEventListener('click', () => {
- console.log('Crear nueva factura');
- alert('Funcionalidad de crear factura pendiente');
+ window.location.hash = '#create-invoice';
});
const selectAllCheckbox = container.querySelector('#select-all');
@@ -157,27 +183,26 @@ export function renderFacturasPage() {
const searchInput = container.querySelector('.search-input');
searchInput.addEventListener('input', (e) => {
- const searchTerm = e.target.value.toLowerCase();
- const items = container.querySelectorAll('.invoice-item');
- items.forEach(item => {
- const text = item.textContent.toLowerCase();
- item.style.display = text.includes(searchTerm) ? 'grid' : 'none';
- });
+ 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) => {
- const status = e.target.value;
- const items = container.querySelectorAll('.invoice-item');
- items.forEach(item => {
- if (!status) {
- item.style.display = 'grid';
- } else {
- const badge = item.querySelector('.status-badge');
- const itemStatus = badge.className.split(' ').find(c => c.startsWith('status-'))?.replace('status-', '');
- item.style.display = itemStatus === status ? 'grid' : 'none';
- }
- });
+ currentFilter = e.target.value;
+ console.log(`Filtro aplicado: ${currentFilter || 'Todos'}`);
+ applyFilters();
});
// Event listeners de paginación
diff --git a/src/pages/pagesRegistry.js b/src/pages/pagesRegistry.js
index 995409a..0e7814a 100755
--- a/src/pages/pagesRegistry.js
+++ b/src/pages/pagesRegistry.js
@@ -1,6 +1,7 @@
import { renderDashboard } from './DashboardPage.js';
import { renderTestPage } from './test.js';
import { renderFacturasPage } from './Facturas.js';
+import { renderCreateInvoicePage } from './CreateInvoicePage.js';
// Registry of all pages available in the application
export const pagesRegistry = [
@@ -28,6 +29,14 @@ export const pagesRegistry = [
showInSidebar: true,
render: renderFacturasPage
},
+ {
+ route: 'create-invoice',
+ name: 'Nueva Factura',
+ icon: '➕',
+ requiresAuth: true,
+ showInSidebar: false, // No mostrar en sidebar
+ render: renderCreateInvoicePage
+ },
{
route: 'clients',
name: 'Clientes',
diff --git a/src/router.js b/src/router.js
index 142b3f6..a5f7380 100755
--- a/src/router.js
+++ b/src/router.js
@@ -4,7 +4,7 @@ import { createSidebar } from './components/Sidebar.js';
import { getAvailablePages, getPageByRoute } from './pages/pagesRegistry.js';
// 🔧 Modo DEV: cambiar a false para activar login
-const DEV_MODE = true;
+const DEV_MODE = false;
export function initRouter() {
const app = document.querySelector('#app');
diff --git a/src/style.css b/src/style.css
index d1c95e4..b566d17 100755
--- a/src/style.css
+++ b/src/style.css
@@ -815,3 +815,249 @@ button:focus-visible {
min-height: 100vh;
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;
+ }
+}
\ No newline at end of file