refactor(cms): migrate store theme UI from tenancy to CMS module

Move store theme admin pages, templates, and JS from tenancy module
to CMS module where the data layer (model, service, API, schemas)
already lives. Eliminates split ownership.

Moved:
- Route handlers: GET /store-themes, GET /stores/{code}/theme
- Templates: store-theme.html, store-themes.html
- JS: store-theme.js, store-themes.js
- Updated static references: tenancy_static → cms_static

Deleted old tenancy files (no remaining references).
Menu item in CMS definition already pointed to correct route.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 10:30:09 +02:00
parent 21e4ac5124
commit 51a2114e02
7 changed files with 52 additions and 50 deletions

View File

@@ -0,0 +1,332 @@
// static/admin/js/store-theme.js (FIXED VERSION)
/**
* Store Theme Editor - Alpine.js Component
* Manages theme customization for store shops
*
* REQUIRES: log-config.js to be loaded first
*/
// ============================================================================
// LOGGING CONFIGURATION (using centralized logger)
// ============================================================================
// Use the pre-configured theme logger from centralized log-config.js
const themeLog = window.LogConfig.loggers.storeTheme;
// ============================================================================
// ALPINE.JS COMPONENT
// ============================================================================
function adminStoreTheme() {
return {
// ✅ CRITICAL: Inherit base layout functionality
...data(),
// ✅ CRITICAL: Set page identifier
currentPage: 'store-theme',
// Page state
storeCode: null,
store: null,
loading: true,
saving: false,
error: null,
showResetThemeModal: false,
// Theme data structure matching StoreTheme model
themeData: {
theme_name: 'default',
colors: {
primary: '#6366f1',
secondary: '#8b5cf6',
accent: '#ec4899',
background: '#ffffff',
text: '#1f2937',
border: '#e5e7eb'
},
fonts: {
heading: 'Inter, sans-serif',
body: 'Inter, sans-serif',
size_base: '16px',
size_heading: '2rem'
},
layout: {
style: 'grid',
header_position: 'fixed',
product_card_style: 'card',
sidebar_position: 'left'
},
branding: {
logo_url: '',
favicon_url: '',
banner_url: ''
},
custom_css: '',
social_links: {
facebook: '',
instagram: '',
twitter: '',
linkedin: ''
}
},
// Available presets
presets: [],
selectedPreset: null,
// ====================================================================
// INITIALIZATION
// ====================================================================
async init() {
// Load i18n translations
await I18n.loadModule('tenancy');
// Guard against multiple initialization
if (window._adminStoreThemeInitialized) return;
window._adminStoreThemeInitialized = true;
themeLog.info('Initializing store theme editor');
// Start performance timer
const startTime = performance.now();
try {
// Extract store code from URL
const urlParts = window.location.pathname.split('/');
this.storeCode = urlParts[urlParts.indexOf('stores') + 1];
themeLog.debug('Store code from URL:', this.storeCode);
if (!this.storeCode) {
throw new Error('Store code not found in URL');
}
// Load data in parallel
themeLog.group('Loading theme data');
await Promise.all([
this.loadStore(),
this.loadTheme(),
this.loadPresets()
]);
themeLog.groupEnd();
// Log performance
const duration = performance.now() - startTime;
window.LogConfig.logPerformance('Theme Editor Init', duration);
themeLog.info('Theme editor initialized successfully');
} catch (error) {
// Use centralized error logger
window.LogConfig.logError(error, 'Theme Editor Init');
this.error = error.message || 'Failed to initialize theme editor';
Utils.showToast(this.error, 'error');
} finally {
this.loading = false;
}
},
// ====================================================================
// DATA LOADING
// ====================================================================
async loadStore() {
themeLog.info('Loading store data');
const url = `/admin/stores/${this.storeCode}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
try {
// ✅ FIX: apiClient returns data directly, not response.data
const response = await apiClient.get(url);
// ✅ Direct assignment - response IS the data
this.store = response;
window.LogConfig.logApiCall('GET', url, this.store, 'response');
themeLog.debug('Store loaded:', this.store);
} catch (error) {
themeLog.error('Failed to load store:', error);
throw error;
}
},
async loadTheme() {
themeLog.info('Loading theme data');
const url = `/admin/store-themes/${this.storeCode}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
try {
// ✅ FIX: apiClient returns data directly
const response = await apiClient.get(url);
// Merge with default theme data
this.themeData = {
...this.themeData,
...response
};
window.LogConfig.logApiCall('GET', url, this.themeData, 'response');
themeLog.debug('Theme loaded:', this.themeData);
} catch (error) {
themeLog.warn('Failed to load theme, using defaults:', error);
// Continue with default theme
}
},
async loadPresets() {
themeLog.info('Loading theme presets');
const url = '/admin/store-themes/presets';
window.LogConfig.logApiCall('GET', url, null, 'request');
try {
// ✅ FIX: apiClient returns data directly
const response = await apiClient.get(url);
// ✅ Access presets directly from response, not response.data.presets
this.presets = response.presets || [];
window.LogConfig.logApiCall('GET', url, response, 'response');
themeLog.debug(`Loaded ${this.presets.length} presets`);
} catch (error) {
themeLog.error('Failed to load presets:', error);
this.presets = [];
}
},
// ====================================================================
// THEME OPERATIONS
// ====================================================================
async saveTheme() {
if (this.saving) return;
themeLog.info('Saving theme changes');
this.saving = true;
this.error = null;
const startTime = performance.now();
try {
const url = `/admin/store-themes/${this.storeCode}`;
window.LogConfig.logApiCall('PUT', url, this.themeData, 'request');
// ✅ FIX: apiClient returns data directly
const response = await apiClient.put(url, this.themeData);
window.LogConfig.logApiCall('PUT', url, response, 'response');
const duration = performance.now() - startTime;
window.LogConfig.logPerformance('Save Theme', duration);
themeLog.info('Theme saved successfully');
Utils.showToast(I18n.t('tenancy.messages.theme_saved_successfully'), 'success');
} catch (error) {
window.LogConfig.logError(error, 'Save Theme');
this.error = 'Failed to save theme';
Utils.showToast(this.error, 'error');
} finally {
this.saving = false;
}
},
async applyPreset(presetName) {
themeLog.info(`Applying preset: ${presetName}`);
this.saving = true;
try {
const url = `/admin/store-themes/${this.storeCode}/preset/${presetName}`;
window.LogConfig.logApiCall('POST', url, null, 'request');
// ✅ FIX: apiClient returns data directly
const response = await apiClient.post(url);
window.LogConfig.logApiCall('POST', url, response, 'response');
// ✅ FIX: Access theme directly from response, not response.data.theme
if (response && response.theme) {
this.themeData = {
...this.themeData,
...response.theme
};
}
themeLog.info(`Preset '${presetName}' applied successfully`);
Utils.showToast(`Applied ${presetName} preset`, 'success');
} catch (error) {
window.LogConfig.logError(error, 'Apply Preset');
Utils.showToast(I18n.t('tenancy.messages.failed_to_apply_preset'), 'error');
} finally {
this.saving = false;
}
},
async resetTheme() {
themeLog.warn('Resetting theme to default');
this.saving = true;
try {
const url = `/admin/store-themes/${this.storeCode}`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, null, 'response');
// Reload theme data
await this.loadTheme();
themeLog.info('Theme reset successfully');
Utils.showToast(I18n.t('tenancy.messages.theme_reset_to_default'), 'success');
} catch (error) {
window.LogConfig.logError(error, 'Reset Theme');
Utils.showToast(I18n.t('tenancy.messages.failed_to_reset_theme'), 'error');
} finally {
this.saving = false;
}
},
// ====================================================================
// UTILITY METHODS
// ====================================================================
previewTheme() {
themeLog.debug('Opening theme preview');
const previewUrl = `/store/${this.store?.subdomain || this.storeCode}`;
window.open(previewUrl, '_blank');
},
updateColor(key, value) {
themeLog.debug(`Color updated: ${key} = ${value}`);
this.themeData.colors[key] = value;
},
updateFont(type, value) {
themeLog.debug(`Font updated: ${type} = ${value}`);
this.themeData.fonts[type] = value;
},
updateLayout(key, value) {
themeLog.debug(`Layout updated: ${key} = ${value}`);
this.themeData.layout[key] = value;
}
};
}
// ============================================================================
// MODULE LOADED
// ============================================================================
themeLog.info('Store theme editor module loaded');

View File

@@ -0,0 +1,171 @@
// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/store-themes.js
/**
* Admin store themes selection page
*/
// ✅ Use centralized logger
const storeThemesLog = window.LogConfig.loggers.storeTheme;
storeThemesLog.info('Loading...');
function adminStoreThemes() {
storeThemesLog.debug('adminStoreThemes() called');
return {
// Inherit base layout state
...data(),
// Set page identifier
currentPage: 'store-theme',
// State
loading: false,
error: '',
stores: [],
selectedStoreCode: '',
// Selected store for filter (Tom Select)
selectedStore: null,
storeSelector: null,
// Search/filter
searchQuery: '',
async init() {
// Guard against multiple initialization
if (window._adminStoreThemesInitialized) {
return;
}
window._adminStoreThemesInitialized = true;
storeThemesLog.info('Store Themes init() called');
// Initialize store selector (Tom Select)
this.$nextTick(() => {
this.initStoreSelector();
});
// Check localStorage for saved store
const savedStoreId = localStorage.getItem('store_themes_selected_store_id');
if (savedStoreId) {
storeThemesLog.info('Restoring saved store:', savedStoreId);
await this.loadStores();
// Restore store after stores are loaded
setTimeout(async () => {
await this.restoreSavedStore(parseInt(savedStoreId));
}, 200);
} else {
await this.loadStores();
}
storeThemesLog.info('Store Themes initialization complete');
},
/**
* Restore saved store from localStorage
*/
async restoreSavedStore(storeId) {
try {
const store = await apiClient.get(`/admin/stores/${storeId}`);
if (this.storeSelector && store) {
// Use the store selector's setValue method
this.storeSelector.setValue(store.id, store);
// Set the filter state
this.selectedStore = store;
storeThemesLog.info('Restored store:', store.name);
}
} catch (error) {
storeThemesLog.warn('Failed to restore saved store, clearing localStorage:', error);
localStorage.removeItem('store_themes_selected_store_id');
}
},
/**
* Initialize store selector with Tom Select
*/
initStoreSelector() {
if (!this.$refs.storeSelect) {
storeThemesLog.warn('Store select element not found');
return;
}
this.storeSelector = initStoreSelector(this.$refs.storeSelect, {
placeholder: 'Search store...',
onSelect: (store) => {
storeThemesLog.info('Store selected:', store);
this.selectedStore = store;
// Save to localStorage
localStorage.setItem('store_themes_selected_store_id', store.id.toString());
},
onClear: () => {
storeThemesLog.info('Store filter cleared');
this.selectedStore = null;
// Clear from localStorage
localStorage.removeItem('store_themes_selected_store_id');
}
});
},
/**
* Clear store filter
*/
clearStoreFilter() {
if (this.storeSelector) {
this.storeSelector.clear();
}
this.selectedStore = null;
// Clear from localStorage
localStorage.removeItem('store_themes_selected_store_id');
},
async loadStores() {
this.loading = true;
this.error = '';
try {
const response = await apiClient.get('/admin/stores?limit=1000');
this.stores = response.stores || [];
storeThemesLog.debug('Loaded stores:', this.stores.length);
} catch (error) {
storeThemesLog.error('Failed to load stores:', error);
this.error = error.message || 'Failed to load stores';
} finally {
this.loading = false;
}
},
/**
* Computed: Filtered stores based on search and selected store
*/
get filteredStores() {
let filtered = this.stores;
// If a store is selected via Tom Select, show only that store
if (this.selectedStore) {
filtered = this.stores.filter(v => v.id === this.selectedStore.id);
}
// Otherwise filter by search query
else if (this.searchQuery) {
const query = this.searchQuery.toLowerCase();
filtered = this.stores.filter(v =>
v.name.toLowerCase().includes(query) ||
(v.store_code && v.store_code.toLowerCase().includes(query))
);
}
return filtered;
},
navigateToTheme() {
if (!this.selectedStoreCode) {
return;
}
window.location.href = `/admin/stores/${this.selectedStoreCode}/theme`;
}
};
}
storeThemesLog.info('Module loaded');