252 lines
8.5 KiB
TypeScript
252 lines
8.5 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import {
|
|
ArrowRightIcon,
|
|
BuildingOffice2Icon,
|
|
PencilIcon,
|
|
CheckIcon,
|
|
XMarkIcon,
|
|
} from '@heroicons/react/24/outline';
|
|
import { toast } from 'sonner';
|
|
import { api } from '../lib/api';
|
|
import type { ApiResponse } from '../lib/api';
|
|
import { formatDate } from '../lib/utils';
|
|
|
|
const HUES_LIST = [256, 205, 162, 295, 272];
|
|
|
|
interface ClinicDetail {
|
|
uuid: string;
|
|
name: string;
|
|
is_active: boolean;
|
|
phone: string | null;
|
|
logo: string | null;
|
|
doctors_count?: number;
|
|
created_at?: number;
|
|
}
|
|
|
|
export default function ClinicDetailPage() {
|
|
const { uuid } = useParams<{ uuid: string }>();
|
|
const navigate = useNavigate();
|
|
const qc = useQueryClient();
|
|
|
|
const [editOpen, setEditOpen] = useState(false);
|
|
const [editName, setEditName] = useState('');
|
|
const [editPhone, setEditPhone] = useState('');
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['clinic-detail', uuid],
|
|
queryFn: () => api.get<ApiResponse<ClinicDetail>>(`/api/v1/clinic/${uuid}`),
|
|
enabled: !!uuid,
|
|
});
|
|
|
|
const clinic: ClinicDetail | undefined = data?.data;
|
|
|
|
useEffect(() => {
|
|
if (clinic) {
|
|
setEditName(clinic.name ?? '');
|
|
setEditPhone(clinic.phone ?? '');
|
|
}
|
|
}, [clinic]);
|
|
|
|
const toggleMutation = useMutation({
|
|
mutationFn: () => api.patch<ApiResponse<{ is_active: boolean }>>(`/api/v1/admin/clinic/${uuid}/status`, {}),
|
|
onSuccess: () => {
|
|
toast.success('وضعیت کلینیک تغییر کرد');
|
|
qc.invalidateQueries({ queryKey: ['clinic-detail', uuid] });
|
|
qc.invalidateQueries({ queryKey: ['admin-clinics'] });
|
|
},
|
|
onError: (err: Error) => toast.error(err.message),
|
|
});
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: (d: { name: string; phone?: string }) =>
|
|
api.patch<ApiResponse<ClinicDetail>>(`/api/v1/clinic/${uuid}`, d),
|
|
onSuccess: () => {
|
|
toast.success('اطلاعات کلینیک ذخیره شد');
|
|
setEditOpen(false);
|
|
qc.invalidateQueries({ queryKey: ['clinic-detail', uuid] });
|
|
qc.invalidateQueries({ queryKey: ['admin-clinics'] });
|
|
},
|
|
onError: (err: Error) => toast.error(err.message),
|
|
});
|
|
|
|
const hue = HUES_LIST[(uuid?.charCodeAt(0) ?? 0) % HUES_LIST.length];
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="fade-in">
|
|
<div className="card card-pad">
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
<div key={i} className="skeleton" style={{ height: 22, borderRadius: 6, marginBottom: 12 }} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!clinic) {
|
|
return (
|
|
<div className="fade-in">
|
|
<div className="card card-pad">
|
|
<div className="empty">
|
|
<BuildingOffice2Icon style={{ width: 36, height: 36 }} />
|
|
<p>کلینیک یافت نشد</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="fade-in">
|
|
{/* Header */}
|
|
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<button
|
|
className="btn ghost sm"
|
|
onClick={() => navigate('/admin/clinics')}
|
|
style={{ padding: '6px 10px' }}
|
|
>
|
|
<ArrowRightIcon style={{ width: 16, height: 16 }} />
|
|
</button>
|
|
<div>
|
|
<h1 className="section-title">{clinic.name}</h1>
|
|
<div className="muted">جزئیات کلینیک</div>
|
|
</div>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<button
|
|
className={`btn sm ${clinic.is_active ? 'soft' : 'primary'}`}
|
|
onClick={() => toggleMutation.mutate()}
|
|
disabled={toggleMutation.isPending}
|
|
>
|
|
{clinic.is_active ? 'غیرفعال کردن' : 'فعال کردن'}
|
|
</button>
|
|
<button className="btn ghost sm" onClick={() => setEditOpen(true)}>
|
|
<PencilIcon style={{ width: 15, height: 15 }} />
|
|
ویرایش
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main info card */}
|
|
<div className="card card-pad">
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 20 }}>
|
|
{clinic.logo ? (
|
|
<img
|
|
src={clinic.logo}
|
|
alt=""
|
|
className="avatar lg"
|
|
style={{ objectFit: 'cover' }}
|
|
/>
|
|
) : (
|
|
<div
|
|
className="avatar lg"
|
|
style={{
|
|
background: `linear-gradient(145deg, oklch(0.62 0.15 ${hue}), oklch(0.48 0.16 ${hue}))`,
|
|
fontSize: 28,
|
|
fontWeight: 700,
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{clinic.name?.[0] ?? '?'}
|
|
</div>
|
|
)}
|
|
<div>
|
|
<div style={{ fontWeight: 700, fontSize: 18 }}>{clinic.name}</div>
|
|
<span className={`badge ${clinic.is_active ? 'green' : 'gray'}`} style={{ marginTop: 4 }}>
|
|
<span className="bdot" />
|
|
{clinic.is_active ? 'فعال' : 'غیرفعال'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 16 }}>
|
|
<InfoCard label="تلفن" value={clinic.phone ? <span dir="ltr">{clinic.phone}</span> : '—'} />
|
|
{clinic.doctors_count !== undefined && (
|
|
<InfoCard
|
|
label="تعداد پزشکان"
|
|
value={
|
|
<span className="badge blue">
|
|
<span className="bdot" />
|
|
{clinic.doctors_count} پزشک
|
|
</span>
|
|
}
|
|
/>
|
|
)}
|
|
{clinic.created_at && (
|
|
<InfoCard label="تاریخ ثبت" value={formatDate(String(clinic.created_at))} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Edit modal */}
|
|
{editOpen && (
|
|
<div className="overlay" onClick={() => setEditOpen(false)}>
|
|
<div className="modal" style={{ maxWidth: 420 }} onClick={(e) => e.stopPropagation()}>
|
|
<div className="modal-head">
|
|
<b>ویرایش کلینیک</b>
|
|
<button className="mini-btn" onClick={() => setEditOpen(false)}>
|
|
<XMarkIcon style={{ width: 16, height: 16 }} />
|
|
</button>
|
|
</div>
|
|
<div className="modal-body" style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
|
<div>
|
|
<label style={{ fontSize: 13, fontWeight: 600, marginBottom: 6, display: 'block' }}>
|
|
نام کلینیک
|
|
</label>
|
|
<input
|
|
className="input"
|
|
value={editName}
|
|
onChange={(e) => setEditName(e.target.value)}
|
|
placeholder="نام کلینیک"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label style={{ fontSize: 13, fontWeight: 600, marginBottom: 6, display: 'block' }}>
|
|
تلفن
|
|
</label>
|
|
<input
|
|
className="input"
|
|
value={editPhone}
|
|
onChange={(e) => setEditPhone(e.target.value)}
|
|
placeholder="مثال: 021-12345678"
|
|
dir="ltr"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="modal-foot">
|
|
<button className="btn ghost sm" onClick={() => setEditOpen(false)}>
|
|
انصراف
|
|
</button>
|
|
<button
|
|
className="btn primary sm"
|
|
disabled={updateMutation.isPending || !editName.trim()}
|
|
onClick={() => updateMutation.mutate({ name: editName.trim(), phone: editPhone.trim() || undefined })}
|
|
>
|
|
<CheckIcon style={{ width: 15, height: 15 }} />
|
|
{updateMutation.isPending ? 'در حال ذخیره...' : 'ذخیره'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function InfoCard({ label, value }: { label: string; value: React.ReactNode }) {
|
|
return (
|
|
<div style={{
|
|
background: 'var(--surface-2, var(--bg))',
|
|
border: '1px solid var(--border)',
|
|
borderRadius: 10,
|
|
padding: '12px 14px',
|
|
}}>
|
|
<div className="muted" style={{ fontSize: 12, marginBottom: 4 }}>{label}</div>
|
|
<div style={{ fontWeight: 600 }}>{value}</div>
|
|
</div>
|
|
);
|
|
}
|