Преглед изворни кода

feat: enhance navigation with shop selector and AI submenu

- Create ShopContext to persist selected shop globally across pages
- Add shop selector to sidebar header for better visibility
- Implement collapsible AI submenu with Configuration and Knowledge Base
- Auto-select first store if no shop is selected
- Save/load selected shop from localStorage
- Add submenu support using Collapsible component
- Reorganize navigation with AI Assistant section
- Knowledge Base now accessible from AI submenu
- Shop selector always visible and persists across page changes

BREAKING: Moved AI Config and Knowledge Base under AI Assistant submenu
Fszontagh пре 4 месеци
родитељ
комит
595c642e44

+ 7 - 4
shopcall.ai-main/src/App.tsx

@@ -6,6 +6,7 @@ import { TooltipProvider } from "@/components/ui/tooltip";
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 import { BrowserRouter, Routes, Route } from "react-router-dom";
 import { BrowserRouter, Routes, Route } from "react-router-dom";
 import { AuthProvider } from "./components/context/AuthContext";
 import { AuthProvider } from "./components/context/AuthContext";
+import { ShopProvider } from "./components/context/ShopContext";
 import PrivateRoute from "./components/PrivateRoute";
 import PrivateRoute from "./components/PrivateRoute";
 import { Loader2 } from "lucide-react";
 import { Loader2 } from "lucide-react";
 
 
@@ -54,8 +55,9 @@ const App = () => (
       <Sonner />
       <Sonner />
       <BrowserRouter>
       <BrowserRouter>
         <AuthProvider>
         <AuthProvider>
-          <Suspense fallback={<PageLoader />}>
-            <Routes>
+          <ShopProvider>
+            <Suspense fallback={<PageLoader />}>
+              <Routes>
               {/* Critical routes - no lazy loading */}
               {/* Critical routes - no lazy loading */}
               <Route path="/" element={<Index />} />
               <Route path="/" element={<Index />} />
               <Route path="/signup" element={<Signup />} />
               <Route path="/signup" element={<Signup />} />
@@ -84,8 +86,9 @@ const App = () => (
               {/*<Route path="/contact" element={<Contact />} />*/}
               {/*<Route path="/contact" element={<Contact />} />*/}
               {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
               {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
               <Route path="*" element={<NotFound />} />
               <Route path="*" element={<NotFound />} />
-            </Routes>
-          </Suspense>
+              </Routes>
+            </Suspense>
+          </ShopProvider>
         </AuthProvider>
         </AuthProvider>
       </BrowserRouter>
       </BrowserRouter>
     </TooltipProvider>
     </TooltipProvider>

+ 141 - 13
shopcall.ai-main/src/components/AppSidebar.tsx

@@ -1,4 +1,5 @@
 
 
+import { useState, useEffect } from "react";
 import {
 import {
   Sidebar,
   Sidebar,
   SidebarContent,
   SidebarContent,
@@ -7,20 +8,55 @@ import {
   SidebarMenu,
   SidebarMenu,
   SidebarMenuButton,
   SidebarMenuButton,
   SidebarMenuItem,
   SidebarMenuItem,
+  SidebarMenuSub,
+  SidebarMenuSubItem,
+  SidebarMenuSubButton,
   SidebarHeader,
   SidebarHeader,
   SidebarFooter,
   SidebarFooter,
 } from "@/components/ui/sidebar";
 } from "@/components/ui/sidebar";
 import { useNavigate } from "react-router-dom";
 import { useNavigate } from "react-router-dom";
-import { LayoutDashboard, Phone, BarChart3, Settings, CreditCard, Layers3, PhoneCall, LogOut } from "lucide-react";
+import { LayoutDashboard, Phone, BarChart3, Settings, CreditCard, Layers3, PhoneCall, LogOut, Brain, Database, Store, ChevronDown } from "lucide-react";
 import { useAuth } from "@/components/context/AuthContext";
 import { useAuth } from "@/components/context/AuthContext";
+import { useShop } from "@/components/context/ShopContext";
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { LanguageSelector } from "./LanguageSelector";
 import { LanguageSelector } from "./LanguageSelector";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
+import { API_URL } from "@/lib/config";
 
 
 export function AppSidebar() {
 export function AppSidebar() {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const navigate = useNavigate();
   const navigate = useNavigate();
   const currentPath = window.location.pathname;
   const currentPath = window.location.pathname;
   const { logout } = useAuth();
   const { logout } = useAuth();
+  const { selectedShop, setSelectedShop, stores, setStores } = useShop();
+  const [isAIMenuOpen, setIsAIMenuOpen] = useState(false);
+
+  // Fetch stores on component mount
+  useEffect(() => {
+    const fetchStores = async () => {
+      try {
+        const sessionData = localStorage.getItem('session_data');
+        if (!sessionData) return;
+
+        const session = JSON.parse(sessionData);
+        const response = await fetch(`${API_URL}/api/stores`, {
+          headers: {
+            'Authorization': `Bearer ${session.session.access_token}`,
+          }
+        });
+
+        if (response.ok) {
+          const data = await response.json();
+          setStores(data.stores || []);
+        }
+      } catch (error) {
+        console.error('Error fetching stores:', error);
+      }
+    };
+
+    fetchStores();
+  }, [setStores]);
 
 
   const menuItems = [
   const menuItems = [
     {
     {
@@ -56,11 +92,6 @@ export function AppSidebar() {
   ];
   ];
 
 
   const configItems = [
   const configItems = [
-    {
-      title: t('sidebar.aiConfig'),
-      icon: Settings,
-      url: "/ai-config",
-    },
     {
     {
       title: "Billing & Plan",
       title: "Billing & Plan",
       icon: CreditCard,
       icon: CreditCard,
@@ -68,15 +99,49 @@ export function AppSidebar() {
     },
     },
   ];
   ];
 
 
+  const handleShopChange = (shopId: string) => {
+    const shop = stores.find(s => s.id === shopId);
+    if (shop) {
+      setSelectedShop(shop);
+    }
+  };
+
   return (
   return (
     <Sidebar className="border-r border-slate-700/50 bg-slate-900 text-white">
     <Sidebar className="border-r border-slate-700/50 bg-slate-900 text-white">
-      <SidebarHeader className="p-6 bg-slate-900">
+      <SidebarHeader className="p-4 bg-slate-900 space-y-4">
         <div className="flex items-center gap-3">
         <div className="flex items-center gap-3">
           <img src="/uploads/e0ddbf09-622c-426a-851f-149776e300c0.png" alt="ShopCall.ai" className="w-8 h-8" />
           <img src="/uploads/e0ddbf09-622c-426a-851f-149776e300c0.png" alt="ShopCall.ai" className="w-8 h-8" />
           <span className="text-xl font-bold text-white">ShopCall.ai</span>
           <span className="text-xl font-bold text-white">ShopCall.ai</span>
         </div>
         </div>
+
+        {/* Shop Selector */}
+        {stores.length > 0 && (
+          <div className="pt-2">
+            <div className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 px-1">
+              {t('sidebar.activeShop') || 'Active Shop'}
+            </div>
+            <Select value={selectedShop?.id || ''} onValueChange={handleShopChange}>
+              <SelectTrigger className="bg-slate-800 border-slate-600 text-white hover:bg-slate-700">
+                <div className="flex items-center gap-2 overflow-hidden">
+                  <Store className="w-4 h-4 text-cyan-400 flex-shrink-0" />
+                  <SelectValue placeholder="Select shop" />
+                </div>
+              </SelectTrigger>
+              <SelectContent className="bg-slate-800 border-slate-600">
+                {stores.map((store) => (
+                  <SelectItem key={store.id} value={store.id} className="text-white hover:bg-slate-700">
+                    <div className="flex flex-col items-start">
+                      <div className="font-medium">{store.store_name || 'Unnamed Store'}</div>
+                      <div className="text-xs text-slate-400 capitalize">{store.platform_name}</div>
+                    </div>
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+          </div>
+        )}
       </SidebarHeader>
       </SidebarHeader>
-      
+
       <SidebarContent className="px-3 bg-slate-900">
       <SidebarContent className="px-3 bg-slate-900">
         <SidebarGroup>
         <SidebarGroup>
           <SidebarGroupContent>
           <SidebarGroupContent>
@@ -85,8 +150,8 @@ export function AppSidebar() {
                 const isActive = currentPath === item.url;
                 const isActive = currentPath === item.url;
                 return (
                 return (
                   <SidebarMenuItem key={item.title}>
                   <SidebarMenuItem key={item.title}>
-                    <SidebarMenuButton 
-                      asChild 
+                    <SidebarMenuButton
+                      asChild
                       className={`w-full justify-start text-slate-300 hover:text-white hover:bg-slate-800/50 ${
                       className={`w-full justify-start text-slate-300 hover:text-white hover:bg-slate-800/50 ${
                         isActive ? 'bg-cyan-500 text-white hover:bg-cyan-600' : ''
                         isActive ? 'bg-cyan-500 text-white hover:bg-cyan-600' : ''
                       }`}
                       }`}
@@ -103,7 +168,70 @@ export function AppSidebar() {
           </SidebarGroupContent>
           </SidebarGroupContent>
         </SidebarGroup>
         </SidebarGroup>
 
 
-        <SidebarGroup className="mt-8">
+        {/* AI Assistant Section */}
+        <SidebarGroup className="mt-4">
+          <div className="px-3 py-2 text-xs font-semibold text-slate-500 uppercase tracking-wider">
+            {t('sidebar.aiAssistant') || 'AI Assistant'}
+          </div>
+          <SidebarGroupContent>
+            <SidebarMenu>
+              <Collapsible open={isAIMenuOpen} onOpenChange={setIsAIMenuOpen}>
+                <SidebarMenuItem>
+                  <CollapsibleTrigger asChild>
+                    <SidebarMenuButton className="w-full justify-start text-slate-300 hover:text-white hover:bg-slate-800/50">
+                      <div className="flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer w-full">
+                        <Brain className="w-4 h-4" />
+                        <span className="flex-1">{t('sidebar.aiMenu') || 'AI Configuration'}</span>
+                        <ChevronDown className={`w-4 h-4 transition-transform ${isAIMenuOpen ? 'rotate-180' : ''}`} />
+                      </div>
+                    </SidebarMenuButton>
+                  </CollapsibleTrigger>
+                  <CollapsibleContent>
+                    <SidebarMenuSub>
+                      <SidebarMenuSubItem>
+                        <SidebarMenuSubButton
+                          asChild
+                          className={`text-slate-300 hover:text-white hover:bg-slate-800/50 ${
+                            currentPath === '/ai-config' ? 'bg-cyan-500/20 text-cyan-400' : ''
+                          }`}
+                        >
+                          <a onClick={() => navigate('/ai-config')} className="flex items-center gap-2 cursor-pointer">
+                            <Settings className="w-3 h-3" />
+                            <span>{t('sidebar.configuration') || 'Configuration'}</span>
+                          </a>
+                        </SidebarMenuSubButton>
+                      </SidebarMenuSubItem>
+                      <SidebarMenuSubItem>
+                        <SidebarMenuSubButton
+                          asChild
+                          className={`text-slate-300 hover:text-white hover:bg-slate-800/50 ${
+                            currentPath === '/manage-store-data' ? 'bg-cyan-500/20 text-cyan-400' : ''
+                          }`}
+                        >
+                          <a
+                            onClick={() => {
+                              if (selectedShop) {
+                                navigate(`/manage-store-data?shop=${selectedShop.id}`);
+                              } else {
+                                navigate('/manage-store-data');
+                              }
+                            }}
+                            className="flex items-center gap-2 cursor-pointer"
+                          >
+                            <Database className="w-3 h-3" />
+                            <span>{t('sidebar.knowledgeBase') || 'Knowledge Base'}</span>
+                          </a>
+                        </SidebarMenuSubButton>
+                      </SidebarMenuSubItem>
+                    </SidebarMenuSub>
+                  </CollapsibleContent>
+                </SidebarMenuItem>
+              </Collapsible>
+            </SidebarMenu>
+          </SidebarGroupContent>
+        </SidebarGroup>
+
+        <SidebarGroup className="mt-4">
           <div className="px-3 py-2 text-xs font-semibold text-slate-500 uppercase tracking-wider">
           <div className="px-3 py-2 text-xs font-semibold text-slate-500 uppercase tracking-wider">
             {t('sidebar.configuration')}
             {t('sidebar.configuration')}
           </div>
           </div>
@@ -113,8 +241,8 @@ export function AppSidebar() {
                 const isActive = currentPath === item.url;
                 const isActive = currentPath === item.url;
                 return (
                 return (
                   <SidebarMenuItem key={item.title}>
                   <SidebarMenuItem key={item.title}>
-                    <SidebarMenuButton 
-                      asChild 
+                    <SidebarMenuButton
+                      asChild
                       className={`w-full justify-start text-slate-300 hover:text-white hover:bg-slate-800/50 ${
                       className={`w-full justify-start text-slate-300 hover:text-white hover:bg-slate-800/50 ${
                         isActive ? 'bg-cyan-500 text-white hover:bg-cyan-600' : ''
                         isActive ? 'bg-cyan-500 text-white hover:bg-cyan-600' : ''
                       }`}
                       }`}

+ 65 - 0
shopcall.ai-main/src/components/context/ShopContext.tsx

@@ -0,0 +1,65 @@
+import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
+
+interface StoreData {
+  id: string;
+  store_name: string | null;
+  platform_name: string;
+  store_url: string | null;
+}
+
+interface ShopContextType {
+  selectedShop: StoreData | null;
+  setSelectedShop: (shop: StoreData | null) => void;
+  stores: StoreData[];
+  setStores: (stores: StoreData[]) => void;
+  isLoading: boolean;
+}
+
+const ShopContext = createContext<ShopContextType | undefined>(undefined);
+
+export const ShopProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
+  const [selectedShop, setSelectedShopState] = useState<StoreData | null>(null);
+  const [stores, setStores] = useState<StoreData[]>([]);
+  const [isLoading, setIsLoading] = useState(true);
+
+  // Load selected shop from localStorage on mount
+  useEffect(() => {
+    const savedShopId = localStorage.getItem('selected_shop_id');
+    if (savedShopId && stores.length > 0) {
+      const shop = stores.find(s => s.id === savedShopId);
+      if (shop) {
+        setSelectedShopState(shop);
+      } else if (stores.length > 0) {
+        // If saved shop not found, select first available
+        setSelectedShopState(stores[0]);
+      }
+    } else if (stores.length > 0 && !selectedShop) {
+      // Auto-select first store if none selected
+      setSelectedShopState(stores[0]);
+    }
+    setIsLoading(false);
+  }, [stores]);
+
+  const setSelectedShop = (shop: StoreData | null) => {
+    setSelectedShopState(shop);
+    if (shop) {
+      localStorage.setItem('selected_shop_id', shop.id);
+    } else {
+      localStorage.removeItem('selected_shop_id');
+    }
+  };
+
+  return (
+    <ShopContext.Provider value={{ selectedShop, setSelectedShop, stores, setStores, isLoading }}>
+      {children}
+    </ShopContext.Provider>
+  );
+};
+
+export const useShop = (): ShopContextType => {
+  const context = useContext(ShopContext);
+  if (context === undefined) {
+    throw new Error('useShop must be used within a ShopProvider');
+  }
+  return context;
+};