Files
clinicpro/assets/admin/pages/RepresentationDetailPage.tsx
T

285 lines
11 KiB
TypeScript

import React, { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { PencilIcon, TrashIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/outline';
import { toast } from 'sonner';
import { api } from '../lib/api';
import type { ApiResponse, PaginatedResponse } from '../lib/api';
import type { Representation, City } from '../types';
import { formatDate, formatNumber } from '../lib/utils';
import PageHeader from '../components/ui/PageHeader';
import { ActiveBadge } from '../components/ui/StatusBadge';
import ConfirmDialog from '../components/ui/ConfirmDialog';
import Modal from '../components/ui/Modal';
function DetailRow({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className="cp-info-row items-start gap-4">
<span className="text-sm text-gray-500 w-40 shrink-0">{label}</span>
<span className="cp-info-value">{value ?? '—'}</span>
</div>
);
}
export default function RepresentationDetailPage() {
const { uuid } = useParams<{ uuid: string }>();
const navigate = useNavigate();
const qc = useQueryClient();
const [deleteOpen, setDeleteOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
const [formData, setFormData] = useState({ full_name: '', city_id: '', mobile_number: '', commission_percent: '' });
const citiesQuery = useQuery({
queryKey: ['cities-select'],
queryFn: () => api.get<PaginatedResponse<City>>('/api/v1/admin/cities?limit=200'),
staleTime: 5 * 60_000,
});
const cities: City[] = citiesQuery.data?.data ?? [];
const { data, isLoading } = useQuery({
queryKey: ['representation', uuid],
queryFn: () => api.get<ApiResponse<{ data: Representation }>>(`/api/v1/representation/${uuid}`),
enabled: !!uuid,
});
const rep: Representation | undefined = (data?.data as any)?.data ?? data?.data;
const updateMutation = useMutation({
mutationFn: (d: Partial<Representation>) =>
api.patch<ApiResponse<Representation>>(`/api/v1/representation/${uuid}`, d),
onSuccess: () => {
toast.success('اطلاعات بروزرسانی شد');
setEditOpen(false);
qc.invalidateQueries({ queryKey: ['representation', uuid] });
qc.invalidateQueries({ queryKey: ['representations'] });
},
onError: (err: Error) => toast.error(err.message),
});
const toggleActiveMutation = useMutation({
mutationFn: () =>
api.patch<ApiResponse<Representation>>(`/api/v1/representation/${uuid}`, {
active: !(rep?.active ?? rep?.is_active),
}),
onSuccess: () => {
toast.success('وضعیت تغییر کرد');
qc.invalidateQueries({ queryKey: ['representation', uuid] });
},
onError: (err: Error) => toast.error(err.message),
});
const deleteMutation = useMutation({
mutationFn: () => api.delete<ApiResponse<null>>(`/api/v1/representation/${uuid}`),
onSuccess: () => {
toast.success('نماینده حذف شد');
navigate('/admin/representations');
},
onError: (err: Error) => toast.error(err.message),
});
const openEdit = () => {
if (!rep) return;
setFormData({
full_name: rep.full_name ?? rep.domain ?? '',
city_id: rep.city_id ? String(rep.city_id) : '',
mobile_number: rep.mobile_number ?? '',
commission_percent: String(rep.commission_percent),
});
setEditOpen(true);
};
const isActive = rep?.active ?? rep?.is_active ?? false;
if (isLoading) {
return (
<div>
<PageHeader
title="جزئیات نماینده"
breadcrumbs={[
{ label: 'داشبورد', to: '/admin/dashboard' },
{ label: 'نمایندگان', to: '/admin/representations' },
{ label: 'جزئیات' },
]}
/>
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6 animate-pulse">
<div className="space-y-4">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="h-8 bg-gray-100 rounded" />
))}
</div>
</div>
</div>
);
}
if (!rep) {
return (
<div>
<PageHeader
title="نماینده یافت نشد"
breadcrumbs={[
{ label: 'داشبورد', to: '/admin/dashboard' },
{ label: 'نمایندگان', to: '/admin/representations' },
]}
/>
<div className="cp-card p-12 text-center text-slate-400 dark:text-slate-500">
نماینده‌ای با این شناسه یافت نشد.
</div>
</div>
);
}
const name = rep.full_name ?? rep.domain ?? '—';
return (
<div>
<PageHeader
title={name}
breadcrumbs={[
{ label: 'داشبورد', to: '/admin/dashboard' },
{ label: 'نمایندگان', to: '/admin/representations' },
{ label: name },
]}
action={
<div className="flex items-center gap-2">
<button
onClick={() => toggleActiveMutation.mutate()}
disabled={toggleActiveMutation.isPending}
className={`flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border transition-colors disabled:opacity-50 ${
isActive
? 'border-red-300 text-red-600 hover:bg-red-50'
: 'border-green-300 text-green-600 hover:bg-green-50'
}`}
>
{isActive
? <><XCircleIcon className="w-4 h-4" /> غیرفعال کردن</>
: <><CheckCircleIcon className="w-4 h-4" /> فعال کردن</>
}
</button>
<button
onClick={openEdit}
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors"
>
<PencilIcon className="w-4 h-4" />
ویرایش
</button>
<button
onClick={() => setDeleteOpen(true)}
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-[10px] border border-red-300 text-red-600 hover:bg-red-50 transition-colors"
>
<TrashIcon className="w-4 h-4" />
حذف
</button>
</div>
}
/>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Basic Info */}
<div className="cp-card p-6">
<h2 className="text-sm font-semibold text-gray-700 mb-4">اطلاعات پایه</h2>
<DetailRow label="نام کامل" value={rep.full_name} />
<DetailRow label="موبایل" value={
rep.mobile_number
? <span dir="ltr">{rep.mobile_number}</span>
: null
} />
<DetailRow label="شهر" value={rep.city ?? (rep.city_id ? `شناسه ${rep.city_id}` : null)} />
<DetailRow label="درصد کمیسیون" value={`${formatNumber(rep.commission_percent)}٪`} />
<DetailRow label="وضعیت" value={<ActiveBadge active={isActive} />} />
<DetailRow label="تاریخ ثبت" value={formatDate(rep.created_at)} />
</div>
{/* Bank Account */}
<div className="cp-card p-6">
<h2 className="text-sm font-semibold text-gray-700 mb-4">اطلاعات بانکی</h2>
{rep.bank_account ? (
<>
<DetailRow label="شماره کارت" value={
rep.bank_account.card
? <span dir="ltr" className="font-mono tracking-widest">{rep.bank_account.card}</span>
: null
} />
<DetailRow label="نام بانک" value={rep.bank_account.bank_name} />
<DetailRow label="شماره شبا" value={
rep.bank_account.iban
? <span dir="ltr" className="font-mono text-xs">{rep.bank_account.iban}</span>
: null
} />
</>
) : (
<p className="text-sm text-gray-400 text-center py-6">اطلاعات بانکی ثبت نشده</p>
)}
</div>
</div>
{/* Edit Modal */}
<Modal open={editOpen} title="ویرایش نماینده"
onClose={() => setEditOpen(false)}
footer={
<>
<button onClick={() => setEditOpen(false)}
className="btn ghost sm">
لغو
</button>
<button
onClick={() => updateMutation.mutate({
full_name: formData.full_name,
city_id: formData.city_id ? parseInt(formData.city_id) : null,
mobile_number: formData.mobile_number || null,
commission_percent: parseFloat(formData.commission_percent) || rep.commission_percent,
} as any)}
disabled={updateMutation.isPending}
className="btn primary sm">
{updateMutation.isPending ? 'در حال ذخیره...' : 'ذخیره'}
</button>
</>
}
>
<div className="space-y-4">
<div>
<label className="">نام کامل</label>
<input value={formData.full_name} onChange={(e) => setFormData((p) => ({ ...p, full_name: e.target.value }))}
className="cp-input h-11" />
</div>
<div>
<label className="">شهر</label>
<select
value={formData.city_id}
onChange={(e) => setFormData((p) => ({ ...p, city_id: e.target.value }))}
className="cp-input h-11"
>
<option value="">انتخاب شهر</option>
{cities.map((c) => (
<option key={c.id} value={String(c.id)}>{c.name}</option>
))}
</select>
</div>
<div>
<label className="">موبایل</label>
<input value={formData.mobile_number} onChange={(e) => setFormData((p) => ({ ...p, mobile_number: e.target.value }))}
dir="ltr" className="cp-input h-11" />
</div>
<div>
<label className="">درصد کمیسیون</label>
<input value={formData.commission_percent} onChange={(e) => setFormData((p) => ({ ...p, commission_percent: e.target.value }))}
type="number" min="0" max="100" dir="ltr"
className="cp-input h-11" />
</div>
</div>
</Modal>
<ConfirmDialog
open={deleteOpen}
title="حذف نماینده"
message={`آیا از حذف نماینده "${name}" اطمینان دارید؟`}
confirmLabel="حذف"
danger
loading={deleteMutation.isPending}
onConfirm={() => deleteMutation.mutate()}
onCancel={() => setDeleteOpen(false)}
/>
</div>
);
}