diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index abff9a3..c6331dc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -63,6 +63,8 @@ import HelpTicketing from './pages/HelpTicketing'; // Import Help page for ticke import HelpApiDocs from './pages/HelpApiDocs'; // Import API documentation page import HelpPluginDocs from './pages/HelpPluginDocs'; // Import Plugin documentation page import PlatformSupport from './pages/PlatformSupport'; // Import Platform Support page (for businesses to contact SmoothSchedule) +import PluginMarketplace from './pages/PluginMarketplace'; // Import Plugin Marketplace page +import MyPlugins from './pages/MyPlugins'; // Import My Plugins page import { Toaster } from 'react-hot-toast'; // Import Toaster for notifications const queryClient = new QueryClient({ @@ -514,6 +516,26 @@ const AppContent: React.FC = () => { } /> } /> } /> + + ) : ( + + ) + } + /> + + ) : ( + + ) + } + /> } /> + {/* Navigation Callout */} +
+

+ + Plugin Management Pages +

+

+ Access these pages from the Plugins dropdown in the sidebar: +

+
+ + +
+
+ {/* Quick Start */} diff --git a/frontend/src/pages/MyPlugins.tsx b/frontend/src/pages/MyPlugins.tsx new file mode 100644 index 0000000..3d343dc --- /dev/null +++ b/frontend/src/pages/MyPlugins.tsx @@ -0,0 +1,526 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; +import { + Package, + Plus, + Trash2, + Star, + RefreshCw, + CheckCircle, + XCircle, + Mail, + BarChart3, + Users, + Calendar, + Link as LinkIcon, + Bot, + Package as PackageIcon, + X, + AlertTriangle +} from 'lucide-react'; +import api from '../api/client'; +import { PluginInstallation, PluginCategory } from '../types'; + +// Category icon mapping +const categoryIcons: Record = { + EMAIL: , + REPORTS: , + CUSTOMER: , + BOOKING: , + INTEGRATION: , + AUTOMATION: , + OTHER: , +}; + +// Category colors +const categoryColors: Record = { + EMAIL: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300', + REPORTS: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300', + CUSTOMER: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300', + BOOKING: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300', + INTEGRATION: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-300', + AUTOMATION: 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300', + OTHER: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300', +}; + +const MyPlugins: React.FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const [selectedPlugin, setSelectedPlugin] = useState(null); + const [showUninstallModal, setShowUninstallModal] = useState(false); + const [showRatingModal, setShowRatingModal] = useState(false); + const [rating, setRating] = useState(0); + const [review, setReview] = useState(''); + + // Fetch installed plugins + const { data: plugins = [], isLoading, error } = useQuery({ + queryKey: ['plugin-installations'], + queryFn: async () => { + const { data } = await api.get('/api/plugin-installations/'); + return data.map((p: any) => ({ + id: String(p.id), + template: String(p.template), + templateName: p.template_name || p.templateName, + templateDescription: p.template_description || p.templateDescription, + category: p.category, + version: p.version, + isActive: p.is_active !== undefined ? p.is_active : p.isActive, + installedAt: p.installed_at || p.installedAt, + hasUpdate: p.has_update !== undefined ? p.has_update : p.hasUpdate || false, + rating: p.rating, + review: p.review, + })); + }, + }); + + // Uninstall plugin mutation + const uninstallMutation = useMutation({ + mutationFn: async (pluginId: string) => { + await api.delete(`/api/plugin-installations/${pluginId}/`); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['plugin-installations'] }); + setShowUninstallModal(false); + setSelectedPlugin(null); + }, + }); + + // Rate plugin mutation + const rateMutation = useMutation({ + mutationFn: async ({ pluginId, rating, review }: { pluginId: string; rating: number; review: string }) => { + const { data } = await api.post(`/api/plugin-installations/${pluginId}/rate/`, { + rating, + review, + }); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['plugin-installations'] }); + setShowRatingModal(false); + setSelectedPlugin(null); + setRating(0); + setReview(''); + }, + }); + + // Update plugin mutation + const updateMutation = useMutation({ + mutationFn: async (pluginId: string) => { + const { data } = await api.post(`/api/plugin-installations/${pluginId}/update/`); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['plugin-installations'] }); + }, + }); + + const handleUninstall = (plugin: PluginInstallation) => { + setSelectedPlugin(plugin); + setShowUninstallModal(true); + }; + + const confirmUninstall = () => { + if (selectedPlugin) { + uninstallMutation.mutate(selectedPlugin.id); + } + }; + + const handleRating = (plugin: PluginInstallation) => { + setSelectedPlugin(plugin); + setRating(plugin.rating || 0); + setReview(plugin.review || ''); + setShowRatingModal(true); + }; + + const submitRating = () => { + if (selectedPlugin && rating > 0) { + rateMutation.mutate({ + pluginId: selectedPlugin.id, + rating, + review, + }); + } + }; + + const handleUpdate = (plugin: PluginInstallation) => { + updateMutation.mutate(plugin.id); + }; + + if (isLoading) { + return ( +
+
+
+
+
+ ); + } + + if (error) { + return ( +
+
+

+ {t('common.error')}: {error instanceof Error ? error.message : 'Unknown error'} +

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

+ + {t('plugins.myPlugins', 'My Plugins')} +

+

+ {t('plugins.myPluginsDescription', 'Manage your installed plugins')} +

+
+ +
+ + {/* Plugin List */} + {plugins.length === 0 ? ( +
+ +

+ {t('plugins.noPluginsInstalled', 'No plugins installed yet')} +

+ +
+ ) : ( +
+ {plugins.map((plugin) => ( +
+
+
+ {/* Plugin Info */} +
+
+

+ {plugin.templateName} +

+ + {categoryIcons[plugin.category]} + {plugin.category} + + {plugin.hasUpdate && ( + + + Update Available + + )} +
+ +

+ {plugin.templateDescription} +

+ +
+
+ + {t('plugins.version', 'Version')}: + + {plugin.version} +
+
+ + {t('plugins.installedOn', 'Installed on')}: + + + {new Date(plugin.installedAt).toLocaleDateString()} + +
+
+ {plugin.isActive ? ( + <> + + + {t('plugins.active', 'Active')} + + + ) : ( + <> + + + {t('plugins.inactive', 'Inactive')} + + + )} +
+ {plugin.rating && ( +
+ + {plugin.rating}/5 +
+ )} +
+
+ + {/* Actions */} +
+ {plugin.hasUpdate && ( + + )} + + +
+
+
+
+ ))} +
+ )} + + {/* Info Box */} +
+
+
+ +
+
+

+ {t('plugins.needCustomPlugin', 'Need a custom plugin?')} +

+

+ {t('plugins.customPluginDescription', 'Create your own custom plugins to extend your business functionality with specific features tailored to your needs.')} +

+ +
+
+
+ + {/* Uninstall Confirmation Modal */} + {showUninstallModal && selectedPlugin && ( +
+
+ {/* Modal Header */} +
+
+
+ +
+

+ {t('plugins.confirmUninstall', 'Confirm Uninstall')} +

+
+ +
+ + {/* Modal Body */} +
+

+ {t('plugins.uninstallWarning', 'Are you sure you want to uninstall')} {selectedPlugin.templateName}? +

+

+ {t('plugins.uninstallNote', 'This action cannot be undone. Your plugin data and settings will be removed.')} +

+
+ + {/* Modal Footer */} +
+ + +
+
+
+ )} + + {/* Rating Modal */} + {showRatingModal && selectedPlugin && ( +
+
+ {/* Modal Header */} +
+

+ {t('plugins.ratePlugin', 'Rate Plugin')} +

+ +
+ + {/* Modal Body */} +
+
+

+ {selectedPlugin.templateName} +

+

+ {t('plugins.rateDescription', 'Share your experience with this plugin')} +

+
+ + {/* Star Rating */} +
+ +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+
+ + {/* Review */} +
+ +