264 lines
9.1 KiB
TypeScript
264 lines
9.1 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useSearchParams } from 'react-router-dom';
|
|
import { Brand } from '../types';
|
|
import { brandApi } from '../services/api';
|
|
import AddBrandModal from './AddBrandModal';
|
|
import ConfirmDeleteModal from './ConfirmDeleteModal';
|
|
|
|
const BrandList: React.FC = () => {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [brands, setBrands] = useState<Brand[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
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();
|
|
|
|
// Check if we should auto-open the modal
|
|
if (searchParams.get('add') === 'true') {
|
|
setIsModalOpen(true);
|
|
// Remove the parameter from URL
|
|
setSearchParams({});
|
|
}
|
|
}, [searchParams, setSearchParams]);
|
|
|
|
const fetchBrands = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await brandApi.getAll();
|
|
setBrands(response.data);
|
|
} catch (err) {
|
|
setError('Failed to fetch brands');
|
|
console.error('Error fetching brands:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleBrandAdded = () => {
|
|
fetchBrands(); // Refresh the brands list
|
|
};
|
|
|
|
const handleEditBrand = (brand: Brand) => {
|
|
setEditingBrand(brand);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleDeleteBrand = (brand: Brand) => {
|
|
setDeletingBrand(brand);
|
|
};
|
|
|
|
const confirmDelete = async () => {
|
|
if (!deletingBrand) return;
|
|
|
|
try {
|
|
setDeleteLoading(true);
|
|
await brandApi.delete(deletingBrand.id);
|
|
setDeletingBrand(null);
|
|
fetchBrands(); // Refresh the brands list
|
|
} catch (err: any) {
|
|
console.error('Error deleting brand:', err);
|
|
// Handle specific error message from backend
|
|
if (err.response?.status === 400) {
|
|
setError('Cannot delete brand: products are still associated with this brand');
|
|
} else {
|
|
setError('Failed to delete brand. Please try again.');
|
|
}
|
|
} finally {
|
|
setDeleteLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCloseModal = () => {
|
|
setIsModalOpen(false);
|
|
setEditingBrand(null);
|
|
};
|
|
|
|
const handleCloseDeleteModal = () => {
|
|
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">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<h1 className="text-2xl font-bold text-gray-900">Brands</h1>
|
|
<button
|
|
onClick={() => setIsModalOpen(true)}
|
|
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
|
>
|
|
Add New Brand
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-white shadow rounded-lg overflow-hidden">
|
|
{brands.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 48 48">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a1.994 1.994 0 01-1.414.586H7m0-18v18m0-18h.01" />
|
|
</svg>
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No brands</h3>
|
|
<p className="mt-1 text-sm text-gray-500">Get started by adding your first brand.</p>
|
|
</div>
|
|
) : (
|
|
<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 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 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 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
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{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">
|
|
{brand.name}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{new Date(brand.created_at).toLocaleDateString()}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{brand.updated_at ? new Date(brand.updated_at).toLocaleDateString() : '-'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
<button
|
|
onClick={() => handleEditBrand(brand)}
|
|
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
|
>
|
|
Edit
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeleteBrand(brand)}
|
|
className="text-red-600 hover:text-red-900"
|
|
>
|
|
Delete
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
|
|
<AddBrandModal
|
|
isOpen={isModalOpen}
|
|
onClose={handleCloseModal}
|
|
onBrandAdded={handleBrandAdded}
|
|
editBrand={editingBrand}
|
|
/>
|
|
|
|
<ConfirmDeleteModal
|
|
isOpen={!!deletingBrand}
|
|
onClose={handleCloseDeleteModal}
|
|
onConfirm={confirmDelete}
|
|
title="Delete Brand"
|
|
message={`Are you sure you want to delete "${deletingBrand?.name}"? This action cannot be undone.`}
|
|
isLoading={deleteLoading}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BrandList;
|