sorting in tables

This commit is contained in:
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" 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> <option value={0}>Select a product</option>
{products.map(product => ( {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}> <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} {product.name}{product.organic ? '🌱' : ''}{product.brand ? ` (${product.brand.name})` : ''} {product.weight ? `${product.weight}${product.weight_unit}` : product.weight_unit}
</option> </option>
))
}
</optgroup>
))} ))}
</select> </select>
</div> </div>

View File

@@ -14,6 +14,8 @@ const BrandList: React.FC = () => {
const [editingBrand, setEditingBrand] = useState<Brand | null>(null); const [editingBrand, setEditingBrand] = useState<Brand | null>(null);
const [deletingBrand, setDeletingBrand] = useState<Brand | null>(null); const [deletingBrand, setDeletingBrand] = useState<Brand | null>(null);
const [deleteLoading, setDeleteLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false);
const [sortField, setSortField] = useState<keyof Brand>('name');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
useEffect(() => { useEffect(() => {
fetchBrands(); fetchBrands();
@@ -82,6 +84,58 @@ const BrandList: React.FC = () => {
setDeletingBrand(null); 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) { if (loading) {
return ( return (
<div className="flex justify-center items-center h-64"> <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"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Name
{getSortIcon('name')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Created
{getSortIcon('created_at')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Updated
{getSortIcon('updated_at')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions Actions
@@ -136,7 +208,7 @@ const BrandList: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{brands.map((brand) => ( {sortedBrands.map((brand) => (
<tr key={brand.id} className="hover:bg-gray-50"> <tr key={brand.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900"> <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 [editingCategory, setEditingCategory] = useState<GroceryCategory | null>(null);
const [deletingCategory, setDeletingCategory] = useState<GroceryCategory | null>(null); const [deletingCategory, setDeletingCategory] = useState<GroceryCategory | null>(null);
const [deleteLoading, setDeleteLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false);
const [sortField, setSortField] = useState<keyof GroceryCategory>('name');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
useEffect(() => { useEffect(() => {
fetchCategories(); fetchCategories();
@@ -72,6 +74,58 @@ const GroceryCategoryList: React.FC = () => {
fetchCategories(); 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) { if (loading) {
return ( return (
<div className="flex justify-center items-center h-64"> <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"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Name
{getSortIcon('name')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Created
{getSortIcon('created_at')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions Actions
@@ -127,7 +193,7 @@ const GroceryCategoryList: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{categories.map((category) => ( {sortedCategories.map((category) => (
<tr key={category.id} className="hover:bg-gray-50"> <tr key={category.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900"> <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 [editingGrocery, setEditingGrocery] = useState<Grocery | null>(null);
const [deletingGrocery, setDeletingGrocery] = useState<Grocery | null>(null); const [deletingGrocery, setDeletingGrocery] = useState<Grocery | null>(null);
const [deleteLoading, setDeleteLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false);
const [sortField, setSortField] = useState<string>('name');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
useEffect(() => { useEffect(() => {
fetchGroceries(); fetchGroceries();
@@ -72,6 +74,76 @@ const GroceryList: React.FC = () => {
fetchGroceries(); 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) { if (loading) {
return ( return (
<div className="flex justify-center items-center h-64"> <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"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Name
{getSortIcon('name')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Category
{getSortIcon('category')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Created
{getSortIcon('created_at')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions Actions
@@ -130,7 +220,7 @@ const GroceryList: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{groceries.map((grocery) => ( {sortedGroceries.map((grocery) => (
<tr key={grocery.id} className="hover:bg-gray-50"> <tr key={grocery.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900"> <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 [editingProduct, setEditingProduct] = useState<Product | null>(null);
const [deletingProduct, setDeletingProduct] = useState<Product | null>(null); const [deletingProduct, setDeletingProduct] = useState<Product | null>(null);
const [deleteLoading, setDeleteLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false);
const [sortField, setSortField] = useState<string>('name');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
useEffect(() => { useEffect(() => {
fetchProducts(); fetchProducts();
@@ -77,6 +79,92 @@ const ProductList: React.FC = () => {
setDeletingProduct(null); 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) { if (loading) {
return ( return (
<div className="flex justify-center items-center h-64"> <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"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Name
{getSortIcon('name')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Grocery
{getSortIcon('grocery')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 cursor-pointer hover:bg-gray-100 select-none"
onClick={() => handleSort('brand')}
>
<div className="flex items-center">
Brand Brand
{getSortIcon('brand')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Weight
</th> {getSortIcon('weight')}
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> </div>
Organic
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions Actions
@@ -140,7 +258,7 @@ const ProductList: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{products.map((product) => ( {sortedProducts.map((product) => (
<tr key={product.id} className="hover:bg-gray-50"> <tr key={product.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900"> <div className="text-sm font-medium text-gray-900">
@@ -149,7 +267,9 @@ const ProductList: React.FC = () => {
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{product.grocery.name}</div> <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>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{product.brand ? product.brand.name : '-'} {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"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{product.weight ? `${product.weight}${product.weight_unit}` : '-'} {product.weight ? `${product.weight}${product.weight_unit}` : '-'}
</td> </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"> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button <button
onClick={() => handleEdit(product)} onClick={() => handleEdit(product)}

View File

@@ -14,6 +14,8 @@ const ShopList: React.FC = () => {
const [editingShop, setEditingShop] = useState<Shop | null>(null); const [editingShop, setEditingShop] = useState<Shop | null>(null);
const [deletingShop, setDeletingShop] = useState<Shop | null>(null); const [deletingShop, setDeletingShop] = useState<Shop | null>(null);
const [deleteLoading, setDeleteLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false);
const [sortField, setSortField] = useState<keyof Shop>('name');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
useEffect(() => { useEffect(() => {
fetchShops(); fetchShops();
@@ -77,6 +79,58 @@ const ShopList: React.FC = () => {
setDeletingShop(null); 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) { if (loading) {
return ( return (
<div className="flex justify-center items-center h-64"> <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"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Name
{getSortIcon('name')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 City
{getSortIcon('city')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Address
{getSortIcon('address')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Created
{getSortIcon('created_at')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions Actions
@@ -134,7 +212,7 @@ const ShopList: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{shops.map((shop) => ( {sortedShops.map((shop) => (
<tr key={shop.id} className="hover:bg-gray-50"> <tr key={shop.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900"> <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 [hoveredEvent, setHoveredEvent] = useState<ShoppingEvent | null>(null);
const [showItemsPopup, setShowItemsPopup] = useState(false); const [showItemsPopup, setShowItemsPopup] = useState(false);
const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 }); const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 });
const [sortField, setSortField] = useState<string>('date');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
useEffect(() => { useEffect(() => {
fetchEvents(); fetchEvents();
@@ -157,6 +159,95 @@ const ShoppingEventList: React.FC = () => {
setShowItemsPopup(true); 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) { if (loading) {
return ( return (
<div className="flex justify-center items-center h-64"> <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"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Shop
{getSortIcon('shop')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Date
{getSortIcon('date')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Items
{getSortIcon('items')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Total
{getSortIcon('total')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <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 Notes
{getSortIcon('notes')}
</div>
</th> </th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions Actions
@@ -217,7 +338,7 @@ const ShoppingEventList: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{events.map((event) => ( {sortedEvents.map((event) => (
<tr key={event.id} className="hover:bg-gray-50"> <tr key={event.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{event.shop.name}</div> <div className="text-sm font-medium text-gray-900">{event.shop.name}</div>