285 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|