import React, { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { TrashIcon, PlusIcon, PencilIcon, TagIcon, MapPinIcon, BuildingOffice2Icon, HeartIcon, ShieldCheckIcon, WrenchScrewdriverIcon, PhotoIcon, XMarkIcon, } 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 { PaginatedResponse } from '../lib/api'; import type { Province, City, SpecialtyFull, DoctorService, Insurance, Tag, Representation } from '../types'; import { useAuthStore } from '../stores/authStore'; import DataTable, { Column } from '../components/ui/DataTable'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import Modal from '../components/ui/Modal'; import Pagination from '../components/ui/Pagination'; import SearchableSelect from '../components/ui/SearchableSelect'; type TabKey = 'provinces' | 'cities' | 'specialties' | 'doctor_services' | 'insurances' | 'tags'; interface TabConfig { key: TabKey; label: string; icon: React.ElementType; hue: number; } const TABS: TabConfig[] = [ { key: 'provinces', label: 'استان‌ها', icon: MapPinIcon, hue: 256 }, { key: 'cities', label: 'شهرها', icon: BuildingOffice2Icon, hue: 205 }, { key: 'specialties', label: 'تخصص‌ها', icon: HeartIcon, hue: 295 }, { key: 'doctor_services', label: 'خدمات پزشک', icon: WrenchScrewdriverIcon, hue: 162 }, { key: 'insurances', label: 'بیمه‌ها', icon: ShieldCheckIcon, hue: 205 }, { key: 'tags', label: 'تگ‌ها', icon: TagIcon, hue: 272 }, ]; // ── Logo uploader ────────────────────────────────────────────────────────────── function LogoUploadField({ value, onChange, uploadUrl }: { value: string | null; onChange: (url: string | null) => void; uploadUrl: string; }) { const [uploading, setUploading] = useState(false); const token = useAuthStore((s) => s.token); const handleFile = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setUploading(true); try { const res = await fetch(uploadUrl, { method: 'POST', headers: { 'Content-Disposition': `attachment; filename="${file.name}"`, 'Content-Type': 'application/octet-stream', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: file, }); const json = await res.json(); if (json.success) { onChange(json.data.url); toast.success('لگو آپلود شد'); } else { toast.error(json.errors?.[0]?.message ?? 'خطا در آپلود'); } } catch { toast.error('خطا در آپلود تصویر'); } finally { setUploading(false); e.target.value = ''; } }; return (
{value ? (
logo
) : (
)} JPG، PNG یا WebP
); } // ── Status badge ─────────────────────────────────────────────────────────────── function SBadge({ status }: { status: number }) { return ( {status === 1 ? 'فعال' : 'غیرفعال'} ); } // ── Tab sub-component wrapper ────────────────────────────────────────────────── function TabActions({ label, onClick }: { label: string; onClick: () => void }) { return (
); } // ── Provinces Tab ────────────────────────────────────────────────────────────── const provinceSchema = z.object({ name: z.string().min(1, 'نام الزامی است'), weight: z.string().optional(), status: z.string().optional(), }); type ProvinceForm = z.infer; function ProvincesTab() { const qc = useQueryClient(); const [page, setPage] = useState(1); const [search, setSearch] = useState(''); const [addOpen, setAddOpen] = useState(false); const [editTarget, setEditTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const { data, isLoading } = useQuery({ queryKey: ['admin-provinces', page, search], queryFn: () => api.get>(`/api/v1/admin/provinces?page=${page}&limit=20&search=${encodeURIComponent(search)}`), }); const items = data?.data ?? []; const total = data?.meta?.totalRecords ?? 0; const { register, handleSubmit, reset, control, formState: { errors } } = useForm({ resolver: zodResolver(provinceSchema), defaultValues: { status: '1', weight: '0' }, }); const closeModal = () => { setAddOpen(false); setEditTarget(null); reset({ status: '1', weight: '0' }); }; const createMutation = useMutation({ mutationFn: (d: ProvinceForm) => api.post('/api/v1/admin/province', { name: d.name.trim(), weight: parseInt(d.weight ?? '0'), status: parseInt(d.status ?? '1') }), onSuccess: () => { toast.success('استان اضافه شد'); closeModal(); qc.invalidateQueries({ queryKey: ['admin-provinces'] }); }, onError: (err: Error) => toast.error(err.message), }); const updateMutation = useMutation({ mutationFn: ({ id, d }: { id: number; d: ProvinceForm }) => api.patch(`/api/v1/admin/province/${id}`, { name: d.name.trim(), weight: parseInt(d.weight ?? '0'), status: parseInt(d.status ?? '1') }), onSuccess: () => { toast.success('استان بروزرسانی شد'); closeModal(); qc.invalidateQueries({ queryKey: ['admin-provinces'] }); }, onError: (err: Error) => toast.error(err.message), }); const deleteMutation = useMutation({ mutationFn: (p: Province) => api.delete(`/api/v1/admin/province/${p.id}`), onSuccess: () => { toast.success('استان حذف شد'); setDeleteTarget(null); qc.invalidateQueries({ queryKey: ['admin-provinces'] }); }, onError: (err: Error) => toast.error(err.message), }); const openEdit = (p: Province) => { setEditTarget(p); reset({ name: p.name, weight: String(p.weight), status: String(p.status) }); }; const columns: Column[] = [ { key: 'id', header: 'شناسه', render: (p) => {p.id} }, { key: 'name', header: 'نام', render: (p) => {p.name} }, { key: 'weight', header: 'ترتیب', render: (p) => {p.weight} }, { key: 'status', header: 'وضعیت', render: (p) => }, ]; return ( <> { reset({ status: '1', weight: '0' }); setAddOpen(true); }} /> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در استان‌ها..." emptyMessage="هیچ استانی یافت نشد" actions={(p) => ( <> )} /> {total > 20 &&
} } >
editTarget ? updateMutation.mutate({ id: editTarget.id, d }) : createMutation.mutate(d))}>
{errors.name &&

{errors.name.message}

}
( field.onChange(v ?? '1')} isClearable={false} /> )} />
deleteTarget && deleteMutation.mutate(deleteTarget)} onCancel={() => setDeleteTarget(null)} /> ); } // ── Cities Tab ───────────────────────────────────────────────────────────────── const citySchema = z.object({ name: z.string().min(1, 'نام الزامی است'), province_id: z.number().nullable().optional(), weight: z.string().optional(), status: z.string().optional(), representation_id: z.number().nullable().optional(), contact_phone: z.string().optional(), email: z.string().optional(), description: z.string().optional(), slogan: z.string().optional(), domain: z.string().optional(), keywords: z.string().optional(), footer_description: z.string().optional(), }); type CityForm = z.infer; function CitiesTab() { const qc = useQueryClient(); const [page, setPage] = useState(1); const [search, setSearch] = useState(''); const [addOpen, setAddOpen] = useState(false); const [editTarget, setEditTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const { data, isLoading } = useQuery({ queryKey: ['admin-cities', page, search], queryFn: () => api.get>(`/api/v1/admin/cities?page=${page}&limit=20&search=${encodeURIComponent(search)}`), }); const provincesQuery = useQuery({ queryKey: ['admin-provinces-select'], queryFn: () => api.get>('/api/v1/admin/provinces?limit=100'), staleTime: 60_000, }); const provinceOptions = (provincesQuery.data?.data ?? []).map((p) => ({ value: p.id, label: p.name })); const provinceMap = Object.fromEntries((provincesQuery.data?.data ?? []).map((p) => [p.id, p.name])); const representationsQuery = useQuery({ queryKey: ['representations-select'], queryFn: () => api.get>('/api/v1/admin/representations?limit=200'), staleTime: 2 * 60_000, }); const representationOptions = (representationsQuery.data?.data ?? []).map((r) => ({ value: r.id, label: r.full_name + (r.mobile_number ? ` — ${r.mobile_number}` : ''), })); const representationMap = Object.fromEntries((representationsQuery.data?.data ?? []).map((r) => [r.id, r.full_name])); const items = data?.data ?? []; const total = data?.meta?.totalRecords ?? 0; const { register, handleSubmit, reset, control, formState: { errors } } = useForm({ resolver: zodResolver(citySchema), defaultValues: { status: '1', weight: '0' }, }); const closeModal = () => { setAddOpen(false); setEditTarget(null); reset({ status: '1', weight: '0' }); }; const buildPayload = (d: CityForm) => ({ name: d.name.trim(), weight: parseInt(d.weight ?? '0'), status: parseInt(d.status ?? '1'), province_id: d.province_id ?? null, representation_id: d.representation_id ?? null, ...(d.contact_phone?.trim() && { contact_phone: d.contact_phone.trim() }), ...(d.email?.trim() && { email: d.email.trim() }), ...(d.description?.trim() && { description: d.description.trim() }), ...(d.slogan?.trim() && { slogan: d.slogan.trim() }), ...(d.domain?.trim() && { domain: d.domain.trim() }), ...(d.keywords?.trim() && { keywords: d.keywords.trim() }), ...(d.footer_description?.trim() && { footer_description: d.footer_description.trim() }), }); const createMutation = useMutation({ mutationFn: (d: CityForm) => api.post('/api/v1/admin/city', buildPayload(d)), onSuccess: () => { toast.success('شهر اضافه شد'); closeModal(); qc.invalidateQueries({ queryKey: ['admin-cities'] }); }, onError: (err: Error) => toast.error(err.message), }); const updateMutation = useMutation({ mutationFn: ({ id, d }: { id: number; d: CityForm }) => api.patch(`/api/v1/admin/city/${id}`, buildPayload(d)), onSuccess: () => { toast.success('شهر بروزرسانی شد'); closeModal(); qc.invalidateQueries({ queryKey: ['admin-cities'] }); }, onError: (err: Error) => toast.error(err.message), }); const deleteMutation = useMutation({ mutationFn: (c: City) => api.delete(`/api/v1/admin/city/${c.id}`), onSuccess: () => { toast.success('شهر حذف شد'); setDeleteTarget(null); qc.invalidateQueries({ queryKey: ['admin-cities'] }); }, onError: (err: Error) => toast.error(err.message), }); const openEdit = (c: City) => { setEditTarget(c); reset({ name: c.name, weight: String(c.weight), status: String(c.status), province_id: c.province_id ?? null, representation_id: c.representation_id ?? null, contact_phone: c.contact_phone ?? '', email: c.email ?? '', description: c.description ?? '', slogan: c.slogan ?? '', domain: c.domain ?? '', keywords: c.keywords ?? '', footer_description: c.footer_description ?? '', }); }; const columns: Column[] = [ { key: 'id', header: 'شناسه', render: (c) => {c.id} }, { key: 'name', header: 'نام', render: (c) => {c.name} }, { key: 'province_id', header: 'استان', render: (c) => c.province_id ? {provinceMap[c.province_id] ?? `#${c.province_id}`} : }, { key: 'representation_id', header: 'نماینده', render: (c) => c.representation_id ? {representationMap[c.representation_id] ?? `#${c.representation_id}`} : }, { key: 'domain', header: 'دامنه', render: (c) => c.domain ? {c.domain} : }, { key: 'weight', header: 'ترتیب', render: (c) => {c.weight} }, { key: 'status', header: 'وضعیت', render: (c) => }, ]; return ( <> { reset({ status: '1', weight: '0' }); setAddOpen(true); }} /> columns={columns} data={items} loading={isLoading} searchValue={search} onSearchChange={(v) => { setSearch(v); setPage(1); }} searchPlaceholder="جستجو در شهرها..." emptyMessage="هیچ شهری یافت نشد" actions={(c) => ( <> )} /> {total > 20 &&
} } >
editTarget ? updateMutation.mutate({ id: editTarget.id, d }) : createMutation.mutate(d))}>
{errors.name &&

{errors.name.message}

}
( field.onChange(v as number | null)} placeholder="-- انتخاب استان --" isClearable isLoading={provincesQuery.isLoading} noOptionsMessage="استانی یافت نشد" /> )} />
( field.onChange(v as number | null)} placeholder="انتخاب نماینده..." isClearable isLoading={representationsQuery.isLoading} noOptionsMessage="هیچ نماینده‌ای یافت نشد" /> )} />