feat(admin): separate platform CRUD from CMS, add entity selector macro
Some checks failed
Some checks failed
- Move platforms menu from CMS to Platform Admin section with create/edit - Add platform create page, API endpoint, and service method - Remove CMS-specific content from platform list and detail pages - Create shared entity_selector + entity_selected_badge Jinja macros - Create entity-selector.js generalizing store-selector.js for any entity - Add Tom Select merchant filter to stores page with localStorage persistence - Migrate store-products page to use shared macros (remove 53 lines of duped CSS) - Fix broken icons: puzzle→puzzle-piece, building-storefront→store, language→translate, server→cube Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
134
app/modules/tenancy/static/admin/js/platform-create.js
Normal file
134
app/modules/tenancy/static/admin/js/platform-create.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Platform Create - Alpine.js Component
|
||||
*
|
||||
* Handles platform creation for multi-platform CMS.
|
||||
*/
|
||||
|
||||
const platformCreateLog = window.LogConfig.createLogger('PLATFORM_CREATE');
|
||||
|
||||
function platformCreate() {
|
||||
return {
|
||||
// Inherit base layout functionality from init-alpine.js
|
||||
...data(),
|
||||
|
||||
// Page identification
|
||||
currentPage: 'platform-create',
|
||||
|
||||
// State
|
||||
saving: false,
|
||||
error: null,
|
||||
|
||||
// Language editing
|
||||
currentLang: 'fr',
|
||||
languageNames: {
|
||||
fr: 'Fran\u00e7ais',
|
||||
de: 'Deutsch',
|
||||
en: 'English',
|
||||
lb: 'L\u00ebtzebuergesch',
|
||||
},
|
||||
|
||||
// Form data
|
||||
formData: {
|
||||
code: '',
|
||||
name: '',
|
||||
description_translations: { fr: '', de: '', en: '', lb: '' },
|
||||
domain: '',
|
||||
path_prefix: '',
|
||||
default_language: 'fr',
|
||||
supported_languages: ['fr', 'de', 'en', 'lb'],
|
||||
is_active: true,
|
||||
is_public: true,
|
||||
},
|
||||
|
||||
errors: {},
|
||||
|
||||
// Available languages
|
||||
availableLanguages: [
|
||||
{ code: 'fr', name: 'French' },
|
||||
{ code: 'de', name: 'German' },
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'lb', name: 'Luxembourgish' },
|
||||
],
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
platformCreateLog.info('=== PLATFORM CREATE PAGE INITIALIZING ===');
|
||||
|
||||
// Duplicate initialization guard
|
||||
if (window._platformCreateInitialized) {
|
||||
platformCreateLog.warn('Platform create page already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._platformCreateInitialized = true;
|
||||
|
||||
platformCreateLog.info('=== PLATFORM CREATE PAGE INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
async handleSubmit() {
|
||||
this.saving = true;
|
||||
this.error = null;
|
||||
this.errors = {};
|
||||
|
||||
try {
|
||||
// Sync base description from default language translation
|
||||
const defaultLang = this.formData.default_language || 'fr';
|
||||
const baseDesc = this.formData.description_translations[defaultLang] || '';
|
||||
|
||||
const payload = {
|
||||
code: this.formData.code,
|
||||
name: this.formData.name,
|
||||
description: baseDesc || null,
|
||||
description_translations: this.formData.description_translations,
|
||||
domain: this.formData.domain || null,
|
||||
path_prefix: this.formData.path_prefix || null,
|
||||
default_language: this.formData.default_language,
|
||||
supported_languages: this.formData.supported_languages,
|
||||
is_active: this.formData.is_active,
|
||||
is_public: this.formData.is_public,
|
||||
};
|
||||
|
||||
const response = await apiClient.post('/admin/platforms', payload);
|
||||
|
||||
platformCreateLog.info(`Created platform: ${response.code}`);
|
||||
|
||||
// Redirect to the new platform's detail page
|
||||
window.location.href = `/admin/platforms/${response.code}`;
|
||||
} catch (err) {
|
||||
platformCreateLog.error('Error creating platform:', err);
|
||||
this.error = err.message || 'Failed to create platform';
|
||||
|
||||
// Handle validation errors
|
||||
if (err.details) {
|
||||
this.errors = err.details;
|
||||
}
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Helper Methods
|
||||
isLanguageSupported(code) {
|
||||
return this.formData.supported_languages.includes(code);
|
||||
},
|
||||
|
||||
toggleLanguage(code) {
|
||||
const index = this.formData.supported_languages.indexOf(code);
|
||||
if (index > -1) {
|
||||
// Don't allow removing the last language
|
||||
if (this.formData.supported_languages.length > 1) {
|
||||
this.formData.supported_languages.splice(index, 1);
|
||||
// Switch tab if the removed language was active
|
||||
if (this.currentLang === code) {
|
||||
this.currentLang = this.formData.supported_languages[0];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.formData.supported_languages.push(code);
|
||||
// Initialize empty translation for new language
|
||||
if (!this.formData.description_translations[code]) {
|
||||
this.formData.description_translations[code] = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -17,7 +17,6 @@ function platformDetail() {
|
||||
// State
|
||||
platform: null,
|
||||
stats: null,
|
||||
recentPages: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
platformCode: null,
|
||||
@@ -41,10 +40,7 @@ function platformDetail() {
|
||||
if (match) {
|
||||
this.platformCode = match[1];
|
||||
platformDetailLog.info('Viewing platform:', this.platformCode);
|
||||
await Promise.all([
|
||||
this.loadPlatform(),
|
||||
this.loadRecentPages(),
|
||||
]);
|
||||
await this.loadPlatform();
|
||||
} else {
|
||||
platformDetailLog.error('No platform code in URL');
|
||||
this.error = 'Platform code not found in URL';
|
||||
@@ -74,19 +70,6 @@ function platformDetail() {
|
||||
}
|
||||
},
|
||||
|
||||
async loadRecentPages() {
|
||||
try {
|
||||
// Load recent content pages for this platform
|
||||
const response = await apiClient.get(`/admin/content-pages?platform_code=${this.platformCode}&limit=5`);
|
||||
this.recentPages = response.items || response || [];
|
||||
platformDetailLog.info(`Loaded ${this.recentPages.length} recent pages`);
|
||||
} catch (err) {
|
||||
platformDetailLog.error('Error loading recent pages:', err);
|
||||
// Non-fatal - don't throw
|
||||
this.recentPages = [];
|
||||
}
|
||||
},
|
||||
|
||||
// Helper Methods
|
||||
getPlatformIcon(code) {
|
||||
const icons = {
|
||||
@@ -98,22 +81,6 @@ function platformDetail() {
|
||||
return icons[code] || 'globe-alt';
|
||||
},
|
||||
|
||||
getPageTypeLabel(page) {
|
||||
if (page.is_platform_page) return 'Marketing';
|
||||
if (page.store_id) return 'Store Override';
|
||||
return 'Store Default';
|
||||
},
|
||||
|
||||
getPageTypeBadgeClass(page) {
|
||||
if (page.is_platform_page) {
|
||||
return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200';
|
||||
}
|
||||
if (page.store_id) {
|
||||
return 'bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200';
|
||||
}
|
||||
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '—';
|
||||
const date = new Date(dateString);
|
||||
|
||||
@@ -28,11 +28,16 @@ function adminStores() {
|
||||
showDeleteStoreModal: false,
|
||||
storeToDelete: null,
|
||||
|
||||
// Merchant filter (Tom Select)
|
||||
selectedMerchant: null,
|
||||
merchantSelectInstance: null,
|
||||
|
||||
// Search and filters
|
||||
filters: {
|
||||
search: '',
|
||||
is_active: '',
|
||||
is_verified: ''
|
||||
is_verified: '',
|
||||
merchant_id: ''
|
||||
},
|
||||
|
||||
// Pagination state (server-side)
|
||||
@@ -62,9 +67,16 @@ function adminStores() {
|
||||
this.pagination.per_page = await window.PlatformSettings.getRowsPerPage();
|
||||
}
|
||||
|
||||
// Initialize merchant selector (Tom Select)
|
||||
this.$nextTick(() => {
|
||||
this.initMerchantSelect();
|
||||
});
|
||||
|
||||
storesLog.group('Loading stores data');
|
||||
await this.loadStores();
|
||||
await this.loadStats();
|
||||
await Promise.all([
|
||||
this.loadStores(),
|
||||
this.loadStats(),
|
||||
]);
|
||||
storesLog.groupEnd();
|
||||
|
||||
storesLog.info('=== STORES PAGE INITIALIZATION COMPLETE ===');
|
||||
@@ -163,6 +175,9 @@ function adminStores() {
|
||||
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');
|
||||
@@ -230,6 +245,64 @@ function adminStores() {
|
||||
}
|
||||
},
|
||||
|
||||
// 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) {
|
||||
|
||||
Reference in New Issue
Block a user