supa-tid/index.html
2025-06-16 20:27:41 +05:30

2000 lines
73 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Trade Intelligence - AI Trade Management</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Plus Jakarta Sans', 'sans-serif']
}
}
}
}
</script>
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
<script src="./config.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
margin: 0;
padding: 0;
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
@keyframes pulse {
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
}
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.toast-enter {
animation: slideInRight 0.3s ease-out;
}
.toast-exit {
animation: slideOutRight 0.3s ease-in;
}
</style>
</head>
<body>
<!-- Navigation Header -->
<nav class="bg-white/10 backdrop-blur-md border-b border-white/20 sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center py-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center shadow-lg">
<i class="fas fa-ship text-white text-lg"></i>
</div>
<div>
<h1 class="text-lg font-bold text-white">Trade Intelligence</h1>
<p class="text-sm text-white">AI Trade Intelligence</p>
</div>
</div>
<div class="hidden md:flex items-center gap-6">
<div class="text-right">
<p class="text-sm text-white">Last updated</p>
<p class="font-semibold text-white" id="lastUpdated"></p>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<span class="text-sm text-green-600 font-medium text-white">Online</span>
</div>
<button onclick="logout()" class="text-sm text-white/80 hover:text-white px-3 py-1 rounded-lg hover:bg-white/10 transition-colors">
<i class="fas fa-sign-out-alt mr-1"></i>
Logout
</button>
</div>
</div>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 min-h-screen">
<!-- Header Section -->
<div class="text-center mb-8">
<h1 class="text-3xl lg:text-4xl font-bold text-white mb-4">AI Trade Intelligence</h1>
</div>
<!-- Enhanced Search Command Center -->
<div class="bg-white rounded-2xl p-6 mb-8 shadow-lg border border-gray-200">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
<i class="fas fa-brain text-white"></i>
</div>
<h2 class="text-xl font-bold text-gray-800">Trade AI Command</h2>
</div>
<!-- Main Search Bar -->
<div class="flex flex-col sm:flex-row gap-3 mb-4">
<div class="flex-1">
<input
id="searchInput"
type="text"
placeholder="find buyer from uk"
class="w-full px-3 py-2.5 sm:px-4 sm:py-3 rounded-xl border border-gray-300 bg-white text-gray-800 placeholder-gray-500 focus:border-blue-400 focus:ring-2 focus:ring-blue-400/50 outline-none transition-all text-sm sm:text-base"
/>
</div>
<div class="flex gap-2 sm:gap-3">
<button
id="searchButton"
onclick="performSearch()"
class="flex-1 sm:w-auto bg-blue-600 hover:bg-blue-700 text-white px-3 py-2.5 sm:px-6 sm:py-3 rounded-xl font-semibold transition-all flex items-center justify-center gap-1 sm:gap-2 min-w-[90px] sm:min-w-[120px] text-sm sm:text-base"
>
<i id="searchIcon" class="fas fa-search text-xs sm:text-sm"></i>
<span id="searchText">Search</span>
</button>
<button
onclick="openCreateModal()"
class="flex-shrink-0 bg-green-600 hover:bg-green-700 text-white px-3 py-2.5 sm:px-4 sm:py-2.5 rounded-xl font-medium transition-all flex items-center justify-center gap-1 text-sm sm:text-base"
>
<i class="fas fa-plus text-xs sm:text-sm"></i>
<span class="hidden xs:inline">Create</span>
</button>
</div>
</div>
<!-- Quick Actions -->
<div class="mb-4">
<div class="flex flex-wrap gap-2">
<span class="text-xs sm:text-sm text-gray-500 mr-1 sm:mr-2 mb-1">Quick actions:</span>
<button onclick="quickSearch('all buyers')" class="text-xs sm:text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 px-2 py-1 sm:px-3 sm:py-1.5 rounded-full transition-colors min-h-[28px] sm:min-h-[32px]">
All Buyers
</button>
<button onclick="quickSearch('all sellers')" class="text-xs sm:text-sm bg-green-100 hover:bg-green-200 text-green-700 px-2 py-1 sm:px-3 sm:py-1.5 rounded-full transition-colors min-h-[28px] sm:min-h-[32px]">
All Sellers
</button>
<button onclick="quickSearch('gulfood data')" class="text-xs sm:text-sm bg-orange-100 hover:bg-orange-200 text-orange-700 px-2 py-1 sm:px-3 sm:py-1.5 rounded-full transition-colors min-h-[28px] sm:min-h-[32px]">
Gulfood Data
</button>
<button onclick="clearResults()" class="text-xs sm:text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 px-2 py-1 sm:px-3 sm:py-1.5 rounded-full transition-colors min-h-[28px] sm:min-h-[32px]">
Clear Results
</button>
</div>
</div>
<!-- Advanced Search Panel -->
<div class="border-t border-gray-200 pt-4">
<div class="flex items-center gap-2 mb-4">
<i class="fas fa-sliders-h text-gray-600"></i>
<h3 class="text-sm font-semibold text-gray-700">Product Mapping</h3>
<button onclick="toggleAdvancedPanel()" class="sm:hidden ml-auto p-1 text-gray-500 hover:text-gray-700">
<i id="advancedToggleIcon" class="fas fa-chevron-right text-xs"></i>
</button>
</div>
<div id="advancedContent" class="hidden sm:grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4 mb-4">
<!-- Search Type -->
<div>
<label class="block text-xs sm:text-sm font-medium text-gray-700 mb-1 sm:mb-2">Search Type</label>
<select id="searchType" class="w-full px-2 py-2 sm:px-3 sm:py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm sm:text-base">
<option value="all">All Companies</option>
<option value="buyer">Buyers Only</option>
<option value="seller">Sellers Only</option>
</select>
</div>
<!-- Product/Brand -->
<div>
<label class="block text-xs sm:text-sm font-medium text-gray-700 mb-1 sm:mb-2">Product/Brand</label>
<input
id="productSearch"
type="text"
placeholder="FMCG"
class="w-full px-2 py-2 sm:px-3 sm:py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm sm:text-base"
/>
</div>
<!-- Country -->
<div class="relative sm:col-span-2 lg:col-span-1">
<label class="block text-xs sm:text-sm font-medium text-gray-700 mb-1 sm:mb-2">Country</label>
<input
id="countryFilter"
type="text"
placeholder="Type country name..."
class="w-full px-2 py-2 sm:px-3 sm:py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm sm:text-base"
oninput="showCountrySuggestions()"
onfocus="showCountrySuggestions()"
onblur="hideCountrySuggestions()"
/>
<div id="countrySuggestions" class="absolute z-10 w-full bg-white border border-gray-300 rounded-lg mt-1 max-h-48 overflow-y-auto hidden shadow-lg">
<!-- Country suggestions will be populated here -->
</div>
</div>
</div>
<!-- Advanced Search Actions -->
<div id="advancedActions" class="hidden sm:flex gap-2 sm:gap-3">
<button
onclick="performProductMatching()"
class="flex-auto bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 sm:px-6 sm:py-2.5 rounded-lg font-semibold transition-all flex items-center justify-center gap-2 text-sm sm:text-base"
>
<i class="fas fa-link text-xs sm:text-sm"></i>
<span>Product Mapping</span>
</button>
<button
onclick="resetAdvancedFilters()"
class="flex-shrink-0 bg-gray-500 hover:bg-gray-600 text-white px-3 py-2 sm:px-4 sm:py-2.5 rounded-lg font-medium transition-all flex items-center justify-center gap-1 text-sm sm:text-base"
>
<i class="fas fa-undo text-xs sm:text-sm"></i>
<span class="hidden xs:inline">Reset</span>
</button>
</div>
</div>
</div>
<!-- Product Matching Results Section -->
<div id="productMatchingResults" class="hidden bg-white rounded-2xl p-6 shadow-lg border border-gray-200 mb-8">
<div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<i class="fas fa-link text-purple-600"></i>
</div>
<div class="flex-1">
<h3 class="text-base sm:text-lg font-bold text-gray-800">Product Mapping Results</h3>
<p class="text-sm sm:text-base text-gray-600" id="productMatchingSubtitle">Find buyers and sellers for your products</p>
</div>
<button onclick="closeProductMapping()" class="text-gray-400 hover:text-gray-600 p-2">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Product Info Header -->
<div class="bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg p-4 mb-6" id="productInfoHeader">
<!-- Product details will be populated here -->
</div>
<!-- Buyers and Sellers Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Buyers Section -->
<div class="bg-blue-50 rounded-lg p-4">
<div class="flex items-center gap-2 mb-4">
<i class="fas fa-shopping-cart text-blue-600"></i>
<h4 class="text-base sm:text-lg font-semibold text-blue-800" id="buyersTitle">Buyers (0)</h4>
</div>
<div class="space-y-0 max-h-96 overflow-y-auto custom-scrollbar" id="buyersList">
<!-- Buyers will be populated here -->
</div>
</div>
<!-- Sellers Section -->
<div class="bg-green-50 rounded-lg p-4">
<div class="flex items-center gap-2 mb-4">
<i class="fas fa-store text-green-600"></i>
<h4 class="text-base sm:text-lg font-semibold text-green-800" id="sellersTitle">Sellers (0)</h4>
</div>
<div class="space-y-0 max-h-96 overflow-y-auto custom-scrollbar" id="sellersList">
<!-- Sellers will be populated here -->
</div>
</div>
</div>
</div>
<!-- Results Section -->
<div class="mb-8">
<!-- Results will show here -->
<div id="resultsSection" class="hidden">
<div class="bg-white rounded-2xl p-4 shadow-lg border border-gray-200">
<div class="mb-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-bold text-gray-800">Results</h3>
<div class="flex items-center gap-2 flex-shrink-0">
<select id="sortBy" onchange="sortResults()" class="text-xs border border-gray-300 rounded-lg px-2 py-1 focus:border-blue-500">
<option value="default">Default</option>
<option value="name">By Name</option>
<option value="country">By Country</option>
<option value="products">By Products</option>
</select>
<button onclick="exportResults()" class="text-xs bg-green-100 hover:bg-green-200 text-green-700 px-2 py-1 sm:px-3 sm:py-1 rounded-lg transition-colors flex items-center gap-1 flex-shrink-0">
<i class="fas fa-download text-xs"></i>
<span class="hidden sm:inline">Export</span>
</button>
</div>
</div>
<p id="resultsCount" class="text-sm text-gray-600"></p>
</div>
<div id="resultsContainer" class="grid grid-cols-1 lg:grid-cols-2 gap-3 sm:gap-4 lg:gap-6 custom-scrollbar max-h-[600px] overflow-y-auto"></div>
</div>
</div>
<!-- Initial state -->
<div id="initialState" class="bg-white rounded-2xl p-6 shadow-lg border border-gray-200">
<div class="text-center py-12 text-gray-500">
<svg class="w-16 h-16 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<p class="text-lg">Enter a query above to search your database</p>
</div>
</div>
</div>
</div>
<!-- Login Screen -->
<div id="loginScreen" class="fixed inset-0 bg-gradient-to-br from-blue-600 to-purple-700 flex items-center justify-center z-50">
<div class="bg-white rounded-2xl p-8 shadow-2xl max-w-md w-full mx-4">
<div class="text-center mb-8">
<div class="w-16 h-16 bg-blue-600 rounded-xl flex items-center justify-center mx-auto mb-4">
<i class="fas fa-ship text-white text-2xl"></i>
</div>
<h1 class="text-2xl font-bold text-gray-800">Trade Intelligence</h1>
<p class="text-gray-600">Please sign in to continue</p>
</div>
<form id="loginForm" onsubmit="handleLogin(event)">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Username</label>
<input
type="text"
id="username"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none"
placeholder="Enter username"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
<input
type="password"
id="password"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none"
placeholder="Enter password"
/>
</div>
<div id="loginError" class="hidden bg-red-50 border border-red-200 rounded-lg p-3 text-red-700 text-sm">
Invalid username or password
</div>
<button
type="submit"
class="w-full bg-blue-600 hover:bg-blue-700 text-white py-3 px-4 rounded-lg font-semibold transition-colors"
>
Sign In
</button>
</div>
</form>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="fixed top-4 right-4 z-50 space-y-2">
<!-- Toast notifications will be dynamically added here -->
</div>
<!-- Edit Modal -->
<div id="editModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50">
<div class="bg-white rounded-2xl p-4 max-w-sm w-full mx-4 shadow-2xl max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold text-gray-800">Edit Record</h3>
<button onclick="closeModal('editModal')" class="text-gray-400 hover:text-gray-600 p-1">
<i class="fas fa-times"></i>
</button>
</div>
<form id="modalEditForm" class="space-y-3"></form>
<div class="flex gap-2 mt-4">
<button onclick="closeModal('editModal')" class="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 py-2 px-3 text-sm rounded-lg font-medium transition-colors">
Cancel
</button>
<button onclick="submitModalEdit()" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white py-2 px-3 text-sm rounded-lg font-medium transition-colors">
Save
</button>
</div>
</div>
</div>
<!-- Create Modal -->
<div id="createModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50">
<div class="bg-white rounded-2xl p-4 max-w-sm w-full mx-4 shadow-2xl max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold text-gray-800">Create Company</h3>
<button onclick="closeCreateModal()" class="text-gray-400 hover:text-gray-600 p-1">
<i class="fas fa-times"></i>
</button>
</div>
<form id="createForm" onsubmit="handleCreateSubmit(event)">
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Table</label>
<select id="createTable" onchange="updateCreateFormFields()" class="w-full px-2 py-2 text-sm border border-gray-300 rounded-lg focus:border-blue-500">
<option value="buyer">Buyers</option>
<option value="seller">Sellers</option>
<option value="gulfood">Gulfood</option>
</select>
</div>
<div id="createFormFields">
<!-- Dynamic fields will be populated here -->
</div>
</div>
<div class="flex gap-2 mt-4">
<button type="button" onclick="closeCreateModal()" class="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 py-2 px-3 text-sm rounded-lg font-medium transition-colors">
Cancel
</button>
<button type="submit" class="flex-1 bg-green-600 hover:bg-green-700 text-white py-2 px-3 text-sm rounded-lg font-medium transition-colors">
Create
</button>
</div>
</form>
</div>
</div>
<script>
// Authentication System
const AUTH_WEBHOOK = 'https://m8taq6tk.rpcld.cc/webhook/login';
function checkAuthentication() {
const isAuthenticated = localStorage.getItem('authenticated');
const loginScreen = document.getElementById('loginScreen');
if (isAuthenticated === 'true') {
loginScreen.style.display = 'none';
return true;
} else {
loginScreen.style.display = 'flex';
return false;
}
}
async function handleLogin(event) {
event.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorDiv = document.getElementById('loginError');
const submitButton = event.target.querySelector('button[type="submit"]');
// Show loading state
const originalButtonText = submitButton.innerHTML;
submitButton.disabled = true;
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Signing In...';
try {
const response = await fetch(AUTH_WEBHOOK, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
"username": username,
"password": password
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.success === true) {
// Successful login
localStorage.setItem('authenticated', 'true');
localStorage.setItem('currentUser', result.username || username);
document.getElementById('loginScreen').style.display = 'none';
errorDiv.classList.add('hidden');
// Show success message
showToast(`Welcome back, ${result.username || username}!`, 'success');
// Initialize dashboard
initializeDashboard();
} else {
// Failed login
errorDiv.textContent = result.message || 'Invalid username or password';
errorDiv.classList.remove('hidden');
document.getElementById('password').value = '';
}
} catch (error) {
errorDiv.textContent = 'Login failed. Please check your connection and try again.';
errorDiv.classList.remove('hidden');
document.getElementById('password').value = '';
} finally {
// Reset button state
submitButton.disabled = false;
submitButton.innerHTML = originalButtonText;
}
}
function logout() {
localStorage.removeItem('authenticated');
localStorage.removeItem('currentUser');
location.reload();
}
// Check if required dependencies are loaded
if (typeof supabase === 'undefined') {
alert('Failed to load Supabase. Please refresh the page.');
}
if (typeof APP_CONFIG === 'undefined') {
alert('Configuration not loaded. Please check config.js');
}
const client = supabase.createClient(APP_CONFIG.supabaseUrl, APP_CONFIG.supabaseKey);
const tableFields = {
buyer: ["buyer", "country", "category", "brands", "phone_number", "mail_id", "website"],
seller: ["seller", "country", "category", "brands", "phone_number", "mail_id", "website"],
gulfood: ["company_name", "address", "phone_number", "mail_id", "website"]
};
// Validate and normalize AI-generated filters
function validateAndNormalizeFilters(filters) {
if (!filters || !Array.isArray(filters)) {
return [];
}
return filters.map(filter => {
// Ensure filter has required properties
if (!filter || typeof filter !== 'object') {
return null;
}
// Normalize filter structure
const normalizedFilter = {
field: filter.field || 'all',
operator: filter.operator || 'ilike',
value: filter.value || ''
};
// If the value doesn't have % wildcards, add them
if (normalizedFilter.operator === 'ilike' && normalizedFilter.value &&
!normalizedFilter.value.startsWith('%') && !normalizedFilter.value.endsWith('%')) {
normalizedFilter.value = `%${normalizedFilter.value}%`;
}
return normalizedFilter;
}).filter(filter => filter !== null); // Remove invalid filters
}
// Smart filter function for cross-table compatibility
function filterForTable(filters, tableName) {
const validFields = tableFields[tableName];
return filters.filter(filter => {
// Always include fields that exist in this table
if (validFields.includes(filter.field)) {
return true;
}
// Handle cross-table company name searches
if (filter.field === 'buyer' && tableName === 'seller') {
// Map buyer field to seller field
filter.field = 'seller';
return true;
} else if (filter.field === 'seller' && tableName === 'buyer') {
// Map seller field to buyer field
filter.field = 'buyer';
return true;
} else if ((filter.field === 'buyer' || filter.field === 'seller') && tableName === 'gulfood') {
// Map buyer/seller field to company_name for gulfood
filter.field = 'company_name';
return true;
} else if (filter.field === 'company_name' && (tableName === 'buyer' || tableName === 'seller')) {
// Map company_name to appropriate field for buyer/seller tables
filter.field = tableName;
return true;
}
// Handle generic company searches - create appropriate company name filter for each table
const companySearchFields = ['company', 'name', 'business_name', 'firm', 'entity', 'all'];
if (companySearchFields.includes(filter.field)) {
if (tableName === 'buyer') {
filter.field = 'buyer';
return true;
} else if (tableName === 'seller') {
filter.field = 'seller';
return true;
} else if (tableName === 'gulfood') {
filter.field = 'company_name';
return true;
}
}
// CRITICAL: Handle when AI generates 'all' field for specific company searches
// This ensures we search ALL tables when looking for a specific company
if (filter.field === 'all') {
if (tableName === 'buyer') {
filter.field = 'buyer';
return true;
} else if (tableName === 'seller') {
filter.field = 'seller';
return true;
} else if (tableName === 'gulfood') {
filter.field = 'company_name';
return true;
}
}
// Exclude incompatible fields
return false;
}).map(filter => ({...filter})); // Clone to avoid modifying original
}
let editContext = { id: null, table: null };
// Simple cache implementation
const queryCache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
function getCacheKey(table, filters, page, pageSize) {
return JSON.stringify({ table, filters, page, pageSize });
}
function getCachedResult(cacheKey) {
const cached = queryCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return cached.data;
}
return null;
}
function setCachedResult(cacheKey, data) {
queryCache.set(cacheKey, {
data,
timestamp: Date.now()
});
// Clean old cache entries if cache gets too large
if (queryCache.size > 100) {
const oldEntries = Array.from(queryCache.entries())
.filter(([_, entry]) => Date.now() - entry.timestamp > CACHE_DURATION);
oldEntries.forEach(([key, _]) => queryCache.delete(key));
}
}
function clearCache() {
queryCache.clear();
}
function setSearchButtonLoading(isLoading) {
const button = document.getElementById('searchButton');
const icon = document.getElementById('searchIcon');
const text = document.getElementById('searchText');
if (isLoading) {
button.disabled = true;
button.classList.add('opacity-75', 'cursor-not-allowed');
button.classList.remove('hover:bg-blue-700');
icon.className = 'fas fa-spinner fa-spin text-xs sm:text-sm';
text.textContent = 'Searching...';
} else {
button.disabled = false;
button.classList.remove('opacity-75', 'cursor-not-allowed');
button.classList.add('hover:bg-blue-700');
icon.className = 'fas fa-search text-xs sm:text-sm';
text.textContent = 'Search';
}
}
function showToast(message, type = 'success', duration = 4000) {
const container = document.getElementById('toastContainer');
const toastId = 'toast-' + Date.now();
// Determine colors and icon based on type
const config = {
success: {
bgColor: 'bg-green-500',
icon: 'fas fa-check-circle',
borderColor: 'border-green-400'
},
error: {
bgColor: 'bg-red-500',
icon: 'fas fa-exclamation-circle',
borderColor: 'border-red-400'
},
warning: {
bgColor: 'bg-yellow-500',
icon: 'fas fa-exclamation-triangle',
borderColor: 'border-yellow-400'
},
info: {
bgColor: 'bg-blue-500',
icon: 'fas fa-info-circle',
borderColor: 'border-blue-400'
}
};
const { bgColor, icon, borderColor } = config[type] || config.success;
// Create toast element
const toast = document.createElement('div');
toast.id = toastId;
toast.className = `${bgColor} text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 min-w-[320px] max-w-md border-l-4 ${borderColor} toast-enter`;
toast.innerHTML = `
<i class="${icon} text-lg"></i>
<span class="flex-1 text-sm font-medium">${message}</span>
<button onclick="removeToast('${toastId}')" class="text-white hover:text-gray-200 transition-colors">
<i class="fas fa-times text-sm"></i>
</button>
`;
// Add to container
container.appendChild(toast);
// Auto remove after duration
setTimeout(() => {
removeToast(toastId);
}, duration);
}
function removeToast(toastId) {
const toast = document.getElementById(toastId);
if (toast) {
toast.classList.remove('toast-enter');
toast.classList.add('toast-exit');
// Remove from DOM after animation
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}
}
function openModal(id) {
document.getElementById(id).classList.remove("hidden");
document.body.style.overflow = 'hidden';
}
function closeModal(id) {
document.getElementById(id).classList.add("hidden");
document.body.style.overflow = 'auto';
}
function toggleAdvancedPanel() {
const content = document.getElementById('advancedContent');
const actions = document.getElementById('advancedActions');
const icon = document.getElementById('advancedToggleIcon');
// Check if currently hidden (using Tailwind's hidden class)
if (content.classList.contains('hidden')) {
// Show content
content.classList.remove('hidden');
content.classList.add('grid');
actions.classList.remove('hidden');
actions.classList.add('flex');
icon.classList.remove('fa-chevron-right');
icon.classList.add('fa-chevron-down');
} else {
// Hide content
content.classList.remove('grid');
content.classList.add('hidden');
actions.classList.remove('flex');
actions.classList.add('hidden');
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-right');
}
}
function setSearchQuery(query) {
document.getElementById('searchInput').value = query;
performSearch();
}
function openCreateModal() {
updateCreateFields();
openModal("createModal");
}
function openEditModal(record, table) {
const fields = tableFields[table];
const form = document.getElementById("modalEditForm");
form.innerHTML = fields.map(f => `
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">${f.replace('_', ' ').toUpperCase()}</label>
<input name='${f}' value='${record[f] || ''}'
class='w-full px-2 py-2 text-sm border border-gray-300 rounded-lg focus:border-blue-500' />
</div>
`).join('');
editContext = { id: record.id, table };
openModal("editModal");
}
async function submitModalEdit() {
try {
const inputs = document.querySelectorAll("#modalEditForm input");
const update = {};
inputs.forEach(i => update[i.name] = i.value);
const { error } = await client.from(editContext.table).update(update).eq("id", editContext.id);
if (error) {
showToast("Update failed: " + error.message, 'error');
return;
}
showToast("Record updated successfully!", 'success');
clearCache(); // Clear cache after data modification
closeModal("editModal");
// Refresh current search if one exists
if (searchState.query) {
performSearch();
}
} catch (error) {
showToast("Update failed: " + error.message, 'error');
}
}
function renderRecord(r) {
const fields = tableFields[r.__table];
const tableColors = {
buyer: 'border-l-blue-500',
seller: 'border-l-green-500',
gulfood: 'border-l-purple-500'
};
const tableBadges = {
buyer: { color: 'bg-blue-100 text-blue-700', icon: 'fas fa-shopping-cart', label: 'BUYER' },
seller: { color: 'bg-green-100 text-green-700', icon: 'fas fa-store', label: 'SELLER' },
gulfood: { color: 'bg-purple-100 text-purple-700', icon: 'fas fa-globe', label: 'GULFOOD' }
};
const countryFlag = r.country === 'United Kingdom' ? '🇬🇧' :
r.country === 'India' ? '🇮🇳' :
r.country === 'United States' ? '🇺🇸' :
r.country === 'UAE' ? '🇦🇪' :
r.country === 'China' ? '🇨🇳' :
r.country === 'Germany' ? '🇩🇪' :
r.country === 'France' ? '🇫🇷' :
r.country === 'Italy' ? '🇮🇹' : '🌍';
const badge = tableBadges[r.__table];
return `
<div class='bg-white p-3 sm:p-4 border border-gray-200 rounded-xl hover:shadow-lg transition-all duration-200 hover:border-gray-300 ${tableColors[r.__table]} border-l-4 relative' data-id='${r.id}' data-table='${r.__table}'>
<!-- Action buttons in top right -->
<div class='absolute top-2 right-2 sm:top-3 sm:right-3 flex gap-1'>
<button onclick='openEditModal(${JSON.stringify(r)}, "${r.__table}")'
class='p-1 sm:p-1.5 text-blue-600 hover:bg-blue-50 rounded-lg text-xs transition-colors'>
<i class='fas fa-edit'></i>
</button>
<button onclick='deleteRecord(this)'
class='p-1 sm:p-1.5 text-red-600 hover:bg-red-50 rounded-lg text-xs transition-colors'>
<i class='fas fa-trash'></i>
</button>
</div>
<div class='flex items-center gap-2 sm:gap-3 mb-2 sm:mb-3 pr-12 sm:pr-16'>
<span class='text-base sm:text-lg'>${countryFlag}</span>
<div class='flex-1 min-w-0'>
<h4 class='text-xs sm:text-sm font-bold text-gray-900 truncate leading-tight'>
${r.buyer || r.seller || r.company_name || 'Unnamed Record'}
</h4>
<p class='text-xs text-gray-500'>${r.country || 'Unknown Location'}</p>
</div>
</div>
<!-- Badge below title -->
<div class='mb-2 sm:mb-3'>
<span class='inline-flex items-center gap-1 px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full text-xs font-medium ${badge.color}'>
<i class='${badge.icon} text-xs'></i>
<span class='hidden sm:inline'>${badge.label}</span>
<span class='sm:hidden'>${badge.label.slice(0,3).toUpperCase()}</span>
</span>
</div>
<div class='space-y-1.5 sm:space-y-2 mb-2 sm:mb-3'>
${r.phone_number ? `
<div class='flex items-center gap-2'>
<i class='fas fa-phone text-green-600 text-xs w-3 sm:w-4'></i>
<span class='text-xs sm:text-sm text-gray-600 truncate'>${r.phone_number}</span>
</div>
` : ''}
${r.mail_id ? `
<div class='flex items-center gap-2'>
<i class='fas fa-envelope text-blue-600 text-xs w-3 sm:w-4'></i>
<a href='mailto:${r.mail_id}' class='text-xs sm:text-sm text-blue-600 hover:text-blue-800 truncate'>${r.mail_id}</a>
</div>
` : ''}
${r.website ? `
<div class='flex items-center gap-2'>
<i class='fas fa-globe text-purple-600 text-xs w-3 sm:w-4'></i>
<a href='${r.website}' target='_blank' class='text-xs sm:text-sm text-purple-600 hover:text-purple-800 truncate'>${r.website}</a>
</div>
` : ''}
${r.address ? `
<div class='flex items-start gap-2'>
<i class='fas fa-map-marker-alt text-red-600 text-xs w-3 sm:w-4 mt-0.5'></i>
<span class='text-xs sm:text-sm text-gray-600 line-clamp-2'>${r.address}</span>
</div>
` : ''}
</div>
${(r.brands || r.category) ? `
<div class='flex flex-wrap gap-1'>
${r.brands ? r.brands.split(',').slice(0, window.innerWidth < 640 ? 2 : 3).map(brand => `
<span class='bg-gray-100 text-gray-700 px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full text-xs truncate max-w-[80px] sm:max-w-none'>${brand.trim()}</span>
`).join('') : ''}
${r.category ? `
<span class='bg-blue-100 text-blue-700 px-1.5 py-0.5 sm:px-2 sm:py-1 rounded-full text-xs truncate max-w-[100px] sm:max-w-none'>${r.category}</span>
` : ''}
${r.brands && r.brands.split(',').length > (window.innerWidth < 640 ? 2 : 3) ? `
<span class='text-xs text-gray-400'>+${r.brands.split(',').length - (window.innerWidth < 640 ? 2 : 3)} more</span>
` : ''}
</div>
` : ''}
</div>
`;
}
async function parseQueryWithAI(naturalQuery) {
// Check if webhook is configured
if (!APP_CONFIG.useWebhook || APP_CONFIG.webhookUrl === 'YOUR_WEBHOOK_URL_HERE') {
return parseQueryFallback(naturalQuery);
}
try {
// Add timeout and better error handling
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15 second timeout
const response = await fetch(APP_CONFIG.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ query: naturalQuery }),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
let errorMessage = `HTTP ${response.status}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch (e) {
errorMessage = response.statusText || errorMessage;
}
throw new Error(errorMessage);
}
const result = await response.json();
// Handle webhook response format
if (result.success) {
return {
table: result.table,
filters: result.filters
};
} else {
throw new Error(result.error || 'webhook failed');
}
} catch (error) {
if (error.name === 'AbortError') {
// Timeout occurred
} else if (error.message.includes('Failed to fetch')) {
// Network error
} else {
// Other error
}
// Fallback to simple keyword-based parsing
const fallbackResult = parseQueryFallback(naturalQuery);
return fallbackResult;
}
}
function parseQueryFallback(query) {
const lowerQuery = query.toLowerCase();
// Determine table based on keywords
let table = "all";
if (lowerQuery.includes("buyer") && !lowerQuery.includes("seller")) {
table = "buyer";
} else if (lowerQuery.includes("seller") && !lowerQuery.includes("buyer")) {
table = "seller";
} else if (lowerQuery.includes("gulfood")) {
table = "gulfood";
}
// Extract potential filters
const filters = [];
// Extract company names from "find" queries
if (lowerQuery.includes("find")) {
const companyNameMatch = query.match(/find\s+(.+)/i);
if (companyNameMatch) {
const companyName = companyNameMatch[1].trim();
filters.push({ field: "all", operator: "ilike", value: `%${companyName}%` });
}
}
// Country filters
const countries = ["india", "uk", "united kingdom", "usa", "united states", "uae", "china", "germany", "france", "italy"];
for (const country of countries) {
if (lowerQuery.includes(country)) {
const countryValue = country === "uk" ? "United Kingdom" :
country === "usa" ? "United States" :
country.charAt(0).toUpperCase() + country.slice(1);
filters.push({ field: "country", operator: "ilike", value: `%${countryValue}%` });
break;
}
}
// Product/brand filters (only for buyer/seller tables)
if (table !== "gulfood") {
const products = ["pepsi", "cosmetic", "fmcg", "food", "beverage"];
for (const product of products) {
if (lowerQuery.includes(product)) {
filters.push({ field: "brands", operator: "ilike", value: `%${product}%` });
filters.push({ field: "category", operator: "ilike", value: `%${product}%` });
break;
}
}
}
return { table, filters };
}
async function fetchFrom(table, filters, page = 0, pageSize = 100) {
const cacheKey = getCacheKey(table, filters, page, pageSize);
// Check cache first
const cachedResult = getCachedResult(cacheKey);
if (cachedResult) {
return cachedResult;
}
try {
let q = client.from(table).select("*", { count: 'exact' });
const allowed = tableFields[table];
if (filters && filters.length > 0) {
for (const f of filters) {
if (allowed.includes(f.field)) {
if (f.operator === 'ilike') {
q = q.ilike(f.field, f.value);
} else if (f.operator === 'eq') {
q = q.eq(f.field, f.value);
}
}
}
}
// Add pagination with safety check
const start = page * pageSize;
const end = start + pageSize - 1;
// Only add range if start is reasonable
if (start >= 0) {
q = q.range(start, end);
}
const { data, error, count } = await q;
if (error) {
throw new Error(`Database query failed: ${error.message}`);
}
const result = {
data: (data || []).map(row => ({ ...row, __table: table })),
count: count || 0,
hasMore: (data || []).length === pageSize && count > (page + 1) * pageSize
};
// Cache the result
setCachedResult(cacheKey, result);
return result;
} catch (error) {
throw new Error(`Failed to fetch ${table} data: ${error.message}`);
}
}
let searchState = { query: '', page: 0, hasMore: false, results: [] };
async function performSearch(loadMore = false) {
const query = document.getElementById("searchInput").value.trim();
const resultsContainer = document.getElementById("resultsContainer");
const resultStats = document.getElementById("resultsCount");
const resultsSection = document.getElementById("resultsSection");
const initialState = document.getElementById("initialState");
if (!query) {
showErrorMessage("Please enter a search query");
return;
}
// Show loading state on search button
if (!loadMore) {
setSearchButtonLoading(true);
}
// Reset pagination if new search
if (!loadMore || searchState.query !== query) {
searchState = { query, page: 0, hasMore: false, results: [] };
// Clear cache for new searches to ensure fresh data
if (searchState.query !== query) {
clearCache();
}
}
// Show results section, hide initial state and product mapping
resultsSection.classList.remove("hidden");
initialState.style.display = "none";
document.getElementById("productMatchingResults").classList.add("hidden");
if (!loadMore) {
resultsContainer.innerHTML = `
<div class="col-span-full flex flex-col items-center py-8 sm:py-12">
<div class="animate-spin rounded-full h-8 w-8 sm:h-12 sm:w-12 border-4 border-blue-600 border-t-transparent mb-3 sm:mb-4"></div>
<p class="text-sm sm:text-base text-gray-600">Searching with AI...</p>
</div>
`;
resultStats.innerHTML = "";
}
try {
const ai = await parseQueryWithAI(query);
// CRITICAL: Validate and normalize filters before processing
const validatedFilters = validateAndNormalizeFilters(ai.filters);
// BULLETPROOF: Detect specific company name searches and ensure comprehensive search
const isSpecificCompanySearch = query.toLowerCase().includes('find') &&
validatedFilters.some(f => f.field === 'all' || f.field === 'company_name' || f.field === 'buyer' || f.field === 'seller' ||
(f.value && f.value.length > 10 && /[A-Z]/.test(f.value))); // Long value with capitals likely a company name
if (isSpecificCompanySearch && ai.table === 'all') {
// Find the company search value
const companyFilter = validatedFilters.find(f =>
f.field === 'all' || f.field === 'company_name' || f.field === 'buyer' || f.field === 'seller' ||
(f.value && f.value.length > 10)
);
if (companyFilter) {
const companyValue = companyFilter.value;
// Remove the original filter and add comprehensive filters for all tables
const otherFilters = validatedFilters.filter(f => f !== companyFilter);
// Add filters for all company name fields
validatedFilters.length = 0; // Clear array
validatedFilters.push(
...otherFilters,
{ field: 'buyer', operator: 'ilike', value: companyValue },
{ field: 'seller', operator: 'ilike', value: companyValue },
{ field: 'company_name', operator: 'ilike', value: companyValue }
);
}
}
let newResults = [];
let totalCount = 0;
if (ai.table === "all") {
// Smart filter distribution for cross-table search
const buyerFilters = filterForTable(validatedFilters, "buyer");
const sellerFilters = filterForTable(validatedFilters, "seller");
const gulfoodFilters = filterForTable(validatedFilters, "gulfood");
const [buyerRes, sellerRes, gulfoodRes] = await Promise.all([
fetchFrom("buyer", buyerFilters, searchState.page),
fetchFrom("seller", sellerFilters, searchState.page),
fetchFrom("gulfood", gulfoodFilters, searchState.page)
]);
newResults = [...buyerRes.data, ...sellerRes.data, ...gulfoodRes.data];
totalCount = buyerRes.count + sellerRes.count + gulfoodRes.count;
searchState.hasMore = buyerRes.hasMore || sellerRes.hasMore || gulfoodRes.hasMore;
} else if (ai.table === "gulfood") {
// For gulfood table, filter compatible fields only
const gulfoodFilters = filterForTable(validatedFilters, "gulfood");
const result = await fetchFrom(ai.table, gulfoodFilters, searchState.page);
newResults = result.data;
totalCount = result.count;
searchState.hasMore = result.hasMore;
} else {
// For buyer/seller tables, filter compatible fields only
const tableFilters = filterForTable(validatedFilters, ai.table);
const result = await fetchFrom(ai.table, tableFilters, searchState.page);
newResults = result.data;
totalCount = result.count;
searchState.hasMore = result.hasMore;
}
// If no new results on load more, mark as no more available
if (loadMore && newResults.length === 0) {
searchState.hasMore = false;
}
if (loadMore) {
searchState.results = [...searchState.results, ...newResults];
} else {
searchState.results = newResults;
}
if (searchState.results.length === 0) {
resultsContainer.innerHTML = `
<div class="col-span-full flex flex-col items-center py-12">
<svg class="w-16 h-16 mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<p class="text-lg text-gray-500">No results found</p>
<p class="text-gray-400">Try a different search query</p>
</div>
`;
} else {
const resultsHTML = searchState.results.map(r => renderRecord(r)).join('');
const loadMoreButton = searchState.hasMore ? `
<div class="col-span-full flex justify-center mt-4 sm:mt-6">
<button onclick="loadMoreResults()" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 sm:px-6 sm:py-3 rounded-lg font-medium transition-colors text-sm sm:text-base w-full sm:w-auto max-w-xs">
Load More Results
</button>
</div>
` : '';
resultsContainer.innerHTML = resultsHTML + loadMoreButton;
}
resultStats.innerHTML = `Found ${totalCount} companies matching "${query}" (showing ${searchState.results.length})`;
searchState.page++;
} catch (e) {
showErrorMessage(`Search failed: ${e.message}`);
} finally {
// Hide loading state on search button
if (!loadMore) {
setSearchButtonLoading(false);
}
}
}
function loadMoreResults() {
// Disable button to prevent multiple clicks
const loadMoreBtn = document.querySelector('button[onclick="loadMoreResults()"]');
if (loadMoreBtn) {
loadMoreBtn.disabled = true;
loadMoreBtn.innerHTML = '<i class="fas fa-spinner animate-spin mr-2"></i>Loading...';
}
performSearch(true).finally(() => {
// Re-enable button if it still exists (might be removed if no more results)
if (loadMoreBtn && !loadMoreBtn.closest('.col-span-full').removed) {
loadMoreBtn.disabled = false;
loadMoreBtn.innerHTML = 'Load More Results';
}
});
}
function showErrorMessage(message) {
const resultsContainer = document.getElementById("resultsContainer");
const resultsSection = document.getElementById("resultsSection");
const initialState = document.getElementById("initialState");
resultsSection.classList.remove("hidden");
initialState.style.display = "none";
resultsContainer.innerHTML = `
<div class='col-span-full'>
<div class='bg-red-50 border border-red-200 rounded-xl p-6 text-center'>
<svg class="w-12 h-12 mx-auto mb-4 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 class="text-lg font-semibold text-red-900 mb-2">Error</h3>
<p class="text-red-700">${message}</p>
<button onclick="clearResults()" class="mt-4 bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg">
Try Again
</button>
</div>
</div>
`;
}
async function performAdvancedSearch() {
const searchType = document.getElementById("searchType").value;
const productSearch = document.getElementById("productSearch").value;
const countryFilter = document.getElementById("countryFilter").value;
let query = "";
if (searchType === "buyer") {
query = "all buyers";
} else if (searchType === "seller") {
query = "all sellers";
} else {
query = "all companies";
}
if (productSearch) {
query += ` with ${productSearch}`;
}
if (countryFilter) {
query += ` from ${countryFilter}`;
}
document.getElementById("searchInput").value = query;
performSearch();
}
// Country suggestions functionality
let allCountries = [];
let countrySuggestionsTimeout;
async function loadAllCountries() {
try {
// Fetch unique countries from buyer and seller tables only
// Gulfood table doesn't have a 'country' field - it uses 'address'
const [buyerCountries, sellerCountries] = await Promise.all([
client.from('buyer').select('country').not('country', 'is', null),
client.from('seller').select('country').not('country', 'is', null)
]);
const countrySet = new Set();
// Add countries from buyer and seller tables
[...(buyerCountries.data || []), ...(sellerCountries.data || [])]
.forEach(item => {
if (item.country && item.country.trim()) {
countrySet.add(item.country.trim());
}
});
allCountries = Array.from(countrySet).sort();
} catch (error) {
// Fallback to common countries
allCountries = [
'India', 'United Kingdom', 'United States', 'UAE', 'China',
'Germany', 'France', 'Italy', 'Canada', 'Australia', 'Japan',
'Singapore', 'Malaysia', 'Thailand', 'South Korea', 'Brazil'
];
}
}
function showCountrySuggestions() {
const input = document.getElementById('countryFilter');
const suggestions = document.getElementById('countrySuggestions');
const query = input.value.toLowerCase().trim();
// Clear previous timeout
clearTimeout(countrySuggestionsTimeout);
countrySuggestionsTimeout = setTimeout(() => {
if (allCountries.length === 0) {
loadAllCountries().then(() => showCountrySuggestions());
return;
}
// Filter countries based on input
const filteredCountries = allCountries.filter(country =>
country.toLowerCase().includes(query)
).slice(0, 10); // Limit to 10 suggestions
if (filteredCountries.length > 0) {
suggestions.innerHTML = filteredCountries.map(country => `
<div class="px-3 py-2 hover:bg-blue-50 cursor-pointer border-b border-gray-100 last:border-b-0"
onclick="selectCountry('${country}')">
${country}
</div>
`).join('');
suggestions.classList.remove('hidden');
} else {
suggestions.classList.add('hidden');
}
}, 150); // Debounce
}
function hideCountrySuggestions() {
setTimeout(() => {
document.getElementById('countrySuggestions').classList.add('hidden');
}, 200); // Delay to allow click events
}
function selectCountry(country) {
document.getElementById('countryFilter').value = country;
document.getElementById('countrySuggestions').classList.add('hidden');
}
async function performProductMatching() {
const productSearch = document.getElementById("productSearch").value.trim();
const countryFilter = document.getElementById("countryFilter").value;
if (!productSearch) {
showErrorMessage("Please enter a product/brand to find mapping");
return;
}
// Show product matching results section
document.getElementById("productMatchingResults").classList.remove("hidden");
document.getElementById("resultsSection").classList.add("hidden");
document.getElementById("initialState").style.display = "none";
// Update header info
const productHeader = document.getElementById("productInfoHeader");
productHeader.innerHTML = `
<div class="flex items-center justify-between">
<div>
<h3 class="text-base sm:text-lg font-bold text-gray-800">Product: ${productSearch}</h3>
${countryFilter ? `<p class="text-sm sm:text-base text-gray-600">Filtered by: ${countryFilter}</p>` : ''}
</div>
<div class="flex items-center gap-4">
<span class="text-sm sm:text-base text-blue-600 font-semibold" id="buyersCount">0 Buyers</span>
<span class="text-sm sm:text-base text-green-600 font-semibold" id="sellersCount">0 Sellers</span>
</div>
</div>
`;
// Show loading state
document.getElementById("buyersList").innerHTML = `
<div class="text-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-2 border-blue-600 border-t-transparent mx-auto mb-2"></div>
<p class="text-blue-600 text-sm">Loading buyers...</p>
</div>
`;
document.getElementById("sellersList").innerHTML = `
<div class="text-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-2 border-green-600 border-t-transparent mx-auto mb-2"></div>
<p class="text-green-600 text-sm">Loading sellers...</p>
</div>
`;
try {
// Create database-side filters for product search
const productFilters = [
{ field: 'brands', operator: 'ilike', value: `%${productSearch}%` },
{ field: 'category', operator: 'ilike', value: `%${productSearch}%` }
];
// Add country filter if specified
const filters = [...productFilters];
if (countryFilter && countryFilter !== "") {
filters.push({ field: 'country', operator: 'eq', value: countryFilter });
}
// Optimized database queries with proper filtering
const fetchOptimizedBuyers = async () => {
try {
let query = client.from("buyer").select("*", { count: 'exact' });
// Apply product search using OR condition for brands and category
query = query.or(`brands.ilike.%${productSearch}%,category.ilike.%${productSearch}%,buyer.ilike.%${productSearch}%`);
// Apply country filter if specified
if (countryFilter && countryFilter !== "") {
query = query.eq("country", countryFilter);
}
// Limit results for performance
query = query.range(0, 199); // First 200 results
const { data, error, count } = await query;
if (error) throw error;
return {
data: (data || []).map(row => ({ ...row, __table: 'buyer' })),
count: count || 0
};
} catch (error) {
throw new Error(`Failed to fetch buyers: ${error.message}`);
}
};
const fetchOptimizedSellers = async () => {
try {
let query = client.from("seller").select("*", { count: 'exact' });
// Apply product search using OR condition for brands and category
query = query.or(`brands.ilike.%${productSearch}%,category.ilike.%${productSearch}%,seller.ilike.%${productSearch}%`);
// Apply country filter if specified
if (countryFilter && countryFilter !== "") {
query = query.eq("country", countryFilter);
}
// Limit results for performance
query = query.range(0, 199); // First 200 results
const { data, error, count } = await query;
if (error) throw error;
return {
data: (data || []).map(row => ({ ...row, __table: 'seller' })),
count: count || 0
};
} catch (error) {
throw new Error(`Failed to fetch sellers: ${error.message}`);
}
};
// Fetch buyers and sellers in parallel with optimized queries
const [buyerResult, sellerResult] = await Promise.all([
fetchOptimizedBuyers(),
fetchOptimizedSellers()
]);
const buyers = buyerResult.data;
const sellers = sellerResult.data;
// Update counts with actual database counts
document.getElementById("buyersTitle").textContent = `Buyers (${buyerResult.count})`;
document.getElementById("sellersTitle").textContent = `Sellers (${sellerResult.count})`;
document.getElementById("buyersCount").textContent = `${buyerResult.count} Buyers`;
document.getElementById("sellersCount").textContent = `${sellerResult.count} Sellers`;
// Render buyers
if (buyers.length === 0) {
document.getElementById("buyersList").innerHTML = `
<div class="text-center py-8 text-gray-500">
<i class="fas fa-search text-2xl mb-2"></i>
<p>No buyers found for "${productSearch}"</p>
</div>
`;
} else {
document.getElementById("buyersList").innerHTML = buyers.map(buyer => renderMappingCard(buyer)).join('');
}
// Render sellers
if (sellers.length === 0) {
document.getElementById("sellersList").innerHTML = `
<div class="text-center py-8 text-gray-500">
<i class="fas fa-search text-2xl mb-2"></i>
<p>No sellers found for "${productSearch}"</p>
</div>
`;
} else {
document.getElementById("sellersList").innerHTML = sellers.map(seller => renderMappingCard(seller)).join('');
}
} catch (error) {
const errorMessage = error.message || 'Unknown error occurred';
document.getElementById("buyersList").innerHTML = `
<div class="bg-red-50 border border-red-200 rounded-lg p-4 text-center">
<i class="fas fa-exclamation-triangle text-red-600 mb-2"></i>
<p class="text-red-700 text-sm">Error loading buyers: ${errorMessage}</p>
</div>
`;
document.getElementById("sellersList").innerHTML = `
<div class="bg-red-50 border border-red-200 rounded-lg p-4 text-center">
<i class="fas fa-exclamation-triangle text-red-600 mb-2"></i>
<p class="text-red-700 text-sm">Error loading sellers: ${errorMessage}</p>
</div>
`;
}
}
function renderMappingCard(record) {
const countryFlag = record.country === 'United Kingdom' ? '🇬🇧' :
record.country === 'India' ? '🇮🇳' :
record.country === 'United States' ? '🇺🇸' :
record.country === 'UAE' ? '🇦🇪' :
record.country === 'China' ? '🇨🇳' :
record.country === 'Germany' ? '🇩🇪' :
record.country === 'France' ? '🇫🇷' :
record.country === 'Italy' ? '🇮🇹' : '🌍';
const companyName = record.buyer || record.seller || record.company_name || 'Unnamed Company';
return `
<div class="bg-white rounded-lg p-4 border border-gray-200 hover:shadow-md transition-all mb-3">
<div class="flex items-center gap-2 mb-3">
<span class="text-lg">${countryFlag}</span>
<h5 class="font-bold text-gray-900 text-sm flex-1">${companyName}</h5>
</div>
<div class="space-y-2">
<div class="text-xs text-gray-600">
<i class="fas fa-map-marker-alt mr-1"></i>
${record.country || 'Unknown Location'}
</div>
${record.mail_id ? `
<div class="text-xs text-blue-600">
<i class="fas fa-envelope mr-1"></i>
<a href="mailto:${record.mail_id}" class="hover:underline">${record.mail_id}</a>
</div>
` : ''}
${record.phone_number ? `
<div class="text-xs text-green-600">
<i class="fas fa-phone mr-1"></i>
${record.phone_number}
</div>
` : ''}
${record.website ? `
<div class="text-xs text-purple-600">
<i class="fas fa-globe mr-1"></i>
<a href="${record.website}" target="_blank" class="hover:underline">${record.website}</a>
</div>
` : ''}
${record.brands ? `
<div class="text-xs text-gray-700 mt-2 pt-2 border-t border-gray-100">
<span class="font-medium">Products:</span>
<span class="text-gray-600">${record.brands}</span>
</div>
` : ''}
${record.category ? `
<div class="text-xs text-gray-700">
<span class="font-medium">Category:</span>
<span class="text-gray-600">${record.category}</span>
</div>
` : ''}
</div>
</div>
`;
}
function closeProductMapping() {
document.getElementById("productMatchingResults").classList.add("hidden");
document.getElementById("resultsSection").classList.add("hidden");
document.getElementById("initialState").style.display = "block";
}
function resetAdvancedFilters() {
document.getElementById("searchType").value = "all";
document.getElementById("productSearch").value = "";
document.getElementById("countryFilter").value = "";
}
function sortResults() {
const sortBy = document.getElementById('sortBy')?.value;
if (!sortBy || sortBy === 'default') {
return;
}
if (!searchState.results || searchState.results.length === 0) {
return;
}
// Simple sorting implementation
const sorted = [...searchState.results].sort((a, b) => {
let aVal = '';
let bVal = '';
switch(sortBy) {
case 'name':
aVal = a.buyer || a.seller || a.company_name || '';
bVal = b.buyer || b.seller || b.company_name || '';
break;
case 'country':
aVal = a.country || '';
bVal = b.country || '';
break;
case 'products':
aVal = a.brands || a.category || '';
bVal = b.brands || b.category || '';
break;
default:
return 0;
}
return aVal.localeCompare(bVal);
});
searchState.results = sorted;
// Re-render results
const resultsContainer = document.getElementById("resultsContainer");
if (resultsContainer) {
const resultsHTML = searchState.results.map(r => renderRecord(r)).join('');
resultsContainer.innerHTML = resultsHTML;
}
}
function exportResults() {
if (!searchState.results || searchState.results.length === 0) {
showToast("No results to export", 'warning');
return;
}
try {
// Convert to CSV
const headers = ['Name', 'Country', 'Type', 'Products/Category', 'Phone', 'Email', 'Website'];
const rows = searchState.results.map(r => [
r.buyer || r.seller || r.company_name || '',
r.country || '',
r.__table || '',
r.brands || r.category || '',
r.phone_number || '',
r.mail_id || '',
r.website || ''
]);
const csvContent = [headers, ...rows]
.map(row => row.map(field => `"${field}"`).join(','))
.join('\n');
// Download CSV
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `search_results_${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
showToast(`Exported ${searchState.results.length} records successfully`, 'success');
} catch (error) {
showToast('Export failed: ' + error.message, 'error');
}
}
function quickSearch(type) {
const searchInput = document.getElementById("searchInput");
if (searchInput) {
searchInput.value = type;
performSearch();
}
}
function clearResults() {
const resultsSection = document.getElementById("resultsSection");
const initialState = document.getElementById("initialState");
const searchInput = document.getElementById("searchInput");
if (resultsSection) {
resultsSection.classList.add("hidden");
}
if (initialState) {
initialState.style.display = "block";
}
if (searchInput) {
searchInput.value = "";
}
// Hide product mapping results
document.getElementById("productMatchingResults").classList.add("hidden");
// Reset search state
searchState = { query: '', page: 0, hasMore: false, results: [] };
}
async function deleteRecord(btn) {
try {
const card = btn.closest("[data-id]");
if (!card) {
return;
}
const table = card.dataset.table;
const id = card.dataset.id;
if (!table || !id) {
showToast("Could not delete: missing record information", 'error');
return;
}
if (!confirm("Delete this record? This action cannot be undone.")) return;
const { error } = await client.from(table).delete().eq("id", id);
if (error) {
showToast("Delete failed: " + error.message, 'error');
return;
}
showToast("Record deleted successfully", 'success');
clearCache(); // Clear cache after data modification
// Refresh current search if one exists
if (searchState.query) {
performSearch();
}
} catch (error) {
showToast("Delete failed: " + error.message, 'error');
}
}
// Table field definitions based on CSV structure
const tableFieldsDefinition = {
buyer: [
{ name: 'buyer', label: 'Company Name', type: 'text', required: true },
{ name: 'country', label: 'Country', type: 'text', required: true },
{ name: 'phone_number', label: 'Phone Number', type: 'tel', required: false },
{ name: 'mail_id', label: 'Email', type: 'email', required: false },
{ name: 'website', label: 'Website', type: 'url', required: false },
{ name: 'category', label: 'Category', type: 'text', required: false },
{ name: 'brands', label: 'Brands', type: 'text', required: false }
],
seller: [
{ name: 'seller', label: 'Company Name', type: 'text', required: true },
{ name: 'country', label: 'Country', type: 'text', required: true },
{ name: 'phone_number', label: 'Phone Number', type: 'tel', required: false },
{ name: 'mail_id', label: 'Email', type: 'email', required: false },
{ name: 'website', label: 'Website', type: 'url', required: false },
{ name: 'category', label: 'Category', type: 'text', required: false },
{ name: 'brands', label: 'Brands', type: 'text', required: false }
],
gulfood: [
{ name: 'company_name', label: 'Company Name', type: 'text', required: true },
{ name: 'address', label: 'Address', type: 'text', required: false },
{ name: 'phone_number', label: 'Phone Number', type: 'tel', required: false },
{ name: 'mail_id', label: 'Email', type: 'email', required: false },
{ name: 'website', label: 'Website', type: 'url', required: false }
]
};
function updateCreateFormFields() {
const table = document.getElementById('createTable').value;
const fields = tableFieldsDefinition[table];
const container = document.getElementById('createFormFields');
container.innerHTML = fields.map(field => `
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">
${field.label} ${field.required ? '*' : ''}
</label>
<input
type="${field.type}"
name="${field.name}"
${field.required ? 'required' : ''}
class="w-full px-2 py-2 text-sm border border-gray-300 rounded-lg focus:border-blue-500"
placeholder="Enter ${field.label.toLowerCase()}"
>
</div>
`).join('');
}
// Create Modal Functions
function openCreateModal() {
document.getElementById('createModal').classList.remove('hidden');
updateCreateFormFields(); // Initialize with default fields
}
function closeCreateModal() {
document.getElementById('createModal').classList.add('hidden');
document.getElementById('createForm').reset();
}
async function handleCreateSubmit(event) {
event.preventDefault();
try {
const table = document.getElementById('createTable').value;
const formData = new FormData(event.target);
const data = {};
// Collect all form data
for (let [key, value] of formData.entries()) {
if (value.trim()) { // Only include non-empty values
data[key] = value.trim();
}
}
const { data: result, error } = await client.from(table).insert([data]);
if (error) {
showToast("Create failed: " + (error.message || JSON.stringify(error)), 'error');
return;
}
showToast("Record created successfully!", 'success');
clearCache();
closeCreateModal();
// Refresh current search if one exists
if (searchState.query) {
performSearch();
}
} catch (error) {
showToast("Create failed: " + (error.message || JSON.stringify(error)), 'error');
}
}
function updateLastUpdated() {
const now = new Date();
const timeString = now.toLocaleTimeString('en-US', {
hour12: true,
hour: 'numeric',
minute: '2-digit',
second: '2-digit'
});
const lastUpdatedElement = document.getElementById('lastUpdated');
if (lastUpdatedElement) {
lastUpdatedElement.textContent = timeString;
}
}
function initializeDashboard() {
try {
// Check if required elements exist
const searchInput = document.getElementById('searchInput');
if (!searchInput) {
return;
}
// Search on Enter
searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
// Load countries for suggestions
loadAllCountries();
// Update last updated time
updateLastUpdated();
setInterval(updateLastUpdated, 1000);
} catch (error) {
alert('Failed to initialize dashboard. Please refresh the page.');
}
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
// Check authentication first
if (checkAuthentication()) {
initializeDashboard();
}
});
</script>
<style>
/* Custom animations */
@keyframes spin {
to { transform: rotate(360deg); }
}
.animate-spin {
animation: spin 1s linear infinite;
}
/* Smooth transitions */
* {
transition: all 0.2s ease;
}
/* Focus states */
input:focus, select:focus, button:focus {
outline: none;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
</style>
</body>
</html>