mirror of
https://github.com/thecyberlearn/supa-tid.git
synced 2026-08-18 09:53:02 +00:00
1897 lines
70 KiB
HTML
1897 lines
70 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>
|
||
// Configure Tailwind for proper styling
|
||
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">Victor AI Agent</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">6:08:40 AM</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>
|
||
// n8n Authentication System
|
||
const N8N_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 {
|
||
console.log('Attempting login with n8n webhook...');
|
||
|
||
const response = await fetch(N8N_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();
|
||
console.log('Authentication response:', result);
|
||
|
||
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) {
|
||
console.error('Login error:', 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') {
|
||
console.error('Supabase library not loaded');
|
||
alert('Failed to load Supabase. Please refresh the page.');
|
||
}
|
||
|
||
if (typeof APP_CONFIG === 'undefined') {
|
||
console.error('APP_CONFIG not loaded');
|
||
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"]
|
||
};
|
||
|
||
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) {
|
||
console.error('Update error:', 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) {
|
||
console.error('Submit edit error:', error);
|
||
showToast("Update failed: " + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
|
||
// Event listener moved to DOMContentLoaded
|
||
|
||
|
||
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 n8n is configured
|
||
if (!APP_CONFIG.useN8n || APP_CONFIG.n8nWebhookUrl === 'YOUR_N8N_WEBHOOK_URL_HERE') {
|
||
console.log('🔄 n8n not configured, using fallback parsing');
|
||
return parseQueryFallback(naturalQuery);
|
||
}
|
||
|
||
try {
|
||
console.log('🤖 Parsing query with n8n AI:', naturalQuery);
|
||
|
||
// Add timeout and better error handling
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15 second timeout for n8n
|
||
|
||
const response = await fetch(APP_CONFIG.n8nWebhookUrl, {
|
||
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 n8n response format
|
||
if (result.success) {
|
||
console.log('✅ n8n AI parsing successful:', result);
|
||
if (result.fallback) {
|
||
console.log('ℹ️ Used fallback logic in n8n');
|
||
}
|
||
return {
|
||
table: result.table,
|
||
filters: result.filters
|
||
};
|
||
} else {
|
||
throw new Error(result.error || 'n8n workflow failed');
|
||
}
|
||
|
||
} catch (error) {
|
||
if (error.name === 'AbortError') {
|
||
console.warn('⚠️ n8n AI parsing timeout, using local fallback');
|
||
} else if (error.message.includes('Failed to fetch')) {
|
||
console.warn('⚠️ Cannot reach n8n webhook, using local fallback. Check your n8n workflow is active.');
|
||
} else {
|
||
console.warn('⚠️ n8n AI parsing failed, using local fallback:', error.message);
|
||
}
|
||
|
||
// Fallback to simple keyword-based parsing
|
||
const fallbackResult = parseQueryFallback(naturalQuery);
|
||
console.log('🔄 Local fallback parsing result:', fallbackResult);
|
||
console.log('🔄 Fallback filters:', fallbackResult.filters);
|
||
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 = [];
|
||
|
||
// 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) {
|
||
console.log('Using cached result for:', table);
|
||
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) {
|
||
console.error('Supabase error:', 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) {
|
||
console.error('Error in fetchFrom:', 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);
|
||
console.log('🔍 Search query:', query);
|
||
console.log('🔍 AI parsing result:', ai);
|
||
|
||
let newResults = [];
|
||
let totalCount = 0;
|
||
|
||
if (ai.table === "all") {
|
||
const [buyerRes, sellerRes, gulfoodRes] = await Promise.all([
|
||
fetchFrom("buyer", ai.filters, searchState.page),
|
||
fetchFrom("seller", ai.filters, searchState.page),
|
||
fetchFrom("gulfood", [], searchState.page) // No filters for gulfood since it has different fields
|
||
]);
|
||
|
||
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, don't apply country/brand/category filters
|
||
const gulfoodFilters = ai.filters.filter(f =>
|
||
["company_name", "address", "phone_number", "mail_id", "website"].includes(f.field)
|
||
);
|
||
console.log('🔍 Gulfood filters:', gulfoodFilters);
|
||
const result = await fetchFrom(ai.table, gulfoodFilters, searchState.page);
|
||
newResults = result.data;
|
||
totalCount = result.count;
|
||
searchState.hasMore = result.hasMore;
|
||
} else {
|
||
const result = await fetchFrom(ai.table, ai.filters, 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) {
|
||
console.error('Search error:', 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();
|
||
console.log('Loaded countries:', allCountries);
|
||
} catch (error) {
|
||
console.error('Error loading countries:', 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) {
|
||
console.error('Buyer fetch error:', 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) {
|
||
console.error('Seller fetch error:', 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) {
|
||
console.error('Product mapping error:', 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) {
|
||
console.error('Export error:', error);
|
||
showToast('Export failed: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
function quickSearch(type) {
|
||
const searchInput = document.getElementById("searchInput");
|
||
if (searchInput) {
|
||
searchInput.value = type;
|
||
performSearch();
|
||
} else {
|
||
console.error('Search input not found');
|
||
}
|
||
}
|
||
|
||
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) {
|
||
console.error('Could not find card element');
|
||
return;
|
||
}
|
||
|
||
const table = card.dataset.table;
|
||
const id = card.dataset.id;
|
||
|
||
if (!table || !id) {
|
||
console.error('Missing table or id data');
|
||
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) {
|
||
console.error('Delete error:', 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) {
|
||
console.error('Delete record error:', 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();
|
||
}
|
||
}
|
||
|
||
console.log('Attempting to insert into table:', table);
|
||
console.log('Data to insert:', data);
|
||
|
||
const { data: result, error } = await client.from(table).insert([data]);
|
||
if (error) {
|
||
console.error('Create error details:', error);
|
||
showToast("Create failed: " + (error.message || JSON.stringify(error)), 'error');
|
||
return;
|
||
}
|
||
|
||
console.log('Insert successful:', result);
|
||
showToast("Record created successfully!", 'success');
|
||
clearCache();
|
||
closeCreateModal();
|
||
|
||
// Refresh current search if one exists
|
||
if (searchState.query) {
|
||
performSearch();
|
||
}
|
||
} catch (error) {
|
||
console.error('Create record error:', error);
|
||
showToast("Create failed: " + (error.message || JSON.stringify(error)), 'error');
|
||
}
|
||
}
|
||
|
||
function initializeDashboard() {
|
||
try {
|
||
console.log('Initializing dashboard...');
|
||
|
||
// Check if required elements exist
|
||
const searchInput = document.getElementById('searchInput');
|
||
|
||
if (!searchInput) {
|
||
console.error('Search input not found');
|
||
return;
|
||
}
|
||
|
||
// Search on Enter
|
||
searchInput.addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
performSearch();
|
||
}
|
||
});
|
||
|
||
// Load countries for suggestions
|
||
loadAllCountries();
|
||
|
||
console.log('Dashboard initialized successfully');
|
||
|
||
} catch (error) {
|
||
console.error('Initialization error:', 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>
|