import React, { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { EyeIcon, NoSymbolIcon, PlusIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'; import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse, PaginatedResponse } from '../lib/api'; import MobileInput from '../components/ui/MobileInput'; import { iranMobileSchema } from '../lib/utils'; import type { Representation, City } from '../types'; import { formatDate, formatRial, formatNumber } from '../lib/utils'; import DataTable, { Column } from '../components/ui/DataTable'; import { ActiveBadge } from '../components/ui/StatusBadge'; import Pagination from '../components/ui/Pagination'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import Modal from '../components/ui/Modal'; import SearchableSelect from '../components/ui/SearchableSelect'; import { numericField } from '../lib/forms'; const schema = z.object({ full_name: z.string().min(2, 'نام الزامی است'), mobile_number: iranMobileSchema, city_ids: z.array(z.number()).optional(), domain: z.string().optional(), is_global: z.boolean().optional(), commission_percent: z.coerce.number().min(0).max(100), }); type FormData = z.infer; export default function RepresentationsPage() { const navigate = useNavigate(); const qc = useQueryClient(); const [page, setPage] = useState(1); const [search, setSearch] = useState(''); const [cityFilter, setCityFilter] = useState(null); const [addOpen, setAddOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const limit = 15; const citiesQuery = useQuery({ queryKey: ['cities-select'], queryFn: () => api.get>('/api/v1/admin/cities?limit=200'), staleTime: 5 * 60_000, }); const cities: City[] = citiesQuery.data?.data ?? []; const cityOptions = cities.map((c) => ({ value: c.id, label: c.name })); const { data, isLoading } = useQuery({ queryKey: ['representations', page, search, cityFilter], queryFn: () => { const params = new URLSearchParams({ page: String(page), limit: String(limit) }); if (search) params.set('search', search); if (cityFilter !== null) params.set('city_id', String(cityFilter)); return api.get>(`/api/v1/admin/representations?${params}`); }, }); const { register, handleSubmit, reset, control, watch, formState: { errors, isSubmitting } } = useForm({ resolver: zodResolver(schema), defaultValues: { commission_percent: 10, city_ids: [], is_global: false }, }); const isGlobal = watch('is_global') ?? false; const createMutation = useMutation({ mutationFn: (d: FormData) => api.post>('/api/v1/representation', { full_name: d.full_name, mobile_number: d.mobile_number, city_ids: d.city_ids ?? [], is_global: d.is_global ?? false, ...(d.domain?.trim() && { domain: d.domain.trim() }), commission_percent: d.commission_percent, }), onSuccess: () => { toast.success('نماینده اضافه شد'); setAddOpen(false); reset(); qc.invalidateQueries({ queryKey: ['representations'] }); }, onError: (err: Error) => toast.error(err.message), }); const deactivateMutation = useMutation({ mutationFn: (r: Representation) => api.patch>(`/api/v1/representation/${r.uuid}`, { active: false }), onSuccess: () => { toast.success('نماینده غیرفعال شد'); setDeleteTarget(null); qc.invalidateQueries({ queryKey: ['representations'] }); }, onError: (err: Error) => toast.error(err.message), }); const columns: Column[] = [ { key: 'full_name', header: 'نام', render: (r) => ( {r.full_name} {r.is_global && سراسری ✓} ) }, { key: 'mobile_number', header: 'موبایل', render: (r) => {r.mobile_number ?? '—'} }, { key: 'domain', header: 'دامنه', render: (r) => r.domain ? {r.domain} : '—' }, { key: 'city', header: 'شهرها', render: (r) => r.city ?? '—' }, { key: 'commission_percent', header: 'کمیسیون', render: (r) => `${formatNumber(r.commission_percent)}٪` }, { key: 'doctor_count', header: 'تعداد پزشکان', render: (r) => formatNumber(r.doctor_count ?? 0) }, { key: 'appointment_count', header: 'تعداد نوبت', render: (r) => formatNumber(r.appointment_count ?? 0) }, { key: 'wallet_balance', header: 'موجودی کیف‌پول', render: (r) => formatRial(r.wallet_balance ?? 0) }, { key: 'is_active', header: 'وضعیت', render: (r) => }, { key: 'created_at', header: 'تاریخ ثبت', render: (r) => formatDate(r.created_at) }, ]; const items = data?.data ?? []; const total = data?.meta?.totalRecords ?? 0; return (

نمایندگان

{total} نماینده ثبت‌شده
{ setSearch(e.target.value); setPage(1); }} placeholder="جستجو بر اساس نام یا موبایل..." />
{ setCityFilter(val as number | null); setPage(1); }} placeholder="فیلتر شهر..." isClearable isLoading={citiesQuery.isLoading} noOptionsMessage="هیچ شهری یافت نشد" />
columns={columns} data={items} loading={isLoading} emptyMessage="هیچ نماینده‌ای یافت نشد" actions={(rep) => ( <> {(rep.is_active ?? rep.active) && ( )} )} />
{ setAddOpen(false); reset(); }} footer={ <> } >
createMutation.mutate(d))}>
{errors.full_name &&

{errors.full_name.message}

}
{errors.mobile_number &&

{errors.mobile_number.message}

}
{errors.domain &&

{errors.domain.message}

}
{ const selected: number[] = field.value ?? []; return (
!selected.includes(o.value))} value={null} onChange={(val) => { if (val !== null && !selected.includes(val as number)) { field.onChange([...selected, val as number]); } }} placeholder={isGlobal ? 'اختیاری — افزودن شهر...' : 'افزودن شهر...'} isClearable isLoading={citiesQuery.isLoading} noOptionsMessage="هیچ شهری یافت نشد" /> {selected.length > 0 && (
{selected.map((id) => ( {cityOptions.find((o) => o.value === id)?.label ?? id} ))}
)}
); }} />
{errors.commission_percent &&

{errors.commission_percent.message}

}
deleteTarget && deactivateMutation.mutate(deleteTarget)} onCancel={() => setDeleteTarget(null)} />
); }