canina/frontend/admin-panel/src/pages/Pets.tsx

171 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from 'react';
import { Search, Trash2, Heart, Image as ImageIcon } from 'lucide-react';
import { toast } from 'react-hot-toast';
import api, { BASE_DOMAIN } from '../services/api';
import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination';
import ConfirmModal from '../components/ui/ConfirmModal';
export interface Pet {
id: string;
name: string;
species?: string;
type?: string;
breed?: string;
age?: number;
weight?: number;
activityLevel?: string;
imageUrl?: string;
avatarUrl?: string;
user?: {
firstName: ReactNode;
lastName: ReactNode;
name?: string;
mobile?: string;
email?: string;
};
ownerName?: string;
createdAt?: string;
}
export default function Pets() {
const [pets, setPets] = useState<Pet[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const limit = 10;
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
const fetchPets = useCallback(async () => {
try {
setIsLoading(true);
const res = await api.get('/admin/pets', { params: { page, limit, search } });
if (res.data?.data) {
setPets(res.data.data);
setTotalPages(res.data.meta?.lastPage || 1);
}
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
}, [page, search]);
useEffect(() => {
const timer = setTimeout(() => {
fetchPets();
}, 500);
return () => clearTimeout(timer);
}, [fetchPets]);
const confirmDelete = async () => {
if (!deleteTargetId) return;
try {
await api.delete(`/admin/pets/${deleteTargetId}`);
toast.success('پروفایل حیوان خانگی با موفقیت حذف شد');
fetchPets();
} catch (error) {
console.error('Delete failed', error);
toast.error('خطا در حذف پروفایل');
} finally {
setDeleteTargetId(null);
}
};
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row justify-between gap-4">
<div>
<h2 className="text-2xl font-black text-gray-900 flex items-center gap-2">
<Heart className="w-6 h-6 text-purple-600 fill-purple-100" />
حیوانات خانگی کاربران (Pets)
</h2>
<p className="text-gray-500 font-medium mt-1">مشاهده و مدیریت پروفایل حیوانات خانگی ثبت شده توسط کاربران</p>
</div>
</div>
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="جستجو در نام حیوان..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
/>
</div>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-right">
<thead className="bg-gray-50 border-b border-gray-100">
<tr>
<th className="py-4 px-6 text-sm font-bold text-gray-500">تصویر</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500">نام و نژاد</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500">صاحب حیوان</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500">مشخصات فیزیکی</th>
<th className="py-4 px-6 text-sm font-bold text-gray-500 w-24">عملیات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{isLoading ? (
<tr><td colSpan={5} className="py-12 text-center"><Spinner size="lg" className="mx-auto text-purple-600" /></td></tr>
) : pets.length === 0 ? (
<tr><td colSpan={5} className="py-12 text-center text-gray-500 font-medium">هیچ حیوانی یافت نشد</td></tr>
) : (
pets.map((pet) => (
<tr key={pet.id} className="hover:bg-gray-50/50 transition-colors">
<td className="py-3 px-6">
{pet.imageUrl ? (
<img src={pet.imageUrl.startsWith('http') ? pet.imageUrl : `${BASE_DOMAIN}${pet.imageUrl}`} alt={pet.name} className="w-12 h-12 rounded-full object-cover border-2 border-purple-100" />
) : (
<div className="w-12 h-12 rounded-full bg-purple-50 flex items-center justify-center text-purple-300 border-2 border-purple-100">
<ImageIcon className="w-5 h-5" />
</div>
)}
</td>
<td className="py-4 px-6">
<div className="font-bold text-gray-900">{pet.name} <span className="text-xs px-2 py-0.5 bg-gray-100 text-gray-600 rounded-lg">{pet.type}</span></div>
<div className="text-gray-500 text-sm">{pet.breed}</div>
</td>
<td className="py-4 px-6">
<div className="font-bold text-gray-700">{pet.user?.firstName} {pet.user?.lastName}</div>
<div className="text-gray-500 text-xs font-mono">{pet.user?.mobile || pet.user?.email}</div>
</td>
<td className="py-4 px-6 text-gray-600 text-sm">
{pet.age} ساله، {pet.weight} کیلوگرم<br />
<span className="text-xs text-gray-400">تحرک: {pet.activityLevel}</span>
</td>
<td className="py-4 px-6">
<div className="flex items-center gap-2">
<button onClick={() => setDeleteTargetId(pet.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"><Trash2 className="w-4 h-4" /></button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{!isLoading && totalPages > 1 && (
<div className="p-4 border-t border-gray-100 flex justify-center bg-gray-50/50">
<Pagination currentPage={page} totalPages={totalPages} onPageChange={setPage} />
</div>
)}
</div>
<ConfirmModal
isOpen={!!deleteTargetId}
title="حذف حیوان خانگی"
message="آیا از حذف پروفایل این حیوان خانگی مطمئن هستید؟ این عملیات قابل بازگشت نیست."
onConfirm={confirmDelete}
onCancel={() => setDeleteTargetId(null)}
/>
</div>
);
}