feat: add module info and configuration pages to admin panel

Add dedicated pages for viewing module details and configuring module
settings. Includes routes, templates, and JS components for module
info page showing features, menu items, dependencies, and self-contained
module paths. Also adds View/Configure navigation buttons to the platform
modules list.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 22:55:12 +01:00
parent ec4ec045fc
commit 33072057c2
5 changed files with 539 additions and 0 deletions

View File

@@ -1274,6 +1274,69 @@ async def admin_platform_modules(
)
@router.get(
"/platforms/{platform_code}/modules/{module_code}",
response_class=HTMLResponse,
include_in_schema=False,
)
async def admin_module_info(
request: Request,
platform_code: str = Path(..., description="Platform code"),
module_code: str = Path(..., description="Module code"),
current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)),
db: Session = Depends(get_db),
):
"""
Render module info/detail page.
Shows module details including features, menu items, dependencies,
and self-contained module information.
"""
# Only super admins can access module details
if not current_user.is_super_admin:
return RedirectResponse(url=f"/admin/platforms/{platform_code}", status_code=302)
return templates.TemplateResponse(
"admin/module-info.html",
{
"request": request,
"user": current_user,
"platform_code": platform_code,
"module_code": module_code,
},
)
@router.get(
"/platforms/{platform_code}/modules/{module_code}/config",
response_class=HTMLResponse,
include_in_schema=False,
)
async def admin_module_config(
request: Request,
platform_code: str = Path(..., description="Platform code"),
module_code: str = Path(..., description="Module code"),
current_user: User = Depends(require_menu_access("platforms", FrontendType.ADMIN)),
db: Session = Depends(get_db),
):
"""
Render module configuration page.
Allows configuring module-specific settings.
"""
# Only super admins can access module configuration
if not current_user.is_super_admin:
return RedirectResponse(url=f"/admin/platforms/{platform_code}", status_code=302)
return templates.TemplateResponse(
"admin/module-config.html",
{
"request": request,
"user": current_user,
"platform_code": platform_code,
"module_code": module_code,
},
)
# ============================================================================
# CONTENT MANAGEMENT SYSTEM (CMS) ROUTES
# ============================================================================

View File

@@ -0,0 +1,274 @@
{# app/templates/admin/module-info.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/alerts.html' import alert_dynamic, error_state, loading_state %}
{% from 'shared/macros/headers.html' import page_header %}
{% block title %}Module Details{% endblock %}
{% block alpine_data %}adminModuleInfo('{{ platform_code }}', '{{ module_code }}'){% endblock %}
{% block content %}
<!-- Header with Back Button -->
<div class="flex items-center justify-between my-6">
<div class="flex items-center">
<a :href="`/admin/platforms/${platformCode}/modules`" class="mr-4 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<span x-html="$icon('arrow-left', 'w-5 h-5 text-gray-600 dark:text-gray-400')"></span>
</a>
<div>
<div class="flex items-center">
<span x-html="$icon(getModuleIcon(moduleCode), 'w-8 h-8 text-purple-600 dark:text-purple-400 mr-3')"></span>
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200" x-text="module?.name || 'Loading...'"></h2>
<code class="ml-3 text-sm bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded" x-text="moduleCode"></code>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1" x-text="module?.description || ''"></p>
</div>
</div>
<div class="flex items-center gap-3">
<!-- Configure Button -->
<a x-show="hasConfig(moduleCode)"
:href="`/admin/platforms/${platformCode}/modules/${moduleCode}/config`"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 focus:outline-none focus:ring focus:ring-purple-400">
<span x-html="$icon('cog', 'w-4 h-4 mr-2')"></span>
Configure
</a>
<!-- Status Badge & Toggle -->
<div class="flex items-center gap-3">
<span x-show="module?.is_enabled"
class="px-3 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Enabled
</span>
<span x-show="!module?.is_enabled && !module?.is_core"
class="px-3 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
Disabled
</span>
<span x-show="module?.is_core"
class="px-3 py-1 text-xs font-semibold rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
Core
</span>
<!-- Toggle (only for non-core modules) -->
<button
x-show="!module?.is_core"
@click="toggleModule()"
:disabled="saving"
:class="{
'bg-purple-600': module?.is_enabled,
'bg-gray-200 dark:bg-gray-600': !module?.is_enabled
}"
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
role="switch"
:aria-checked="module?.is_enabled"
>
<span
:class="{
'translate-x-5': module?.is_enabled,
'translate-x-0': !module?.is_enabled
}"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
></span>
</button>
</div>
</div>
</div>
{{ alert_dynamic(type='success', title='Success', message_var='successMessage', show_condition='successMessage') }}
{{ error_state('Error', show_condition='error') }}
{{ loading_state('Loading module details...') }}
<!-- Main Content -->
<div x-show="!loading && module" class="space-y-6">
<!-- Description Card -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">Description</h3>
<p class="text-gray-600 dark:text-gray-400" x-text="module?.description || 'No description available.'"></p>
</div>
<!-- Features -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Features</h3>
<div x-show="module?.features?.length > 0" class="flex flex-wrap gap-2">
<template x-for="feature in module?.features" :key="feature">
<span class="px-3 py-1 text-sm rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300" x-text="feature"></span>
</template>
</div>
<p x-show="!module?.features?.length" class="text-gray-500 dark:text-gray-400 text-sm">No features defined for this module.</p>
</div>
<!-- Menu Items -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Admin Menu Items -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<span x-html="$icon('office-building', 'w-5 h-5 inline mr-2 text-purple-600 dark:text-purple-400')"></span>
Admin Menu Items
</h3>
<div x-show="module?.admin_menu_items?.length > 0" class="space-y-2">
<template x-for="item in module?.admin_menu_items" :key="item">
<div class="flex items-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded">
<span x-html="$icon('menu-alt-2', 'w-4 h-4 text-gray-500 mr-2')"></span>
<span class="text-sm text-gray-700 dark:text-gray-300" x-text="item"></span>
</div>
</template>
</div>
<p x-show="!module?.admin_menu_items?.length" class="text-gray-500 dark:text-gray-400 text-sm">No admin menu items.</p>
</div>
<!-- Vendor Menu Items -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<span x-html="$icon('building-storefront', 'w-5 h-5 inline mr-2 text-teal-600 dark:text-teal-400')"></span>
Vendor Menu Items
</h3>
<div x-show="module?.vendor_menu_items?.length > 0" class="space-y-2">
<template x-for="item in module?.vendor_menu_items" :key="item">
<div class="flex items-center p-2 bg-gray-50 dark:bg-gray-700/50 rounded">
<span x-html="$icon('menu-alt-2', 'w-4 h-4 text-gray-500 mr-2')"></span>
<span class="text-sm text-gray-700 dark:text-gray-300" x-text="item"></span>
</div>
</template>
</div>
<p x-show="!module?.vendor_menu_items?.length" class="text-gray-500 dark:text-gray-400 text-sm">No vendor menu items.</p>
</div>
</div>
<!-- Dependencies -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Requires (Dependencies) -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<span x-html="$icon('link', 'w-5 h-5 inline mr-2 text-amber-600 dark:text-amber-400')"></span>
Dependencies
<span class="text-xs font-normal text-gray-500 dark:text-gray-400 ml-2">(requires)</span>
</h3>
<div x-show="module?.requires?.length > 0" class="flex flex-wrap gap-2">
<template x-for="dep in module?.requires" :key="dep">
<a :href="`/admin/platforms/${platformCode}/modules/${dep}`"
class="px-3 py-1 text-sm rounded-full bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 hover:bg-amber-200 dark:hover:bg-amber-900/50 transition-colors"
x-text="dep"></a>
</template>
</div>
<p x-show="!module?.requires?.length" class="text-gray-500 dark:text-gray-400 text-sm">No dependencies.</p>
</div>
<!-- Dependents (Required By) -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<span x-html="$icon('users', 'w-5 h-5 inline mr-2 text-blue-600 dark:text-blue-400')"></span>
Dependents
<span class="text-xs font-normal text-gray-500 dark:text-gray-400 ml-2">(required by)</span>
</h3>
<div x-show="module?.dependent_modules?.length > 0" class="flex flex-wrap gap-2">
<template x-for="dep in module?.dependent_modules" :key="dep">
<a :href="`/admin/platforms/${platformCode}/modules/${dep}`"
class="px-3 py-1 text-sm rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 hover:bg-blue-200 dark:hover:bg-blue-900/50 transition-colors"
x-text="dep"></a>
</template>
</div>
<p x-show="!module?.dependent_modules?.length" class="text-gray-500 dark:text-gray-400 text-sm">No modules depend on this one.</p>
</div>
</div>
<!-- Self-Contained Module Info (if applicable) -->
<div x-show="module?.is_self_contained" class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
<span x-html="$icon('puzzle', 'w-5 h-5 inline mr-2 text-green-600 dark:text-green-400')"></span>
Self-Contained Module
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Services -->
<div class="flex items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span x-show="module?.self_contained_info?.services_path" x-html="$icon('check-circle', 'w-5 h-5 text-green-500 mr-3')"></span>
<span x-show="!module?.self_contained_info?.services_path" x-html="$icon('x-circle', 'w-5 h-5 text-gray-400 mr-3')"></span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Services</p>
<code x-show="module?.self_contained_info?.services_path" class="text-xs text-gray-500 dark:text-gray-400" x-text="module?.self_contained_info?.services_path"></code>
<p x-show="!module?.self_contained_info?.services_path" class="text-xs text-gray-400">Not defined</p>
</div>
</div>
<!-- Models -->
<div class="flex items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span x-show="module?.self_contained_info?.models_path" x-html="$icon('check-circle', 'w-5 h-5 text-green-500 mr-3')"></span>
<span x-show="!module?.self_contained_info?.models_path" x-html="$icon('x-circle', 'w-5 h-5 text-gray-400 mr-3')"></span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Models</p>
<code x-show="module?.self_contained_info?.models_path" class="text-xs text-gray-500 dark:text-gray-400" x-text="module?.self_contained_info?.models_path"></code>
<p x-show="!module?.self_contained_info?.models_path" class="text-xs text-gray-400">Not defined</p>
</div>
</div>
<!-- Templates -->
<div class="flex items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span x-show="module?.self_contained_info?.templates_path" x-html="$icon('check-circle', 'w-5 h-5 text-green-500 mr-3')"></span>
<span x-show="!module?.self_contained_info?.templates_path" x-html="$icon('x-circle', 'w-5 h-5 text-gray-400 mr-3')"></span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Templates</p>
<code x-show="module?.self_contained_info?.templates_path" class="text-xs text-gray-500 dark:text-gray-400" x-text="module?.self_contained_info?.templates_path"></code>
<p x-show="!module?.self_contained_info?.templates_path" class="text-xs text-gray-400">Not defined</p>
</div>
</div>
<!-- Locales -->
<div class="flex items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span x-show="module?.self_contained_info?.locales_path" x-html="$icon('check-circle', 'w-5 h-5 text-green-500 mr-3')"></span>
<span x-show="!module?.self_contained_info?.locales_path" x-html="$icon('x-circle', 'w-5 h-5 text-gray-400 mr-3')"></span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Locales</p>
<code x-show="module?.self_contained_info?.locales_path" class="text-xs text-gray-500 dark:text-gray-400" x-text="module?.self_contained_info?.locales_path"></code>
<p x-show="!module?.self_contained_info?.locales_path" class="text-xs text-gray-400">Not defined</p>
</div>
</div>
<!-- Static Files -->
<div class="flex items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span x-show="module?.self_contained_info?.static_path" x-html="$icon('check-circle', 'w-5 h-5 text-green-500 mr-3')"></span>
<span x-show="!module?.self_contained_info?.static_path" x-html="$icon('x-circle', 'w-5 h-5 text-gray-400 mr-3')"></span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">Static Files</p>
<code x-show="module?.self_contained_info?.static_path" class="text-xs text-gray-500 dark:text-gray-400" x-text="module?.self_contained_info?.static_path"></code>
<p x-show="!module?.self_contained_info?.static_path" class="text-xs text-gray-400">Not defined</p>
</div>
</div>
<!-- API Routes -->
<div class="flex items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<span x-show="module?.self_contained_info?.api_path" x-html="$icon('check-circle', 'w-5 h-5 text-green-500 mr-3')"></span>
<span x-show="!module?.self_contained_info?.api_path" x-html="$icon('x-circle', 'w-5 h-5 text-gray-400 mr-3')"></span>
<div>
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">API Routes</p>
<code x-show="module?.self_contained_info?.api_path" class="text-xs text-gray-500 dark:text-gray-400" x-text="module?.self_contained_info?.api_path"></code>
<p x-show="!module?.self_contained_info?.api_path" class="text-xs text-gray-400">Not defined</p>
</div>
</div>
</div>
</div>
<!-- Module Type Info -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Module Information</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700">
<span class="text-gray-600 dark:text-gray-400">Module Type</span>
<span x-show="module?.is_core" class="px-2 py-1 text-xs font-medium rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">Core</span>
<span x-show="!module?.is_core" class="px-2 py-1 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">Optional</span>
</div>
<div class="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700">
<span class="text-gray-600 dark:text-gray-400">Self-Contained</span>
<span x-show="module?.is_self_contained" class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">Yes</span>
<span x-show="!module?.is_self_contained" class="px-2 py-1 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">No</span>
</div>
<div class="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700">
<span class="text-gray-600 dark:text-gray-400">Has Configuration</span>
<span x-show="hasConfig(moduleCode)" class="px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">Yes</span>
<span x-show="!hasConfig(moduleCode)" class="px-2 py-1 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400">No</span>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='admin/js/module-info.js') }}"></script>
{% endblock %}

View File

@@ -136,6 +136,21 @@
</div>
</div>
<div class="flex items-center gap-3 ml-4">
<!-- View Details Button -->
<a :href="`/admin/platforms/${platformCode}/modules/${module.code}`"
class="p-1.5 rounded-lg text-gray-500 hover:text-purple-600 hover:bg-purple-50 dark:text-gray-400 dark:hover:text-purple-400 dark:hover:bg-purple-900/20 transition-colors"
title="View Details">
<span x-html="$icon('eye', 'w-4 h-4')"></span>
</a>
<!-- Configure Button (if has config) -->
<a x-show="hasConfig(module.code)"
:href="`/admin/platforms/${platformCode}/modules/${module.code}/config`"
class="p-1.5 rounded-lg text-gray-500 hover:text-blue-600 hover:bg-blue-50 dark:text-gray-400 dark:hover:text-blue-400 dark:hover:bg-blue-900/20 transition-colors"
title="Configure">
<span x-html="$icon('cog', 'w-4 h-4')"></span>
</a>
<span class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Enabled
</span>
@@ -202,6 +217,21 @@
</div>
</div>
<div class="flex items-center gap-3 ml-4">
<!-- View Details Button -->
<a :href="`/admin/platforms/${platformCode}/modules/${module.code}`"
class="p-1.5 rounded-lg text-gray-500 hover:text-purple-600 hover:bg-purple-50 dark:text-gray-400 dark:hover:text-purple-400 dark:hover:bg-purple-900/20 transition-colors"
title="View Details">
<span x-html="$icon('eye', 'w-4 h-4')"></span>
</a>
<!-- Configure Button (if has config) -->
<a x-show="hasConfig(module.code)"
:href="`/admin/platforms/${platformCode}/modules/${module.code}/config`"
class="p-1.5 rounded-lg text-gray-500 hover:text-blue-600 hover:bg-blue-50 dark:text-gray-400 dark:hover:text-blue-400 dark:hover:bg-blue-900/20 transition-colors"
title="Configure">
<span x-html="$icon('cog', 'w-4 h-4')"></span>
</a>
<!-- Status Badge -->
<span x-show="module.is_enabled"
class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">

View File

@@ -0,0 +1,166 @@
// static/admin/js/module-info.js
// Module info/detail page management
const moduleInfoLog = window.LogConfig?.loggers?.moduleInfo || window.LogConfig?.createLogger?.('moduleInfo') || console;
function adminModuleInfo(platformCode, moduleCode) {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Page-specific state
currentPage: 'platforms',
platformCode: platformCode,
moduleCode: moduleCode,
loading: true,
error: null,
successMessage: null,
saving: false,
// Data
platform: null,
module: null,
// Module icons mapping (must match icons.js definitions)
getModuleIcon(code) {
const icons = {
'core': 'home',
'platform-admin': 'office-building',
'billing': 'credit-card',
'inventory': 'archive',
'orders': 'shopping-cart',
'marketplace': 'shopping-bag',
'customers': 'users',
'cms': 'document-text',
'analytics': 'chart-bar',
'messaging': 'chat',
'dev-tools': 'code',
'monitoring': 'chart-pie'
};
return icons[code] || 'puzzle-piece';
},
// Modules with configuration options
hasConfig(code) {
return ['billing', 'inventory', 'orders', 'marketplace',
'customers', 'cms', 'analytics', 'messaging', 'monitoring'].includes(code);
},
async init() {
// Guard against duplicate initialization
if (window._moduleInfoInitialized) {
moduleInfoLog.warn('Already initialized, skipping');
return;
}
window._moduleInfoInitialized = true;
moduleInfoLog.info('=== MODULE INFO PAGE INITIALIZING ===');
moduleInfoLog.info('Platform code:', this.platformCode);
moduleInfoLog.info('Module code:', this.moduleCode);
try {
await this.loadPlatform();
await this.loadModuleInfo();
moduleInfoLog.info('=== MODULE INFO PAGE INITIALIZED ===');
} catch (error) {
moduleInfoLog.error('Failed to initialize module info page:', error);
this.error = 'Failed to load page data. Please refresh.';
}
},
async refresh() {
this.error = null;
this.successMessage = null;
await this.loadModuleInfo();
},
async loadPlatform() {
try {
this.platform = await apiClient.get(`/admin/platforms/${this.platformCode}`);
moduleInfoLog.info('Loaded platform:', this.platform?.name);
} catch (error) {
moduleInfoLog.error('Failed to load platform:', error);
throw error;
}
},
async loadModuleInfo() {
this.loading = true;
this.error = null;
try {
const platformId = this.platform?.id;
if (!platformId) {
throw new Error('Platform not loaded');
}
// Get all modules and find the specific one
const moduleConfig = await apiClient.get(`/admin/modules/platforms/${platformId}`);
this.module = moduleConfig.modules?.find(m => m.code === this.moduleCode);
if (!this.module) {
throw new Error(`Module '${this.moduleCode}' not found`);
}
moduleInfoLog.info('Loaded module info:', {
code: this.module.code,
name: this.module.name,
is_enabled: this.module.is_enabled,
is_core: this.module.is_core
});
} catch (error) {
moduleInfoLog.error('Failed to load module info:', error);
this.error = error.message || 'Failed to load module information';
} finally {
this.loading = false;
}
},
async toggleModule() {
if (this.module?.is_core) {
moduleInfoLog.warn('Cannot toggle core module:', this.moduleCode);
return;
}
this.saving = true;
this.error = null;
this.successMessage = null;
const action = this.module.is_enabled ? 'disable' : 'enable';
try {
const platformId = this.platform?.id;
const endpoint = `/admin/modules/platforms/${platformId}/${action}`;
const result = await apiClient.post(endpoint, {
module_code: this.moduleCode
});
moduleInfoLog.info(`${action}d module:`, this.moduleCode, result);
// Show success message
if (result.also_enabled?.length > 0) {
this.successMessage = `Module '${this.module.name}' enabled. Also enabled dependencies: ${result.also_enabled.join(', ')}`;
} else if (result.also_disabled?.length > 0) {
this.successMessage = `Module '${this.module.name}' disabled. Also disabled dependents: ${result.also_disabled.join(', ')}`;
} else {
this.successMessage = `Module '${this.module.name}' ${action}d successfully`;
}
// Reload module info to get updated state
await this.loadModuleInfo();
// Clear success message after delay
setTimeout(() => {
this.successMessage = null;
}, 5000);
} catch (error) {
moduleInfoLog.error(`Failed to ${action} module:`, error);
this.error = error.message || `Failed to ${action} module`;
} finally {
this.saving = false;
}
}
};
}

View File

@@ -58,6 +58,12 @@ function adminPlatformModules(platformCode) {
return icons[moduleCode] || 'puzzle-piece';
},
// Modules with configuration options
hasConfig(moduleCode) {
return ['billing', 'inventory', 'orders', 'marketplace',
'customers', 'cms', 'analytics', 'messaging', 'monitoring'].includes(moduleCode);
},
async init() {
// Guard against duplicate initialization
if (window._platformModulesInitialized) {