canina/frontend/admin-panel/src/pages/Pets.tsx
2026-08-17 11:46:49 +03:30

355 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, Eye, Clock, Activity, FileText, X, Dog, Cat, ShieldCheck } 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?: string;
lastName?: string;
name?: string;
mobile?: string;
email?: string;
};
medicalConditions?: Array<{ condition: string }>;
reminders?: Array<{
id: string;
title: string;
time: string;
frequency: string;
}>;
healthLogs?: Array<{
id: string;
appetite: string;
energy: string;
digestion: string;
note?: string;
loggedDate: string;
createdAt: string;
}>;
prescriptions?: Array<{
id: string;
status: string;
notes?: string;
adminNotes?: string;
createdAt: 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 [selectedPetForView, setSelectedPetForView] = useState<Pet | null>(null);
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 font-vazir text-right" dir="rtl">
<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-xs 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 text-xs font-bold"
/>
</div>
</div>
<div className="bg-white rounded-2xl shadow-xs 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-xs font-black text-gray-500">تصویر</th>
<th className="py-4 px-6 text-xs font-black text-gray-500">نام و نژاد</th>
<th className="py-4 px-6 text-xs font-black text-gray-500">صاحب حیوان</th>
<th className="py-4 px-6 text-xs font-black text-gray-500">مشخصات فیزیکی</th>
<th className="py-4 px-6 text-xs font-black text-gray-500">سوابق سلامت</th>
<th className="py-4 px-6 text-xs font-black text-gray-500 w-28">عملیات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{isLoading ? (
<tr><td colSpan={6} className="py-12 text-center"><Spinner size="lg" className="mx-auto text-purple-600" /></td></tr>
) : pets.length === 0 ? (
<tr><td colSpan={6} 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-2xl object-cover border-2 border-purple-100 shadow-xs" />
) : (
<div className="w-12 h-12 rounded-2xl bg-purple-50 flex items-center justify-center text-purple-400 border border-purple-100">
{pet.type === 'گربه' ? <Cat className="w-6 h-6" /> : <Dog className="w-6 h-6" />}
</div>
)}
</td>
<td className="py-4 px-6">
<div className="font-black text-gray-900 flex items-center gap-1.5">
<span>{pet.name}</span>
<span className="text-[10px] px-2 py-0.5 bg-purple-50 text-purple-700 rounded-md font-bold">{pet.type || 'سگ'}</span>
</div>
<div className="text-gray-500 text-xs mt-0.5">{pet.breed || 'نژاد نامشخص'}</div>
</td>
<td className="py-4 px-6">
<div className="font-bold text-gray-800 text-xs">{pet.user?.firstName || ''} {pet.user?.lastName || ''}</div>
<div className="text-gray-400 text-[11px] font-mono mt-0.5">{pet.user?.mobile || pet.user?.email || '-'}</div>
</td>
<td className="py-4 px-6 text-gray-600 text-xs">
{pet.age ? `${pet.age} ساله` : 'سن نامشخص'}، {pet.weight ? `${pet.weight} کیلوگرم` : 'وزن نامشخص'}<br />
<span className="text-[11px] text-gray-400 font-medium">تحرک: {pet.activityLevel || 'متوسط'}</span>
</td>
<td className="py-4 px-6 text-xs">
<div className="flex flex-col gap-1">
<span className="text-[11px] text-gray-600 font-bold">
{pet.reminders?.length || 0} یادآور • {pet.healthLogs?.length || 0} گزارش
</span>
{pet.prescriptions && pet.prescriptions.length > 0 && (
<span className="text-[10px] text-purple-700 font-bold">
{pet.prescriptions.length} نسخه پزشکی ثبت شده
</span>
)}
</div>
</td>
<td className="py-4 px-6">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setSelectedPetForView(pet)}
className="p-2 text-purple-600 hover:bg-purple-50 rounded-xl transition-colors cursor-pointer"
title="مشاهده پرونده کامل سلامت"
>
<Eye className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => setDeleteTargetId(pet.id)}
className="p-2 text-red-500 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
title="حذف پت"
>
<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>
{/* Pet Medical Dossier Details Modal */}
{selectedPetForView && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir" dir="rtl">
<div className="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl border border-gray-100 flex flex-col">
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between sticky top-0 bg-white z-10">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-purple-100 text-purple-700 flex items-center justify-center font-black">
{selectedPetForView.type === 'گربه' ? <Cat className="w-5 h-5" /> : <Dog className="w-5 h-5" />}
</div>
<div>
<h3 className="text-base font-black text-gray-900">پرونده پزشکی و پایش سلامت: {selectedPetForView.name}</h3>
<p className="text-xs text-gray-400 font-mono">شناسه: {selectedPetForView.id}</p>
</div>
</div>
<button
type="button"
onClick={() => setSelectedPetForView(null)}
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6 space-y-6">
{/* Pet Info Banner */}
<div className="p-4 bg-purple-50/60 rounded-2xl border border-purple-100 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 text-xs">
<div>
<div className="font-black text-gray-900 text-sm">{selectedPetForView.name} ({selectedPetForView.breed || selectedPetForView.type})</div>
<div className="text-gray-600 mt-1">
سرپرست: {selectedPetForView.user?.firstName} {selectedPetForView.user?.lastName} | همراه: <span className="font-mono">{selectedPetForView.user?.mobile || '-'}</span>
</div>
</div>
<div className="text-purple-900 font-bold bg-white px-3 py-1.5 rounded-xl border border-purple-200">
{selectedPetForView.age} سال | {selectedPetForView.weight} کیلوگرم | تحرک {selectedPetForView.activityLevel || 'متوسط'}
</div>
</div>
{/* Medical Conditions */}
<div>
<h4 className="text-xs font-black text-gray-900 flex items-center gap-2 mb-2">
<ShieldCheck className="w-4 h-4 text-purple-600" />
سوابق و حساسیت‌های پزشکی (Medical Conditions)
</h4>
{selectedPetForView.medicalConditions && selectedPetForView.medicalConditions.length > 0 ? (
<div className="flex flex-wrap gap-2">
{selectedPetForView.medicalConditions.map((mc, idx) => (
<span key={idx} className="px-3 py-1 bg-gray-100 text-gray-800 text-xs font-bold rounded-xl border border-gray-200">
{mc.condition}
</span>
))}
</div>
) : (
<p className="text-xs text-gray-400">مورد پزشکی خاصی ثبت نشده است.</p>
)}
</div>
{/* Active Reminders */}
<div>
<h4 className="text-xs font-black text-gray-900 flex items-center gap-2 mb-2">
<Clock className="w-4 h-4 text-purple-600" />
یادآورهای فعال (Reminders)
</h4>
{selectedPetForView.reminders && selectedPetForView.reminders.length > 0 ? (
<div className="space-y-2">
{selectedPetForView.reminders.map((rem) => (
<div key={rem.id} className="p-3 bg-gray-50 rounded-xl border border-gray-200 flex items-center justify-between text-xs">
<span className="font-bold text-gray-800">{rem.title}</span>
<span className="text-purple-700 font-mono font-bold bg-white px-2 py-0.5 rounded-md border border-purple-100">
ساعت {rem.time} ({rem.frequency})
</span>
</div>
))}
</div>
) : (
<p className="text-xs text-gray-400">یادآوری فعالی ثبت نشده است.</p>
)}
</div>
{/* Health Logs */}
<div>
<h4 className="text-xs font-black text-gray-900 flex items-center gap-2 mb-2">
<Activity className="w-4 h-4 text-purple-600" />
آخرین گزارشات روزانه سلامت (Health Logs)
</h4>
{selectedPetForView.healthLogs && selectedPetForView.healthLogs.length > 0 ? (
<div className="space-y-2">
{selectedPetForView.healthLogs.map((log) => (
<div key={log.id} className="p-3 bg-gray-50 rounded-xl border border-gray-200 text-xs space-y-1">
<div className="flex items-center justify-between text-gray-500 font-bold">
<span>تاریخ: {new Date(log.loggedDate || log.createdAt).toLocaleDateString('fa-IR')}</span>
<span className="text-gray-700 font-black">اشتها: {log.appetite} | انرژی: {log.energy} | گوارش: {log.digestion}</span>
</div>
{log.note && <p className="text-gray-700 font-medium">{log.note}</p>}
</div>
))}
</div>
) : (
<p className="text-xs text-gray-400">گزارش سلامتی ثبت نشده است.</p>
)}
</div>
{/* Prescriptions */}
<div>
<h4 className="text-xs font-black text-gray-900 flex items-center gap-2 mb-2">
<FileText className="w-4 h-4 text-purple-600" />
نسخه‌های پزشکی این همدم (Prescriptions)
</h4>
{selectedPetForView.prescriptions && selectedPetForView.prescriptions.length > 0 ? (
<div className="space-y-2">
{selectedPetForView.prescriptions.map((rx) => (
<div key={rx.id} className="p-3 bg-purple-50/50 rounded-xl border border-purple-100 text-xs space-y-1">
<div className="flex items-center justify-between font-bold">
<span className="font-mono">نسخه #{rx.id.slice(0, 8)}</span>
<span className="text-purple-700 font-black">{rx.status}</span>
</div>
{rx.notes && <p className="text-gray-600">یادداشت کاربر: {rx.notes}</p>}
{rx.adminNotes && <p className="text-purple-900 font-bold">دستور پزشک: {rx.adminNotes}</p>}
</div>
))}
</div>
) : (
<p className="text-xs text-gray-400">نسخه‌ای برای این پت ثبت نشده است.</p>
)}
</div>
</div>
</div>
</div>
)}
<ConfirmModal
isOpen={!!deleteTargetId}
title="حذف حیوان خانگی"
message="آیا از حذف پروفایل این حیوان خانگی مطمئن هستید؟ این عملیات قابل بازگشت نیست."
onConfirm={confirmDelete}
onCancel={() => setDeleteTargetId(null)}
/>
</div>
);
}