// Global state let rainControls = null; // { freeze, unfreeze } exposed from initMatrixRain let appState = { categories: [], intents: [], cities: [], districts: [], streets: [], positions: [], currentView: 'dashboard', editingCategory: null, editingIntent: null, editingCity: null, editingDistrict: null, editingStreet: null, }; let deleteContext = { type: null, id: null, name: null, }; // Initialize the app document.addEventListener('DOMContentLoaded', async () => { rainControls = initMatrixRain(); initializeEventListeners(); await loadAllData(); switchView('dashboard'); // Auto-freeze 1s after page finishes loading setTimeout(() => rainControls?.freeze(), 500); }); // Initialize all event listeners function initializeEventListeners() { // Navigation document.querySelectorAll('.nav-btn').forEach(btn => { btn.addEventListener('click', () => { const view = btn.dataset.view; switchView(view); }); }); // Search document.getElementById('searchInput').addEventListener('input', () => { const searchTerm = document.getElementById('searchInput').value.toLowerCase(); applySearch(searchTerm); }); // Refresh button document.getElementById('refreshBtn').addEventListener('click', loadAllData); // Category modal document.getElementById('addCategoryBtn').addEventListener('click', showAddCategoryModal); document.getElementById('categoryForm').addEventListener('submit', handleCategorySave); // Intent modal document.getElementById('addIntentBtn').addEventListener('click', showAddIntentModal); document.getElementById('intentForm').addEventListener('submit', handleIntentSave); // Cities / Districts / Streets modals document.getElementById('addCityBtn').addEventListener('click', () => showCityModal()); document.getElementById('cityForm').addEventListener('submit', handleCitySave); document.getElementById('addDistrictBtn').addEventListener('click', () => showDistrictModal()); document.getElementById('districtForm').addEventListener('submit', handleDistrictSave); document.getElementById('addStreetBtn').addEventListener('click', () => showStreetModal()); document.getElementById('streetForm').addEventListener('submit', handleStreetSave); // Maps filters document.getElementById('districtCityFilter').addEventListener('change', () => { const cityId = parseInt(document.getElementById('districtCityFilter').value) || null; const filtered = cityId ? appState.districts.filter(d => d.city === cityId) : appState.districts; renderDistrictsTable(filtered); }); document.getElementById('streetCityFilter').addEventListener('change', async () => { const cityId = parseInt(document.getElementById('streetCityFilter').value) || null; // cascade districts filter const districtSel = document.getElementById('streetDistrictFilter'); const districtOpts = cityId ? appState.districts.filter(d => d.city === cityId) : appState.districts; districtSel.innerHTML = ''; districtOpts.forEach(d => { const o = document.createElement('option'); o.value = d.id; o.textContent = d.name; districtSel.appendChild(o); }); const filtered = cityId ? appState.streets.filter(s => s.city_id === cityId) : appState.streets; renderStreetsTable(filtered); }); document.getElementById('streetDistrictFilter').addEventListener('change', () => { const distId = parseInt(document.getElementById('streetDistrictFilter').value) || null; const filtered = distId ? appState.streets.filter(s => s.district_id === distId) : appState.streets; renderStreetsTable(filtered); }); document.getElementById('positionStreetFilter').addEventListener('change', () => { const streetId = parseInt(document.getElementById('positionStreetFilter').value) || null; const filtered = streetId ? appState.positions.filter(p => p.street_id === streetId) : appState.positions; renderPositionsTable(filtered); }); // Street city→district cascade in modal document.getElementById('streetCity').addEventListener('change', () => { const cityId = parseInt(document.getElementById('streetCity').value) || null; const distSel = document.getElementById('streetDistrict'); distSel.innerHTML = ''; const filtered = cityId ? appState.districts.filter(d => d.city === cityId) : appState.districts; filtered.forEach(d => { const o = document.createElement('option'); o.value = d.id; o.textContent = d.name; distSel.appendChild(o); }); }); // Modal close buttons document.querySelectorAll('.modal-close').forEach(btn => { btn.addEventListener('click', (e) => { const modalId = e.currentTarget.dataset.modal; UIComponents.closeModal(modalId); }); }); document.querySelectorAll('.btn-secondary[data-modal]').forEach(btn => { btn.addEventListener('click', (e) => { const modalId = e.currentTarget.dataset.modal; UIComponents.closeModal(modalId); }); }); // Filter/Sort document.getElementById('categoryFilter').addEventListener('change', applyIntentFilters); document.getElementById('priorityFilter').addEventListener('change', applyIntentFilters); // Delete confirmation document.getElementById('confirmDeleteBtn').addEventListener('click', handleConfirmDelete); // Dashboard search — Enter shows inline results, Escape collapses const dashSearch = document.getElementById('dashboardSearch'); const searchResults = document.getElementById('searchResults'); if (dashSearch && searchResults) { dashSearch.addEventListener('keydown', (e) => { if (e.key === 'Enter') { const term = dashSearch.value.trim().toLowerCase(); rainControls?.unfreeze(); renderDashboardResults(term, searchResults); } if (e.key === 'Escape') { collapseDashboardResults(searchResults); dashSearch.value = ''; } }); dashSearch.addEventListener('input', () => { if (dashSearch.value.trim() === '') { collapseDashboardResults(searchResults); } }); } // Close modals when clicking outside document.querySelectorAll('.modal').forEach(modal => { modal.addEventListener('click', (e) => { if (e.target === modal) { UIComponents.closeModal(modal.id); } }); }); // Pathfinder document.getElementById('findPathBtn').addEventListener('click', handleFindPath); document.getElementById('pathResetBtn').addEventListener('click', () => { document.getElementById('pathResult').innerHTML = ''; document.getElementById('pathStartSearch').value = ''; document.getElementById('pathEndSearch').value = ''; populatePathfinderDropdowns('', ''); }); document.getElementById('pathDownloadBtn').addEventListener('click', () => { const base = API_BASE_URL.replace('/api', ''); window.open(base + '/export/position.bin', '_blank'); }); document.getElementById('pathStartSearch').addEventListener('input', e => { filterPathDropdown('pathStart', e.target.value); }); document.getElementById('pathEndSearch').addEventListener('input', e => { filterPathDropdown('pathEnd', e.target.value); }); } // Build helper maps function buildCategoriesMap(categories) { return Object.fromEntries(categories.map(c => [c.id, c.label])); } function buildNameMap(items) { return Object.fromEntries(items.map(i => [i.id, i.name])); } // Load all data from API async function loadAllData() { try { UIComponents.updateStatus(false); await api.healthCheck(); [appState.categories, appState.intents, appState.cities] = await Promise.all([ api.getCategories(), api.getIntents(), api.getCities(), ]); appState.districts = await api.getDistricts(); appState.streets = await api.getStreets(); appState.positions = await api.getPositions(); // Intents / Categories const categoriesMap = buildCategoriesMap(appState.categories); UIComponents.renderCategoriesTable(appState.categories); UIComponents.renderIntentsTable(appState.intents, categoriesMap); UIComponents.populateCategoryDropdown(appState.categories); UIComponents.populateFilterDropdowns(appState.categories); UIComponents.updateDashboardStats(appState.categories, appState.intents); // Maps tables renderCitiesTable(appState.cities); renderDistrictsTable(appState.districts); renderStreetsTable(appState.streets); renderPositionsTable(appState.positions); // Populate maps filter dropdowns populateMapsDropdowns(); populatePathfinderDropdowns(); UIComponents.updateStatus(true); UIComponents.showToast('Data loaded successfully', 'success'); } catch (error) { console.error('Error loading data:', error); UIComponents.updateStatus(false); UIComponents.showToast(`Failed to load data: ${error.message}`, 'error'); } } // Switch view function switchView(viewName) { // Update navigation document.querySelectorAll('.nav-btn').forEach(btn => { btn.classList.remove('active'); if (btn.dataset.view === viewName) { btn.classList.add('active'); } }); // Update content document.querySelectorAll('.view').forEach(view => { view.classList.remove('active'); }); const viewElement = document.getElementById(viewName); if (viewElement) { viewElement.classList.add('active'); } // Update title const titles = { dashboard: 'Dashboard', categories: 'Categories Management', intents: 'Intents Management', cities: 'Cities Management', districts: 'Districts Management', streets: 'Streets Management', positions: 'Positions Management', pathfinder: 'A* Pathfinder', }; document.getElementById('viewTitle').textContent = titles[viewName] || 'Dashboard'; appState.currentView = viewName; } // Apply search across all views function applySearch(searchTerm) { if (appState.currentView === 'categories') { const filtered = appState.categories.filter(cat => cat.id.toString().includes(searchTerm) || cat.label.toLowerCase().includes(searchTerm) ); UIComponents.renderCategoriesTable(filtered); } else if (appState.currentView === 'intents') { const categoriesMap = buildCategoriesMap(appState.categories); const filtered = appState.intents.filter(intent => { const catLabel = (intent.category_label || categoriesMap[intent.category_id] || 'unknown').toLowerCase(); return intent.name.toLowerCase().includes(searchTerm) || catLabel.includes(searchTerm); }); UIComponents.renderIntentsTable(filtered, categoriesMap); } } // ==================== CATEGORIES ==================== function showAddCategoryModal() { appState.editingCategory = null; UIComponents.clearForm('categoryForm'); document.getElementById('categoryModalTitle').textContent = 'Add Category'; UIComponents.openModal('categoryModal'); } window.editCategory = function(category) { appState.editingCategory = category; document.getElementById('categoryLabel').value = category.label; document.getElementById('categoryModalTitle').textContent = 'Edit Category'; UIComponents.openModal('categoryModal'); }; window.deleteCategory = function(categoryId) { const category = appState.categories.find(c => c.id === categoryId); deleteContext.type = 'category'; deleteContext.id = categoryId; deleteContext.name = category?.label || categoryId; document.getElementById('deleteMessage').textContent = `Are you sure you want to delete category "${deleteContext.name}"?`; UIComponents.openModal('deleteModal'); }; async function handleCategorySave(e) { e.preventDefault(); const categoryLabel = document.getElementById('categoryLabel').value; try { if (appState.editingCategory) { // Update await api.updateCategory(appState.editingCategory.id, { label: categoryLabel, }); UIComponents.showToast('Category updated successfully', 'success'); } else { // Create await api.createCategory({ label: categoryLabel, }); UIComponents.showToast('Category created successfully', 'success'); } UIComponents.closeModal('categoryModal'); await loadAllData(); } catch (error) { UIComponents.showToast(`Error saving category: ${error.message}`, 'error'); } } // ==================== INTENTS ==================== function showAddIntentModal() { appState.editingIntent = null; UIComponents.clearForm('intentForm'); document.getElementById('intentModalTitle').textContent = 'Add Intent'; UIComponents.openModal('intentModal'); } window.editIntent = function(intent) { appState.editingIntent = intent; document.getElementById('intentName').value = intent.name; document.getElementById('intentCategory').value = intent.category_id; // Convert int priority to string label const priorityMap = { 0: 'Low', 1: 'Medium', 2: 'High', 3: 'Critical' }; const priorityLabel = typeof intent.priority === 'number' ? priorityMap[intent.priority] : intent.priority; document.getElementById('intentPriority').value = priorityLabel || 'Medium'; document.getElementById('intentModalTitle').textContent = 'Edit Intent'; UIComponents.openModal('intentModal'); }; window.deleteIntent = function(intentId) { const intent = appState.intents.find(i => i.id === intentId); deleteContext.type = 'intent'; deleteContext.id = intentId; deleteContext.name = intent?.name || intentId; document.getElementById('deleteMessage').textContent = `Are you sure you want to delete intent "${deleteContext.name}"?`; UIComponents.openModal('deleteModal'); }; async function handleIntentSave(e) { e.preventDefault(); const intentName = document.getElementById('intentName').value; const categoryId = parseInt(document.getElementById('intentCategory').value); const priorityLabel = document.getElementById('intentPriority').value; // Convert string priority label to int const priorityIntMap = { 'Low': 0, 'Medium': 1, 'High': 2, 'Critical': 3 }; const priority = priorityIntMap[priorityLabel] || 1; if (!categoryId) { UIComponents.showToast('Please select a category', 'error'); return; } try { if (appState.editingIntent) { // Update await api.updateIntent(appState.editingIntent.id, { name: intentName, category_id: categoryId, priority: priority, }); UIComponents.showToast('Intent updated successfully', 'success'); } else { // Create await api.createIntent({ name: intentName, category_id: categoryId, priority: priority, }); UIComponents.showToast('Intent created successfully', 'success'); } UIComponents.closeModal('intentModal'); await loadAllData(); } catch (error) { UIComponents.showToast(`Error saving intent: ${error.message}`, 'error'); } } // Apply filters to intents view function applyIntentFilters() { const categoryId = parseInt(document.getElementById('categoryFilter').value); const priority = document.getElementById('priorityFilter').value; let filtered = appState.intents; if (categoryId && !isNaN(categoryId)) { filtered = filtered.filter(i => i.category_id === categoryId); } if (priority) { // priority filter value is string label; compare against int stored in data const priorityIntMap = { 'Low': 0, 'Medium': 1, 'High': 2, 'Critical': 3 }; const priorityInt = priorityIntMap[priority]; filtered = filtered.filter(i => i.priority === priorityInt || i.priority === priority ); } const categoriesMap = buildCategoriesMap(appState.categories); UIComponents.renderIntentsTable(filtered, categoriesMap); } // ==================== DELETE CONFIRMATION ==================== async function handleConfirmDelete() { try { if (deleteContext.type === 'category') { await api.deleteCategory(deleteContext.id); UIComponents.showToast('Category deleted successfully', 'success'); } else if (deleteContext.type === 'intent') { await api.deleteIntent(deleteContext.id); UIComponents.showToast('Intent deleted successfully', 'success'); } else if (deleteContext.type === 'city') { await api.deleteCity(deleteContext.id); UIComponents.showToast('City deleted', 'success'); } else if (deleteContext.type === 'district') { await api.deleteDistrict(deleteContext.id); UIComponents.showToast('District deleted', 'success'); } else if (deleteContext.type === 'street') { await api.deleteStreet(deleteContext.id); UIComponents.showToast('Street deleted', 'success'); } else if (deleteContext.type === 'position') { await api.deletePosition(deleteContext.id); UIComponents.showToast('Position deleted', 'success'); } UIComponents.closeModal('deleteModal'); await loadAllData(); } catch (error) { UIComponents.showToast(`Error deleting item: ${error.message}`, 'error'); } } // ==================== MAPS: RENDER HELPERS ==================== function populateMapsDropdowns() { // districtCityFilter const dcf = document.getElementById('districtCityFilter'); const scf = document.getElementById('streetCityFilter'); const psf = document.getElementById('positionStreetFilter'); [dcf, scf].forEach(sel => { if (!sel) return; const cur = sel.value; sel.innerHTML = ''; appState.cities.forEach(c => { const o = document.createElement('option'); o.value = c.id; o.textContent = c.name; sel.appendChild(o); }); sel.value = cur; }); if (psf) { const cur = psf.value; psf.innerHTML = ''; appState.streets.forEach(s => { const o = document.createElement('option'); o.value = s.id; o.textContent = s.name; psf.appendChild(o); }); psf.value = cur; } // Modal selects ['districtCity', 'streetCity'].forEach(id => { const sel = document.getElementById(id); if (!sel) return; const cur = sel.value; sel.innerHTML = ''; appState.cities.forEach(c => { const o = document.createElement('option'); o.value = c.id; o.textContent = c.name; sel.appendChild(o); }); sel.value = cur; }); } function renderCitiesTable(cities) { const thead = document.getElementById('citiesTableHead'); const tbody = document.getElementById('citiesTableBody'); if (!thead || !tbody) return; tbody.innerHTML = ''; if (!cities || cities.length === 0) { thead.innerHTML = ''; tbody.innerHTML = '
| # | Street | Type | District | City |
|---|---|---|---|---|
| ${i + 1} | ${s.name || '—'} | ${s.type || '—'} | ${distMap[s.district_id] || s.district_id || '—'} | ${cityMap[s.city_id] || s.city_id || '—'} |