diff --git a/RestroHub-FrontEnd/src/components/admin/Sidebar.jsx b/RestroHub-FrontEnd/src/components/admin/Sidebar.jsx index 1953e8f8..d8019c16 100644 --- a/RestroHub-FrontEnd/src/components/admin/Sidebar.jsx +++ b/RestroHub-FrontEnd/src/components/admin/Sidebar.jsx @@ -15,13 +15,19 @@ import { ChevronsLeft, ChevronsRight, ChefHat, + ShieldCheck, } from 'lucide-react'; import { useAdminTheme } from '@context/AdminThemeContext'; +import { FULL_ADMIN_ROLES, hasAnyRole, readStoredRoles } from '../../utils/auth'; const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => { const location = useLocation(); const sidebarRef = useRef(null); const { isDark } = useAdminTheme(); + const roles = readStoredRoles(); + const limitedAdminRoles = ['MANAGER', 'STAFF']; + const superAdminOnly = ['SUPER_ADMIN']; + const allAdminRoles = [...FULL_ADMIN_ROLES, ...limitedAdminRoles]; const [expandedMenus, setExpandedMenus] = useState({ store: false, @@ -61,10 +67,10 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => { { label: 'Menu', items: [ - { type: 'link', name: 'Dashboard', path: '/admin/dashboard', icon: LayoutDashboard }, - { type: 'link', name: 'Kitchen Display', path: '/admin/kds', icon: ChefHat }, - { type: 'link', name: 'Menus', path: '/admin/menus', icon: UtensilsCrossed }, - { type: 'link', name: 'Orders', path: '/admin/orders', icon: ShoppingCart }, + { type: 'link', name: 'Dashboard', path: '/admin/dashboard', icon: LayoutDashboard, allowedRoles: FULL_ADMIN_ROLES }, + { type: 'link', name: 'Kitchen Display', path: '/admin/kds', icon: ChefHat, allowedRoles: allAdminRoles }, + { type: 'link', name: 'Menus', path: '/admin/menus', icon: UtensilsCrossed, allowedRoles: FULL_ADMIN_ROLES }, + { type: 'link', name: 'Orders', path: '/admin/orders', icon: ShoppingCart, allowedRoles: allAdminRoles }, ], }, { @@ -75,8 +81,9 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => { name: 'Store', icon: Store, menuKey: 'store', + allowedRoles: FULL_ADMIN_ROLES, children: [ - { name: 'Branches', path: '/admin/store/branches', icon: Building2 }, + { name: 'Branches', path: '/admin/store/branches', icon: Building2, allowedRoles: FULL_ADMIN_ROLES }, ], }, { @@ -84,9 +91,10 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => { name: 'Marketing', icon: Megaphone, menuKey: 'marketing', + allowedRoles: FULL_ADMIN_ROLES, children: [ - { name: 'Website', path: '/admin/marketing/website', icon: Globe }, - { name: 'QR Display', path: '/admin/marketing/qr-display', icon: QrCode }, + { name: 'Website', path: '/admin/marketing/website', icon: Globe, allowedRoles: FULL_ADMIN_ROLES }, + { name: 'QR Display', path: '/admin/marketing/qr-display', icon: QrCode, allowedRoles: FULL_ADMIN_ROLES }, ], }, ], @@ -94,11 +102,34 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => { { label: 'Payments', items: [ - { type: 'link', name: 'UPI Links', path: '/admin/upi-links', icon: CreditCard }, + { type: 'link', name: 'UPI Links', path: '/admin/upi-links', icon: CreditCard, allowedRoles: FULL_ADMIN_ROLES }, + ], + }, + { + label: 'Platform', + items: [ + { type: 'link', name: 'Subscriptions', path: '/admin/subscriptions', icon: ShieldCheck, allowedRoles: superAdminOnly }, ], }, ]; + const canViewItem = (item) => !item.allowedRoles || hasAnyRole(roles, item.allowedRoles); + + const visibleNavSections = navSections + .map((section) => ({ + ...section, + items: section.items + .map((item) => { + if (item.type !== 'expandable') return item; + return { + ...item, + children: item.children.filter(canViewItem), + }; + }) + .filter((item) => canViewItem(item) && (item.type !== 'expandable' || item.children.length > 0)), + })) + .filter((section) => section.items.length > 0); + // ============================================ // SINGLE NAV LINK // ============================================ @@ -369,7 +400,7 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => { {/* NAVIGATION */} {/* ================================= */}
- {navSections.map((section, sectionIndex) => ( + {visibleNavSections.map((section, sectionIndex) => (
0 ? 'mt-4' : ''}> {/* Section Label */} {!collapsed ? ( @@ -458,4 +489,4 @@ const Sidebar = ({ open, setOpen, collapsed, setCollapsed }) => { ); }; -export default Sidebar; \ No newline at end of file +export default Sidebar; diff --git a/RestroHub-FrontEnd/src/components/admin/subscriptions/SubscriptionManagement.jsx b/RestroHub-FrontEnd/src/components/admin/subscriptions/SubscriptionManagement.jsx new file mode 100644 index 00000000..3af0eb2d --- /dev/null +++ b/RestroHub-FrontEnd/src/components/admin/subscriptions/SubscriptionManagement.jsx @@ -0,0 +1,694 @@ +import { useEffect, useMemo, useState } from 'react'; +import { Navigate } from 'react-router-dom'; +import toast from 'react-hot-toast'; +import { + Check, + Edit2, + Loader2, + Plus, + RefreshCw, + Search, + ShieldCheck, + Trash2, + X, +} from 'lucide-react'; +import api from '@services/common/api'; +import { hasAnyRole, readStoredRoles } from '../../../utils/auth'; + +const emptyFeatureForm = { + featureKey: '', + displayName: '', + description: '', + isActive: true, +}; + +const emptyPlanForm = { + name: '', + description: '', + price: '', + billingCycle: 'MONTHLY', + isActive: true, + featureValues: {}, +}; + +const pickData = (response) => response.data?.data || response.data || []; + +const SubscriptionManagement = () => { + const roles = readStoredRoles(); + const [activeTab, setActiveTab] = useState('plans'); + const [plans, setPlans] = useState([]); + const [features, setFeatures] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [planSearch, setPlanSearch] = useState(''); + const [featureSearch, setFeatureSearch] = useState(''); + const [editingPlanId, setEditingPlanId] = useState(null); + const [editingFeatureId, setEditingFeatureId] = useState(null); + const [planForm, setPlanForm] = useState(emptyPlanForm); + const [featureForm, setFeatureForm] = useState(emptyFeatureForm); + const [selectedFeatureIds, setSelectedFeatureIds] = useState([]); + const [confirmTarget, setConfirmTarget] = useState(null); + const [assignment, setAssignment] = useState({ + restaurantId: '', + planId: '', + durationInMonths: 1, + isAutoRenew: true, + }); + const [restaurantSubscription, setRestaurantSubscription] = useState(null); + const [assignmentLoading, setAssignmentLoading] = useState(false); + + const fetchSubscriptions = async () => { + setLoading(true); + try { + const [plansResponse, featuresResponse] = await Promise.all([ + api.get('/secure/api/v1/admin/subscriptions/plans'), + api.get('/secure/api/v1/admin/subscriptions/features'), + ]); + const planList = pickData(plansResponse); + const featureList = pickData(featuresResponse); + setPlans(Array.isArray(planList) ? planList : []); + setFeatures(Array.isArray(featureList) ? featureList : []); + } catch (error) { + toast.error(error.response?.data?.message || 'Unable to load subscriptions'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchSubscriptions(); + }, []); + + const filteredPlans = useMemo(() => { + const query = planSearch.trim().toLowerCase(); + if (!query) return plans; + return plans.filter((plan) => + [plan.name, plan.description, plan.billingCycle].some((value) => + String(value || '').toLowerCase().includes(query) + ) + ); + }, [plans, planSearch]); + + const filteredFeatures = useMemo(() => { + const query = featureSearch.trim().toLowerCase(); + if (!query) return features; + return features.filter((feature) => + [feature.featureKey, feature.displayName, feature.description].some((value) => + String(value || '').toLowerCase().includes(query) + ) + ); + }, [features, featureSearch]); + + if (!hasAnyRole(roles, ['SUPER_ADMIN'])) { + return ; + } + + const resetPlanForm = () => { + setEditingPlanId(null); + setPlanForm(emptyPlanForm); + setSelectedFeatureIds([]); + }; + + const resetFeatureForm = () => { + setEditingFeatureId(null); + setFeatureForm(emptyFeatureForm); + }; + + const editPlan = (plan) => { + const featureValues = {}; + const featureIds = (plan.features || []).map((mapping) => { + featureValues[mapping.featureId] = mapping.featureValue || 'true'; + return Number(mapping.featureId); + }); + + setEditingPlanId(plan.id); + setSelectedFeatureIds(featureIds); + setPlanForm({ + name: plan.name || '', + description: plan.description || '', + price: plan.price ?? '', + billingCycle: plan.billingCycle || 'MONTHLY', + isActive: plan.isActive !== false, + featureValues, + }); + setActiveTab('plans'); + }; + + const editFeature = (feature) => { + setEditingFeatureId(feature.id); + setFeatureForm({ + featureKey: feature.featureKey || '', + displayName: feature.displayName || '', + description: feature.description || '', + isActive: feature.isActive !== false, + }); + setActiveTab('features'); + }; + + const togglePlanFeature = (featureId) => { + setSelectedFeatureIds((current) => + current.includes(featureId) + ? current.filter((id) => id !== featureId) + : [...current, featureId] + ); + setPlanForm((current) => ({ + ...current, + featureValues: { + ...current.featureValues, + [featureId]: current.featureValues[featureId] || 'true', + }, + })); + }; + + const savePlan = async (event) => { + event.preventDefault(); + setSaving(true); + const payload = { + name: planForm.name.trim(), + description: planForm.description.trim(), + price: Number(planForm.price), + billingCycle: planForm.billingCycle, + isActive: planForm.isActive, + features: selectedFeatureIds.map((featureId) => ({ + featureId: Number(featureId), + featureValue: planForm.featureValues[featureId] || 'true', + })), + }; + + try { + if (editingPlanId) { + await api.put(`/secure/api/v1/admin/subscriptions/plans/${editingPlanId}`, payload); + toast.success('Plan updated'); + } else { + await api.post('/secure/api/v1/admin/subscriptions/plans', payload); + toast.success('Plan created'); + } + resetPlanForm(); + await fetchSubscriptions(); + } catch (error) { + toast.error(error.response?.data?.message || 'Unable to save plan'); + } finally { + setSaving(false); + } + }; + + const saveFeature = async (event) => { + event.preventDefault(); + setSaving(true); + const payload = { + featureKey: featureForm.featureKey.trim(), + displayName: featureForm.displayName.trim(), + description: featureForm.description.trim(), + isActive: featureForm.isActive, + }; + + try { + if (editingFeatureId) { + await api.put(`/secure/api/v1/admin/subscriptions/features/${editingFeatureId}`, payload); + toast.success('Feature updated'); + } else { + await api.post('/secure/api/v1/admin/subscriptions/features', payload); + toast.success('Feature created'); + } + resetFeatureForm(); + await fetchSubscriptions(); + } catch (error) { + toast.error(error.response?.data?.message || 'Unable to save feature'); + } finally { + setSaving(false); + } + }; + + const deleteSelected = async () => { + if (!confirmTarget) return; + setSaving(true); + try { + const endpoint = + confirmTarget.type === 'plan' + ? `/secure/api/v1/admin/subscriptions/plans/${confirmTarget.id}` + : `/secure/api/v1/admin/subscriptions/features/${confirmTarget.id}`; + await api.delete(endpoint); + toast.success(confirmTarget.type === 'plan' ? 'Plan deleted' : 'Feature deleted'); + await fetchSubscriptions(); + } catch (error) { + toast.error(error.response?.data?.message || 'Unable to delete item'); + } finally { + setSaving(false); + setConfirmTarget(null); + } + }; + + const assignPlan = async (event) => { + event.preventDefault(); + setAssignmentLoading(true); + try { + const response = await api.post( + `/secure/api/v1/admin/subscriptions/restaurants/${assignment.restaurantId}/assign`, + { + planId: Number(assignment.planId), + isAutoRenew: assignment.isAutoRenew, + durationInMonths: Number(assignment.durationInMonths), + } + ); + setRestaurantSubscription(pickData(response)); + toast.success('Plan assigned'); + } catch (error) { + toast.error(error.response?.data?.message || 'Unable to assign plan'); + } finally { + setAssignmentLoading(false); + } + }; + + const fetchRestaurantSubscription = async () => { + if (!assignment.restaurantId) { + toast.error('Enter a restaurant ID'); + return; + } + setAssignmentLoading(true); + try { + const response = await api.get(`/secure/api/v1/restaurant/${assignment.restaurantId}/subscription`); + setRestaurantSubscription(pickData(response)); + } catch (error) { + setRestaurantSubscription(null); + toast.error(error.response?.data?.message || 'No subscription found'); + } finally { + setAssignmentLoading(false); + } + }; + + return ( +
+
+
+
+ + SuperAdmin +
+

Subscription Management

+
+ +
+ +
+ {[ + ['plans', 'Plans'], + ['features', 'Features'], + ['assignment', 'Restaurant Assignment'], + ].map(([value, label]) => ( + + ))} +
+ + {loading ? ( +
+ {[1, 2, 3].map((item) => ( +
+ ))} +
+ ) : ( + <> + {activeTab === 'plans' && ( +
+
+
+

{editingPlanId ? 'Edit Plan' : 'Create Plan'}

+ {editingPlanId && ( + + )} +
+ setPlanForm({ ...planForm, name: event.target.value })} + required + placeholder="Plan name" + className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm" + /> +