import React, { useMemo, useState } from "react"; import { Puzzle, Search, Filter, ArrowUpDown, ExternalLink, Crown, } from "lucide-react"; import { __ } from "../lib/i18n"; import { PageHeader } from "../components/common/PageHeader"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "../components/ui/card"; import { Badge } from "../components/ui/badge"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Select } from "../components/ui/select"; import { useModulesQuery, useToggleModule, ModuleDefinition, useBulkToggleModules, } from "../hooks/useModules"; import { usePermissions } from "../hooks/usePermissions"; import { PremiumUpgradeDialog } from "../components/modules/PremiumUpgradeDialog"; const Modules: React.FC = () => { const { can } = usePermissions(); const canManageModules = can("yatra_edit_trips"); const { data: modules = [], isLoading, error } = useModulesQuery(); const toggleMutation = useToggleModule(); const bulkToggleMutation = useBulkToggleModules(); const [searchTerm, setSearchTerm] = useState(""); const [categoryFilter, setCategoryFilter] = useState<"all" | string>("all"); const [sortOption, setSortOption] = useState< "name_asc" | "name_desc" | "status_enabled" | "status_disabled" >("name_asc"); const [selected, setSelected] = useState>(new Set()); const categories = useMemo(() => { const set = new Set(); modules.forEach((module) => { if (module.category) { set.add(module.category); } }); return Array.from(set).sort((a, b) => a.localeCompare(b)); }, [modules]); const filteredModules = useMemo(() => { let list = modules; if (searchTerm.trim()) { const term = searchTerm.toLowerCase(); list = list.filter( (module) => module.name.toLowerCase().includes(term) || (module.description?.toLowerCase().includes(term) ?? false) || (module.tags?.some((tag) => tag.toLowerCase().includes(term)) ?? false), ); } if (categoryFilter !== "all") { list = list.filter( (module) => (module.category || __("General", "yatra")) === categoryFilter, ); } const sorted = [...list]; sorted.sort((a, b) => { switch (sortOption) { case "name_desc": return b.name.localeCompare(a.name); case "status_enabled": return Number(b.enabled) - Number(a.enabled); case "status_disabled": return Number(a.enabled) - Number(b.enabled); case "name_asc": default: return a.name.localeCompare(b.name); } }); return sorted; }, [modules, searchTerm, categoryFilter, sortOption]); const moduleMap = useMemo(() => { const map = new Map(); modules.forEach((module) => { map.set(module.slug, module); }); return map; }, [modules]); const [premiumDialog, setPremiumDialog] = useState<{ open: boolean; module?: ModuleDefinition; }>({ open: false }); const handleToggle = (module: ModuleDefinition) => { if (module.is_core || !canManageModules) return; // Show premium dialog only if module requires Pro and is NOT available (Pro not active or module not in Pro) if (module.is_premium && !module.enabled && !module.is_available) { setPremiumDialog({ open: true, module }); return; } toggleMutation.mutate({ slug: module.slug, enabled: !module.enabled, name: module.name, }); }; const renderToggle = (module: ModuleDefinition) => { // Module is locked if it's premium, not enabled, and not available (Pro not active) const isLockedPremium = module.is_premium && !module.enabled && !module.is_available; // When Pro is active and module is available, allow toggling (user can enable/disable) const disabled = module.is_core || toggleMutation.isPending || !canManageModules; return ( ); }; const handleSelect = (slug: string) => { const module = moduleMap.get(slug); // Only show premium dialog if module is premium, not enabled, and not available if (module?.is_premium && !module.enabled && !module.is_available) { setPremiumDialog({ open: true, module }); return; } setSelected((prev) => { const next = new Set(prev); if (next.has(slug)) { next.delete(slug); } else { next.add(slug); } return next; }); }; const handleSelectAllVisible = () => { setSelected((prev) => { const next = new Set(prev); const allSelected = filteredModules.every((module) => next.has(module.slug), ); if (allSelected) { filteredModules.forEach((module) => next.delete(module.slug)); } else { filteredModules.forEach((module) => next.add(module.slug)); } return next; }); }; const bulkUpdate = (enabled: boolean) => { const items = Array.from(selected) .map((slug) => moduleMap.get(slug)) .filter((module): module is ModuleDefinition => !!module) // Only filter out premium modules that are NOT available (Pro not active) .filter( (module) => !(module.is_premium && enabled && !module.is_available), ); if (items.length === 0) { if (selected.size > 0 && enabled) { // Only show premium dialog for modules that are not available const premiumModules = Array.from(selected) .map((slug) => moduleMap.get(slug)) .filter((module) => module?.is_premium && !module?.is_available); if (premiumModules.length > 0) { setPremiumDialog({ open: true, module: premiumModules[0] }); } } return; } const payload = items.map((module) => ({ slug: module.slug, enabled, name: module.name, })); bulkToggleMutation.mutate(payload, { onSuccess: () => setSelected(new Set()), }); }; const isAllVisibleSelected = filteredModules.length > 0 && filteredModules.every((module) => selected.has(module.slug)); const selectedCount = selected.size; return (
window.location.reload()} className="flex items-center gap-2" > {__("Refresh Modules", "yatra")} } />
{__("Showing {count} modules", "yatra").replace( "{count}", String(filteredModules.length), )}
setSearchTerm(e.target.value)} placeholder={__("Search modules...", "yatra")} className="pl-9" />
{(searchTerm || categoryFilter !== "all" || sortOption !== "name_asc") && ( )}
{isLoading && (
{Array.from({ length: 3 }).map((_, idx) => (
))}
)} {error && ( {__("Failed to load modules", "yatra")} )} {!isLoading && !error && modules.length === 0 && ( {__("No modules available yet.", "yatra")} )} {!isLoading && !error && filteredModules.length > 0 && (
{filteredModules.map((module) => ( setPremiumDialog({ open: true, module }), onKeyDown: (event: React.KeyboardEvent) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); setPremiumDialog({ open: true, module }); } }, } : {})} >
handleSelect(module.slug)} /> {module.enabled && ( {__("Enabled", "yatra")} )}
{ event.stopPropagation(); handleSelect(module.slug); }} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleSelect(module.slug); } }} > {module.name} {module.is_core && ( {__("Core", "yatra")} )} {/* Only show Premium badge if module is premium AND not available (Pro not active) */} {module.is_premium && !module.is_available && ( {__("Premium", "yatra")} )} {module.description}

{module.category || __("General", "yatra")}

{renderToggle(module)}
{module.video_url && ( {__("Video", "yatra")} )}
{module.slug === "dynamic_form_field" && module.enabled && ( {__("Settings", "yatra")} )} {module.docs_url && ( {__("Docs", "yatra")} )}
))}
)} setPremiumDialog({ open: false })} />
); }; export default Modules;