diff --git a/app.js b/app.js new file mode 100644 index 0000000..9a60c20 --- /dev/null +++ b/app.js @@ -0,0 +1,575 @@ +// =================================== +// Order Dashboard - Main JavaScript +// =================================== + +// Global state +let allData = []; +let filteredData = []; +let currentSort = { column: null, direction: null }; +let filters = {}; +let globalSearch = ''; +let apiCredentials = null; + +// API Configuration +const API_URL = 'https://n8n.finorbrands.com/webhook/61f89d5c-8474-4045-b52c-50ee608435c0'; + +// Initialize +document.addEventListener('DOMContentLoaded', () => { + updateAuthUI(); + loadData(); + setupClickOutside(); +}); + +// =================================== +// Authentication +// =================================== + +function updateAuthUI() { + const btnText = document.getElementById('authBtnText'); + if (!btnText) return; + + const storedUsername = localStorage.getItem('api_username'); + + if (storedUsername) { + btnText.textContent = `Logged in as ${storedUsername}`; + } else { + btnText.textContent = 'Enter Credentials'; + } +} + +function toggleAuth() { + const storedUsername = localStorage.getItem('api_username'); + + if (storedUsername) { + localStorage.removeItem('api_username'); + localStorage.removeItem('api_password'); + apiCredentials = null; + updateAuthUI(); + loadData(); + } else { + showCredentialPrompt(); + } +} + +// =================================== +// Data Loading +// =================================== + +async function loadData() { + const tableBody = document.getElementById('tableBody'); + if (!tableBody) return; + + tableBody.innerHTML = ` + + +
+ Loading data... + + + `; + + try { + console.group('%c Webhook Call', 'background: #6366f1; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;'); + console.log('%c API URL:', 'font-weight: bold; color: #6366f1;', API_URL); + + const storedUsername = localStorage.getItem('api_username'); + const storedPassword = localStorage.getItem('api_password'); + + const startTime = performance.now(); + + const fetchOptions = {}; + if (storedUsername && storedPassword) { + apiCredentials = { username: storedUsername, password: storedPassword }; + const credentials = btoa(`${storedUsername}:${storedPassword}`); + fetchOptions.headers = { + 'Authorization': `Basic ${credentials}`, + 'Accept': 'application/json' + }; + console.log('%c Using Basic Auth', 'color: #10b981;', `User: ${storedUsername}`); + } + + const response = await fetch(API_URL, fetchOptions); + const duration = performance.now() - startTime; + + console.log('%c Response Status:', response.ok ? 'color: #10b981' : 'color: #ef4444', response.status, response.statusText); + console.log('%c Response Time:', 'font-weight: bold;', `${duration.toFixed(0)}ms`); + + if (response.status === 401) { + throw new Error('Authorization required'); + } + + if (response.ok) { + const result = await response.json(); + console.log('%c Response Data:', 'font-weight: bold;', result); + + if (result.data) { + allData = result.data; + console.log('%c Data loaded:', 'color: #10b981; font-weight: bold;', allData.length, 'records'); + console.log('%c First record:', 'color: #6366f1;', allData[0]); + } else { + throw new Error('Invalid response format'); + } + } else { + throw new Error('API error'); + } + + console.groupEnd(); + } catch (error) { + console.error('%c Fetch Error:', 'background: #ef4444; color: white; padding: 4px 8px; border-radius: 4px;', error.message); + + if (error.message === 'Authorization required') { + showCredentialPrompt(); + } + + console.log('Using sample data:', error.message); + } + + filteredData = [...allData]; + + console.group('%c Filter State', 'background: #8b5cf6; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;'); + console.log('%c Current filters:', 'font-weight: bold;', filters); + console.log('%c Total records after filter:', 'font-weight: bold;', filteredData.length); + console.groupEnd(); + + applyFilters(); + updateStats(); +} + +// =================================== +// Filtering & Sorting +// =================================== + +function applyFilters() { + let result = filteredData.filter(item => { + if (!globalSearch) return true; + const searchLower = globalSearch.toLowerCase(); + return Object.values(item).some(value => + value !== null && value.toString().toLowerCase().includes(searchLower) + ); + }); + + Object.entries(filters).forEach(([column, selectedValues]) => { + if (selectedValues.length > 0) { + result = result.filter(item => + selectedValues.includes(item[column] === null ? 'null' : item[column]) + ); + } + }); + + renderTable(result); + updateStats(result.length); + + console.group('%c Table View', 'background: #06b6d4; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;'); + console.log('%c Rendering', 'font-weight: bold;', result.length, 'records'); + console.log('%c View range:', 'color: #64748b;', '1-', result.length); + console.groupEnd(); +} + +function sortTable(column) { + const direction = currentSort.column === column && currentSort.direction === 'asc' ? 'desc' : 'asc'; + + filteredData.sort((a, b) => { + let valA = a[column]; + let valB = b[column]; + + if (valA === null) valA = ''; + if (valB === null) valB = ''; + + if (typeof valA === 'string' && typeof valB === 'string') { + valA = valA.toLowerCase(); + valB = valB.toLowerCase(); + } + + if (valA < valB) return direction === 'asc' ? -1 : 1; + if (valA > valB) return direction === 'asc' ? 1 : -1; + return 0; + }); + + currentSort = { column, direction }; + renderTable(filteredData); + updateSortIndicators(column, direction); +} + +function updateSortIndicators(column, direction) { + document.querySelectorAll('.sort-indicator').forEach(indicator => { + indicator.classList.remove('active'); + }); + + const th = Array.from(document.querySelectorAll('th')).find( + th => th.textContent.includes(column) + ); + if (th) { + const indicator = th.querySelector('.sort-indicator'); + if (indicator) { + indicator.classList.add('active'); + indicator.innerHTML = direction === 'asc' + ? '' + : ''; + } + } +} + +// =================================== +// Table Rendering +// =================================== + +function renderTable(data) { + const tableBody = document.getElementById('tableBody'); + if (!tableBody) return; + + if (data.length === 0) { + tableBody.innerHTML = ` + + + +

No orders found matching your filters

+ + + `; + return; + } + + tableBody.innerHTML = data.map(item => ` + + ${item.Month || ''} + ${item.Odoo_AR_Nr || ''} + ${item.Order_ID || ''} + +
+ + ${item.Country_Name || 'N/A'} +
+ + + + ${item.VAT || 'N/A'} + + + +
+ + ${item.Remark || 'N/A'} +
+ + + + + + `).join(''); +} + +function getCountryClass(country) { + if (country === null || country === undefined) return 'unknown'; + const str = String(country); + if (str.includes('Finnland')) return 'finland'; + if (str.includes('Spanien')) return 'spanien'; + if (str.includes('Deutschland')) return 'deutschland'; + return 'unknown'; +} + +function getRemarkClass(remark) { + if (remark === null || remark === undefined) return ''; + const str = String(remark); + if (str.includes('NOK')) return 'remark-nok'; + if (str.includes('found')) return 'remark-danger'; + if (str.includes('valid') || str.includes('OK')) return 'remark-success'; + return ''; +} + +function getRemarkIcon(remark) { + if (remark === null || remark === undefined) return 'fa-file-alt'; + const str = String(remark); + if (str.includes('NOK')) return 'fa-times-circle'; + if (str.includes('found')) return 'fa-exclamation-triangle'; + if (str.includes('valid') || str.includes('OK')) return 'fa-check-circle'; + return 'fa-file-alt'; +} + +// =================================== +// Recheck Action +// =================================== + +async function handleRecheck(orderId, isChecked) { + console.group('%c Recheck', 'background: #6366f1; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;'); + console.log('%c Order ID:', 'font-weight: bold;', orderId); + console.log('%c Status:', isChecked ? 'color: #10b981' : 'color: #94a3b8', isChecked ? 'Checked' : 'Unchecked'); + + try { + const response = await fetch(API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(apiCredentials ? { + 'Authorization': `Basic ${btoa(`${apiCredentials.username}:${apiCredentials.password}`)}` + } : {}) + }, + body: JSON.stringify({ + Order_ID: orderId, + recheck: isChecked + }) + }); + + if (response.ok) { + console.log('%c Success:', 'color: #10b981; font-weight: bold;', 'Recheck status updated'); + } else { + console.error('%c Error:', 'background: #ef4444; color: white; padding: 4px 8px; border-radius: 4px;', response.status, response.statusText); + } + + console.groupEnd(); + } catch (error) { + console.error('%c Request Error:', 'background: #ef4444; color: white; padding: 4px 8px; border-radius: 4px;', error.message); + console.groupEnd(); + } +} + +// =================================== +// Column Filters +// =================================== + +function toggleFilter(column) { + const container = document.getElementById('filterContainer'); + + document.querySelectorAll('.filter-dropdown').forEach(dropdown => { + if (dropdown.dataset.column !== column) { + dropdown.classList.remove('show'); + } + }); + + let dropdown = document.querySelector(`.filter-dropdown[data-column="${column}"]`); + + if (!dropdown) { + dropdown = createFilterDropdown(column); + container.appendChild(dropdown); + } + + if (dropdown.classList.contains('show')) { + dropdown.classList.remove('show'); + } else { + dropdown.classList.add('show'); + positionDropdown(dropdown, column); + } +} + +function createFilterDropdown(column) { + const dropdown = document.createElement('div'); + dropdown.className = 'filter-dropdown'; + dropdown.dataset.column = column; + + const uniqueValues = [...new Set( + allData.map(item => item[column] === null ? 'null' : item[column]) + )]; + + dropdown.innerHTML = ` +
+

Filter ${column}

+ +
+
+ ${uniqueValues.map(value => ` + + `).join('')} +
+ + `; + + dropdown.addEventListener('change', (e) => { + if (e.target.type === 'checkbox') { + updateFilter(column, e.target.value, e.target.checked); + } + }); + + return dropdown; +} + +function positionDropdown(dropdown, column) { + const th = Array.from(document.querySelectorAll('th')).find( + th => th.textContent.includes(column) + ); + if (th) { + const rect = th.getBoundingClientRect(); + const tableRect = document.querySelector('table').getBoundingClientRect(); + dropdown.style.left = `${rect.left - tableRect.left}px`; + dropdown.style.width = `${rect.width}px`; + } +} + +function updateFilter(column, value, isChecked) { + if (!filters[column]) { + filters[column] = []; + } + + const valueStr = value === 'null' ? 'null' : value; + + if (isChecked) { + if (!filters[column].includes(valueStr)) { + filters[column].push(valueStr); + } + } else { + filters[column] = filters[column].filter(v => v !== valueStr); + } + + updateFilterIndicators(column); + applyFilters(); +} + +function updateFilterIndicators(column) { + const th = Array.from(document.querySelectorAll('th')).find( + th => th.textContent.includes(column) + ); + if (th) { + const indicator = th.querySelector('.filter-indicator'); + if (indicator) { + if (filters[column] && filters[column].length > 0) { + indicator.classList.add('active'); + } else { + indicator.classList.remove('active'); + } + } + } +} + +function clearFilter(column) { + if (filters[column]) { + filters[column] = []; + } + document.querySelectorAll(`.filter-dropdown[data-column="${column}"] input[type="checkbox"]`) + .forEach(checkbox => checkbox.checked = false); + + updateFilterIndicators(column); + applyFilters(); +} + +function setupClickOutside() { + document.addEventListener('click', (e) => { + if (!e.target.closest('.filter-indicator') && + !e.target.closest('.filter-dropdown')) { + document.querySelectorAll('.filter-dropdown').forEach(dropdown => { + dropdown.classList.remove('show'); + }); + } + }); +} + +function handleGlobalSearch(search) { + globalSearch = search; + applyFilters(); +} + +// =================================== +// Statistics +// =================================== + +function updateStats(count = filteredData.length) { + const totalOrders = document.getElementById('totalOrders'); + const totalCountries = document.getElementById('totalCountries'); + const totalVAT = document.getElementById('totalVAT'); + const tableCount = document.getElementById('tableCount'); + + if (totalOrders) totalOrders.textContent = allData.length; + if (totalCountries) totalCountries.textContent = + [...new Set(allData.map(item => item.Country_Name))].length; + if (totalVAT) totalVAT.textContent = + allData.filter(item => item.VAT !== null).length; + if (tableCount) tableCount.textContent = `${count} order${count !== 1 ? 's' : ''}`; +} + +// =================================== +// Credential Prompt +// =================================== + +function showCredentialPrompt() { + const existing = document.getElementById('credentialPrompt'); + if (existing) existing.remove(); + + const prompt = document.createElement('div'); + prompt.id = 'credentialPrompt'; + prompt.innerHTML = ` +
+
+
+
+ +

API Credentials Required

+
+

The webhook API requires authentication. Please enter your credentials below:

+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+
+ + Your credentials will be used for this session only. They are not stored permanently. +
+
+
+
+ `; + document.body.appendChild(prompt); + + document.getElementById('credentialForm').addEventListener('submit', handleCredentials); + document.querySelector('.credential-overlay').addEventListener('click', function(e) { + if (e.target === this) { + closeCredentialPrompt(); + } + }); +} + +function closeCredentialPrompt() { + const prompt = document.getElementById('credentialPrompt'); + if (prompt) { + document.body.removeChild(prompt); + } +} + +function handleCredentials(e) { + e.preventDefault(); + + const username = document.getElementById('username').value; + const password = document.getElementById('password').value; + + console.group('%c Credentials', 'background: #6366f1; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;'); + console.log('%c Username:', 'font-weight: bold;', username); + console.log('%c Password length:', 'font-weight: bold;', password.length, 'characters'); + console.groupEnd(); + + localStorage.setItem('api_username', username); + localStorage.setItem('api_password', password); + apiCredentials = { username, password }; + + console.log('%c Credentials saved:', 'color: #10b981; font-weight: bold;', { + username, + passwordLength: password.length, + authHeader: `Basic ${btoa(`${username}:${password}`)}` + }); + + closeCredentialPrompt(); + console.log('%c Retrying connection with credentials...', 'color: #10b981; font-weight: bold;'); + loadData(); +}