diff --git a/inventory/src/components/products/ProductSearchDialog.tsx b/inventory/src/components/products/ProductSearchDialog.tsx
deleted file mode 100644
index 6664f78..0000000
--- a/inventory/src/components/products/ProductSearchDialog.tsx
+++ /dev/null
@@ -1,1611 +0,0 @@
-import React, { useState, useEffect, useCallback, useMemo } 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";
-
-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;
-}
-
-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;
-}) => (
-
-));
-
-// Create a memoized filter component
-const FilterSelects = React.memo(({
- selectedCompany,
- setSelectedCompany,
- selectedDateFilter,
- setSelectedDateFilter,
- companies,
- onFilterChange
-}: {
- selectedCompany: string;
- setSelectedCompany: (company: string) => void;
- selectedDateFilter: string;
- setSelectedDateFilter: (filter: string) => void;
- companies: { label: string; value: string; }[];
- onFilterChange: () => void;
-}) => (
-
-
-
-
-
-
-
-
-
-
-
-));
-
-// Create a memoized results table component
-const ResultsTable = React.memo(({
- results,
- selectedCompany,
- onSelect
-}: {
- results: any[];
- selectedCompany: string;
- onSelect: (product: any) => void;
-}) => (
-
-
-
- Name
- {selectedCompany === 'all' && Company}
- Line
- Price
- Total Sold
- Date In
- Last Sold
-
-
-
- {results.map((product) => (
- onSelect(product)}
- >
- {product.title}
- {selectedCompany === 'all' && {product.brand || '-'}}
- {product.line || '-'}
-
- {product.price != null ? `$${Number(product.price).toFixed(2)}` : '-'}
-
- {product.total_sold || 0}
-
- {product.first_received
- ? new Date(product.first_received).toLocaleDateString()
- : '-'}
-
-
- {product.date_last_sold
- ? new Date(product.date_last_sold).toLocaleDateString()
- : '-'}
-
-
- ))}
-
-
-));
-
-export function ProductSearchDialog({ isOpen, onClose, onTemplateCreated }: ProductSearchDialogProps) {
- const [searchTerm, setSearchTerm] = useState('');
- const [searchResults, setSearchResults] = useState([]);
- const [isLoading, setIsLoading] = useState(false);
- const [step, setStep] = useState<'search' | 'form'>('search');
- const [formData, setFormData] = useState({
- company: '',
- product_type: '',
- });
- const [fieldOptions, setFieldOptions] = useState(null);
- const [, setProductLines] = useState([]);
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [currentPage, setCurrentPage] = useState(1);
- const productsPerPage = 500;
-
- // Filter states
- const [selectedCompany, setSelectedCompany] = useState('all');
- const [selectedDateFilter, setSelectedDateFilter] = useState('none');
-
- // Sorting states
- const [sortField, setSortField] = useState(null);
- const [sortDirection, setSortDirection] = useState(null);
-
- const [hasSearched, setHasSearched] = useState(false);
-
- // Add this new function to handle all search state changes
- const handleSearchStateChange = async () => {
- setIsLoading(true);
- setHasSearched(true);
-
- // If no active filters or search term, reset everything
- if (!searchTerm.trim() && selectedCompany === 'all' && selectedDateFilter === 'none') {
- setSearchResults([]);
- setHasSearched(false);
- setIsLoading(false);
- return;
- }
-
- try {
- const params: Record = {};
-
- // Always include search term if present
- if (searchTerm.trim()) {
- params.q = searchTerm.trim();
- }
-
- // Add filters
- if (selectedCompany !== 'all') {
- params.company = selectedCompany;
- // Only use wildcard if there's no search term
- if (!searchTerm.trim()) {
- params.q = '*';
- }
- }
-
- if (selectedDateFilter !== 'none') {
- params.dateRange = selectedDateFilter;
- // Only use wildcard if there's no search term and no company filter
- if (!searchTerm.trim() && selectedCompany === 'all') {
- params.q = '*';
- }
- }
-
- const response = await axios.get('/api/import/search-products', { params });
- 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,
- // ... other parsing remains the same
- })));
- } 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);
- }
- };
-
- // Reset all search state when dialog is closed
- useEffect(() => {
- if (!isOpen) {
- // Reset search state
- setSearchTerm('');
- setSearchResults([]);
- setSelectedCompany('all');
- setSelectedDateFilter('none');
- setSortField(null);
- setSortDirection(null);
- setCurrentPage(1);
- setStep('search');
- setHasSearched(false);
- }
- }, [isOpen]);
-
- // Fetch field options when component mounts
- useEffect(() => {
- if (isOpen) {
- fetchFieldOptions();
- // Perform initial search if any filters are already applied
- if (selectedCompany !== 'all' || selectedDateFilter !== 'none') {
- handleSearchStateChange();
- }
- }
- }, [isOpen]);
-
- // Update the useEffect for filter changes to use the new search function
- useEffect(() => {
- if (selectedCompany !== 'all' || selectedDateFilter !== 'none') {
- handleSearchStateChange();
- }
- }, [selectedCompany, selectedDateFilter]);
-
- // Fetch product lines when company changes
- useEffect(() => {
- if (formData.company) {
- fetchProductLines(formData.company);
- }
- }, [formData.company]);
-
- 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 fetchProductLines = async (companyId: string) => {
- try {
- const response = await axios.get(`/api/import/product-lines/${companyId}`);
- setProductLines(response.data);
- } catch (error) {
- console.error('Error fetching product lines:', error);
- setProductLines([]);
- }
- };
-
- // Update the search input handlers
- const handleSearch = () => {
- handleSearchStateChange();
- };
-
- const clearSearch = () => {
- setSearchTerm('');
- handleSearchStateChange();
- };
-
- const clearFilters = () => {
- setSelectedCompany('all');
- setSelectedDateFilter('none');
- handleSearchStateChange();
- };
-
- const handleProductSelect = (product: Product) => {
-
- // Ensure all values are of the correct type
- setFormData({
- company: product.brand_id ? String(product.brand_id) : '',
- product_type: '',
- // For supplier, we need to find the supplier ID by matching the vendor name
- // vendor_reference is NOT the supplier ID, it's the supplier's product identifier
- supplier: undefined, // We'll set this below if we can find a match
- msrp: typeof product.regular_price === 'number' ? product.regular_price :
- typeof product.regular_price === 'string' ? parseFloat(product.regular_price) : undefined,
- cost_each: typeof product.cost_price === 'number' ? product.cost_price :
- typeof product.cost_price === 'string' ? parseFloat(product.cost_price) : undefined,
- qty_per_unit: typeof product.moq === 'number' ? product.moq :
- typeof product.moq === 'string' ? parseInt(product.moq, 10) : undefined,
- hts_code: product.harmonized_tariff_code || undefined,
- description: product.description || undefined,
- weight: typeof product.weight === 'number' ? product.weight :
- typeof product.weight === 'string' ? parseFloat(product.weight) : undefined,
- length: typeof product.length === 'number' ? product.length :
- typeof product.length === 'string' ? parseFloat(product.length) : undefined,
- width: typeof product.width === 'number' ? product.width :
- typeof product.width === 'string' ? parseFloat(product.width) : undefined,
- height: typeof product.height === 'number' ? product.height :
- typeof product.height === 'string' ? parseFloat(product.height) : undefined,
- categories: [],
- ship_restrictions: undefined
- });
-
- // Try to find the supplier ID from the vendor name
- 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) {
- setFormData(prev => ({
- ...prev,
- supplier: supplierOption.value
- }));
-
- } else {
- console.log('No supplier match found for vendor:', product.vendor);
- }
- }
-
- // Fetch product categories
- if (product.pid) {
-
- fetchProductCategories(product.pid);
- }
-
- setStep('form');
- };
-
- // Add a function to fetch product categories
- const fetchProductCategories = async (productId: number) => {
- try {
- const response = await axios.get(`/api/import/product-categories/${productId}`);
-
- if (response.data && Array.isArray(response.data)) {
- // Filter out categories with type 20 (themes) and type 21 (subthemes)
- const filteredCategories = response.data.filter((category: any) =>
- category.type !== 20 && category.type !== 21
- );
-
- // Extract category IDs and update form data
- const categoryIds = filteredCategories.map((category: any) => category.value);
- setFormData(prev => ({
- ...prev,
- categories: categoryIds
- }));
- }
- } catch (error) {
- console.error('Error fetching product categories:', error);
- toast.error('Failed to fetch product categories', {
- description: 'Could not retrieve categories for this product'
- });
- }
- };
-
- const handleInputChange = (e: React.ChangeEvent) => {
- const { name, value } = e.target;
- setFormData(prev => ({ ...prev, [name]: value }));
- };
-
- const handleTextAreaChange = (e: React.ChangeEvent) => {
- const { name, value } = e.target;
- setFormData(prev => ({ ...prev, [name]: value }));
- };
-
- const handleMultiSelectChange = (name: string, value: string) => {
- setFormData(prev => {
- const currentValues = prev[name as keyof typeof prev] as string[] || [];
- const valueSet = new Set(currentValues);
-
- if (valueSet.has(value)) {
- valueSet.delete(value);
- } else {
- valueSet.add(value);
- }
-
- return {
- ...prev,
- [name]: Array.from(valueSet),
- };
- });
- };
-
- const handleSelectChange = (name: string, value: string | string[]) => {
- setFormData(prev => ({ ...prev, [name]: value }));
- };
-
- const handleNumberInputChange = (e: React.ChangeEvent) => {
- const { name, value } = e.target;
- const numValue = value === '' ? undefined : parseFloat(value);
- setFormData(prev => ({ ...prev, [name]: numValue }));
- };
-
- const handleSubmit = async () => {
- // Validate required fields
- if (!formData.company || !formData.product_type) {
- toast.error('Validation Error', {
- description: 'Company and Product Type are required'
- });
- return;
- }
-
- // Log supplier information for debugging
- if (formData.supplier) {
-
- } else {
- console.log('No supplier selected for submission');
- }
-
- // Log categories information for debugging
- if (formData.categories && formData.categories.length > 0) {
-
- } else {
- console.log('No categories selected for submission');
- }
-
- setIsSubmitting(true);
- try {
- // Prepare the data to be sent - ensure all fields are properly formatted
- const dataToSend = {
- company: formData.company,
- product_type: formData.product_type,
- supplier: formData.supplier || null,
- msrp: formData.msrp !== undefined ? Number(formData.msrp) : null,
- cost_each: formData.cost_each !== undefined ? Number(formData.cost_each) : null,
- qty_per_unit: formData.qty_per_unit !== undefined ? Number(formData.qty_per_unit) : null,
- case_qty: formData.case_qty !== undefined ? Number(formData.case_qty) : null,
- hts_code: formData.hts_code || null,
- description: formData.description || null,
- weight: formData.weight !== undefined ? Number(formData.weight) : null,
- length: formData.length !== undefined ? Number(formData.length) : null,
- width: formData.width !== undefined ? Number(formData.width) : null,
- height: formData.height !== undefined ? Number(formData.height) : null,
- tax_cat: formData.tax_cat || null,
- size_cat: formData.size_cat || null,
- categories: formData.categories || [],
- ship_restrictions: formData.ship_restrictions || null
- };
-
-
- const response = await axios.post('/api/templates', dataToSend);
-
-
- if (response.status >= 200 && response.status < 300) {
- toast.success('Template created successfully');
- onTemplateCreated();
- onClose();
- } else {
- throw new Error(`Server responded with status: ${response.status}`);
- }
- } catch (error) {
- console.error('Error creating template:', error);
- let errorMessage = 'Failed to create template';
-
- if (axios.isAxiosError(error)) {
- console.error('Axios error details:', {
- status: error.response?.status,
- data: error.response?.data,
- headers: error.response?.headers
- });
-
- if (error.response?.data?.message) {
- errorMessage = error.response.data.message;
- } else if (error.response?.data?.error) {
- errorMessage = error.response.data.error;
- } else if (error.message) {
- errorMessage = error.message;
- }
- }
-
- toast.error('Template Creation Failed', {
- description: errorMessage
- });
- } finally {
- setIsSubmitting(false);
- }
- };
-
- const handleCancel = () => {
- if (step === 'form') {
- setStep('search');
- } else {
- // Reset form data when closing
- setFormData({
- company: '',
- product_type: '',
- });
- onClose();
- }
- };
-
- // 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;
- });
- };
-
- // Apply filters to search results
- const getFilteredResults = () => {
- if (!searchResults) return [];
-
- return searchResults.filter(product => {
- // Apply company filter if set
- if (selectedCompany && selectedCompany !== "all" && product.brand_id !== selectedCompany) {
- return false;
- }
-
- // The date filtering is now handled on the server side with the dateRange parameter
- // No need to filter dates on the client side anymore
-
- return true;
- });
- };
-
- 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 ;
- }
-
- return sortDirection === 'asc'
- ?
- : ;
- };
-
- // Sortable table header component
- const SortableTableHead = ({ field, children }: { field: SortField, children: React.ReactNode }) => (
- toggleSort(field)}
- >
-
- {children}
- {getSortIcon(field)}
-
-
- );
-
- // Render search step
- const renderSearchStep = () => (
- <>
-
- Search Products
-
- Search for a product you want to use as a template.
-
-
-
-
-
-
-
-
-
- {fieldOptions && ((selectedCompany && selectedCompany !== 'all') || selectedDateFilter !== 'none') && (
-
-
Active filters:
-
- {selectedCompany && selectedCompany !== "all" && (
-
- Company: {fieldOptions?.companies?.find(c => c.value === selectedCompany)?.label || 'Unknown'}
-
-
- )}
- {selectedDateFilter && selectedDateFilter !== 'none' && (
-
- Date: {(() => {
- const selectedOption = DATE_FILTER_OPTIONS.find(o => o.value === selectedDateFilter);
- return selectedOption ? selectedOption.label : 'Custom range';
- })()}
-
-
- )}
-
-
-
- )}
-
-
-
-
- {isLoading ? (
-
-
- Searching...
-
- ) : !hasSearched ? (
-
- Use the search field or filters to find products
-
- ) : searchResults.length === 0 ? (
-
- No products found matching your criteria
-
- ) : (
- <>
-
- {sortedResults.length} products found
-
-
-
-
-
- Name
- {selectedCompany === 'all' && (
- Company
- )}
- Line
- Price
- Total Sold
- Date In
- Last Sold
-
-
-
- {currentProductsFiltered.map((product) => (
- handleProductSelect(product)}
- >
- {product.title}
- {selectedCompany === 'all' && (
- {product.brand || '-'}
- )}
- {product.line || '-'}
-
- {product.price !== null && product.price !== undefined
- ? `$${typeof product.price === 'number'
- ? product.price.toFixed(2)
- : parseFloat(String(product.price)).toFixed(2)}`
- : '-'}
-
-
- {product.total_sold !== null && product.total_sold !== undefined
- ? typeof product.total_sold === 'number'
- ? product.total_sold
- : parseInt(String(product.total_sold), 10)
- : 0}
-
-
- {product.first_received
- ? (() => {
- try {
- return new Date(product.first_received).toLocaleDateString();
- } catch (e) {
- console.error('Error formatting first_received date:', e);
- return '-';
- }
- })()
- : '-'}
-
-
- {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 '-';
- }
- })()
- : '-'}
-
-
- ))}
-
-
-
-
- {totalPagesFiltered > 1 && (
-
-
-
-
- currentPage > 1 && paginate(currentPage - 1)}
- className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
- />
-
-
- {getPageNumbers().map((page, index) => (
-
- {page === 'ellipsis-start' || page === 'ellipsis-end' ? (
-
- ) : (
- typeof page === 'number' && paginate(page)}
- className={typeof page === 'number' ? "cursor-pointer" : ""}
- >
- {page}
-
- )}
-
- ))}
-
-
- currentPage < totalPagesFiltered && paginate(currentPage + 1)}
- className={currentPage === totalPagesFiltered ? "pointer-events-none opacity-50" : "cursor-pointer"}
- />
-
-
-
-
- )}
- >
- )}
-
-
-
-
-
-
-
- >
- );
-
- const renderFormStep = () => {
- if (!fieldOptions) return Loading...
;
-
- // Helper function to sort options with selected value at top
- 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);
- if (aSelected && !bSelected) return -1;
- if (!aSelected && bSelected) return 1;
- return 0;
- }
- if (a.value === selectedValue) return -1;
- if (b.value === selectedValue) return 1;
- return 0;
- });
- };
-
- // Helper function to get selected category labels
- const getSelectedCategoryLabels = () => {
- if (!formData.categories?.length) return "";
- const labels = formData.categories
- .map(value => fieldOptions.categories.find(cat => cat.value === value)?.label)
- .filter(Boolean);
- return labels.join(", ");
- };
-
- // Sort categories with selected ones first and respect levels
- const getSortedCategories = () => {
- const selected = new Set(formData.categories || []);
-
- // Filter categories to only include types 10, 11, 12, and 13 (proper categories, not themes)
- const validCategoryTypes = [10, 11, 12, 13];
- const filteredCategories = fieldOptions.categories.filter(
- category => category.type && validCategoryTypes.includes(category.type)
- );
-
- return [...filteredCategories].sort((a, b) => {
- const aSelected = selected.has(a.value);
- const bSelected = selected.has(b.value);
- if (aSelected && !bSelected) return -1;
- if (!aSelected && bSelected) return 1;
- return 0;
- });
- };
-
- return (
- <>
-
- Create Template from Product
-
- Create a new template for importing products. Company and Product Type combination must be unique.
-
-
-
-
- >
- );
- };
-
- return (
-
- );
-}
\ No newline at end of file
diff --git a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx
index 6c10a0c..577f73f 100644
--- a/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx
+++ b/inventory/src/lib/react-spreadsheet-import/src/steps/ValidationStepNew/components/ValidationContainer.tsx
@@ -6,13 +6,14 @@ import { Loader2, X, Plus, Edit3, Sparkles, FileText } from 'lucide-react'
import { toast } from 'sonner'
import { Switch } from '@/components/ui/switch'
import { useRsi } from '../../../hooks/useRsi'
-import { ProductSearchDialog } from '@/components/products/ProductSearchDialog'
import SearchableTemplateSelect from './SearchableTemplateSelect'
import { useAiValidation } from '../hooks/useAiValidation'
import { AiValidationDialogs } from './AiValidationDialogs'
import config from '@/config'
import { Fields } from '../../../types'
import { SearchProductTemplateDialog } from '@/components/templates/SearchProductTemplateDialog'
+import { TemplateForm } from '@/components/templates/TemplateForm'
+import axios from 'axios'
/**
* ValidationContainer component - the main wrapper for the validation step
@@ -576,6 +577,143 @@ const ValidationContainer = ({
// State for product search dialog
const [isProductSearchDialogOpen, setIsProductSearchDialogOpen] = useState(false)
+ // Add new state for template form dialog
+ const [isTemplateFormOpen, setIsTemplateFormOpen] = useState(false)
+ const [templateFormInitialData, setTemplateFormInitialData] = useState(null)
+ const [fieldOptions, setFieldOptions] = useState(null)
+
+ // Function to fetch field options for template form
+ const fetchFieldOptions = useCallback(async () => {
+ try {
+ const response = await axios.get('/api/import/field-options');
+ console.log('Field options from API:', response.data);
+
+ // Check if suppliers are included in the response
+ if (response.data && response.data.suppliers) {
+ console.log('Suppliers available:', response.data.suppliers.length);
+ } else {
+ console.warn('No suppliers found in field options response');
+ }
+
+ setFieldOptions(response.data);
+ return response.data;
+ } catch (error) {
+ console.error('Error fetching field options:', error);
+ toast.error('Failed to load field options');
+ return null;
+ }
+ }, []);
+
+ // Function to prepare row data for the template form
+ const prepareRowDataForTemplateForm = useCallback(() => {
+ // Get the selected row key (should be only one)
+ const selectedKey = Object.entries(rowSelection)
+ .filter(([_, selected]) => selected === true)
+ .map(([key, _]) => key)[0];
+
+ if (!selectedKey) return null;
+
+ // Try to find the row in the data array
+ let selectedRow;
+
+ // First check if the key is an index in filteredData
+ const numericIndex = parseInt(selectedKey);
+ if (!isNaN(numericIndex) && numericIndex >= 0 && numericIndex < filteredData.length) {
+ selectedRow = filteredData[numericIndex];
+ }
+
+ // If not found by index, try to find it by __index property
+ if (!selectedRow) {
+ selectedRow = data.find(row => row.__index === selectedKey);
+ }
+
+ // If still not found, return null
+ if (!selectedRow) {
+ console.error('Selected row not found:', selectedKey);
+ return null;
+ }
+
+ // TemplateForm expects supplier as a NUMBER - the field options have numeric values
+ // Convert the supplier to a number if possible
+ let supplierValue;
+ if (selectedRow.supplier) {
+ const numSupplier = Number(selectedRow.supplier);
+ supplierValue = !isNaN(numSupplier) ? numSupplier : selectedRow.supplier;
+ } else {
+ supplierValue = undefined;
+ }
+
+ // Create template form data with the correctly typed supplier value
+ return {
+ company: selectedRow.company || '',
+ product_type: selectedRow.product_type || '',
+ supplier: supplierValue,
+ msrp: selectedRow.msrp ? Number(Number(selectedRow.msrp).toFixed(2)) : undefined,
+ cost_each: selectedRow.cost_each ? Number(Number(selectedRow.cost_each).toFixed(2)) : undefined,
+ qty_per_unit: selectedRow.qty_per_unit ? Number(selectedRow.qty_per_unit) : undefined,
+ case_qty: selectedRow.case_qty ? Number(selectedRow.case_qty) : undefined,
+ hts_code: selectedRow.hts_code || undefined,
+ description: selectedRow.description || undefined,
+ weight: selectedRow.weight ? Number(Number(selectedRow.weight).toFixed(2)) : undefined,
+ length: selectedRow.length ? Number(Number(selectedRow.length).toFixed(2)) : undefined,
+ width: selectedRow.width ? Number(Number(selectedRow.width).toFixed(2)) : undefined,
+ height: selectedRow.height ? Number(Number(selectedRow.height).toFixed(2)) : undefined,
+ tax_cat: selectedRow.tax_cat ? String(selectedRow.tax_cat) : undefined,
+ size_cat: selectedRow.size_cat ? String(selectedRow.size_cat) : undefined,
+ categories: Array.isArray(selectedRow.categories) ? selectedRow.categories :
+ (selectedRow.categories ? [selectedRow.categories] : []),
+ ship_restrictions: selectedRow.ship_restrictions ? String(selectedRow.ship_restrictions) : undefined
+ };
+ }, [data, filteredData, rowSelection]);
+
+ // Add useEffect to fetch field options when template form opens
+ useEffect(() => {
+ if (isTemplateFormOpen && !fieldOptions) {
+ fetchFieldOptions();
+ }
+ }, [isTemplateFormOpen, fieldOptions, fetchFieldOptions]);
+
+ // Function to handle opening the template form
+ const openTemplateForm = useCallback(async () => {
+ const templateData = prepareRowDataForTemplateForm();
+ if (!templateData) return;
+
+ setTemplateFormInitialData(templateData);
+
+ // Always fetch fresh field options to ensure supplier list is up to date
+ try {
+ const options = await fetchFieldOptions();
+ if (options && options.suppliers) {
+ console.log(`Loaded ${options.suppliers.length} suppliers for template form`);
+
+ // Log if we can find a match for our supplier
+ if (templateData.supplier !== undefined) {
+ // Need to compare numeric values since supplier options have numeric values
+ const supplierMatch = options.suppliers.find(s =>
+ s.value === templateData.supplier ||
+ Number(s.value) === Number(templateData.supplier)
+ );
+
+ console.log('Found supplier match:', supplierMatch ? 'Yes' : 'No',
+ 'For supplier value:', templateData.supplier,
+ 'Type:', typeof templateData.supplier);
+
+ if (supplierMatch) {
+ console.log('Matched supplier:', supplierMatch.label);
+ }
+ }
+
+ setIsTemplateFormOpen(true);
+ } else {
+ console.error('Failed to load suppliers for template form');
+ toast.error('Could not load supplier options');
+ }
+ } catch (error) {
+ console.error('Error loading field options:', error);
+ toast.error('Failed to prepare template form');
+ }
+ }, [prepareRowDataForTemplateForm, fetchFieldOptions]);
+
// Handle next button click - memoized
const handleNext = useCallback(() => {
// Make sure any pending item numbers are applied
@@ -864,7 +1002,7 @@ const ValidationContainer = ({
@@ -956,6 +1094,19 @@ const ValidationContainer = ({
onClose={() => setIsProductSearchDialogOpen(false)}
onTemplateCreated={loadTemplates}
/>
+
+ {/* Template Form Dialog */}
+ setIsTemplateFormOpen(false)}
+ onSuccess={() => {
+ loadTemplates();
+ setIsTemplateFormOpen(false);
+ }}
+ initialData={templateFormInitialData}
+ mode="create"
+ fieldOptions={fieldOptions}
+ />
)
}