- {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' && (
+
+
+
+
+
+
+ setPlanSearch(event.target.value)}
+ placeholder="Search plans"
+ className="w-full rounded-md border border-gray-300 bg-white py-2 pl-9 pr-3 text-sm"
+ />
+
+
+ {filteredPlans.map((plan) => (
+
+
+
+
{plan.name}
+
{plan.description}
+
+
+ {plan.isActive !== false ? 'Active' : 'Inactive'}
+
+
+
+ Rs. {plan.price}
+ {plan.billingCycle}
+
+
+ {(plan.features || []).map((feature) => (
+
+ {feature.featureKey}: {feature.featureValue}
+
+ ))}
+
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+ {activeTab === 'features' && (
+
+
+
+
+
+
+ setFeatureSearch(event.target.value)}
+ placeholder="Search features"
+ className="w-full rounded-md border border-gray-300 bg-white py-2 pl-9 pr-3 text-sm"
+ />
+
+
+ {filteredFeatures.map((feature) => (
+
+
+
+
{feature.displayName}
+
{feature.featureKey}
+
{feature.description}
+
+ {feature.isActive !== false &&
}
+
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+ {activeTab === 'assignment' && (
+
+
+
+
+
Current Subscription
+ {restaurantSubscription ? (
+
+
+
Restaurant: {restaurantSubscription.restaurantId}
+
Status: {restaurantSubscription.status}
+
Plan: {restaurantSubscription.plan?.name}
+
Auto renew: {restaurantSubscription.isAutoRenew ? 'Yes' : 'No'}
+
Start: {restaurantSubscription.startDate || '-'}
+
End: {restaurantSubscription.endDate || '-'}
+
+
+ {(restaurantSubscription.plan?.features || []).map((feature) => (
+
+ {feature.featureKey}: {feature.featureValue}
+
+ ))}
+
+
+ ) : (
+
No subscription selected
+ )}
+
+
+ )}
+ >
+ )}
+
+ {confirmTarget && (
+
+
+
Confirm delete
+
+ Delete {confirmTarget.name}?
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default SubscriptionManagement;
diff --git a/RestroHub-FrontEnd/src/routes/index.jsx b/RestroHub-FrontEnd/src/routes/index.jsx
index 84b98327..4e2d28e7 100644
--- a/RestroHub-FrontEnd/src/routes/index.jsx
+++ b/RestroHub-FrontEnd/src/routes/index.jsx
@@ -28,6 +28,7 @@ import QRDisplay from '@components/admin/marketing/qr/QRDisplay';
import UPILinks from '@components/admin/upi/UPILinks';
import KitchenDisplaySystem from '@components/admin/kds/KitchenDisplaySystem';
import Profile from '@components/admin/profile/Profile';
+import SubscriptionManagement from '@components/admin/subscriptions/SubscriptionManagement';
const AppRoutes = () => {
return (
@@ -64,6 +65,10 @@ const AppRoutes = () => {
} />
} />
} />
+
}
+ />
} />
} />
@@ -74,4 +79,4 @@ const AppRoutes = () => {
);
};
-export default AppRoutes;
\ No newline at end of file
+export default AppRoutes;
diff --git a/RestroHub-FrontEnd/src/services/public/ApiService.js b/RestroHub-FrontEnd/src/services/public/ApiService.js
index 9d3c4623..2ec5bd96 100644
--- a/RestroHub-FrontEnd/src/services/public/ApiService.js
+++ b/RestroHub-FrontEnd/src/services/public/ApiService.js
@@ -60,6 +60,7 @@ try {
console.error("Failed to parse response:", err);
throw new Error("Invalid server response");
}
+};
const ApiService = {
// ============================================
diff --git a/RestroHub-FrontEnd/src/utils/auth.js b/RestroHub-FrontEnd/src/utils/auth.js
new file mode 100644
index 00000000..aeb31fe4
--- /dev/null
+++ b/RestroHub-FrontEnd/src/utils/auth.js
@@ -0,0 +1,31 @@
+export const ADMIN_ACCESS_ROLES = ['SUPER_ADMIN', 'ADMIN', 'MANAGER', 'STAFF'];
+export const FULL_ADMIN_ROLES = ['SUPER_ADMIN', 'ADMIN'];
+export const LIMITED_ADMIN_ROLES = ['MANAGER', 'STAFF'];
+
+export const normalizeRole = (role) => {
+ const roleName = typeof role === 'string' ? role : role?.authority || role?.name || '';
+ return roleName.replace(/^ROLE_/, '').toUpperCase();
+};
+
+export const readStoredRoles = () => {
+ try {
+ const rolesStr = localStorage.getItem('roles');
+ const roles = rolesStr ? JSON.parse(rolesStr) : [];
+ return Array.isArray(roles) ? roles.map(normalizeRole).filter(Boolean) : [];
+ } catch {
+ console.error('Failed to parse roles');
+ return [];
+ }
+};
+
+export const hasAnyRole = (roles, allowedRoles = []) => {
+ const normalizedRoles = roles.map(normalizeRole);
+ const allowed = allowedRoles.map(normalizeRole);
+ return normalizedRoles.some((role) => allowed.includes(role));
+};
+
+export const getDefaultAdminPath = (roles) => {
+ if (hasAnyRole(roles, FULL_ADMIN_ROLES)) return '/admin/dashboard';
+ if (hasAnyRole(roles, LIMITED_ADMIN_ROLES)) return '/admin/kds';
+ return '/unauthorized';
+};