|
|
@@ -20,7 +20,7 @@ import {
|
|
|
AlertDialogHeader,
|
|
|
AlertDialogTitle,
|
|
|
} from "@/components/ui/alert-dialog";
|
|
|
-import { Loader2, Search, Package, ChevronLeft, ChevronRight } from "lucide-react";
|
|
|
+import { Loader2, Search, Package, ChevronLeft, ChevronRight, Tag } from "lucide-react";
|
|
|
import { API_URL } from "@/lib/config";
|
|
|
import { useToast } from "@/hooks/use-toast";
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
@@ -32,13 +32,24 @@ interface StoreData {
|
|
|
store_url: string | null;
|
|
|
}
|
|
|
|
|
|
+interface Category {
|
|
|
+ category_id: string;
|
|
|
+ category_name: string;
|
|
|
+ product_count: number;
|
|
|
+ is_excluded: boolean;
|
|
|
+}
|
|
|
+
|
|
|
interface Product {
|
|
|
id: string;
|
|
|
name: string;
|
|
|
sku: string;
|
|
|
price: string;
|
|
|
currency: string;
|
|
|
+ categories: any[];
|
|
|
enabled_in_context: boolean;
|
|
|
+ excluded_by_individual: boolean;
|
|
|
+ excluded_by_category: boolean;
|
|
|
+ exclusion_reason: string | null;
|
|
|
}
|
|
|
|
|
|
type DataItem = Product;
|
|
|
@@ -64,7 +75,7 @@ export function ManageStoreDataContent() {
|
|
|
// Filter and search state
|
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
const [statusFilter, setStatusFilter] = useState<"all" | "enabled" | "disabled">("all");
|
|
|
- const [platformFilter, setPlatformFilter] = useState<string>("all");
|
|
|
+ const [categoryFilter, setCategoryFilter] = useState<string>("all");
|
|
|
|
|
|
// Data state
|
|
|
const [data, setData] = useState<DataItem[]>([]);
|
|
|
@@ -73,6 +84,10 @@ export function ManageStoreDataContent() {
|
|
|
const [enabledCount, setEnabledCount] = useState(0);
|
|
|
const [disabledCount, setDisabledCount] = useState(0);
|
|
|
|
|
|
+ // Categories state
|
|
|
+ const [categories, setCategories] = useState<Category[]>([]);
|
|
|
+ const [categoriesLoading, setCategoriesLoading] = useState(false);
|
|
|
+
|
|
|
// Pagination state
|
|
|
const [page, setPage] = useState(1);
|
|
|
const [pageSize, setPageSize] = useState(25);
|
|
|
@@ -148,12 +163,53 @@ export function ManageStoreDataContent() {
|
|
|
fetchStores();
|
|
|
}, [searchParams, toast]);
|
|
|
|
|
|
+ // Fetch categories when store changes
|
|
|
+ useEffect(() => {
|
|
|
+ if (selectedStore) {
|
|
|
+ fetchCategories();
|
|
|
+ }
|
|
|
+ }, [selectedStore]);
|
|
|
+
|
|
|
// Fetch data when tab, store, or filters change
|
|
|
useEffect(() => {
|
|
|
if (selectedStore) {
|
|
|
fetchData();
|
|
|
}
|
|
|
- }, [selectedStore, activeTab, page, pageSize, searchQuery, statusFilter]);
|
|
|
+ }, [selectedStore, activeTab, page, pageSize, searchQuery, statusFilter, categoryFilter]);
|
|
|
+
|
|
|
+ const fetchCategories = async () => {
|
|
|
+ if (!selectedStore) return;
|
|
|
+
|
|
|
+ setCategoriesLoading(true);
|
|
|
+ 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/categories?store_id=${selectedStore.id}`, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Failed to fetch categories');
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+ if (result.success) {
|
|
|
+ setCategories(result.categories || []);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error fetching categories:', error);
|
|
|
+ // Don't show toast for categories - it's not critical
|
|
|
+ } finally {
|
|
|
+ setCategoriesLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
if (!selectedStore) return;
|
|
|
@@ -182,6 +238,10 @@ export function ManageStoreDataContent() {
|
|
|
params.append('enabled', statusFilter === 'enabled' ? 'true' : 'false');
|
|
|
}
|
|
|
|
|
|
+ if (categoryFilter !== 'all') {
|
|
|
+ params.append('category', categoryFilter);
|
|
|
+ }
|
|
|
+
|
|
|
const endpoint = `/api/store-data/${activeTab}?${params.toString()}`;
|
|
|
const response = await fetch(`${API_URL}${endpoint}`, {
|
|
|
headers: {
|
|
|
@@ -280,6 +340,52 @@ export function ManageStoreDataContent() {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ const handleToggleCategory = async (categoryId: string, categoryName: string, currentExcluded: 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);
|
|
|
+
|
|
|
+ const response = await fetch(`${API_URL}/api/store-data/categories/${categoryId}/exclude`, {
|
|
|
+ method: 'PUT',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ store_id: selectedStore.id,
|
|
|
+ category_name: categoryName,
|
|
|
+ exclude: !currentExcluded
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error('Failed to update category');
|
|
|
+ }
|
|
|
+
|
|
|
+ toast({
|
|
|
+ title: "Category Updated",
|
|
|
+ description: `Category "${categoryName}" ${!currentExcluded ? 'excluded' : 'included'} successfully`,
|
|
|
+ });
|
|
|
+
|
|
|
+ // Refresh categories and products
|
|
|
+ fetchCategories();
|
|
|
+ fetchData();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error toggling category:', error);
|
|
|
+ toast({
|
|
|
+ title: "Error",
|
|
|
+ description: 'Failed to update category',
|
|
|
+ variant: "destructive"
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
const handleBulkAction = async (enable: boolean) => {
|
|
|
if (!selectedStore || selectedItems.size === 0) return;
|
|
|
|
|
|
@@ -357,6 +463,8 @@ export function ManageStoreDataContent() {
|
|
|
description: t('manageStoreData.toast.successEnableAllDescription', { type: activeTab }),
|
|
|
});
|
|
|
|
|
|
+ // Refresh categories (in case they were excluded) and products
|
|
|
+ fetchCategories();
|
|
|
fetchData();
|
|
|
} catch (error) {
|
|
|
console.error('Error enabling all:', error);
|
|
|
@@ -430,28 +538,51 @@ export function ManageStoreDataContent() {
|
|
|
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 renderProductRow = (product: Product) => {
|
|
|
+ const firstCategory = product.categories && product.categories.length > 0
|
|
|
+ ? product.categories[0]
|
|
|
+ : null;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <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>
|
|
|
+ {firstCategory && (
|
|
|
+ <Badge variant="outline" className="text-xs">
|
|
|
+ {firstCategory.name || firstCategory.category_name || firstCategory}
|
|
|
+ </Badge>
|
|
|
+ )}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <Switch
|
|
|
+ checked={product.enabled_in_context}
|
|
|
+ onCheckedChange={() => handleToggleItem(product.id, product.enabled_in_context)}
|
|
|
+ className="data-[state=checked]:bg-cyan-500"
|
|
|
+ />
|
|
|
+ {!product.enabled_in_context && product.exclusion_reason && (
|
|
|
+ <Badge
|
|
|
+ variant={product.exclusion_reason === 'category' ? 'secondary' : 'destructive'}
|
|
|
+ className="text-xs"
|
|
|
+ >
|
|
|
+ {product.exclusion_reason === 'category' ? 'By Category' : 'Individual'}
|
|
|
+ </Badge>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ );
|
|
|
+ };
|
|
|
|
|
|
if (loading) {
|
|
|
return (
|
|
|
@@ -553,8 +684,70 @@ export function ManageStoreDataContent() {
|
|
|
<SelectItem value="disabled">Disabled Only</SelectItem>
|
|
|
</SelectContent>
|
|
|
</Select>
|
|
|
+
|
|
|
+ {/* Category Filter */}
|
|
|
+ {categories.length > 0 && (
|
|
|
+ <Select value={categoryFilter} onValueChange={(value) => setCategoryFilter(value)}>
|
|
|
+ <SelectTrigger className="w-[200px] bg-slate-700 border-slate-600 text-white">
|
|
|
+ <Tag className="w-4 h-4 mr-2" />
|
|
|
+ <SelectValue placeholder="All Categories" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent className="bg-slate-700 border-slate-600">
|
|
|
+ <SelectItem value="all">All Categories</SelectItem>
|
|
|
+ {categories.map((cat) => (
|
|
|
+ <SelectItem key={cat.category_id} value={cat.category_id}>
|
|
|
+ <div className="flex items-center justify-between w-full gap-2">
|
|
|
+ <span className={cat.is_excluded ? 'line-through text-slate-500' : ''}>
|
|
|
+ {cat.category_name}
|
|
|
+ </span>
|
|
|
+ <Badge variant="secondary" className="text-xs">
|
|
|
+ {cat.product_count}
|
|
|
+ </Badge>
|
|
|
+ </div>
|
|
|
+ </SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ )}
|
|
|
</div>
|
|
|
|
|
|
+ {/* Category Management Section */}
|
|
|
+ {categories.length > 0 && (
|
|
|
+ <div className="mb-6 p-4 bg-slate-700/30 rounded-lg border border-slate-600">
|
|
|
+ <div className="flex items-center justify-between mb-3">
|
|
|
+ <h3 className="text-white font-medium flex items-center gap-2">
|
|
|
+ <Tag className="w-4 h-4" />
|
|
|
+ Category Management
|
|
|
+ </h3>
|
|
|
+ </div>
|
|
|
+ <div className="flex flex-wrap gap-2">
|
|
|
+ {categories.map((cat) => (
|
|
|
+ <div
|
|
|
+ key={cat.category_id}
|
|
|
+ className={`flex items-center gap-2 px-3 py-1.5 rounded-md border ${
|
|
|
+ cat.is_excluded
|
|
|
+ ? 'bg-red-900/20 border-red-800 text-red-300'
|
|
|
+ : 'bg-slate-700 border-slate-600 text-white'
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ <span className="text-sm">{cat.category_name}</span>
|
|
|
+ <Badge variant="secondary" className="text-xs">
|
|
|
+ {cat.product_count}
|
|
|
+ </Badge>
|
|
|
+ <Button
|
|
|
+ size="sm"
|
|
|
+ variant="ghost"
|
|
|
+ className="h-6 px-2 text-xs"
|
|
|
+ onClick={() => handleToggleCategory(cat.category_id, cat.category_name, cat.is_excluded)}
|
|
|
+ >
|
|
|
+ {cat.is_excluded ? 'Include' : 'Exclude'}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
{/* Bulk Actions */}
|
|
|
{selectedItems.size > 0 && (
|
|
|
<div className="flex gap-2 mb-4 p-3 bg-slate-700/50 rounded-lg">
|
|
|
@@ -585,7 +778,7 @@ export function ManageStoreDataContent() {
|
|
|
onClick={() => setConfirmDialog({
|
|
|
open: true,
|
|
|
title: `Enable All ${activeTab}`,
|
|
|
- description: `Are you sure you want to enable all ${activeTab} for AI context?`,
|
|
|
+ description: `Are you sure you want to enable all ${activeTab} for AI context? This will also clear all category exclusions.`,
|
|
|
action: handleEnableAll
|
|
|
})}
|
|
|
className="border-cyan-500 text-cyan-500 hover:bg-cyan-500 hover:text-white"
|
|
|
@@ -630,7 +823,8 @@ export function ManageStoreDataContent() {
|
|
|
<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>
|
|
|
+ <TableHead className="text-slate-300">Category</TableHead>
|
|
|
+ <TableHead className="text-slate-300">Status</TableHead>
|
|
|
</TableRow>
|
|
|
</TableHeader>
|
|
|
<TableBody className="bg-slate-800">
|