| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Fruit Ripeness Classification Dashboard</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> |
| <style> |
| .ripeness-unripe { |
| background-color: #fef9c3; |
| color: #ca8a04; |
| } |
| .ripeness-ripe { |
| background-color: #dcfce7; |
| color: #15803d; |
| } |
| .ripeness-overripe { |
| background-color: #fee2e2; |
| color: #b91c1c; |
| } |
| .confidence-bar { |
| position: relative; |
| height: 8px; |
| background-color: #e5e7eb; |
| border-radius: 4px; |
| overflow: hidden; |
| } |
| .confidence-fill { |
| position: absolute; |
| height: 100%; |
| left: 0; |
| top: 0; |
| border-radius: 4px; |
| } |
| .high-confidence { |
| background-color: #10b981; |
| } |
| .medium-confidence { |
| background-color: #f59e0b; |
| } |
| .low-confidence { |
| background-color: #ef4444; |
| } |
| .mismatch-highlight { |
| border-left: 4px solid #ef4444; |
| animation: pulse 2s infinite; |
| } |
| @keyframes pulse { |
| 0% { background-color: white; } |
| 50% { background-color: #fee2e2; } |
| 100% { background-color: white; } |
| } |
| .scrollbar-hide::-webkit-scrollbar { |
| display: none; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 min-h-screen"> |
| |
| <header class="bg-white shadow"> |
| <div class="max-w-7xl mx-auto px-4 py-4 sm:px-6 lg:px-8 flex justify-between items-center"> |
| <div class="flex items-center"> |
| <div class="flex-shrink-0 flex items-center"> |
| <i class="fas fa-apple-alt text-3xl text-green-600 mr-2"></i> |
| <span class="text-xl font-bold text-gray-900">FruitAI Monitor</span> |
| </div> |
| </div> |
| <div class="flex items-center space-x-4"> |
| <button id="exportBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium flex items-center"> |
| <i class="fas fa-file-export mr-2"></i> Export Data |
| </button> |
| <div class="relative"> |
| <div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> |
| <i class="fas fa-calendar text-gray-500"></i> |
| </div> |
| <input type="text" id="dateRangePicker" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5" placeholder="Select date range"> |
| </div> |
| </div> |
| </div> |
| </header> |
|
|
| |
| <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> |
| |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6"> |
| <div class="bg-white rounded-lg shadow p-4"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <h3 class="text-gray-500 text-sm font-medium">Total Classifications</h3> |
| <p id="totalClassifications" class="text-2xl font-bold text-gray-900">0</p> |
| </div> |
| <div class="p-3 rounded-full bg-blue-100 text-blue-600"> |
| <i class="fas fa-list-ol text-lg"></i> |
| </div> |
| </div> |
| </div> |
| |
| <div class="bg-white rounded-lg shadow p-4"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <h3 class="text-gray-500 text-sm font-medium">Accuracy Rate</h3> |
| <p id="accuracyRate" class="text-2xl font-bold text-gray-900">0%</p> |
| </div> |
| <div class="p-3 rounded-full bg-green-100 text-green-600"> |
| <i class="fas fa-check-circle text-lg"></i> |
| </div> |
| </div> |
| </div> |
| |
| <div class="bg-white rounded-lg shadow p-4"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <h3 class="text-gray-500 text-sm font-medium">Mismatch Rate</h3> |
| <p id="mismatchRate" class="text-2xl font-bold text-gray-900">0%</p> |
| </div> |
| <div class="p-3 rounded-full bg-red-100 text-red-600"> |
| <i class="fas fa-times-circle text-lg"></i> |
| </div> |
| </div> |
| </div> |
| |
| <div class="bg-white rounded-lg shadow p-4"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <h3 class="text-gray-500 text-sm font-medium">Avg Confidence</h3> |
| <p id="avgConfidence" class="text-2xl font-bold text-gray-900">0%</p> |
| </div> |
| <div class="p-3 rounded-full bg-yellow-100 text-yellow-600"> |
| <i class="fas fa-chart-line text-lg"></i> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> |
| <div class="bg-white p-4 rounded-lg shadow"> |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Accuracy Over Time</h3> |
| <canvas id="accuracyChart" height="300"></canvas> |
| </div> |
| |
| <div class="bg-white p-4 rounded-lg shadow"> |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Confidence Distribution</h3> |
| <canvas id="confidenceChart" height="300"></canvas> |
| </div> |
| |
| <div class="bg-white p-4 rounded-lg shadow"> |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Classification Distribution</h3> |
| <canvas id="classificationChart" height="300"></canvas> |
| </div> |
| |
| <div class="bg-white p-4 rounded-lg shadow"> |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Mismatch Analysis</h3> |
| <canvas id="mismatchChart" height="300"></canvas> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white rounded-lg shadow p-4 mb-6"> |
| <h3 class="text-lg font-medium text-gray-900 mb-4">Filters</h3> |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4"> |
| <div> |
| <label for="fruitTypeFilter" class="block text-sm font-medium text-gray-700 mb-1">Fruit Type</label> |
| <select id="fruitTypeFilter" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"> |
| <option value="">All Fruits</option> |
| </select> |
| </div> |
| |
| <div> |
| <label for="predictionFilter" class="block text-sm font-medium text-gray-700 mb-1">Prediction</label> |
| <select id="predictionFilter" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"> |
| <option value="">All</option> |
| <option value="unripe">Unripe</option> |
| <option value="ripe">Ripe</option> |
| <option value="overripe">Overripe</option> |
| </select> |
| </div> |
| |
| <div> |
| <label for="feedbackFilter" class="block text-sm font-medium text-gray-700 mb-1">Feedback</label> |
| <select id="feedbackFilter" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"> |
| <option value="">All</option> |
| <option value="confirmed">Confirmed</option> |
| <option value="overridden">Overridden</option> |
| </select> |
| </div> |
| |
| <div> |
| <label for="confidenceFilter" class="block text-sm font-medium text-gray-700 mb-1">Confidence</label> |
| <select id="confidenceFilter" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"> |
| <option value="">All</option> |
| <option value="high">High (≥80%)</option> |
| <option value="medium">Medium (60-79%)</option> |
| <option value="low">Low (<60%)</option> |
| </select> |
| </div> |
| |
| <div class="flex items-end"> |
| <button id="applyFilters" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium w-full flex items-center justify-center"> |
| <i class="fas fa-filter mr-2"></i> Apply Filters |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white rounded-lg shadow overflow-hidden"> |
| <div class="flex justify-between items-center p-4 border-b"> |
| <h3 class="text-lg font-medium text-gray-900">Classification Results</h3> |
| <div class="flex space-x-2"> |
| <div class="relative"> |
| <div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> |
| <i class="fas fa-search text-gray-400"></i> |
| </div> |
| <input type="text" id="searchInput" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5" placeholder="Search..."> |
| </div> |
| <button id="resetFilters" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-md text-sm font-medium flex items-center"> |
| <i class="fas fa-redo mr-2"></i> Reset |
| </button> |
| </div> |
| </div> |
| |
| <div class="overflow-x-auto"> |
| <table class="min-w-full divide-y divide-gray-200"> |
| <thead class="bg-gray-50"> |
| <tr> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer sort" data-sort="fruit"> |
| <div class="flex items-center"> |
| Fruit <i class="fas fa-sort ml-1 text-gray-400"></i> |
| </div> |
| </th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer sort" data-sort="ai_prediction"> |
| <div class="flex items-center"> |
| AI Prediction <i class="fas fa-sort ml-1 text-gray-400"></i> |
| </div> |
| </th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Image</th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer sort" data-sort="confidence"> |
| <div class="flex items-center"> |
| Confidence <i class="fas fa-sort ml-1 text-gray-400"></i> |
| </div> |
| </th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer sort" data-sort="user_feedback"> |
| <div class="flex items-center"> |
| User Feedback <i class="fas fa-sort ml-1 text-gray-400"></i> |
| </div> |
| </th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer sort" data-sort="feedback_status"> |
| <div class="flex items-center"> |
| Status <i class="fas fa-sort ml-1 text-gray-400"></i> |
| </div> |
| </th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer sort" data-sort="timestamp"> |
| <div class="flex items-center"> |
| Timestamp <i class="fas fa-sort ml-1 text-gray-400"></i> |
| </div> |
| </th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> |
| </tr> |
| </thead> |
| <tbody id="resultsTable" class="bg-white divide-y divide-gray-200"> |
| |
| </tbody> |
| </table> |
| </div> |
| |
| <div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6"> |
| <div class="flex-1 flex justify-between sm:hidden"> |
| <button id="prevPageMobile" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"> |
| Previous |
| </button> |
| <button id="nextPageMobile" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"> |
| Next |
| </button> |
| </div> |
| <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between"> |
| <div> |
| <p id="paginationText" class="text-sm text-gray-700"> |
| Showing <span class="font-medium">1</span> to <span class="font-medium">10</span> of <span class="font-medium">200</span> results |
| </p> |
| </div> |
| <div> |
| <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination"> |
| <button id="firstPage" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"> |
| <span class="sr-only">First</span> |
| <i class="fas fa-angle-double-left"></i> |
| </button> |
| <button id="prevPage" class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"> |
| <span class="sr-only">Previous</span> |
| <i class="fas fa-angle-left"></i> |
| </button> |
| <div id="pageNumbers" class="flex items-center"> |
| |
| </div> |
| <button id="nextPage" class="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"> |
| <span class="sr-only">Next</span> |
| <i class="fas fa-angle-right"></i> |
| </button> |
| <button id="lastPage" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"> |
| <span class="sr-only">Last</span> |
| <i class="fas fa-angle-double-right"></i> |
| </button> |
| </nav> |
| </div> |
| </div> |
| </div> |
| </div> |
| </main> |
|
|
| |
| <div id="detailModal" class="fixed z-10 inset-0 overflow-y-auto hidden" aria-labelledby="modal-title" role="dialog" aria-modal="true"> |
| <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> |
| <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div> |
| <span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span> |
| <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full"> |
| <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> |
| <div class="sm:flex sm:items-start"> |
| <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full"> |
| <h3 class="text-lg leading-6 font-medium text-gray-900" id="modalTitle">Classification Details</h3> |
| <div class="mt-2"> |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> |
| <div> |
| <div id="detailImage" class="w-full h-48 bg-gray-200 rounded-md overflow-hidden flex items-center justify-center"> |
| <i class="fas fa-image text-gray-400 text-4xl"></i> |
| </div> |
| <div class="mt-2 text-sm"> |
| <p><span class="font-medium">Classification ID:</span> <span id="detailId"></span></p> |
| <p><span class="font-medium">Uploaded by:</span> <span id="detailUploadedBy"></span></p> |
| <p><span class="font-medium">Location:</span> <span id="detailLocation"></span></p> |
| <p><span class="font-medium">Harvested on:</span> <span id="detailHarvestDate"></span></p> |
| </div> |
| </div> |
| |
| <div> |
| <div class="bg-gray-50 p-3 rounded-md"> |
| <h4 class="font-medium text-gray-900 mb-2">AI Analysis</h4> |
| <div class="mb-2"> |
| <span class="font-medium">Prediction:</span> <span id="detailAiPrediction" class="px-2 py-1 rounded-full text-xs font-medium"></span> |
| </div> |
| <div class="mb-2"> |
| <div class="flex justify-between mb-1"> |
| <span class="font-medium">Confidence:</span> |
| <span id="detailConfidence" class="font-medium"></span> |
| </div> |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> |
| <div id="detailConfidenceBar" class="h-2.5 rounded-full"></div> |
| </div> |
| </div> |
| <div class="mb-2"> |
| <span class="font-medium">Temperature:</span> <span id="detailTemperature"></span>°C |
| </div> |
| <div class="mb-2"> |
| <span class="font-medium">Humidity:</span> <span id="detailHumidity"></span>% |
| </div> |
| <div> |
| <span class="font-medium">Weight:</span> <span id="detailWeight"></span>g |
| </div> |
| </div> |
| </div> |
| |
| <div> |
| <div class="bg-gray-50 p-3 rounded-md mb-3"> |
| <h4 class="font-medium text-gray-900 mb-2">Human Feedback</h4> |
| <div class="mb-2"> |
| <span class="font-medium">Feedback:</span> <span id="detailUserFeedback" class="px-2 py-1 rounded-full text-xs font-medium"></span> |
| </div> |
| <div class="mb-2"> |
| <span class="font-medium">Status:</span> <span id="detailFeedbackStatus" class="px-2 py-1 rounded-full text-xs font-medium"></span> |
| </div> |
| <div class="mb-2"> |
| <span class="font-medium">Timestamp:</span> <span id="detailTimestamp"></span> |
| </div> |
| </div> |
| <div class="bg-gray-50 p-3 rounded-md"> |
| <h4 class="font-medium text-gray-900 mb-2">Notes</h4> |
| <p id="detailNotes" class="text-sm italic"></p> |
| </div> |
| </div> |
| </div> |
| |
| <div class="mt-4 grid grid-cols-2 gap-4"> |
| <button type="button" id="editBtn" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none sm:text-sm"> |
| <i class="fas fa-edit mr-2"></i> Edit Feedback |
| </button> |
| <button type="button" class="w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:text-sm" onclick="document.getElementById('detailModal').classList.add('hidden')"> |
| <i class="fas fa-times mr-2"></i> Close |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| function generateMockData(count = 200) { |
| const fruits = [ |
| 'apple', 'mango', 'orange' |
| ]; |
| |
| const ripeness = ['unripe', 'ripe', 'overripe']; |
| const feedbackOptions = ['confirmed', 'overridden']; |
| const locations = [ |
| 'Farm A', 'Farm B', 'Farm C', 'Farm D', 'Farm E', |
| 'Orchard X', 'Greenhouse Y', 'Plantation Z', |
| 'Organic Valley', 'Mountain View Farms' |
| ]; |
| |
| const users = [ |
| 'User1', 'User2', 'User3', 'User4', 'User5', |
| 'InspectorA', 'QualityB', 'TesterC', 'AgentD', 'ManagerE' |
| ]; |
| |
| const notesOptions = [ |
| "Color grading matches maturity level", |
| "Slight bruising detected", |
| "Perfect specimen", |
| "Irregular shape but good quality", |
| "Sun-exposed side shows advanced ripeness", |
| "Harvested slightly early", |
| "Optimal sweetness level", |
| "Needs immediate processing", |
| "Excellent for long-term storage", |
| "Best for juicing" |
| ]; |
| |
| const data = []; |
| const startDate = new Date(); |
| startDate.setDate(startDate.getDate() - 60); |
| |
| for (let i = 0; i < count; i++) { |
| const fruit = fruits[Math.floor(Math.random() * fruits.length)]; |
| const aiPrediction = ripeness[Math.floor(Math.random() * ripeness.length)]; |
| |
| |
| let confidence; |
| if (Math.random() > 0.1) { |
| confidence = Math.floor(Math.random() * 20) + 75; |
| } else { |
| confidence = Math.floor(Math.random() * 50) + 30; |
| } |
| |
| let feedback = feedbackOptions[Math.floor(Math.random() * feedbackOptions.length)]; |
| let userFeedback; |
| |
| |
| if (Math.random() < 0.15) { |
| feedback = 'overridden'; |
| } |
| |
| if (feedback === 'confirmed') { |
| userFeedback = aiPrediction; |
| } else { |
| |
| userFeedback = ripeness.filter(r => r !== aiPrediction)[Math.floor(Math.random() * (ripeness.length - 1))]; |
| |
| |
| if (confidence < 60 && Math.random() < 0.7) { |
| feedback = 'overridden'; |
| userFeedback = ripeness.filter(r => r !== aiPrediction)[Math.floor(Math.random() * (ripeness.length - 1))]; |
| } |
| } |
| |
| |
| const timestamp = new Date( |
| startDate.getTime() + |
| Math.random() * 60 * 24 * 60 * 60 * 1000 + |
| Math.random() * 8 * 60 * 60 * 1000 |
| ); |
| |
| data.push({ |
| id: `FR-${('0000' + i).slice(-4)}-${timestamp.getFullYear().toString().slice(-2)}`, |
| fruit: fruit, |
| image: getFruitImage(fruit), |
| ai_prediction: aiPrediction, |
| confidence: confidence, |
| user_feedback: userFeedback, |
| feedback_status: feedback, |
| timestamp: timestamp.toISOString(), |
| location: locations[Math.floor(Math.random() * locations.length)], |
| uploaded_by: users[Math.floor(Math.random() * users.length)], |
| notes: notesOptions[Math.floor(Math.random() * notesOptions.length)], |
| |
| harvest_date: new Date( |
| timestamp.getTime() - |
| Math.random() * 7 * 24 * 60 * 60 * 1000 |
| ).toISOString(), |
| temperature: Math.round((20 + Math.random() * 15) * 10) / 10, |
| humidity: Math.round((60 + Math.random() * 30) * 10) / 10, |
| weight: Math.round((100 + Math.random() * 400) * 10) / 10 |
| }); |
| } |
| |
| return data; |
| } |
| |
| |
| function getFruitImage(fruit) { |
| const images = { |
| apple: 'https://images.unsplash.com/photo-1568702846914-96b305d2aaeb?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| banana: 'https://images.unsplash.com/photo-1603833665858-e61bb17a7218?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| mango: 'https://images.unsplash.com/photo-1553279768-865429fa0078?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| orange: 'https://images.unsplash.com/photo-1547514701-42782101795e?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| strawberry: 'https://images.unsplash.com/photo-1464965911861-746a04b4bca6?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| peach: 'https://images.unsplash.com/photo-1559181567-c3190ca9959b?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| pear: 'https://images.unsplash.com/photo-1530893609605-72d7600779ae?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| grape: 'https://images.unsplash.com/photo-1517587171378-24a6fba3c2af?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| kiwi: 'https://images.unsplash.com/photo-1598283027164-78bd17e81663?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| pineapple: 'https://images.unsplash.com/photo-1490885578164-de435ba9c64b?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| watermelon: 'https://images.unsplash.com/photo-1571575173700-afb9492e6a50?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| blueberry: 'https://images.unsplash.com/photo-1498557850523-fd3d118b962e?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| raspberry: 'https://images.unsplash.com/photo-1518633626590-c51b881e2c96?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| blackberry: 'https://images.unsplash.com/photo-1493925415034-d6f1a3f436b1?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80', |
| cherry: 'https://images.unsplash.com/photo-1533158313479-c24d2f16f3b5?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=80' |
| }; |
| return images[fruit] || images['apple']; |
| } |
| |
| |
| function getFruitColor(fruit) { |
| const colors = { |
| apple: '#db2777', |
| banana: '#f59e0b', |
| mango: '#ea580c', |
| orange: '#ea580c', |
| strawberry: '#e11d48', |
| peach: '#f97316', |
| pear: '#84cc16', |
| grape: '#7e22ce', |
| kiwi: '#22c55e', |
| pineapple: '#facc15', |
| watermelon: '#ef4444', |
| blueberry: '#3b82f6', |
| raspberry: '#ec4899', |
| blackberry: '#6b7280', |
| cherry: '#b91c1c' |
| }; |
| return colors[fruit] || '#6b7280'; |
| } |
| |
| |
| function getRipenessClass(ripeness) { |
| return `ripeness-${ripeness}`; |
| } |
| |
| function getRipenessText(ripeness) { |
| return ripeness.charAt(0).toUpperCase() + ripeness.slice(1); |
| } |
| |
| |
| function getStatusClass(status) { |
| return { |
| 'confirmed': 'bg-green-100 text-green-800', |
| 'overridden': 'bg-red-100 text-red-800' |
| }[status]; |
| } |
| |
| function getStatusText(status) { |
| return { |
| 'confirmed': 'Confirmed', |
| 'overridden': 'Overridden' |
| }[status]; |
| } |
| |
| |
| function getConfidenceClass(confidence) { |
| if (confidence >= 80) { |
| return 'high-confidence'; |
| } else if (confidence >= 60) { |
| return 'medium-confidence'; |
| } else { |
| return 'low-confidence'; |
| } |
| } |
| |
| |
| function formatDate(dateString) { |
| return moment(dateString).format('MMM D, YYYY h:mm A'); |
| } |
| |
| |
| function initDashboard() { |
| |
| const allData = generateMockData(200); |
| |
| |
| let currentPage = 1; |
| let itemsPerPage = 10; |
| let filteredData = [...allData]; |
| let sortColumn = null; |
| let sortDirection = 'asc'; |
| let currentDetail = null; |
| |
| |
| const resultsTable = document.getElementById('resultsTable'); |
| const paginationText = document.getElementById('paginationText'); |
| const pageNumbers = document.getElementById('pageNumbers'); |
| const fruitTypeFilter = document.getElementById('fruitTypeFilter'); |
| const predictionFilter = document.getElementById('predictionFilter'); |
| const feedbackFilter = document.getElementById('feedbackFilter'); |
| const confidenceFilter = document.getElementById('confidenceFilter'); |
| const applyFilters = document.getElementById('applyFilters'); |
| const resetFilters = document.getElementById('resetFilters'); |
| const searchInput = document.getElementById('searchInput'); |
| const exportBtn = document.getElementById('exportBtn'); |
| const totalClassifications = document.getElementById('totalClassifications'); |
| const accuracyRate = document.getElementById('accuracyRate'); |
| const mismatchRate = document.getElementById('mismatchRate'); |
| const avgConfidence = document.getElementById('avgConfidence'); |
| const detailModal = document.getElementById('detailModal'); |
| |
| |
| let accuracyChart, confidenceChart, classificationChart, mismatchChart; |
| |
| |
| const dateRangePicker = document.getElementById('dateRangePicker'); |
| dateRangePicker.addEventListener('input', applyFiltersFunction); |
| |
| |
| const uniqueFruits = [...new Set(allData.map(item => item.fruit))]; |
| uniqueFruits.forEach(fruit => { |
| const option = document.createElement('option'); |
| option.value = fruit; |
| option.textContent = fruit.charAt(0).toUpperCase() + fruit.slice(1); |
| fruitTypeFilter.appendChild(option); |
| }); |
| |
| |
| function renderTable() { |
| const startIndex = (currentPage - 1) * itemsPerPage; |
| const endIndex = Math.min(startIndex + itemsPerPage, filteredData.length); |
| const currentData = filteredData.slice(startIndex, endIndex); |
| |
| resultsTable.innerHTML = ''; |
| |
| if (currentData.length === 0) { |
| resultsTable.innerHTML = ` |
| <tr> |
| <td colspan="8" class="px-6 py-4 text-center text-gray-500"> |
| No results found matching your filters. |
| </td> |
| </tr> |
| `; |
| return; |
| } |
| |
| currentData.forEach(item => { |
| const row = document.createElement('tr'); |
| |
| |
| if (item.feedback_status === 'overridden') { |
| row.classList.add('mismatch-highlight'); |
| } |
| |
| row.innerHTML = ` |
| <td class="px-6 py-4 whitespace-nowrap"> |
| <div class="flex items-center"> |
| <div class="w-4 h-4 rounded-full mr-2" style="background-color: ${getFruitColor(item.fruit)}"></div> |
| <div class="text-sm font-medium text-gray-900 capitalize">${item.fruit}</div> |
| </div> |
| </td> |
| |
| <td class="px-6 py-4 whitespace-nowrap"> |
| <span class="px-2 py-1 text-xs font-medium rounded-full capitalize ${getRipenessClass(item.ai_prediction)}"> |
| ${getRipenessText(item.ai_prediction)} |
| </span> |
| </td> |
| |
| <td class="px-6 py-4 whitespace-nowrap"> |
| <div class="w-10 h-10 rounded-md overflow-hidden"> |
| <img src="${item.image}" alt="${item.fruit}" class="w-full h-full object-cover"> |
| </div> |
| </td> |
| |
| <td class="px-6 py-4 whitespace-nowrap"> |
| <div class="flex items-center"> |
| <div class="flex-shrink-0 mr-2 text-sm font-medium">${item.confidence}%</div> |
| <div class="w-full max-w-xs"> |
| <div class="confidence-bar"> |
| <div class="confidence-fill ${getConfidenceClass(item.confidence)}" style="width: ${item.confidence}%"></div> |
| </div> |
| </div> |
| </div> |
| </td> |
| |
| <td class="px-6 py-4 whitespace-nowrap"> |
| <span class="px-2 py-1 text-xs font-medium rounded-full capitalize ${getRipenessClass(item.user_feedback)}"> |
| ${getRipenessText(item.user_feedback)} |
| </span> |
| </td> |
| |
| <td class="px-6 py-4 whitespace-nowrap"> |
| <span class="px-2 py-1 text-xs font-medium rounded-full ${getStatusClass(item.feedback_status)}"> |
| ${getStatusText(item.feedback_status)} |
| </span> |
| </td> |
| |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> |
| ${formatDate(item.timestamp)} |
| </td> |
| |
| <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> |
| <button class="text-indigo-600 hover:text-indigo-900 mr-2 view-detail" data-id="${item.id}"> |
| <i class="fas fa-eye"></i> |
| </button> |
| </td> |
| `; |
| |
| resultsTable.appendChild(row); |
| }); |
| |
| |
| paginationText.textContent = `Showing ${startIndex + 1} to ${endIndex} of ${filteredData.length} results`; |
| |
| |
| renderPagination(); |
| |
| |
| document.querySelectorAll('.view-detail').forEach(btn => { |
| btn.addEventListener('click', function() { |
| const id = this.getAttribute('data-id'); |
| showDetail(id); |
| }); |
| }); |
| } |
| |
| |
| function renderPagination() { |
| pageNumbers.innerHTML = ''; |
| const totalPages = Math.ceil(filteredData.length / itemsPerPage); |
| |
| |
| if (currentPage > 3) { |
| const pageItem = document.createElement('button'); |
| pageItem.textContent = '1'; |
| pageItem.className = `relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium ${currentPage === 1 ? 'bg-blue-50 text-blue-600' : 'bg-white text-gray-500 hover:bg-gray-50'}`; |
| pageItem.addEventListener('click', () => { |
| currentPage = 1; |
| renderTable(); |
| }); |
| pageNumbers.appendChild(pageItem); |
| |
| if (currentPage > 4) { |
| const ellipsis = document.createElement('span'); |
| ellipsis.className = 'relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700'; |
| ellipsis.textContent = '...'; |
| pageNumbers.appendChild(ellipsis); |
| } |
| } |
| |
| |
| const startPage = Math.max(1, currentPage - 2); |
| const endPage = Math.min(totalPages, currentPage + 2); |
| |
| for (let i = startPage; i <= endPage; i++) { |
| const pageItem = document.createElement('button'); |
| pageItem.textContent = i; |
| pageItem.className = `relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium ${currentPage === i ? 'bg-blue-50 text-blue-600' : 'bg-white text-gray-500 hover:bg-gray-50'}`; |
| pageItem.addEventListener('click', () => { |
| currentPage = i; |
| renderTable(); |
| }); |
| pageNumbers.appendChild(pageItem); |
| } |
| |
| |
| if (currentPage < totalPages - 2) { |
| if (currentPage < totalPages - 3) { |
| const ellipsis = document.createElement('span'); |
| ellipsis.className = 'relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700'; |
| ellipsis.textContent = '...'; |
| pageNumbers.appendChild(ellipsis); |
| } |
| |
| const pageItem = document.createElement('button'); |
| pageItem.textContent = totalPages; |
| pageItem.className = `relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium ${currentPage === totalPages ? 'bg-blue-50 text-blue-600' : 'bg-white text-gray-500 hover:bg-gray-50'}`; |
| pageItem.addEventListener('click', () => { |
| currentPage = totalPages; |
| renderTable(); |
| }); |
| pageNumbers.appendChild(pageItem); |
| } |
| } |
| |
| |
| function applyFiltersFunction() { |
| currentPage = 1; |
| filteredData = [...allData]; |
| |
| |
| if (fruitTypeFilter.value) { |
| filteredData = filteredData.filter(item => item.fruit === fruitTypeFilter.value); |
| } |
| |
| |
| if (predictionFilter.value) { |
| filteredData = filteredData.filter(item => item.ai_prediction === predictionFilter.value); |
| } |
| |
| |
| if (feedbackFilter.value) { |
| filteredData = filteredData.filter(item => item.feedback_status === feedbackFilter.value); |
| } |
| |
| |
| if (confidenceFilter.value) { |
| filteredData = filteredData.filter(item => { |
| if (confidenceFilter.value === 'high') return item.confidence >= 80; |
| if (confidenceFilter.value === 'medium') return item.confidence >= 60 && item.confidence < 80; |
| if (confidenceFilter.value === 'low') return item.confidence < 60; |
| return true; |
| }); |
| } |
| |
| |
| if (dateRangePicker.value) { |
| const [startDateStr, endDateStr] = dateRangePicker.value.split(' - '); |
| const startDate = new Date(startDateStr); |
| const endDate = new Date(endDateStr); |
| |
| filteredData = filteredData.filter(item => { |
| const itemDate = new Date(item.timestamp); |
| return itemDate >= startDate && itemDate <= endDate; |
| }); |
| } |
| |
| |
| if (searchInput.value) { |
| const searchTerm = searchInput.value.toLowerCase(); |
| filteredData = filteredData.filter(item => |
| item.fruit.toLowerCase().includes(searchTerm) || |
| item.ai_prediction.toLowerCase().includes(searchTerm) || |
| item.user_feedback.toLowerCase().includes(searchTerm) || |
| item.location.toLowerCase().includes(searchTerm) || |
| item.uploaded_by.toLowerCase().includes(searchTerm) || |
| item.id.toLowerCase().includes(searchTerm) |
| ); |
| } |
| |
| |
| if (sortColumn) { |
| filteredData.sort((a, b) => { |
| let aValue = a[sortColumn]; |
| let bValue = b[sortColumn]; |
| |
| |
| if (sortColumn === 'timestamp') { |
| aValue = new Date(a.timestamp).getTime(); |
| bValue = new Date(b.timestamp).getTime(); |
| } |
| |
| if (typeof aValue === 'string') aValue = aValue.toLowerCase(); |
| if (typeof bValue === 'string') bValue = bValue.toLowerCase(); |
| |
| if (aValue < bValue) return sortDirection === 'asc' ? -1 : 1; |
| if (aValue > bValue) return sortDirection === 'asc' ? 1 : -1; |
| return 0; |
| }); |
| } |
| |
| |
| updateMetrics(); |
| |
| |
| renderCharts(); |
| |
| |
| renderTable(); |
| } |
| |
| |
| function resetFiltersFunction() { |
| fruitTypeFilter.value = ''; |
| predictionFilter.value = ''; |
| feedbackFilter.value = ''; |
| confidenceFilter.value = ''; |
| dateRangePicker.value = ''; |
| searchInput.value = ''; |
| sortColumn = null; |
| sortDirection = 'asc'; |
| |
| |
| document.querySelectorAll('.sort i').forEach(icon => { |
| icon.classList.remove('fa-sort-up', 'fa-sort-down'); |
| icon.classList.add('fa-sort'); |
| }); |
| |
| applyFiltersFunction(); |
| } |
| |
| |
| function showDetail(id) { |
| const item = allData.find(item => item.id === id); |
| if (!item) return; |
| |
| currentDetail = item; |
| |
| |
| document.getElementById('modalTitle').textContent = `${item.fruit.charAt(0).toUpperCase() + item.fruit.slice(1)} Classification`; |
| document.getElementById('detailId').textContent = item.id; |
| document.getElementById('detailAiPrediction').textContent = getRipenessText(item.ai_prediction); |
| document.getElementById('detailAiPrediction').className = `px-2 py-1 rounded-full text-xs font-medium capitalize ${getRipenessClass(item.ai_prediction)}`; |
| document.getElementById('detailConfidence').textContent = `${item.confidence}%`; |
| document.getElementById('detailConfidenceBar').className = `h-2.5 rounded-full ${getConfidenceClass(item.confidence)}`; |
| document.getElementById('detailConfidenceBar').style.width = `${item.confidence}%`; |
| document.getElementById('detailUserFeedback').textContent = getRipenessText(item.user_feedback); |
| document.getElementById('detailUserFeedback').className = `px-2 py-1 rounded-full text-xs font-medium capitalize ${getRipenessClass(item.user_feedback)}`; |
| document.getElementById('detailFeedbackStatus').textContent = getStatusText(item.feedback_status); |
| document.getElementById('detailFeedbackStatus').className = `px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(item.feedback_status)}`; |
| document.getElementById('detailTimestamp').textContent = formatDate(item.timestamp); |
| document.getElementById('detailUploadedBy').textContent = item.uploaded_by; |
| document.getElementById('detailLocation').textContent = item.location; |
| document.getElementById('detailHarvestDate').textContent = formatDate(item.harvest_date); |
| document.getElementById('detailTemperature').textContent = item.temperature; |
| document.getElementById('detailHumidity').textContent = item.humidity; |
| document.getElementById('detailWeight').textContent = item.weight; |
| document.getElementById('detailNotes').textContent = item.notes; |
| |
| |
| const detailImage = document.getElementById('detailImage'); |
| detailImage.innerHTML = ''; |
| const img = document.createElement('img'); |
| img.src = item.image; |
| img.alt = item.fruit; |
| img.className = 'w-full h-full object-cover'; |
| detailImage.appendChild(img); |
| |
| |
| detailModal.classList.remove('hidden'); |
| } |
| |
| |
| function updateMetrics() { |
| totalClassifications.textContent = filteredData.length; |
| |
| |
| const confirmedCount = filteredData.filter(item => item.feedback_status === 'confirmed').length; |
| const accuracy = filteredData.length > 0 ? Math.round((confirmedCount / filteredData.length) * 100) : 0; |
| accuracyRate.textContent = `${accuracy}%`; |
| |
| |
| const mismatchCount = filteredData.filter(item => item.feedback_status === 'overridden').length; |
| const mismatchRateValue = filteredData.length > 0 ? Math.round((mismatchCount / filteredData.length) * 100) : 0; |
| mismatchRate.textContent = `${mismatchRateValue}%`; |
| |
| |
| const avgConfidenceValue = filteredData.length > 0 |
| ? Math.round(filteredData.reduce((sum, item) => sum + item.confidence, 0) / filteredData.length) |
| : 0; |
| avgConfidence.textContent = `${avgConfidenceValue}%`; |
| } |
| |
| |
| function renderCharts() { |
| |
| if (accuracyChart) accuracyChart.destroy(); |
| if (confidenceChart) confidenceChart.destroy(); |
| if (classificationChart) classificationChart.destroy(); |
| if (mismatchChart) mismatchChart.destroy(); |
| |
| |
| const weeklyGroups = {}; |
| filteredData.forEach(item => { |
| const week = moment(item.timestamp).startOf('week').format('MMM D'); |
| if (!weeklyGroups[week]) { |
| weeklyGroups[week] = { |
| total: 0, |
| confirmed: 0, |
| overridden: 0 |
| }; |
| } |
| weeklyGroups[week].total++; |
| if (item.feedback_status === 'confirmed') { |
| weeklyGroups[week].confirmed++; |
| } else { |
| weeklyGroups[week].overridden++; |
| } |
| }); |
| |
| const weeks = Object.keys(weeklyGroups); |
| const confirmedData = weeks.map(week => weeklyGroups[week].confirmed); |
| const overriddenData = weeks.map(week => weeklyGroups[week].overridden); |
| |
| |
| const accuracyCtx = document.getElementById('accuracyChart').getContext('2d'); |
| accuracyChart = new Chart(accuracyCtx, { |
| type: 'line', |
| data: { |
| labels: weeks, |
| datasets: [ |
| { |
| label: 'Accuracy Rate (%)', |
| data: weeks.map(week => { |
| const group = weeklyGroups[week]; |
| return group.total > 0 ? Math.round((group.confirmed / group.total) * 100) : 0; |
| }), |
| borderColor: '#10b981', |
| backgroundColor: 'rgba(16, 185, 129, 0.1)', |
| borderWidth: 2, |
| fill: true, |
| tension: 0.4 |
| } |
| ] |
| }, |
| options: { |
| responsive: true, |
| plugins: { |
| legend: { |
| display: true, |
| position: 'top' |
| }, |
| tooltip: { |
| callbacks: { |
| label: function(context) { |
| return `${context.dataset.label}: ${context.raw}%`; |
| } |
| } |
| } |
| }, |
| scales: { |
| y: { |
| beginAtZero: true, |
| max: 100, |
| ticks: { |
| callback: function(value) { |
| return `${value}%`; |
| } |
| } |
| } |
| } |
| } |
| }); |
| |
| |
| const confidenceCtx = document.getElementById('confidenceChart').getContext('2d'); |
| confidenceChart = new Chart(confidenceCtx, { |
| type: 'bar', |
| data: { |
| labels: ['<60%', '60-69%', '70-79%', '80-89%', '90%+'], |
| datasets: [ |
| { |
| label: 'Confirmed', |
| data: [ |
| filteredData.filter(item => item.feedback_status === 'confirmed' && item.confidence < 60).length, |
| filteredData.filter(item => item.feedback_status === 'confirmed' && item.confidence >= 60 && item.confidence < 70).length, |
| filteredData.filter(item => item.feedback_status === 'confirmed' && item.confidence >= 70 && item.confidence < 80).length, |
| filteredData.filter(item => item.feedback_status === 'confirmed' && item.confidence >= 80 && item.confidence < 90).length, |
| filteredData.filter(item => item.feedback_status === 'confirmed' && item.confidence >= 90).length |
| ], |
| backgroundColor: '#10b981', |
| borderColor: '#10b981', |
| borderWidth: 1 |
| }, |
| { |
| label: 'Overridden', |
| data: [ |
| filteredData.filter(item => item.feedback_status === 'overridden' && item.confidence < 60).length, |
| filteredData.filter(item => item.feedback_status === 'overridden' && item.confidence >= 60 && item.confidence < 70).length, |
| filteredData.filter(item => item.feedback_status === 'overridden' && item.confidence >= 70 && item.confidence < 80).length, |
| filteredData.filter(item => item.feedback_status === 'overridden' && item.confidence >= 80 && item.confidence < 90).length, |
| filteredData.filter(item => item.feedback_status === 'overridden' && item.confidence >= 90).length |
| ], |
| backgroundColor: '#ef4444', |
| borderColor: '#ef4444', |
| borderWidth: 1 |
| } |
| ] |
| }, |
| options: { |
| responsive: true, |
| plugins: { |
| legend: { |
| display: true, |
| position: 'top' |
| } |
| }, |
| scales: { |
| x: { |
| stacked: false |
| }, |
| y: { |
| stacked: false |
| } |
| } |
| } |
| }); |
| |
| |
| const classificationCtx = document.getElementById('classificationChart').getContext('2d'); |
| classificationChart = new Chart(classificationCtx, { |
| type: 'pie', |
| data: { |
| labels: ['Unripe', 'Ripe', 'Overripe'], |
| datasets: [ |
| { |
| data: [ |
| filteredData.filter(item => item.ai_prediction === 'unripe').length, |
| filteredData.filter(item => item.ai_prediction === 'ripe').length, |
| filteredData.filter(item => item.ai_prediction === 'overripe').length |
| ], |
| backgroundColor: ['#f59e0b', '#10b981', '#ef4444'], |
| borderColor: ['#f59e0b', '#10b981', '#ef4444'], |
| borderWidth: 1 |
| } |
| ] |
| }, |
| options: { |
| responsive: true, |
| plugins: { |
| legend: { |
| display: true, |
| position: 'right' |
| } |
| } |
| } |
| }); |
| |
| |
| const mismatchCtx = document.getElementById('mismatchChart').getContext('2d'); |
| |
| |
| const mismatchByFruit = {}; |
| filteredData.filter(item => item.feedback_status === 'overridden').forEach(item => { |
| if (!mismatchByFruit[item.fruit]) { |
| mismatchByFruit[item.fruit] = 0; |
| } |
| mismatchByFruit[item.fruit]++; |
| }); |
| |
| const fruits = Object.keys(mismatchByFruit).map(fruit => fruit.charAt(0).toUpperCase() + fruit.slice(1)); |
| const mismatchCounts = Object.values(mismatchByFruit); |
| |
| mismatchChart = new Chart(mismatchCtx, { |
| type: 'bar', |
| data: { |
| labels: fruits, |
| datasets: [ |
| { |
| label: 'Mismatch Count', |
| data: mismatchCounts, |
| backgroundColor: fruits.map(fruit => getFruitColor(fruit.toLowerCase())), |
| borderColor: fruits.map(fruit => getFruitColor(fruit.toLowerCase())), |
| borderWidth: 1 |
| } |
| ] |
| }, |
| options: { |
| responsive: true, |
| plugins: { |
| legend: { |
| display: false |
| } |
| }, |
| scales: { |
| y: { |
| beginAtZero: true |
| } |
| } |
| } |
| }); |
| } |
| |
| |
| function exportData() { |
| const dataStr = JSON.stringify(filteredData, null, 2); |
| const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr); |
| |
| const exportFileDefaultName = `fruit-ai-export-${moment().format('YYYY-MM-DD')}.json`; |
| |
| const linkElement = document.createElement('a'); |
| linkElement.setAttribute('href', dataUri); |
| linkElement.setAttribute('download', exportFileDefaultName); |
| linkElement.click(); |
| } |
| |
| |
| document.querySelectorAll('.sort').forEach(header => { |
| header.addEventListener('click', function() { |
| const column = this.getAttribute('data-sort'); |
| const icon = this.querySelector('i'); |
| |
| |
| document.querySelectorAll('.sort i').forEach(otherIcon => { |
| if (otherIcon !== icon) { |
| otherIcon.classList.remove('fa-sort-up', 'fa-sort-down'); |
| otherIcon.classList.add('fa-sort'); |
| } |
| }); |
| |
| |
| if (sortColumn === column) { |
| sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; |
| } else { |
| sortColumn = column; |
| sortDirection = 'asc'; |
| } |
| |
| |
| icon.classList.remove('fa-sort'); |
| icon.classList.add(sortDirection === 'asc' ? 'fa-sort-up' : 'fa-sort-down'); |
| |
| applyFiltersFunction(); |
| }); |
| }); |
| |
| |
| document.getElementById('firstPage').addEventListener('click', () => { |
| currentPage = 1; |
| renderTable(); |
| }); |
| |
| document.getElementById('prevPage').addEventListener('click', () => { |
| if (currentPage > 1) { |
| currentPage--; |
| renderTable(); |
| } |
| }); |
| |
| document.getElementById('nextPage').addEventListener('click', () => { |
| if (currentPage < Math.ceil(filteredData.length / itemsPerPage)) { |
| currentPage++; |
| renderTable(); |
| } |
| }); |
| |
| document.getElementById('lastPage').addEventListener('click', () => { |
| currentPage = Math.ceil(filteredData.length / itemsPerPage); |
| renderTable(); |
| }); |
| |
| document.getElementById('prevPageMobile').addEventListener('click', () => { |
| if (currentPage > 1) { |
| currentPage--; |
| renderTable(); |
| } |
| }); |
| |
| document.getElementById('nextPageMobile').addEventListener('click', () => { |
| if (currentPage < Math.ceil(filteredData.length / itemsPerPage)) { |
| currentPage++; |
| renderTable(); |
| } |
| }); |
| |
| |
| applyFilters.addEventListener('click', applyFiltersFunction); |
| resetFilters.addEventListener('click', resetFiltersFunction); |
| searchInput.addEventListener('keyup', function(e) { |
| if (e.key === 'Enter') { |
| applyFiltersFunction(); |
| } |
| }); |
| exportBtn.addEventListener('click', exportData); |
| |
| |
| detailModal.addEventListener('click', function(e) { |
| if (e.target === this) { |
| detailModal.classList.add('hidden'); |
| } |
| }); |
| |
| |
| document.querySelector('#detailModal button[onclick]').addEventListener('click', function() { |
| detailModal.classList.add('hidden'); |
| }); |
| |
| |
| applyFiltersFunction(); |
| |
| |
| dateRangePicker.value = `${moment().subtract(30, 'days').format('MM/DD/YYYY')} - ${moment().format('MM/DD/YYYY')}`; |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', initDashboard); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=mrwhy06/monitor" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> |
| </html> |