Fix data coming in correctly when copying template from an existing product, automatically strip out deals and black friday categories
This commit is contained in:
@@ -526,7 +526,7 @@ router.get('/field-options', async (req, res) => {
|
|||||||
|
|
||||||
// Fetch tax categories
|
// Fetch tax categories
|
||||||
const [taxCategories] = await connection.query(`
|
const [taxCategories] = await connection.query(`
|
||||||
SELECT tax_code_id as value, name as label
|
SELECT CAST(tax_code_id AS CHAR) as value, name as label
|
||||||
FROM product_tax_codes
|
FROM product_tax_codes
|
||||||
ORDER BY tax_code_id = 0 DESC, name
|
ORDER BY tax_code_id = 0 DESC, name
|
||||||
`);
|
`);
|
||||||
@@ -820,6 +820,8 @@ router.get('/search-products', async (req, res) => {
|
|||||||
s.companyname AS vendor,
|
s.companyname AS vendor,
|
||||||
sid.supplier_itemnumber AS vendor_reference,
|
sid.supplier_itemnumber AS vendor_reference,
|
||||||
sid.notions_itemnumber AS notions_reference,
|
sid.notions_itemnumber AS notions_reference,
|
||||||
|
sid.supplier_id AS supplier,
|
||||||
|
sid.notions_case_pack AS case_qty,
|
||||||
pc1.name AS brand,
|
pc1.name AS brand,
|
||||||
p.company AS brand_id,
|
p.company AS brand_id,
|
||||||
pc2.name AS line,
|
pc2.name AS line,
|
||||||
@@ -839,7 +841,10 @@ router.get('/search-products', async (req, res) => {
|
|||||||
p.country_of_origin,
|
p.country_of_origin,
|
||||||
ci.totalsold AS total_sold,
|
ci.totalsold AS total_sold,
|
||||||
p.datein AS first_received,
|
p.datein AS first_received,
|
||||||
pls.date_sold AS date_last_sold
|
pls.date_sold AS date_last_sold,
|
||||||
|
IF(p.tax_code IS NULL, '', CAST(p.tax_code AS CHAR)) AS tax_code,
|
||||||
|
CAST(p.size_cat AS CHAR) AS size_cat,
|
||||||
|
CAST(p.shipping_restrictions AS CHAR) AS shipping_restrictions
|
||||||
FROM products p
|
FROM products p
|
||||||
LEFT JOIN product_current_prices pcp ON p.pid = pcp.pid AND pcp.active = 1
|
LEFT JOIN product_current_prices pcp ON p.pid = pcp.pid AND pcp.active = 1
|
||||||
LEFT JOIN supplier_item_data sid ON p.pid = sid.pid
|
LEFT JOIN supplier_item_data sid ON p.pid = sid.pid
|
||||||
@@ -893,6 +898,21 @@ router.get('/search-products', async (req, res) => {
|
|||||||
|
|
||||||
const [results] = await connection.query(query, queryParams);
|
const [results] = await connection.query(query, queryParams);
|
||||||
|
|
||||||
|
// Debug log to check values
|
||||||
|
if (results.length > 0) {
|
||||||
|
console.log('Product search result sample fields:', {
|
||||||
|
pid: results[0].pid,
|
||||||
|
tax_code: results[0].tax_code,
|
||||||
|
tax_code_type: typeof results[0].tax_code,
|
||||||
|
tax_code_value: `Value: '${results[0].tax_code}'`,
|
||||||
|
size_cat: results[0].size_cat,
|
||||||
|
shipping_restrictions: results[0].shipping_restrictions,
|
||||||
|
supplier: results[0].supplier,
|
||||||
|
case_qty: results[0].case_qty,
|
||||||
|
moq: results[0].moq
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.json(results);
|
res.json(results);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error searching products:', error);
|
console.error('Error searching products:', error);
|
||||||
@@ -1008,7 +1028,7 @@ router.get('/product-categories/:pid', async (req, res) => {
|
|||||||
|
|
||||||
// Query to get categories for a specific product
|
// Query to get categories for a specific product
|
||||||
const query = `
|
const query = `
|
||||||
SELECT pc.cat_id, pc.name, pc.type, pc.combined_name
|
SELECT pc.cat_id, pc.name, pc.type, pc.combined_name, pc.master_cat_id
|
||||||
FROM product_category_index pci
|
FROM product_category_index pci
|
||||||
JOIN product_categories pc ON pci.cat_id = pc.cat_id
|
JOIN product_categories pc ON pci.cat_id = pc.cat_id
|
||||||
WHERE pci.pid = ?
|
WHERE pci.pid = ?
|
||||||
@@ -1023,8 +1043,61 @@ router.get('/product-categories/:pid', async (req, res) => {
|
|||||||
console.log(`Product ${pid} has ${rows.length} categories with types: ${uniqueTypes.join(', ')}`);
|
console.log(`Product ${pid} has ${rows.length} categories with types: ${uniqueTypes.join(', ')}`);
|
||||||
console.log('Categories:', rows.map(row => ({ id: row.cat_id, name: row.name, type: row.type })));
|
console.log('Categories:', rows.map(row => ({ id: row.cat_id, name: row.name, type: row.type })));
|
||||||
|
|
||||||
|
// Check for parent categories to filter out deals and black friday
|
||||||
|
const sectionQuery = `
|
||||||
|
SELECT pc.cat_id, pc.name
|
||||||
|
FROM product_categories pc
|
||||||
|
WHERE pc.type = 10 AND (LOWER(pc.name) LIKE '%deal%' OR LOWER(pc.name) LIKE '%black friday%')
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [dealSections] = await connection.query(sectionQuery);
|
||||||
|
const dealSectionIds = dealSections.map(section => section.cat_id);
|
||||||
|
|
||||||
|
console.log('Filtering out categories from deal sections:', dealSectionIds);
|
||||||
|
|
||||||
|
// Filter out categories from deals and black friday sections
|
||||||
|
const filteredCategories = rows.filter(category => {
|
||||||
|
// Direct check for top-level deal sections
|
||||||
|
if (category.type === 10) {
|
||||||
|
return !dealSectionIds.some(id => id === category.cat_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For categories (type 11), check if their parent is a deal section
|
||||||
|
if (category.type === 11) {
|
||||||
|
return !dealSectionIds.some(id => id === category.master_cat_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For subcategories (type 12), get their parent category first
|
||||||
|
if (category.type === 12) {
|
||||||
|
const parentId = category.master_cat_id;
|
||||||
|
// Find the parent category in our rows
|
||||||
|
const parentCategory = rows.find(c => c.cat_id === parentId);
|
||||||
|
// If parent not found or parent's parent is not a deal section, keep it
|
||||||
|
return !parentCategory || !dealSectionIds.some(id => id === parentCategory.master_cat_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For subsubcategories (type 13), check their hierarchy manually
|
||||||
|
if (category.type === 13) {
|
||||||
|
const parentId = category.master_cat_id;
|
||||||
|
// Find the parent subcategory
|
||||||
|
const parentSubcategory = rows.find(c => c.cat_id === parentId);
|
||||||
|
if (!parentSubcategory) return true;
|
||||||
|
|
||||||
|
// Find the grandparent category
|
||||||
|
const grandparentId = parentSubcategory.master_cat_id;
|
||||||
|
const grandparentCategory = rows.find(c => c.cat_id === grandparentId);
|
||||||
|
// If grandparent not found or grandparent's parent is not a deal section, keep it
|
||||||
|
return !grandparentCategory || !dealSectionIds.some(id => id === grandparentCategory.master_cat_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep all other category types
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Filtered out ${rows.length - filteredCategories.length} deal/black friday categories`);
|
||||||
|
|
||||||
// Format the response to match the expected format in the frontend
|
// Format the response to match the expected format in the frontend
|
||||||
const categories = rows.map(category => ({
|
const categories = filteredCategories.map(category => ({
|
||||||
value: category.cat_id.toString(),
|
value: category.cat_id.toString(),
|
||||||
label: category.name,
|
label: category.name,
|
||||||
type: category.type,
|
type: category.type,
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ interface Product {
|
|||||||
date_last_sold: string | null;
|
date_last_sold: string | null;
|
||||||
supplier?: string;
|
supplier?: string;
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
|
tax_code?: string;
|
||||||
|
size_cat?: string;
|
||||||
|
shipping_restrictions?: string;
|
||||||
|
case_qty?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FieldOption {
|
interface FieldOption {
|
||||||
@@ -336,6 +340,17 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated
|
|||||||
const [sortField, setSortField] = useState<SortField>(null);
|
const [sortField, setSortField] = useState<SortField>(null);
|
||||||
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
|
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
|
||||||
|
|
||||||
|
// Debug log for selectedProduct values
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (selectedProduct) {
|
||||||
|
console.log('Selected Product Details:', {
|
||||||
|
tax_code: selectedProduct.tax_code,
|
||||||
|
size_cat: selectedProduct.size_cat,
|
||||||
|
shipping_restrictions: selectedProduct.shipping_restrictions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [selectedProduct]);
|
||||||
|
|
||||||
// Simple helper function to check if any filters are active
|
// Simple helper function to check if any filters are active
|
||||||
const hasActiveFilters = () => {
|
const hasActiveFilters = () => {
|
||||||
return searchParams.company !== 'all' || searchParams.dateFilter !== 'none';
|
return searchParams.company !== 'all' || searchParams.dateFilter !== 'none';
|
||||||
@@ -479,6 +494,16 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleProductSelect = async (product: Product) => {
|
const handleProductSelect = async (product: Product) => {
|
||||||
|
console.log('Selected product from list:', {
|
||||||
|
pid: product.pid,
|
||||||
|
tax_code: product.tax_code,
|
||||||
|
tax_code_type: typeof product.tax_code,
|
||||||
|
tax_code_value: `Value: '${product.tax_code}'`,
|
||||||
|
size_cat: product.size_cat,
|
||||||
|
shipping_restrictions: product.shipping_restrictions,
|
||||||
|
case_qty: product.case_qty
|
||||||
|
});
|
||||||
|
|
||||||
setSelectedProduct(product);
|
setSelectedProduct(product);
|
||||||
|
|
||||||
// Try to find a matching company ID
|
// Try to find a matching company ID
|
||||||
@@ -491,6 +516,25 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch product categories if pid is available
|
||||||
|
if (product.pid) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/import/product-categories/${product.pid}`);
|
||||||
|
const productCategories = response.data;
|
||||||
|
|
||||||
|
// Update the selected product with the categories
|
||||||
|
setSelectedProduct(prev => ({
|
||||||
|
...prev!,
|
||||||
|
categories: productCategories.map((cat: any) => cat.value)
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching product categories:', error);
|
||||||
|
toast.error('Failed to fetch product categories', {
|
||||||
|
description: 'Could not retrieve categories for this product'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setStep('form');
|
setStep('form');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -892,21 +936,28 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated
|
|||||||
isOpen={true}
|
isOpen={true}
|
||||||
onClose={() => setStep('search')}
|
onClose={() => setStep('search')}
|
||||||
onSuccess={onTemplateCreated}
|
onSuccess={onTemplateCreated}
|
||||||
initialData={selectedProduct ? {
|
initialData={selectedProduct ? (() => {
|
||||||
company: selectedProduct.brand_id,
|
console.log('Creating TemplateForm initialData with tax_code:', selectedProduct.tax_code);
|
||||||
product_type: '',
|
return {
|
||||||
supplier: selectedProduct.supplier,
|
company: selectedProduct.brand_id,
|
||||||
msrp: selectedProduct.regular_price ? Number(Number(selectedProduct.regular_price).toFixed(2)) : undefined,
|
product_type: '',
|
||||||
cost_each: selectedProduct.cost_price ? Number(Number(selectedProduct.cost_price).toFixed(2)) : undefined,
|
supplier: selectedProduct.supplier,
|
||||||
qty_per_unit: selectedProduct.moq ? Number(selectedProduct.moq) : undefined,
|
msrp: selectedProduct.regular_price ? Number(Number(selectedProduct.regular_price).toFixed(2)) : undefined,
|
||||||
hts_code: selectedProduct.harmonized_tariff_code || undefined,
|
cost_each: selectedProduct.cost_price ? Number(Number(selectedProduct.cost_price).toFixed(2)) : undefined,
|
||||||
description: selectedProduct.description || undefined,
|
qty_per_unit: selectedProduct.moq ? Number(selectedProduct.moq) : undefined,
|
||||||
weight: selectedProduct.weight ? Number(Number(selectedProduct.weight).toFixed(2)) : undefined,
|
case_qty: selectedProduct.case_qty ? Number(selectedProduct.case_qty) : undefined,
|
||||||
length: selectedProduct.length ? Number(Number(selectedProduct.length).toFixed(2)) : undefined,
|
hts_code: selectedProduct.harmonized_tariff_code || undefined,
|
||||||
width: selectedProduct.width ? Number(Number(selectedProduct.width).toFixed(2)) : undefined,
|
description: selectedProduct.description || undefined,
|
||||||
height: selectedProduct.height ? Number(Number(selectedProduct.height).toFixed(2)) : undefined,
|
weight: selectedProduct.weight ? Number(Number(selectedProduct.weight).toFixed(2)) : undefined,
|
||||||
categories: selectedProduct.categories || [],
|
length: selectedProduct.length ? Number(Number(selectedProduct.length).toFixed(2)) : undefined,
|
||||||
} : undefined}
|
width: selectedProduct.width ? Number(Number(selectedProduct.width).toFixed(2)) : undefined,
|
||||||
|
height: selectedProduct.height ? Number(Number(selectedProduct.height).toFixed(2)) : undefined,
|
||||||
|
categories: selectedProduct.categories || [],
|
||||||
|
tax_cat: selectedProduct.tax_code ? String(selectedProduct.tax_code) : undefined,
|
||||||
|
size_cat: selectedProduct.size_cat ? String(selectedProduct.size_cat) : undefined,
|
||||||
|
ship_restrictions: selectedProduct.shipping_restrictions ? String(selectedProduct.shipping_restrictions) : undefined
|
||||||
|
};
|
||||||
|
})() : undefined}
|
||||||
mode="create"
|
mode="create"
|
||||||
fieldOptions={fieldOptions}
|
fieldOptions={fieldOptions}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,914 +0,0 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
||||||
import { Loader2, Search, ChevronsUpDown, Check, X, ChevronUp, ChevronDown } from 'lucide-react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import {
|
|
||||||
Pagination,
|
|
||||||
PaginationContent,
|
|
||||||
PaginationEllipsis,
|
|
||||||
PaginationItem,
|
|
||||||
PaginationLink,
|
|
||||||
PaginationNext,
|
|
||||||
PaginationPrevious,
|
|
||||||
} from "@/components/ui/pagination";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { TemplateForm } from '@/components/templates/TemplateForm';
|
|
||||||
|
|
||||||
interface ProductSearchDialogProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onTemplateCreated: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Product {
|
|
||||||
pid: number;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
sku: string;
|
|
||||||
barcode: string;
|
|
||||||
harmonized_tariff_code: string;
|
|
||||||
price: number;
|
|
||||||
regular_price: number;
|
|
||||||
cost_price: number;
|
|
||||||
vendor: string;
|
|
||||||
vendor_reference: string;
|
|
||||||
notions_reference: string;
|
|
||||||
brand: string;
|
|
||||||
brand_id: string;
|
|
||||||
line: string;
|
|
||||||
line_id: string;
|
|
||||||
subline: string;
|
|
||||||
subline_id: string;
|
|
||||||
artist: string;
|
|
||||||
artist_id: string;
|
|
||||||
moq: number;
|
|
||||||
weight: number;
|
|
||||||
length: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
country_of_origin: string;
|
|
||||||
total_sold: number;
|
|
||||||
first_received: string | null;
|
|
||||||
date_last_sold: string | null;
|
|
||||||
supplier?: string;
|
|
||||||
categories?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FieldOption {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
type?: number;
|
|
||||||
level?: number;
|
|
||||||
hexColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FieldOptions {
|
|
||||||
companies: FieldOption[];
|
|
||||||
artists: FieldOption[];
|
|
||||||
sizes: FieldOption[];
|
|
||||||
themes: FieldOption[];
|
|
||||||
categories: FieldOption[];
|
|
||||||
colors: FieldOption[];
|
|
||||||
suppliers: FieldOption[];
|
|
||||||
taxCategories: FieldOption[];
|
|
||||||
shippingRestrictions: FieldOption[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TemplateFormData {
|
|
||||||
company: string;
|
|
||||||
product_type: string;
|
|
||||||
supplier?: string;
|
|
||||||
msrp?: number;
|
|
||||||
cost_each?: number;
|
|
||||||
qty_per_unit?: number;
|
|
||||||
case_qty?: number;
|
|
||||||
hts_code?: string;
|
|
||||||
description?: string;
|
|
||||||
weight?: number;
|
|
||||||
length?: number;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
tax_cat?: string;
|
|
||||||
size_cat?: string;
|
|
||||||
categories?: string[];
|
|
||||||
ship_restrictions?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add sorting types
|
|
||||||
type SortDirection = 'asc' | 'desc' | null;
|
|
||||||
type SortField = 'title' | 'brand' | 'line' | 'price' | 'total_sold' | 'first_received' | 'date_last_sold' | null;
|
|
||||||
|
|
||||||
// Date filter options
|
|
||||||
const DATE_FILTER_OPTIONS = [
|
|
||||||
{ label: "Any Time", value: "none" },
|
|
||||||
{ label: "Last week", value: "1week" },
|
|
||||||
{ label: "Last month", value: "1month" },
|
|
||||||
{ label: "Last 2 months", value: "2months" },
|
|
||||||
{ label: "Last 3 months", value: "3months" },
|
|
||||||
{ label: "Last 6 months", value: "6months" },
|
|
||||||
{ label: "Last year", value: "1year" }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Create a memoized search component to prevent unnecessary re-renders
|
|
||||||
const SearchInput = React.memo(({
|
|
||||||
searchTerm,
|
|
||||||
setSearchTerm,
|
|
||||||
handleSearch,
|
|
||||||
isLoading,
|
|
||||||
onClear
|
|
||||||
}: {
|
|
||||||
searchTerm: string;
|
|
||||||
setSearchTerm: (term: string) => void;
|
|
||||||
handleSearch: () => void;
|
|
||||||
isLoading: boolean;
|
|
||||||
onClear: () => void;
|
|
||||||
}) => (
|
|
||||||
<form onSubmit={(e) => { e.preventDefault(); handleSearch(); }} className="flex items-center space-x-2">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<Input
|
|
||||||
placeholder="Search..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
|
||||||
className="pr-8"
|
|
||||||
/>
|
|
||||||
{searchTerm && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="absolute right-0 top-0 h-full w-8 p-0"
|
|
||||||
onClick={onClear}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button type="submit" disabled={isLoading}>
|
|
||||||
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
));
|
|
||||||
|
|
||||||
// Create a memoized filter component
|
|
||||||
const FilterSelects = React.memo(({
|
|
||||||
selectedCompany,
|
|
||||||
selectedDateFilter,
|
|
||||||
companies
|
|
||||||
}: {
|
|
||||||
selectedCompany: string;
|
|
||||||
selectedDateFilter: string;
|
|
||||||
companies: FieldOption[];
|
|
||||||
}) => (
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="company-filter" className="text-sm">Filter by Company</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedCompany}
|
|
||||||
onValueChange={(value) => handleFilterChange('company', value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="company-filter">
|
|
||||||
<SelectValue placeholder="Any Company" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Any Company</SelectItem>
|
|
||||||
{companies.map((company) => (
|
|
||||||
<SelectItem key={company.value} value={company.value}>
|
|
||||||
{company.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="date-filter" className="text-sm">Filter by Date</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedDateFilter}
|
|
||||||
onValueChange={(value) => handleFilterChange('dateFilter', value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="date-filter">
|
|
||||||
<SelectValue placeholder="Any Time" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{DATE_FILTER_OPTIONS.map((filter) => (
|
|
||||||
<SelectItem key={filter.value} value={filter.value}>
|
|
||||||
{filter.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
// Create a memoized results table component
|
|
||||||
const ResultsTable = React.memo(({
|
|
||||||
results,
|
|
||||||
selectedCompany,
|
|
||||||
onSelect
|
|
||||||
}: {
|
|
||||||
results: any[];
|
|
||||||
selectedCompany: string;
|
|
||||||
onSelect: (product: any) => void;
|
|
||||||
}) => (
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Name</TableHead>
|
|
||||||
{selectedCompany === 'all' && <TableHead>Company</TableHead>}
|
|
||||||
<TableHead>Line</TableHead>
|
|
||||||
<TableHead>Price</TableHead>
|
|
||||||
<TableHead>Total Sold</TableHead>
|
|
||||||
<TableHead>Date In</TableHead>
|
|
||||||
<TableHead>Last Sold</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{results.map((product) => (
|
|
||||||
<TableRow
|
|
||||||
key={product.pid}
|
|
||||||
className="cursor-pointer hover:bg-muted/50"
|
|
||||||
onClick={() => onSelect(product)}
|
|
||||||
>
|
|
||||||
<TableCell>{product.title}</TableCell>
|
|
||||||
{selectedCompany === 'all' && <TableCell>{product.brand || '-'}</TableCell>}
|
|
||||||
<TableCell>{product.line || '-'}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{product.price != null ? `$${Number(product.price).toFixed(2)}` : '-'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{product.total_sold || 0}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{product.first_received
|
|
||||||
? new Date(product.first_received).toLocaleDateString()
|
|
||||||
: '-'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{product.date_last_sold
|
|
||||||
? new Date(product.date_last_sold).toLocaleDateString()
|
|
||||||
: '-'}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
));
|
|
||||||
|
|
||||||
export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated }: ProductSearchDialogProps) {
|
|
||||||
// Product search state
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
|
||||||
const [searchResults, setSearchResults] = useState<Product[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [step, setStep] = useState<'search' | 'form'>('search');
|
|
||||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
|
||||||
const [fieldOptions, setFieldOptions] = useState<FieldOptions | null>(null);
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const productsPerPage = 500;
|
|
||||||
|
|
||||||
// Filter states - using a single object for filters to ensure atomic updates
|
|
||||||
const [filters, setFilters] = useState({
|
|
||||||
company: 'all',
|
|
||||||
dateFilter: 'none'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sorting states
|
|
||||||
const [sortField, setSortField] = useState<SortField>(null);
|
|
||||||
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
|
|
||||||
|
|
||||||
const [hasSearched, setHasSearched] = useState(false);
|
|
||||||
|
|
||||||
// Simplified function to check if any filters are active
|
|
||||||
const hasActiveFilters = useCallback(() => {
|
|
||||||
return filters.company !== 'all' || filters.dateFilter !== 'none';
|
|
||||||
}, [filters]);
|
|
||||||
|
|
||||||
// Simplified search state change handler
|
|
||||||
const handleSearchStateChange = useCallback(async (searchTermToUse = searchTerm) => {
|
|
||||||
console.log('Search state change with:', { searchTermToUse, filters });
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const hasSearchTerm = searchTermToUse.trim().length > 0;
|
|
||||||
const hasFilters = hasActiveFilters();
|
|
||||||
setHasSearched(hasSearchTerm || hasFilters);
|
|
||||||
|
|
||||||
// If no active filters or search term, reset everything
|
|
||||||
if (!hasSearchTerm && !hasFilters) {
|
|
||||||
console.log('No search term or filters, resetting results');
|
|
||||||
setSearchResults([]);
|
|
||||||
setHasSearched(false);
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const params: Record<string, any> = {};
|
|
||||||
|
|
||||||
// Add search term if it exists
|
|
||||||
if (hasSearchTerm) {
|
|
||||||
params.q = searchTermToUse.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add company filter if selected
|
|
||||||
if (filters.company !== 'all') {
|
|
||||||
params.company = filters.company;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add date filter if selected
|
|
||||||
if (filters.dateFilter !== 'none') {
|
|
||||||
params.dateRange = filters.dateFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we only have filters (no search term), use wildcard search
|
|
||||||
if (!hasSearchTerm && hasFilters) {
|
|
||||||
params.q = '*';
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Search params:', params);
|
|
||||||
|
|
||||||
const response = await axios.get('/api/import/search-products', { params });
|
|
||||||
console.log('Search response:', response.data);
|
|
||||||
|
|
||||||
setSearchResults(response.data.map((product: Product) => ({
|
|
||||||
...product,
|
|
||||||
pid: typeof product.pid === 'string' ? parseInt(product.pid, 10) : product.pid,
|
|
||||||
price: typeof product.price === 'string' ? parseFloat(product.price) : product.price,
|
|
||||||
})));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error searching products:', error);
|
|
||||||
toast.error('Failed to search products', {
|
|
||||||
description: 'Could not retrieve search results from the server'
|
|
||||||
});
|
|
||||||
setSearchResults([]);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [hasActiveFilters, filters, searchTerm]);
|
|
||||||
|
|
||||||
// Trigger search when dialog opens with active filters
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen && hasSearched) {
|
|
||||||
handleSearchStateChange();
|
|
||||||
}
|
|
||||||
}, [isOpen, hasSearched, handleSearchStateChange]);
|
|
||||||
|
|
||||||
// Simplified handler functions
|
|
||||||
const handleSearch = useCallback(() => {
|
|
||||||
handleSearchStateChange();
|
|
||||||
}, [handleSearchStateChange]);
|
|
||||||
|
|
||||||
const clearSearch = useCallback(() => {
|
|
||||||
setSearchTerm('');
|
|
||||||
handleSearchStateChange('');
|
|
||||||
}, [handleSearchStateChange]);
|
|
||||||
|
|
||||||
const clearFilters = useCallback(() => {
|
|
||||||
setFilters({
|
|
||||||
company: 'all',
|
|
||||||
dateFilter: 'none'
|
|
||||||
});
|
|
||||||
// Give UI time to update
|
|
||||||
setTimeout(() => {
|
|
||||||
handleSearchStateChange();
|
|
||||||
}, 0);
|
|
||||||
}, [handleSearchStateChange]);
|
|
||||||
|
|
||||||
// Handler for filter changes
|
|
||||||
const handleFilterChange = useCallback((type: 'company' | 'dateFilter', value: string) => {
|
|
||||||
setFilters(prev => {
|
|
||||||
const newFilters = { ...prev, [type]: value };
|
|
||||||
console.log(`Filter ${type} changed to ${value}`, newFilters);
|
|
||||||
|
|
||||||
// Schedule search with updated filters
|
|
||||||
setTimeout(() => {
|
|
||||||
handleSearchStateChange();
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
return newFilters;
|
|
||||||
});
|
|
||||||
}, [handleSearchStateChange]);
|
|
||||||
|
|
||||||
// Reset all search state when dialog is closed
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) {
|
|
||||||
setSearchTerm('');
|
|
||||||
setSearchResults([]);
|
|
||||||
setFilters({
|
|
||||||
company: 'all',
|
|
||||||
dateFilter: 'none'
|
|
||||||
});
|
|
||||||
setSortField(null);
|
|
||||||
setSortDirection(null);
|
|
||||||
setCurrentPage(1);
|
|
||||||
setStep('search');
|
|
||||||
setHasSearched(false);
|
|
||||||
setSelectedProduct(null);
|
|
||||||
} else {
|
|
||||||
// Fetch field options when dialog opens
|
|
||||||
fetchFieldOptions();
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
// Fetch field options when component mounts
|
|
||||||
const fetchFieldOptions = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('/api/import/field-options');
|
|
||||||
setFieldOptions(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching field options:', error);
|
|
||||||
toast.error('Failed to fetch field options', {
|
|
||||||
description: 'Could not retrieve field options from the server'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProductSelect = async (product: Product) => {
|
|
||||||
console.log('Selected product:', product);
|
|
||||||
console.log('Brand ID:', product.brand_id);
|
|
||||||
|
|
||||||
// Try to find the supplier ID from the vendor name
|
|
||||||
let supplierValue;
|
|
||||||
if (product.vendor && fieldOptions) {
|
|
||||||
let supplierOption = fieldOptions.suppliers.find(
|
|
||||||
supplier => supplier.label.toLowerCase() === product.vendor.toLowerCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
// If no exact match, try partial match
|
|
||||||
if (!supplierOption) {
|
|
||||||
supplierOption = fieldOptions.suppliers.find(
|
|
||||||
supplier => supplier.label.toLowerCase().includes(product.vendor.toLowerCase()) ||
|
|
||||||
product.vendor.toLowerCase().includes(supplier.label.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supplierOption) {
|
|
||||||
supplierValue = supplierOption.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch categories for the product
|
|
||||||
let categories: string[] = [];
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`/api/import/product-categories/${product.pid}`);
|
|
||||||
if (response.data && Array.isArray(response.data)) {
|
|
||||||
// Filter out themes and subthemes (types 20 and 21)
|
|
||||||
categories = response.data
|
|
||||||
.filter((category: any) => category.type !== 20 && category.type !== 21)
|
|
||||||
.map((category: any) => category.value);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching product categories:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure brand_id is properly set
|
|
||||||
const companyId = product.brand_id || '';
|
|
||||||
console.log('Setting company ID:', companyId);
|
|
||||||
|
|
||||||
const selectedProduct = {
|
|
||||||
...product,
|
|
||||||
brand_id: companyId, // Ensure brand_id is set
|
|
||||||
supplier: supplierValue,
|
|
||||||
categories: categories || []
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Setting selected product:', selectedProduct);
|
|
||||||
setSelectedProduct(selectedProduct);
|
|
||||||
setStep('form');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get current products for pagination
|
|
||||||
const totalPages = Math.ceil(searchResults.length / productsPerPage);
|
|
||||||
|
|
||||||
// Change page
|
|
||||||
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
|
|
||||||
|
|
||||||
// Generate page numbers for pagination
|
|
||||||
const getPageNumbers = () => {
|
|
||||||
const pageNumbers = [];
|
|
||||||
const maxPagesToShow = 5;
|
|
||||||
|
|
||||||
if (totalPages <= maxPagesToShow) {
|
|
||||||
// If we have fewer pages than the max to show, display all pages
|
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
|
||||||
pageNumbers.push(i);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Always include first page
|
|
||||||
pageNumbers.push(1);
|
|
||||||
|
|
||||||
// Calculate start and end of middle pages
|
|
||||||
let startPage = Math.max(2, currentPage - 1);
|
|
||||||
let endPage = Math.min(totalPages - 1, currentPage + 1);
|
|
||||||
|
|
||||||
// Adjust if we're near the beginning
|
|
||||||
if (currentPage <= 3) {
|
|
||||||
endPage = Math.min(totalPages - 1, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust if we're near the end
|
|
||||||
if (currentPage >= totalPages - 2) {
|
|
||||||
startPage = Math.max(2, totalPages - 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ellipsis after first page if needed
|
|
||||||
if (startPage > 2) {
|
|
||||||
pageNumbers.push('ellipsis-start');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add middle pages
|
|
||||||
for (let i = startPage; i <= endPage; i++) {
|
|
||||||
pageNumbers.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ellipsis before last page if needed
|
|
||||||
if (endPage < totalPages - 1) {
|
|
||||||
pageNumbers.push('ellipsis-end');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always include last page
|
|
||||||
pageNumbers.push(totalPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageNumbers;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sort function
|
|
||||||
const toggleSort = (field: SortField) => {
|
|
||||||
if (sortField === field) {
|
|
||||||
// Toggle direction if already sorting by this field
|
|
||||||
if (sortDirection === 'asc') {
|
|
||||||
setSortDirection('desc');
|
|
||||||
} else if (sortDirection === 'desc') {
|
|
||||||
setSortField(null);
|
|
||||||
setSortDirection(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Set new sort field and direction
|
|
||||||
setSortField(field);
|
|
||||||
setSortDirection('asc');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset to page 1 when sorting changes
|
|
||||||
setCurrentPage(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply sorting to results
|
|
||||||
const getSortedResults = (results: Product[]) => {
|
|
||||||
if (!sortField || !sortDirection) return results;
|
|
||||||
|
|
||||||
return [...results].sort((a, b) => {
|
|
||||||
let valueA: any;
|
|
||||||
let valueB: any;
|
|
||||||
|
|
||||||
// Extract the correct field values
|
|
||||||
switch (sortField) {
|
|
||||||
case 'title':
|
|
||||||
valueA = a.title?.toLowerCase() || '';
|
|
||||||
valueB = b.title?.toLowerCase() || '';
|
|
||||||
break;
|
|
||||||
case 'brand':
|
|
||||||
valueA = a.brand?.toLowerCase() || '';
|
|
||||||
valueB = b.brand?.toLowerCase() || '';
|
|
||||||
break;
|
|
||||||
case 'line':
|
|
||||||
valueA = a.line?.toLowerCase() || '';
|
|
||||||
valueB = b.line?.toLowerCase() || '';
|
|
||||||
break;
|
|
||||||
case 'price':
|
|
||||||
valueA = typeof a.price === 'number' ? a.price : parseFloat(String(a.price) || '0');
|
|
||||||
valueB = typeof b.price === 'number' ? b.price : parseFloat(String(b.price) || '0');
|
|
||||||
break;
|
|
||||||
case 'total_sold':
|
|
||||||
valueA = typeof a.total_sold === 'number' ? a.total_sold : parseInt(String(a.total_sold) || '0', 10);
|
|
||||||
valueB = typeof b.total_sold === 'number' ? b.total_sold : parseInt(String(b.total_sold) || '0', 10);
|
|
||||||
break;
|
|
||||||
case 'first_received':
|
|
||||||
valueA = a.first_received ? new Date(a.first_received).getTime() : 0;
|
|
||||||
valueB = b.first_received ? new Date(b.first_received).getTime() : 0;
|
|
||||||
break;
|
|
||||||
case 'date_last_sold':
|
|
||||||
valueA = a.date_last_sold ? new Date(a.date_last_sold).getTime() : 0;
|
|
||||||
valueB = b.date_last_sold ? new Date(b.date_last_sold).getTime() : 0;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare the values
|
|
||||||
if (valueA < valueB) {
|
|
||||||
return sortDirection === 'asc' ? -1 : 1;
|
|
||||||
}
|
|
||||||
if (valueA > valueB) {
|
|
||||||
return sortDirection === 'asc' ? 1 : -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update getFilteredResults to remove redundant company filtering
|
|
||||||
const getFilteredResults = () => {
|
|
||||||
if (!searchResults) return [];
|
|
||||||
return searchResults;
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredResults = getFilteredResults();
|
|
||||||
const sortedResults = getSortedResults(filteredResults);
|
|
||||||
|
|
||||||
// Get current products for pagination
|
|
||||||
const indexOfLastProductFiltered = currentPage * productsPerPage;
|
|
||||||
const indexOfFirstProductFiltered = indexOfLastProductFiltered - productsPerPage;
|
|
||||||
const currentProductsFiltered = sortedResults.slice(indexOfFirstProductFiltered, indexOfLastProductFiltered);
|
|
||||||
const totalPagesFiltered = Math.ceil(sortedResults.length / productsPerPage);
|
|
||||||
|
|
||||||
// Get sort icon
|
|
||||||
const getSortIcon = (field: SortField) => {
|
|
||||||
if (sortField !== field) {
|
|
||||||
return <ChevronsUpDown className="ml-1 h-3 w-3 opacity-50" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sortDirection === 'asc'
|
|
||||||
? <ChevronUp className="ml-1 h-3 w-3" />
|
|
||||||
: <ChevronDown className="ml-1 h-3 w-3" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sortable table header component
|
|
||||||
const SortableTableHead = ({ field, children }: { field: SortField, children: React.ReactNode }) => (
|
|
||||||
<TableHead
|
|
||||||
className="cursor-pointer hover:bg-muted/50 transition-colors"
|
|
||||||
onClick={() => toggleSort(field)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
{children}
|
|
||||||
{getSortIcon(field)}
|
|
||||||
</div>
|
|
||||||
</TableHead>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
|
||||||
<DialogContent className={`max-h-[95vh] flex flex-col p-6 ${
|
|
||||||
step === 'search'
|
|
||||||
? searchResults.length > 0
|
|
||||||
? 'max-w-5xl'
|
|
||||||
: 'max-w-2xl'
|
|
||||||
: 'max-w-2xl'
|
|
||||||
}`}>
|
|
||||||
{step === 'search' ? (
|
|
||||||
<>
|
|
||||||
<DialogHeader className="px-0">
|
|
||||||
<DialogTitle>Search Products</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Search for a product you want to use as a template.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden py-4 px-1">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<SearchInput
|
|
||||||
searchTerm={searchTerm}
|
|
||||||
setSearchTerm={setSearchTerm}
|
|
||||||
handleSearch={handleSearch}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onClear={clearSearch}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FilterSelects
|
|
||||||
selectedCompany={filters.company}
|
|
||||||
selectedDateFilter={filters.dateFilter}
|
|
||||||
companies={fieldOptions?.companies || []}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{fieldOptions && ((filters.company && filters.company !== 'all') || filters.dateFilter !== 'none') && (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div className="text-sm text-muted-foreground">Active filters:</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{filters.company && filters.company !== "all" && (
|
|
||||||
<Badge variant="secondary" className="flex items-center gap-1">
|
|
||||||
Company: {fieldOptions?.companies?.find(c => c.value === filters.company)?.label || 'Unknown'}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-4 w-4 p-0 ml-1"
|
|
||||||
onClick={() => {
|
|
||||||
setFilters({
|
|
||||||
company: 'all',
|
|
||||||
dateFilter: 'none'
|
|
||||||
});
|
|
||||||
handleSearchStateChange();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{filters.dateFilter && filters.dateFilter !== 'none' && (
|
|
||||||
<Badge variant="secondary" className="flex items-center gap-1">
|
|
||||||
Date: {(() => {
|
|
||||||
const selectedOption = DATE_FILTER_OPTIONS.find(o => o.value === filters.dateFilter);
|
|
||||||
return selectedOption ? selectedOption.label : 'Custom range';
|
|
||||||
})()}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-4 w-4 p-0 ml-1"
|
|
||||||
onClick={() => {
|
|
||||||
setFilters({
|
|
||||||
company: 'all',
|
|
||||||
dateFilter: 'none'
|
|
||||||
});
|
|
||||||
handleSearchStateChange();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 text-xs"
|
|
||||||
onClick={clearFilters}
|
|
||||||
>
|
|
||||||
Clear All
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<ScrollArea className="flex-1 -mr-6 pr-6 overflow-y-auto max-h-[60vh]">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="text-center py-4 text-muted-foreground flex items-center justify-center gap-2">
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
<span>Searching...</span>
|
|
||||||
</div>
|
|
||||||
) : !hasSearched ? (
|
|
||||||
<div className="text-center py-4 text-muted-foreground">
|
|
||||||
Use the search field or filters to find products
|
|
||||||
</div>
|
|
||||||
) : searchResults.length === 0 ? (
|
|
||||||
<div className="text-center py-4 text-muted-foreground">
|
|
||||||
No products found matching your criteria
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="text-sm text-muted-foreground mb-2">
|
|
||||||
{sortedResults.length} products found
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<Table>
|
|
||||||
<TableHeader className="sticky top-0 bg-background z-10 border-b">
|
|
||||||
<TableRow>
|
|
||||||
<SortableTableHead field="title">Name</SortableTableHead>
|
|
||||||
{filters.company === 'all' && (
|
|
||||||
<SortableTableHead field="brand">Company</SortableTableHead>
|
|
||||||
)}
|
|
||||||
<SortableTableHead field="line">Line</SortableTableHead>
|
|
||||||
<SortableTableHead field="price">Price</SortableTableHead>
|
|
||||||
<SortableTableHead field="total_sold">Total Sold</SortableTableHead>
|
|
||||||
<SortableTableHead field="first_received">Date In</SortableTableHead>
|
|
||||||
<SortableTableHead field="date_last_sold">Last Sold</SortableTableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{currentProductsFiltered.map((product) => (
|
|
||||||
<TableRow
|
|
||||||
key={product.pid}
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => handleProductSelect(product)}
|
|
||||||
>
|
|
||||||
<TableCell className="font-medium">{product.title}</TableCell>
|
|
||||||
{filters.company === 'all' && (
|
|
||||||
<TableCell>{product.brand || '-'}</TableCell>
|
|
||||||
)}
|
|
||||||
<TableCell>{product.line || '-'}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{product.price !== null && product.price !== undefined
|
|
||||||
? `$${typeof product.price === 'number'
|
|
||||||
? product.price.toFixed(2)
|
|
||||||
: parseFloat(String(product.price)).toFixed(2)}`
|
|
||||||
: '-'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{product.total_sold !== null && product.total_sold !== undefined
|
|
||||||
? typeof product.total_sold === 'number'
|
|
||||||
? product.total_sold
|
|
||||||
: parseInt(String(product.total_sold), 10)
|
|
||||||
: 0}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{product.first_received
|
|
||||||
? (() => {
|
|
||||||
try {
|
|
||||||
return new Date(product.first_received).toLocaleDateString();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error formatting first_received date:', e);
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
: '-'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{product.date_last_sold
|
|
||||||
? (() => {
|
|
||||||
try {
|
|
||||||
return new Date(product.date_last_sold).toLocaleDateString();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error formatting date_last_sold date:', e);
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
: '-'}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{totalPagesFiltered > 1 && (
|
|
||||||
<div className="mt-4">
|
|
||||||
<Pagination>
|
|
||||||
<PaginationContent>
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationPrevious
|
|
||||||
onClick={() => currentPage > 1 && paginate(currentPage - 1)}
|
|
||||||
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
|
|
||||||
{getPageNumbers().map((page, index) => (
|
|
||||||
<PaginationItem key={index}>
|
|
||||||
{page === 'ellipsis-start' || page === 'ellipsis-end' ? (
|
|
||||||
<PaginationEllipsis />
|
|
||||||
) : (
|
|
||||||
<PaginationLink
|
|
||||||
isActive={page === currentPage}
|
|
||||||
onClick={() => typeof page === 'number' && paginate(page)}
|
|
||||||
className={typeof page === 'number' ? "cursor-pointer" : ""}
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</PaginationLink>
|
|
||||||
)}
|
|
||||||
</PaginationItem>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<PaginationItem>
|
|
||||||
<PaginationNext
|
|
||||||
onClick={() => currentPage < totalPagesFiltered && paginate(currentPage + 1)}
|
|
||||||
className={currentPage === totalPagesFiltered ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
|
||||||
/>
|
|
||||||
</PaginationItem>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter className="px-0 mt-6">
|
|
||||||
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<TemplateForm
|
|
||||||
isOpen={true}
|
|
||||||
onClose={() => setStep('search')}
|
|
||||||
onSuccess={onTemplateCreated}
|
|
||||||
initialData={selectedProduct ? {
|
|
||||||
company: selectedProduct.brand_id,
|
|
||||||
product_type: '',
|
|
||||||
supplier: selectedProduct.supplier,
|
|
||||||
msrp: selectedProduct.regular_price ? Number(Number(selectedProduct.regular_price).toFixed(2)) : undefined,
|
|
||||||
cost_each: selectedProduct.cost_price ? Number(Number(selectedProduct.cost_price).toFixed(2)) : undefined,
|
|
||||||
qty_per_unit: selectedProduct.moq ? Number(selectedProduct.moq) : undefined,
|
|
||||||
hts_code: selectedProduct.harmonized_tariff_code || undefined,
|
|
||||||
description: selectedProduct.description || undefined,
|
|
||||||
weight: selectedProduct.weight ? Number(Number(selectedProduct.weight).toFixed(2)) : undefined,
|
|
||||||
length: selectedProduct.length ? Number(Number(selectedProduct.length).toFixed(2)) : undefined,
|
|
||||||
width: selectedProduct.width ? Number(Number(selectedProduct.width).toFixed(2)) : undefined,
|
|
||||||
height: selectedProduct.height ? Number(Number(selectedProduct.height).toFixed(2)) : undefined,
|
|
||||||
categories: selectedProduct.categories || [],
|
|
||||||
} : undefined}
|
|
||||||
mode="create"
|
|
||||||
fieldOptions={fieldOptions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -86,10 +86,10 @@ export function TemplateForm({
|
|||||||
length: undefined,
|
length: undefined,
|
||||||
width: undefined,
|
width: undefined,
|
||||||
height: undefined,
|
height: undefined,
|
||||||
tax_cat: undefined,
|
tax_cat: "0",
|
||||||
size_cat: undefined,
|
size_cat: undefined,
|
||||||
categories: [],
|
categories: [],
|
||||||
ship_restrictions: undefined
|
ship_restrictions: "0"
|
||||||
};
|
};
|
||||||
|
|
||||||
const [formData, setFormData] = React.useState<TemplateFormData>(defaultFormData);
|
const [formData, setFormData] = React.useState<TemplateFormData>(defaultFormData);
|
||||||
@@ -248,14 +248,14 @@ export function TemplateForm({
|
|||||||
const getSortedOptions = (options: FieldOption[], selectedValue?: string | string[]) => {
|
const getSortedOptions = (options: FieldOption[], selectedValue?: string | string[]) => {
|
||||||
return [...options].sort((a, b) => {
|
return [...options].sort((a, b) => {
|
||||||
if (Array.isArray(selectedValue)) {
|
if (Array.isArray(selectedValue)) {
|
||||||
const aSelected = selectedValue.includes(a.value);
|
const aSelected = selectedValue.includes(String(a.value));
|
||||||
const bSelected = selectedValue.includes(b.value);
|
const bSelected = selectedValue.includes(String(b.value));
|
||||||
if (aSelected && !bSelected) return -1;
|
if (aSelected && !bSelected) return -1;
|
||||||
if (!aSelected && bSelected) return 1;
|
if (!aSelected && bSelected) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (a.value === selectedValue) return -1;
|
if (String(a.value) === String(selectedValue)) return -1;
|
||||||
if (b.value === selectedValue) return 1;
|
if (String(b.value) === String(selectedValue)) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -318,7 +318,7 @@ export function TemplateForm({
|
|||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 h-4 w-4 flex-shrink-0",
|
"mr-2 h-4 w-4 flex-shrink-0",
|
||||||
(formData.categories || []).includes(option.value)
|
(formData.categories || []).some(cat => String(cat) === String(option.value))
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0"
|
: "opacity-0"
|
||||||
)}
|
)}
|
||||||
@@ -335,7 +335,7 @@ export function TemplateForm({
|
|||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 h-4 w-4",
|
"mr-2 h-4 w-4",
|
||||||
selectedValue === option.value
|
String(selectedValue) === String(option.value)
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0"
|
: "opacity-0"
|
||||||
)}
|
)}
|
||||||
@@ -603,9 +603,22 @@ export function TemplateForm({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formData.tax_cat !== undefined
|
{formData.tax_cat !== undefined
|
||||||
? (fieldOptions.taxCategories.find(
|
? (() => {
|
||||||
(cat) => cat.value === formData.tax_cat
|
console.log('Looking for tax_cat:', {
|
||||||
)?.label || "Not Specifically Set")
|
formData_tax_cat: formData.tax_cat,
|
||||||
|
formData_tax_cat_type: typeof formData.tax_cat,
|
||||||
|
taxCategories: fieldOptions.taxCategories.map(cat => ({
|
||||||
|
value: cat.value,
|
||||||
|
value_type: typeof cat.value,
|
||||||
|
label: cat.label
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
const match = fieldOptions.taxCategories.find(
|
||||||
|
(cat) => String(cat.value) === String(formData.tax_cat)
|
||||||
|
);
|
||||||
|
console.log('Tax category match:', match);
|
||||||
|
return match?.label || "Not Specifically Set";
|
||||||
|
})()
|
||||||
: "Select tax category..."}
|
: "Select tax category..."}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user