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:
2025-03-09 15:38:13 -04:00
parent 78a0018940
commit c3c48669ad
4 changed files with 167 additions and 944 deletions

View File

@@ -526,7 +526,7 @@ router.get('/field-options', async (req, res) => {
// Fetch tax categories
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
ORDER BY tax_code_id = 0 DESC, name
`);
@@ -820,6 +820,8 @@ router.get('/search-products', async (req, res) => {
s.companyname AS vendor,
sid.supplier_itemnumber AS vendor_reference,
sid.notions_itemnumber AS notions_reference,
sid.supplier_id AS supplier,
sid.notions_case_pack AS case_qty,
pc1.name AS brand,
p.company AS brand_id,
pc2.name AS line,
@@ -839,7 +841,10 @@ router.get('/search-products', async (req, res) => {
p.country_of_origin,
ci.totalsold AS total_sold,
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
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
@@ -893,6 +898,21 @@ router.get('/search-products', async (req, res) => {
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);
} catch (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
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
JOIN product_categories pc ON pci.cat_id = pc.cat_id
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('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
const categories = rows.map(category => ({
const categories = filteredCategories.map(category => ({
value: category.cat_id.toString(),
label: category.name,
type: category.type,

View File

@@ -63,6 +63,10 @@ interface Product {
date_last_sold: string | null;
supplier?: string;
categories?: string[];
tax_code?: string;
size_cat?: string;
shipping_restrictions?: string;
case_qty?: number;
}
interface FieldOption {
@@ -336,6 +340,17 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated
const [sortField, setSortField] = useState<SortField>(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
const hasActiveFilters = () => {
return searchParams.company !== 'all' || searchParams.dateFilter !== 'none';
@@ -479,6 +494,16 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated
};
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);
// 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');
};
@@ -892,13 +936,16 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated
isOpen={true}
onClose={() => setStep('search')}
onSuccess={onTemplateCreated}
initialData={selectedProduct ? {
initialData={selectedProduct ? (() => {
console.log('Creating TemplateForm initialData with tax_code:', selectedProduct.tax_code);
return {
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,
case_qty: selectedProduct.case_qty ? Number(selectedProduct.case_qty) : undefined,
hts_code: selectedProduct.harmonized_tariff_code || undefined,
description: selectedProduct.description || undefined,
weight: selectedProduct.weight ? Number(Number(selectedProduct.weight).toFixed(2)) : undefined,
@@ -906,7 +953,11 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated
width: selectedProduct.width ? Number(Number(selectedProduct.width).toFixed(2)) : undefined,
height: selectedProduct.height ? Number(Number(selectedProduct.height).toFixed(2)) : undefined,
categories: selectedProduct.categories || [],
} : undefined}
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"
fieldOptions={fieldOptions}
/>

View File

@@ -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>
);
}

View File

@@ -86,10 +86,10 @@ export function TemplateForm({
length: undefined,
width: undefined,
height: undefined,
tax_cat: undefined,
tax_cat: "0",
size_cat: undefined,
categories: [],
ship_restrictions: undefined
ship_restrictions: "0"
};
const [formData, setFormData] = React.useState<TemplateFormData>(defaultFormData);
@@ -248,14 +248,14 @@ export function TemplateForm({
const getSortedOptions = (options: FieldOption[], selectedValue?: string | string[]) => {
return [...options].sort((a, b) => {
if (Array.isArray(selectedValue)) {
const aSelected = selectedValue.includes(a.value);
const bSelected = selectedValue.includes(b.value);
const aSelected = selectedValue.includes(String(a.value));
const bSelected = selectedValue.includes(String(b.value));
if (aSelected && !bSelected) return -1;
if (!aSelected && bSelected) return 1;
return 0;
}
if (a.value === selectedValue) return -1;
if (b.value === selectedValue) return 1;
if (String(a.value) === String(selectedValue)) return -1;
if (String(b.value) === String(selectedValue)) return 1;
return 0;
});
};
@@ -318,7 +318,7 @@ export function TemplateForm({
<Check
className={cn(
"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-0"
)}
@@ -335,7 +335,7 @@ export function TemplateForm({
<Check
className={cn(
"mr-2 h-4 w-4",
selectedValue === option.value
String(selectedValue) === String(option.value)
? "opacity-100"
: "opacity-0"
)}
@@ -603,9 +603,22 @@ export function TemplateForm({
)}
>
{formData.tax_cat !== undefined
? (fieldOptions.taxCategories.find(
(cat) => cat.value === formData.tax_cat
)?.label || "Not Specifically Set")
? (() => {
console.log('Looking for tax_cat:', {
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..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>