From 713ef5d76b270f94715e3c84eeb825e8784873f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81?= Date: Fri, 23 Jan 2026 18:36:23 +0100 Subject: [PATCH 1/8] un manejador de pages --- .env | 0 .gitignore | 0 index.html | 0 package-lock.json | 0 package.json | 0 public/vite.svg | 0 src/components/Login.js | 0 src/components/Sidebar.js | 0 src/counter.js | 0 src/javascript.svg | 0 src/main.js | 0 src/pages/DashboardPage.js | 0 src/pages/LoginPage.js | 0 src/pages/pagesRegistry.js | 0 src/pages/test.js | 0 src/router.js | 0 src/services/auth.js | 0 src/style.css | 0 18 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .env mode change 100644 => 100755 .gitignore mode change 100644 => 100755 index.html mode change 100644 => 100755 package-lock.json mode change 100644 => 100755 package.json mode change 100644 => 100755 public/vite.svg mode change 100644 => 100755 src/components/Login.js mode change 100644 => 100755 src/components/Sidebar.js mode change 100644 => 100755 src/counter.js mode change 100644 => 100755 src/javascript.svg mode change 100644 => 100755 src/main.js mode change 100644 => 100755 src/pages/DashboardPage.js mode change 100644 => 100755 src/pages/LoginPage.js mode change 100644 => 100755 src/pages/pagesRegistry.js mode change 100644 => 100755 src/pages/test.js mode change 100644 => 100755 src/router.js mode change 100644 => 100755 src/services/auth.js mode change 100644 => 100755 src/style.css diff --git a/.env b/.env old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/index.html b/index.html old mode 100644 new mode 100755 diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 diff --git a/package.json b/package.json old mode 100644 new mode 100755 diff --git a/public/vite.svg b/public/vite.svg old mode 100644 new mode 100755 diff --git a/src/components/Login.js b/src/components/Login.js old mode 100644 new mode 100755 diff --git a/src/components/Sidebar.js b/src/components/Sidebar.js old mode 100644 new mode 100755 diff --git a/src/counter.js b/src/counter.js old mode 100644 new mode 100755 diff --git a/src/javascript.svg b/src/javascript.svg old mode 100644 new mode 100755 diff --git a/src/main.js b/src/main.js old mode 100644 new mode 100755 diff --git a/src/pages/DashboardPage.js b/src/pages/DashboardPage.js old mode 100644 new mode 100755 diff --git a/src/pages/LoginPage.js b/src/pages/LoginPage.js old mode 100644 new mode 100755 diff --git a/src/pages/pagesRegistry.js b/src/pages/pagesRegistry.js old mode 100644 new mode 100755 diff --git a/src/pages/test.js b/src/pages/test.js old mode 100644 new mode 100755 diff --git a/src/router.js b/src/router.js old mode 100644 new mode 100755 diff --git a/src/services/auth.js b/src/services/auth.js old mode 100644 new mode 100755 diff --git a/src/style.css b/src/style.css old mode 100644 new mode 100755 From 3241c77646b4c8778c1d95ba2c141faff36c7e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81?= Date: Fri, 23 Jan 2026 19:53:02 +0100 Subject: [PATCH 2/8] pull que arregla error de login --- src/components/InvoiceItem.js | 77 ++++++++++ src/pages/Facturas.js | 128 ++++++++++++++++ src/pages/pagesRegistry.js | 7 +- src/router.js | 2 +- src/style.css | 266 +++++++++++++++++++++++++++++++++- 5 files changed, 472 insertions(+), 8 deletions(-) create mode 100644 src/components/InvoiceItem.js create mode 100755 src/pages/Facturas.js diff --git a/src/components/InvoiceItem.js b/src/components/InvoiceItem.js new file mode 100644 index 0000000..2f6e782 --- /dev/null +++ b/src/components/InvoiceItem.js @@ -0,0 +1,77 @@ +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*/` +
+ +
+
${invoice.number}
+
+ + ${getStatusText(invoice.status)} + +
+
${invoice.id}
+
+ 👤 + Cliente #${invoice.clientId} +
+
${formatDate(invoice.date)}
+
${formatCurrency(invoice.total)}
+
${formatCurrency(invoice.remainToPay)}
+
+ +
+ `; + + // 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; +} diff --git a/src/pages/Facturas.js b/src/pages/Facturas.js new file mode 100755 index 0000000..b6064ec --- /dev/null +++ b/src/pages/Facturas.js @@ -0,0 +1,128 @@ +import { InvoiceItem } from '../components/InvoiceItem.js'; + +export function renderFacturasPage() { + const container = document.createElement('div'); + container.className = 'facturas-page'; + + container.innerHTML = /*html*/` +
+

Facturas

+ +
+ +
+ + +
+ +
+
+
+ +
+
Número
+
Estado
+
ID
+
Cliente
+
Fecha
+
Total
+
Pendiente
+
Acciones
+
+
+
Cargando facturas...
+
+
+ `; + + // Función para cargar las facturas + async function loadInvoices() { + const invoicesList = container.querySelector('.invoices-list'); + + try { + 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' + } + }); + + if (!response.ok) { + throw new Error('Error al cargar facturas'); + } + + const invoices = await response.json(); + + // Limpiar lista + invoicesList.innerHTML = ''; + + // Si no hay facturas + if (invoices.length === 0) { + invoicesList.innerHTML = '
No hay facturas disponibles
'; + return; + } + + // Renderizar cada factura + invoices.forEach(invoice => { + const invoiceItem = InvoiceItem(invoice); + invoicesList.appendChild(invoiceItem); + }); + + } catch (error) { + console.error('Error al cargar facturas:', error); + invoicesList.innerHTML = '
Error al cargar las facturas. Por favor, intenta de nuevo.
'; + } + } + + // Event listeners + const newInvoiceBtn = container.querySelector('.btn-new-invoice'); + newInvoiceBtn.addEventListener('click', () => { + console.log('Crear nueva factura'); + alert('Funcionalidad de crear factura pendiente'); + }); + + 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 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 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'; + } + }); + }); + + // Cargar facturas al montar el componente + loadInvoices(); + + return container; +} diff --git a/src/pages/pagesRegistry.js b/src/pages/pagesRegistry.js index 8a7bf57..995409a 100755 --- a/src/pages/pagesRegistry.js +++ b/src/pages/pagesRegistry.js @@ -1,5 +1,6 @@ import { renderDashboard } from './DashboardPage.js'; import { renderTestPage } from './test.js'; +import { renderFacturasPage } from './Facturas.js'; // Registry of all pages available in the application export const pagesRegistry = [ @@ -25,11 +26,7 @@ export const pagesRegistry = [ icon: '📄', requiresAuth: true, showInSidebar: true, - render: () => { - const div = document.createElement('div'); - div.innerHTML = '

Facturas

Página en construcción...

'; - return div; - } + render: renderFacturasPage }, { route: 'clients', diff --git a/src/router.js b/src/router.js index a5f7380..142b3f6 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 = false; +const DEV_MODE = true; export function initRouter() { const app = document.querySelector('#app'); diff --git a/src/style.css b/src/style.css index 17b2db5..d25beb5 100755 --- a/src/style.css +++ b/src/style.css @@ -432,9 +432,271 @@ button:focus-visible { } } -/* Test Page Styles */ -.test-page { +/* Facturas Page Styles */ +.facturas-page { 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); +} + +.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 60px 150px 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: #6b7280; +} + +.status-validated { + background: #dbeafe; + color: #1d4ed8; +} + +.status-paid { + background: #d1fae5; + color: #059669; +} + +.status-unpaid { + background: #fee2e2; + color: #dc2626; +} + +.status-canceled { + background: #f3f4f6; + color: #374151; + 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); +} + +/* Responsive */ +@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 { From 6408d3325b92cb1c065733b6b2046175a22c94ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81?= Date: Tue, 27 Jan 2026 18:51:20 +0100 Subject: [PATCH 3/8] crear apartado facturas --- src/components/InvoiceItem.js | 1 - src/pages/Facturas.js | 134 +++++++++++++++++++++++++++------- src/style.css | 99 ++++++++++++++++++++++--- 3 files changed, 197 insertions(+), 37 deletions(-) diff --git a/src/components/InvoiceItem.js b/src/components/InvoiceItem.js index 2f6e782..26cf6cb 100644 --- a/src/components/InvoiceItem.js +++ b/src/components/InvoiceItem.js @@ -50,7 +50,6 @@ export function InvoiceItem(invoice) { ${getStatusText(invoice.status)} -
${invoice.id}
👤 Cliente #${invoice.clientId} diff --git a/src/pages/Facturas.js b/src/pages/Facturas.js index b6064ec..879d5a1 100755 --- a/src/pages/Facturas.js +++ b/src/pages/Facturas.js @@ -4,6 +4,12 @@ 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 + container.innerHTML = /*html*/`

Facturas

@@ -29,7 +35,6 @@ export function renderFacturasPage() {
Número
Estado
-
ID
Cliente
Fecha
Total
@@ -40,42 +45,81 @@ export function renderFacturasPage() {
Cargando facturas...
+ + `; - // Función para cargar las facturas - async function loadInvoices() { + // 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); + + // 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); + + console.log(`Página ${currentPage}/${totalPages}, Facturas en esta página: ${invoices.length}, Total: ${allInvoices.length}`); + + // Actualizar UI de paginación + updatePaginationUI(); + + // Limpiar lista + invoicesList.innerHTML = ''; + + // Si no hay facturas + if (invoices.length === 0) { + invoicesList.innerHTML = '
No hay facturas disponibles
'; + 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 = '
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' + const response = await fetch( + `${import.meta.env.VITE_API_BASE_URL}/api/Invoices`, + { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } } - }); + ); if (!response.ok) { throw new Error('Error al cargar facturas'); } - const invoices = await response.json(); + const data = await response.json(); - // Limpiar lista - invoicesList.innerHTML = ''; - - // Si no hay facturas - if (invoices.length === 0) { - invoicesList.innerHTML = '
No hay facturas disponibles
'; - return; - } - - // Renderizar cada factura - invoices.forEach(invoice => { - const invoiceItem = InvoiceItem(invoice); - invoicesList.appendChild(invoiceItem); - }); + // Manejo de respuesta - obtener todas las facturas + allInvoices = Array.isArray(data) ? data : data.data || data.invoices || []; + + console.log(`Total de facturas cargadas: ${allInvoices.length}`); + + // Renderizar la primera página + renderPage(1); } catch (error) { console.error('Error al cargar facturas:', error); @@ -83,6 +127,21 @@ export function renderFacturasPage() { } } + // Función para actualizar la UI de paginación + function updatePaginationUI() { + 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; + + // 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', () => { @@ -121,8 +180,31 @@ export function renderFacturasPage() { }); }); - // Cargar facturas al montar el componente - loadInvoices(); + // 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; } diff --git a/src/style.css b/src/style.css index d25beb5..d1c95e4 100755 --- a/src/style.css +++ b/src/style.css @@ -229,22 +229,22 @@ button:focus-visible { .status-pending { background-color: #fef3c7; - color: #d97706; + color: #000000; } .status-paid { background-color: #d1fae5; - color: #059669; + color: #000000; } .status-overdue { background-color: #fee2e2; - color: #dc2626; + color: #000000; } .status-draft { background-color: #f1f5f9; - color: #64748b; + color: #000000; } .amount-display { @@ -480,6 +480,7 @@ button:focus-visible { border-radius: 8px; font-size: 0.95rem; background: var(--card-bg); + color: #000000; } .search-input { @@ -502,7 +503,7 @@ button:focus-visible { .invoices-header, .invoice-item { display: grid; - grid-template-columns: 40px 150px 120px 60px 150px 120px 120px 120px 80px; + grid-template-columns: 40px 150px 120px 120px 120px 120px 120px 120px 80px; gap: 1rem; padding: 1rem 1.5rem; align-items: center; @@ -552,27 +553,27 @@ button:focus-visible { .status-draft { background: #f3f4f6; - color: #6b7280; + color: #000000; } .status-validated { background: #dbeafe; - color: #1d4ed8; + color: #000000; } .status-paid { background: #d1fae5; - color: #059669; + color: #000000; } .status-unpaid { background: #fee2e2; - color: #dc2626; + color: #000000; } .status-canceled { background: #f3f4f6; - color: #374151; + color: #000000; text-decoration: line-through; } @@ -633,7 +634,85 @@ button:focus-visible { 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 { From c2b23ddf4c6b8600522bf8e9570b5d4db48fb076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81?= Date: Mon, 2 Feb 2026 17:58:52 +0100 Subject: [PATCH 4/8] Crear si no lo esta y actualizar facturas --- src/components/InvoiceItem.js | 2 +- src/pages/CreateInvoicePage.js | 297 +++++++++++++++++++++++++++++++++ src/pages/Facturas.js | 89 ++++++---- src/pages/pagesRegistry.js | 9 + src/router.js | 2 +- src/style.css | 246 +++++++++++++++++++++++++++ 6 files changed, 611 insertions(+), 34 deletions(-) create mode 100644 src/pages/CreateInvoicePage.js 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) {
👤 - Cliente #${invoice.clientId} + ${invoice.clientName || 'Sin nombre'}
${formatDate(invoice.date)}
${formatCurrency(invoice.total)}
diff --git a/src/pages/CreateInvoicePage.js b/src/pages/CreateInvoicePage.js new file mode 100644 index 0000000..c043d6f --- /dev/null +++ b/src/pages/CreateInvoicePage.js @@ -0,0 +1,297 @@ +export function renderCreateInvoicePage() { + const container = document.createElement('div'); + container.className = 'create-invoice-page'; + + container.innerHTML = /*html*/` +
+
+

Nuevo Borrador

+

Crear un nuevo borrador

+
+ +
+ +
+
+

Información General

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

Líneas de Borrador

+ +
+ +
+ +
+
+ +
+ + +
+
+ `; + + // 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*/` +
+ Línea ${lineCounter} + +
+ +
+
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ `; + + // 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*/`
@@ -55,19 +58,25 @@ export function renderFacturasPage() {
`; + // 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 From aa8f0494442abd2b9a4e61c68c20313ac90ea2b2 Mon Sep 17 00:00:00 2001 From: javiermengual Date: Tue, 3 Feb 2026 11:45:54 +0100 Subject: [PATCH 5/8] Refactor invoice loading and search functionality for improved performance and user experience --- src/pages/Facturas.js | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/pages/Facturas.js b/src/pages/Facturas.js index e211206..2f935dc 100755 --- a/src/pages/Facturas.js +++ b/src/pages/Facturas.js @@ -67,7 +67,7 @@ export function renderFacturasPage() { // 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); @@ -100,23 +100,23 @@ export function renderFacturasPage() { // Función para cargar todas las facturas async function loadAllInvoices() { const invoicesList = container.querySelector('.invoices-list'); - + try { invoicesList.innerHTML = '
Cargando facturas...
'; - + 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: { @@ -130,13 +130,13 @@ export function renderFacturasPage() { } 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); @@ -183,16 +183,15 @@ export function renderFacturasPage() { const searchInput = container.querySelector('.search-input'); searchInput.addEventListener('input', (e) => { - const inputValue = e.target.value; + const inputValue = e.target.value.trim(); // Debounce para no hacer muchas llamadas clearTimeout(searchInput.debounceTimer); searchInput.debounceTimer = setTimeout(() => { - // Solo aplicar búsqueda si contiene al menos un número - const hasNumber = /\d/.test(inputValue); - if (hasNumber && inputValue !== 'I' && inputValue !== 'IN' && inputValue !== 'N') { + // Validación mínima: al menos 2 caracteres para activar búsqueda + if (inputValue.length >= 2) { searchTerm = inputValue; } else { - searchTerm = ''; // Si no tiene número, mostrar todas + searchTerm = ''; // Si es muy corto, mostrar todas } applyFilters(); }, 500); // Espera 500ms después de que el usuario deja de escribir From f7b376e10feb5fb13ae5c396a4b79e51d2ba8ab1 Mon Sep 17 00:00:00 2001 From: javiermengual Date: Tue, 3 Feb 2026 13:44:59 +0100 Subject: [PATCH 6/8] Refactor Create Invoice Page for ultra compact design and improved user experience --- src/pages/CreateInvoicePage.js | 302 +++++++++++---------------- src/style.css | 366 +++++++++++++++++++-------------- 2 files changed, 332 insertions(+), 336 deletions(-) diff --git a/src/pages/CreateInvoicePage.js b/src/pages/CreateInvoicePage.js index c043d6f..8b5ca82 100644 --- a/src/pages/CreateInvoicePage.js +++ b/src/pages/CreateInvoicePage.js @@ -3,212 +3,169 @@ export function renderCreateInvoicePage() { container.className = 'create-invoice-page'; container.innerHTML = /*html*/` -
-
-

Nuevo Borrador

-

Crear un nuevo borrador

-
- +
+

Nueva Factura

+
-
-
-

Información General

- -
-
- - -
- -
- - -
+ +
+
+ +
-
-
- - -
- -
- - -
+
+ +
-
-
- - -
-
- -
-
- - -
+
+ +
-
-
-

Líneas de Borrador

- +
+
+ +
- -
- + +
+ +
-
- - +
+
+

Líneas de Factura

+ +
+ +
+
+ +
+ +
`; - // Establecer fecha de hoy por defecto + const clientSelect = container.querySelector('#clientId'); + + async function loadClients() { + try { + const token = localStorage.getItem('token'); + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/Clients?limit=1000`, { + headers: { 'Authorization': `Bearer ${token}` } + }); + + if (!response.ok) throw new Error('Error al cargar clientes'); + + const clients = await response.json(); + + clientSelect.innerHTML = ''; + clients.forEach(client => { + const option = document.createElement('option'); + option.value = client.id; + option.textContent = client.name; + clientSelect.appendChild(option); + }); + + clientSelect.disabled = false; + } catch (error) { + console.error('Error:', error); + clientSelect.innerHTML = ''; + } + } + + loadClients(); + 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.className = 'line-row-compact'; lineDiv.innerHTML = /*html*/` -
- Línea ${lineCounter} - -
- -
-
- - -
- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
+ + + + +
0.00 €
+ `; - // Event listeners para calcular subtotal - const quantityInput = lineDiv.querySelector('.line-quantity'); - const priceInput = lineDiv.querySelector('.line-unitPrice'); - const subtotalInput = lineDiv.querySelector('.line-subtotal'); + const descInput = lineDiv.querySelector('.line-desc-compact'); + const qtyInput = lineDiv.querySelector('.line-qty-compact'); + const priceInput = lineDiv.querySelector('.line-price-compact'); + const taxInput = lineDiv.querySelector('.line-tax-compact'); + const totalDiv = lineDiv.querySelector('.line-total-compact'); - function updateSubtotal() { - const quantity = parseFloat(quantityInput.value) || 0; + function updateTotal() { + const qty = parseFloat(qtyInput.value) || 0; const price = parseFloat(priceInput.value) || 0; - const subtotal = quantity * price; - subtotalInput.value = `${subtotal.toFixed(2)} €`; - updateTotal(); + const tax = parseFloat(taxInput.value) || 0; + const subtotal = qty * price; + const total = subtotal * (1 + tax / 100); + totalDiv.textContent = `${total.toFixed(2)} €`; } - quantityInput.addEventListener('input', updateSubtotal); - priceInput.addEventListener('input', updateSubtotal); + qtyInput.addEventListener('input', updateTotal); + priceInput.addEventListener('input', updateTotal); + taxInput.addEventListener('input', updateTotal); - // Event listener para eliminar línea - lineDiv.querySelector('.btn-remove-line').addEventListener('click', () => { + lineDiv.querySelector('.btn-delete-compact').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()); + 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.')) { + if (confirm('¿Cancelar? Se perderán los cambios.')) { 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'); + alert('Selecciona un cliente'); + return; + } + + const lines = container.querySelectorAll('.line-row-compact'); + if (lines.length === 0) { + alert('Añade al menos una línea'); return; } @@ -216,40 +173,40 @@ export function renderCreateInvoicePage() { 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; - } - + let hasEmptyLine = false; 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); + const desc = line.querySelector('.line-desc-compact').value.trim(); + const qty = parseFloat(line.querySelector('.line-qty-compact').value); + const price = parseFloat(line.querySelector('.line-price-compact').value); + const tax = parseFloat(line.querySelector('.line-tax-compact').value); + + if (!desc || !qty || !price) { + hasEmptyLine = true; + return; + } + + formData.lines.push({ + description: desc, + quantity: qty, + unitPrice: price, + taxRate: tax + }); }); - // 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'); + if (hasEmptyLine) { + alert('Completa todos los campos de las líneas'); return; } - // Enviar a la API try { - const submitBtn = container.querySelector('.btn-submit'); + const submitBtn = container.querySelector('.btn-submit-compact'); submitBtn.disabled = true; - submitBtn.textContent = 'Creando...'; + submitBtn.textContent = 'Guardando...'; const token = localStorage.getItem('token'); const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/Invoices`, { @@ -263,32 +220,21 @@ export function renderCreateInvoicePage() { if (!response.ok) { const error = await response.text(); - throw new Error(error || 'Error al crear el borrador'); + throw new Error(error || 'Error al crear'); } const invoiceId = await response.json(); - alert(`Borrador creado exitosamente con ID: ${invoiceId}`); - - // Redirigir inmediatamente a la lista de facturas + alert(`Factura creada (ID: ${invoiceId})`); window.location.hash = '#invoices'; } catch (error) { - console.error('Error al crear borrador:', error); + console.error('Error:', error); + alert(error.message || 'Error al crear la factura'); - // 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'); + const submitBtn = container.querySelector('.btn-submit-compact'); if (submitBtn) { submitBtn.disabled = false; - submitBtn.textContent = 'Crear Borrador'; + submitBtn.textContent = 'Crear Factura'; } } }); diff --git a/src/style.css b/src/style.css index b566d17..5d8142e 100755 --- a/src/style.css +++ b/src/style.css @@ -815,249 +815,299 @@ button:focus-visible { min-height: 100vh; width: 100%; } -/* Create Invoice Page Styles */ +/* Create Invoice Page - Ultra Compact */ .create-invoice-page { - padding: 2rem; - max-width: 1200px; + padding: 1rem; + max-width: 1400px; margin: 0 auto; + min-height: 100vh; } -.create-invoice-header { +.invoice-header-compact { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 2rem; + margin-bottom: 1rem; + flex-shrink: 0; } -.create-invoice-header h1 { +.invoice-header-compact h1 { margin: 0; - font-size: 2rem; + font-size: 1.5rem; 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; + background: transparent; + color: #64748b; + border: 1px solid #e2e8f0; + padding: 0.4rem 0.8rem; + border-radius: 4px; cursor: pointer; - font-weight: 600; + font-weight: 500; + font-size: 0.9rem; transition: all 0.2s; } .btn-back:hover { background: #f1f5f9; + color: #475569; + border-color: #cbd5e1; } -.invoice-form { +.invoice-form-compact { background: var(--card-bg); - border-radius: 12px; + border-radius: 8px; border: 1px solid var(--border-color); - padding: 2rem; + padding: 1rem; } -.form-section { - margin-bottom: 2rem; +.form-grid-compact { + display: grid; + grid-template-columns: 2fr 1fr 1fr; + gap: 0.75rem; + margin-bottom: 0.75rem; + flex-shrink: 0; } -.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 { +.notes-grid-compact { display: grid; grid-template-columns: 1fr 1fr; - gap: 1.5rem; - margin-bottom: 1.5rem; + gap: 0.75rem; + margin-bottom: 0.75rem; } -.form-row-inline { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; - gap: 1rem; - margin-bottom: 1rem; -} - -.form-group { +.form-field-compact { display: flex; flex-direction: column; } -.form-group.full-width { +.form-field-compact.full-width { grid-column: 1 / -1; } -.form-group label { - font-weight: 600; - margin-bottom: 0.5rem; +.form-field-compact label { + font-weight: 500; + margin-bottom: 0.25rem; color: var(--text-primary); + font-size: 0.8rem; } -.form-group input, -.form-group textarea, -.form-group select { - padding: 0.75rem; +.form-field-compact input, +.form-field-compact select, +.form-field-compact textarea { + padding: 0.4rem 0.5rem; border: 1px solid var(--border-color); - border-radius: 6px; - font-size: 1rem; + border-radius: 4px; + font-size: 0.9rem; color: #000000; background: var(--card-bg); - transition: border-color 0.2s; + resize: vertical; + min-height: 38px; } -.form-group input:focus, -.form-group textarea:focus, -.form-group select:focus { +.form-field-compact input:focus, +.form-field-compact select:focus, +.form-field-compact textarea:focus { outline: none; border-color: var(--primary); } -.form-group input[readonly] { - background: #f1f5f9; - cursor: not-allowed; +.lines-section-compact { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; } -.invoice-line { - background: #f8fafc; - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 1.5rem; - margin-bottom: 1rem; -} - -.line-header { +.lines-header-compact { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 1rem; + margin-bottom: 0.5rem; + flex-shrink: 0; } -.line-number { - font-weight: 700; +.lines-header-compact h3 { + margin: 0; + font-size: 0.95rem; 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); +.btn-add-compact { + background: #f1f5f9; color: var(--text-primary); border: 1px solid var(--border-color); - padding: 0.75rem 2rem; - border-radius: 8px; + padding: 0.3rem 0.6rem; + border-radius: 4px; cursor: pointer; - font-weight: 600; + font-weight: 500; + font-size: 0.85rem; transition: all 0.2s; } -.btn-cancel:hover { - background: #f1f5f9; +.btn-add-compact:hover { + background: #e2e8f0; + border-color: #cbd5e1; } -.btn-submit { - background: var(--primary); +.lines-table-compact { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.line-row-compact { + display: grid; + grid-template-columns: 4fr 0.8fr 1.2fr 0.8fr 1.2fr auto; + gap: 0.5rem; + align-items: center; + background: #f8fafc; + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 0.4rem; +} + +.line-desc-compact, +.line-qty-compact, +.line-price-compact, +.line-tax-compact { + padding: 0.35rem 0.4rem; + border: 1px solid var(--border-color); + border-radius: 3px; + font-size: 0.85rem; + background: white; + color: #000000; +} + +.line-desc-compact { + width: 100%; +} + +.line-desc-compact:focus, +.line-qty-compact:focus, +.line-price-compact:focus, +.line-tax-compact:focus { + outline: none; + border-color: var(--primary); +} + +.line-total-compact { + font-weight: 600; + color: var(--text-primary); + font-size: 0.9rem; + text-align: right; + padding-right: 0.5rem; +} + +.btn-delete-compact { + background: transparent; + color: #94a3b8; + border: 1px solid transparent; + width: 28px; + height: 28px; + border-radius: 4px; + cursor: pointer; + font-size: 1.4rem; + font-weight: 300; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + flex-shrink: 0; +} + +.btn-delete-compact:hover { + color: #64748b; + background: #f1f5f9; + border-color: #e2e8f0; +} + +.form-actions-compact { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 1rem; + padding-top: 0.75rem; + border-top: 1px solid var(--border-color); + flex-shrink: 0; +} + +.btn-cancel-compact { + background: #f8fafc; + color: #64748b; + border: 1px solid #e2e8f0; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-weight: 500; + font-size: 0.9rem; + transition: all 0.2s; +} + +.btn-cancel-compact:hover { + background: #f1f5f9; + color: #475569; +} + +.btn-submit-compact { + background: #334155; color: white; border: none; - padding: 0.75rem 2rem; - border-radius: 8px; + padding: 0.5rem 1.5rem; + border-radius: 4px; cursor: pointer; - font-weight: 600; - transition: background 0.2s; + font-weight: 500; + font-size: 0.9rem; + transition: all 0.2s; } -.btn-submit:hover { - background: var(--primary-hover); +.btn-submit-compact:hover { + background: #475569; } -.btn-submit:disabled { - background: #cbd5e1; +.btn-submit-compact:disabled { + background: #e2e8f0; + color: #94a3b8; cursor: not-allowed; } @media (max-width: 768px) { .create-invoice-page { - padding: 1rem; + padding: 0.5rem; + height: calc(100vh - 1rem); } - .create-invoice-header { - flex-direction: column; - align-items: flex-start; - gap: 1rem; - } - - .form-row, - .form-row-inline { + .form-grid-compact { grid-template-columns: 1fr; + gap: 0.5rem; } - .invoice-form { - padding: 1rem; + .form-field-compact.full-width { + grid-column: 1; + } + + .line-row-compact { + grid-template-columns: 1fr auto; + gap: 0.5rem; + padding: 0.75rem; + } + + .line-desc-compact { + grid-column: 1; + } + + .line-qty-compact, + .line-price-compact, + .line-tax-compact, + .line-total-compact { + width: 60px; + } + + .btn-delete-compact { + grid-column: 2; } } \ No newline at end of file From 84c99b8155501b03dad9bf89ef61bb748b15efb5 Mon Sep 17 00:00:00 2001 From: javiermengual Date: Tue, 3 Feb 2026 16:26:05 +0100 Subject: [PATCH 7/8] Add Invoice Modal component and integrate with invoice management --- index.html | 26 +- src/components/InvoiceItem.js | 10 +- src/components/InvoiceModal.js | 470 +++++++++++++++++++++++++++++++++ src/pages/Facturas.js | 15 +- src/services/invoices.js | 98 +++++++ src/style.css | 19 +- src/styles/modal.css | 392 +++++++++++++++++++++++++++ 7 files changed, 1005 insertions(+), 25 deletions(-) create mode 100644 src/components/InvoiceModal.js create mode 100644 src/services/invoices.js create mode 100644 src/styles/modal.css diff --git a/index.html b/index.html index 70d54c7..491f46d 100755 --- a/index.html +++ b/index.html @@ -1,13 +1,17 @@ - - - - - doli-front - - -
- - - + + + + + + doli-front + + + + +
+ + + + \ No newline at end of file diff --git a/src/components/InvoiceItem.js b/src/components/InvoiceItem.js index 5bc89d2..f79c2d7 100644 --- a/src/components/InvoiceItem.js +++ b/src/components/InvoiceItem.js @@ -1,4 +1,4 @@ -export function InvoiceItem(invoice) { +export function InvoiceItem(invoice, onView) { const item = document.createElement('div'); item.className = 'invoice-item'; @@ -58,7 +58,7 @@ export function InvoiceItem(invoice) {
${formatCurrency(invoice.total)}
${formatCurrency(invoice.remainToPay)}
-
@@ -67,9 +67,9 @@ export function InvoiceItem(invoice) { // 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}`); + if (onView) { + onView(invoice.id); + } }); return item; diff --git a/src/components/InvoiceModal.js b/src/components/InvoiceModal.js new file mode 100644 index 0000000..c02febd --- /dev/null +++ b/src/components/InvoiceModal.js @@ -0,0 +1,470 @@ +import { getInvoiceById, updateInvoice, validateInvoice, addInvoiceLine } from '../services/invoices.js'; + +export function InvoiceModal(invoiceId, onClose, onUpdate) { + const modal = document.createElement('div'); + modal.className = 'modal-overlay'; + + let invoice = null; + let isLoading = true; + + // Formatear fecha para input type="date" + const formatDateForInput = (dateString) => { + if (!dateString) return ''; + const date = new Date(dateString); + return date.toISOString().split('T')[0]; + }; + + // Formatear fecha para mostrar + const formatDate = (dateString) => { + if (!dateString) return '-'; + 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 || 0); + }; + + // Obtener texto de estado + const getStatusText = (status) => { + const statusTextMap = { + 'draft': 'Borrador', + 'validated': 'Validada', + 'paid': 'Pagada', + 'unpaid': 'Impagada', + 'canceled': 'Cancelada' + }; + return statusTextMap[status] || status; + }; + + // Renderizar contenido del modal + const renderContent = () => { + const modalContent = modal.querySelector('.modal-content'); + + if (isLoading) { + modalContent.innerHTML = ` + + + `; + return; + } + + if (!invoice) { + modalContent.innerHTML = ` + + + `; + return; + } + + const isDraft = invoice.status === 'draft'; + const canEdit = isDraft || invoice.status === 'validated' || invoice.status === 'unpaid'; + + modalContent.innerHTML = ` + + + + + + `; + + attachEventListeners(); + }; + + // Adjuntar event listeners + const attachEventListeners = () => { + // Botón cerrar + const closeBtn = modal.querySelector('.btn-close'); + closeBtn?.addEventListener('click', handleClose); + + // Botón cancelar modal + const cancelBtn = modal.querySelector('.btn-cancel-modal'); + cancelBtn?.addEventListener('click', handleClose); + + // Botón guardar + const saveBtn = modal.querySelector('.btn-save'); + saveBtn?.addEventListener('click', handleSave); + + // Botón validar + const validateBtn = modal.querySelector('.btn-validate'); + validateBtn?.addEventListener('click', handleValidate); + + // Botón añadir línea + const addLineBtn = modal.querySelector('.btn-add-line'); + addLineBtn?.addEventListener('click', toggleAddLineForm); + + // Botón guardar línea + const saveLineBtn = modal.querySelector('.btn-save-line'); + saveLineBtn?.addEventListener('click', handleSaveLine); + + // Botón cancelar línea + const cancelLineBtn = modal.querySelector('.btn-cancel-line'); + cancelLineBtn?.addEventListener('click', () => { + const form = modal.querySelector('.add-line-form'); + if (form) form.style.display = 'none'; + }); + + // Click fuera del modal + modal.addEventListener('click', (e) => { + if (e.target === modal) { + handleClose(); + } + }); + }; + + // Manejar cierre + const handleClose = () => { + modal.remove(); + if (onClose) onClose(); + }; + + // Manejar guardado + const handleSave = async () => { + const form = modal.querySelector('#invoice-form'); + const formData = new FormData(form); + + const data = { + number: formData.get('number') || undefined, + expireDate: formData.get('expireDate') || undefined, + notePublic: formData.get('notePublic') || undefined, + notePrivate: formData.get('notePrivate') || undefined + }; + + // Filtrar valores undefined + Object.keys(data).forEach(key => { + if (data[key] === undefined || data[key] === '') { + delete data[key]; + } + }); + + try { + const saveBtn = modal.querySelector('.btn-save'); + saveBtn.disabled = true; + saveBtn.textContent = 'Guardando...'; + + await updateInvoice(invoiceId, data); + + alert('Factura actualizada correctamente'); + if (onUpdate) onUpdate(); + handleClose(); + } catch (error) { + console.error('Error al guardar:', error); + alert('Error al guardar la factura: ' + error.message); + } finally { + const saveBtn = modal.querySelector('.btn-save'); + if (saveBtn) { + saveBtn.disabled = false; + saveBtn.textContent = 'Guardar Cambios'; + } + } + }; + + // Manejar validación + const handleValidate = async () => { + if (!confirm('¿Estás seguro de que quieres validar esta factura? No podrás editarla completamente después.')) { + return; + } + + try { + const validateBtn = modal.querySelector('.btn-validate'); + validateBtn.disabled = true; + validateBtn.textContent = 'Validando...'; + + await validateInvoice(invoiceId); + + alert('Factura validada correctamente'); + if (onUpdate) onUpdate(); + + // Recargar la factura para mostrar el nuevo estado + await loadInvoice(); + } catch (error) { + console.error('Error al validar:', error); + alert('Error al validar la factura: ' + error.message); + + const validateBtn = modal.querySelector('.btn-validate'); + if (validateBtn) { + validateBtn.disabled = false; + validateBtn.textContent = '✓ Validar Factura'; + } + } + }; + + // Mostrar/ocultar formulario de añadir línea + const toggleAddLineForm = () => { + const form = modal.querySelector('.add-line-form'); + if (form) { + const isVisible = form.style.display !== 'none'; + form.style.display = isVisible ? 'none' : 'block'; + + if (!isVisible) { + // Limpiar campos + modal.querySelector('#line-description').value = ''; + modal.querySelector('#line-quantity').value = '1'; + modal.querySelector('#line-price').value = '0'; + modal.querySelector('#line-tax').value = '21'; + } + } + }; + + // Guardar nueva línea + const handleSaveLine = async () => { + const description = modal.querySelector('#line-description').value.trim(); + const quantity = parseFloat(modal.querySelector('#line-quantity').value); + const unitPrice = parseFloat(modal.querySelector('#line-price').value); + const taxRate = parseFloat(modal.querySelector('#line-tax').value); + + if (!description) { + alert('La descripción es obligatoria'); + return; + } + + if (!quantity || quantity <= 0) { + alert('La cantidad debe ser mayor que 0'); + return; + } + + if (unitPrice < 0) { + alert('El precio no puede ser negativo'); + return; + } + + if (taxRate < 0 || taxRate > 100) { + alert('El IVA debe estar entre 0 y 100'); + return; + } + + try { + const saveBtn = modal.querySelector('.btn-save-line'); + saveBtn.disabled = true; + saveBtn.textContent = 'Guardando...'; + + await addInvoiceLine(invoiceId, { + description, + quantity, + unitPrice, + taxRate + }); + + alert('Línea añadida correctamente'); + toggleAddLineForm(); + await loadInvoice(); + } catch (error) { + console.error('Error al añadir línea:', error); + alert('Error al añadir línea: ' + error.message); + } finally { + const saveBtn = modal.querySelector('.btn-save-line'); + if (saveBtn) { + saveBtn.disabled = false; + saveBtn.textContent = 'Guardar Línea'; + } + } + }; + + // Cargar factura + const loadInvoice = async () => { + try { + isLoading = true; + renderContent(); + + invoice = await getInvoiceById(invoiceId); + isLoading = false; + renderContent(); + } catch (error) { + console.error('Error al cargar factura:', error); + isLoading = false; + invoice = null; + renderContent(); + } + }; + + // Inicializar modal + modal.innerHTML = ` + + `; + + // Cargar factura + loadInvoice(); + + return modal; +} diff --git a/src/pages/Facturas.js b/src/pages/Facturas.js index 2f935dc..1d1c5b3 100755 --- a/src/pages/Facturas.js +++ b/src/pages/Facturas.js @@ -1,4 +1,5 @@ import { InvoiceItem } from '../components/InvoiceItem.js'; +import { InvoiceModal } from '../components/InvoiceModal.js'; export function renderFacturasPage() { const container = document.createElement('div'); @@ -16,7 +17,6 @@ export function renderFacturasPage() { container.innerHTML = /*html*/`

Facturas

-
@@ -24,11 +24,11 @@ export function renderFacturasPage() { +
@@ -92,11 +92,20 @@ export function renderFacturasPage() { // Renderizar cada factura invoices.forEach(invoice => { - const invoiceItem = InvoiceItem(invoice); + const invoiceItem = InvoiceItem(invoice, handleViewInvoice); invoicesList.appendChild(invoiceItem); }); } + // Manejar ver/editar factura + function handleViewInvoice(invoiceId) { + const modal = InvoiceModal(invoiceId, null, () => { + // Callback cuando se actualiza la factura + loadAllInvoices(); + }); + document.body.appendChild(modal); + } + // Función para cargar todas las facturas async function loadAllInvoices() { const invoicesList = container.querySelector('.invoices-list'); diff --git a/src/services/invoices.js b/src/services/invoices.js new file mode 100644 index 0000000..bcfc386 --- /dev/null +++ b/src/services/invoices.js @@ -0,0 +1,98 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + +function getAuthHeaders() { + const token = localStorage.getItem('token'); + return { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }; +} + +export async function getInvoiceById(id) { + const response = await fetch(`${API_BASE_URL}/api/Invoices/${id}`, { + method: 'GET', + headers: getAuthHeaders() + }); + + if (!response.ok) { + throw new Error('Error al obtener la factura'); + } + + return await response.json(); +} + +export async function updateInvoice(id, data) { + const response = await fetch(`${API_BASE_URL}/api/Invoices/${id}`, { + method: 'PUT', + headers: getAuthHeaders(), + body: JSON.stringify(data) + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.detail || 'Error al actualizar la factura'); + } + + return true; +} + +export async function validateInvoice(id) { + const response = await fetch(`${API_BASE_URL}/api/Invoices/${id}/validate`, { + method: 'POST', + headers: getAuthHeaders() + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.detail || 'Error al validar la factura'); + } + + return true; +} + +export async function updateInvoiceStatus(id, status) { + const response = await fetch(`${API_BASE_URL}/api/Invoices/${id}/status`, { + method: 'PATCH', + headers: getAuthHeaders(), + body: JSON.stringify({ status }) + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.detail || 'Error al actualizar el estado'); + } + + return true; +} + +export async function addInvoiceLine(id, lineData) { + const response = await fetch(`${API_BASE_URL}/api/Invoices/${id}/lines`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify(lineData) + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.detail || 'Error al agregar línea'); + } + + return await response.json(); +} + +export async function deleteInvoice(id) { + // Nota: No hay endpoint DELETE en el OpenAPI spec, + // pero podríamos usar el endpoint de cambio de estado a "canceled" + // o implementarlo si está disponible en el backend + const response = await fetch(`${API_BASE_URL}/api/Invoices/${id}`, { + method: 'DELETE', + headers: getAuthHeaders() + }); + + if (!response.ok) { + // Si no existe DELETE, intentar cancelar + return await updateInvoiceStatus(id, 'canceled'); + } + + return true; +} diff --git a/src/style.css b/src/style.css index 5d8142e..82db186 100755 --- a/src/style.css +++ b/src/style.css @@ -31,6 +31,7 @@ a { color: var(--primary); text-decoration: inherit; } + a:hover { color: var(--primary-hover); } @@ -57,9 +58,11 @@ h1 { will-change: filter; transition: filter 300ms; } + .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } + .logo.vanilla:hover { filter: drop-shadow(0 0 2em #f7df1eaa); } @@ -84,9 +87,11 @@ button { cursor: pointer; transition: background-color 0.25s, border-color 0.25s; } + button:hover { background-color: var(--primary-hover); } + button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; @@ -97,6 +102,7 @@ button:focus-visible { color: var(--text-primary); background-color: #f8fafc; } + a:hover { color: var(--primary-hover); } @@ -405,7 +411,7 @@ button:focus-visible { background-color: #f8fafc; } -.main-content > div { +.main-content>div { max-width: 1280px; margin: 0 auto; padding: 2rem; @@ -440,9 +446,6 @@ button:focus-visible { } .facturas-header { - display: flex; - justify-content: space-between; - align-items: center; margin-bottom: 2rem; } @@ -461,6 +464,7 @@ button:focus-visible { cursor: pointer; font-weight: 600; transition: background-color 0.2s; + margin-left: auto; } .btn-new-invoice:hover { @@ -471,6 +475,7 @@ button:focus-visible { display: flex; gap: 1rem; margin-bottom: 1.5rem; + align-items: center; } .search-input, @@ -714,6 +719,7 @@ button:focus-visible { } @media (max-width: 1400px) { + .invoices-header, .invoice-item { grid-template-columns: 40px 120px 100px 50px 130px 100px 100px 100px 70px; @@ -758,13 +764,13 @@ button:focus-visible { padding: 1.5rem; } - .invoice-item > div { + .invoice-item>div { display: flex; justify-content: space-between; align-items: center; } - .invoice-item > div::before { + .invoice-item>div::before { content: attr(class); font-weight: 600; color: var(--text-secondary); @@ -815,6 +821,7 @@ button:focus-visible { min-height: 100vh; width: 100%; } + /* Create Invoice Page - Ultra Compact */ .create-invoice-page { padding: 1rem; diff --git a/src/styles/modal.css b/src/styles/modal.css new file mode 100644 index 0000000..4a32a54 --- /dev/null +++ b/src/styles/modal.css @@ -0,0 +1,392 @@ +/* Modal Styles */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; + animation: fadeIn 0.2s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.modal-content { + background: var(--card-bg); + border-radius: 12px; + max-width: 900px; + width: 100%; + max-height: 90vh; + display: flex; + flex-direction: column; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + animation: slideUp 0.3s ease-out; +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } +} + +.modal-header { + padding: 1.5rem 2rem; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; +} + +.modal-title { + display: flex; + align-items: center; + gap: 1rem; +} + +.modal-title h2 { + margin: 0; + font-size: 1.5rem; + color: var(--text-primary); +} + +.btn-close { + background: none; + border: none; + font-size: 2rem; + line-height: 1; + color: var(--text-secondary); + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s; +} + +.btn-close:hover { + background-color: #f1f5f9; + color: var(--text-primary); +} + +.modal-body { + padding: 2rem; + overflow-y: auto; + flex: 1; +} + +.modal-footer { + padding: 1.5rem 2rem; + border-top: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; + gap: 1rem; +} + +.footer-actions-left, +.footer-actions-right { + display: flex; + gap: 0.75rem; +} + +/* Form Styles in Modal */ +.form-section { + margin-bottom: 2rem; +} + +.form-section:last-child { + margin-bottom: 0; +} + +.form-section h3 { + margin: 0 0 1rem 0; + font-size: 1.125rem; + color: var(--text-primary); + font-weight: 600; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; +} + +.form-row { + display: grid; + grid-template-columns: 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-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.form-group input, +.form-group textarea, +.form-group select { + padding: 0.625rem 0.75rem; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 0.9375rem; + transition: all 0.2s; + font-family: inherit; + background-color: #ffffff; +} + +.form-group input:focus, +.form-group textarea:focus, +.form-group select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); +} + +.form-group input:disabled, +.form-group textarea:disabled, +.form-group select:disabled { + background-color: #f8fafc; + color: var(--text-secondary); + cursor: not-allowed; +} + +.form-group input[readonly] { + background-color: #f8fafc; + color: var(--text-secondary); +} + +/* Add Line Form */ +.add-line-form { + background-color: #f8fafc; + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 1rem; + border: 2px dashed var(--border-color); +} + +.form-group textarea { + resize: vertical; + min-height: 80px; +} + +/* Invoice Lines Table */ +.invoice-lines { + margin: 1rem 0; +} + +.lines-table { + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; +} + +.lines-table thead { + background-color: #f8fafc; +} + +.lines-table th { + padding: 0.75rem; + text-align: left; + font-weight: 600; + color: var(--text-secondary); + border-bottom: 2px solid var(--border-color); +} + +.lines-table td { + padding: 0.75rem; + border-bottom: 1px solid var(--border-color); +} + +.lines-table tbody tr:hover { + background-color: #f8fafc; +} + +.no-lines { + text-align: center; + color: var(--text-secondary); + font-style: italic; +} + +/* Invoice Totals */ +.invoice-totals { + margin-top: 1.5rem; + padding: 1rem; + background-color: #f8fafc; + border-radius: 8px; + display: flex; + flex-direction: column; + gap: 0.75rem; + max-width: 300px; + margin-left: auto; +} + +.total-row { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.9375rem; +} + +.total-row.pending { + font-size: 1.125rem; + color: var(--primary); + padding-top: 0.75rem; + border-top: 2px solid var(--border-color); +} + +/* Button Styles */ +.btn-small { + padding: 0.5rem 1rem; + font-size: 0.875rem; +} + +.btn-primary { + background-color: var(--primary); + color: white; +} + +.btn-primary:hover { + background-color: var(--primary-hover); +} + +.btn-success { + background-color: var(--success); + color: white; +} + +.btn-success:hover { + background-color: var(--success-hover); +} + +.btn-cancel { + background-color: transparent; + color: var(--text-secondary); + border: 1px solid var(--border-color); +} + +.btn-cancel:hover { + background-color: #f8fafc; + color: var(--text-primary); +} + +/* Invoice Actions Buttons */ +.invoice-actions { + display: flex; + gap: 0.5rem; + align-items: center; + justify-content: center; +} + +.btn-action { + background: none; + border: none; + font-size: 1.25rem; + cursor: pointer; + padding: 0.25rem 0.5rem; + border-radius: 4px; + transition: all 0.2s; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.btn-action:hover { + background-color: #f1f5f9; + transform: scale(1.1); +} + +.btn-delete:hover { + background-color: #fee2e2; +} + +/* Loading and Error States */ +.loading { + text-align: center; + padding: 2rem; + color: var(--text-secondary); +} + +.error { + text-align: center; + padding: 2rem; + color: var(--danger); +} + +/* Responsive Modal */ +@media (max-width: 768px) { + .modal-overlay { + padding: 0; + } + + .modal-content { + max-width: 100%; + max-height: 100vh; + border-radius: 0; + } + + .modal-header, + .modal-body, + .modal-footer { + padding: 1rem; + } + + .form-row { + grid-template-columns: 1fr; + gap: 0.75rem; + } + + .invoice-totals { + max-width: 100%; + } + + .modal-footer { + flex-direction: column; + } + + .footer-actions-left, + .footer-actions-right { + width: 100%; + } + + .footer-actions-left button, + .footer-actions-right button { + flex: 1; + } +} \ No newline at end of file From c5aab5e1fcd438f8958b6afdd2819ac040434cb6 Mon Sep 17 00:00:00 2001 From: javiermengual Date: Tue, 3 Feb 2026 16:55:40 +0100 Subject: [PATCH 8/8] Refactor invoice components for improved structure and styling --- src/components/InvoiceItem.js | 29 ++--- src/components/InvoiceModal.js | 19 ++- src/pages/Facturas.js | 47 ++++--- src/pages/pagesRegistry.js | 19 +-- src/style.css | 222 +++++++++++++++------------------ src/styles/modal.css | 1 + 6 files changed, 150 insertions(+), 187 deletions(-) diff --git a/src/components/InvoiceItem.js b/src/components/InvoiceItem.js index f79c2d7..275e0e8 100644 --- a/src/components/InvoiceItem.js +++ b/src/components/InvoiceItem.js @@ -1,5 +1,5 @@ export function InvoiceItem(invoice, onView) { - const item = document.createElement('div'); + const item = document.createElement('tr'); item.className = 'invoice-item'; // Formatear fecha @@ -41,27 +41,24 @@ export function InvoiceItem(invoice, onView) { }; item.innerHTML = /*html*/` -
- -
-
${invoice.number}
-
+ ${invoice.number} + ${getStatusText(invoice.status)} -
-
- 👤 + + + ${invoice.clientName || 'Sin nombre'} -
-
${formatDate(invoice.date)}
-
${formatCurrency(invoice.total)}
-
${formatCurrency(invoice.remainToPay)}
-
+ + ${formatDate(invoice.date)} + ${formatCurrency(invoice.total)} + ${formatCurrency(invoice.remainToPay)} + -
+ `; // Event listener para el botón de ver diff --git a/src/components/InvoiceModal.js b/src/components/InvoiceModal.js index c02febd..42f1bb2 100644 --- a/src/components/InvoiceModal.js +++ b/src/components/InvoiceModal.js @@ -138,20 +138,14 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {
- +
- +
@@ -239,7 +233,7 @@ export function InvoiceModal(invoiceId, onClose, onUpdate) {