|
|
@@ -0,0 +1,855 @@
|
|
|
+import { useState, useEffect } from "react";
|
|
|
+import { useSearchParams } from "react-router-dom";
|
|
|
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
+import { Button } from "@/components/ui/button";
|
|
|
+import { Input } from "@/components/ui/input";
|
|
|
+import { Label } from "@/components/ui/label";
|
|
|
+import { Switch } from "@/components/ui/switch";
|
|
|
+import { Checkbox } from "@/components/ui/checkbox";
|
|
|
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
|
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
|
+import { Badge } from "@/components/ui/badge";
|
|
|
+import {
|
|
|
+ AlertDialog,
|
|
|
+ AlertDialogAction,
|
|
|
+ AlertDialogCancel,
|
|
|
+ AlertDialogContent,
|
|
|
+ AlertDialogDescription,
|
|
|
+ AlertDialogFooter,
|
|
|
+ AlertDialogHeader,
|
|
|
+ AlertDialogTitle,
|
|
|
+} from "@/components/ui/alert-dialog";
|
|
|
+import { Loader2, Search, Package, ShoppingCart, Users, ChevronLeft, ChevronRight } from "lucide-react";
|
|
|
+import { API_URL } from "@/lib/config";
|
|
|
+import { useToast } from "@/hooks/use-toast";
|
|
|
+
|
|
|
+interface StoreData {
|
|
|
+ id: string;
|
|
|
+ store_name: string | null;
|
|
|
+ platform_name: string;
|
|
|
+ store_url: string | null;
|
|
|
+}
|
|
|
+
|
|
|
+interface Product {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ sku: string;
|
|
|
+ price: string;
|
|
|
+ currency: string;
|
|
|
+ enabled_in_context: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+interface Order {
|
|
|
+ id: string;
|
|
|
+ order_number: string;
|
|
|
+ customer_name: string;
|
|
|
+ total: string;
|
|
|
+ currency: string;
|
|
|
+ enabled_in_context: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+interface Customer {
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ email: string;
|
|
|
+ orders_count: number;
|
|
|
+ enabled_in_context: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+type DataItem = Product | Order | Customer;
|
|
|
+
|
|
|
+interface DataResponse {
|
|
|
+ success: boolean;
|
|
|
+ data: DataItem[];
|
|
|
+ total: number;
|
|
|
+ enabled_count: number;
|
|
|
+ disabled_count: number;
|
|
|
+}
|
|
|
+
|
|
|
+export function ManageStoreDataContent() {
|
|
|
+ const [searchParams] = useSearchParams();
|
|
|
+ const { toast } = useToast();
|
|
|
+
|
|
|
+ const [stores, setStores] = useState<StoreData[]>([]);
|
|
|
+ const [selectedStore, setSelectedStore] = useState<StoreData | null>(null);
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
+ const [activeTab, setActiveTab] = useState<"products" | "orders" | "customers">("products");
|
|
|
+
|
|
|
+ // Filter and search state
|
|
|
+ const [searchQuery, setSearchQuery] = useState("");
|
|
|
+ const [statusFilter, setStatusFilter] = useState<"all" | "enabled" | "disabled">("all");
|
|
|
+ const [platformFilter, setPlatformFilter] = useState<string>("all");
|
|
|
+
|
|
|
+ // Data state
|
|
|
+ const [data, setData] = useState<DataItem[]>([]);
|
|
|
+ const [dataLoading, setDataLoading] = useState(false);
|
|
|
+ const [totalCount, setTotalCount] = useState(0);
|
|
|
+ const [enabledCount, setEnabledCount] = useState(0);
|
|
|
+ const [disabledCount, setDisabledCount] = useState(0);
|
|
|
+
|
|
|
+ // Pagination state
|
|
|
+ const [page, setPage] = useState(1);
|
|
|
+ const [pageSize, setPageSize] = useState(25);
|
|
|
+
|
|
|
+ // Selection state
|
|
|
+ const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
|
|
+ const [selectAll, setSelectAll] = useState(false);
|
|
|
+
|
|
|
+ // Confirmation dialog state
|
|
|
+ const [confirmDialog, setConfirmDialog] = useState<{
|
|
|
+ open: boolean;
|
|
|
+ title: string;
|
|
|
+ description: string;
|
|
|
+ action: () => void;
|
|
|
+ }>({
|
|
|
+ open: false,
|
|
|
+ title: "",
|
|
|
+ description: "",
|
|
|
+ action: () => {},
|
|
|
+ });
|
|
|
+
|
|
|
+ // Fetch stores on mount
|
|
|
+ useEffect(() => {
|
|
|
+ const fetchStores = async () => {
|
|
|
+ try {
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
+ if (!sessionData) {
|
|
|
+ throw new Error('No session data found');
|
|
|
+ }
|
|
|
+
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
+ const response = await fetch(`${API_URL}/api/stores`, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Failed to fetch stores');
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+ if (result.success) {
|
|
|
+ const storesList = result.stores || [];
|
|
|
+ setStores(storesList);
|
|
|
+
|
|
|
+ // Select store from URL parameter or first store
|
|
|
+ const shopId = searchParams.get('shop');
|
|
|
+ if (shopId) {
|
|
|
+ const store = storesList.find((s: StoreData) => s.id === shopId);
|
|
|
+ if (store) {
|
|
|
+ setSelectedStore(store);
|
|
|
+ } else if (storesList.length > 0) {
|
|
|
+ setSelectedStore(storesList[0]);
|
|
|
+ }
|
|
|
+ } else if (storesList.length > 0) {
|
|
|
+ setSelectedStore(storesList[0]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error fetching stores:', error);
|
|
|
+ toast({
|
|
|
+ title: "Error",
|
|
|
+ description: "Failed to load stores. Please try again.",
|
|
|
+ variant: "destructive"
|
|
|
+ });
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ fetchStores();
|
|
|
+ }, [searchParams, toast]);
|
|
|
+
|
|
|
+ // Fetch data when tab, store, or filters change
|
|
|
+ useEffect(() => {
|
|
|
+ if (selectedStore) {
|
|
|
+ fetchData();
|
|
|
+ }
|
|
|
+ }, [selectedStore, activeTab, page, pageSize, searchQuery, statusFilter]);
|
|
|
+
|
|
|
+ const fetchData = async () => {
|
|
|
+ if (!selectedStore) return;
|
|
|
+
|
|
|
+ setDataLoading(true);
|
|
|
+ try {
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
+ if (!sessionData) {
|
|
|
+ throw new Error('No session data found');
|
|
|
+ }
|
|
|
+
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
+
|
|
|
+ // Build query parameters
|
|
|
+ const params = new URLSearchParams({
|
|
|
+ store_id: selectedStore.id,
|
|
|
+ page: page.toString(),
|
|
|
+ limit: pageSize.toString(),
|
|
|
+ });
|
|
|
+
|
|
|
+ if (searchQuery) {
|
|
|
+ params.append('search', searchQuery);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (statusFilter !== 'all') {
|
|
|
+ params.append('enabled', statusFilter === 'enabled' ? 'true' : 'false');
|
|
|
+ }
|
|
|
+
|
|
|
+ const endpoint = `/api/store-data/${activeTab}?${params.toString()}`;
|
|
|
+ const response = await fetch(`${API_URL}${endpoint}`, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Failed to fetch data');
|
|
|
+ }
|
|
|
+
|
|
|
+ const result: DataResponse = await response.json();
|
|
|
+ if (result.success) {
|
|
|
+ setData(result.data);
|
|
|
+ setTotalCount(result.total);
|
|
|
+ setEnabledCount(result.enabled_count);
|
|
|
+ setDisabledCount(result.disabled_count);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error fetching data:', error);
|
|
|
+ toast({
|
|
|
+ title: "Error",
|
|
|
+ description: `Failed to load ${activeTab}. Please try again.`,
|
|
|
+ variant: "destructive"
|
|
|
+ });
|
|
|
+ } finally {
|
|
|
+ setDataLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleToggleItem = async (itemId: string, currentEnabled: boolean) => {
|
|
|
+ if (!selectedStore) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
+ if (!sessionData) {
|
|
|
+ throw new Error('No session data found');
|
|
|
+ }
|
|
|
+
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
+
|
|
|
+ // Optimistic update
|
|
|
+ setData(prevData =>
|
|
|
+ prevData.map(item =>
|
|
|
+ item.id === itemId
|
|
|
+ ? { ...item, enabled_in_context: !currentEnabled }
|
|
|
+ : item
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+ const response = await fetch(`${API_URL}/api/store-data/${activeTab}/${itemId}/toggle`, {
|
|
|
+ method: 'PUT',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ store_id: selectedStore.id,
|
|
|
+ enabled: !currentEnabled
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ // Revert on error
|
|
|
+ setData(prevData =>
|
|
|
+ prevData.map(item =>
|
|
|
+ item.id === itemId
|
|
|
+ ? { ...item, enabled_in_context: currentEnabled }
|
|
|
+ : item
|
|
|
+ )
|
|
|
+ );
|
|
|
+ throw new Error('Failed to update item');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update counts
|
|
|
+ if (currentEnabled) {
|
|
|
+ setEnabledCount(prev => prev - 1);
|
|
|
+ setDisabledCount(prev => prev + 1);
|
|
|
+ } else {
|
|
|
+ setEnabledCount(prev => prev + 1);
|
|
|
+ setDisabledCount(prev => prev - 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ toast({
|
|
|
+ title: "Success",
|
|
|
+ description: `Item ${!currentEnabled ? 'enabled' : 'disabled'} successfully.`,
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error toggling item:', error);
|
|
|
+ toast({
|
|
|
+ title: "Error",
|
|
|
+ description: "Failed to update item. Please try again.",
|
|
|
+ variant: "destructive"
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleBulkAction = async (enable: boolean) => {
|
|
|
+ if (!selectedStore || selectedItems.size === 0) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
+ if (!sessionData) {
|
|
|
+ throw new Error('No session data found');
|
|
|
+ }
|
|
|
+
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
+
|
|
|
+ const response = await fetch(`${API_URL}/api/store-data/${activeTab}/bulk-toggle`, {
|
|
|
+ method: 'PUT',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ store_id: selectedStore.id,
|
|
|
+ item_ids: Array.from(selectedItems),
|
|
|
+ enabled: enable
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Failed to update items');
|
|
|
+ }
|
|
|
+
|
|
|
+ toast({
|
|
|
+ title: "Success",
|
|
|
+ description: `${selectedItems.size} items ${enable ? 'enabled' : 'disabled'} successfully.`,
|
|
|
+ });
|
|
|
+
|
|
|
+ setSelectedItems(new Set());
|
|
|
+ setSelectAll(false);
|
|
|
+ fetchData();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error with bulk action:', error);
|
|
|
+ toast({
|
|
|
+ title: "Error",
|
|
|
+ description: "Failed to update items. Please try again.",
|
|
|
+ variant: "destructive"
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleEnableAll = async () => {
|
|
|
+ if (!selectedStore) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
+ if (!sessionData) {
|
|
|
+ throw new Error('No session data found');
|
|
|
+ }
|
|
|
+
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
+
|
|
|
+ const response = await fetch(`${API_URL}/api/store-data/${activeTab}/enable-all`, {
|
|
|
+ method: 'PUT',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ store_id: selectedStore.id
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Failed to enable all items');
|
|
|
+ }
|
|
|
+
|
|
|
+ toast({
|
|
|
+ title: "Success",
|
|
|
+ description: `All ${activeTab} enabled successfully.`,
|
|
|
+ });
|
|
|
+
|
|
|
+ fetchData();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error enabling all:', error);
|
|
|
+ toast({
|
|
|
+ title: "Error",
|
|
|
+ description: "Failed to enable all items. Please try again.",
|
|
|
+ variant: "destructive"
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDisableAll = async () => {
|
|
|
+ if (!selectedStore) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
+ if (!sessionData) {
|
|
|
+ throw new Error('No session data found');
|
|
|
+ }
|
|
|
+
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
+
|
|
|
+ const response = await fetch(`${API_URL}/api/store-data/${activeTab}/disable-all`, {
|
|
|
+ method: 'PUT',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ store_id: selectedStore.id
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Failed to disable all items');
|
|
|
+ }
|
|
|
+
|
|
|
+ toast({
|
|
|
+ title: "Success",
|
|
|
+ description: `All ${activeTab} disabled successfully.`,
|
|
|
+ });
|
|
|
+
|
|
|
+ fetchData();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error disabling all:', error);
|
|
|
+ toast({
|
|
|
+ title: "Error",
|
|
|
+ description: "Failed to disable all items. Please try again.",
|
|
|
+ variant: "destructive"
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSelectAll = (checked: boolean) => {
|
|
|
+ setSelectAll(checked);
|
|
|
+ if (checked) {
|
|
|
+ setSelectedItems(new Set(data.map(item => item.id)));
|
|
|
+ } else {
|
|
|
+ setSelectedItems(new Set());
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSelectItem = (itemId: string, checked: boolean) => {
|
|
|
+ const newSelected = new Set(selectedItems);
|
|
|
+ if (checked) {
|
|
|
+ newSelected.add(itemId);
|
|
|
+ } else {
|
|
|
+ newSelected.delete(itemId);
|
|
|
+ }
|
|
|
+ setSelectedItems(newSelected);
|
|
|
+ setSelectAll(newSelected.size === data.length);
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderProductRow = (product: Product) => (
|
|
|
+ <TableRow key={product.id}>
|
|
|
+ <TableCell>
|
|
|
+ <Checkbox
|
|
|
+ checked={selectedItems.has(product.id)}
|
|
|
+ onCheckedChange={(checked) => handleSelectItem(product.id, checked as boolean)}
|
|
|
+ />
|
|
|
+ </TableCell>
|
|
|
+ <TableCell className="text-white font-medium">{product.name}</TableCell>
|
|
|
+ <TableCell className="text-slate-400">{product.sku || 'N/A'}</TableCell>
|
|
|
+ <TableCell className="text-slate-300">
|
|
|
+ {product.price} {product.currency}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Switch
|
|
|
+ checked={product.enabled_in_context}
|
|
|
+ onCheckedChange={() => handleToggleItem(product.id, product.enabled_in_context)}
|
|
|
+ className="data-[state=checked]:bg-cyan-500"
|
|
|
+ />
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ );
|
|
|
+
|
|
|
+ const renderOrderRow = (order: Order) => (
|
|
|
+ <TableRow key={order.id}>
|
|
|
+ <TableCell>
|
|
|
+ <Checkbox
|
|
|
+ checked={selectedItems.has(order.id)}
|
|
|
+ onCheckedChange={(checked) => handleSelectItem(order.id, checked as boolean)}
|
|
|
+ />
|
|
|
+ </TableCell>
|
|
|
+ <TableCell className="text-white font-medium">{order.order_number}</TableCell>
|
|
|
+ <TableCell className="text-slate-400">{order.customer_name}</TableCell>
|
|
|
+ <TableCell className="text-slate-300">
|
|
|
+ {order.total} {order.currency}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Switch
|
|
|
+ checked={order.enabled_in_context}
|
|
|
+ onCheckedChange={() => handleToggleItem(order.id, order.enabled_in_context)}
|
|
|
+ className="data-[state=checked]:bg-cyan-500"
|
|
|
+ />
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ );
|
|
|
+
|
|
|
+ const renderCustomerRow = (customer: Customer) => (
|
|
|
+ <TableRow key={customer.id}>
|
|
|
+ <TableCell>
|
|
|
+ <Checkbox
|
|
|
+ checked={selectedItems.has(customer.id)}
|
|
|
+ onCheckedChange={(checked) => handleSelectItem(customer.id, checked as boolean)}
|
|
|
+ />
|
|
|
+ </TableCell>
|
|
|
+ <TableCell className="text-white font-medium">{customer.name}</TableCell>
|
|
|
+ <TableCell className="text-slate-400">{customer.email}</TableCell>
|
|
|
+ <TableCell className="text-slate-300">{customer.orders_count}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Switch
|
|
|
+ checked={customer.enabled_in_context}
|
|
|
+ onCheckedChange={() => handleToggleItem(customer.id, customer.enabled_in_context)}
|
|
|
+ className="data-[state=checked]:bg-cyan-500"
|
|
|
+ />
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ );
|
|
|
+
|
|
|
+ if (loading) {
|
|
|
+ return (
|
|
|
+ <div className="flex-1 flex items-center justify-center min-h-screen bg-slate-900">
|
|
|
+ <Loader2 className="w-8 h-8 text-cyan-500 animate-spin" />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (stores.length === 0) {
|
|
|
+ return (
|
|
|
+ <div className="flex-1 space-y-6 p-8 bg-slate-900">
|
|
|
+ <div className="flex items-center justify-center min-h-[60vh]">
|
|
|
+ <div className="text-center">
|
|
|
+ <Package className="w-16 h-16 text-slate-600 mx-auto mb-4" />
|
|
|
+ <h3 className="text-lg font-semibold text-white mb-2">No Stores Connected</h3>
|
|
|
+ <p className="text-slate-400 mb-6">
|
|
|
+ Please connect a store first before managing data.
|
|
|
+ </p>
|
|
|
+ <Button
|
|
|
+ className="bg-cyan-500 hover:bg-cyan-600 text-white"
|
|
|
+ onClick={() => window.location.href = '/webshops'}
|
|
|
+ >
|
|
|
+ Go to Webshops
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ const totalPages = Math.ceil(totalCount / pageSize);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="flex-1 space-y-6 p-8 bg-slate-900">
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div>
|
|
|
+ <h2 className="text-3xl font-bold tracking-tight text-white">Manage Store Data</h2>
|
|
|
+ <p className="text-slate-400">Control which data is included in AI context</p>
|
|
|
+ </div>
|
|
|
+ {stores.length > 1 && (
|
|
|
+ <Select value={selectedStore?.id} onValueChange={(value) => {
|
|
|
+ const store = stores.find(s => s.id === value);
|
|
|
+ if (store) setSelectedStore(store);
|
|
|
+ }}>
|
|
|
+ <SelectTrigger className="w-[300px] bg-slate-700 border-slate-600 text-white">
|
|
|
+ <SelectValue />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent className="bg-slate-700 border-slate-600">
|
|
|
+ {stores.map((store) => (
|
|
|
+ <SelectItem key={store.id} value={store.id}>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-white">{store.store_name || 'Unnamed Store'}</span>
|
|
|
+ <span className="text-slate-400 text-sm capitalize">({store.platform_name})</span>
|
|
|
+ </div>
|
|
|
+ </SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Card className="bg-slate-800 border-slate-700">
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle className="text-white">Data Management</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <Tabs value={activeTab} onValueChange={(value) => {
|
|
|
+ setActiveTab(value as "products" | "orders" | "customers");
|
|
|
+ setPage(1);
|
|
|
+ setSelectedItems(new Set());
|
|
|
+ setSelectAll(false);
|
|
|
+ }}>
|
|
|
+ <TabsList className="bg-slate-700 mb-6">
|
|
|
+ <TabsTrigger value="products" className="data-[state=active]:bg-slate-600 data-[state=active]:text-white">
|
|
|
+ <Package className="w-4 h-4 mr-2" />
|
|
|
+ Products ({enabledCount}/{totalCount})
|
|
|
+ </TabsTrigger>
|
|
|
+ <TabsTrigger value="orders" className="data-[state=active]:bg-slate-600 data-[state=active]:text-white">
|
|
|
+ <ShoppingCart className="w-4 h-4 mr-2" />
|
|
|
+ Orders ({enabledCount}/{totalCount})
|
|
|
+ </TabsTrigger>
|
|
|
+ <TabsTrigger value="customers" className="data-[state=active]:bg-slate-600 data-[state=active]:text-white">
|
|
|
+ <Users className="w-4 h-4 mr-2" />
|
|
|
+ Customers ({enabledCount}/{totalCount})
|
|
|
+ </TabsTrigger>
|
|
|
+ </TabsList>
|
|
|
+
|
|
|
+ {/* Search and Filter Bar */}
|
|
|
+ <div className="flex gap-4 mb-6">
|
|
|
+ <div className="flex-1 relative">
|
|
|
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-slate-400" />
|
|
|
+ <Input
|
|
|
+ placeholder={`Search ${activeTab}...`}
|
|
|
+ value={searchQuery}
|
|
|
+ onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
+ className="pl-10 bg-slate-700 border-slate-600 text-white"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <Select value={statusFilter} onValueChange={(value) => setStatusFilter(value as "all" | "enabled" | "disabled")}>
|
|
|
+ <SelectTrigger className="w-[180px] bg-slate-700 border-slate-600 text-white">
|
|
|
+ <SelectValue />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent className="bg-slate-700 border-slate-600">
|
|
|
+ <SelectItem value="all">All Items</SelectItem>
|
|
|
+ <SelectItem value="enabled">Enabled Only</SelectItem>
|
|
|
+ <SelectItem value="disabled">Disabled Only</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Bulk Actions */}
|
|
|
+ {selectedItems.size > 0 && (
|
|
|
+ <div className="flex gap-2 mb-4 p-3 bg-slate-700/50 rounded-lg">
|
|
|
+ <span className="text-white mr-4">{selectedItems.size} items selected</span>
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ variant="outline"
|
|
|
+ onClick={() => handleBulkAction(true)}
|
|
|
+ className="border-cyan-500 text-cyan-500 hover:bg-cyan-500 hover:text-white"
|
|
|
+ >
|
|
|
+ Enable Selected
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ variant="outline"
|
|
|
+ onClick={() => handleBulkAction(false)}
|
|
|
+ className="border-slate-400 text-slate-400 hover:bg-slate-600"
|
|
|
+ >
|
|
|
+ Disable Selected
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <div className="flex gap-2 mb-4">
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ variant="outline"
|
|
|
+ onClick={() => setConfirmDialog({
|
|
|
+ open: true,
|
|
|
+ title: `Enable All ${activeTab}`,
|
|
|
+ description: `Are you sure you want to enable all ${activeTab} for AI context?`,
|
|
|
+ action: handleEnableAll
|
|
|
+ })}
|
|
|
+ className="border-cyan-500 text-cyan-500 hover:bg-cyan-500 hover:text-white"
|
|
|
+ >
|
|
|
+ Enable All
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ variant="outline"
|
|
|
+ onClick={() => setConfirmDialog({
|
|
|
+ open: true,
|
|
|
+ title: `Disable All ${activeTab}`,
|
|
|
+ description: `Are you sure you want to disable all ${activeTab} from AI context?`,
|
|
|
+ action: handleDisableAll
|
|
|
+ })}
|
|
|
+ className="border-slate-400 text-slate-400 hover:bg-slate-600"
|
|
|
+ >
|
|
|
+ Disable All
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <TabsContent value="products">
|
|
|
+ {dataLoading ? (
|
|
|
+ <div className="flex justify-center py-8">
|
|
|
+ <Loader2 className="w-6 h-6 text-cyan-500 animate-spin" />
|
|
|
+ </div>
|
|
|
+ ) : data.length === 0 ? (
|
|
|
+ <div className="text-center py-8 text-slate-400">
|
|
|
+ No products found
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="rounded-md border border-slate-700">
|
|
|
+ <Table>
|
|
|
+ <TableHeader className="bg-slate-700/50">
|
|
|
+ <TableRow>
|
|
|
+ <TableHead className="w-12">
|
|
|
+ <Checkbox
|
|
|
+ checked={selectAll}
|
|
|
+ onCheckedChange={handleSelectAll}
|
|
|
+ />
|
|
|
+ </TableHead>
|
|
|
+ <TableHead className="text-slate-300">Name</TableHead>
|
|
|
+ <TableHead className="text-slate-300">SKU</TableHead>
|
|
|
+ <TableHead className="text-slate-300">Price</TableHead>
|
|
|
+ <TableHead className="text-slate-300">Enabled</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody className="bg-slate-800">
|
|
|
+ {data.map(item => renderProductRow(item as Product))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </TabsContent>
|
|
|
+
|
|
|
+ <TabsContent value="orders">
|
|
|
+ {dataLoading ? (
|
|
|
+ <div className="flex justify-center py-8">
|
|
|
+ <Loader2 className="w-6 h-6 text-cyan-500 animate-spin" />
|
|
|
+ </div>
|
|
|
+ ) : data.length === 0 ? (
|
|
|
+ <div className="text-center py-8 text-slate-400">
|
|
|
+ No orders found
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="rounded-md border border-slate-700">
|
|
|
+ <Table>
|
|
|
+ <TableHeader className="bg-slate-700/50">
|
|
|
+ <TableRow>
|
|
|
+ <TableHead className="w-12">
|
|
|
+ <Checkbox
|
|
|
+ checked={selectAll}
|
|
|
+ onCheckedChange={handleSelectAll}
|
|
|
+ />
|
|
|
+ </TableHead>
|
|
|
+ <TableHead className="text-slate-300">Order Number</TableHead>
|
|
|
+ <TableHead className="text-slate-300">Customer</TableHead>
|
|
|
+ <TableHead className="text-slate-300">Total</TableHead>
|
|
|
+ <TableHead className="text-slate-300">Enabled</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody className="bg-slate-800">
|
|
|
+ {data.map(item => renderOrderRow(item as Order))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </TabsContent>
|
|
|
+
|
|
|
+ <TabsContent value="customers">
|
|
|
+ {dataLoading ? (
|
|
|
+ <div className="flex justify-center py-8">
|
|
|
+ <Loader2 className="w-6 h-6 text-cyan-500 animate-spin" />
|
|
|
+ </div>
|
|
|
+ ) : data.length === 0 ? (
|
|
|
+ <div className="text-center py-8 text-slate-400">
|
|
|
+ No customers found
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="rounded-md border border-slate-700">
|
|
|
+ <Table>
|
|
|
+ <TableHeader className="bg-slate-700/50">
|
|
|
+ <TableRow>
|
|
|
+ <TableHead className="w-12">
|
|
|
+ <Checkbox
|
|
|
+ checked={selectAll}
|
|
|
+ onCheckedChange={handleSelectAll}
|
|
|
+ />
|
|
|
+ </TableHead>
|
|
|
+ <TableHead className="text-slate-300">Name</TableHead>
|
|
|
+ <TableHead className="text-slate-300">Email</TableHead>
|
|
|
+ <TableHead className="text-slate-300">Orders</TableHead>
|
|
|
+ <TableHead className="text-slate-300">Enabled</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody className="bg-slate-800">
|
|
|
+ {data.map(item => renderCustomerRow(item as Customer))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </TabsContent>
|
|
|
+ </Tabs>
|
|
|
+
|
|
|
+ {/* Pagination */}
|
|
|
+ {totalCount > 0 && (
|
|
|
+ <div className="flex items-center justify-between mt-6">
|
|
|
+ <div className="flex items-center gap-4">
|
|
|
+ <Label className="text-slate-300">Items per page:</Label>
|
|
|
+ <Select value={pageSize.toString()} onValueChange={(value) => {
|
|
|
+ setPageSize(Number(value));
|
|
|
+ setPage(1);
|
|
|
+ }}>
|
|
|
+ <SelectTrigger className="w-[100px] bg-slate-700 border-slate-600 text-white">
|
|
|
+ <SelectValue />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent className="bg-slate-700 border-slate-600">
|
|
|
+ <SelectItem value="25">25</SelectItem>
|
|
|
+ <SelectItem value="50">50</SelectItem>
|
|
|
+ <SelectItem value="100">100</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-slate-400 text-sm">
|
|
|
+ Page {page} of {totalPages} ({totalCount} total)
|
|
|
+ </span>
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ variant="outline"
|
|
|
+ onClick={() => setPage(prev => Math.max(1, prev - 1))}
|
|
|
+ disabled={page === 1}
|
|
|
+ className="border-slate-600 text-white"
|
|
|
+ >
|
|
|
+ <ChevronLeft className="w-4 h-4" />
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ variant="outline"
|
|
|
+ onClick={() => setPage(prev => Math.min(totalPages, prev + 1))}
|
|
|
+ disabled={page === totalPages}
|
|
|
+ className="border-slate-600 text-white"
|
|
|
+ >
|
|
|
+ <ChevronRight className="w-4 h-4" />
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* Confirmation Dialog */}
|
|
|
+ <AlertDialog open={confirmDialog.open} onOpenChange={(open) => setConfirmDialog({ ...confirmDialog, open })}>
|
|
|
+ <AlertDialogContent className="bg-slate-800 border-slate-700">
|
|
|
+ <AlertDialogHeader>
|
|
|
+ <AlertDialogTitle className="text-white">{confirmDialog.title}</AlertDialogTitle>
|
|
|
+ <AlertDialogDescription className="text-slate-400">
|
|
|
+ {confirmDialog.description}
|
|
|
+ </AlertDialogDescription>
|
|
|
+ </AlertDialogHeader>
|
|
|
+ <AlertDialogFooter>
|
|
|
+ <AlertDialogCancel className="bg-slate-700 text-white border-slate-600">
|
|
|
+ Cancel
|
|
|
+ </AlertDialogCancel>
|
|
|
+ <AlertDialogAction
|
|
|
+ onClick={() => {
|
|
|
+ confirmDialog.action();
|
|
|
+ setConfirmDialog({ ...confirmDialog, open: false });
|
|
|
+ }}
|
|
|
+ className="bg-cyan-500 hover:bg-cyan-600 text-white"
|
|
|
+ >
|
|
|
+ Confirm
|
|
|
+ </AlertDialogAction>
|
|
|
+ </AlertDialogFooter>
|
|
|
+ </AlertDialogContent>
|
|
|
+ </AlertDialog>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|