@@ -77,11 +77,6 @@ const editSchema = z.object({
telephone : z.string ( ) . optional ( ) ,
info : z.string ( ) . optional ( ) ,
is_247 : z.boolean ( ) ,
address : z.string ( ) . optional ( ) ,
province_id : z.number ( ) . nullable ( ) ,
city_id : z.number ( ) . nullable ( ) ,
latitude : z.string ( ) . optional ( ) ,
longitude : z.string ( ) . optional ( ) ,
specialties : z.array ( z . number ( ) ) ,
insurance : z.array ( z . number ( ) ) ,
doctor_services : z.array ( z . number ( ) ) ,
@@ -237,16 +232,18 @@ function MapFlyController({ target }: { target: [number, number] | null }) {
useEffect ( ( ) = > { if ( target ) map . flyTo ( target , 12 , { duration : 1.2 } ) ; } , [ target , map ] ) ;
return null ;
}
function MapPicker ( { lat , lng , onChange , flyTarget } : {
function MapPicker ( { lat , lng , onChange , flyTarget , initialCenter } : {
lat : number | null ; lng : number | null ;
onChange : ( lat : number , lng : number ) = > void ;
flyTarget ? : [ number , number ] | null ;
initialCenter ? : [ number , number ] | null ;
} ) {
const pos : [ number , number ] | null = lat !== null && lng !== null ? [ lat , lng ] : null ;
const center : [ number , number ] = pos ? ? IRAN_CENTER ;
const center : [ number , number ] = initialCenter ? ? pos ? ? IRAN_CENTER ;
const zoom = initialCenter ? ? pos ? 13 : 5 ;
return (
< div style = { { borderRadius : 10 , overflow : 'hidden' , border : '1px solid var(--border)' , height : 260 } } >
< MapContainer center = { center } zoom = { pos ? 13 : 5 } style = { { height : '100%' , width : '100%' } } >
< MapContainer key = { ` ${ center [ 0 ] } , ${ center [ 1 ] } ` } center = { center } zoom = { zoom } style = { { height : '100%' , width : '100%' } } >
< TileLayer url = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution = '© <a href="https://openstreetmap.org">OpenStreetMap</a>' / >
< MapClickHandler onPick = { onChange } / >
@@ -271,12 +268,9 @@ async function geocodeCity(name: string): Promise<[number, number] | null> {
// ── Edit Modal ─────────────────────────────────────────────────────────────
function EditModal ( { clinic , onClose , onSaved , initialTab = 'basic' } : {
function EditModal ( { clinic , onClose , onSaved } : {
clinic : ClinicDetail ; onClose : ( ) = > void ; onSaved : ( ) = > void ;
initialTab ? : 'basic' | 'location' | 'tags' ;
} ) {
const [ mapFlyTarget , setMapFlyTarget ] = useState < [ number , number ] | null > ( null ) ;
const { register , handleSubmit , watch , setValue , formState : { errors } } = useForm < EditForm > ( {
resolver : zodResolver ( editSchema ) ,
defaultValues : {
@@ -284,36 +278,16 @@ function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: {
telephone : clinic.phone ? ? clinic . phone_number ? ? '' ,
info : clinic.caption ? ? '' ,
is_247 : clinic [ '24_7' ] ? ? false ,
address : clinic.location ? ? '' ,
province_id : clinic.state?. [ 0 ] ? Number ( clinic . state [ 0 ] . id ) : null ,
city_id : clinic.city?. [ 0 ] ? Number ( clinic . city [ 0 ] . id ) : null ,
latitude : clinic.map?.latitude ? ? '' ,
longitude : clinic.map?.longitude ? ? '' ,
specialties : ( clinic . specialties ? ? [ ] ) . map ( s = > Number ( s . id ) ) ,
insurance : ( clinic . list_bime ? ? [ ] ) . map ( s = > Number ( s . id ) ) ,
doctor_services : ( clinic . services ? ? [ ] ) . map ( s = > Number ( s . id ) ) ,
} ,
} ) ;
const provinceId = watch ( 'province_id' ) ;
const cityId = watch ( 'city_id' ) ;
const lat = watch ( 'latitude' ) ;
const lng = watch ( 'longitude' ) ;
const latN = lat ? parseFloat ( lat ) : null ;
const lngN = lng ? parseFloat ( lng ) : null ;
const watchedSpec = ( watch ( 'specialties' ) ? ? [ ] ) as number [ ] ;
const watchedIns = ( watch ( 'insurance' ) ? ? [ ] ) as number [ ] ;
const watchedSrv = ( watch ( 'doctor_services' ) ? ? [ ] ) as number [ ] ;
const provincesQ = useQuery ( {
queryKey : [ 'provinces' ] , staleTime : 600_000 ,
queryFn : ( ) = > api . get < ApiResponse < any > > ( '/api/v1/provinces' ) ,
} ) ;
const citiesQ = useQuery ( {
queryKey : [ 'cities' , provinceId ] , staleTime : 300_000 ,
queryFn : ( ) = > api . get < ApiResponse < any > > ( ` /api/v1/cities?province_id= ${ provinceId } ` ) ,
enabled : ! ! provinceId ,
} ) ;
const specialtiesQ = useQuery ( {
queryKey : [ 'specialties-all' ] , staleTime : 600_000 ,
queryFn : ( ) = > api . get < ApiResponse < any > > ( '/api/v1/specialties' ) ,
@@ -327,8 +301,6 @@ function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: {
queryFn : ( ) = > api . get < ApiResponse < any > > ( '/api/v1/doctor-services' ) ,
} ) ;
const provinces : Opt [ ] = useMemo ( ( ) = > ( provincesQ . data ? . data ? . data ? ? provincesQ . data ? . data ? ? [ ] ) . map ( ( p : any ) = > ( { id : Number ( p . id ? ? p . nid ) , name : p.name } ) ) , [ provincesQ . data ] ) ;
const cities : Opt [ ] = useMemo ( ( ) = > ( citiesQ . data ? . data ? . data ? ? citiesQ . data ? . data ? ? [ ] ) . map ( ( c : any ) = > ( { id : Number ( c . id ? ? c . nid ) , name : c.name } ) ) , [ citiesQ . data ] ) ;
const specialties : Opt [ ] = useMemo ( ( ) = > {
const raw = specialtiesQ . data ? . data ? . data ? ? specialtiesQ . data ? . data ? ? [ ] ;
return raw . map ( ( s : any ) = > ( { id : Number ( s . id ) , name : s.name } ) ) ;
@@ -348,11 +320,6 @@ function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: {
telephone : values.telephone || undefined ,
info : values.info || undefined ,
'24_7' : values . is_247 ,
address : values.address || undefined ,
state : values.province_id ? [ values . province_id ] : undefined ,
city : values.city_id ? [ values . city_id ] : undefined ,
latitude : values.latitude ? parseFloat ( values . latitude ) : undefined ,
longitude : values.longitude ? parseFloat ( values . longitude ) : undefined ,
specialties : values.specialties ,
insurance : values.insurance ,
doctor_services : values.doctor_services ,
@@ -361,7 +328,7 @@ function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: {
onError : ( e : Error ) = > toast . error ( e . message ) ,
} ) ;
const [ activeTab , setActiveTab ] = useState < 'basic' | 'location' | 'tags' > ( initialTab ) ;
const [ activeTab , setActiveTab ] = useState < 'basic' | 'tags' > ( 'basic' ) ;
return (
< div className = "overlay" onClick = { onClose } >
@@ -375,7 +342,7 @@ function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: {
{ /* Tab switcher */ }
< div style = { { borderBottom : '1px solid var(--border)' , padding : '0 16px' , display : 'flex' , gap : 4 } } >
{ ( [ [ 'basic' , 'اطلاعات پایه' ] , [ 'location' , 'موقعیت' ] , [ ' tags', 'تخصص و بیمه' ] ] as const ) . map ( ( [ id , label ] ) = > (
{ ( [ [ 'basic' , 'اطلاعات پایه' ] , [ 'tags' , 'تخصص و بیمه' ] ] as const ) . map ( ( [ id , label ] ) = > (
< button key = { id } type = "button" onClick = { ( ) = > setActiveTab ( id ) }
style = { {
padding : '10px 14px' , fontSize : 13 , fontWeight : 600 , border : 'none' , background : 'none' ,
@@ -413,59 +380,6 @@ function EditModal({ clinic, onClose, onSaved, initialTab = 'basic' }: {
< / >
) }
{ /* ── Location tab ── */ }
{ activeTab === 'location' && (
< >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
< div >
< label style = { { fontSize : 13 , fontWeight : 600 , display : 'block' , marginBottom : 6 } } > ا س ت ا ن < / label >
< SearchableSelectField
options = { provinces }
value = { provinceId ? ? null }
placeholder = "انتخاب استان"
onChange = { val = > { setValue ( 'province_id' , val ) ; setValue ( 'city_id' , null ) ; } }
/ >
< / div >
< div >
< label style = { { fontSize : 13 , fontWeight : 600 , display : 'block' , marginBottom : 6 } } > ش ه ر < / label >
< SearchableSelectField
options = { cities }
value = { cityId ? ? null }
placeholder = { provinceId ? 'انتخاب شهر' : 'ابتدا استان انتخاب کنید' }
disabled = { ! provinceId }
onChange = { ( val , label ) = > {
setValue ( 'city_id' , val ) ;
if ( label ) geocodeCity ( label ) . then ( c = > { if ( c ) setMapFlyTarget ( c ) ; } ) ;
} }
/ >
< / div >
< / div >
< div >
< label style = { { fontSize : 13 , fontWeight : 600 , display : 'block' , marginBottom : 6 } } > آ د ر س < / label >
< textarea className = "input" rows = { 2 } style = { { resize : 'none' } } placeholder = "خیابان، کوچه، پلاک..." { ...register ( 'address' ) } / >
< / div >
< div >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 6 } } >
< label style = { { fontSize : 13 , fontWeight : 600 } } > م و ق ع ی ت ر و ی ن ق ش ه < / label >
{ latN && lngN && (
< span className = "muted" style = { { fontSize : 11 , direction : 'ltr' } } > { latN . toFixed ( 4 ) } , { lngN . toFixed ( 4 ) } < / span >
) }
< / div >
< p className = "muted" style = { { fontSize : 12 , marginBottom : 6 } } > ب ر ا ی ت ع ی ی ن م و ق ع ی ت ر و ی ن ق ش ه ک ل ی ک ک ن ی د < / p >
< MapPicker
lat = { latN } lng = { lngN } flyTarget = { mapFlyTarget }
onChange = { ( lt , ln ) = > { setValue ( 'latitude' , String ( lt ) ) ; setValue ( 'longitude' , String ( ln ) ) ; } }
/ >
{ latN && lngN && (
< button type = "button" className = "btn ghost sm" style = { { marginTop : 6 , fontSize : 12 } }
onClick = { ( ) = > { setValue ( 'latitude' , '' ) ; setValue ( 'longitude' , '' ) ; } } >
< XMarkIcon style = { { width : 13 , height : 13 } } / > ح ذ ف م و ق ع ی ت
< / button >
) }
< / div >
< / >
) }
{ /* ── Tags tab ── */ }
{ activeTab === 'tags' && (
< >
@@ -525,10 +439,10 @@ export default function ClinicDetailPage() {
const qc = useQueryClient ( ) ;
const primaryRole = useAuthStore ( s = > s . primaryRole ) ;
const dbUuid = useAuthStore ( s = > s . dbUuid ) ;
const authToken = useAuthStore ( s = > s . token ) ;
const isOwner = primaryRole === 'clinic' && dbUuid === uuid ;
const [ editOpen , setEditOpen ] = useState ( false ) ;
const [ editInitialTab , setEditInitialTab ] = useState < 'basic' | 'location' | 'tags' > ( 'basic' ) ;
const [ deleteOpen , setDeleteOpen ] = useState ( false ) ;
const [ inviteOpen , setInviteOpen ] = useState ( false ) ;
const [ doctorsTab , setDoctorsTab ] = useState < 'doctors' | 'invitations' > ( 'doctors' ) ;
@@ -562,11 +476,39 @@ export default function ClinicDetailPage() {
} ) ;
const clinicAddresses : ClinicAddress [ ] = ( clinicAddressesQ . data ? . data as any ) ? . data ? ? clinicAddressesQ . data ? . data ? ? [ ] ;
const [ addrFormOpen , setAddrFormOpen ] = useState ( false ) ;
const [ addrFormOpen , setAddrFormOpen ] = useState ( false ) ;
const [ editingClinicAddr , setEditingClinicAddr ] = useState < ClinicAddress | null > ( null ) ;
const [ deleteAddrConfirm , setDeleteAddrConfirm ] = useState < ClinicAddress | null > ( null ) ;
const [ addrForm , setAddrForm ] = useState ( { name : '' , address : '' , telephone : '' } ) ;
const emptyAddrForm = { name : '' , address : '' , telephone : '' , province_id : null as number | null , city_id : null as number | null , latitude : null as number | null , longitude : null as number | null } ;
const [ addrForm , setAddrForm ] = useState ( emptyAddrForm ) ;
const [ addrMapFlyTarget , setAddrMapFlyTarget ] = useState < [ number , number ] | null > ( null ) ;
const provincesQ = useQuery ( {
queryKey : [ 'provinces' ] , staleTime : 600_000 ,
queryFn : ( ) = > api . get < ApiResponse < any > > ( '/api/v1/provinces' ) ,
} ) ;
const citiesQ = useQuery ( {
queryKey : [ 'cities' , addrForm . province_id ] , staleTime : 300_000 ,
queryFn : ( ) = > api . get < ApiResponse < any > > ( ` /api/v1/cities?province_id= ${ addrForm . province_id } ` ) ,
enabled : ! ! addrForm . province_id ,
} ) ;
const provinces : Opt [ ] = useMemo ( ( ) = > ( provincesQ . data ? . data ? . data ? ? provincesQ . data ? . data ? ? [ ] ) . map ( ( p : any ) = > ( { id : Number ( p . id ? ? p . nid ) , name : p.name } ) ) , [ provincesQ . data ] ) ;
const addrCities : Opt [ ] = useMemo ( ( ) = > ( citiesQ . data ? . data ? . data ? ? citiesQ . data ? . data ? ? [ ] ) . map ( ( c : any ) = > ( { id : Number ( c . id ? ? c . nid ) , name : c.name } ) ) , [ citiesQ . data ] ) ;
const openAddrForm = ( addr : ClinicAddress | null ) = > {
setEditingClinicAddr ( addr ) ;
const lat = addr ? . map ? . latitude ? parseFloat ( addr . map . latitude ) : null ;
const lng = addr ? . map ? . longitude ? parseFloat ( addr . map . longitude ) : null ;
setAddrForm ( addr ? {
name : addr.name ? ? '' , address : addr.address ? ? '' , telephone : addr.telephone ? ? '' ,
province_id : addr.province ? Number ( addr . province . id ) : null ,
city_id : addr.city ? Number ( addr . city . id ) : null ,
latitude : lat , longitude : lng ,
} : emptyAddrForm ) ;
setAddrMapFlyTarget ( lat && lng ? [ lat , lng ] : null ) ;
setAddrFormOpen ( true ) ;
} ;
const saveAddrMutation = useMutation ( {
mutationFn : ( payload : typeof addrForm ) = > {
@@ -580,7 +522,7 @@ export default function ClinicDetailPage() {
qc . invalidateQueries ( { queryKey : [ 'clinic-addresses' , uuid ] } ) ;
setAddrFormOpen ( false ) ;
setEditingClinicAddr ( null ) ;
setAddrForm ( { name : '' , address : '' , telephone : '' } ) ;
setAddrForm ( emptyAddrForm ) ;
} ,
onError : ( ) = > toast . error ( 'خطا در ذخیره آدرس' ) ,
} ) ;
@@ -607,10 +549,7 @@ export default function ClinicDetailPage() {
const invitationList : ClinicInvitation [ ] = invitationsQ . data ? . data ? ? [ ] ;
const openEdit = ( tab : 'basic' | 'location' | 'tags' = 'basic' ) = > {
setEditInitialTab ( tab ) ;
setEditOpen ( true ) ;
} ;
const openEdit = ( ) = > setEditOpen ( true ) ;
const toggleMut = useMutation ( {
mutationFn : ( ) = > api . patch < ApiResponse < any > > ( ` /api/v1/admin/clinic/ ${ uuid } /status ` , { } ) ,
@@ -655,7 +594,7 @@ export default function ClinicDetailPage() {
headers : {
'Content-Disposition' : ` filename=" ${ file . name } " ` ,
'Content-Type' : file . type || 'application/octet-stream' ,
Authorization : ` Bearer ${ JSON . parse ( localStorage . getItem ( 'clinicpro-auth' ) ? ? '{}' ) ? . state ? . token ? ? '' } ` ,
Authorization : ` Bearer ${ authToken ? ? '' } ` ,
} ,
body : file ,
} ) ;
@@ -670,24 +609,27 @@ export default function ClinicDetailPage() {
finally { setLogoUploading ( false ) ; }
} ;
const handleGalleryUpload = async ( file : File ) = > {
const handleGalleryUpload = async ( files : FileList ) = > {
setGalleryUploading ( true ) ;
try {
const res = await fetch ( '/file/upload/clinic_pro/clinic/field_image_clinic' , {
method : 'POST' ,
headers : {
'Content-Disposition' : ` filename=" ${ file . name } " ` ,
'Content-Type' : file . type || 'application/octet-stream' ,
Authorization : ` Bearer ${ JSON . parse ( localStorage . getItem ( 'clinicpro-auth' ) ? ? '{}' ) ? . state ? . token ? ? '' } ` ,
} ,
body : file ,
} ) ;
const json = await res . json ( ) ;
const url = json ? . data ? . url ;
if ( url && clinic ) {
const uploaded : { url : string } [ ] = [ ] ;
for ( const file of Array . from ( files ) ) {
const res = await fetch ( '/file/upload/clinic_pro/clinic/field_image_clinic' , {
method : 'POST' ,
headers : {
'Content-Disposition' : ` filename=" ${ file . name } " ` ,
'Content-Type' : file . type || 'application/octet-stream' ,
Authorization : ` Bearer ${ authToken ? ? '' } ` ,
} ,
body : file ,
} ) ;
const json = await res . json ( ) ;
if ( json ? . data ? . url ) uploaded . push ( { url : json.data.url } ) ;
}
if ( uploaded . length > 0 && clinic ) {
const existing = ( clinic . images_clinic ? ? [ ] ) . filter ( img = > img ? . url ) ;
await api . patch ( ` /api/v1/clinic/ ${ uuid } ` , { image_clinic : [ . . . existing , { url } ] } ) ;
toast . success ( ' تصویر اضافه شد' ) ;
await api . patch ( ` /api/v1/clinic/ ${ uuid } ` , { image_clinic : [ . . . existing , . . . uploaded ] } ) ;
toast . success ( ` ${ uploaded . length } تصویر اضافه شد` ) ;
qc . invalidateQueries ( { queryKey : [ 'clinic-detail' , uuid ] } ) ;
}
} catch ( e : any ) { toast . error ( e . message ) ; }
@@ -696,10 +638,14 @@ export default function ClinicDetailPage() {
const hue = HUES_LIST [ ( uuid ? . charCodeAt ( 0 ) ? ? 0 ) % HUES_LIST . length ] ;
const logo = clinic ? . logo ? ? clinic ? . clinic_logo ;
const lat = clinic ? . map ? . latitude ? parseFloat ( clinic . map . latitude ) : null ;
const lng = clinic ? . map ? . longitude ? parseFloat ( clinic . map . longitude ) : null ;
const cityName = clinic ? . city ? . [ 0 ] ? . name ;
const provinceName = clinic ? . state ? . [ 0 ] ? . name ;
// شهر و استان از آدرس DoctorAddress (منبع واقعی) نه از Clinic entity
const primaryAddr = clinicAddresses [ 0 ] ? ? null ;
const cityName = primaryAddr ? . city ? . name ? ? clinic ? . city ? . [ 0 ] ? . name ;
const provinceName = primaryAddr ? . province ? . name ? ? clinic ? . state ? . [ 0 ] ? . name ;
const lat = primaryAddr ? . map ? . latitude ? parseFloat ( primaryAddr . map . latitude )
: clinic ? . map ? . latitude ? parseFloat ( clinic . map . latitude ) : null ;
const lng = primaryAddr ? . map ? . longitude ? parseFloat ( primaryAddr . map . longitude )
: clinic ? . map ? . longitude ? parseFloat ( clinic . map . longitude ) : null ;
if ( isLoading ) {
return (
@@ -741,7 +687,7 @@ export default function ClinicDetailPage() {
< / div >
< / div >
< div style = { { display : 'flex' , gap : 8 } } >
< button className = "btn ghost sm" onClick = { ( ) = > openEdit ( 'basic' ) } >
< button className = "btn ghost sm" onClick = { ( ) = > openEdit ( ) } >
< PencilIcon style = { { width : 15 , height : 15 } } / > و ی ر ا ی ش
< / button >
{ primaryRole === 'admin' && (
@@ -806,7 +752,7 @@ export default function ClinicDetailPage() {
label = "شهر"
value = { [ cityName , provinceName ] . filter ( Boolean ) . join ( '، ' ) || '—' } / >
< InfoTile icon = { < BuildingOffice2Icon style = { { width : 15 , height : 15 } } / > }
label = "آدرس" value = { clinic . location ? ? '—' } fullWidth / >
label = "آدرس" value = { primaryAddr ? . address ? ? clinic . location ? ? '—' } fullWidth / >
< / div >
{ clinic . caption && (
@@ -944,8 +890,8 @@ export default function ClinicDetailPage() {
< PlusIcon style = { { width : 14 , height : 14 } } / >
{ galleryUploading ? 'در حال آپلود...' : 'افزودن تصویر' }
< / button >
< input ref = { galleryInputRef } type = "file" accept = "image/*" style = { { display : 'none' } }
onChange = { e = > e . target . files ? . [ 0 ] && handleGalleryUpload ( e . target . files [ 0 ] ) } / >
< input ref = { galleryInputRef } type = "file" accept = "image/*" multiple style = { { display : 'none' } }
onChange = { e = > e . target . files ? . length && handleGalleryUpload ( e . target . files ) } / >
< / div >
{ ( clinic . images_clinic ? ? [ ] ) . length === 0 ? (
< div className = "empty" style = { { padding : '20px 0' } } >
@@ -984,7 +930,7 @@ export default function ClinicDetailPage() {
< div className = "card card-pad" >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 12 } } >
< b style = { { fontSize : 13 } } > ت خ ص ص ه ا ، ب ی م ه و خ د م ا ت < / b >
< button className = "btn ghost sm" style = { { fontSize : 12 } } onClick = { ( ) = > openEdit ( 'tags' ) } >
< button className = "btn ghost sm" style = { { fontSize : 12 } } onClick = { ( ) = > openEdit ( ) } >
< PencilIcon style = { { width : 13 , height : 13 } } / > و ی ر ا ی ش
< / button >
< / div >
@@ -1045,12 +991,8 @@ export default function ClinicDetailPage() {
< div style = { { fontWeight : 600 , fontSize : 14 } } >
آ د ر س ه ا ی ک ل ی ن ی ک ( { formatNumber ( clinicAddresses . length ) } )
< / div >
{ isOwner && (
< button className = "btn primary sm" onClick = { ( ) = > {
setEditingClinicAddr ( null ) ;
setAddrForm ( { name : '' , address : '' , telephone : '' } ) ;
setAddrFormOpen ( true ) ;
} } >
{ ( isOwner || primaryRole === 'admin' ) && clinicAddresses . length === 0 && (
< button className = "btn primary sm" onClick = { ( ) = > openAddrForm ( null ) } >
< PlusIcon style = { { width : 14 , height : 14 } } / >
ا ف ز و د ن آ د ر س
< / button >
@@ -1084,13 +1026,9 @@ export default function ClinicDetailPage() {
< / div >
) }
< / div >
{ isOwner && (
{ ( isOwner || primaryRole === 'admin' ) && (
< div style = { { display : 'flex' , gap : 6 , flexShrink : 0 } } >
< button className = "btn ghost sm" style = { { padding : '4px 8px' } } onClick = { ( ) = > {
setEditingClinicAddr ( addr ) ;
setAddrForm ( { name : addr.name ? ? '' , address : addr.address ? ? '' , telephone : addr.telephone ? ? '' } ) ;
setAddrFormOpen ( true ) ;
} } >
< button className = "btn ghost sm" style = { { padding : '4px 8px' } } onClick = { ( ) = > openAddrForm ( addr ) } >
< PencilIcon style = { { width : 14 , height : 14 } } / >
< / button >
< button className = "btn ghost sm" style = { { padding : '4px 8px' , color : 'var(--error)' } }
@@ -1110,50 +1048,92 @@ export default function ClinicDetailPage() {
< / div >
{ /* Clinic Address Form Modal */ }
{ addrFormOpen && (
< div className = "modal-backdrop " onClick = { ( ) = > setAddrFormOpen ( false ) } >
< div className = "modal" style = { { maxWidth : 440 } } onClick = { e = > e . stopPropagation ( ) } >
< div className = "modal-header " >
< span > { editingClinicAddr ? 'ویرایش آدرس' : 'افزودن آدرس جدید' } < / span >
< button className = "btn ghost sm " onClick = { ( ) = > setAddrFormOpen ( false ) } >
< XMarkIcon style = { { width : 18 , height : 18 } } / >
{ addrFormOpen && createPortal (
< div className = "overlay " onClick = { ( ) = > setAddrFormOpen ( false ) } >
< div className = "modal" style = { { maxWidth : 460 } } onClick = { e = > e . stopPropagation ( ) } >
< div className = "modal-head" >
< b > { editingClinicAddr ? 'ویرایش آدرس' : 'افزودن آدرس جدید' } < / b >
< button className = "mini-btn " onClick = { ( ) = > setAddrFormOpen ( false ) } >
< XMarkIcon style = { { width : 16 , height : 16 } } / >
< / button >
< / div >
< div className = "modal-body" style = { { display : 'flex' , flexDirection : 'column' , gap : 14 } } >
< div >
< label className = "field-label" > ن ا م ش ع ب ه / ع ن و ا ن < / label >
< div className = "field" >
< input type = "text" placeholder = "مثال: شعبه مرکزی"
value = { addrForm . name }
onChange = { e = > setAddrForm ( f = > ( { . . . f , name : e.target.value } ) ) } / >
< label style = { { fontSize : 13 , fontWeight : 600 , display : 'block' , marginBottom : 6 } } > ن ا م ش ع ب ه / ع ن و ا ن < / label >
< input className = "input" placeholder = "مثال: شعبه مرکزی"
value = { addrForm . name }
onChange = { e = > setAddrForm ( f = > ( { . . . f , name : e.target.value } ) ) } / >
< / div >
< div style = { { display : 'grid' , gridTemplateColumns : '1fr 1fr' , gap : 12 } } >
< div >
< label style = { { fontSize : 13 , fontWeight : 600 , display : 'block' , marginBottom : 6 } } > ا س ت ا ن < / label >
< SearchableSelectField
options = { provinces }
value = { addrForm . province_id }
placeholder = "انتخاب استان"
onChange = { val = > setAddrForm ( f = > ( { . . . f , province_id : val , city_id : null } ) ) }
/ >
< / div >
< div >
< label style = { { fontSize : 13 , fontWeight : 600 , display : 'block' , marginBottom : 6 } } > ش ه ر < / label >
< SearchableSelectField
options = { addrCities }
value = { addrForm . city_id }
placeholder = { addrForm . province_id ? 'انتخاب شهر' : 'ابتدا استان' }
disabled = { ! addrForm . province_id }
onChange = { ( val , label ) = > {
setAddrForm ( f = > ( { . . . f , city_id : val } ) ) ;
if ( label ) geocodeCity ( label ) . then ( c = > { if ( c ) setAddrMapFlyTarget ( c ) ; } ) ;
} }
/ >
< / div >
< / div >
< div >
< label className = "field-label" > آ د ر س < / label >
< div className = "field" >
< input type = "text" placeholder = "آدرس کامل"
value = { addrForm . address }
onChange = { e = > setAddrForm ( f = > ( { . . . f , address : e.target.value } ) ) } / >
< / div >
< label style = { { fontSize : 13 , fontWeight : 600 , display : 'block' , marginBottom : 6 } } > آ د ر س ک ا م ل < / label >
< textarea className = "input" rows = { 2 } style = { { resize : 'none' } } placeholder = "خیابان، کوچه، پلاک..."
value = { addrForm . address }
onChange = { e = > setAddrForm ( f = > ( { . . . f , address : e.target.value } ) ) } / >
< / div >
< div >
< label className = "field-label" > ت ل ف ن < / label >
< div className = "field" >
< input type = "text" placeholder = "مثال: 02112345678"
value = { addrForm . telephone }
onChange = { e = > setAddrForm ( f = > ( { . . . f , telephone : e.target.value } ) ) } / >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 6 } } >
< label style = { { fontSize : 13 , fontWeight : 600 } } > م و ق ع ی ت ر و ی ن ق ش ه < / label >
{ addrForm . latitude && addrForm . longitude && (
< span className = "muted" style = { { fontSize : 11 , direction : 'ltr' } } >
{ addrForm . latitude . toFixed ( 4 ) } , { addrForm . longitude . toFixed ( 4 ) }
< / span >
) }
< / div >
< p className = "muted" style = { { fontSize : 12 , marginBottom : 6 } } > ب ر ا ی ت ع ی ی ن م و ق ع ی ت د ق ی ق ر و ی ن ق ش ه ک ل ی ک ک ن ی د < / p >
< MapPicker
lat = { addrForm . latitude } lng = { addrForm . longitude }
flyTarget = { addrMapFlyTarget }
initialCenter = { addrForm . latitude && addrForm . longitude ? [ addrForm . latitude , addrForm . longitude ] : null }
onChange = { ( lt , ln ) = > setAddrForm ( f = > ( { . . . f , latitude : lt , longitude : ln } ) ) }
/ >
{ addrForm . latitude && addrForm . longitude && (
< button type = "button" className = "btn ghost sm" style = { { marginTop : 6 , fontSize : 12 } }
onClick = { ( ) = > setAddrForm ( f = > ( { . . . f , latitude : null , longitude : null } ) ) } >
< XMarkIcon style = { { width : 13 , height : 13 } } / > ح ذ ف م و ق ع ی ت
< / button >
) }
< / div >
< div >
< label style = { { fontSize : 13 , fontWeight : 600 , display : 'block' , marginBottom : 6 } } > ت ل ف ن < / label >
< input className = "input" dir = "ltr" placeholder = "مثال: 02112345678"
value = { addrForm . telephone }
onChange = { e = > setAddrForm ( f = > ( { . . . f , telephone : e.target.value } ) ) } / >
< / div >
< / div >
< div className = "modal-footer " >
< button className = "btn ghost" onClick = { ( ) = > setAddrFormOpen ( false ) } > ا ن ص ر ا ف < / button >
< button className = "btn primary" disabled = { saveAddrMutation . isPending }
< div className = "modal-foot" >
< button className = "btn ghost sm " onClick = { ( ) = > setAddrFormOpen ( false ) } > ا ن ص ر ا ف < / button >
< button className = "btn primary sm " disabled = { saveAddrMutation . isPending }
onClick = { ( ) = > saveAddrMutation . mutate ( addrForm ) } >
{ saveAddrMutation . isPending ? 'در حال ذخیره...' : 'ذخیره' }
< / button >
< / div >
< / div >
< / div >
< / div > ,
document . body ,
) }
{ /* Delete Address Confirm */ }
@@ -1170,7 +1150,6 @@ export default function ClinicDetailPage() {
{ editOpen && createPortal (
< EditModal
clinic = { clinic }
initialTab = { editInitialTab }
onClose = { ( ) = > setEditOpen ( false ) }
onSaved = { ( ) = > { qc . invalidateQueries ( { queryKey : [ 'clinic-detail' , uuid ] } ) ; qc . invalidateQueries ( { queryKey : [ 'admin-clinics' ] } ) ; } }
/ > ,