feat: Implement centralized module management system

- Add ModuleRegistry for managing built-in modules (newsletter, wishlist, affiliate, subscription, licensing)
- Add ModulesController REST API for module enable/disable
- Create Modules settings page with category grouping and toggle controls
- Integrate module checks across admin-spa and customer-spa
- Add useModules hook for both SPAs to check module status
- Hide newsletter from footer builder when module disabled
- Hide wishlist features when module disabled (product cards, account menu, wishlist page)
- Protect wishlist API endpoints with module checks
- Auto-update navigation tree when modules toggled
- Clean up obsolete documentation files
- Add comprehensive documentation:
  - MODULE_SYSTEM_IMPLEMENTATION.md
  - MODULE_INTEGRATION_SUMMARY.md
  - ADDON_MODULE_INTEGRATION.md (proposal)
  - ADDON_MODULE_DESIGN_DECISIONS.md (design doc)
  - FEATURE_ROADMAP.md
  - SHIPPING_INTEGRATION.md

Module system provides:
- Centralized enable/disable for all features
- Automatic navigation updates
- Frontend/backend integration
- Foundation for addon-module unification
This commit is contained in:
Dwindi Ramadhana
2025-12-26 19:19:49 +07:00
parent 0b2c8a56d6
commit 07020bc0dd
59 changed files with 3891 additions and 12132 deletions

View File

@@ -0,0 +1,180 @@
import React from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { SettingsLayout } from './components/SettingsLayout';
import { SettingsCard } from './components/SettingsCard';
import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge';
import { RefreshCw, Mail, Heart, Users, RefreshCcw, Key } from 'lucide-react';
import { toast } from 'sonner';
import { __ } from '@/lib/i18n';
interface Module {
id: string;
label: string;
description: string;
category: string;
icon: string;
enabled: boolean;
features: string[];
}
interface ModulesData {
modules: Record<string, Module>;
grouped: {
marketing: Module[];
customers: Module[];
products: Module[];
};
}
export default function Modules() {
const queryClient = useQueryClient();
const { data: modulesData, isLoading } = useQuery<ModulesData>({
queryKey: ['modules'],
queryFn: async () => {
const response = await api.get('/modules');
// api.get returns JSON directly, not wrapped in .data
return response as ModulesData;
},
});
const toggleModule = useMutation({
mutationFn: async ({ moduleId, enabled }: { moduleId: string; enabled: boolean }) => {
return api.post('/modules/toggle', { module_id: moduleId, enabled });
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ['modules'] });
toast.success(
variables.enabled
? __('Module enabled successfully')
: __('Module disabled successfully')
);
},
onError: (error: any) => {
toast.error(error?.message || __('Failed to toggle module'));
},
});
const getIcon = (iconName: string) => {
const icons: Record<string, any> = {
mail: Mail,
heart: Heart,
users: Users,
'refresh-cw': RefreshCcw,
key: Key,
};
const Icon = icons[iconName] || Mail;
return <Icon className="h-5 w-5" />;
};
const getCategoryLabel = (category: string) => {
const labels: Record<string, string> = {
marketing: __('Marketing & Sales'),
customers: __('Customer Experience'),
products: __('Products & Inventory'),
};
return labels[category] || category;
};
const categories = ['marketing', 'customers', 'products'];
return (
<SettingsLayout
title={__('Module Management')}
description={__('Enable or disable features to customize your store')}
isLoading={isLoading}
>
{/* Info Card */}
<div className="bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900 rounded-lg p-4 mb-6">
<div className="text-sm space-y-2">
<p className="text-blue-900 dark:text-blue-100">
{__(
'Modules allow you to enable only the features you need. Disabling unused modules improves performance and reduces clutter in your admin panel.'
)}
</p>
<p className="text-xs text-blue-700 dark:text-blue-300">
💡 {__('Tip: When you disable a module, its menu items and settings will be hidden from the admin panel, and its features will be disabled on the frontend.')}
</p>
</div>
</div>
{/* Module Categories */}
{categories.map((category) => {
const modules = modulesData?.grouped[category as keyof typeof modulesData.grouped] || [];
if (modules.length === 0) return null;
return (
<SettingsCard
key={category}
title={getCategoryLabel(category)}
description={__('Manage modules in this category')}
>
<div className="space-y-4">
{modules.map((module) => (
<div
key={module.id}
className="flex items-start gap-4 p-4 border rounded-lg bg-card hover:bg-accent/5 transition-colors"
>
{/* Icon */}
<div
className={`p-3 rounded-lg ${
module.enabled
? 'bg-primary/10 text-primary'
: 'bg-muted text-muted-foreground'
}`}
>
{getIcon(module.icon)}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-medium text-sm">{module.label}</h3>
{module.enabled && (
<Badge variant="secondary" className="text-xs">
{__('Active')}
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground mb-3">
{module.description}
</p>
{/* Features List */}
{module.features && module.features.length > 0 && (
<ul className="space-y-1">
{module.features.map((feature, index) => (
<li
key={index}
className="text-xs text-muted-foreground flex items-center gap-2"
>
<span className="text-primary"></span>
{feature}
</li>
))}
</ul>
)}
</div>
{/* Toggle Switch */}
<div className="flex items-center">
<Switch
checked={module.enabled}
onCheckedChange={(enabled) =>
toggleModule.mutate({ moduleId: module.id, enabled })
}
disabled={toggleModule.isPending}
/>
</div>
</div>
))}
</div>
</SettingsCard>
);
})}
</SettingsLayout>
);
}