// noqa: js-006 - async init pattern is safe, loadData has try/catch // static/admin/js/stores.js // ✅ Use centralized logger - ONE LINE! const storesLog = window.LogConfig.loggers.stores; // ============================================ // STORE LIST FUNCTION // ============================================ function adminStores() { return { // Inherit base layout functionality from init-alpine.js ...data(), // ✅ CRITICAL: Page identifier for sidebar active state currentPage: 'stores', // Stores page specific state stores: [], stats: { total: 0, verified: 0, pending: 0, inactive: 0 }, loading: false, error: null, showDeleteStoreModal: false, storeToDelete: null, // Merchant filter (Tom Select) selectedMerchant: null, merchantSelectInstance: null, // Search and filters filters: { search: '', is_active: '', is_verified: '', merchant_id: '' }, // Pagination state (server-side) pagination: { page: 1, per_page: 20, total: 0, pages: 0 }, // Initialize async init() { // Load i18n translations await I18n.loadModule('tenancy'); storesLog.info('=== STORES PAGE INITIALIZING ==='); // Prevent multiple initializations if (window._storesInitialized) { storesLog.warn('Stores page already initialized, skipping...'); return; } window._storesInitialized = true; // Load platform settings for rows per page if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); } // Initialize merchant selector (Tom Select) this.$nextTick(() => { this.initMerchantSelect(); }); storesLog.group('Loading stores data'); await Promise.all([ this.loadStores(), this.loadStats(), ]); storesLog.groupEnd(); storesLog.info('=== STORES PAGE INITIALIZATION COMPLETE ==='); }, // Debounced search debouncedSearch() { if (this._searchTimeout) { clearTimeout(this._searchTimeout); } this._searchTimeout = setTimeout(() => { storesLog.info('Search triggered:', this.filters.search); this.pagination.page = 1; this.loadStores(); }, 300); }, // Computed: Get stores for current page (already paginated from server) get paginatedStores() { return this.stores; }, // Computed: Total number of pages get totalPages() { return this.pagination.pages; }, // Computed: Start index for pagination display get startIndex() { if (this.pagination.total === 0) return 0; return (this.pagination.page - 1) * this.pagination.per_page + 1; }, // Computed: End index for pagination display get endIndex() { const end = this.pagination.page * this.pagination.per_page; return end > this.pagination.total ? this.pagination.total : end; }, // Computed: Generate page numbers array with ellipsis get pageNumbers() { const pages = []; const totalPages = this.totalPages; const current = this.pagination.page; if (totalPages <= 7) { // Show all pages if 7 or fewer for (let i = 1; i <= totalPages; i++) { pages.push(i); } } else { // Always show first page pages.push(1); if (current > 3) { pages.push('...'); } // Show pages around current page const start = Math.max(2, current - 1); const end = Math.min(totalPages - 1, current + 1); for (let i = start; i <= end; i++) { pages.push(i); } if (current < totalPages - 2) { pages.push('...'); } // Always show last page pages.push(totalPages); } return pages; }, // Load stores list with search and pagination async loadStores() { storesLog.info('Loading stores list...'); this.loading = true; this.error = null; try { // Build query parameters const params = new URLSearchParams(); params.append('skip', (this.pagination.page - 1) * this.pagination.per_page); params.append('limit', this.pagination.per_page); if (this.filters.search) { params.append('search', this.filters.search); } if (this.filters.is_active !== '') { params.append('is_active', this.filters.is_active); } if (this.filters.is_verified !== '') { params.append('is_verified', this.filters.is_verified); } if (this.filters.merchant_id !== '') { params.append('merchant_id', this.filters.merchant_id); } const url = `/admin/stores?${params}`; window.LogConfig.logApiCall('GET', url, null, 'request'); const startTime = performance.now(); const response = await apiClient.get(url); const duration = performance.now() - startTime; window.LogConfig.logApiCall('GET', url, response, 'response'); window.LogConfig.logPerformance('Load Stores', duration); // Handle response with pagination info if (response.stores) { this.stores = response.stores; this.pagination.total = response.total; this.pagination.pages = Math.ceil(response.total / this.pagination.per_page); storesLog.info(`Loaded ${this.stores.length} stores (total: ${response.total})`); } else { // Fallback for different response structures this.stores = response.items || response || []; this.pagination.total = this.stores.length; this.pagination.pages = Math.ceil(this.stores.length / this.pagination.per_page); storesLog.info(`Stores loaded in ${duration}ms`, { count: this.stores.length, hasStores: this.stores.length > 0 }); } if (this.stores.length > 0) { storesLog.debug('First store:', this.stores[0]); } } catch (error) { window.LogConfig.logError(error, 'Load Stores'); this.error = error.message || 'Failed to load stores'; Utils.showToast(I18n.t('tenancy.messages.failed_to_load_stores'), 'error'); } finally { this.loading = false; } }, // Load statistics async loadStats() { storesLog.info('Loading store statistics...'); try { const url = '/admin/stores/stats'; window.LogConfig.logApiCall('GET', url, null, 'request'); const startTime = performance.now(); const response = await apiClient.get(url); const duration = performance.now() - startTime; window.LogConfig.logApiCall('GET', url, response, 'response'); window.LogConfig.logPerformance('Load Store Stats', duration); this.stats = response; storesLog.info(`Stats loaded in ${duration}ms`, this.stats); } catch (error) { window.LogConfig.logError(error, 'Load Store Stats'); // Don't show error toast for stats, just log it } }, // Initialize merchant selector (Tom Select autocomplete) initMerchantSelect() { if (!window.initEntitySelector) { storesLog.warn('initEntitySelector not available yet, retrying...'); setTimeout(() => this.initMerchantSelect(), 200); return; } this.merchantSelectInstance = initEntitySelector(this.$refs.merchantSelect, { apiEndpoint: '/admin/merchants', responseKey: 'merchants', searchFields: ['name'], codeField: null, placeholder: 'Filter by merchant...', noResultsText: 'No merchants found', onSelect: (merchant) => { storesLog.info('Merchant selected:', merchant); this.selectedMerchant = merchant; this.filters.merchant_id = merchant.id; this.pagination.page = 1; localStorage.setItem('stores_selected_merchant_id', merchant.id); localStorage.setItem('stores_selected_merchant_data', JSON.stringify(merchant)); this.loadStores(); }, onClear: () => { this.clearMerchantFilter(); } }); // Restore from localStorage const savedMerchantId = localStorage.getItem('stores_selected_merchant_id'); if (savedMerchantId) { const savedData = JSON.parse(localStorage.getItem('stores_selected_merchant_data') || 'null'); if (savedData) { this.selectedMerchant = savedData; this.filters.merchant_id = parseInt(savedMerchantId); // Wait for Tom Select to init, then set value setTimeout(() => { this.merchantSelectInstance?.setValue(parseInt(savedMerchantId), savedData); }, 500); } } }, // Clear merchant filter clearMerchantFilter() { storesLog.info('Clearing merchant filter'); this.selectedMerchant = null; this.filters.merchant_id = ''; this.pagination.page = 1; localStorage.removeItem('stores_selected_merchant_id'); localStorage.removeItem('stores_selected_merchant_data'); if (this.merchantSelectInstance) { this.merchantSelectInstance.clear(); } this.loadStores(); }, // Pagination: Go to specific page goToPage(pageNum) { if (pageNum === '...' || pageNum < 1 || pageNum > this.totalPages) { return; } storesLog.info('Going to page:', pageNum); this.pagination.page = pageNum; this.loadStores(); }, // Pagination: Go to next page nextPage() { if (this.pagination.page < this.totalPages) { storesLog.info('Going to next page'); this.pagination.page++; this.loadStores(); } }, // Pagination: Go to previous page previousPage() { if (this.pagination.page > 1) { storesLog.info('Going to previous page'); this.pagination.page--; this.loadStores(); } }, // Format date (matches dashboard pattern) formatDate(dateString) { if (!dateString) { storesLog.debug('formatDate called with empty dateString'); return '-'; } const formatted = Utils.formatDate(dateString); storesLog.debug(`Date formatted: ${dateString} -> ${formatted}`); return formatted; }, // View store details viewStore(storeCode) { storesLog.info('Navigating to store details:', storeCode); const url = `/admin/stores/${storeCode}`; storesLog.debug('Navigation URL:', url); window.location.href = url; }, // Edit store editStore(storeCode) { storesLog.info('Navigating to store edit:', storeCode); const url = `/admin/stores/${storeCode}/edit`; storesLog.debug('Navigation URL:', url); window.location.href = url; }, // Prompt delete store promptDeleteStore(store) { storesLog.info('Delete store requested:', store.store_code); this.storeToDelete = store; this.showDeleteStoreModal = true; }, // Delete store async deleteStore(store) { try { const url = `/admin/stores/${store.store_code}`; window.LogConfig.logApiCall('DELETE', url, null, 'request'); storesLog.info('Deleting store:', store.store_code); await apiClient.delete(url); window.LogConfig.logApiCall('DELETE', url, null, 'response'); Utils.showToast(I18n.t('tenancy.messages.store_deleted_successfully'), 'success'); storesLog.info('Store deleted successfully'); // Reload data await this.loadStores(); await this.loadStats(); } catch (error) { window.LogConfig.logError(error, 'Delete Store'); Utils.showToast(error.message || 'Failed to delete store', 'error'); } }, // Refresh stores list async refresh() { storesLog.info('=== STORES REFRESH TRIGGERED ==='); storesLog.group('Refreshing stores data'); await this.loadStores(); await this.loadStats(); storesLog.groupEnd(); Utils.showToast(I18n.t('tenancy.messages.stores_list_refreshed'), 'success'); storesLog.info('=== STORES REFRESH COMPLETE ==='); } }; } storesLog.info('Stores module loaded');