Crear si no lo esta y actualizar facturas
This commit is contained in:
parent
6408d3325b
commit
c2b23ddf4c
|
|
@ -52,7 +52,7 @@ export function InvoiceItem(invoice) {
|
||||||
</div>
|
</div>
|
||||||
<div class="invoice-client">
|
<div class="invoice-client">
|
||||||
<span class="client-icon">👤</span>
|
<span class="client-icon">👤</span>
|
||||||
Cliente #${invoice.clientId}
|
${invoice.clientName || 'Sin nombre'}
|
||||||
</div>
|
</div>
|
||||||
<div class="invoice-date">${formatDate(invoice.date)}</div>
|
<div class="invoice-date">${formatDate(invoice.date)}</div>
|
||||||
<div class="invoice-total">${formatCurrency(invoice.total)}</div>
|
<div class="invoice-total">${formatCurrency(invoice.total)}</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,297 @@
|
||||||
|
export function renderCreateInvoicePage() {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'create-invoice-page';
|
||||||
|
|
||||||
|
container.innerHTML = /*html*/`
|
||||||
|
<div class="create-invoice-header">
|
||||||
|
<div>
|
||||||
|
<h1>Nuevo Borrador</h1>
|
||||||
|
<p class="subtitle">Crear un nuevo borrador</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn-back" id="back-btn">← Volver a Facturas</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="invoice-form" id="invoice-form">
|
||||||
|
<div class="form-section">
|
||||||
|
<h2>Información General</h2>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="clientId">Cliente ID *</label>
|
||||||
|
<input type="number" id="clientId" name="clientId" required min="1" placeholder="ID del cliente" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="reference">Referencia</label>
|
||||||
|
<input type="text" id="reference" name="reference" placeholder="Ej: FAC-2026-001" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="date">Fecha *</label>
|
||||||
|
<input type="date" id="date" name="date" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="expireDate">Fecha de vencimiento *</label>
|
||||||
|
<input type="date" id="expireDate" name="expireDate" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="notePublic">Nota pública</label>
|
||||||
|
<textarea id="notePublic" name="notePublic" rows="3" placeholder="Nota visible para el cliente"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label for="notePrivate">Nota privada</label>
|
||||||
|
<textarea id="notePrivate" name="notePrivate" rows="3" placeholder="Nota interna"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>Líneas de Borrador</h2>
|
||||||
|
<button type="button" class="btn-add-line" id="add-line-btn">+ Añadir Línea</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="invoice-lines-container">
|
||||||
|
<!-- Las líneas se agregarán dinámicamente aquí -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn-cancel" id="cancel-btn">Cancelar</button>
|
||||||
|
<button type="submit" class="btn-submit">Crear Borrador</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Establecer fecha de hoy por defecto
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
container.querySelector('#date').value = today;
|
||||||
|
|
||||||
|
// Establecer fecha de vencimiento (30 días después)
|
||||||
|
const expireDate = new Date();
|
||||||
|
expireDate.setDate(expireDate.getDate() + 30);
|
||||||
|
container.querySelector('#expireDate').value = expireDate.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Contador de líneas
|
||||||
|
let lineCounter = 0;
|
||||||
|
|
||||||
|
// Función para crear una nueva línea de borrador
|
||||||
|
function createInvoiceLine() {
|
||||||
|
lineCounter++;
|
||||||
|
const lineDiv = document.createElement('div');
|
||||||
|
lineDiv.className = 'invoice-line';
|
||||||
|
lineDiv.dataset.lineId = lineCounter;
|
||||||
|
|
||||||
|
lineDiv.innerHTML = /*html*/`
|
||||||
|
<div class="line-header">
|
||||||
|
<span class="line-number">Línea ${lineCounter}</span>
|
||||||
|
<button type="button" class="btn-remove-line" data-line-id="${lineCounter}">🗑️ Eliminar</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="line-content">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Descripción *</label>
|
||||||
|
<input type="text" class="line-description" required placeholder="Descripción del producto/servicio" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row-inline">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Cantidad *</label>
|
||||||
|
<input type="number" class="line-quantity" required min="0.01" step="0.01" value="1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Precio unitario (€) *</label>
|
||||||
|
<input type="number" class="line-unitPrice" required min="0" step="0.01" placeholder="0.00" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>IVA (%) *</label>
|
||||||
|
<input type="number" class="line-taxRate" required min="0" max="100" step="0.01" value="21" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Subtotal</label>
|
||||||
|
<input type="text" class="line-subtotal" readonly value="0.00 €" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Event listeners para calcular subtotal
|
||||||
|
const quantityInput = lineDiv.querySelector('.line-quantity');
|
||||||
|
const priceInput = lineDiv.querySelector('.line-unitPrice');
|
||||||
|
const subtotalInput = lineDiv.querySelector('.line-subtotal');
|
||||||
|
|
||||||
|
function updateSubtotal() {
|
||||||
|
const quantity = parseFloat(quantityInput.value) || 0;
|
||||||
|
const price = parseFloat(priceInput.value) || 0;
|
||||||
|
const subtotal = quantity * price;
|
||||||
|
subtotalInput.value = `${subtotal.toFixed(2)} €`;
|
||||||
|
updateTotal();
|
||||||
|
}
|
||||||
|
|
||||||
|
quantityInput.addEventListener('input', updateSubtotal);
|
||||||
|
priceInput.addEventListener('input', updateSubtotal);
|
||||||
|
|
||||||
|
// Event listener para eliminar línea
|
||||||
|
lineDiv.querySelector('.btn-remove-line').addEventListener('click', () => {
|
||||||
|
lineDiv.remove();
|
||||||
|
updateLineNumbers();
|
||||||
|
updateTotal();
|
||||||
|
});
|
||||||
|
|
||||||
|
return lineDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para actualizar números de línea
|
||||||
|
function updateLineNumbers() {
|
||||||
|
const lines = container.querySelectorAll('.invoice-line');
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
line.querySelector('.line-number').textContent = `Línea ${index + 1}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para actualizar el total
|
||||||
|
function updateTotal() {
|
||||||
|
const lines = container.querySelectorAll('.invoice-line');
|
||||||
|
let total = 0;
|
||||||
|
lines.forEach(line => {
|
||||||
|
const quantity = parseFloat(line.querySelector('.line-quantity').value) || 0;
|
||||||
|
const price = parseFloat(line.querySelector('.line-unitPrice').value) || 0;
|
||||||
|
const taxRate = parseFloat(line.querySelector('.line-taxRate').value) || 0;
|
||||||
|
const subtotal = quantity * price;
|
||||||
|
const withTax = subtotal * (1 + taxRate / 100);
|
||||||
|
total += withTax;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mostrar total en algún lugar si lo deseas
|
||||||
|
console.log('Total borrador:', total.toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agregar primera línea por defecto
|
||||||
|
const linesContainer = container.querySelector('#invoice-lines-container');
|
||||||
|
linesContainer.appendChild(createInvoiceLine());
|
||||||
|
|
||||||
|
// Event listener para añadir línea
|
||||||
|
container.querySelector('#add-line-btn').addEventListener('click', () => {
|
||||||
|
linesContainer.appendChild(createInvoiceLine());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listener para volver
|
||||||
|
container.querySelector('#back-btn').addEventListener('click', () => {
|
||||||
|
window.location.hash = '#invoices';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listener para cancelar
|
||||||
|
container.querySelector('#cancel-btn').addEventListener('click', () => {
|
||||||
|
if (confirm('¿Estás seguro de que quieres cancelar? Se perderán los datos no guardados.')) {
|
||||||
|
window.location.hash = '#invoices';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listener para enviar formulario
|
||||||
|
container.querySelector('#invoice-form').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Recopilar datos del formulario
|
||||||
|
const clientIdValue = parseInt(container.querySelector('#clientId').value);
|
||||||
|
|
||||||
|
// Validar clientId
|
||||||
|
if (!clientIdValue || isNaN(clientIdValue) || clientIdValue < 1) {
|
||||||
|
alert('Por favor ingresa un ID de cliente válido');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
clientId: clientIdValue,
|
||||||
|
date: container.querySelector('#date').value + 'T00:00:00',
|
||||||
|
expireDate: container.querySelector('#expireDate').value + 'T00:00:00',
|
||||||
|
reference: container.querySelector('#reference').value || null,
|
||||||
|
notePublic: container.querySelector('#notePublic').value || null,
|
||||||
|
notePrivate: container.querySelector('#notePrivate').value || null,
|
||||||
|
lines: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recopilar líneas
|
||||||
|
const lines = container.querySelectorAll('.invoice-line');
|
||||||
|
if (lines.length === 0) {
|
||||||
|
alert('Debes agregar al menos una línea al borrador');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
const lineData = {
|
||||||
|
description: line.querySelector('.line-description').value,
|
||||||
|
quantity: parseFloat(line.querySelector('.line-quantity').value),
|
||||||
|
unitPrice: parseFloat(line.querySelector('.line-unitPrice').value),
|
||||||
|
taxRate: parseFloat(line.querySelector('.line-taxRate').value)
|
||||||
|
};
|
||||||
|
formData.lines.push(lineData);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validar que todos los campos requeridos estén completos
|
||||||
|
if (!formData.clientId || formData.lines.some(l => !l.description || !l.quantity || !l.unitPrice)) {
|
||||||
|
alert('Por favor completa todos los campos obligatorios');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar a la API
|
||||||
|
try {
|
||||||
|
const submitBtn = container.querySelector('.btn-submit');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.textContent = 'Creando...';
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/Invoices`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(error || 'Error al crear el borrador');
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoiceId = await response.json();
|
||||||
|
alert(`Borrador creado exitosamente con ID: ${invoiceId}`);
|
||||||
|
|
||||||
|
// Redirigir inmediatamente a la lista de facturas
|
||||||
|
window.location.hash = '#invoices';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al crear borrador:', error);
|
||||||
|
|
||||||
|
// Mensaje de error más específico
|
||||||
|
let errorMessage = 'Error al crear el borrador';
|
||||||
|
if (error.message.includes('foreign key constraint')) {
|
||||||
|
errorMessage = 'Error: El cliente con ese ID no existe. Por favor verifica el ID del cliente.';
|
||||||
|
} else if (error.message) {
|
||||||
|
errorMessage = `Error: ${error.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(errorMessage);
|
||||||
|
|
||||||
|
const submitBtn = container.querySelector('.btn-submit');
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = 'Crear Borrador';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,9 @@ export function renderFacturasPage() {
|
||||||
let totalPages = 1;
|
let totalPages = 1;
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
let allInvoices = []; // Almacenar todas las facturas
|
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*/`
|
container.innerHTML = /*html*/`
|
||||||
<div class="facturas-header">
|
<div class="facturas-header">
|
||||||
|
|
@ -55,19 +58,25 @@ export function renderFacturasPage() {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Función para aplicar filtros combinados
|
||||||
|
function applyFilters() {
|
||||||
|
// Ya no filtra localmente, hace llamada a la API
|
||||||
|
loadAllInvoices();
|
||||||
|
}
|
||||||
|
|
||||||
// Función para renderizar la página actual
|
// Función para renderizar la página actual
|
||||||
function renderPage(page = 1) {
|
function renderPage(page = 1) {
|
||||||
const invoicesList = container.querySelector('.invoices-list');
|
const invoicesList = container.querySelector('.invoices-list');
|
||||||
|
|
||||||
currentPage = page;
|
currentPage = page;
|
||||||
totalPages = Math.ceil(allInvoices.length / pageSize);
|
totalPages = Math.ceil(filteredInvoices.length / pageSize);
|
||||||
|
|
||||||
// Calcular índices para la página
|
// Calcular índices para la página
|
||||||
const startIndex = (page - 1) * pageSize;
|
const startIndex = (page - 1) * pageSize;
|
||||||
const endIndex = Math.min(startIndex + pageSize, allInvoices.length);
|
const endIndex = Math.min(startIndex + pageSize, filteredInvoices.length);
|
||||||
const invoices = allInvoices.slice(startIndex, endIndex);
|
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
|
// Actualizar UI de paginación
|
||||||
updatePaginationUI();
|
updatePaginationUI();
|
||||||
|
|
@ -96,16 +105,25 @@ export function renderFacturasPage() {
|
||||||
invoicesList.innerHTML = '<div class="loading">Cargando facturas...</div>';
|
invoicesList.innerHTML = '<div class="loading">Cargando facturas...</div>';
|
||||||
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
const response = await fetch(
|
|
||||||
`${import.meta.env.VITE_API_BASE_URL}/api/Invoices`,
|
// Construir URL con parámetros
|
||||||
{
|
let url = `${import.meta.env.VITE_API_BASE_URL}/api/Invoices?limit=1000`;
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
if (currentFilter) {
|
||||||
'Authorization': `Bearer ${token}`,
|
url += `&status=${currentFilter}`;
|
||||||
'Content-Type': 'application/json'
|
}
|
||||||
}
|
|
||||||
|
if (searchTerm) {
|
||||||
|
url += `&search=${encodeURIComponent(searchTerm)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Error al cargar facturas');
|
throw new Error('Error al cargar facturas');
|
||||||
|
|
@ -115,6 +133,7 @@ export function renderFacturasPage() {
|
||||||
|
|
||||||
// Manejo de respuesta - obtener todas las facturas
|
// Manejo de respuesta - obtener todas las facturas
|
||||||
allInvoices = Array.isArray(data) ? data : data.data || data.invoices || [];
|
allInvoices = Array.isArray(data) ? data : data.data || data.invoices || [];
|
||||||
|
filteredInvoices = [...allInvoices];
|
||||||
|
|
||||||
console.log(`Total de facturas cargadas: ${allInvoices.length}`);
|
console.log(`Total de facturas cargadas: ${allInvoices.length}`);
|
||||||
|
|
||||||
|
|
@ -129,6 +148,7 @@ export function renderFacturasPage() {
|
||||||
|
|
||||||
// Función para actualizar la UI de paginación
|
// Función para actualizar la UI de paginación
|
||||||
function updatePaginationUI() {
|
function updatePaginationUI() {
|
||||||
|
const paginationDiv = container.querySelector('.pagination');
|
||||||
const currentPageSpan = container.querySelector('.current-page');
|
const currentPageSpan = container.querySelector('.current-page');
|
||||||
const totalPagesSpan = container.querySelector('.total-pages');
|
const totalPagesSpan = container.querySelector('.total-pages');
|
||||||
const btnPrev = container.querySelector('.btn-prev');
|
const btnPrev = container.querySelector('.btn-prev');
|
||||||
|
|
@ -137,6 +157,13 @@ export function renderFacturasPage() {
|
||||||
currentPageSpan.textContent = currentPage;
|
currentPageSpan.textContent = currentPage;
|
||||||
totalPagesSpan.textContent = totalPages;
|
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
|
// Deshabilitar botones según corresponda
|
||||||
btnPrev.disabled = currentPage === 1;
|
btnPrev.disabled = currentPage === 1;
|
||||||
btnNext.disabled = currentPage === totalPages;
|
btnNext.disabled = currentPage === totalPages;
|
||||||
|
|
@ -145,8 +172,7 @@ export function renderFacturasPage() {
|
||||||
// Event listeners
|
// Event listeners
|
||||||
const newInvoiceBtn = container.querySelector('.btn-new-invoice');
|
const newInvoiceBtn = container.querySelector('.btn-new-invoice');
|
||||||
newInvoiceBtn.addEventListener('click', () => {
|
newInvoiceBtn.addEventListener('click', () => {
|
||||||
console.log('Crear nueva factura');
|
window.location.hash = '#create-invoice';
|
||||||
alert('Funcionalidad de crear factura pendiente');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectAllCheckbox = container.querySelector('#select-all');
|
const selectAllCheckbox = container.querySelector('#select-all');
|
||||||
|
|
@ -157,27 +183,26 @@ export function renderFacturasPage() {
|
||||||
|
|
||||||
const searchInput = container.querySelector('.search-input');
|
const searchInput = container.querySelector('.search-input');
|
||||||
searchInput.addEventListener('input', (e) => {
|
searchInput.addEventListener('input', (e) => {
|
||||||
const searchTerm = e.target.value.toLowerCase();
|
const inputValue = e.target.value;
|
||||||
const items = container.querySelectorAll('.invoice-item');
|
// Debounce para no hacer muchas llamadas
|
||||||
items.forEach(item => {
|
clearTimeout(searchInput.debounceTimer);
|
||||||
const text = item.textContent.toLowerCase();
|
searchInput.debounceTimer = setTimeout(() => {
|
||||||
item.style.display = text.includes(searchTerm) ? 'grid' : 'none';
|
// 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');
|
const filterStatus = container.querySelector('.filter-status');
|
||||||
filterStatus.addEventListener('change', (e) => {
|
filterStatus.addEventListener('change', (e) => {
|
||||||
const status = e.target.value;
|
currentFilter = e.target.value;
|
||||||
const items = container.querySelectorAll('.invoice-item');
|
console.log(`Filtro aplicado: ${currentFilter || 'Todos'}`);
|
||||||
items.forEach(item => {
|
applyFilters();
|
||||||
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';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Event listeners de paginación
|
// Event listeners de paginación
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { renderDashboard } from './DashboardPage.js';
|
import { renderDashboard } from './DashboardPage.js';
|
||||||
import { renderTestPage } from './test.js';
|
import { renderTestPage } from './test.js';
|
||||||
import { renderFacturasPage } from './Facturas.js';
|
import { renderFacturasPage } from './Facturas.js';
|
||||||
|
import { renderCreateInvoicePage } from './CreateInvoicePage.js';
|
||||||
|
|
||||||
// Registry of all pages available in the application
|
// Registry of all pages available in the application
|
||||||
export const pagesRegistry = [
|
export const pagesRegistry = [
|
||||||
|
|
@ -28,6 +29,14 @@ export const pagesRegistry = [
|
||||||
showInSidebar: true,
|
showInSidebar: true,
|
||||||
render: renderFacturasPage
|
render: renderFacturasPage
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
route: 'create-invoice',
|
||||||
|
name: 'Nueva Factura',
|
||||||
|
icon: '➕',
|
||||||
|
requiresAuth: true,
|
||||||
|
showInSidebar: false, // No mostrar en sidebar
|
||||||
|
render: renderCreateInvoicePage
|
||||||
|
},
|
||||||
{
|
{
|
||||||
route: 'clients',
|
route: 'clients',
|
||||||
name: 'Clientes',
|
name: 'Clientes',
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { createSidebar } from './components/Sidebar.js';
|
||||||
import { getAvailablePages, getPageByRoute } from './pages/pagesRegistry.js';
|
import { getAvailablePages, getPageByRoute } from './pages/pagesRegistry.js';
|
||||||
|
|
||||||
// 🔧 Modo DEV: cambiar a false para activar login
|
// 🔧 Modo DEV: cambiar a false para activar login
|
||||||
const DEV_MODE = true;
|
const DEV_MODE = false;
|
||||||
|
|
||||||
export function initRouter() {
|
export function initRouter() {
|
||||||
const app = document.querySelector('#app');
|
const app = document.querySelector('#app');
|
||||||
|
|
|
||||||
246
src/style.css
246
src/style.css
|
|
@ -815,3 +815,249 @@ button:focus-visible {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
/* Create Invoice Page Styles */
|
||||||
|
.create-invoice-page {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-invoice-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-invoice-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-invoice-header .subtitle {
|
||||||
|
margin: 0.5rem 0 0 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-form {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 2px solid var(--primary);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-line {
|
||||||
|
background: var(--success);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-line:hover {
|
||||||
|
background: var(--success-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row-inline {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group textarea,
|
||||||
|
.form-group select {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #000000;
|
||||||
|
background: var(--card-bg);
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group textarea:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[readonly] {
|
||||||
|
background: #f1f5f9;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-line {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-number {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove-line {
|
||||||
|
background: var(--danger);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove-line:hover {
|
||||||
|
background: var(--danger-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover {
|
||||||
|
background: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:disabled {
|
||||||
|
background: #cbd5e1;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.create-invoice-page {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-invoice-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row,
|
||||||
|
.form-row-inline {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-form {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue