Files
tmp/index.html
T
2026-09-03 10:24:27 -03:00

1431 lines
49 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Order Dashboard</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--secondary: #8b5cf6;
--dark: #0f172a;
--light: #f8fafc;
--border: #e2e8f0;
--hover: #f1f5f9;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
min-height: 100vh;
color: var(--dark);
padding: 2rem;
}
/* Credential Modal Reset */
#credentialPrompt {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
z-index: 9999 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 100% !important;
height: 100% !important;
}
#credentialPrompt .credential-overlay,
#credentialPrompt .credential-modal {
position: static !important;
margin: auto !important;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
margin-bottom: 2rem;
}
h1 {
color: white;
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 1rem;
}
h1 i {
color: var(--primary);
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 12px;
box-shadow: var(--shadow);
transition: transform 0.2s ease;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-card i {
color: var(--primary);
font-size: 1.5rem;
margin-bottom: 1rem;
}
.stat-card .value {
font-size: 2rem;
font-weight: 700;
color: var(--dark);
}
.stat-card .label {
font-size: 0.875rem;
color: #64748b;
margin-top: 0.5rem;
}
.controls {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
align-items: center;
}
.refresh-btn {
background: var(--primary);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: background 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.refresh-btn:hover {
background: var(--primary-dark);
}
.refresh-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.search-box {
flex: 1;
min-width: 250px;
position: relative;
}
.search-box input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.5rem;
border: 2px solid var(--border);
border-radius: 8px;
font-size: 0.9rem;
transition: border-color 0.2s ease;
}
.search-box input:focus {
outline: none;
border-color: var(--primary);
}
.search-box i {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: #64748b;
}
.table-container {
background: white;
border-radius: 12px;
box-shadow: var(--shadow);
overflow: hidden;
}
.table-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
background: var(--light);
border-bottom: 1px solid var(--border);
}
.table-info {
display: flex;
align-items: center;
gap: 1rem;
}
.table-info span {
color: #64748b;
font-size: 0.875rem;
}
.table-wrapper {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
background: var(--light);
}
th {
padding: 1rem 1.5rem;
text-align: left;
font-weight: 600;
color: var(--dark);
border-bottom: 2px solid var(--border);
position: sticky;
top: 0;
background: var(--light);
z-index: 10;
}
th:hover {
background: var(--hover);
}
th .sort-indicator {
display: inline-flex;
flex-direction: column;
margin-left: 0.5rem;
cursor: pointer;
opacity: 0.3;
}
th .sort-indicator:hover {
opacity: 1;
}
th .sort-indicator i {
font-size: 0.75rem;
line-height: 1;
}
th .sort-indicator.active {
opacity: 1;
}
th .sort-indicator.active i:first-child {
color: var(--primary);
}
th .sort-indicator.active i:last-child {
color: #cbd5e1;
}
th .filter-indicator {
display: inline-flex;
margin-left: 0.5rem;
cursor: pointer;
color: var(--primary);
}
th .filter-indicator i {
font-size: 0.875rem;
opacity: 0.3;
}
th .filter-indicator.active i {
opacity: 1;
}
td {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
font-size: 0.9rem;
color: #475569;
}
tr:hover {
background: var(--hover);
}
.cell-country {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.country-badge {
width: 8px;
height: 8px;
border-radius: 50%;
}
.country-badge.finland {
background: #003580;
}
.country-badge.spanien {
background: #aa151b;
}
.country-badge.deutschland {
background: #000000;
}
.country-badge.unknown {
background: #94a3b8;
}
.country-badge.b2b {
background: #00008b;
}
.cell-vat {
font-family: monospace;
font-size: 0.85rem;
}
.cell-vat.empty {
color: #94a3b8;
font-style: italic;
}
.cell-remark {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.cell-remark.warning {
color: var(--warning);
}
.cell-remark.success {
color: var(--success);
}
.cell-remark.danger {
color: var(--danger);
}
.cell-remark.remark-nok {
color: var(--danger);
font-weight: 600;
}
.cell-remark.remark-danger {
color: var(--danger);
}
.cell-remark.remark-success {
color: var(--success);
}
.recheck-checkbox {
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
accent-color: var(--primary);
border: 2px solid var(--border);
border-radius: 4px;
}
.recheck-checkbox:checked {
background-color: var(--primary);
border-color: var(--primary);
}
.recheck-checkbox:hover {
border-color: var(--primary);
}
/* Filter Dropdown */
.filter-dropdown {
position: absolute;
top: 100%;
right: 0;
background: white;
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
z-index: 100;
min-width: 200px;
margin-top: 0.5rem;
padding: 0.5rem;
display: none;
}
.filter-dropdown.show {
display: block;
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.filter-header h4 {
font-size: 0.875rem;
font-weight: 600;
color: var(--dark);
}
.filter-header .close-btn {
background: none;
border: none;
cursor: pointer;
font-size: 1.25rem;
color: #64748b;
}
.filter-list {
max-height: 200px;
overflow-y: auto;
}
.filter-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem;
cursor: pointer;
transition: background 0.2s ease;
}
.filter-item:hover {
background: var(--hover);
border-radius: 4px;
}
.filter-item input {
width: 1rem;
height: 1rem;
cursor: pointer;
}
.filter-item label {
flex: 1;
font-size: 0.875rem;
color: var(--dark);
}
.filter-clear {
margin-top: 0.75rem;
padding: 0.5rem;
background: var(--hover);
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 0.875rem;
color: #64748b;
}
.filter-clear:hover {
background: #e2e8f0;
}
/* Loading */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 3rem;
color: var(--primary);
}
.spinner {
width: 3rem;
height: 3rem;
border: 3px solid rgba(99, 102, 241, 0.1);
border-radius: 50%;
border-top-color: var(--primary);
animation: spin 1s ease-in-out infinite;
margin-right: 1rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: #64748b;
}
.empty-state i {
font-size: 3rem;
color: #cbd5e1;
margin-bottom: 1rem;
}
.empty-state p {
font-size: 1rem;
}
/* Responsive */
@media (max-width: 768px) {
h1 {
font-size: 1.5rem;
}
.controls {
flex-direction: column;
}
.search-box {
width: 100%;
}
th, td {
padding: 0.75rem 1rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-chart-line"></i> Order Dashboard</h1>
<p style="color: #94a3b8;">Real-time order management with filtering and sorting</p>
</header>
<div class="stats" id="stats">
<div class="stat-card">
<i class="fas fa-file-invoice-dollar"></i>
<div class="value" id="totalOrders">0</div>
<div class="label">Total Orders</div>
</div>
<div class="stat-card">
<i class="fas fa-globe-americas"></i>
<div class="value" id="totalCountries">0</div>
<div class="label">Countries</div>
</div>
<div class="stat-card">
<i class="fas fa-file-alt"></i>
<div class="value" id="totalVAT">0</div>
<div class="label">Entries with VAT</div>
</div>
</div>
<div class="controls">
<button class="refresh-btn" onclick="loadData()">
<i class="fas fa-sync-alt"></i> Refresh Data
</button>
<button class="refresh-btn" id="authBtn" onclick="toggleAuth()" style="background: var(--secondary);">
<i class="fas fa-key"></i> <span id="authBtnText">Enter Credentials</span>
</button>
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="globalSearch" placeholder="Search orders, countries, VAT numbers..." oninput="handleGlobalSearch(this.value)">
</div>
</div>
<div class="table-container">
<div class="table-header">
<div class="table-info">
<i class="fas fa-table" style="color: var(--primary);"></i>
<span id="tableCount">0 orders</span>
</div>
</div>
<div class="table-wrapper">
<table id="dataTable">
<thead>
<tr>
<th>
Month
<div class="filter-indicator" onclick="toggleFilter('Month')">
<i class="fas fa-filter"></i>
</div>
<div class="sort-indicator" onclick="sortTable('Month')">
<i class="fas fa-arrow-up"></i>
<i class="fas fa-arrow-down"></i>
</div>
</th>
<th>
Odoo AR Nr
<div class="filter-indicator" onclick="toggleFilter('Odoo_AR_Nr')">
<i class="fas fa-filter"></i>
</div>
<div class="sort-indicator" onclick="sortTable('Odoo_AR_Nr')">
<i class="fas fa-arrow-up"></i>
<i class="fas fa-arrow-down"></i>
</div>
</th>
<th>
Order ID
<div class="filter-indicator" onclick="toggleFilter('Order_ID')">
<i class="fas fa-filter"></i>
</div>
<div class="sort-indicator" onclick="sortTable('Order_ID')">
<i class="fas fa-arrow-up"></i>
<i class="fas fa-arrow-down"></i>
</div>
</th>
<th>
Country
<div class="filter-indicator" onclick="toggleFilter('Country_Name')">
<i class="fas fa-filter"></i>
</div>
<div class="sort-indicator" onclick="sortTable('Country_Name')">
<i class="fas fa-arrow-up"></i>
<i class="fas fa-arrow-down"></i>
</div>
</th>
<th>
VAT
<div class="filter-indicator" onclick="toggleFilter('VAT')">
<i class="fas fa-filter"></i>
</div>
<div class="sort-indicator" onclick="sortTable('VAT')">
<i class="fas fa-arrow-up"></i>
<i class="fas fa-arrow-down"></i>
</div>
</th>
<th>
Remark
<div class="filter-indicator" onclick="toggleFilter('Remark')">
<i class="fas fa-filter"></i>
</div>
<div class="sort-indicator" onclick="sortTable('Remark')">
<i class="fas fa-arrow-up"></i>
<i class="fas fa-arrow-down"></i>
</div>
</th>
<th>
Recheck
<div class="filter-indicator" onclick="toggleFilter('recheck')">
<i class="fas fa-filter"></i>
</div>
<div class="sort-indicator" onclick="sortTable('recheck')">
<i class="fas fa-arrow-up"></i>
<i class="fas fa-arrow-down"></i>
</div>
</th>
</tr>
</thead>
<tbody id="tableBody">
<tr>
<td colspan="7" class="loading">
<div class="spinner"></div>
<span>Loading data...</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Filter dropdown container -->
<div id="filterContainer"></div>
<script>
// Sample data based on the provided structure
let allData = [
{"Month":"8","Odoo_AR_Nr":"1563","Order_ID":"29810","Country_Name":"Finnland","VAT":null,"Remark":"Ref not found"},
{"Month":"8","Odoo_AR_Nr":"1509","Order_ID":"29814","Country_Name":"Spanien B2B","VAT":"ES39905339V","Remark":"Ref not found"},
{"Month":"8","Odoo_AR_Nr":"1564","Order_ID":"29829","Country_Name":"Spanien B2B","VAT":"ESB44160166","Remark":"Ref not found"},
{"Month":"8","Odoo_AR_Nr":"1593","Order_ID":"29821","Country_Name":"Deutschland B2B","VAT":null,"Remark":"Ref not found"},
{"Month":"8","Odoo_AR_Nr":"1593","Order_ID":"29839","Country_Name":"Deutschland B2B","VAT":null,"Remark":"Ref not found"}
];
let filteredData = [...allData];
let currentSort = { column: null, direction: null };
let filters = {};
let globalSearch = '';
let apiCredentials = null;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
updateAuthUI();
loadData();
setupClickOutside();
});
function updateAuthUI() {
const btnText = document.getElementById('authBtnText');
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) {
// Clear credentials
localStorage.removeItem('api_username');
localStorage.removeItem('api_password');
apiCredentials = null;
updateAuthUI();
loadData();
} else {
// Show credential prompt
showCredentialPrompt();
}
}
async function loadData() {
const tableBody = document.getElementById('tableBody');
tableBody.innerHTML = `
<tr>
<td colspan="6" class="loading">
<div class="spinner"></div>
<span>Loading data...</span>
</td>
</tr>
`;
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;', 'https://n8n.finorbrands.com/webhook/61f89d5c-8474-4045-b52c-50ee608435c0');
// Get stored credentials if available
const storedUsername = localStorage.getItem('api_username');
const storedPassword = localStorage.getItem('api_password');
const startTime = performance.now();
// Build fetch options with credentials if available
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('https://n8n.finorbrands.com/webhook/61f89d5c-8474-4045-b52c-50ee608435c0', 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) {
// Authorization required - prompt for credentials
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; font-weight: bold;', error.message);
// Show credential prompt for 401 errors
if (error.message === 'Authorization required') {
showCredentialPrompt();
}
console.log('Using sample data:', error.message);
// Using the sample data from the prompt
}
filteredData = [...allData];
// Debug: Log filter state
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();
}
function applyFilters() {
// Apply global search
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)
);
});
// Apply column filters
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);
// Debug: Log current view
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 renderTable(data) {
const tableBody = document.getElementById('tableBody');
if (data.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="7" class="empty-state">
<i class="fas fa-search"></i>
<p>No orders found matching your filters</p>
</td>
</tr>
`;
return;
}
tableBody.innerHTML = data.map(item => `
<tr>
<td>${item.Month}</td>
<td>${item.Odoo_AR_Nr}</td>
<td>${item.Order_ID}</td>
<td>
<div class="cell-country">
<span class="country-badge ${getCountryClass(item.Country_Name)}"></span>
${item.Country_Name}
</div>
</td>
<td>
<span class="cell-vat ${!item.VAT ? 'empty' : ''}">
${item.VAT || 'N/A'}
</span>
</td>
<td>
<div class="cell-remark ${getRemarkClass(item.Remark)}">
<i class="fas ${getRemarkIcon(item.Remark)}"></i>
${item.Remark || 'N/A'}
</div>
</td>
<td>
<input type="checkbox"
class="recheck-checkbox"
${item.recheck ? 'checked' : ''}
onchange="handleRecheck('${item.Order_ID}', this.checked)">
</td>
</tr>
`).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';
}
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('https://n8n.finorbrands.com/webhook/61f89d5c-8474-4045-b52c-50ee608435c0', {
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();
}
}
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];
// Handle null values
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');
// Update arrow icons
indicator.innerHTML = direction === 'asc'
? '<i class="fas fa-arrow-up"></i><i class="fas fa-arrow-down"></i>'
: '<i class="fas fa-arrow-down"></i><i class="fas fa-arrow-up"></i>';
}
}
}
function toggleFilter(column) {
const container = document.getElementById('filterContainer');
// Close other filter dropdowns
document.querySelectorAll('.filter-dropdown').forEach(dropdown => {
if (dropdown.dataset.column !== column) {
dropdown.classList.remove('show');
}
});
// Toggle current dropdown
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;
// Get unique values for this column
const uniqueValues = [...new Set(
allData.map(item => item[column] === null ? 'null' : item[column])
)];
dropdown.innerHTML = `
<div class="filter-header">
<h4>Filter ${column}</h4>
<button class="close-btn" onclick="this.closest('.filter-dropdown').classList.remove('show')">&times;</button>
</div>
<div class="filter-list">
${uniqueValues.map(value => `
<label class="filter-item">
<input type="checkbox"
data-column="${column}"
value="${value}"
${filters[column] && filters[column].includes(value) ? 'checked' : ''}>
<span>${value === 'null' ? 'No Value' : value}</span>
</label>
`).join('')}
</div>
<button class="filter-clear" onclick="clearFilter('${column}')">
Clear Filter
</button>
`;
// Add event listener for checkbox changes
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);
}
// Update filter indicators
updateFilterIndicators(column);
// Apply filters
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();
}
function updateStats(count = filteredData.length) {
document.getElementById('totalOrders').textContent = allData.length;
document.getElementById('totalCountries').textContent =
[...new Set(allData.map(item => item.Country_Name))].length;
document.getElementById('totalVAT').textContent =
allData.filter(item => item.VAT !== null).length;
document.getElementById('tableCount').textContent = `${count} order${count !== 1 ? 's' : ''}`;
}
function showCredentialPrompt() {
// Remove existing prompt if present
const existing = document.getElementById('credentialPrompt');
if (existing) existing.remove();
// Create prompt container
const prompt = document.createElement('div');
prompt.id = 'credentialPrompt';
prompt.innerHTML = `
<div class="credential-overlay">
<div class="credential-modal">
<div class="credential-content">
<div class="credential-header">
<i class="fas fa-key"></i>
<h2>API Credentials Required</h2>
</div>
<p>The webhook API requires authentication. Please enter your credentials below:</p>
<form id="credentialForm">
<div class="form-group">
<label for="username">Username</label>
<div class="input-group">
<i class="fas fa-user"></i>
<input type="text" id="username" placeholder="Enter username" autocomplete="username">
</div>
</div>
<div class="form-group">
<label for="password">Password/API Key</label>
<div class="input-group">
<i class="fas fa-lock"></i>
<input type="password" id="password" placeholder="Enter password or API key" autocomplete="current-password">
</div>
</div>
<div class="form-actions">
<button type="button" class="btn-secondary" onclick="closeCredentialPrompt()">Cancel</button>
<button type="submit" class="btn-primary">Connect</button>
</div>
</form>
<div class="credential-help">
<i class="fas fa-info-circle"></i>
<span>Your credentials will be used for this session only. They are not stored permanently.</span>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(prompt);
// Add event listener to form
document.getElementById('credentialForm').addEventListener('submit', handleCredentials);
// Close only when clicking the overlay itself, not its children
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();
// Store credentials for future requests
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}`)}`
});
// Close the prompt
closeCredentialPrompt();
// Attempt to reload with credentials
console.log('%c Retrying connection with credentials...', 'color: #10b981; font-weight: bold;');
loadData();
}
// Add credential prompt styles
const style = document.createElement('style');
style.textContent = `
#credentialPrompt {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
z-index: 9999 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 100vw !important;
height: 100vh !important;
margin: 0 !important;
padding: 0 !important;
}
.credential-overlay {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
background: rgba(15, 23, 42, 0.85) !important;
backdrop-filter: blur(8px) !important;
-webkit-backdrop-filter: blur(8px) !important;
z-index: 9998 !important;
width: 100vw !important;
height: 100vh !important;
}
.credential-modal {
position: static !important;
background: white;
border-radius: 16px;
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
width: 90% !important;
max-width: 450px !important;
padding: 2rem !important;
animation: modalPop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) !important;
z-index: 9999 !important;
transform: none !important;
}
@keyframes modalPop {
from {
opacity: 0;
transform: scale(0.95) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.credential-content {
text-align: center;
}
.credential-header {
margin-bottom: 1.5rem;
}
.credential-header i {
color: var(--primary);
font-size: 2.5rem;
margin-bottom: 0.5rem;
display: block;
}
.credential-header h2 {
font-size: 1.5rem;
font-weight: 700;
color: var(--dark);
margin: 0;
}
.credential-modal p {
color: #64748b;
margin-bottom: 1.5rem;
line-height: 1.6;
}
.form-group {
margin-bottom: 1.25rem;
text-align: left;
}
.form-group label {
display: block;
font-size: 0.875rem;
font-weight: 600;
color: var(--dark);
margin-bottom: 0.5rem;
}
.input-group {
position: relative;
}
.input-group i {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
}
.form-group input {
width: 100%;
padding: 0.875rem 1rem 0.875rem 2.75rem;
border: 2px solid var(--border);
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s ease;
}
.form-group input:focus {
outline: none;
border-color: var(--primary);
}
.form-actions {
display: flex;
gap: 0.75rem;
margin: 1.5rem 0;
}
.btn-primary {
flex: 1;
background: var(--primary);
color: white;
border: none;
padding: 0.875rem 1.5rem;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-secondary {
background: var(--hover);
color: #64748b;
border: none;
padding: 0.875rem 1.5rem;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease;
}
.btn-secondary:hover {
background: #e2e8f0;
}
.credential-help {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: var(--hover);
border-radius: 8px;
font-size: 0.875rem;
color: #64748b;
}
.credential-help i {
color: var(--primary);
}
@media (max-width: 480px) {
.credential-modal {
padding: 1.5rem;
margin: 1rem;
}
.form-actions {
flex-direction: column;
}
.btn-primary, .btn-secondary {
width: 100%;
}
}
`;
document.head.appendChild(style);
</script>
</body>
</html>