sorting in tables
This commit is contained in:
parent
629a89524c
commit
7037be370e
@ -305,10 +305,28 @@ const AddShoppingEventModal: React.FC<AddShoppingEventModalProps> = ({
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value={0}>Select a product</option>
|
||||
{products.map(product => (
|
||||
<option key={product.id} value={product.id}>
|
||||
{product.name}{product.organic ? '🌱' : ''} ({product.grocery.category.name}) {product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit}
|
||||
</option>
|
||||
{Object.entries(
|
||||
products.reduce((groups, product) => {
|
||||
const category = product.grocery.category.name;
|
||||
if (!groups[category]) {
|
||||
groups[category] = [];
|
||||
}
|
||||
groups[category].push(product);
|
||||
return groups;
|
||||
}, {} as Record<string, typeof products>)
|
||||
)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([category, categoryProducts]) => (
|
||||
<optgroup key={category} label={category}>
|
||||
{categoryProducts
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(product => (
|
||||
<option key={product.id} value={product.id}>
|
||||
{product.name}{product.organic ? '🌱' : ''}{product.brand ? ` (${product.brand.name})` : ''} {product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -14,6 +14,8 @@ const BrandList: React.FC = () => {
|
||||
const [editingBrand, setEditingBrand] = useState<Brand | null>(null);
|
||||
const [deletingBrand, setDeletingBrand] = useState<Brand | null>(null);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [sortField, setSortField] = useState<keyof Brand>('name');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
|
||||
useEffect(() => {
|
||||
fetchBrands();
|
||||
@ -82,6 +84,58 @@ const BrandList: React.FC = () => {
|
||||
setDeletingBrand(null);
|
||||
};
|
||||
|
||||
const handleSort = (field: keyof Brand) => {
|
||||
if (field === sortField) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const sortedBrands = [...brands].sort((a, b) => {
|
||||
let aValue = a[sortField];
|
||||
let bValue = b[sortField];
|
||||
|
||||
// Handle null/undefined values
|
||||
if (aValue === null || aValue === undefined) aValue = '';
|
||||
if (bValue === null || bValue === undefined) bValue = '';
|
||||
|
||||
// Convert to string for comparison
|
||||
const aStr = String(aValue).toLowerCase();
|
||||
const bStr = String(bValue).toLowerCase();
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return aStr.localeCompare(bStr);
|
||||
} else {
|
||||
return bStr.localeCompare(aStr);
|
||||
}
|
||||
});
|
||||
|
||||
const getSortIcon = (field: keyof Brand) => {
|
||||
if (sortField !== field) {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
@ -121,14 +175,32 @@ const BrandList: React.FC = () => {
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('created_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Created
|
||||
{getSortIcon('created_at')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Updated
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('updated_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Updated
|
||||
{getSortIcon('updated_at')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
@ -136,7 +208,7 @@ const BrandList: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{brands.map((brand) => (
|
||||
{sortedBrands.map((brand) => (
|
||||
<tr key={brand.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
|
||||
@ -12,6 +12,8 @@ const GroceryCategoryList: React.FC = () => {
|
||||
const [editingCategory, setEditingCategory] = useState<GroceryCategory | null>(null);
|
||||
const [deletingCategory, setDeletingCategory] = useState<GroceryCategory | null>(null);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [sortField, setSortField] = useState<keyof GroceryCategory>('name');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories();
|
||||
@ -72,6 +74,58 @@ const GroceryCategoryList: React.FC = () => {
|
||||
fetchCategories();
|
||||
};
|
||||
|
||||
const handleSort = (field: keyof GroceryCategory) => {
|
||||
if (field === sortField) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const sortedCategories = [...categories].sort((a, b) => {
|
||||
let aValue = a[sortField];
|
||||
let bValue = b[sortField];
|
||||
|
||||
// Handle null/undefined values
|
||||
if (aValue === null || aValue === undefined) aValue = '';
|
||||
if (bValue === null || bValue === undefined) bValue = '';
|
||||
|
||||
// Convert to string for comparison
|
||||
const aStr = String(aValue).toLowerCase();
|
||||
const bStr = String(bValue).toLowerCase();
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return aStr.localeCompare(bStr);
|
||||
} else {
|
||||
return bStr.localeCompare(aStr);
|
||||
}
|
||||
});
|
||||
|
||||
const getSortIcon = (field: keyof GroceryCategory) => {
|
||||
if (sortField !== field) {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
@ -115,11 +169,23 @@ const GroceryCategoryList: React.FC = () => {
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('created_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Created
|
||||
{getSortIcon('created_at')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
@ -127,7 +193,7 @@ const GroceryCategoryList: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{categories.map((category) => (
|
||||
{sortedCategories.map((category) => (
|
||||
<tr key={category.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
|
||||
@ -12,6 +12,8 @@ const GroceryList: React.FC = () => {
|
||||
const [editingGrocery, setEditingGrocery] = useState<Grocery | null>(null);
|
||||
const [deletingGrocery, setDeletingGrocery] = useState<Grocery | null>(null);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [sortField, setSortField] = useState<string>('name');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
|
||||
useEffect(() => {
|
||||
fetchGroceries();
|
||||
@ -72,6 +74,76 @@ const GroceryList: React.FC = () => {
|
||||
fetchGroceries();
|
||||
};
|
||||
|
||||
const handleSort = (field: string) => {
|
||||
if (field === sortField) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const sortedGroceries = [...groceries].sort((a, b) => {
|
||||
let aValue: any;
|
||||
let bValue: any;
|
||||
|
||||
switch (sortField) {
|
||||
case 'name':
|
||||
aValue = a.name;
|
||||
bValue = b.name;
|
||||
break;
|
||||
case 'category':
|
||||
aValue = a.category.name;
|
||||
bValue = b.category.name;
|
||||
break;
|
||||
case 'created_at':
|
||||
aValue = a.created_at;
|
||||
bValue = b.created_at;
|
||||
break;
|
||||
default:
|
||||
aValue = '';
|
||||
bValue = '';
|
||||
}
|
||||
|
||||
// Handle null/undefined values
|
||||
if (aValue === null || aValue === undefined) aValue = '';
|
||||
if (bValue === null || bValue === undefined) bValue = '';
|
||||
|
||||
// Convert to string for comparison
|
||||
const aStr = String(aValue).toLowerCase();
|
||||
const bStr = String(bValue).toLowerCase();
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return aStr.localeCompare(bStr);
|
||||
} else {
|
||||
return bStr.localeCompare(aStr);
|
||||
}
|
||||
});
|
||||
|
||||
const getSortIcon = (field: string) => {
|
||||
if (sortField !== field) {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
@ -115,14 +187,32 @@ const GroceryList: React.FC = () => {
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Category
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('category')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Category
|
||||
{getSortIcon('category')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('created_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Created
|
||||
{getSortIcon('created_at')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
@ -130,7 +220,7 @@ const GroceryList: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{groceries.map((grocery) => (
|
||||
{sortedGroceries.map((grocery) => (
|
||||
<tr key={grocery.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
|
||||
@ -14,6 +14,8 @@ const ProductList: React.FC = () => {
|
||||
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
|
||||
const [deletingProduct, setDeletingProduct] = useState<Product | null>(null);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [sortField, setSortField] = useState<string>('name');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts();
|
||||
@ -77,6 +79,92 @@ const ProductList: React.FC = () => {
|
||||
setDeletingProduct(null);
|
||||
};
|
||||
|
||||
const handleSort = (field: string) => {
|
||||
if (field === sortField) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const sortedProducts = [...products].sort((a, b) => {
|
||||
let aValue: any;
|
||||
let bValue: any;
|
||||
|
||||
switch (sortField) {
|
||||
case 'name':
|
||||
aValue = a.name;
|
||||
bValue = b.name;
|
||||
break;
|
||||
case 'grocery':
|
||||
aValue = a.grocery.name;
|
||||
bValue = b.grocery.name;
|
||||
break;
|
||||
case 'category':
|
||||
aValue = a.grocery.category.name;
|
||||
bValue = b.grocery.category.name;
|
||||
break;
|
||||
case 'brand':
|
||||
aValue = a.brand?.name || '';
|
||||
bValue = b.brand?.name || '';
|
||||
break;
|
||||
case 'weight':
|
||||
aValue = a.weight || 0;
|
||||
bValue = b.weight || 0;
|
||||
break;
|
||||
default:
|
||||
aValue = '';
|
||||
bValue = '';
|
||||
}
|
||||
|
||||
// Handle null/undefined values
|
||||
if (aValue === null || aValue === undefined) aValue = '';
|
||||
if (bValue === null || bValue === undefined) bValue = '';
|
||||
|
||||
// Convert to string for comparison (except for numbers)
|
||||
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||||
if (sortDirection === 'asc') {
|
||||
return aValue - bValue;
|
||||
} else {
|
||||
return bValue - aValue;
|
||||
}
|
||||
} else {
|
||||
const aStr = String(aValue).toLowerCase();
|
||||
const bStr = String(bValue).toLowerCase();
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return aStr.localeCompare(bStr);
|
||||
} else {
|
||||
return bStr.localeCompare(aStr);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const getSortIcon = (field: string) => {
|
||||
if (sortField !== field) {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
@ -119,20 +207,50 @@ const ProductList: React.FC = () => {
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Grocery
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('grocery')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Grocery
|
||||
{getSortIcon('grocery')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Brand
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('category')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Category
|
||||
{getSortIcon('category')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Weight
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('brand')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Brand
|
||||
{getSortIcon('brand')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Organic
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('weight')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Weight
|
||||
{getSortIcon('weight')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
@ -140,7 +258,7 @@ const ProductList: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{products.map((product) => (
|
||||
{sortedProducts.map((product) => (
|
||||
<tr key={product.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
@ -149,7 +267,9 @@ const ProductList: React.FC = () => {
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-900">{product.grocery.name}</div>
|
||||
<div className="text-xs text-gray-500">{product.grocery.category.name}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{product.grocery.category.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{product.brand ? product.brand.name : '-'}
|
||||
@ -157,15 +277,6 @@ const ProductList: React.FC = () => {
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{product.weight ? `${product.weight}${product.weight_unit}` : '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
||||
product.organic
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{product.organic ? 'Organic' : 'Conventional'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button
|
||||
onClick={() => handleEdit(product)}
|
||||
|
||||
@ -14,6 +14,8 @@ const ShopList: React.FC = () => {
|
||||
const [editingShop, setEditingShop] = useState<Shop | null>(null);
|
||||
const [deletingShop, setDeletingShop] = useState<Shop | null>(null);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [sortField, setSortField] = useState<keyof Shop>('name');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
|
||||
useEffect(() => {
|
||||
fetchShops();
|
||||
@ -77,6 +79,58 @@ const ShopList: React.FC = () => {
|
||||
setDeletingShop(null);
|
||||
};
|
||||
|
||||
const handleSort = (field: keyof Shop) => {
|
||||
if (field === sortField) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const sortedShops = [...shops].sort((a, b) => {
|
||||
let aValue = a[sortField];
|
||||
let bValue = b[sortField];
|
||||
|
||||
// Handle null/undefined values
|
||||
if (aValue === null || aValue === undefined) aValue = '';
|
||||
if (bValue === null || bValue === undefined) bValue = '';
|
||||
|
||||
// Convert to string for comparison
|
||||
const aStr = String(aValue).toLowerCase();
|
||||
const bStr = String(bValue).toLowerCase();
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return aStr.localeCompare(bStr);
|
||||
} else {
|
||||
return bStr.localeCompare(aStr);
|
||||
}
|
||||
});
|
||||
|
||||
const getSortIcon = (field: keyof Shop) => {
|
||||
if (sortField !== field) {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
@ -116,17 +170,41 @@ const ShopList: React.FC = () => {
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
{getSortIcon('name')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
City
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('city')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
City
|
||||
{getSortIcon('city')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Address
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('address')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Address
|
||||
{getSortIcon('address')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('created_at')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Created
|
||||
{getSortIcon('created_at')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
@ -134,7 +212,7 @@ const ShopList: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{shops.map((shop) => (
|
||||
{sortedShops.map((shop) => (
|
||||
<tr key={shop.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
|
||||
@ -17,6 +17,8 @@ const ShoppingEventList: React.FC = () => {
|
||||
const [hoveredEvent, setHoveredEvent] = useState<ShoppingEvent | null>(null);
|
||||
const [showItemsPopup, setShowItemsPopup] = useState(false);
|
||||
const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 });
|
||||
const [sortField, setSortField] = useState<string>('date');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||
|
||||
useEffect(() => {
|
||||
fetchEvents();
|
||||
@ -157,6 +159,95 @@ const ShoppingEventList: React.FC = () => {
|
||||
setShowItemsPopup(true);
|
||||
};
|
||||
|
||||
const handleSort = (field: string) => {
|
||||
if (field === sortField) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
};
|
||||
|
||||
const sortedEvents = [...events].sort((a, b) => {
|
||||
let aValue: any;
|
||||
let bValue: any;
|
||||
|
||||
switch (sortField) {
|
||||
case 'shop':
|
||||
aValue = a.shop.name;
|
||||
bValue = b.shop.name;
|
||||
break;
|
||||
case 'date':
|
||||
aValue = new Date(a.date);
|
||||
bValue = new Date(b.date);
|
||||
break;
|
||||
case 'items':
|
||||
aValue = a.products.length;
|
||||
bValue = b.products.length;
|
||||
break;
|
||||
case 'total':
|
||||
aValue = a.total_amount || 0;
|
||||
bValue = b.total_amount || 0;
|
||||
break;
|
||||
case 'notes':
|
||||
aValue = a.notes || '';
|
||||
bValue = b.notes || '';
|
||||
break;
|
||||
default:
|
||||
aValue = '';
|
||||
bValue = '';
|
||||
}
|
||||
|
||||
// Handle different data types
|
||||
if (aValue instanceof Date && bValue instanceof Date) {
|
||||
if (sortDirection === 'asc') {
|
||||
return aValue.getTime() - bValue.getTime();
|
||||
} else {
|
||||
return bValue.getTime() - aValue.getTime();
|
||||
}
|
||||
} else if (typeof aValue === 'number' && typeof bValue === 'number') {
|
||||
if (sortDirection === 'asc') {
|
||||
return aValue - bValue;
|
||||
} else {
|
||||
return bValue - aValue;
|
||||
}
|
||||
} else {
|
||||
// String comparison
|
||||
const aStr = String(aValue).toLowerCase();
|
||||
const bStr = String(bValue).toLowerCase();
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return aStr.localeCompare(bStr);
|
||||
} else {
|
||||
return bStr.localeCompare(aStr);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const getSortIcon = (field: string) => {
|
||||
if (sortField !== field) {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortDirection === 'asc') {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<svg className="w-4 h-4 ml-1 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
@ -196,20 +287,50 @@ const ShoppingEventList: React.FC = () => {
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Shop
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('shop')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Shop
|
||||
{getSortIcon('shop')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Date
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('date')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Date
|
||||
{getSortIcon('date')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Items
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('items')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Items
|
||||
{getSortIcon('items')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Total
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('total')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Total
|
||||
{getSortIcon('total')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Notes
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100 select-none"
|
||||
onClick={() => handleSort('notes')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Notes
|
||||
{getSortIcon('notes')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
@ -217,7 +338,7 @@ const ShoppingEventList: React.FC = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{events.map((event) => (
|
||||
{sortedEvents.map((event) => (
|
||||
<tr key={event.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900">{event.shop.name}</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user