@@ -975,8 +975,8 @@ function AddressModal({ open, onClose, existing, doctorUuid, onSaved }: {
// ── Address Card ───────────────────────────────────────────────────────────
function AddressCard ( { addr , onEdit , onDelete } : {
addr : AddressData ; onEdit : ( ) = > void ; onDelete : ( ) = > void ;
function AddressCard ( { addr , onEdit , onDelete , readOnly = false } : {
addr : AddressData ; onEdit : ( ) = > void ; onDelete : ( ) = > void ; readOnly? : boolean ;
} ) {
return (
< div className = "flex items-start gap-3 p-4 rounded-xl bg-slate-50 dark:bg-gray-800/60 border border-slate-100 dark:border-gray-700 group" >
@@ -1004,16 +1004,18 @@ function AddressCard({ addr, onEdit, onDelete }: {
) }
< / div >
< / div >
< div className = "opacity-0 group-hover:opacity-100 flex items-center gap-1 transition-opacity shrink-0" >
< button onClick = { onEdit }
className = "w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition-colors" >
< PencilIcon className = "w-3.5 h-3.5" / >
< / button >
< button onClick = { onDelete }
className = "w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors" >
< TrashIcon className = "w-3.5 h-3.5" / >
< / button >
< / div >
{ ! readOnly && (
< div className = "opacity-0 group-hover:opacity-100 flex items-center gap-1 transition-opacity shrink-0" >
< button onClick = { onEdit }
className = "w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition-colors" >
< PencilIcon className = "w-3.5 h-3.5" / >
< / button >
< button onClick = { onDelete }
className = "w-7 h-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors" >
< TrashIcon className = "w-3.5 h-3.5" / >
< / button >
< / div >
) }
< / div >
) ;
}
@@ -1213,7 +1215,7 @@ function SessionEditor({ session, onChange, onRemove, addresses }: {
// ── Weekly Schedule Tab ────────────────────────────────────────────────────
function WeeklyScheduleTab ( { doctorUuid , addresses } : { doctorUuid : string ; addresses : AddressData [ ] } ) {
function WeeklyScheduleTab ( { doctorUuid , addresses , readOnly = false } : { doctorUuid : string ; addresses : AddressData [ ] ; readOnly? : boolean } ) {
const qc = useQueryClient ( ) ;
const [ scheduleMap , setScheduleMap ] = useState < NewScheduleMap > ( EMPTY_NEW_SCHEDULE ) ;
const [ scheduleUuid , setScheduleUuid ] = useState < string | null > ( null ) ;
@@ -1315,6 +1317,48 @@ function WeeklyScheduleTab({ doctorUuid, addresses }: { doctorUuid: string; addr
< / div >
) ;
// نمای فقطخواندنی برای نماینده: برنامهی هفتگی بهصورت متن، بدون فرم.
if ( readOnly ) {
if ( scheduleQ . isLoading ) {
return < div className = "space-y-2" > { Array . from ( { length : 4 } ) . map ( ( _ , i ) = > < div key = { i } className = "h-12 rounded-xl skeleton" / > ) } < / div > ;
}
return (
< div className = "space-y-2" >
{ totalSlots > 0 && (
< div className = "flex items-center gap-2 mb-3 p-3 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/30" >
< CalendarIcon className = "w-4 h-4 text-emerald-600 dark:text-emerald-400 shrink-0" / >
< span className = "text-sm text-emerald-700 dark:text-emerald-300" >
م ج م و ع < strong > { totalSlots } < / strong > ن و ب ت د ر ه ف ت ه
< / span >
< / div >
) }
{ SCHEDULE_DAYS . map ( day = > {
const activeSessions = ( scheduleMap [ day . key ] ? . sessions ? ? [ ] ) . filter ( s = > s . active ) ;
const daySlots = activeSessions . reduce ( ( sum , s ) = > sum + calcSlotCount ( s ) , 0 ) ;
return (
< div key = { day . key } className = "flex items-center gap-3 px-4 py-3 rounded-xl border border-slate-200 dark:border-gray-700 bg-white dark:bg-gray-800/40" >
< b style = { { fontSize : 14 , minWidth : 56 , flexShrink : 0 } } > { day . label } < / b >
< div className = "flex-1 flex gap-2 items-center flex-wrap min-w-0" >
{ activeSessions . length > 0 ? activeSessions . map ( ( s , i ) = > (
< span key = { i } className = "text-xs px-2 py-1 rounded-lg bg-slate-100 dark:bg-gray-700 text-slate-700 dark:text-slate-200" style = { { fontFamily : 'monospace' , direction : 'ltr' , whiteSpace : 'nowrap' } } >
{ s . start_time } – { s . end_time }
< / span >
) ) : (
< span className = "muted text-xs" > ت ع ط ی ل < / span >
) }
< / div >
{ daySlots > 0 && (
< span className = "badge green" style = { { flexShrink : 0 } } >
< span className = "bdot" / > { daySlots } ن و ب ت
< / span >
) }
< / div >
) ;
} ) }
< / div >
) ;
}
return (
< div className = "space-y-2" >
{ /* ─ خلاصه کل هفته */ }
@@ -1452,11 +1496,13 @@ function WeeklyScheduleTab({ doctorUuid, addresses }: { doctorUuid: string; addr
{ hasAnyOverlap ? 'تداخل زمانی در برنامه وجود دارد' : 'مکان مطب برای همه بازهها الزامی است' }
< / p >
) }
< button type = "button" onClick = { ( ) = > saveMut . mutate ( ) }
disabled = { saveMut . isPending || hasAnyOverlap || missingLocation }
className = "btn primary sm" style = { { marginInlineStart : 'auto' , opacity : ( saveMut . isPending || hasAnyOverlap || missingLocation ) ? 0.5 : 1 } } >
{ saveMut . isPending ? 'در حال ذخیره...' : 'ذخیره برنامه هفتگی' }
< / button >
{ ! readOnly && (
< button type = "button" onClick = { ( ) = > saveMut . mutate ( ) }
disabled = { saveMut . isPending || hasAnyOverlap || missingLocation }
className = "btn primary sm" style = { { marginInlineStart : 'auto' , opacity : ( saveMut . isPending || hasAnyOverlap || missingLocation ) ? 0.5 : 1 } } >
{ saveMut . isPending ? 'در حال ذخیره...' : 'ذخیره برنامه هفتگی' }
< / button >
) }
< / div >
< / div >
) ;
@@ -1604,7 +1650,7 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, onSaved, addre
// ── Date Overrides Tab ─────────────────────────────────────────────────────
function DateOverridesTab ( { doctorUuid , addresses } : { doctorUuid : string ; addresses : AddressData [ ] } ) {
function DateOverridesTab ( { doctorUuid , addresses , readOnly = false } : { doctorUuid : string ; addresses : AddressData [ ] ; readOnly? : boolean } ) {
const qc = useQueryClient ( ) ;
const [ modalOpen , setModalOpen ] = useState ( false ) ;
const [ editing , setEditing ] = useState < DateOverrideData | null > ( null ) ;
@@ -1643,12 +1689,14 @@ function DateOverridesTab({ doctorUuid, addresses }: { doctorUuid: string; addre
return (
< div className = "space-y-3" >
< div className = "flex justify-end" >
< button type = "button" onClick = { ( ) = > { setEditing ( null ) ; setModalOpen ( true ) ; } }
className = "flex items-center gap-1.5 text-sm cp-btn-primary" >
< PlusIcon className = "w-4 h-4" / > ت ا ر ی خ خ ا ص ج د ی د
< / button >
< / div >
{ ! readOnly && (
< div className = "flex justify-end" >
< button type = "button" onClick = { ( ) = > { setEditing ( null ) ; setModalOpen ( true ) ; } }
className = "flex items-center gap-1.5 text-sm cp-btn-primary" >
< PlusIcon className = "w-4 h-4" / > ت ا ر ی خ خ ا ص ج د ی د
< / button >
< / div >
) }
{ overrides . length === 0 ? (
< div className = "text-center py-10 text-slate-400 dark:text-slate-500" >
< CalendarIcon className = "w-10 h-10 mx-auto mb-2 opacity-30" / >
@@ -1673,16 +1721,18 @@ function DateOverridesTab({ doctorUuid, addresses }: { doctorUuid: string; addre
< / div >
{ ov . reason && < p className = "text-xs text-slate-400 dark:text-slate-500 mt-0.5" > { ov . reason } < / p > }
< / div >
< div className = "flex items-center gap-1 shrink-0" >
< button onClick = { ( ) = > { setEditing ( ov ) ; setModalOpen ( true ) ; } }
className = "w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition-colors" >
< PencilIcon className = "w-3.5 h-3.5" / >
< / button >
< button onClick = { ( ) = > setDeletingUuid ( ov . uuid ) }
className = "w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors" >
< TrashIcon className = "w-3.5 h-3.5" / >
< / button >
< / div >
{ ! readOnly && (
< div className = "flex items-center gap-1 shrink-0" >
< button onClick = { ( ) = > { setEditing ( ov ) ; setModalOpen ( true ) ; } }
className = "w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition-colors" >
< PencilIcon className = "w-3.5 h-3.5" / >
< / button >
< button onClick = { ( ) = > setDeletingUuid ( ov . uuid ) }
className = "w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors" >
< TrashIcon className = "w-3.5 h-3.5" / >
< / button >
< / div >
) }
< / div >
) ) }
< / div >
@@ -1772,7 +1822,7 @@ function HolidayModal({ open, onClose, existing, doctorUuid, onSaved }: {
// ── Holidays Tab ───────────────────────────────────────────────────────────
function HolidaysTab ( { doctorUuid } : { doctorUuid : string } ) {
function HolidaysTab ( { doctorUuid , readOnly = false } : { doctorUuid : string ; readOnly? : boolean } ) {
const qc = useQueryClient ( ) ;
const [ modalOpen , setModalOpen ] = useState ( false ) ;
const [ editing , setEditing ] = useState < HolidayData | null > ( null ) ;
@@ -1805,12 +1855,14 @@ function HolidaysTab({ doctorUuid }: { doctorUuid: string }) {
return (
< div className = "space-y-3" >
< div className = "flex justify-end" >
< button type = "button" onClick = { ( ) = > { setEditing ( null ) ; setModalOpen ( true ) ; } }
className = "flex items-center gap-1.5 text-sm cp-btn-primary" >
< PlusIcon className = "w-4 h-4" / > ت ع ط ی ل ا ت ج د ی د
< / button >
< / div >
{ ! readOnly && (
< div className = "flex justify-end" >
< button type = "button" onClick = { ( ) = > { setEditing ( null ) ; setModalOpen ( true ) ; } }
className = "flex items-center gap-1.5 text-sm cp-btn-primary" >
< PlusIcon className = "w-4 h-4" / > ت ع ط ی ل ا ت ج د ی د
< / button >
< / div >
) }
{ holidays . length === 0 ? (
< div className = "text-center py-10 text-slate-400 dark:text-slate-500" >
< CalendarIcon className = "w-10 h-10 mx-auto mb-2 opacity-30" / >
@@ -1845,20 +1897,22 @@ function HolidaysTab({ doctorUuid }: { doctorUuid: string }) {
< / div >
{ h . reason && < p className = "text-xs text-slate-400 dark:text-slate-500 mt-0.5" > { h . reason } < / p > }
< / div >
< div className = "flex items-center gap-1 shrink-0" >
< button onClick = { ( ) = > toggleMut . mutate ( h ) } disabled = { toggleMut . isPending }
className = { ` text-xs px-2.5 h-7 rounded-lg border transition-colors disabled:opacity-50 ${ h . active ? 'border-slate-200 dark:border-gray-600 text-slate-500 dark:text-slate-400 hover:border-slate-300 hover:text-slate-700 dark:hover:text-slate-200' : 'border-emerald-200 dark:border-emerald-500/40 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-500/10' } ` } >
{ h . active ? 'غیرفعال' : 'فعال' }
< / button >
< button onClick = { ( ) = > { setEditing ( h ) ; setModalOpen ( true ) ; } }
className = "w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition-colors" >
< PencilIcon className = "w-3.5 h-3.5" / >
< / button >
< button onClick = { ( ) = > setDeletingUuid ( h . uuid ) }
className = "w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors" >
< TrashIcon className = "w-3.5 h-3.5" / >
< / button >
< / div >
{ ! readOnly && (
< div className = "flex items-center gap-1 shrink-0" >
< button onClick = { ( ) = > toggleMut . mutate ( h ) } disabled = { toggleMut . isPending }
className = { ` text-xs px-2.5 h-7 rounded-lg border transition-colors disabled:opacity-50 ${ h . active ? 'border-slate-200 dark:border-gray-600 text-slate-500 dark:text-slate-400 hover:border-slate-300 hover:text-slate-700 dark:hover:text-slate-200' : 'border-emerald-200 dark:border-emerald-500/40 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-500/10' } ` } >
{ h . active ? 'غیرفعال' : 'فعال' }
< / button >
< button onClick = { ( ) = > { setEditing ( h ) ; setModalOpen ( true ) ; } }
className = "w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-500/10 transition-colors" >
< PencilIcon className = "w-3.5 h-3.5" / >
< / button >
< button onClick = { ( ) = > setDeletingUuid ( h . uuid ) }
className = "w-8 h-8 flex items-center justify-center rounded-lg text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors" >
< TrashIcon className = "w-3.5 h-3.5" / >
< / button >
< / div >
) }
< / div >
) ;
} ) }
@@ -1882,7 +1936,7 @@ const SCHEDULE_TABS = [
{ id : 'holidays' as const , label : 'تعطیلات' } ,
] ;
function ScheduleSection ( { doctorUuid } : { doctorUuid : string } ) {
function ScheduleSection ( { doctorUuid , readOnly = false } : { doctorUuid : string ; readOnly? : boolean } ) {
const [ tab , setTab ] = useState < 'weekly' | 'overrides' | 'holidays' > ( 'weekly' ) ;
const locationsQ = useQuery ( {
@@ -1908,9 +1962,9 @@ function ScheduleSection({ doctorUuid }: { doctorUuid: string }) {
< / button >
) ) }
< / div >
{ tab === 'weekly' && < WeeklyScheduleTab doctorUuid = { doctorUuid } addresses = { availableLocations } / > }
{ tab === 'overrides' && < DateOverridesTab doctorUuid = { doctorUuid } addresses = { availableLocations } / > }
{ tab === 'holidays' && < HolidaysTab doctorUuid = { doctorUuid } / > }
{ tab === 'weekly' && < WeeklyScheduleTab doctorUuid = { doctorUuid } addresses = { availableLocations } readOnly = { readOnly } / > }
{ tab === 'overrides' && < DateOverridesTab doctorUuid = { doctorUuid } addresses = { availableLocations } readOnly = { readOnly } / > }
{ tab === 'holidays' && < HolidaysTab doctorUuid = { doctorUuid } readOnly = { readOnly } / > }
< / div >
) ;
}
@@ -2272,6 +2326,8 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
const dbUuid = useAuthStore ( s = > s . dbUuid ) ;
const doctorUuid = useAuthStore ( s = > s . doctorUuid ) ;
const uuid = isOwnProfile ? ( doctorUuid ? ? dbUuid ? ? undefined ) : paramUuid ;
// نماینده فقط مشاهده میکند؛ هیچ بخشی قابل ویرایش نیست.
const isReadOnly = primaryRole === 'representation' ;
const [ editOpen , setEditOpen ] = useState ( searchParams . get ( 'edit' ) === '1' ) ;
const [ deleteOpen , setDeleteOpen ] = useState ( false ) ;
@@ -2503,8 +2559,8 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
< div className = "flex flex-col items-center gap-1.5" >
< DoctorAvatar name = { doctor . name } img = { mainImage } idx = { idNum }
onUpload = { primaryRole !== 'clinic' ? handleImageUpload : undefined } uploading = { uploadingImg } / >
{ primaryRole !== 'clinic' && (
onUpload = { primaryRole !== 'clinic' && ! isReadOnly ? handleImageUpload : undefined } uploading = { uploadingImg } / >
{ primaryRole !== 'clinic' && ! isReadOnly && (
< span className = "text-[10px] text-slate-400 dark:text-slate-500" > ک ل ی ک ب ر ا ی ت غ ی ی ر ع ک س < / span >
) }
< / div >
@@ -2545,14 +2601,14 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
< / div >
< div className = "flex items-center gap-2 sm:mb-1 flex-wrap" >
{ primaryRole !== 'clinic' && (
{ primaryRole !== 'clinic' && ! isReadOnly && (
< button onClick = { ( ) = > setEditOpen ( true ) } className = "cp-btn-primary text-sm" >
< PencilIcon className = "w-4 h-4" / > و ی ر ا ی ش
< / button >
) }
{ /* Toggle active / Delete — فقط ادمین */ }
{ ! isOwnProfile && primaryRole !== 'clinic' && (
{ ! isOwnProfile && primaryRole !== 'clinic' && ! isReadOnly && (
< >
< button
onClick = { ( ) = > setToggleConfirm ( true ) }
@@ -2620,15 +2676,17 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
< h2 className = "text-sm font-semibold text-slate-700 dark:text-slate-300" >
آ د ر س ه ا ی م ط ب ( { formatNumber ( addresses . length ) } )
< / h2 >
< button onClick = { ( ) = > { setEditingAddr ( null ) ; setAddrModalOpen ( true ) ; } }
className = "flex items-center gap-1.5 text-xs font-medium text-primary-600 dark:text-primary-400 hover:underline" >
< PlusIcon className = "w-4 h-4" / > ا ف ز و د ن آ د ر س
< / button >
{ ! isReadOnly && (
< button onClick = { ( ) = > { setEditingAddr ( null ) ; setAddrModalOpen ( true ) ; } }
className = "flex items-center gap-1.5 text-xs font-medium text-primary-600 dark:text-primary-400 hover:underline" >
< PlusIcon className = "w-4 h-4" / > ا ف ز و د ن آ د ر س
< / button >
) }
< / div >
{ addresses . length > 0 ? (
< div className = "space-y-2" >
{ addresses . map ( addr = > (
< AddressCard key = { addr . id } addr = { addr }
< AddressCard key = { addr . id } addr = { addr } readOnly = { isReadOnly }
onEdit = { ( ) = > { setEditingAddr ( addr ) ; setAddrModalOpen ( true ) ; } }
onDelete = { ( ) = > setDeletingAddrId ( addr . id ) }
/ >
@@ -2638,15 +2696,17 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
< div className = "text-center py-8 text-slate-400 dark:text-slate-500" >
< MapPinIcon className = "w-10 h-10 mx-auto mb-2 opacity-30" / >
< p className = "text-sm" > ه ن و ز آ د ر س ی ث ب ت ن ش د ه ا س ت < / p >
< button onClick = { ( ) = > { setEditingAddr ( null ) ; setAddrModalOpen ( true ) ; } }
className = "mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline" >
ا و ل ی ن آ د ر س ر ا ا ض ا ف ه ک ن ی د
< / button >
{ ! isReadOnly && (
< button onClick = { ( ) = > { setEditingAddr ( null ) ; setAddrModalOpen ( true ) ; } }
className = "mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline" >
ا و ل ی ن آ د ر س ر ا ا ض ا ف ه ک ن ی د
< / button >
) }
< / div >
) }
< / div >
{ uuid && < ScheduleSection doctorUuid = { uuid } / > }
{ uuid && < ScheduleSection doctorUuid = { uuid } readOnly = { isReadOnly } / > }
{ isOwnProfile && < ClinicInvitationsSection / > }
@@ -2686,9 +2746,11 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
< / div >
: < p className = "text-sm text-slate-400" > ت خ ص ص ی ث ب ت ن ش د ه < / p >
}
< button onClick = { ( ) = > setEditOpen ( true ) } className = "w-full mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right" >
و ی ر ا ی ش ت خ ص ص ه ا ←
< / button >
{ ! isReadOnly && (
< button onClick = { ( ) = > setEditOpen ( true ) } className = "w-full mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right" >
و ی ر ا ی ش ت خ ص ص ه ا ←
< / button >
) }
< / div >
< div className = "cp-card p-5" >
@@ -2703,9 +2765,11 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil
< / div >
: < p className = "text-sm text-slate-400" > خ د م ت ی ث ب ت ن ش د ه < / p >
}
< button onClick = { ( ) = > setEditOpen ( true ) } className = "w-full mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right" >
و ی ر ا ی ش خ د م ا ت ←
< / button >
{ ! isReadOnly && (
< button onClick = { ( ) = > setEditOpen ( true ) } className = "w-full mt-3 text-xs text-primary-600 dark:text-primary-400 hover:underline text-right" >
و ی ر ا ی ش خ د م ا ت ←
< / button >
) }
< / div >
< div className = "cp-card p-5" >