// 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');
lastSearchResults = [];
invoiceCards.forEach(card => {
const invoiceData = extractInvoiceDataFromCard(card);
if (invoiceData) {
lastSearchResults.push(invoiceData);
console.log('Extracted invoice data:', invoiceData);
}
});
console.log(`Captured ${lastSearchResults.length} invoices from search results`);
// If we're currently in board view, update it immediately
if (currentView === 'board') {
populateBoardWithSearchResults();
}
}
// 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
`;
// Clear board if currently in board view
if (currentView === 'board') {
clearBoard();
}
}
// View switching functionality
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, populate it with search results
if (viewName === 'board') {
if (lastSearchResults.length > 0) {
populateBoardWithSearchResults();
} else {
checkBoardState();
}
}
console.log(`Switched to ${viewName} view`);
}
// 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();
}
// Populate board with search results
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');
}
// 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('Search for invoices in the Search View to populate the board', '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 (placeholder)
function showInvoiceDetails(invoiceId) {
console.log(`Showing details for invoice ${invoiceId}`);
// TODO: Implement invoice details modal
showMessage(`Details for invoice ${invoiceId}`, 'info');
}
// 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;