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