feat: unify doctor title handling and enhance specialty selection
- Implemented a helper function `displayDoctorName` to prepend "دکتر" to doctor names for consistent display across the application. - Updated various components (InviteDoctorModal, DashboardPage, DoctorDetailPage, DoctorsPage, etc.) to utilize the new helper for rendering doctor names. - Modified the DoctorFormPage to automatically add the "دکتر" title in the UI without requiring user input. - Fixed the EditSpecialtyPicker component to allow multiple specialty selections, resolving a UI bug where only one specialty could be selected at a time. - Ensured that the backend strips the "دکتر" title from the name during pre-registration and doctor creation processes. - Added tests for the new functionality, including checks for title handling and specialty selection logic. - Updated API documentation to reflect changes in name handling and display logic.
This commit is contained in:
@@ -23,7 +23,7 @@ import 'leaflet/dist/leaflet.css';
|
||||
import { api, ApiError } from '../lib/api';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import type { ApiResponse } from '../lib/api';
|
||||
import { formatNumber, iranMobileOptionalSchema, toDate, toGregorianDate } from '../lib/utils';
|
||||
import { formatNumber, iranMobileOptionalSchema, toDate, toGregorianDate, displayDoctorName } from '../lib/utils';
|
||||
import MobileInput from '../components/ui/MobileInput';
|
||||
import Modal from '../components/ui/Modal';
|
||||
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
||||
@@ -34,6 +34,7 @@ import ImageCropModal from '../components/ImageCropModal';
|
||||
import { ScheduleSection } from '../components/schedule/ScheduleSection';
|
||||
import type { AddressData } from '../components/schedule/ScheduleSection';
|
||||
import { latinDigitsField } from '../lib/forms';
|
||||
import { toggleSpecialtyChild, toggleSpecialtyRoot, removeSpecialtyEntry } from '../lib/specialtySelection';
|
||||
|
||||
// Fix leaflet default marker icons
|
||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||
@@ -856,25 +857,13 @@ function EditSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
const isRootSelected = (parentId: number): boolean =>
|
||||
!(childMap[parentId]?.length) && selected.includes(parentId);
|
||||
|
||||
const selectChild = (child: SpecialtyOpt) => {
|
||||
const parentId = child.parent_id!;
|
||||
if (selected.includes(child.id)) {
|
||||
onChange([]);
|
||||
} else {
|
||||
onChange([parentId, child.id]);
|
||||
}
|
||||
};
|
||||
const selectChild = (child: SpecialtyOpt) =>
|
||||
onChange(toggleSpecialtyChild(selected, child.id, child.parent_id!, childMap));
|
||||
|
||||
const selectRoot = (root: SpecialtyOpt) => {
|
||||
if (selected.includes(root.id)) {
|
||||
onChange([]);
|
||||
} else {
|
||||
setActiveParentId(null);
|
||||
onChange([root.id]);
|
||||
}
|
||||
};
|
||||
const selectRoot = (root: SpecialtyOpt) => onChange(toggleSpecialtyRoot(selected, root.id));
|
||||
|
||||
const removeEntry = () => { onChange([]); setActiveParentId(null); };
|
||||
const removeEntry = ({ parentId, childId }: EditSelectedEntry) =>
|
||||
onChange(removeSpecialtyEntry(selected, childId, parentId, childMap));
|
||||
|
||||
const chips: EditSelectedEntry[] = useMemo(() => {
|
||||
return selected
|
||||
@@ -901,7 +890,7 @@ function EditSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
return (
|
||||
<span key={childId} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 12, padding: '3px 10px 3px 6px', borderRadius: 999, background: 'var(--primary-soft2)', color: 'var(--primary-700)', fontWeight: 600 }}>
|
||||
{label}
|
||||
<button type="button" onClick={() => removeEntry()}
|
||||
<button type="button" onClick={() => removeEntry({ parentId, childId })}
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 16, height: 16, borderRadius: '50%', background: 'var(--primary-soft)', border: 'none', cursor: 'pointer', color: 'var(--primary-600)', padding: 0, fontSize: 12, fontWeight: 700 }}>×</button>
|
||||
</span>
|
||||
);
|
||||
@@ -919,19 +908,15 @@ function EditSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
const rootSel = !hasChildren && isRootSelected(p.id);
|
||||
const isMarked = childSel !== null || rootSel;
|
||||
const isActive = activeParentId === p.id;
|
||||
const hasSelection = selected.length > 0;
|
||||
const isDisabled = hasSelection && !isMarked;
|
||||
return (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
disabled={isDisabled}
|
||||
onClick={() => hasChildren ? setActiveParentId(p.id) : selectRoot(p)}
|
||||
style={{
|
||||
width: '100%', textAlign: 'right', display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '10px 14px', fontSize: 13, border: 'none',
|
||||
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
||||
opacity: isDisabled ? 0.35 : 1,
|
||||
cursor: 'pointer',
|
||||
background: isActive ? 'var(--primary-soft)' : rootSel ? 'var(--primary-soft)' : 'transparent',
|
||||
color: (isActive || rootSel) ? 'var(--primary-700)' : 'var(--text)',
|
||||
fontWeight: (isActive || rootSel) ? 700 : 400,
|
||||
@@ -964,7 +949,7 @@ function EditSpecialtyPicker({ selected, onChange, specialties }: {
|
||||
) : (
|
||||
<>
|
||||
<div style={{ padding: '8px 14px', fontSize: 11, fontWeight: 700, color: 'var(--text-3)', background: 'var(--surface-2)', borderBottom: '1px solid var(--border)', letterSpacing: '.2px' }}>
|
||||
انتخاب تخصص — یک مورد
|
||||
انتخاب تخصص — چند مورد مجاز
|
||||
</div>
|
||||
{activeChildren.map(s => {
|
||||
const checked = selected.includes(s.id);
|
||||
@@ -1442,7 +1427,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 sm:mb-1">
|
||||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-50">دکتر {doctor.name}</h1>
|
||||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-50">{displayDoctorName(doctor.name)}</h1>
|
||||
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
|
||||
{/* Active status badge */}
|
||||
{doctor.active
|
||||
@@ -1720,7 +1705,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
<EditSectionHeader icon={<PhoneIcon style={{ width: 16, height: 16 }} />} title="اطلاعات حساب" />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
||||
<EditField label="نام کامل" required error={(register('name') as any)?.formState?.errors?.name?.message}>
|
||||
<input type="text" className="cp-input" placeholder="دکتر ..." {...register('name')} />
|
||||
<input type="text" className="cp-input" placeholder="مثلاً: حامد حسینی" {...register('name')} />
|
||||
</EditField>
|
||||
<EditField label="شماره موبایل مطب">
|
||||
<MobileInput className="cp-input" {...register('mobile_number')} />
|
||||
@@ -1823,7 +1808,7 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
|
||||
) : (
|
||||
<EditSpecialtyPicker
|
||||
selected={watchedSpecialties}
|
||||
onChange={ids => setValue('specialties', ids)}
|
||||
onChange={ids => setValue('specialties', ids, { shouldDirty: true })}
|
||||
specialties={specialties}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user