Introduces an exportOrdersToCSV function in lib/api-client.ts to allow exporting orders by status as a CSV file. Updates various UI components to use the '•' (bullet) symbol instead of '·' (middle dot) and replaces some emoji/unicode characters for improved consistency and compatibility. Also normalizes the 'use client' directive to include a BOM in many files.
131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { Search, Package } from "lucide-react";
|
|
import { apiRequest } from "@/lib/api";
|
|
|
|
interface Product {
|
|
_id: string;
|
|
name: string;
|
|
description?: string;
|
|
unitType: string;
|
|
pricing: Array<{ minQuantity: number; pricePerUnit: number }>;
|
|
image?: string;
|
|
}
|
|
|
|
interface ProductSelectorProps {
|
|
selectedProducts: string[];
|
|
onSelectionChange: (productIds: string[]) => void;
|
|
}
|
|
|
|
export default function ProductSelector({ selectedProducts, onSelectionChange }: ProductSelectorProps) {
|
|
const [products, setProducts] = useState<Product[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
|
|
useEffect(() => {
|
|
const fetchProducts = async () => {
|
|
try {
|
|
const fetchedProducts = await apiRequest('/products/for-selection', 'GET');
|
|
setProducts(fetchedProducts);
|
|
} catch (error) {
|
|
console.error('Error fetching products:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchProducts();
|
|
}, []);
|
|
|
|
const filteredProducts = products.filter(product =>
|
|
product.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
const handleProductToggle = (productId: string) => {
|
|
const newSelection = selectedProducts.includes(productId)
|
|
? selectedProducts.filter(id => id !== productId)
|
|
: [...selectedProducts, productId];
|
|
onSelectionChange(newSelection);
|
|
};
|
|
|
|
const getMinPrice = (product: Product) => {
|
|
if (!product.pricing || product.pricing.length === 0) return 0;
|
|
const minTier = product.pricing.reduce((min, tier) =>
|
|
tier.pricePerUnit < min.pricePerUnit ? tier : min
|
|
);
|
|
return minTier.pricePerUnit;
|
|
};
|
|
|
|
if (loading) {
|
|
return <div className="text-center py-4">Loading products...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Search products..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
|
|
<ScrollArea className="h-48">
|
|
<div className="space-y-2">
|
|
{filteredProducts.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
<Package className="h-8 w-8 mx-auto mb-2" />
|
|
{searchTerm ? "No products found" : "No products available"}
|
|
</div>
|
|
) : (
|
|
filteredProducts.map((product) => (
|
|
<div
|
|
key={product._id}
|
|
className="flex items-start space-x-3 p-3 border rounded-lg hover:bg-muted/50"
|
|
>
|
|
<Checkbox
|
|
checked={selectedProducts.includes(product._id)}
|
|
onCheckedChange={() => handleProductToggle(product._id)}
|
|
className="mt-0.5"
|
|
/>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-medium text-sm leading-tight mb-1">
|
|
{product.name}
|
|
</p>
|
|
{product.description && (
|
|
<p className="text-xs text-muted-foreground leading-tight line-clamp-2">
|
|
{product.description.length > 80
|
|
? `${product.description.substring(0, 80)}...`
|
|
: product.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="text-sm font-medium text-green-600 dark:text-green-400 flex-shrink-0">
|
|
£{getMinPrice(product).toFixed(2)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
{selectedProducts.length > 0 && (
|
|
<div className="text-sm text-muted-foreground">
|
|
{selectedProducts.length} product(s) selected
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|