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
|
||||
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,
|
||||
|
||||
@@ -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,21 +936,28 @@ export function SearchProductTemplateDialog({ isOpen, onClose, onTemplateCreated
|
||||
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}
|
||||
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,
|
||||
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 || [],
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user