// Enhanced Invoices Page Functionality let currentView = 'search'; let boardInvoices = new Map(); // Store invoice data for board view let draggedInvoice = null; let lastSearchResults = []; // Store the most recent search results // Initialize when page loads document.addEventListener('DOMContentLoaded', function () { initializeInvoicesPage(); }); function initializeInvoicesPage() { // Setup search input enhancements const searchInput = document.getElementById('invoice-search-input'); if (searchInput) { setupSearchEnhancements(searchInput); } // Initialize drag and drop for board view initializeBoardDragDrop(); // Setup HTMX event listeners to capture search results setupSearchResultCapture(); console.log('Invoices page initialized'); } // Setup listeners to capture search results from HTMX function setupSearchResultCapture() { // Listen for HTMX after swap events to capture search results document.body.addEventListener('htmx:afterSwap', function (e) { if (e.detail.target && e.detail.target.id === 'invoice-search-results') { console.log('Search results updated, extracting invoice data...'); extractInvoiceDataFromSearchResults(); } }); } // Extract invoice data from search results HTML function extractInvoiceDataFromSearchResults() { const resultsDiv = document.getElementById('invoice-search-results'); const invoiceCards = resultsDiv.querySelectorAll('.invoice-card'); // Store new search results separately const newSearchResults = []; invoiceCards.forEach(card => { const invoiceData = extractInvoiceDataFromCard(card); if (invoiceData) { newSearchResults.push(invoiceData); console.log('Extracted invoice data:', invoiceData); } }); console.log(`Captured ${newSearchResults.length} new invoices from search results`); // Add new results to existing results (avoid duplicates) newSearchResults.forEach(newInvoice => { const exists = lastSearchResults.some(existing => existing.id === newInvoice.id || existing.invoiceNumber === newInvoice.invoiceNumber ); if (!exists) { lastSearchResults.push(newInvoice); console.log(`Added new invoice to collection: ${newInvoice.id}`); } else { console.log(`Invoice already exists in collection: ${newInvoice.id}`); } }); // If we're currently in board view, add the new invoices to the board if (currentView === 'board') { addNewInvoicesToBoard(newSearchResults); } } // Extract invoice data from a single invoice card element function extractInvoiceDataFromCard(card) { try { // Get invoice number from header const headerElement = card.querySelector('.invoice-header h4'); const invoiceNumberMatch = headerElement ? headerElement.textContent.match(/Invoice #(.+)/) : null; const invoiceNumber = invoiceNumberMatch ? invoiceNumberMatch[1] : null; // Get status const statusElement = card.querySelector('.invoice-status'); const status = statusElement ? statusElement.textContent.trim() : null; // Get the real invoice ID from data attribute (this is the numeric ID we need for API calls) const realInvoiceId = card.dataset.invoiceId; // If we don't have the real ID from data attribute, try to extract from card ID let id = realInvoiceId; if (!id) { const cardId = card.id; const idMatch = cardId.match(/invoice-card-(.+)/); id = idMatch ? idMatch[1] : invoiceNumber; } console.log(`Extracted invoice data: ID=${id}, Number=${invoiceNumber}, Status=${status}`); // Extract customer name const customerElement = Array.from(card.querySelectorAll('.invoice-info p')).find(p => p.innerHTML.includes('Customer:') ); const customerName = customerElement ? customerElement.innerHTML.replace('Customer:', '').trim() : null; // Extract job name const jobElement = Array.from(card.querySelectorAll('.invoice-info p')).find(p => p.innerHTML.includes('Job:') ); const jobName = jobElement ? jobElement.innerHTML.replace('Job:', '').trim() : null; // Extract total price const totalElement = Array.from(card.querySelectorAll('.invoice-info p')).find(p => p.innerHTML.includes('Total:') ); const totalMatch = totalElement ? totalElement.innerHTML.match(/Total:<\/strong>\s*\$(.+)/) : null; const totalPrice = totalMatch ? totalMatch[1].trim() : '0.00'; return { id: id, invoiceNumber: invoiceNumber, status: status, customer: customerName ? { name: customerName } : null, job: jobName ? { name: jobName } : null, totalPrice: totalPrice }; } catch (error) { console.error('Error extracting invoice data from card:', error); return null; } } // Search functionality enhancements function setupSearchEnhancements(searchInput) { const clearBtn = document.querySelector('.search-clear-btn'); searchInput.addEventListener('input', function () { if (this.value.length > 0) { clearBtn.style.display = 'block'; } else { clearBtn.style.display = 'none'; } }); // Add keyboard shortcuts searchInput.addEventListener('keydown', function (e) { if (e.key === 'Escape') { clearSearch(); } }); } function clearSearch() { const searchInput = document.getElementById('invoice-search-input'); const clearBtn = document.querySelector('.search-clear-btn'); const resultsDiv = document.getElementById('invoice-search-results'); searchInput.value = ''; clearBtn.style.display = 'none'; // Clear stored search results lastSearchResults = []; // Reset to no search state resultsDiv.innerHTML = `
📋

Start searching for invoices

Enter an invoice ID, customer name, or job number above to begin

`; // Don't clear board when clearing search - let users keep their board intact console.log('Search cleared, but board preserved'); } // View switching functionality (modified to preserve board) function switchView(viewName) { const searchView = document.getElementById('search-view'); const boardView = document.getElementById('board-view'); const searchBtn = document.getElementById('search-view-btn'); const boardBtn = document.getElementById('board-view-btn'); // Update active states searchBtn.classList.toggle('active', viewName === 'search'); boardBtn.classList.toggle('active', viewName === 'board'); // Show/hide views searchView.classList.toggle('active', viewName === 'search'); boardView.classList.toggle('active', viewName === 'board'); currentView = viewName; // If switching to board view, only populate if board is empty if (viewName === 'board') { const existingCards = document.querySelectorAll('.board-invoice-card'); if (existingCards.length === 0 && lastSearchResults.length > 0) { populateBoardWithSearchResults(); } else if (existingCards.length > 0) { console.log(`Board already has ${existingCards.length} invoices, keeping them`); } else { checkBoardState(); } } console.log(`Switched to ${viewName} view`); } // Populate board with search results (modified to be explicit about clearing) function populateBoardWithSearchResults() { console.log(`Populating board with ${lastSearchResults.length} search results`); // Clear existing board content clearBoard(); // Add each search result to the board lastSearchResults.forEach(invoiceData => { addInvoiceToBoard(invoiceData); }); // Debug the board state after population debugBoardState(); showMessage(`Added ${lastSearchResults.length} invoices to board`, 'success'); } // Clear all invoices from the board function clearBoard() { document.querySelectorAll('.invoice-drop-zone').forEach(zone => { // Remove all invoice cards const cards = zone.querySelectorAll('.board-invoice-card'); cards.forEach(card => card.remove()); // Show empty message const emptyMessage = zone.querySelector('.empty-column-message'); if (emptyMessage) { emptyMessage.style.display = 'block'; } }); // Clear stored data boardInvoices.clear(); // Update counts updateColumnCounts(); console.log('Board cleared'); } // Board functionality function refreshBoard() { console.log('Refreshing board...'); // If we have search results, refresh from those if (lastSearchResults.length > 0) { showBoardLoading(true); setTimeout(() => { populateBoardWithSearchResults(); showBoardLoading(false); showMessage('Board refreshed with search results', 'success'); }, 500); } else { showBoardLoading(true); setTimeout(() => { showBoardLoading(false); showMessage('No search results to refresh. Search for invoices first.', 'info'); }, 500); } } function loadRecentInvoices() { console.log('Loading recent invoices...'); showBoardLoading(true); // TODO: Implement actual API call to load recent invoices // For now, show a helpful message setTimeout(() => { showBoardLoading(false); showMessage('Sorry, this feature is not yet implemented', 'info'); }, 1000); } function checkBoardState() { const board = document.getElementById('invoice-board'); const invoiceCards = board.querySelectorAll('.board-invoice-card'); if (invoiceCards.length === 0) { console.log('Board is empty, showing helpful message'); } } function showBoardLoading(show) { const loading = document.getElementById('board-loading'); loading.style.display = show ? 'flex' : 'none'; } // Kanban drag and drop functionality function initializeBoardDragDrop() { console.log('Initializing board drag and drop'); // Setup drop zones const dropZones = document.querySelectorAll('.invoice-drop-zone'); dropZones.forEach(zone => { zone.addEventListener('dragover', handleDragOver); zone.addEventListener('drop', handleDrop); zone.addEventListener('dragenter', handleDragEnter); zone.addEventListener('dragleave', handleDragLeave); }); } // Drag and drop event handlers function handleDragStart(e) { draggedInvoice = e.target.closest('.board-invoice-card'); // Only handle drag start for invoice board cards if (!draggedInvoice) { console.log('Drag start: No board invoice card found'); return; } draggedInvoice.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', draggedInvoice.dataset.invoiceId); console.log(`Started dragging invoice ${draggedInvoice.dataset.invoiceId} with status ${draggedInvoice.dataset.status}`); } function handleDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } function handleDragEnter(e) { e.preventDefault(); const dropZone = e.target.closest('.invoice-drop-zone'); // Only handle if this is an invoice drop zone and we have a dragged invoice if (dropZone && draggedInvoice && draggedInvoice.classList.contains('board-invoice-card')) { dropZone.classList.add('drag-over'); console.log(`Drag enter: ${dropZone.dataset.status} zone`); // Hide empty message when dragging over const emptyMessage = dropZone.querySelector('.empty-column-message'); if (emptyMessage) { emptyMessage.style.display = 'none'; } } } function handleDragLeave(e) { const dropZone = e.target.closest('.invoice-drop-zone'); // Only handle if this is an invoice drop zone if (dropZone) { // Only remove drag-over if we're actually leaving the drop zone const rect = dropZone.getBoundingClientRect(); const x = e.clientX; const y = e.clientY; if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { dropZone.classList.remove('drag-over'); // Show empty message again if no cards in zone const cards = dropZone.querySelectorAll('.board-invoice-card'); const emptyMessage = dropZone.querySelector('.empty-column-message'); if (cards.length === 0 && emptyMessage) { emptyMessage.style.display = 'block'; } } } } function handleDrop(e) { e.preventDefault(); const dropZone = e.target.closest('.invoice-drop-zone'); // If this isn't an invoice drop zone, ignore the event (let other handlers process it) if (!dropZone) { return; } const newStatus = dropZone.dataset.status; if (!draggedInvoice || !newStatus) { resetDragState(); return; } const invoiceId = draggedInvoice.dataset.invoiceId; const currentStatus = draggedInvoice.dataset.status; console.log(`Dropping invoice ${invoiceId} into ${newStatus} column`); // Check if status change is valid if (!isValidStatusTransition(currentStatus, newStatus)) { showMessage(`Cannot move invoice from ${currentStatus} to ${newStatus}`, 'error'); resetDragState(); return; } // If same status, just move visually if (currentStatus === newStatus) { moveInvoiceToColumn(draggedInvoice, dropZone); resetDragState(); return; } // Update invoice status via API (resetDragState is called in the finally block) updateInvoiceStatus(invoiceId, newStatus, dropZone); } function handleDragEnd(e) { resetDragState(); } function resetDragState() { if (draggedInvoice) { draggedInvoice.classList.remove('dragging'); draggedInvoice = null; } // Remove drag-over classes document.querySelectorAll('.invoice-drop-zone').forEach(zone => { zone.classList.remove('drag-over'); // Show empty messages where appropriate const cards = zone.querySelectorAll('.board-invoice-card'); const emptyMessage = zone.querySelector('.empty-column-message'); if (cards.length === 0 && emptyMessage) { emptyMessage.style.display = 'block'; } }); } function moveInvoiceToColumn(invoiceCard, dropZone) { // Hide empty message const emptyMessage = dropZone.querySelector('.empty-column-message'); if (emptyMessage) { emptyMessage.style.display = 'none'; } // Move card to new column dropZone.appendChild(invoiceCard); // Update column counts updateColumnCounts(); console.log(`Moved invoice card to column ${dropZone.dataset.status}`); } function updateInvoiceStatus(invoiceId, newStatus, dropZone) { console.log(`Updating invoice ${invoiceId} to status ${newStatus}`); // Store reference to dragged invoice before any async operations const invoiceCard = draggedInvoice; // Show loading state on the card const loadingOverlay = document.createElement('div'); loadingOverlay.className = 'card-loading-overlay'; loadingOverlay.innerHTML = '
'; invoiceCard.appendChild(loadingOverlay); // Make API call to update status fetch(`/${newStatus}-invoice/${invoiceId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', } }) .then(response => { console.log(`API response status: ${response.status}`); if (response.ok) { // Update successful - update the card's data and visual elements invoiceCard.dataset.status = newStatus; // Update the status indicator inside the card const statusIndicator = invoiceCard.querySelector('.status-indicator'); if (statusIndicator) { statusIndicator.className = `status-indicator status-${newStatus}`; statusIndicator.textContent = newStatus; } // Move card to new column moveInvoiceToColumn(invoiceCard, dropZone); // Update the stored search results to reflect the status change updateStoredSearchResults(invoiceId, newStatus); // Update the stored board invoice data let boardInvoiceData = boardInvoices.get(invoiceId); if (!boardInvoiceData) { // Try looking up by invoice number if ID lookup failed const invoiceNumber = invoiceCard.querySelector('.invoice-number')?.textContent?.replace('#', ''); boardInvoiceData = boardInvoices.get(invoiceNumber); } if (boardInvoiceData) { boardInvoiceData.status = newStatus; boardInvoices.set(invoiceId, boardInvoiceData); // Also update by invoice number if we have it if (boardInvoiceData.invoiceNumber) { boardInvoices.set(boardInvoiceData.invoiceNumber, boardInvoiceData); } console.log(`Updated board invoice data for ${invoiceId} to status ${newStatus}`); } else { console.log(`Warning: Could not find board invoice data for ${invoiceId}`); } showMessage(`Invoice moved to ${newStatus}`, 'success'); console.log(`Successfully updated invoice ${invoiceId} to ${newStatus}`); // Reset drag state after successful update resetDragState(); } else { // Try to get error details from response return response.text().then(errorText => { console.error(`API error: ${response.status} - ${errorText}`); throw new Error(`Server error: ${response.status} - ${errorText}`); }); } }) .catch(error => { console.error('Error updating invoice status:', error); showMessage(`Failed to update invoice status: ${error.message}`, 'error'); // Reset drag state on error resetDragState(); }) .finally(() => { // Remove loading overlay if (loadingOverlay && loadingOverlay.parentNode) { loadingOverlay.parentNode.removeChild(loadingOverlay); } }); } // Update stored search results when status changes function updateStoredSearchResults(invoiceId, newStatus) { console.log(`Searching for invoice ${invoiceId} in stored search results...`); for (let i = 0; i < lastSearchResults.length; i++) { const result = lastSearchResults[i]; // Check both ID and invoice number for match if (result.id === invoiceId || result.id === invoiceId.toString() || result.invoiceNumber === invoiceId || result.invoiceNumber === invoiceId.toString()) { lastSearchResults[i].status = newStatus; console.log(`Updated stored search result for invoice ${invoiceId} to status ${newStatus}`); return; } } console.log(`Warning: Could not find invoice ${invoiceId} in stored search results`); } // Status transition validation function isValidStatusTransition(from, to) { const transitions = { 'draft': ['ok', 'void'], 'ok': ['draft', 'failed', 'pending_accounting', 'processed', 'void'], 'failed': ['draft', 'ok', 'void'], 'pending_accounting': ['failed', 'processed', 'void'], 'processed': ['void'], 'void': [] // Cannot transition from void }; return transitions[from] && transitions[from].includes(to); } function updateColumnCounts() { document.querySelectorAll('.kanban-column').forEach(column => { const status = column.dataset.status; const cards = column.querySelectorAll('.board-invoice-card'); const countElement = column.querySelector('.invoice-count'); if (countElement) { countElement.textContent = cards.length; } }); } // Add invoice to board (called when loading invoices) function addInvoiceToBoard(invoiceData) { const status = invoiceData.status; const dropZone = document.querySelector(`[data-status="${status}"] .invoice-drop-zone`); if (!dropZone) { console.error(`No drop zone found for status: ${status}`); return; } // Create invoice card element const cardHTML = createBoardCardHTML(invoiceData); const tempDiv = document.createElement('div'); tempDiv.innerHTML = cardHTML; const card = tempDiv.firstElementChild; // Add drag event listeners card.addEventListener('dragstart', handleDragStart); card.addEventListener('dragend', handleDragEnd); console.log(`Added drag event listeners to invoice card ${invoiceData.id}`); console.log(`Card draggable attribute: ${card.getAttribute('draggable')}`); console.log(`Card data-invoice-id: ${card.dataset.invoiceId}`); console.log(`Card data-status: ${card.dataset.status}`); // Hide empty message const emptyMessage = dropZone.querySelector('.empty-column-message'); if (emptyMessage) { emptyMessage.style.display = 'none'; } // Add to drop zone dropZone.appendChild(card); // Store invoice data with both ID and invoice number as keys for easier lookup const invoiceId = invoiceData.id; boardInvoices.set(invoiceId, invoiceData); // Also store by invoice number if different from ID if (invoiceData.invoiceNumber && invoiceData.invoiceNumber !== invoiceId) { boardInvoices.set(invoiceData.invoiceNumber, invoiceData); } console.log(`Stored board invoice data for ID: ${invoiceId}, Number: ${invoiceData.invoiceNumber}`); // Update counts updateColumnCounts(); } function createBoardCardHTML(invoice) { return `
#${invoice.invoiceNumber}
⋮⋮
${invoice.customer ? `
👤 ${invoice.customer.name}
` : ''} ${invoice.job ? `
🔧 ${invoice.job.name}
` : ''}
💰 $${invoice.totalPrice}
`; } // Invoice details modal function showInvoiceDetails(invoiceId) { console.log(`Showing details for invoice ${invoiceId}`); // Find the invoice data in our stored data let invoiceData = boardInvoices.get(invoiceId); // If not found in board, try searching in lastSearchResults if (!invoiceData) { invoiceData = lastSearchResults.find(inv => inv.id === invoiceId || inv.id === invoiceId.toString() || inv.invoiceNumber === invoiceId ); } if (!invoiceData) { showMessage('Invoice details not found', 'error'); return; } // Create and show modal const modal = createInvoiceDetailsModal(invoiceData); document.body.appendChild(modal); // Show modal with animation setTimeout(() => modal.classList.add('show'), 10); // Close modal when clicking outside or on close button modal.addEventListener('click', function (e) { if (e.target === modal || e.target.classList.contains('modal-close')) { closeInvoiceDetailsModal(modal); } }); } function createInvoiceDetailsModal(invoiceData) { const modal = document.createElement('div'); modal.className = 'invoice-details-modal'; modal.innerHTML = ` `; return modal; } function closeInvoiceDetailsModal(modal) { modal.classList.remove('show'); setTimeout(() => { if (document.body.contains(modal)) { document.body.removeChild(modal); } }, 300); } // Utility function for showing messages function showMessage(text, type = 'info') { const msg = document.createElement('div'); msg.textContent = text; msg.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 4px; color: white; font-weight: bold; z-index: 10000; transform: translateX(300px); transition: transform 0.3s ease; background: ${getMessageColor(type)}; `; document.body.appendChild(msg); // Show message setTimeout(() => msg.style.transform = 'translateX(0)', 100); // Hide message setTimeout(() => { msg.style.transform = 'translateX(300px)'; setTimeout(() => { if (document.body.contains(msg)) { document.body.removeChild(msg); } }, 300); }, 3000); } function getMessageColor(type) { const colors = { 'success': '#10b981', 'error': '#ef4444', 'warning': '#f59e0b', 'info': '#3b82f6' }; return colors[type] || colors.info; } // Debug function to check board state function debugBoardState() { console.log('=== BOARD DEBUG INFO ==='); console.log(`Current view: ${currentView}`); console.log(`Last search results count: ${lastSearchResults.length}`); console.log(`Board invoices stored: ${boardInvoices.size}`); const allCards = document.querySelectorAll('.board-invoice-card'); console.log(`Board cards in DOM: ${allCards.length}`); allCards.forEach((card, index) => { console.log(`Card ${index}: ID=${card.dataset.invoiceId}, Status=${card.dataset.status}, Draggable=${card.getAttribute('draggable')}`); }); const dropZones = document.querySelectorAll('.invoice-drop-zone'); dropZones.forEach(zone => { const status = zone.dataset.status; const cardsInZone = zone.querySelectorAll('.board-invoice-card').length; console.log(`Drop zone ${status}: ${cardsInZone} cards`); }); console.log('========================'); } // Make debug function globally accessible for browser console window.debugBoardState = debugBoardState; // Add new invoices to board without clearing existing ones function addNewInvoicesToBoard(newInvoices) { console.log(`Adding ${newInvoices.length} new invoices to existing board`); let addedCount = 0; newInvoices.forEach(invoiceData => { // Check if invoice is already on the board const existingCard = document.getElementById(`board-invoice-${invoiceData.id}`); if (!existingCard) { addInvoiceToBoard(invoiceData); addedCount++; } else { console.log(`Invoice ${invoiceData.id} already on board, skipping`); } }); if (addedCount > 0) { showMessage(`Added ${addedCount} new invoices to board`, 'success'); } else if (newInvoices.length > 0) { showMessage(`All ${newInvoices.length} invoices already on board`, 'info'); } } // Clear board with confirmation function clearBoardAndConfirm() { const existingCards = document.querySelectorAll('.board-invoice-card'); if (existingCards.length === 0) { showMessage('Board is already empty', 'info'); return; } const confirmed = confirm(`Are you sure you want to clear all ${existingCards.length} invoices from the board? This will not affect the invoices themselves, only remove them from the board view.`); if (confirmed) { clearBoard(); showMessage('Board cleared successfully', 'success'); } }