- Add LetzshopVendorCache model to store cached vendor data from Letzshop API - Create LetzshopVendorSyncService for syncing vendor directory - Add Celery task for background vendor sync - Create admin page at /admin/letzshop/vendor-directory with: - Stats dashboard (total, claimed, unclaimed vendors) - Searchable/filterable vendor list - "Sync Now" button to trigger sync - Ability to create platform vendors from Letzshop cache - Add API endpoints for vendor directory management - Add Pydantic schemas for API responses Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
205 lines
6.5 KiB
JavaScript
205 lines
6.5 KiB
JavaScript
// static/admin/js/letzshop-vendor-directory.js
|
|
/**
|
|
* Admin Letzshop Vendor Directory page logic
|
|
* Browse and import vendors from Letzshop marketplace
|
|
*/
|
|
|
|
const letzshopVendorDirectoryLog = window.LogConfig.loggers.letzshopVendorDirectory ||
|
|
window.LogConfig.createLogger('letzshopVendorDirectory', false);
|
|
|
|
letzshopVendorDirectoryLog.info('Loading...');
|
|
|
|
function letzshopVendorDirectory() {
|
|
letzshopVendorDirectoryLog.info('letzshopVendorDirectory() called');
|
|
|
|
return {
|
|
// Inherit base layout state
|
|
...data(),
|
|
|
|
// Set page identifier for sidebar highlighting
|
|
currentPage: 'letzshop-vendor-directory',
|
|
|
|
// Data
|
|
vendors: [],
|
|
stats: {},
|
|
companies: [],
|
|
total: 0,
|
|
page: 1,
|
|
limit: 20,
|
|
hasMore: false,
|
|
|
|
// State
|
|
loading: true,
|
|
syncing: false,
|
|
creating: false,
|
|
error: '',
|
|
successMessage: '',
|
|
|
|
// Filters
|
|
filters: {
|
|
search: '',
|
|
city: '',
|
|
category: '',
|
|
only_unclaimed: false,
|
|
},
|
|
|
|
// Modals
|
|
showDetailModal: false,
|
|
showCreateModal: false,
|
|
selectedVendor: null,
|
|
createVendorData: {
|
|
slug: '',
|
|
name: '',
|
|
company_id: '',
|
|
},
|
|
createError: '',
|
|
|
|
// Init
|
|
async init() {
|
|
// Guard against multiple initialization
|
|
if (window._letzshopVendorDirectoryInitialized) return;
|
|
window._letzshopVendorDirectoryInitialized = true;
|
|
|
|
letzshopVendorDirectoryLog.info('init() called');
|
|
await Promise.all([
|
|
this.loadStats(),
|
|
this.loadVendors(),
|
|
this.loadCompanies(),
|
|
]);
|
|
},
|
|
|
|
// API calls
|
|
async loadStats() {
|
|
try {
|
|
const data = await apiClient.get('/admin/letzshop/vendor-directory/stats');
|
|
if (data.success) {
|
|
this.stats = data.stats;
|
|
}
|
|
} catch (e) {
|
|
letzshopVendorDirectoryLog.error('Failed to load stats:', e);
|
|
}
|
|
},
|
|
|
|
async loadVendors() {
|
|
this.loading = true;
|
|
this.error = '';
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
page: this.page,
|
|
limit: this.limit,
|
|
});
|
|
|
|
if (this.filters.search) params.append('search', this.filters.search);
|
|
if (this.filters.city) params.append('city', this.filters.city);
|
|
if (this.filters.category) params.append('category', this.filters.category);
|
|
if (this.filters.only_unclaimed) params.append('only_unclaimed', 'true');
|
|
|
|
const data = await apiClient.get(`/admin/letzshop/vendor-directory/vendors?${params}`);
|
|
|
|
if (data.success) {
|
|
this.vendors = data.vendors;
|
|
this.total = data.total;
|
|
this.hasMore = data.has_more;
|
|
} else {
|
|
this.error = data.detail || 'Failed to load vendors';
|
|
}
|
|
} catch (e) {
|
|
this.error = 'Failed to load vendors';
|
|
letzshopVendorDirectoryLog.error('Failed to load vendors:', e);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async loadCompanies() {
|
|
try {
|
|
const data = await apiClient.get('/admin/companies?limit=100');
|
|
if (data.companies) {
|
|
this.companies = data.companies;
|
|
}
|
|
} catch (e) {
|
|
letzshopVendorDirectoryLog.error('Failed to load companies:', e);
|
|
}
|
|
},
|
|
|
|
async triggerSync() {
|
|
this.syncing = true;
|
|
this.error = '';
|
|
this.successMessage = '';
|
|
|
|
try {
|
|
const data = await apiClient.post('/admin/letzshop/vendor-directory/sync');
|
|
|
|
if (data.success) {
|
|
this.successMessage = data.message + (data.mode === 'celery' ? ` (Task ID: ${data.task_id})` : '');
|
|
// Reload data after a delay to allow sync to complete
|
|
setTimeout(() => {
|
|
this.loadStats();
|
|
this.loadVendors();
|
|
}, 3000);
|
|
} else {
|
|
this.error = data.detail || 'Failed to trigger sync';
|
|
}
|
|
} catch (e) {
|
|
this.error = 'Failed to trigger sync';
|
|
letzshopVendorDirectoryLog.error('Failed to trigger sync:', e);
|
|
} finally {
|
|
this.syncing = false;
|
|
}
|
|
},
|
|
|
|
async createVendor() {
|
|
if (!this.createVendorData.company_id || !this.createVendorData.slug) return;
|
|
|
|
this.creating = true;
|
|
this.createError = '';
|
|
|
|
try {
|
|
const data = await apiClient.post(
|
|
`/admin/letzshop/vendor-directory/vendors/${this.createVendorData.slug}/create-vendor?company_id=${this.createVendorData.company_id}`
|
|
);
|
|
|
|
if (data.success) {
|
|
this.showCreateModal = false;
|
|
this.successMessage = data.message;
|
|
this.loadVendors();
|
|
this.loadStats();
|
|
} else {
|
|
this.createError = data.detail || 'Failed to create vendor';
|
|
}
|
|
} catch (e) {
|
|
this.createError = 'Failed to create vendor';
|
|
letzshopVendorDirectoryLog.error('Failed to create vendor:', e);
|
|
} finally {
|
|
this.creating = false;
|
|
}
|
|
},
|
|
|
|
// Modal handlers
|
|
showVendorDetail(vendor) {
|
|
this.selectedVendor = vendor;
|
|
this.showDetailModal = true;
|
|
},
|
|
|
|
openCreateVendorModal(vendor) {
|
|
this.createVendorData = {
|
|
slug: vendor.slug,
|
|
name: vendor.name,
|
|
company_id: '',
|
|
};
|
|
this.createError = '';
|
|
this.showCreateModal = true;
|
|
},
|
|
|
|
// Utilities
|
|
formatDate(dateStr) {
|
|
if (!dateStr) return '-';
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
},
|
|
};
|
|
}
|
|
|
|
letzshopVendorDirectoryLog.info('Loaded');
|