sorting in tables

This commit is contained in:
lasse 2025-05-27 23:14:03 +02:00
parent 629a89524c
commit 7037be370e
7 changed files with 620 additions and 64 deletions

View File

@ -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>

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

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

View File

@ -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">

View File

@ -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>