You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
868 lines
30 KiB
868 lines
30 KiB
// 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('<strong>Customer:</strong>')
|
|
);
|
|
const customerName = customerElement ?
|
|
customerElement.innerHTML.replace('<strong>Customer:</strong>', '').trim() : null;
|
|
|
|
// Extract job name
|
|
const jobElement = Array.from(card.querySelectorAll('.invoice-info p')).find(p =>
|
|
p.innerHTML.includes('<strong>Job:</strong>')
|
|
);
|
|
const jobName = jobElement ?
|
|
jobElement.innerHTML.replace('<strong>Job:</strong>', '').trim() : null;
|
|
|
|
// Extract total price
|
|
const totalElement = Array.from(card.querySelectorAll('.invoice-info p')).find(p =>
|
|
p.innerHTML.includes('<strong>Total:</strong>')
|
|
);
|
|
const totalMatch = totalElement ?
|
|
totalElement.innerHTML.match(/<strong>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 = `
|
|
<div class="no-search-state">
|
|
<div class="no-search-icon">📋</div>
|
|
<h4>Start searching for invoices</h4>
|
|
<p>Enter an invoice ID, customer name, or job number above to begin</p>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = '<div class="loading-spinner"></div>';
|
|
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 `
|
|
<div id="board-invoice-${invoice.id}" class="board-invoice-card" draggable="true" data-invoice-id="${invoice.id}" data-status="${invoice.status}">
|
|
<div class="board-card-header">
|
|
<div class="invoice-number">#${invoice.invoiceNumber}</div>
|
|
<div class="card-drag-handle">⋮⋮</div>
|
|
</div>
|
|
|
|
<div class="board-card-content">
|
|
${invoice.customer ? `<div class="card-customer">👤 ${invoice.customer.name}</div>` : ''}
|
|
${invoice.job ? `<div class="card-job">🔧 ${invoice.job.name}</div>` : ''}
|
|
<div class="card-total">💰 $${invoice.totalPrice}</div>
|
|
</div>
|
|
|
|
<div class="board-card-footer">
|
|
<div class="status-indicator status-${invoice.status}">${invoice.status}</div>
|
|
<button class="card-details-btn" onclick="showInvoiceDetails('${invoice.id}')" title="View Details">
|
|
🔍
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 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 = `
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3>Invoice Details</h3>
|
|
<button class="modal-close" type="button">✕</button>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
<div class="invoice-detail-grid">
|
|
<div class="detail-row">
|
|
<span class="detail-label">Invoice #:</span>
|
|
<span class="detail-value">${invoiceData.invoiceNumber || 'N/A'}</span>
|
|
</div>
|
|
|
|
<div class="detail-row">
|
|
<span class="detail-label">Invoice ID:</span>
|
|
<span class="detail-value">${invoiceData.id || 'N/A'}</span>
|
|
</div>
|
|
|
|
<div class="detail-row">
|
|
<span class="detail-label">Status:</span>
|
|
<span class="detail-value">
|
|
<span class="status-badge status-${invoiceData.status}">${invoiceData.status}</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="detail-row">
|
|
<span class="detail-label">Customer:</span>
|
|
<span class="detail-value">${invoiceData.customer ? invoiceData.customer.name : 'N/A'}</span>
|
|
</div>
|
|
|
|
<div class="detail-row">
|
|
<span class="detail-label">Job:</span>
|
|
<span class="detail-value">${invoiceData.job ? invoiceData.job.name : 'N/A'}</span>
|
|
</div>
|
|
|
|
<div class="detail-row">
|
|
<span class="detail-label">Total Price:</span>
|
|
<span class="detail-value total-amount">$${invoiceData.totalPrice || '0.00'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button class="btn-secondary modal-close" type="button">Close</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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');
|
|
}
|
|
}
|