2026-05-31 00:42:08 +03:30
<!DOCTYPE html>
< html lang = "fa" dir = "rtl" >
< head >
< meta charset = "UTF-8" / >
< meta name = "viewport" content = "width=device-width,initial-scale=1.0" / >
< title > پنل مدیریت | دکتر سوسن آلطه< / title >
< link rel = "stylesheet" href = "https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" media = "print" onload = "this.media='all'" / >
< noscript > < link rel = "stylesheet" href = "https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700&display=swap" / > < / noscript >
< style >
* , * :: before , * :: after { box-sizing : border-box ; margin : 0 ; padding : 0 }
: root {
--gold : #B8955A ; --gold-l : #D4B483 ; --gold-pale : #F5ECD8 ;
--bg : #F4F6F8 ; --white : #fff ; --dark : #1E1E1E ; --mid : #5A5A5A ;
--light : #9A9A9A ; --border : #E2E8F0 ; --danger : #E53935 ; --success : #2E7D32 ;
--sidebar : 240 px ;
}
body { font-family : 'Vazirmatn' , Tahoma , sans-serif ; background : var ( - - bg ) ; color : var ( - - dark ) ; direction : rtl ; display : flex ; min-height : 100 vh }
/* ── Sidebar ── */
. sidebar { width : var ( - - sidebar ) ; background : var ( - - dark ) ; color : #fff ; display : flex ; flex-direction : column ; flex-shrink : 0 ; position : fixed ; top : 0 ; right : 0 ; bottom : 0 ; z-index : 50 }
. sidebar-logo { padding : 1.5 rem 1.2 rem ; border-bottom : 1 px solid rgba ( 255 , 255 , 255 , .08 ) ; font-size : .9 rem ; font-weight : 600 ; color : var ( - - gold - l ) ; line-height : 1.4 }
. sidebar-logo span { display : block ; font-size : .7 rem ; color : rgba ( 255 , 255 , 255 , .4 ) ; font-weight : 400 ; margin-top : .2 rem }
nav . sidebar-nav { flex : 1 ; overflow-y : auto ; padding : .8 rem 0 }
. nav-section { font-size : .65 rem ; font-weight : 600 ; letter-spacing : .08 em ; color : rgba ( 255 , 255 , 255 , .3 ) ; padding : .8 rem 1.2 rem .3 rem ; text-transform : uppercase }
. nav-item { display : flex ; align-items : center ; gap : .7 rem ; padding : .65 rem 1.2 rem ; color : rgba ( 255 , 255 , 255 , .65 ) ; cursor : pointer ; font-size : .85 rem ; transition : background .2 s , color .2 s ; border-right : 3 px solid transparent }
. nav-item : hover { background : rgba ( 255 , 255 , 255 , .05 ) ; color : #fff }
. nav-item . active { background : rgba ( 184 , 149 , 90 , .12 ) ; color : var ( - - gold - l ) ; border-right-color : var ( - - gold ) }
. nav-item svg { width : 16 px ; height : 16 px ; flex-shrink : 0 ; opacity : .7 }
. nav-item . active svg { opacity : 1 }
. sidebar-footer { padding : 1 rem 1.2 rem ; border-top : 1 px solid rgba ( 255 , 255 , 255 , .08 ) ; font-size : .75 rem ; color : rgba ( 255 , 255 , 255 , .35 ) }
/* ── Main ── */
. main { margin-right : var ( - - sidebar ) ; flex : 1 ; display : flex ; flex-direction : column ; min-height : 100 vh }
. topbar { background : var ( - - white ) ; border-bottom : 1 px solid var ( - - border ) ; padding : .75 rem 2 rem ; display : flex ; align-items : center ; justify-content : space-between ; position : sticky ; top : 0 ; z-index : 40 }
. topbar-title { font-size : 1 rem ; font-weight : 600 ; color : var ( - - dark ) }
. topbar-actions { display : flex ; gap : .7 rem ; align-items : center }
. page-content { padding : 2 rem ; flex : 1 }
/* ── Cards ── */
. stat-grid { display : grid ; grid-template-columns : repeat ( 4 , 1 fr ) ; gap : 1.2 rem ; margin-bottom : 2 rem }
. stat-card { background : var ( - - white ) ; border-radius : 14 px ; padding : 1.4 rem 1.6 rem ; border : 1 px solid var ( - - border ) ; display : flex ; flex-direction : column ; gap : .4 rem }
. stat-card . label { font-size : .78 rem ; color : var ( - - light ) }
. stat-card . value { font-size : 2 rem ; font-weight : 700 ; color : var ( - - dark ) }
. stat-card . icon { width : 38 px ; height : 38 px ; border-radius : 10 px ; background : var ( - - gold - pale ) ; display : flex ; align-items : center ; justify-content : center ; margin-bottom : .4 rem }
. stat-card . icon svg { width : 18 px ; height : 18 px ; color : var ( - - gold ) }
. card { background : var ( - - white ) ; border-radius : 14 px ; border : 1 px solid var ( - - border ) ; overflow : hidden ; margin-bottom : 1.5 rem }
. card-header { padding : 1.2 rem 1.5 rem ; border-bottom : 1 px solid var ( - - border ) ; display : flex ; align-items : center ; justify-content : space-between }
. card-title { font-size : .95 rem ; font-weight : 600 }
. card-body { padding : 1.5 rem }
/* ── Buttons ── */
. btn { display : inline-flex ; align-items : center ; gap : .4 rem ; padding : .55 rem 1.1 rem ; border-radius : 8 px ; font-family : inherit ; font-size : .85 rem ; font-weight : 500 ; cursor : pointer ; border : 1.5 px solid transparent ; transition : all .2 s }
. btn-primary { background : var ( - - gold ) ; color : #fff ; border-color : var ( - - gold ) }
. btn-primary : hover { background : var ( - - gold - l ) }
. btn-secondary { background : transparent ; color : var ( - - mid ) ; border-color : var ( - - border ) }
. btn-secondary : hover { border-color : var ( - - gold ) ; color : var ( - - gold ) }
. btn-danger { background : transparent ; color : var ( - - danger ) ; border-color : var ( - - danger ) }
. btn-danger : hover { background : var ( - - danger ) ; color : #fff }
. btn-sm { padding : .35 rem .7 rem ; font-size : .78 rem }
. btn svg { width : 15 px ; height : 15 px }
/* ── Table ── */
. table-wrap { overflow-x : auto }
table { width : 100 % ; border-collapse : collapse ; font-size : .85 rem }
th { background : #F8FAFC ; padding : .75 rem 1 rem ; text-align : right ; font-weight : 600 ; color : var ( - - mid ) ; font-size : .78 rem ; border-bottom : 1 px solid var ( - - border ) }
td { padding : .75 rem 1 rem ; border-bottom : 1 px solid var ( - - border ) ; color : var ( - - dark ) ; vertical-align : middle }
tr : last-child td { border-bottom : none }
tr : hover td { background : #FAFBFC }
. badge { display : inline-flex ; align-items : center ; padding : .2 rem .65 rem ; border-radius : 50 px ; font-size : .72 rem ; font-weight : 600 }
. badge-green { background : #E8F5E9 ; color : #2E7D32 }
. badge-red { background : #FFEBEE ; color : #C62828 }
. badge-gold { background : var ( - - gold - pale ) ; color : var ( - - gold ) }
/* ── Forms ── */
. form-grid { display : grid ; gap : 1 rem }
. form-row { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 1 rem }
. form-group { display : flex ; flex-direction : column ; gap : .35 rem }
. form-group label { font-size : .82 rem ; font-weight : 500 ; color : var ( - - dark ) }
. form-group input , . form-group select , . form-group textarea {
border : 1.5 px solid var ( - - border ) ; border-radius : 8 px ; padding : .65 rem .9 rem ;
font-family : inherit ; font-size : .88 rem ; color : var ( - - dark ) ; direction : rtl ;
transition : border-color .2 s , box-shadow .2 s ; outline : none ; background : var ( - - white )
}
. form-group input : focus , . form-group select : focus , . form-group textarea : focus { border-color : var ( - - gold ) ; box-shadow : 0 0 0 3 px rgba ( 184 , 149 , 90 , .1 ) }
. form-group textarea { resize : vertical ; min-height : 100 px }
. form-hint { font-size : .72 rem ; color : var ( - - light ) ; margin-top : .2 rem }
/* ── Upload input group ── */
. input-upload-wrap { display : flex ; gap : .45 rem ; align-items : center }
. input-upload-wrap input { flex : 1 ; min-width : 0 }
. upload-btn { flex-shrink : 0 ; display : inline-flex ; align-items : center ; gap : .3 rem ; padding : .55 rem .8 rem ; border-radius : 8 px ; border : 1.5 px dashed var ( - - gold ) ; background : var ( - - gold - pale ) ; color : var ( - - gold ) ; font-family : inherit ; font-size : .78 rem ; font-weight : 600 ; cursor : pointer ; transition : all .2 s ; white-space : nowrap }
. upload-btn : hover { background : var ( - - gold ) ; color : #fff ; border-style : solid }
. upload-btn : disabled { opacity : .5 ; cursor : not-allowed }
. upload-btn svg { width : 14 px ; height : 14 px }
. upload-remove { flex-shrink : 0 ; display : inline-flex ; align-items : center ; gap : .3 rem ; padding : .55 rem .8 rem ; border-radius : 8 px ; border : 1.5 px solid var ( - - border ) ; background : #fff ; color : #c0392b ; font-family : inherit ; font-size : .78 rem ; font-weight : 600 ; cursor : pointer ; transition : all .2 s }
. upload-remove : hover { background : #c0392b ; color : #fff ; border-color : #c0392b }
. upload-preview { display : none ; width : 100 % ; max-height : 120 px ; object-fit : cover ; border-radius : 8 px ; border : 1.5 px solid var ( - - border ) ; margin-top : .4 rem ; background : var ( - - section - bg ) }
/* ── File Manager button ── */
. fm-open-btn { flex-shrink : 0 ; display : inline-flex ; align-items : center ; justify-content : center ; width : 34 px ; height : 34 px ; border-radius : 8 px ; border : 1.5 px solid var ( - - border ) ; background : var ( - - white ) ; color : var ( - - mid ) ; cursor : pointer ; transition : all .2 s ; padding : 0 }
. fm-open-btn : hover { border-color : var ( - - gold ) ; color : var ( - - gold ) ; background : var ( - - gold - pale ) }
. fm-open-btn svg { width : 15 px ; height : 15 px ; pointer-events : none }
/* ── File Manager modal ── */
. fm-overlay { position : fixed ; inset : 0 ; background : rgba ( 0 , 0 , 0 , .55 ) ; z-index : 300 ; display : flex ; align-items : center ; justify-content : center ; padding : 1 rem }
. fm-modal { background : var ( - - white ) ; border-radius : 18 px ; width : 100 % ; max-width : 880 px ; height : 82 vh ; display : flex ; flex-direction : column ; box-shadow : 0 24 px 80 px rgba ( 0 , 0 , 0 , .22 ) ; overflow : hidden }
. fm-head { padding : 1 rem 1.4 rem ; border-bottom : 1 px solid var ( - - border ) ; display : flex ; align-items : center ; gap : .8 rem ; flex-shrink : 0 }
. fm-head-title { font-size : .95 rem ; font-weight : 600 ; flex : 1 }
. fm-search { flex : 1 ; max-width : 240 px ; border : 1.5 px solid var ( - - border ) ; border-radius : 8 px ; padding : .45 rem .8 rem ; font-family : inherit ; font-size : .83 rem ; direction : rtl ; outline : none ; transition : border-color .2 s }
. fm-search : focus { border-color : var ( - - gold ) }
. fm-body { flex : 1 ; overflow-y : auto ; padding : 1.2 rem }
. fm-grid { display : grid ; grid-template-columns : repeat ( auto - fill , minmax ( 150 px , 1 fr ) ) ; gap : .9 rem }
. fm-tile { border : 2 px solid var ( - - border ) ; border-radius : 12 px ; overflow : hidden ; cursor : pointer ; transition : border-color .2 s , box-shadow .2 s ; background : var ( - - bg ) }
. fm-tile : hover { border-color : var ( - - gold ) ; box-shadow : 0 4 px 18 px rgba ( 184 , 149 , 90 , .18 ) }
. fm-tile . fm-selected { border-color : var ( - - gold ) ; box-shadow : 0 0 0 3 px rgba ( 184 , 149 , 90 , .25 ) }
. fm-thumb { width : 100 % ; aspect-ratio : 1 ; background-size : cover ; background-position : center ; background-color : var ( - - section - bg ) }
. fm-info { padding : .5 rem .6 rem .3 rem ; border-top : 1 px solid var ( - - border ) }
. fm-name { display : block ; font-size : .7 rem ; color : var ( - - dark ) ; white-space : nowrap ; overflow : hidden ; text-overflow : ellipsis ; direction : ltr ; text-align : left }
. fm-meta { font-size : .65 rem ; color : var ( - - light ) }
. fm-actions { display : flex ; gap : .35 rem ; padding : .4 rem .6 rem .6 rem }
. fm-actions . btn { flex : 1 ; padding : .35 rem .4 rem ; font-size : .72 rem ; justify-content : center }
. fm-empty { text-align : center ; padding : 4 rem 2 rem ; color : var ( - - mid ) ; font-size : .9 rem }
. fm-loading { text-align : center ; padding : 4 rem 2 rem ; color : var ( - - gold ) ; display : flex ; flex-direction : column ; align-items : center ; gap : .8 rem ; font-size : .85 rem }
@ keyframes spin { to { transform : rotate ( 360 deg ) } }
. seo-score { display : flex ; align-items : center ; gap : .5 rem ; padding : .6 rem 1 rem ; border-radius : 8 px ; font-size : .8 rem }
. seo-score . good { background : #E8F5E9 ; color : #2E7D32 }
. seo-score . ok { background : #FFF3E0 ; color : #E65100 }
. seo-score . bad { background : #FFEBEE ; color : #C62828 }
/* ── Service Before/After pair ── */
. ba-pair { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : .8 rem }
. ba-pair . upload-preview { max-height : 140 px }
2026-06-02 11:01:12 +03:30
/* ── Image Cropper Modal ── */
. cropper-overlay { position : fixed ; inset : 0 ; background : rgba ( 0 , 0 , 0 , .72 ) ; z-index : 500 ; display : flex ; align-items : center ; justify-content : center ; padding : 1 rem }
. cropper-modal { background : #1a1a1a ; border-radius : 18 px ; width : 100 % ; max-width : 680 px ; display : flex ; flex-direction : column ; box-shadow : 0 24 px 80 px rgba ( 0 , 0 , 0 , .5 ) ; overflow : hidden ; max-height : 92 vh }
. cropper-head { padding : .85 rem 1.2 rem ; display : flex ; align-items : center ; gap : .7 rem ; border-bottom : 1 px solid #333 }
. cropper-head-title { color : #fff ; font-size : .95 rem ; font-weight : 600 ; flex : 1 }
. cropper-stage { flex : 1 ; overflow : hidden ; position : relative ; background : #111 ; cursor : crosshair ; user-select : none ; min-height : 320 px }
. cropper-stage canvas { display : block ; width : 100 % ; height : 100 % ; object-fit : contain }
. cropper-box { position : absolute ; border : 2 px solid #B8955A ; box-shadow : 0 0 0 9999 px rgba ( 0 , 0 , 0 , .52 ) ; cursor : move ; box-sizing : border-box }
. cropper-handle { position : absolute ; width : 10 px ; height : 10 px ; background : #B8955A ; border-radius : 50 % ; z-index : 2 }
. cropper-handle . nw { top : -5 px ; left : -5 px ; cursor : nw-resize }
. cropper-handle . ne { top : -5 px ; right : -5 px ; cursor : ne-resize }
. cropper-handle . sw { bottom : -5 px ; left : -5 px ; cursor : sw-resize }
. cropper-handle . se { bottom : -5 px ; right : -5 px ; cursor : se-resize }
. cropper-foot { padding : .85 rem 1.2 rem ; display : flex ; gap : .6 rem ; align-items : center ; border-top : 1 px solid #333 ; flex-wrap : wrap }
. cropper-hint { color : #888 ; font-size : .78 rem ; flex : 1 }
. cropper-ratio-btns { display : flex ; gap : .4 rem }
. cropper-ratio-btn { background : #2a2a2a ; border : 1.5 px solid #444 ; color : #ccc ; padding : .35 rem .7 rem ; border-radius : 8 px ; font-size : .75 rem ; cursor : pointer ; transition : all .2 s ; font-family : inherit }
. cropper-ratio-btn . active , . cropper-ratio-btn : hover { background : #B8955A ; border-color : #B8955A ; color : #fff }
2026-05-31 00:42:08 +03:30
/* ── Icon Picker ── */
. icon-picker-section { margin-top : .9 rem }
. icon-picker-section > label { display : block ; font-size : .82 rem ; font-weight : 600 ; color : var ( - - mid ) ; margin-bottom : .55 rem }
. icon-selected-preview { display : flex ; align-items : center ; gap : .7 rem ; padding : .6 rem .9 rem ; border : 1.5 px solid var ( - - border ) ; border-radius : 8 px ; background : var ( - - section - bg ) ; margin-bottom : .65 rem ; cursor : pointer ; transition : border-color .2 s }
. icon-selected-preview : hover { border-color : var ( - - gold ) }
. icon-preview-box { width : 40 px ; height : 40 px ; display : flex ; align-items : center ; justify-content : center ; background : var ( - - gold - pale ) ; border-radius : 10 px ; color : var ( - - gold ) ; flex-shrink : 0 }
. icon-preview-box svg { width : 22 px ; height : 22 px }
. icon-preview-label { font-size : .82 rem ; color : var ( - - mid ) }
. icon-grid-wrap { border : 1.5 px solid var ( - - border ) ; border-radius : 10 px ; overflow : hidden ; max-height : 0 ; transition : max-height .3 s ease }
. icon-grid-wrap . open { max-height : 280 px ; overflow-y : auto }
. icon-grid { display : grid ; grid-template-columns : repeat ( 8 , 1 fr ) ; gap : 0 ; padding : .5 rem }
. icon-opt { display : flex ; flex-direction : column ; align-items : center ; gap : .3 rem ; padding : .55 rem .2 rem ; border-radius : 8 px ; cursor : pointer ; border : 2 px solid transparent ; transition : all .15 s ; color : var ( - - mid ) }
. icon-opt : hover { background : var ( - - gold - pale ) ; color : var ( - - gold ) }
. icon-opt . selected { border-color : var ( - - gold ) ; background : var ( - - gold - pale ) ; color : var ( - - gold ) }
. icon-opt svg { width : 20 px ; height : 20 px }
. icon-opt span { font-size : .6 rem ; text-align : center ; line-height : 1.2 }
/* ── Modal ── */
. modal-overlay { position : fixed ; inset : 0 ; background : rgba ( 0 , 0 , 0 , .45 ) ; z-index : 200 ; display : flex ; align-items : center ; justify-content : center ; padding : 1 rem }
2026-06-02 11:22:46 +03:30
. modal-overlay . hidden , . fm-overlay . hidden , . cropper-overlay . hidden { display : none }
2026-05-31 00:42:08 +03:30
. modal { background : var ( - - white ) ; border-radius : 16 px ; width : 100 % ; max-width : 720 px ; max-height : 90 vh ; overflow-y : auto }
. modal-header { padding : 1.2 rem 1.5 rem ; border-bottom : 1 px solid var ( - - border ) ; display : flex ; align-items : center ; justify-content : space-between ; position : sticky ; top : 0 ; background : var ( - - white ) ; z-index : 1 }
. modal-title { font-size : 1 rem ; font-weight : 600 }
. modal-close { background : none ; border : none ; cursor : pointer ; padding : .4 rem ; color : var ( - - light ) }
. modal-body { padding : 1.5 rem }
. modal-footer { padding : 1 rem 1.5 rem ; border-top : 1 px solid var ( - - border ) ; display : flex ; gap : .7 rem ; justify-content : flex-start }
/* ── Toast ── */
. toast-container { position : fixed ; bottom : 1.5 rem ; left : 1.5 rem ; z-index : 300 ; display : flex ; flex-direction : column ; gap : .5 rem }
. toast { background : var ( - - dark ) ; color : #fff ; padding : .7 rem 1.2 rem ; border-radius : 10 px ; font-size : .85 rem ; animation : slideIn .3 s ease ; box-shadow : 0 4 px 20 px rgba ( 0 , 0 , 0 , .2 ) }
. toast . success { border-left : 4 px solid #4CAF50 }
. toast . error { border-left : 4 px solid var ( - - danger ) }
@ keyframes slideIn { from { opacity : 0 ; transform : translateY ( 10 px ) } to { opacity : 1 ; transform : translateY ( 0 ) } }
/* ── Rich text editor minimal ── */
. editor-toolbar { border : 1.5 px solid var ( - - border ) ; border-bottom : none ; border-radius : 8 px 8 px 0 0 ; padding : .4 rem .6 rem ; display : flex ; gap : .3 rem ; flex-wrap : wrap ; background : #F8FAFC }
. editor-toolbar button { background : none ; border : none ; padding : .3 rem .5 rem ; border-radius : 4 px ; cursor : pointer ; font-size : .82 rem ; font-family : inherit ; color : var ( - - mid ) }
. editor-toolbar button : hover { background : var ( - - gold - pale ) ; color : var ( - - gold ) }
. editor-content { border : 1.5 px solid var ( - - border ) ; border-radius : 0 0 8 px 8 px ; padding : .8 rem 1 rem ; min-height : 250 px ; outline : none ; font-size : .9 rem ; line-height : 1.8 ; direction : rtl }
/* ── Pages ── */
. page { display : none }
. page . active { display : block }
/* ── Login ── */
. login-screen { position : fixed ; inset : 0 ; background : var ( - - dark ) ; display : flex ; align-items : center ; justify-content : center ; z-index : 1000 }
. login-box { background : var ( - - white ) ; border-radius : 20 px ; padding : 2.5 rem ; width : 380 px ; text-align : center }
. login-logo { font-size : 1.1 rem ; font-weight : 700 ; color : var ( - - gold ) ; margin-bottom : .3 rem }
. login-sub { font-size : .8 rem ; color : var ( - - light ) ; margin-bottom : 2 rem }
. login-screen . hidden { display : none }
< / style >
< / head >
< body >
<!-- ══ LOGIN ══ -->
< div class = "login-screen" id = "loginScreen" >
< div class = "login-box" >
< div class = "login-logo" > دکتر سوسن آلطه< / div >
< div class = "login-sub" > پنل مدیریت محتوا< / div >
< div class = "form-group" style = "margin-bottom:1rem" >
< label > نام کاربری< / label >
< input type = "text" id = "loginUser" value = "admin" placeholder = "نام کاربری" / >
< / div >
< div class = "form-group" style = "margin-bottom:1.5rem" >
< label > رمز عبور< / label >
< input type = "password" id = "loginPass" value = "admin123" placeholder = "رمز عبور" / >
< / div >
< button class = "btn btn-primary" style = "width:100%;justify-content:center" onclick = "doLogin()" > ورود به پنل< / button >
< p id = "loginErr" style = "color:var(--danger);font-size:.8rem;margin-top:.8rem;display:none" > نام کاربری یا رمز عبور اشتباه است< / p >
< / div >
< / div >
<!-- ══ SIDEBAR ══ -->
< aside class = "sidebar" >
< div class = "sidebar-logo" > مدیریت سایت< span > دکتر سوسن آلطه< / span > < / div >
< nav class = "sidebar-nav" >
< div class = "nav-section" > داشبورد< / div >
< div class = "nav-item active" onclick = "showPage('dashboard',this)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < rect x = "3" y = "3" width = "7" height = "7" / > < rect x = "14" y = "3" width = "7" height = "7" / > < rect x = "14" y = "14" width = "7" height = "7" / > < rect x = "3" y = "14" width = "7" height = "7" / > < / svg >
داشبورد
< / div >
< div class = "nav-section" > محتوای سایت< / div >
< div class = "nav-item" onclick = "showPage('hero',this)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" / > < / svg >
صفحه اصلی (هرو)
< / div >
< div class = "nav-item" onclick = "showPage('about',this)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < circle cx = "12" cy = "8" r = "4" / > < path d = "M4 20c0-4 3.6-7 8-7s8 3 8 7" / > < / svg >
درباره من
< / div >
< div class = "nav-item" onclick = "showPage('contact',this)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13.6a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 3h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L7.91 10.6a16 16 0 0 0 6 6l.91-.91a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" / > < / svg >
اطلاعات تماس
< / div >
< div class = "nav-section" > بخشهای سایت< / div >
< div class = "nav-item" onclick = "showPage('services',this)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" / > < / svg >
خدمات
< / div >
< div class = "nav-item" onclick = "showPage('gallery',this)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < rect x = "3" y = "3" width = "18" height = "18" rx = "2" / > < circle cx = "8.5" cy = "8.5" r = "1.5" / > < polyline points = "21 15 16 10 5 21" / > < / svg >
گالری
< / div >
< div class = "nav-item" onclick = "showPage('testimonials',this)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" / > < / svg >
نظرات بیماران
< / div >
< div class = "nav-section" > وبلاگ و SEO< / div >
< div class = "nav-item" onclick = "showPage('blogposts',this)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" / > < polyline points = "14 2 14 8 20 8" / > < / svg >
مقالات
< / div >
< div class = "nav-item" onclick = "showPage('categories',this)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" / > < / svg >
دستهبندیها
< / div >
< div class = "nav-item" onclick = "showPage('faqs',this)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < circle cx = "12" cy = "12" r = "10" / > < path d = "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" / > < line x1 = "12" y1 = "17" x2 = "12.01" y2 = "17" / > < / svg >
سوالات متداول
< / div >
< div class = "nav-item" onclick = "showPage('seo',this)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < circle cx = "11" cy = "11" r = "8" / > < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" / > < / svg >
گزارش SEO
< / div >
2026-06-02 12:27:16 +03:30
< div class = "nav-section" > مدیریت بیماران< / div >
< div class = "nav-item" onclick = "showPage('patients',this)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" / > < circle cx = "9" cy = "7" r = "4" / > < path d = "M23 21v-2a4 4 0 0 0-3-3.87" / > < path d = "M16 3.13a4 4 0 0 1 0 7.75" / > < / svg >
پرونده بیماران
< / div >
< div class = "nav-item" onclick = "showPage('healthrequests',this)" id = "healthreqNavItem" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13.6a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 3h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L7.91 10.6a16 16 0 0 0 6 6l.91-.91a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" / > < / svg >
درخواستها < span id = "healthreqBadge" style = "display:none;background:#E53935;color:#fff;font-size:.65rem;padding:.1rem .4rem;border-radius:50px;margin-right:.3rem" > < / span >
< / div >
2026-05-31 00:42:08 +03:30
< div class = "nav-section" > تنظیمات< / div >
< div class = "nav-item" onclick = "showPage('security',this)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < rect x = "3" y = "11" width = "18" height = "11" rx = "2" / > < path d = "M7 11V7a5 5 0 0 1 10 0v4" / > < / svg >
تغییر رمز عبور
< / div >
< div class = "nav-item" onclick = "showPage('comments',this)" id = "commentsNavItem" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" / > < / svg >
نظرات < span id = "pendingBadge" style = "display:none;background:#E53935;color:#fff;font-size:.65rem;padding:.1rem .4rem;border-radius:50px;margin-right:.3rem" > < / span >
< / div >
< / nav >
< div class = "sidebar-footer" > API: < span id = "apiLabel" > localhost:5000< / span > < / div >
< / aside >
<!-- ══ MAIN ══ -->
< div class = "main" >
< div class = "topbar" >
< div class = "topbar-title" id = "pageTitle" > داشبورد< / div >
< div class = "topbar-actions" >
< a href = "/" target = "_blank" class = "btn btn-secondary btn-sm" > مشاهده سایت< / a >
< button class = "btn btn-danger btn-sm" onclick = "logout()" > خروج< / button >
< / div >
< / div >
< div class = "page-content" >
<!-- ── DASHBOARD ── -->
< div class = "page active" id = "page-dashboard" >
< div class = "stat-grid" id = "dashStats" >
< div class = "stat-card" > < div class = "icon" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" / > < polyline points = "14 2 14 8 20 8" / > < / svg > < / div > < div class = "label" > مقالات منتشرشده< / div > < div class = "value" id = "ds-posts" > -< / div > < / div >
< div class = "stat-card" > < div class = "icon" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" / > < circle cx = "12" cy = "12" r = "3" / > < / svg > < / div > < div class = "label" > کل بازدید مقالات< / div > < div class = "value" id = "ds-views" > -< / div > < / div >
< div class = "stat-card" > < div class = "icon" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" / > < circle cx = "9" cy = "7" r = "4" / > < / svg > < / div > < div class = "label" > نظرات بیماران< / div > < div class = "value" id = "ds-testimonials" > -< / div > < / div >
< div class = "stat-card" > < div class = "icon" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" / > < / svg > < / div > < div class = "label" > مقالات بدون متا< / div > < div class = "value" id = "ds-nometa" style = "color:var(--danger)" > -< / div > < / div >
2026-06-02 15:09:46 +03:30
< div class = "stat-card" onclick = "showPage('patients',document.getElementById('patientsNavItem'))" style = "cursor:pointer" > < div class = "icon" style = "color:#1565C0" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" / > < circle cx = "9" cy = "7" r = "4" / > < path d = "M23 21v-2a4 4 0 0 0-3-3.87" / > < path d = "M16 3.13a4 4 0 0 1 0 7.75" / > < / svg > < / div > < div class = "label" > تعداد بیماران< / div > < div class = "value" id = "ds-patients" > -< / div > < / div >
< div class = "stat-card" onclick = "showPage('healthrequests',document.getElementById('healthreqNavItem'))" style = "cursor:pointer" > < div class = "icon" style = "color:#E53935" > < svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 13.6a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3.6 3h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L7.91 10.6a16 16 0 0 0 6 6l.91-.91a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" / > < / svg > < / div > < div class = "label" > درخواستهای جدید< / div > < div class = "value" id = "ds-requests" style = "color:var(--danger)" > -< / div > < / div >
2026-05-31 00:42:08 +03:30
< / div >
2026-06-02 15:09:46 +03:30
< div style = "display:grid;grid-template-columns:1fr 1fr;gap:1.2rem;margin-bottom:1.2rem" >
< div class = "card" >
< div class = "card-header" > < div class = "card-title" > پربازدیدترین مقالات< / div > < / div >
< div class = "table-wrap" > < table > < thead > < tr > < th > عنوان< / th > < th > بازدید< / th > < th > عملیات< / th > < / tr > < / thead > < tbody id = "topPostsTable" > < / tbody > < / table > < / div >
< / div >
< div class = "card" >
< div class = "card-header" >
< div class = "card-title" > آخرین درخواستهای سلامت< / div >
< button class = "btn btn-secondary btn-sm" onclick = "showPage('healthrequests',document.getElementById('healthreqNavItem'))" > مشاهده همه< / button >
< / div >
< div class = "table-wrap" > < table > < thead > < tr > < th > نام< / th > < th > تلفن< / th > < th > دسته< / th > < th > وضعیت< / th > < / tr > < / thead > < tbody id = "dashReqTable" > < / tbody > < / table > < / div >
< / div >
2026-05-31 00:42:08 +03:30
< / div >
< / div >
<!-- ── HERO ── -->
< div class = "page" id = "page-hero" >
< div class = "card" >
< div class = "card-header" > < div class = "card-title" > ویرایش بخش هرو< / div > < / div >
< div class = "card-body" >
< div class = "form-grid" >
< div class = "form-row" >
< div class = "form-group" > < label > نام دکتر< / label > < input id = "hero-name" / > < / div >
< div class = "form-group" > < label > تخصص (زیر نام)< / label > < input id = "hero-title" / > < / div >
< / div >
< div class = "form-group" > < label > توضیح کوتاه< / label > < textarea id = "hero-subtitle" rows = "2" > < / textarea > < / div >
< div class = "form-row" >
< div class = "form-group" > < label > آمار ۱ - عدد< / label > < input id = "hero-stat1_num" / > < / div >
< div class = "form-group" > < label > آمار ۱ - برچسب< / label > < input id = "hero-stat1_lbl" / > < / div >
< / div >
< div class = "form-row" >
< div class = "form-group" > < label > آمار ۲ - عدد< / label > < input id = "hero-stat2_num" / > < / div >
< div class = "form-group" > < label > آمار ۲ - برچسب< / label > < input id = "hero-stat2_lbl" / > < / div >
< / div >
< div class = "form-row" >
< div class = "form-group" > < label > آمار ۳ - عدد< / label > < input id = "hero-stat3_num" / > < / div >
< div class = "form-group" > < label > آمار ۳ - برچسب< / label > < input id = "hero-stat3_lbl" / > < / div >
< / div >
< div class = "form-row" >
< div class = "form-group" > < label > دکمه اصلی< / label > < input id = "hero-cta_primary" / > < / div >
< div class = "form-group" > < label > دکمه ثانویه< / label > < input id = "hero-cta_secondary" / > < / div >
< / div >
< div class = "form-row" >
< div class = "form-group" > < label > نشان شناور - عنوان< / label > < input id = "hero-badge_title" placeholder = "متخصص پوست و زیبایی" / > < / div >
< div class = "form-group" > < label > نشان شناور - زیرعنوان< / label > < input id = "hero-badge_subtitle" placeholder = "فارغالتحصیل دانشگاه ایران" / > < / div >
< / div >
< div class = "form-group" > < label > نمایش نشان شناور< / label > < select id = "hero-badge_hidden" > < option value = "false" > نمایش< / option > < option value = "true" > مخفی< / option > < / select > < / div >
< div class = "form-group" >
< label > 📷 تصویر دکتر (هرو)< / label >
< input type = "hidden" id = "hero-image" / >
< div class = "input-upload-wrap" >
< button class = "upload-btn" id = "upload-btn-hero-image" onclick = "uploadImage('hero-image','prev-hero-image')" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "17 8 12 3 7 8" / > < line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / > < / svg >
آپلود تصویر
< / button >
< button type = "button" class = "upload-remove" id = "rm-hero-image" onclick = "removeImage('hero-image','prev-hero-image')" style = "display:none" > حذف< / button >
< / div >
< img id = "prev-hero-image" class = "upload-preview" alt = "تصویر هرو" / >
< / div >
< / div >
< div style = "margin-top:1.5rem" > < button class = "btn btn-primary" onclick = "saveSection('hero')" > ذخیره تغییرات< / button > < / div >
< / div >
< / div >
< / div >
<!-- ── ABOUT ── -->
< div class = "page" id = "page-about" >
< div class = "card" >
< div class = "card-header" > < div class = "card-title" > ویرایش بخش درباره من< / div > < / div >
< div class = "card-body" >
< div class = "form-grid" >
< div class = "form-row" >
< div class = "form-group" > < label > عنوان< / label > < input id = "about-title" / > < / div >
< div class = "form-group" > < label > سالهای تجربه< / label > < input id = "about-years_exp" / > < / div >
< / div >
< div class = "form-group" > < label > بیوگرافی< / label > < textarea id = "about-bio" rows = "4" > < / textarea > < / div >
< div class = "form-group" > < label > دستاورد ۱ < / label > < input id = "about-cred1" / > < / div >
< div class = "form-group" > < label > دستاورد ۲< / label > < input id = "about-cred2" / > < / div >
< div class = "form-group" > < label > دستاورد ۳< / label > < input id = "about-cred3" / > < / div >
< div class = "form-group" > < label > دستاورد ۴< / label > < input id = "about-cred4" / > < / div >
< div class = "form-group" > < label > دستاورد ۵ < / label > < input id = "about-cred5" / > < / div >
< div class = "form-group" >
< label > 📷 تصویر بخش «درباره من»< / label >
< input type = "hidden" id = "about-image" / >
< div class = "input-upload-wrap" >
< button class = "upload-btn" id = "upload-btn-about-image" onclick = "uploadImage('about-image','prev-about-image')" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "17 8 12 3 7 8" / > < line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / > < / svg >
آپلود تصویر
< / button >
< button type = "button" class = "upload-remove" id = "rm-about-image" onclick = "removeImage('about-image','prev-about-image')" style = "display:none" > حذف< / button >
< / div >
< img id = "prev-about-image" class = "upload-preview" alt = "تصویر درباره من" / >
< / div >
< / div >
< div style = "margin-top:1.5rem" > < button class = "btn btn-primary" onclick = "saveSection('about')" > ذخیره تغییرات< / button > < / div >
< / div >
< / div >
< / div >
<!-- ── CONTACT ── -->
< div class = "page" id = "page-contact" >
< div class = "card" >
< div class = "card-header" > < div class = "card-title" > اطلاعات تماس< / div > < / div >
< div class = "card-body" >
< div class = "form-grid" >
< div class = "form-row" >
< div class = "form-group" > < label > تلفن< / label > < input id = "contact-phone" / > < / div >
< div class = "form-group" > < label > ایمیل< / label > < input id = "contact-email" / > < / div >
< / div >
< div class = "form-group" > < label > آدرس< / label > < input id = "contact-address" / > < / div >
< div class = "form-group" > < label > ساعات کاری< / label > < input id = "contact-hours" / > < / div >
< div class = "form-row" >
< div class = "form-group" > < label > لینک اینستاگرام< / label > < input id = "contact-instagram" dir = "ltr" placeholder = "https://instagram.com/..." / > < / div >
< div class = "form-group" > < label > لینک واتساپ< / label > < input id = "contact-whatsapp" dir = "ltr" placeholder = "https://wa.me/98..." / > < / div >
< / div >
< div class = "form-row" >
< div class = "form-group" > < label > لینک تلگرام< / label > < input id = "contact-telegram" dir = "ltr" placeholder = "https://t.me/..." / > < / div >
< div class = "form-group" > < label > لینک بله (Bale)< / label > < input id = "contact-bale" dir = "ltr" placeholder = "https://ble.ir/..." / > < / div >
< / div >
< div class = "form-group" > < label > لینک روبیکا (Rubika)< / label > < input id = "contact-rubika" dir = "ltr" placeholder = "https://rubika.ir/..." / > < / div >
< / div >
< div style = "margin-top:1.5rem" > < button class = "btn btn-primary" onclick = "saveSection('contact')" > ذخیره تغییرات< / button > < / div >
< / div >
< / div >
< / div >
<!-- ── SERVICES ── -->
< div class = "page" id = "page-services" >
< div class = "card" >
< div class = "card-header" >
< div class = "card-title" > خدمات< / div >
< button class = "btn btn-primary btn-sm" onclick = "openSvcModal()" > + افزودن خدمت< / button >
< / div >
< div class = "table-wrap" >
< table > < thead > < tr > < th > #< / th > < th > عنوان< / th > < th > توضیح< / th > < th > وضعیت< / th > < th > عملیات< / th > < / tr > < / thead >
< tbody id = "servicesTable" > < / tbody > < / table >
< / div >
< / div >
< / div >
<!-- ── GALLERY ── -->
< div class = "page" id = "page-gallery" >
< div class = "card" >
< div class = "card-header" >
< div class = "card-title" > گالری< / div >
< button class = "btn btn-primary btn-sm" onclick = "openGalleryModal()" > + افزودن تصویر< / button >
< / div >
< div class = "table-wrap" >
< table > < thead > < tr > < th > دسته< / th > < th > توضیح< / th > < th > وضعیت< / th > < th > عملیات< / th > < / tr > < / thead >
< tbody id = "galleryTable" > < / tbody > < / table >
< / div >
< / div >
< / div >
<!-- ── TESTIMONIALS ── -->
< div class = "page" id = "page-testimonials" >
< div class = "card" >
< div class = "card-header" >
< div class = "card-title" > نظرات بیماران< / div >
< button class = "btn btn-primary btn-sm" onclick = "openTestimModal()" > + افزودن نظر< / button >
< / div >
< div class = "table-wrap" >
< table > < thead > < tr > < th > نام< / th > < th > متن< / th > < th > امتیاز< / th > < th > وضعیت< / th > < th > عملیات< / th > < / tr > < / thead >
< tbody id = "testimonialsTable" > < / tbody > < / table >
< / div >
< / div >
< / div >
<!-- ── BLOG POSTS ── -->
< div class = "page" id = "page-blogposts" >
< div class = "card" >
< div class = "card-header" >
< div class = "card-title" > مقالات وبلاگ< / div >
< button class = "btn btn-primary btn-sm" onclick = "openPostEditor()" > + مقاله جدید< / button >
< / div >
< div class = "table-wrap" >
< table > < thead > < tr > < th > عنوان< / th > < th > دسته< / th > < th > کلیدواژه< / th > < th > بازدید< / th > < th > وضعیت< / th > < th > عملیات< / th > < / tr > < / thead >
< tbody id = "postsTable" > < / tbody > < / table >
< / div >
< / div >
< / div >
<!-- ── CATEGORIES ── -->
< div class = "page" id = "page-categories" >
< div class = "card" >
< div class = "card-header" >
< div class = "card-title" > دستهبندیها< / div >
< button class = "btn btn-primary btn-sm" onclick = "openCatModal()" > + دسته جدید< / button >
< / div >
< div class = "table-wrap" >
< table > < thead > < tr > < th > نام< / th > < th > اسلاگ< / th > < th > مقالات< / th > < th > عملیات< / th > < / tr > < / thead >
< tbody id = "categoriesTable" > < / tbody > < / table >
< / div >
< / div >
< / div >
<!-- ── SEO ── -->
< div class = "page" id = "page-seo" >
< div class = "stat-grid" style = "grid-template-columns:repeat(3,1fr)" >
< div class = "stat-card" > < div class = "label" > مقالات منتشرشده< / div > < div class = "value" id = "seo-posts" > -< / div > < / div >
< div class = "stat-card" > < div class = "label" > کل بازدید< / div > < div class = "value" id = "seo-views" > -< / div > < / div >
< div class = "stat-card" > < div class = "label" > مقالات بدون متا< / div > < div class = "value" id = "seo-nometa" style = "color:var(--danger)" > -< / div > < / div >
< / div >
< div class = "card" >
< div class = "card-header" > < div class = "card-title" > پربازدیدترین مقالات< / div > < / div >
< div class = "table-wrap" > < table > < thead > < tr > < th > عنوان< / th > < th > بازدید< / th > < / tr > < / thead > < tbody id = "seoTopPosts" > < / tbody > < / table > < / div >
< / div >
< div class = "card" >
< div class = "card-header" > < div class = "card-title" > راهنمای بهبود SEO< / div > < / div >
< div class = "card-body" style = "font-size:.88rem;line-height:2;color:var(--mid)" >
< p > ✅ هر مقاله باید < strong > MetaTitle< / strong > زیر ۷۰ کاراکتر داشته باشد< / p >
< p > ✅ < strong > MetaDescription< / strong > بین ۱۵۰-۱۶۰ کاراکتر< / p >
< p > ✅ < strong > کلیدواژه اصلی< / strong > در عنوان، اول پاراگراف و URL وجود داشته باشد< / p >
< p > ✅ هر مقاله حداقل < strong > ۵۰۰ کلمه< / strong > داشته باشد< / p >
< p > ✅ از < strong > H2 و H3< / strong > در ساختار مقاله استفاده کنید< / p >
< p > ✅ لینکدهی داخلی بین مقالات انجام دهید< / p >
< p > ✅ تصاویر دارای < strong > alt text< / strong > فارسی باشند< / p >
< p > 🔗 < a href = "../sitemap.xml" target = "_blank" style = "color:var(--gold)" > مشاهده Sitemap< / a > | < a href = "../robots.txt" target = "_blank" style = "color:var(--gold)" > مشاهده Robots.txt< / a > < / p >
< / div >
< / div >
< / div >
<!-- ══ SECURITY PAGE ══ -->
< div class = "page" id = "page-security" >
< div class = "card" style = "max-width:480px" >
< div class = "card-header" > < div class = "card-title" > 🔒 تغییر رمز عبور مدیر< / div > < / div >
< div class = "card-body" >
< div class = "form-grid" >
< div class = "form-group" >
< label > رمز عبور فعلی< / label >
< input type = "password" id = "pw-current" placeholder = "رمز عبور فعلی را وارد کنید" / >
< / div >
< div class = "form-group" >
< label > رمز عبور جدید< / label >
< input type = "password" id = "pw-new" placeholder = "حداقل ۶ کاراکتر" / >
< / div >
< div class = "form-group" >
< label > تکرار رمز عبور جدید< / label >
< input type = "password" id = "pw-confirm" placeholder = "رمز عبور جدید را تکرار کنید" / >
< / div >
< / div >
< div id = "pw-msg" style = "display:none;margin-top:.8rem;padding:.7rem 1rem;border-radius:8px;font-size:.85rem" > < / div >
< div style = "margin-top:1.2rem;display:flex;gap:.7rem" >
< button class = "btn btn-primary" onclick = "changePassword()" > ذخیره رمز جدید< / button >
< button class = "btn btn-secondary" onclick = "document.getElementById('pw-current').value=document.getElementById('pw-new').value=document.getElementById('pw-confirm').value=''" > پاک کردن< / button >
< / div >
< div style = "margin-top:1.5rem;padding:1rem;background:var(--bg);border-radius:8px;font-size:.82rem;color:var(--light);line-height:1.8" >
< strong style = "color:var(--mid)" > نکات امنیتی:< / strong > < br / >
• از ترکیب حروف، اعداد و نماد استفاده کنید< br / >
• رمز عبور را در جای امنی نگه دارید< br / >
• پس از تغییر رمز، مجدداً وارد شوید
< / div >
< / div >
< / div >
< / div >
2026-06-02 12:27:16 +03:30
<!-- ══ PATIENTS PAGE ══ -->
< div class = "page" id = "page-patients" >
< div class = "card" style = "margin-bottom:1.2rem" >
< div class = "card-header" >
< div class = "card-title" > پرونده بیماران< / div >
< div style = "display:flex;gap:.5rem;align-items:center;flex-wrap:wrap" >
< select id = "patientCatFilter" onchange = "loadPatients()" style = "border:1.5px solid var(--border);border-radius:8px;padding:.4rem .7rem;font-family:inherit;font-size:.82rem;background:var(--white)" >
< option value = "" > همه دستهها< / option >
< option value = "beauty" > زیبایی پوست< / option >
< option value = "health" > سلامت عمومی< / option >
< / select >
< input id = "patientSearch" placeholder = "جستجو نام / تلفن..." oninput = "loadPatients()"
style = "border:1.5px solid var(--border);border-radius:8px;padding:.4rem .8rem;font-family:inherit;font-size:.82rem;outline:none;width:180px" / >
< button class = "btn btn-primary btn-sm" onclick = "openPatientModal()" > + ثبت بیمار جدید< / button >
< / div >
< / div >
< div class = "table-wrap" >
< table >
< thead > < tr > < th > نام< / th > < th > تلفن< / th > < th > سن< / th > < th > جنسیت< / th > < th > دسته< / th > < th > ویزیتها< / th > < th > عملیات< / th > < / tr > < / thead >
< tbody id = "patientsTable" > < / tbody >
< / table >
< / div >
< / div >
< / div >
<!-- ══ PATIENT PROFILE (sub - page) ══ -->
< div class = "page" id = "page-patient-profile" >
< div style = "display:flex;align-items:center;gap:.8rem;margin-bottom:1.2rem" >
< button class = "btn btn-secondary btn-sm" onclick = "showPage('patients',document.querySelector('.nav-item:has(+[onclick*=patients])'))" > ← بازگشت< / button >
< h2 id = "profileName" style = "font-size:1.1rem;font-weight:700" > < / h2 >
< / div >
< div style = "display:grid;grid-template-columns:1fr 1fr;gap:1.2rem" >
<!-- Profile card -->
< div class = "card" >
< div class = "card-header" > < div class = "card-title" > اطلاعات شخصی< / div >
< button class = "btn btn-secondary btn-sm" id = "editPatientBtn" onclick = "" > ✏️ ویرایش< / button >
< / div >
< div class = "modal-body" id = "profileInfo" > < / div >
< / div >
<!-- Medical history -->
< div class = "card" >
< div class = "card-header" > < div class = "card-title" > سابقه پزشکی< / div > < / div >
< div class = "modal-body" id = "profileMedical" > < / div >
< / div >
< / div >
<!-- Visits timeline -->
< div class = "card" style = "margin-top:1.2rem" >
< div class = "card-header" >
< div class = "card-title" > سوابق ویزیت< / div >
< button class = "btn btn-primary btn-sm" onclick = "openVisitModal()" > + ثبت ویزیت جدید< / button >
< / div >
< div id = "visitsTimeline" style = "padding:1.2rem" > < / div >
< / div >
< / div >
<!-- ══ HEALTH REQUESTS PAGE ══ -->
< div class = "page" id = "page-healthrequests" >
< div class = "card" >
< div class = "card-header" >
< div class = "card-title" > درخواستهای مراقبت سلامت< / div >
< div style = "display:flex;gap:.5rem" >
< select id = "reqFilter" onchange = "loadHealthRequests()" style = "border:1.5px solid var(--border);border-radius:8px;padding:.4rem .7rem;font-family:inherit;font-size:.82rem;background:var(--white)" >
< option value = "" > همه< / option >
< option value = "false" > بررسی نشده< / option >
< option value = "true" > بررسی شده< / option >
< / select >
< button class = "btn btn-secondary btn-sm" onclick = "loadHealthRequests()" > 🔄 بازخوانی< / button >
< / div >
< / div >
< div class = "table-wrap" >
< table >
< thead > < tr > < th > نام< / th > < th > تلفن< / th > < th > دسته< / th > < th > پیام< / th > < th > تاریخ< / th > < th > وضعیت< / th > < th > عملیات< / th > < / tr > < / thead >
< tbody id = "healthreqTable" > < / tbody >
< / table >
< / div >
< / div >
< / div >
2026-05-31 00:42:08 +03:30
<!-- ══ COMMENTS PAGE ══ -->
< div class = "page" id = "page-comments" >
< div class = "card" >
< div class = "card-header" >
< div class = "card-title" > مدیریت نظرات< / div >
< button class = "btn btn-secondary btn-sm" onclick = "loadComments()" > 🔄 بازخوانی< / button >
< / div >
< div class = "table-wrap" >
< table >
< thead > < tr > < th > نویسنده< / th > < th > ایمیل< / th > < th > نظر< / th > < th > مقاله< / th > < th > تاریخ< / th > < th > وضعیت< / th > < th > عملیات< / th > < / tr > < / thead >
< tbody id = "commentsTable" > < / tbody >
< / table >
< / div >
< / div >
< / div >
<!-- ── FAQS ── -->
< div class = "page" id = "page-faqs" >
< div class = "card" >
< div class = "card-header" >
< div class = "card-title" > سوالات متداول< / div >
< button class = "btn btn-primary btn-sm" onclick = "openFaqModal()" > + افزودن سوال< / button >
< / div >
< div class = "table-wrap" >
< table > < thead > < tr > < th > #< / th > < th > سوال< / th > < th > وضعیت< / th > < th > عملیات< / th > < / tr > < / thead >
< tbody id = "faqsTable" > < / tbody > < / table >
< / div >
< / div >
< / div >
< / div > <!-- /page - content -->
< / div > <!-- /main -->
<!-- ══ MODALS ══ -->
<!-- Service Modal -->
< div class = "modal-overlay hidden" id = "svcModal" >
< div class = "modal" >
< div class = "modal-header" > < div class = "modal-title" id = "svcModalTitle" > افزودن خدمت< / div > < button class = "modal-close" onclick = "closeModal('svcModal')" > ✕< / button > < / div >
< div class = "modal-body" >
< input type = "hidden" id = "svc-id" / >
< input type = "hidden" id = "svc-icon" / >
< div class = "form-grid" >
< div class = "form-group" > < label > عنوان< / label > < input id = "svc-title" / > < / div >
< div class = "form-group" > < label > توضیح< / label > < textarea id = "svc-desc" rows = "3" > < / textarea > < / div >
< div class = "form-row" >
< div class = "form-group" > < label > ترتیب< / label > < input type = "number" id = "svc-order" value = "1" / > < / div >
< div class = "form-group" > < label > وضعیت< / label > < select id = "svc-active" > < option value = "true" > فعال< / option > < option value = "false" > غیرفعال< / option > < / select > < / div >
< / div >
< div class = "icon-picker-section" >
< label > آیکون خدمت< / label >
< div class = "icon-selected-preview" id = "svc-icon-preview" onclick = "toggleIconGrid()" >
< div class = "icon-preview-box" id = "svc-icon-box" > < / div >
< span class = "icon-preview-label" id = "svc-icon-label" > انتخاب آیکون ▾< / span >
< / div >
< div class = "icon-grid-wrap" id = "svc-icon-grid-wrap" >
< div class = "icon-grid" id = "svc-icon-grid" > < / div >
< / div >
< / div >
< div class = "ba-pair" >
< div class = "form-group" >
< label > 📷 تصویر قبل از درمان< / label >
< input type = "hidden" id = "svc-before" / >
< div class = "input-upload-wrap" >
< button class = "upload-btn" id = "upload-btn-svc-before" onclick = "uploadImage('svc-before','prev-svc-before')" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "17 8 12 3 7 8" / > < line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / > < / svg >
آپلود تصویر
< / button >
< button type = "button" class = "upload-remove" id = "rm-svc-before" onclick = "removeImage('svc-before','prev-svc-before')" style = "display:none" > حذف< / button >
< / div >
< img id = "prev-svc-before" class = "upload-preview" alt = "قبل" / >
< / div >
< div class = "form-group" >
< label > ✨ تصویر بعد از درمان< / label >
< input type = "hidden" id = "svc-after" / >
< div class = "input-upload-wrap" >
< button class = "upload-btn" id = "upload-btn-svc-after" onclick = "uploadImage('svc-after','prev-svc-after')" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "17 8 12 3 7 8" / > < line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / > < / svg >
آپلود تصویر
< / button >
< button type = "button" class = "upload-remove" id = "rm-svc-after" onclick = "removeImage('svc-after','prev-svc-after')" style = "display:none" > حذف< / button >
< / div >
< img id = "prev-svc-after" class = "upload-preview" alt = "بعد" / >
< / div >
< / div >
< / div >
< / div >
< div class = "modal-footer" > < button class = "btn btn-primary" onclick = "saveSvc()" > ذخیره< / button > < button class = "btn btn-secondary" onclick = "closeModal('svcModal')" > انصراف< / button > < / div >
< / div >
< / div >
2026-06-02 12:27:16 +03:30
<!-- Patient Modal -->
< div class = "modal-overlay hidden" id = "patientModal" >
< div class = "modal" >
< div class = "modal-header" >
< div class = "modal-title" id = "patientModalTitle" > ثبت بیمار جدید< / div >
< button class = "modal-close" onclick = "closeModal('patientModal')" > ✕< / button >
< / div >
< div class = "modal-body" >
< input type = "hidden" id = "pt-id" / >
< div style = "display:grid;grid-template-columns:1fr 1fr;gap:.8rem" >
< div class = "form-group" > < label > نام و نام خانوادگی *< / label > < input id = "pt-name" placeholder = "نام کامل" / > < / div >
< div class = "form-group" > < label > شماره تلفن *< / label > < input id = "pt-phone" dir = "ltr" placeholder = "09xx" / > < / div >
< div class = "form-group" > < label > ایمیل< / label > < input id = "pt-email" dir = "ltr" placeholder = "email@example.com" / > < / div >
< div class = "form-group" > < label > سن< / label > < input id = "pt-age" type = "number" min = "0" max = "120" placeholder = "سال" / > < / div >
< div class = "form-group" > < label > وزن (kg)< / label > < input id = "pt-weight" type = "number" step = "0.1" placeholder = "kg" / > < / div >
< div class = "form-group" > < label > قد (cm)< / label > < input id = "pt-height" type = "number" step = "0.1" placeholder = "cm" / > < / div >
< div class = "form-group" > < label > جنسیت< / label >
< select id = "pt-gender" > < option value = "" > انتخاب کنید< / option > < option value = "زن" > زن< / option > < option value = "مرد" > مرد< / option > < / select >
< / div >
< div class = "form-group" > < label > گروه خونی< / label >
< select id = "pt-blood" > < option value = "" > نامشخص< / option > < option > A+< / option > < option > A-< / option > < option > B+< / option > < option > B-< / option > < option > AB+< / option > < option > AB-< / option > < option > O+< / option > < option > O-< / option > < / select >
< / div >
< div class = "form-group" > < label > دسته درمانی< / label >
< select id = "pt-cat" > < option value = "beauty" > زیبایی پوست< / option > < option value = "health" > سلامت عمومی< / option > < / select >
< / div >
< / div >
< div class = "form-group" > < label > سابقه بیماری< / label > < textarea id = "pt-history" rows = "3" placeholder = "دیابت، فشار خون، بیماری قلبی و ..." > < / textarea > < / div >
< div class = "form-group" > < label > حساسیتها< / label > < textarea id = "pt-allergy" rows = "2" placeholder = "حساسیت به دارو یا مواد غذایی ..." > < / textarea > < / div >
< div class = "form-group" > < label > داروهای جاری< / label > < textarea id = "pt-meds" rows = "2" placeholder = "داروهایی که بیمار مصرف میکند ..." > < / textarea > < / div >
< div class = "form-group" > < label > یادداشت کلی< / label > < textarea id = "pt-notes" rows = "2" placeholder = "نکات مهم ..." > < / textarea > < / div >
< / div >
< div class = "modal-footer" >
< button class = "btn btn-primary" onclick = "savePatient()" > ذخیره< / button >
< button class = "btn btn-secondary" onclick = "closeModal('patientModal')" > انصراف< / button >
< / div >
< / div >
< / div >
<!-- Visit Modal -->
2026-06-02 16:15:01 +03:30
<!-- Health Request Detail Modal -->
< div class = "modal-overlay hidden" id = "reqDetailModal" >
< div class = "modal" style = "max-width:560px" >
< div class = "modal-header" >
< div class = "modal-title" > جزئیات درخواست< / div >
< button class = "modal-close" onclick = "closeModal('reqDetailModal')" > ✕< / button >
< / div >
< div class = "modal-body" id = "reqDetailBody" > < / div >
< / div >
< / div >
2026-06-02 12:27:16 +03:30
< div class = "modal-overlay hidden" id = "visitModal" >
< div class = "modal" >
< div class = "modal-header" >
< div class = "modal-title" > ثبت ویزیت / یادداشت پزشکی< / div >
< button class = "modal-close" onclick = "closeModal('visitModal')" > ✕< / button >
< / div >
< div class = "modal-body" >
< input type = "hidden" id = "vt-patientId" / >
< div style = "display:grid;grid-template-columns:1fr 1fr;gap:.8rem" >
< div class = "form-group" > < label > عنوان ویزیت< / label > < input id = "vt-title" placeholder = "مثال: ویزیت اول، جلسه لیزر ۲ ..." / > < / div >
< div class = "form-group" > < label > نوع< / label >
< select id = "vt-type" > < option > ویزیت< / option > < option > آزمایش< / option > < option > پروسیجر< / option > < option > مشاوره< / option > < option > پیگیری< / option > < / select >
< / div >
< div class = "form-group" > < label > تاریخ ویزیت< / label > < input id = "vt-date" type = "datetime-local" / > < / div >
< div class = "form-group" > < label > ویزیت بعدی< / label > < input id = "vt-next" type = "date" placeholder = "اختیاری" / > < / div >
< / div >
< div class = "form-group" > < label > شرح حال / یافتهها< / label > < textarea id = "vt-content" rows = "4" placeholder = "معاینه، شکایت بیمار، یافتههای بالینی ..." > < / textarea > < / div >
< div class = "form-group" > < label > تجویز / دستورالعمل< / label > < textarea id = "vt-rx" rows = "3" placeholder = "دارو، دوز، توصیهها ..." > < / textarea > < / div >
< / div >
< div class = "modal-footer" >
< button class = "btn btn-primary" onclick = "saveVisit()" > ذخیره ویزیت< / button >
< button class = "btn btn-secondary" onclick = "closeModal('visitModal')" > انصراف< / button >
< / div >
< / div >
< / div >
2026-05-31 00:42:08 +03:30
<!-- Gallery Modal -->
< div class = "modal-overlay hidden" id = "galleryModal" >
< div class = "modal" >
< div class = "modal-header" > < div class = "modal-title" id = "galleryModalTitle" > افزودن تصویر< / div > < button class = "modal-close" onclick = "closeModal('galleryModal')" > ✕< / button > < / div >
< div class = "modal-body" >
< input type = "hidden" id = "gal-id" / >
< div class = "form-grid" >
< div class = "form-group" >
< label > تصویر اصلی< / label >
< input type = "hidden" id = "gal-img" / >
< div class = "input-upload-wrap" >
< button class = "upload-btn" id = "upload-btn-gal-img" onclick = "uploadImage('gal-img','prev-gal-img')" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "17 8 12 3 7 8" / > < line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / > < / svg >
آپلود تصویر
< / button >
< button type = "button" class = "upload-remove" id = "rm-gal-img" onclick = "removeImage('gal-img','prev-gal-img')" style = "display:none" > حذف< / button >
< / div >
< img id = "prev-gal-img" class = "upload-preview" alt = "پیشنمایش" / >
< / div >
< div class = "form-row" >
< div class = "form-group" >
< label > تصویر قبل< / label >
< input type = "hidden" id = "gal-before" / >
< div class = "input-upload-wrap" >
< button class = "upload-btn" id = "upload-btn-gal-before" onclick = "uploadImage('gal-before','prev-gal-before')" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "17 8 12 3 7 8" / > < line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / > < / svg >
آپلود تصویر
< / button >
< button type = "button" class = "upload-remove" id = "rm-gal-before" onclick = "removeImage('gal-before','prev-gal-before')" style = "display:none" > حذف< / button >
< / div >
< img id = "prev-gal-before" class = "upload-preview" alt = "پیشنمایش قبل" / >
< / div >
< div class = "form-group" >
< label > تصویر بعد< / label >
< input type = "hidden" id = "gal-after" / >
< div class = "input-upload-wrap" >
< button class = "upload-btn" id = "upload-btn-gal-after" onclick = "uploadImage('gal-after','prev-gal-after')" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "17 8 12 3 7 8" / > < line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / > < / svg >
آپلود تصویر
< / button >
< button type = "button" class = "upload-remove" id = "rm-gal-after" onclick = "removeImage('gal-after','prev-gal-after')" style = "display:none" > حذف< / button >
< / div >
< img id = "prev-gal-after" class = "upload-preview" alt = "پیشنمایش بعد" / >
< / div >
< / div >
< div class = "form-row" >
< div class = "form-group" > < label > دستهبندی< / label > < input id = "gal-cat" placeholder = "بوتاکس، لیزر..." / > < / div >
< div class = "form-group" > < label > ترتیب< / label > < input type = "number" id = "gal-order" value = "1" / > < / div >
< / div >
< div class = "form-group" > < label > توضیح< / label > < input id = "gal-caption" / > < / div >
< div class = "form-group" > < label > وضعیت< / label > < select id = "gal-active" > < option value = "true" > فعال< / option > < option value = "false" > غیرفعال< / option > < / select > < / div >
< / div >
< / div >
< div class = "modal-footer" > < button class = "btn btn-primary" onclick = "saveGallery()" > ذخیره< / button > < button class = "btn btn-secondary" onclick = "closeModal('galleryModal')" > انصراف< / button > < / div >
< / div >
< / div >
<!-- Testimonial Modal -->
< div class = "modal-overlay hidden" id = "testimModal" >
< div class = "modal" >
< div class = "modal-header" > < div class = "modal-title" > افزودن / ویرایش نظر< / div > < button class = "modal-close" onclick = "closeModal('testimModal')" > ✕< / button > < / div >
< div class = "modal-body" >
< input type = "hidden" id = "testim-id" / >
< div class = "form-grid" >
< div class = "form-row" >
< div class = "form-group" > < label > نام< / label > < input id = "testim-name" / > < / div >
< div class = "form-group" > < label > ایموجی< / label > < input id = "testim-emoji" value = "👩" / > < / div >
< / div >
< div class = "form-group" > < label > متن نظر< / label > < textarea id = "testim-text" rows = "3" > < / textarea > < / div >
< div class = "form-row" >
< div class = "form-group" > < label > امتیاز (۱ -۵ )< / label > < input type = "number" id = "testim-rating" value = "5" min = "1" max = "5" / > < / div >
< div class = "form-group" > < label > تاریخ< / label > < input id = "testim-date" placeholder = "اردیبهشت ۱۴۰۳" / > < / div >
< / div >
< div class = "form-group" > < label > وضعیت< / label > < select id = "testim-active" > < option value = "true" > فعال< / option > < option value = "false" > غیرفعال< / option > < / select > < / div >
< / div >
< / div >
< div class = "modal-footer" > < button class = "btn btn-primary" onclick = "saveTestim()" > ذخیره< / button > < button class = "btn btn-secondary" onclick = "closeModal('testimModal')" > انصراف< / button > < / div >
< / div >
< / div >
<!-- Category Modal -->
< div class = "modal-overlay hidden" id = "catModal" >
< div class = "modal" style = "max-width:480px" >
< div class = "modal-header" > < div class = "modal-title" id = "catModalTitle" > دسته جدید< / div > < button class = "modal-close" onclick = "closeModal('catModal')" > ✕< / button > < / div >
< div class = "modal-body" >
< input type = "hidden" id = "cat-id" / >
< div class = "form-grid" >
< div class = "form-group" > < label > نام< / label > < input id = "cat-name" / > < / div >
< div class = "form-group" > < label > اسلاگ (خودکار)< / label > < input id = "cat-slug" dir = "ltr" placeholder = "auto-generated" / > < / div >
< div class = "form-group" > < label > توضیح< / label > < textarea id = "cat-desc" rows = "2" > < / textarea > < / div >
< / div >
< / div >
< div class = "modal-footer" > < button class = "btn btn-primary" onclick = "saveCat()" > ذخیره< / button > < button class = "btn btn-secondary" onclick = "closeModal('catModal')" > انصراف< / button > < / div >
< / div >
< / div >
<!-- Blog Post Editor Modal -->
< div class = "modal-overlay hidden" id = "postModal" >
< div class = "modal" style = "max-width:900px" >
< div class = "modal-header" > < div class = "modal-title" id = "postModalTitle" > مقاله جدید< / div > < button class = "modal-close" onclick = "closeModal('postModal')" > ✕< / button > < / div >
< div class = "modal-body" >
< input type = "hidden" id = "post-id" / >
< div class = "form-grid" >
< div class = "form-group" > < label > عنوان مقاله< / label > < input id = "post-title" oninput = "autoSlug()" / > < / div >
< div class = "form-row" >
< div class = "form-group" > < label > اسلاگ (URL)< / label > < input id = "post-slug" dir = "ltr" / > < / div >
< div class = "form-group" > < label > دستهبندی< / label > < select id = "post-category" > < option value = "" > انتخاب دسته...< / option > < / select > < / div >
< / div >
< div class = "form-group" > < label > خلاصه (Excerpt)< / label > < textarea id = "post-excerpt" rows = "2" > < / textarea > < / div >
< div class = "form-group" >
< label > محتوای مقاله< / label >
< div class = "editor-toolbar" >
< button onclick = "fmt('bold')" > < b > B< / b > < / button >
< button onclick = "fmt('italic')" > < i > I< / i > < / button >
< button onclick = "fmtBlock('h2')" > H2< / button >
< button onclick = "fmtBlock('h3')" > H3< / button >
< button onclick = "fmtBlock('p')" > P< / button >
< button onclick = "fmt('insertUnorderedList')" > • لیست< / button >
< button onclick = "fmt('insertOrderedList')" > ۱ . لیست< / button >
< button onclick = "insLink()" > 🔗 لینک< / button >
< / div >
< div class = "editor-content" id = "post-content" contenteditable = "true" dir = "rtl" > < / div >
< / div >
< div style = "border-top:1px solid var(--border);padding-top:1rem;margin-top:.5rem" >
< p style = "font-size:.82rem;font-weight:600;color:var(--gold);margin-bottom:1rem" > تنظیمات SEO< / p >
< div class = "form-group" > < label > کلیدواژه اصلی (Focus Keyword)< / label > < input id = "post-focus" oninput = "checkSeo()" / > < div class = "form-hint" > کلیدواژهای که میخواهید برای آن رتبه بگیرید< / div > < / div >
< div id = "seoFeedback" style = "margin:.5rem 0" > < / div >
< div class = "form-group" > < label > Meta Title < span style = "font-weight:400;color:var(--light)" id = "mtLen" > (0/70)< / span > < / label > < input id = "post-metatitle" oninput = "updateLen('post-metatitle','mtLen',70)" / > < / div >
< div class = "form-group" > < label > Meta Description < span style = "font-weight:400;color:var(--light)" id = "mdLen" > (0/160)< / span > < / label > < textarea id = "post-metadesc" rows = "2" oninput = "updateLen('post-metadesc','mdLen',160)" > < / textarea > < / div >
< div class = "form-group" > < label > کلیدواژهها (با کاما جدا کنید)< / label > < input id = "post-keywords" placeholder = "بوتاکس, جوانسازی, پوست..." / > < / div >
< div class = "form-group" >
< label > 📷 تصویر شاخص< / label >
< input type = "hidden" id = "post-image" / >
< div class = "input-upload-wrap" >
< button class = "upload-btn" id = "upload-btn-post-image" onclick = "uploadImage('post-image','prev-post-image')" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "17 8 12 3 7 8" / > < line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / > < / svg >
آپلود تصویر
< / button >
< button type = "button" class = "upload-remove" id = "rm-post-image" onclick = "removeImage('post-image','prev-post-image')" style = "display:none" > حذف< / button >
< / div >
< img id = "prev-post-image" class = "upload-preview" alt = "تصویر شاخص" / >
< / div >
< div class = "form-row" >
< div class = "form-group" > < label > نوع Schema< / label > < select id = "post-schematype" > < option value = "MedicalWebPage" > MedicalWebPage< / option > < option value = "Article" > Article< / option > < option value = "FAQPage" > FAQPage< / option > < / select > < / div >
< div class = "form-group" > < label > وضعیت انتشار< / label > < select id = "post-published" > < option value = "false" > پیشنویس< / option > < option value = "true" > منتشر< / option > < / select > < / div >
< / div >
< / div >
< / div >
< / div >
< div class = "modal-footer" > < button class = "btn btn-primary" onclick = "savePost()" > ذخیره مقاله< / button > < button class = "btn btn-secondary" onclick = "closeModal('postModal')" > انصراف< / button > < / div >
< / div >
< / div >
<!-- FAQ Modal -->
< div class = "modal-overlay hidden" id = "faqModal" >
< div class = "modal" style = "max-width:640px" >
< div class = "modal-header" > < div class = "modal-title" id = "faqModalTitle" > افزودن سوال< / div > < button class = "modal-close" onclick = "closeModal('faqModal')" > ✕< / button > < / div >
< div class = "modal-body" >
< input type = "hidden" id = "faq-id" / >
< div class = "form-grid" >
< div class = "form-group" > < label > سوال< / label > < input id = "faq-question" / > < / div >
< div class = "form-group" > < label > پاسخ< / label > < textarea id = "faq-answer" rows = "4" > < / textarea > < / div >
< div class = "form-row" >
< div class = "form-group" > < label > ترتیب< / label > < input type = "number" id = "faq-order" value = "1" / > < / div >
< div class = "form-group" > < label > وضعیت< / label > < select id = "faq-active" > < option value = "true" > فعال< / option > < option value = "false" > غیرفعال< / option > < / select > < / div >
< / div >
< / div >
< / div >
< div class = "modal-footer" > < button class = "btn btn-primary" onclick = "saveFaq()" > ذخیره< / button > < button class = "btn btn-secondary" onclick = "closeModal('faqModal')" > انصراف< / button > < / div >
< / div >
< / div >
<!-- Toast container -->
< div class = "toast-container" id = "toastContainer" > < / div >
< script >
const API = '' ;
let token = localStorage . getItem ( 'dr_token' ) || '' ;
// ── Auth ──────────────────────────────────────────────────────────────────────
async function doLogin ( ) {
const u = document . getElementById ( 'loginUser' ) . value ;
const p = document . getElementById ( 'loginPass' ) . value ;
const r = await fetch ( ` ${ API } /api/auth/login ` , {
method : 'POST' , headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { username : u , password : p } )
} ) ;
if ( ! r . ok ) { document . getElementById ( 'loginErr' ) . style . display = 'block' ; return ; }
const d = await r . json ( ) ;
token = d . token ;
localStorage . setItem ( 'dr_token' , token ) ;
document . getElementById ( 'loginScreen' ) . classList . add ( 'hidden' ) ;
init ( ) ;
}
function logout ( ) {
localStorage . removeItem ( 'dr_token' ) ;
location . reload ( ) ;
}
async function changePassword ( ) {
const cur = document . getElementById ( 'pw-current' ) . value ;
const nw = document . getElementById ( 'pw-new' ) . value ;
const cfm = document . getElementById ( 'pw-confirm' ) . value ;
const msg = document . getElementById ( 'pw-msg' ) ;
const show = ( text , ok ) => {
msg . textContent = text ;
msg . style . cssText = ` display:block;padding:.7rem 1rem;border-radius:8px;font-size:.85rem;background: ${ ok ? '#E8F5E9' : '#FFEBEE' } ;color: ${ ok ? '#2E7D32' : '#C62828' } ` ;
} ;
if ( ! cur || ! nw || ! cfm ) { show ( 'همه فیلدها الزامی هستند.' , false ) ; return ; }
if ( nw !== cfm ) { show ( 'رمز عبور جدید و تکرار آن یکسان نیستند.' , false ) ; return ; }
if ( nw . length < 6 ) { show ( 'رمز عبور جدید باید حداقل ۶ کاراکتر باشد.' , false ) ; return ; }
const result = await api ( '/api/auth/change-password' , {
method : 'POST' ,
body : JSON . stringify ( { currentPassword : cur , newPassword : nw } )
} ) ;
if ( result ) {
show ( result . message || 'رمز عبور با موفقیت تغییر کرد. لطفاً مجدداً وارد شوید.' , true ) ;
document . getElementById ( 'pw-current' ) . value = '' ;
document . getElementById ( 'pw-new' ) . value = '' ;
document . getElementById ( 'pw-confirm' ) . value = '' ;
setTimeout ( ( ) => { logout ( ) ; } , 2500 ) ;
}
}
// ── API helper ────────────────────────────────────────────────────────────────
async function api ( path , opts = { } ) {
const h = { 'Content-Type' : 'application/json' } ;
if ( token ) h [ 'Authorization' ] = ` Bearer ${ token } ` ;
const r = await fetch ( ` ${ API } ${ path } ` , { ... opts , headers : { ... h , ... ( opts . headers || { } ) } } ) ;
if ( r . status === 401 ) { logout ( ) ; return null ; }
if ( r . status === 204 ) return null ;
if ( ! r . ok ) { toast ( 'خطا در سرور: ' + r . status , 'error' ) ; return null ; }
return r . json ( ) . catch ( ( ) => null ) ;
}
function toast ( msg , type = 'success' ) {
const c = document . getElementById ( 'toastContainer' ) ;
const el = document . createElement ( 'div' ) ;
el . className = ` toast ${ type } ` ;
el . textContent = msg ;
c . appendChild ( el ) ;
setTimeout ( ( ) => el . remove ( ) , 3500 ) ;
}
// ── Navigation ────────────────────────────────────────────────────────────────
2026-06-02 12:27:16 +03:30
const pageTitles = { dashboard : 'داشبورد' , hero : 'صفحه اصلی' , about : 'درباره من' , contact : 'تماس' , services : 'خدمات' , gallery : 'گالری' , testimonials : 'نظرات' , blogposts : 'مقالات' , categories : 'دستهبندیها' , faqs : 'سوالات متداول' , seo : 'گزارش SEO' , comments : 'مدیریت نظرات' , security : 'تغییر رمز عبور' , patients : 'پرونده بیماران' , 'patient-profile' : 'پرونده بیمار' , healthrequests : 'درخواستهای سلامت' } ;
2026-05-31 00:42:08 +03:30
function showPage ( name , el ) {
document . querySelectorAll ( '.page' ) . forEach ( p => p . classList . remove ( 'active' ) ) ;
document . querySelectorAll ( '.nav-item' ) . forEach ( n => n . classList . remove ( 'active' ) ) ;
document . getElementById ( 'page-' + name ) . classList . add ( 'active' ) ;
el . classList . add ( 'active' ) ;
document . getElementById ( 'pageTitle' ) . textContent = pageTitles [ name ] || name ;
loadPage ( name ) ;
}
function loadPage ( name ) {
if ( name === 'dashboard' ) loadDashboard ( ) ;
else if ( name === 'hero' ) loadSection ( 'hero' ) ;
else if ( name === 'about' ) loadSection ( 'about' ) ;
else if ( name === 'contact' ) loadSection ( 'contact' ) ;
else if ( name === 'services' ) loadServices ( ) ;
else if ( name === 'gallery' ) loadGallery ( ) ;
else if ( name === 'testimonials' ) loadTestimonials ( ) ;
else if ( name === 'blogposts' ) loadPosts ( ) ;
else if ( name === 'categories' ) loadCategories ( ) ;
else if ( name === 'faqs' ) loadFaqs ( ) ;
else if ( name === 'seo' ) loadSeo ( ) ;
else if ( name === 'comments' ) loadComments ( ) ;
2026-06-02 12:27:16 +03:30
else if ( name === 'patients' ) loadPatients ( ) ;
else if ( name === 'healthrequests' ) loadHealthRequests ( ) ;
}
// ── Patients ──────────────────────────────────────────────────────────────────
let patients = [ ] , currentPatientId = null ;
async function loadPatients ( ) {
const cat = document . getElementById ( 'patientCatFilter' ) . value ;
const search = document . getElementById ( 'patientSearch' ) . value ;
const params = new URLSearchParams ( ) ;
if ( cat ) params . set ( 'category' , cat ) ;
if ( search ) params . set ( 'search' , search ) ;
patients = await api ( '/api/patients?' + params ) || [ ] ;
const catLabel = { beauty : 'زیبایی پوست' , health : 'سلامت عمومی' } ;
document . getElementById ( 'patientsTable' ) . innerHTML = patients . map ( p => `
<tr>
<td><strong> ${ p . fullName } </strong></td>
<td dir="ltr"> ${ p . phoneNumber } </td>
<td> ${ p . age || '-' } </td>
<td> ${ p . gender || '-' } </td>
<td><span style="background: ${ p . category === 'health' ? '#E3F2FD' : '#FCE4EC' } ;color: ${ p . category === 'health' ? '#1565C0' : '#880E4F' } ;padding:2px 8px;border-radius:20px;font-size:.72rem"> ${ catLabel [ p . category ] || p . category } </span></td>
<td> ${ p . visitCount } </td>
<td>
<button class="btn btn-secondary btn-sm" onclick="openProfile( ${ p . id } )">پرونده</button>
<button class="btn btn-secondary btn-sm" onclick="editPatient( ${ p . id } )">ویرایش</button>
<button class="btn btn-danger btn-sm" onclick="deletePatient( ${ p . id } )">حذف</button>
</td>
</tr> ` ) . join ( '' ) || '<tr><td colspan="7" style="text-align:center;color:var(--light);padding:2rem">بیماری ثبت نشده</td></tr>' ;
}
function openPatientModal ( ) {
[ 'id' , 'name' , 'phone' , 'email' , 'age' , 'weight' , 'height' ] . forEach ( f => document . getElementById ( ` pt- ${ f } ` ) . value = '' ) ;
[ 'gender' , 'blood' , 'cat' ] . forEach ( f => document . getElementById ( ` pt- ${ f } ` ) . selectedIndex = 0 ) ;
[ 'history' , 'allergy' , 'meds' , 'notes' ] . forEach ( f => document . getElementById ( ` pt- ${ f } ` ) . value = '' ) ;
document . getElementById ( 'patientModalTitle' ) . textContent = 'ثبت بیمار جدید' ;
document . getElementById ( 'patientModal' ) . classList . remove ( 'hidden' ) ;
}
function editPatient ( id ) {
const p = patients . find ( x => x . id === id ) ; if ( ! p ) return ;
document . getElementById ( 'pt-id' ) . value = p . id ;
document . getElementById ( 'pt-name' ) . value = p . fullName || '' ;
document . getElementById ( 'pt-phone' ) . value = p . phoneNumber || '' ;
document . getElementById ( 'pt-email' ) . value = p . email || '' ;
document . getElementById ( 'pt-age' ) . value = p . age || '' ;
document . getElementById ( 'pt-weight' ) . value = p . weight || '' ;
document . getElementById ( 'pt-height' ) . value = p . height || '' ;
document . getElementById ( 'pt-gender' ) . value = p . gender || '' ;
document . getElementById ( 'pt-blood' ) . value = p . bloodType || '' ;
document . getElementById ( 'pt-cat' ) . value = p . category || 'beauty' ;
document . getElementById ( 'patientModalTitle' ) . textContent = 'ویرایش بیمار' ;
document . getElementById ( 'patientModal' ) . classList . remove ( 'hidden' ) ;
}
async function savePatient ( ) {
const id = document . getElementById ( 'pt-id' ) . value ;
const body = {
fullName : document . getElementById ( 'pt-name' ) . value ,
phoneNumber : document . getElementById ( 'pt-phone' ) . value ,
email : document . getElementById ( 'pt-email' ) . value ,
age : parseInt ( document . getElementById ( 'pt-age' ) . value ) || 0 ,
weight : parseFloat ( document . getElementById ( 'pt-weight' ) . value ) || 0 ,
height : parseFloat ( document . getElementById ( 'pt-height' ) . value ) || 0 ,
gender : document . getElementById ( 'pt-gender' ) . value ,
bloodType : document . getElementById ( 'pt-blood' ) . value ,
diseaseHistory : document . getElementById ( 'pt-history' ) . value ,
allergies : document . getElementById ( 'pt-allergy' ) . value ,
medications : document . getElementById ( 'pt-meds' ) . value ,
notes : document . getElementById ( 'pt-notes' ) . value ,
category : document . getElementById ( 'pt-cat' ) . value ,
isActive : true
} ;
if ( id ) await api ( ` /api/patients/ ${ id } ` , { method : 'PUT' , body : JSON . stringify ( body ) } ) ;
else await api ( '/api/patients' , { method : 'POST' , body : JSON . stringify ( body ) } ) ;
closeModal ( 'patientModal' ) ; toast ( 'ذخیره شد ✓' ) ; loadPatients ( ) ;
}
async function deletePatient ( id ) { if ( ! confirm ( 'حذف بیمار؟' ) ) return ; await api ( ` /api/patients/ ${ id } ` , { method : 'DELETE' } ) ; toast ( 'حذف شد' , 'error' ) ; loadPatients ( ) ; }
// Patient Profile
async function openProfile ( id ) {
currentPatientId = id ;
const p = await api ( ` /api/patients/ ${ id } ` ) ;
if ( ! p ) return ;
document . getElementById ( 'profileName' ) . textContent = p . fullName + ( p . category === 'health' ? ' — سلامت عمومی' : ' — زیبایی پوست' ) ;
document . getElementById ( 'editPatientBtn' ) . onclick = ( ) => editPatient ( id ) ;
document . getElementById ( 'profileInfo' ) . innerHTML = `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.7rem;font-size:.88rem">
<div><b>تلفن:</b> <span dir="ltr"> ${ p . phoneNumber || '-' } </span></div>
<div><b>ایمیل:</b> ${ p . email || '-' } </div>
<div><b>سن:</b> ${ p . age || '-' } </div>
<div><b>جنسیت:</b> ${ p . gender || '-' } </div>
<div><b>وزن:</b> ${ p . weight || '-' } kg</div>
<div><b>قد:</b> ${ p . height || '-' } cm</div>
<div><b>گروه خونی:</b> ${ p . bloodType || '-' } </div>
<div><b>تاریخ ثبت:</b> ${ new Date ( p . createdAt ) . toLocaleDateString ( 'fa-IR' ) } </div>
</div>
${ p . notes ? ` <p style="margin-top:.8rem;color:var(--mid);font-size:.85rem"> ${ p . notes } </p> ` : '' }
` ;
document . getElementById ( 'profileMedical' ) . innerHTML = `
<div style="font-size:.88rem;line-height:2">
<div><b>سابقه بیماری:</b><br><span style="color:var(--mid)"> ${ p . diseaseHistory || '—' } </span></div>
<div style="margin-top:.8rem"><b>حساسیتها:</b><br><span style="color:var(--mid)"> ${ p . allergies || '—' } </span></div>
<div style="margin-top:.8rem"><b>داروها:</b><br><span style="color:var(--mid)"> ${ p . medications || '—' } </span></div>
</div>
` ;
renderVisits ( p . visits || [ ] ) ;
showPage ( 'patient-profile' , null ) ;
}
function renderVisits ( visits ) {
const tl = document . getElementById ( 'visitsTimeline' ) ;
if ( ! visits . length ) { tl . innerHTML = '<p style="color:var(--light);text-align:center;padding:2rem">ویزیتی ثبت نشده</p>' ; return ; }
const typeColors = { 'ویزیت' : '#1976D2' , 'آزمایش' : '#388E3C' , 'پروسیجر' : '#7B1FA2' , 'مشاوره' : '#F57C00' , 'پیگیری' : '#0288D1' } ;
tl . innerHTML = visits . map ( v => `
<div style="border-right:3px solid ${ typeColors [ v . visitType ] || '#999' } ;padding:.8rem 1rem .8rem 0;margin-bottom:1.2rem;padding-right:1rem">
<div style="display:flex;align-items:center;gap:.6rem;margin-bottom:.4rem">
<span style="background: ${ typeColors [ v . visitType ] || '#999' } ;color:#fff;padding:2px 10px;border-radius:20px;font-size:.72rem"> ${ v . visitType } </span>
<strong style="font-size:.9rem"> ${ v . title } </strong>
<span style="color:var(--light);font-size:.78rem;margin-right:auto"> ${ new Date ( v . visitDate ) . toLocaleDateString ( 'fa-IR' ) } </span>
<button class="btn btn-danger btn-sm" style="padding:.2rem .6rem;font-size:.7rem" onclick="deleteVisit( ${ v . id } )">حذف</button>
</div>
${ v . content ? ` <p style="font-size:.85rem;color:var(--dark);margin:.3rem 0"> ${ v . content } </p> ` : '' }
${ v . prescription ? ` <div style="background:#F8F9FA;border-radius:8px;padding:.5rem .8rem;font-size:.82rem;margin-top:.4rem;color:#333"><b>تجویز:</b> ${ v . prescription } </div> ` : '' }
${ v . nextVisitDate ? ` <p style="font-size:.78rem;color:var(--gold);margin-top:.4rem">📅 ویزیت بعدی: ${ new Date ( v . nextVisitDate ) . toLocaleDateString ( 'fa-IR' ) } </p> ` : '' }
</div> ` ) . join ( '' ) ;
}
function openVisitModal ( ) {
document . getElementById ( 'vt-patientId' ) . value = currentPatientId ;
[ 'title' , 'content' , 'rx' ] . forEach ( f => document . getElementById ( ` vt- ${ f } ` ) . value = '' ) ;
document . getElementById ( 'vt-type' ) . selectedIndex = 0 ;
document . getElementById ( 'vt-date' ) . value = new Date ( ) . toISOString ( ) . slice ( 0 , 16 ) ;
document . getElementById ( 'vt-next' ) . value = '' ;
document . getElementById ( 'visitModal' ) . classList . remove ( 'hidden' ) ;
}
async function saveVisit ( ) {
const pid = document . getElementById ( 'vt-patientId' ) . value ;
const body = {
title : document . getElementById ( 'vt-title' ) . value ,
content : document . getElementById ( 'vt-content' ) . value ,
prescription : document . getElementById ( 'vt-rx' ) . value ,
visitType : document . getElementById ( 'vt-type' ) . value ,
visitDate : document . getElementById ( 'vt-date' ) . value || new Date ( ) . toISOString ( ) ,
nextVisitDate : document . getElementById ( 'vt-next' ) . value || null
} ;
await api ( ` /api/patients/ ${ pid } /visits ` , { method : 'POST' , body : JSON . stringify ( body ) } ) ;
closeModal ( 'visitModal' ) ; toast ( 'ویزیت ثبت شد ✓' ) ;
openProfile ( parseInt ( pid ) ) ;
}
async function deleteVisit ( vid ) { if ( ! confirm ( 'حذف ویزیت؟' ) ) return ; await api ( ` /api/patients/visits/ ${ vid } ` , { method : 'DELETE' } ) ; toast ( 'حذف شد' , 'error' ) ; openProfile ( currentPatientId ) ; }
// ── Health Requests ───────────────────────────────────────────────────────────
2026-06-02 16:15:01 +03:30
let _allReqs = [ ] ;
2026-06-02 12:27:16 +03:30
async function loadHealthRequests ( ) {
const filter = document . getElementById ( 'reqFilter' ) . value ;
const params = filter !== '' ? ` ?handled= ${ filter } ` : '' ;
2026-06-02 16:15:01 +03:30
_allReqs = await api ( '/api/health-requests' + params ) || [ ] ;
const pending = _allReqs . filter ( r => ! r . isHandled ) . length ;
2026-06-02 12:27:16 +03:30
const badge = document . getElementById ( 'healthreqBadge' ) ;
if ( pending > 0 ) { badge . textContent = pending ; badge . style . display = 'inline' ; } else { badge . style . display = 'none' ; }
const catLabel = { beauty : 'زیبایی پوست' , health : 'سلامت عمومی' } ;
2026-06-02 16:15:01 +03:30
document . getElementById ( 'healthreqTable' ) . innerHTML = _allReqs . map ( r => `
<tr style=" ${ ! r . isHandled ? 'font-weight:600' : 'opacity:.75' } ">
2026-06-02 12:27:16 +03:30
<td> ${ r . fullName } </td>
<td dir="ltr"> ${ r . phoneNumber } </td>
<td> ${ catLabel [ r . category ] || r . category } </td>
2026-06-02 16:15:01 +03:30
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--mid);font-size:.85rem"> ${ r . message || '—' } </td>
2026-06-02 12:27:16 +03:30
<td> ${ new Date ( r . createdAt ) . toLocaleDateString ( 'fa-IR' ) } </td>
<td><span style="background: ${ r . isHandled ? '#E8F5E9' : '#FFEBEE' } ;color: ${ r . isHandled ? '#388E3C' : '#C62828' } ;padding:2px 8px;border-radius:20px;font-size:.72rem"> ${ r . isHandled ? 'بررسی شده' : 'جدید' } </span></td>
2026-06-02 16:15:01 +03:30
<td style="white-space:nowrap">
<button class="btn btn-secondary btn-sm" onclick="viewReq( ${ r . id } )">مشاهده</button>
2026-06-02 12:27:16 +03:30
${ ! r . isHandled ? ` <button class="btn btn-secondary btn-sm" onclick="handleReq( ${ r . id } )">✓ بررسی شد</button> ` : '' }
<button class="btn btn-danger btn-sm" onclick="deleteReq( ${ r . id } )">حذف</button>
</td>
</tr> ` ) . join ( '' ) || '<tr><td colspan="7" style="text-align:center;color:var(--light);padding:2rem">درخواستی وجود ندارد</td></tr>' ;
2026-05-31 00:42:08 +03:30
}
2026-06-02 16:15:01 +03:30
function viewReq ( id ) {
const r = _allReqs . find ( x => x . id === id ) ; if ( ! r ) return ;
const catLabel = { beauty : 'زیبایی پوست' , health : 'سلامت عمومی' } ;
document . getElementById ( 'reqDetailBody' ) . innerHTML = `
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1.2rem">
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.25rem">نام</span><strong> ${ r . fullName } </strong></div>
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.25rem">تلفن</span><strong dir="ltr"> ${ r . phoneNumber } </strong></div>
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.25rem">ایمیل</span><span> ${ r . email || '—' } </span></div>
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.25rem">دسته</span>
<span style="background: ${ r . category === 'health' ? '#E3F2FD' : 'var(--gold-pale)' } ;color: ${ r . category === 'health' ? '#1565C0' : 'var(--gold)' } ;padding:3px 10px;border-radius:20px;font-size:.78rem"> ${ catLabel [ r . category ] || r . category } </span>
</div>
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.25rem">تاریخ ثبت</span><span> ${ new Date ( r . createdAt ) . toLocaleDateString ( 'fa-IR' ) } </span></div>
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.25rem">وضعیت</span>
<span style="background: ${ r . isHandled ? '#E8F5E9' : '#FFEBEE' } ;color: ${ r . isHandled ? '#388E3C' : '#C62828' } ;padding:3px 10px;border-radius:20px;font-size:.78rem"> ${ r . isHandled ? 'بررسی شده' : 'جدید' } </span>
</div>
</div>
<div><span style="font-size:.75rem;color:var(--light);display:block;margin-bottom:.5rem">پیام / شرح درخواست</span>
<div style="background:var(--section-bg);border-radius:12px;padding:1rem 1.2rem;line-height:2;font-size:.9rem;white-space:pre-wrap;min-height:60px"> ${ r . message || '—' } </div>
</div>
${ ! r . isHandled ? ` <div style="margin-top:1.2rem"><button class="btn btn-primary" onclick="handleReq( ${ r . id } );closeModal('reqDetailModal')">✓ علامتگذاری به عنوان بررسی شده</button></div> ` : '' }
` ;
document . getElementById ( 'reqDetailModal' ) . classList . remove ( 'hidden' ) ;
}
2026-06-02 12:27:16 +03:30
async function handleReq ( id ) { await api ( ` /api/health-requests/ ${ id } ` , { method : 'PUT' } ) ; toast ( 'علامتگذاری شد ✓' ) ; loadHealthRequests ( ) ; }
async function deleteReq ( id ) { if ( ! confirm ( 'حذف؟' ) ) return ; await api ( ` /api/health-requests/ ${ id } ` , { method : 'DELETE' } ) ; toast ( 'حذف شد' , 'error' ) ; loadHealthRequests ( ) ; }
2026-05-31 00:42:08 +03:30
// ── Comments ──────────────────────────────────────────────────────────────────
async function loadComments ( ) {
const all = await api ( '/api/comments' ) || [ ] ;
const pending = all . filter ( c => ! c . isApproved ) ;
// update badge
const badge = document . getElementById ( 'pendingBadge' ) ;
if ( pending . length > 0 ) { badge . textContent = pending . length ; badge . style . display = 'inline' ; }
else badge . style . display = 'none' ;
// update dashboard stat
const dsEl = document . getElementById ( 'ds-testimonials' ) ;
if ( dsEl ) dsEl . textContent = pending . length + ' نظر در انتظار' ;
const tbody = document . getElementById ( 'commentsTable' ) ;
if ( ! tbody ) return ;
if ( ! all . length ) { tbody . innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--light);padding:2rem">هیچ نظری ثبت نشده</td></tr>' ; return ; }
tbody . innerHTML = all . filter ( c => ! c . isAdminReply ) . map ( c => `
<tr id="ctr- ${ c . id } ">
<td><strong> ${ esc ( c . authorName ) } </strong></td>
<td style="font-size:.78rem;color:var(--light)"> ${ esc ( c . authorEmail || '—' ) } </td>
<td style="max-width:220px;white-space:normal;font-size:.83rem"> ${ esc ( c . body ) } </td>
<td><a href="/blog/ ${ c . postSlug } " target="_blank" style="color:var(--gold);font-size:.8rem"> ${ esc ( c . postTitle || '—' ) } </a></td>
<td style="font-size:.78rem;color:var(--light)"> ${ new Date ( c . createdAt ) . toLocaleDateString ( 'fa-IR' ) } </td>
<td><span class="badge ${ c . isApproved ? 'badge-green' : 'badge-red' } "> ${ c . isApproved ? 'تأییدشده' : 'در انتظار' } </span></td>
<td style="white-space:nowrap">
${ ! c . isApproved ? ` <button class="btn btn-secondary btn-sm" onclick="approveComment( ${ c . id } )">✓ تأیید</button> ` : '' }
<button class="btn btn-secondary btn-sm" onclick="toggleReplyBox( ${ c . id } )" style="color:var(--gold)">↩ پاسخ</button>
<button class="btn btn-danger btn-sm" onclick="deleteComment( ${ c . id } )">حذف</button>
</td>
</tr>
<tr id="reply-row- ${ c . id } " style="display:none;background:var(--gold-pale)">
<td colspan="7" style="padding:.8rem 1.2rem">
<div style="display:flex;gap:.6rem;align-items:flex-start">
<div style="font-size:.78rem;color:var(--gold);font-weight:600;padding-top:.5rem;white-space:nowrap">↩ پاسخ به ${ esc ( c . authorName ) } :</div>
<textarea id="replyText- ${ c . id } " rows="2" style="flex:1;border:1.5px solid var(--border);border-radius:8px;padding:.5rem .8rem;font-family:inherit;font-size:.85rem;direction:rtl;resize:none" placeholder="پاسخ خود را بنویسید..."></textarea>
<button class="btn btn-primary btn-sm" onclick="sendReply( ${ c . id } )">ارسال</button>
<button class="btn btn-secondary btn-sm" onclick="toggleReplyBox( ${ c . id } )">✕</button>
</div>
</td>
</tr> ` ) . join ( '' ) ;
}
function esc ( s ) { return String ( s || '' ) . replace ( /&/g , '&' ) . replace ( /</g , '<' ) . replace ( />/g , '>' ) ; }
async function approveComment ( id ) {
await api ( ` /api/comments/ ${ id } /approve ` , { method : 'PUT' } ) ;
toast ( 'نظر تأیید شد ✓' ) ;
loadComments ( ) ;
}
async function deleteComment ( id ) {
if ( ! confirm ( 'این نظر حذف شود؟' ) ) return ;
await api ( ` /api/comments/ ${ id } ` , { method : 'DELETE' } ) ;
toast ( 'نظر حذف شد' , 'error' ) ;
loadComments ( ) ;
}
function toggleReplyBox ( id ) {
const row = document . getElementById ( ` reply-row- ${ id } ` ) ;
if ( ! row ) return ;
const visible = row . style . display !== 'none' ;
row . style . display = visible ? 'none' : 'table-row' ;
if ( ! visible ) document . getElementById ( ` replyText- ${ id } ` ) ? . focus ( ) ;
}
async function sendReply ( id ) {
const textarea = document . getElementById ( ` replyText- ${ id } ` ) ;
const body = textarea ? . value ? . trim ( ) ;
if ( ! body ) { toast ( 'متن پاسخ را وارد کنید' , 'error' ) ; return ; }
const result = await api ( ` /api/comments/ ${ id } /reply ` , {
method : 'POST' ,
body : JSON . stringify ( { body } )
} ) ;
if ( result ) { toast ( 'پاسخ ارسال شد ✓' ) ; loadComments ( ) ; }
}
// ── Dashboard ─────────────────────────────────────────────────────────────────
async function loadDashboard ( ) {
2026-06-02 15:09:46 +03:30
const [ seo , testims , patients , reqs ] = await Promise . all ( [
2026-05-31 00:42:08 +03:30
api ( '/api/seo/stats' ) ,
2026-06-02 15:09:46 +03:30
api ( '/api/testimonials/all' ) ,
api ( '/api/patients' ) ,
api ( '/api/health-requests' )
2026-05-31 00:42:08 +03:30
] ) ;
if ( seo ) {
document . getElementById ( 'ds-posts' ) . textContent = seo . total ;
document . getElementById ( 'ds-views' ) . textContent = seo . views ;
document . getElementById ( 'ds-nometa' ) . textContent = seo . noMeta ;
const tb = document . getElementById ( 'topPostsTable' ) ;
tb . innerHTML = seo . topPosts . map ( p => ` <tr><td> ${ p . title } </td><td><span class="badge badge-gold"> ${ p . viewCount } </span></td><td><a href="/blog/ ${ p . slug } " target="_blank" class="btn btn-secondary btn-sm">مشاهده</a></td></tr> ` ) . join ( '' ) ;
}
if ( testims ) document . getElementById ( 'ds-testimonials' ) . textContent = testims . length ;
2026-06-02 15:09:46 +03:30
if ( patients ) document . getElementById ( 'ds-patients' ) . textContent = patients . length ;
if ( reqs ) {
const pending = reqs . filter ( r => ! r . isHandled ) ;
document . getElementById ( 'ds-requests' ) . textContent = pending . length ;
const catLabel = { beauty : 'زیبایی پوست' , health : 'سلامت عمومی' } ;
document . getElementById ( 'dashReqTable' ) . innerHTML = reqs . slice ( 0 , 6 ) . map ( r => `
<tr style=" ${ ! r . isHandled ? 'font-weight:600' : 'opacity:.65' } ">
<td> ${ r . fullName } </td>
<td dir="ltr" style="font-size:.8rem"> ${ r . phoneNumber } </td>
<td style="font-size:.78rem"> ${ catLabel [ r . category ] || r . category } </td>
<td><span style="background: ${ r . isHandled ? '#E8F5E9' : '#FFEBEE' } ;color: ${ r . isHandled ? '#388E3C' : '#C62828' } ;padding:2px 8px;border-radius:20px;font-size:.7rem"> ${ r . isHandled ? 'بررسی شده' : 'جدید' } </span></td>
</tr> ` ) . join ( '' ) || '<tr><td colspan="4" style="text-align:center;color:var(--light);padding:1rem">درخواستی وجود ندارد</td></tr>' ;
// update sidebar badge
const badge = document . getElementById ( 'healthreqBadge' ) ;
if ( pending . length > 0 ) { badge . textContent = pending . length ; badge . style . display = 'inline' ; } else { badge . style . display = 'none' ; }
}
2026-05-31 00:42:08 +03:30
}
// ── Section settings ──────────────────────────────────────────────────────────
async function loadSection ( sec ) {
const data = await api ( ` /api/settings/ ${ sec } ` ) ;
if ( ! data ) return ;
Object . entries ( data ) . forEach ( ( [ k , v ] ) => {
const el = document . getElementById ( ` ${ sec } - ${ k } ` ) ;
if ( el ) {
el . value = v ;
// auto-show image preview if a corresponding prev- element exists
const prev = document . getElementById ( ` prev- ${ sec } - ${ k } ` ) ;
if ( prev ) syncPreview ( ` ${ sec } - ${ k } ` , ` prev- ${ sec } - ${ k } ` ) ;
}
} ) ;
}
async function saveSection ( sec ) {
const inputs = document . querySelectorAll ( ` [id^=" ${ sec } -"] ` ) ;
const settings = { } ;
inputs . forEach ( el => {
const key = el . id . replace ( ` ${ sec } - ` , '' ) ;
settings [ key ] = el . value ;
} ) ;
await api ( ` /api/settings/ ${ sec } ` , { method : 'PUT' , body : JSON . stringify ( { settings } ) } ) ;
toast ( 'تغییرات با موفقیت ذخیره شد ✓' ) ;
}
// ── Service Icon Picker ───────────────────────────────────────────────────────
const SERVICE _ICONS = [
{ key : 'syringe' , label : 'تزریق' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 2l4 4-4 4"/><path d="M22 6H9"/><path d="M7 8l-5 5 5 5"/><path d="M2 13h10"/><path d="M9 6v12"/><circle cx="14" cy="18" r="2"/></svg>' } ,
{ key : 'laser' , label : 'لیزر' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/><circle cx="12" cy="12" r="3"/></svg>' } ,
{ key : 'face' , label : 'صورت' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="5"/><path d="M3 21c0-4.418 4.03-8 9-8s9 3.582 9 8"/></svg>' } ,
{ key : 'eye' , label : 'چشم' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>' } ,
{ key : 'smile' , label : 'رضایت' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>' } ,
{ key : 'sparkle' , label : 'جوانسازی' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z"/><path d="M19 2l.75 2.25L22 5l-2.25.75L19 8l-.75-2.25L16 5l2.25-.75L19 2z"/></svg>' } ,
{ key : 'drop' , label : 'هیدراتاسیون' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"/></svg>' } ,
{ key : 'leaf' , label : 'طبیعی' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 8C8 10 5.9 16.17 3.82 19.5c1.17.17 2.35.5 3.18.5C12 20 17 14 17 8z"/><path d="M3 22l2-4"/></svg>' } ,
{ key : 'sun' , label : 'نور درمانی' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>' } ,
{ key : 'zap' , label : 'انرژی' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>' } ,
{ key : 'shield' , label : 'محافظت' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>' } ,
{ key : 'heart' , label : 'زیبایی' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>' } ,
{ key : 'star' , label : 'برتر' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>' } ,
{ key : 'award' , label : 'کیفیت' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89L17 22l-5-3-5 3 1.523-9.11"/></svg>' } ,
{ key : 'gem' , label : 'لوکس' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="6 3 18 3 22 9 12 22 2 9"/><polyline points="22 9 12 9 6 3"/><line x1="12" y1="22" x2="12" y2="9"/><line x1="2" y1="9" x2="6" y2="3"/><line x1="22" y1="9" x2="18" y2="3"/></svg>' } ,
{ key : 'scissors' , label : 'اصلاح' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/></svg>' } ,
{ key : 'feather' , label : 'لطیف' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"/><line x1="16" y1="8" x2="2" y2="22"/><line x1="17.5" y1="15" x2="9" y2="15"/></svg>' } ,
{ key : 'activity' , label : 'نشاط' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>' } ,
{ key : 'refresh' , label : 'ترمیم' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>' } ,
{ key : 'plus' , label : 'پزشکی' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>' } ,
{ key : 'wind' , label : 'مراقبت' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2 2 0 1 1 19.5 12H2"/></svg>' } ,
{ key : 'wand' , label : 'جادوی زیبایی' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 4V2m0 14v-2M8 9h2m10 0h2m-4.2 2.8L17.2 13.2M17.2 4.8l1.4 1.4M10.8 11.8L9.4 13.2M10.8 4.8L9.4 3.4"/><path d="M3 21l9-9"/></svg>' } ,
{ key : 'user' , label : 'کلینیک' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>' } ,
{ key : 'package' , label : 'بسته درمانی' , svg : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>' } ,
] ;
function renderIconPicker ( selectedSvg ) {
const grid = document . getElementById ( 'svc-icon-grid' ) ;
grid . innerHTML = SERVICE _ICONS . map ( ic => {
const isSel = selectedSvg && selectedSvg . trim ( ) === ic . svg . trim ( ) ;
return ` <div class="icon-opt ${ isSel ? ' selected' : '' } " title=" ${ ic . label } " onclick="selectSvcIcon(this,' ${ ic . key } ')"> ${ ic . svg } <span> ${ ic . label } </span></div> ` ;
} ) . join ( '' ) ;
// Set preview to the matched icon, or first as default
const toSelect = selectedSvg ? ( SERVICE _ICONS . find ( ic => ic . svg . trim ( ) === selectedSvg . trim ( ) ) || SERVICE _ICONS [ 0 ] ) : SERVICE _ICONS [ 0 ] ;
document . getElementById ( 'svc-icon' ) . value = toSelect . svg ;
document . getElementById ( 'svc-icon-box' ) . innerHTML = toSelect . svg ;
document . getElementById ( 'svc-icon-label' ) . textContent = toSelect . label + ' ▾' ;
if ( ! selectedSvg ) {
const firstEl = grid . querySelector ( '.icon-opt' ) ;
if ( firstEl ) firstEl . classList . add ( 'selected' ) ;
}
}
function selectSvcIcon ( el , key ) {
document . querySelectorAll ( '#svc-icon-grid .icon-opt' ) . forEach ( e => e . classList . remove ( 'selected' ) ) ;
el . classList . add ( 'selected' ) ;
const ic = SERVICE _ICONS . find ( x => x . key === key ) ;
document . getElementById ( 'svc-icon' ) . value = ic . svg ;
document . getElementById ( 'svc-icon-box' ) . innerHTML = ic . svg ;
document . getElementById ( 'svc-icon-label' ) . textContent = ic . label + ' ▾' ;
// close grid
document . getElementById ( 'svc-icon-grid-wrap' ) . classList . remove ( 'open' ) ;
}
function toggleIconGrid ( ) {
document . getElementById ( 'svc-icon-grid-wrap' ) . classList . toggle ( 'open' ) ;
}
// ── Services ──────────────────────────────────────────────────────────────────
let svcs = [ ] ;
async function loadServices ( ) {
svcs = await api ( '/api/services/all' ) || [ ] ;
document . getElementById ( 'servicesTable' ) . innerHTML = svcs . map ( s => `
<tr>
<td> ${ s . order } </td>
<td><strong> ${ s . title } </strong></td>
<td style="max-width:300px;color:var(--mid)"> ${ s . description . substring ( 0 , 60 ) } ...</td>
<td><span class="badge ${ s . isActive ? 'badge-green' : 'badge-red' } "> ${ s . isActive ? 'فعال' : 'غیرفعال' } </span></td>
<td><button class="btn btn-secondary btn-sm" onclick="editSvc( ${ s . id } )">ویرایش</button> <button class="btn btn-danger btn-sm" onclick="deleteSvc( ${ s . id } )">حذف</button></td>
</tr> ` ) . join ( '' ) ;
}
function openSvcModal ( id ) {
document . getElementById ( 'svcModalTitle' ) . textContent = 'افزودن خدمت' ;
document . getElementById ( 'svc-id' ) . value = '' ;
document . getElementById ( 'svc-title' ) . value = '' ;
document . getElementById ( 'svc-desc' ) . value = '' ;
document . getElementById ( 'svc-order' ) . value = svcs . length + 1 ;
document . getElementById ( 'svc-active' ) . value = 'true' ;
document . getElementById ( 'svc-before' ) . value = '' ;
document . getElementById ( 'svc-after' ) . value = '' ;
showPreview ( 'prev-svc-before' , '' ) ;
showPreview ( 'prev-svc-after' , '' ) ;
document . getElementById ( 'svc-icon-grid-wrap' ) . classList . remove ( 'open' ) ;
renderIconPicker ( null ) ;
document . getElementById ( 'svcModal' ) . classList . remove ( 'hidden' ) ;
}
function editSvc ( id ) {
const s = svcs . find ( x => x . id === id ) ;
document . getElementById ( 'svcModalTitle' ) . textContent = 'ویرایش خدمت' ;
document . getElementById ( 'svc-id' ) . value = s . id ;
document . getElementById ( 'svc-title' ) . value = s . title ;
document . getElementById ( 'svc-desc' ) . value = s . description ;
document . getElementById ( 'svc-order' ) . value = s . order ;
document . getElementById ( 'svc-active' ) . value = String ( s . isActive ) ;
document . getElementById ( 'svc-before' ) . value = s . beforeImageUrl || '' ;
document . getElementById ( 'svc-after' ) . value = s . afterImageUrl || '' ;
document . getElementById ( 'svc-icon-grid-wrap' ) . classList . remove ( 'open' ) ;
renderIconPicker ( s . iconSvg || null ) ;
showPreview ( 'prev-svc-before' , s . beforeImageUrl || '' ) ;
showPreview ( 'prev-svc-after' , s . afterImageUrl || '' ) ;
document . getElementById ( 'svcModal' ) . classList . remove ( 'hidden' ) ;
}
async function saveSvc ( ) {
const id = document . getElementById ( 'svc-id' ) . value ;
const body = {
title : document . getElementById ( 'svc-title' ) . value ,
description : document . getElementById ( 'svc-desc' ) . value ,
order : parseInt ( document . getElementById ( 'svc-order' ) . value ) ,
isActive : document . getElementById ( 'svc-active' ) . value === 'true' ,
iconSvg : document . getElementById ( 'svc-icon' ) . value ,
beforeImageUrl : document . getElementById ( 'svc-before' ) . value ,
afterImageUrl : document . getElementById ( 'svc-after' ) . value
} ;
if ( id ) await api ( ` /api/services/ ${ id } ` , { method : 'PUT' , body : JSON . stringify ( body ) } ) ;
else await api ( '/api/services' , { method : 'POST' , body : JSON . stringify ( body ) } ) ;
closeModal ( 'svcModal' ) ;
toast ( 'خدمت ذخیره شد ✓' ) ;
loadServices ( ) ;
}
async function deleteSvc ( id ) {
if ( ! confirm ( 'حذف شود؟' ) ) return ;
await api ( ` /api/services/ ${ id } ` , { method : 'DELETE' } ) ;
toast ( 'حذف شد' , 'error' ) ;
loadServices ( ) ;
}
// ── Image Upload Helper ───────────────────────────────────────────────────────
2026-06-02 11:01:12 +03:30
// ── Image Cropper ─────────────────────────────────────────────────────────────
const cropper = {
inputId : null , previewId : null ,
img : null , naturalW : 0 , naturalH : 0 ,
scale : 1 , offsetX : 0 , offsetY : 0 ,
ratio : 1 , // 0 = free
box : { x : 0 , y : 0 , w : 0 , h : 0 } , // in canvas-display px
drag : null // {type, startX,startY,box0}
} ;
function openCropper ( inputId , previewId , file ) {
cropper . inputId = inputId ; cropper . previewId = previewId ;
const reader = new FileReader ( ) ;
reader . onload = e => {
const img = new Image ( ) ;
img . onload = ( ) => {
cropper . img = img ;
cropper . naturalW = img . naturalWidth ;
cropper . naturalH = img . naturalHeight ;
document . getElementById ( 'cropperModal' ) . classList . remove ( 'hidden' ) ;
requestAnimationFrame ( ( ) => initCropperCanvas ( ) ) ;
} ;
img . src = e . target . result ;
} ;
reader . readAsDataURL ( file ) ;
}
function initCropperCanvas ( ) {
const stage = document . getElementById ( 'cropperStage' ) ;
const canvas = document . getElementById ( 'cropperCanvas' ) ;
const sw = stage . clientWidth , sh = stage . clientHeight || 400 ;
canvas . width = sw ; canvas . height = sh ;
// fit image
const scaleX = sw / cropper . naturalW , scaleY = sh / cropper . naturalH ;
cropper . scale = Math . min ( scaleX , scaleY ) ;
const dw = cropper . naturalW * cropper . scale ;
const dh = cropper . naturalH * cropper . scale ;
cropper . offsetX = ( sw - dw ) / 2 ;
cropper . offsetY = ( sh - dh ) / 2 ;
// default crop = center square (or full if free)
const bSize = Math . min ( dw , dh ) * 0.8 ;
cropper . box = { x : cropper . offsetX + ( dw - bSize ) / 2 , y : cropper . offsetY + ( dh - bSize ) / 2 , w : bSize , h : bSize } ;
if ( cropper . ratio !== 1 ) applyCropRatioToBox ( ) ;
drawCropperCanvas ( ) ;
positionCropBox ( ) ;
document . getElementById ( 'cropBox' ) . classList . remove ( 'hidden' ) ;
bindCropperEvents ( ) ;
}
function drawCropperCanvas ( ) {
const canvas = document . getElementById ( 'cropperCanvas' ) ;
const ctx = canvas . getContext ( '2d' ) ;
ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
ctx . drawImage ( cropper . img , cropper . offsetX , cropper . offsetY ,
cropper . naturalW * cropper . scale , cropper . naturalH * cropper . scale ) ;
}
function positionCropBox ( ) {
const box = document . getElementById ( 'cropBox' ) ;
const b = cropper . box ;
box . style . cssText = ` left: ${ b . x } px;top: ${ b . y } px;width: ${ b . w } px;height: ${ b . h } px;position:absolute; ` ;
}
function clampBox ( b ) {
const ox = cropper . offsetX , oy = cropper . offsetY ;
const mw = cropper . naturalW * cropper . scale , mh = cropper . naturalH * cropper . scale ;
b . w = Math . max ( 40 , Math . min ( b . w , mw ) ) ; b . h = Math . max ( 40 , Math . min ( b . h , mh ) ) ;
b . x = Math . max ( ox , Math . min ( b . x , ox + mw - b . w ) ) ;
b . y = Math . max ( oy , Math . min ( b . y , oy + mh - b . h ) ) ;
return b ;
}
function applyCropRatioToBox ( ) {
if ( ! cropper . ratio ) return ;
const b = cropper . box ;
b . h = b . w / cropper . ratio ;
cropper . box = clampBox ( b ) ;
}
function setCropRatio ( w , h ) {
cropper . ratio = ( w && h ) ? w / h : 0 ;
document . querySelectorAll ( '.cropper-ratio-btn' ) . forEach ( btn => btn . classList . remove ( 'active' ) ) ;
event . target . classList . add ( 'active' ) ;
if ( cropper . img ) { applyCropRatioToBox ( ) ; positionCropBox ( ) ; }
}
function bindCropperEvents ( ) {
const box = document . getElementById ( 'cropBox' ) ;
const stage = document . getElementById ( 'cropperStage' ) ;
// Handles resize
box . querySelectorAll ( '.cropper-handle' ) . forEach ( h => {
h . addEventListener ( 'mousedown' , ev => {
ev . preventDefault ( ) ; ev . stopPropagation ( ) ;
cropper . drag = { type : 'resize' , handle : h . dataset . h , startX : ev . clientX , startY : ev . clientY , box0 : { ... cropper . box } } ;
} ) ;
h . addEventListener ( 'touchstart' , ev => {
ev . preventDefault ( ) ; ev . stopPropagation ( ) ;
const t = ev . touches [ 0 ] ;
cropper . drag = { type : 'resize' , handle : h . dataset . h , startX : t . clientX , startY : t . clientY , box0 : { ... cropper . box } } ;
} ) ;
} ) ;
// Box drag move
box . addEventListener ( 'mousedown' , ev => {
if ( ev . target . classList . contains ( 'cropper-handle' ) ) return ;
ev . preventDefault ( ) ;
cropper . drag = { type : 'move' , startX : ev . clientX , startY : ev . clientY , box0 : { ... cropper . box } } ;
} ) ;
box . addEventListener ( 'touchstart' , ev => {
if ( ev . target . classList . contains ( 'cropper-handle' ) ) return ;
ev . preventDefault ( ) ;
const t = ev . touches [ 0 ] ;
cropper . drag = { type : 'move' , startX : t . clientX , startY : t . clientY , box0 : { ... cropper . box } } ;
} ) ;
const onMove = ( cx , cy ) => {
if ( ! cropper . drag ) return ;
const dx = cx - cropper . drag . startX , dy = cy - cropper . drag . startY ;
const b0 = cropper . drag . box0 , r = cropper . ratio ;
let nb = { ... b0 } ;
if ( cropper . drag . type === 'move' ) {
nb . x = b0 . x + dx ; nb . y = b0 . y + dy ;
} else {
const h = cropper . drag . handle ;
if ( h === 'se' ) { nb . w = Math . max ( 40 , b0 . w + dx ) ; nb . h = r ? nb . w / r : Math . max ( 40 , b0 . h + dy ) ; }
else if ( h === 'sw' ) { nb . w = Math . max ( 40 , b0 . w - dx ) ; nb . x = b0 . x + b0 . w - nb . w ; nb . h = r ? nb . w / r : Math . max ( 40 , b0 . h + dy ) ; }
else if ( h === 'ne' ) { nb . w = Math . max ( 40 , b0 . w + dx ) ; nb . h = r ? nb . w / r : Math . max ( 40 , b0 . h - dy ) ; nb . y = b0 . y + b0 . h - nb . h ; }
else if ( h === 'nw' ) { nb . w = Math . max ( 40 , b0 . w - dx ) ; nb . x = b0 . x + b0 . w - nb . w ; nb . h = r ? nb . w / r : Math . max ( 40 , b0 . h - dy ) ; nb . y = b0 . y + b0 . h - nb . h ; }
}
cropper . box = clampBox ( nb ) ; positionCropBox ( ) ;
} ;
window . addEventListener ( 'mousemove' , ev => onMove ( ev . clientX , ev . clientY ) ) ;
window . addEventListener ( 'touchmove' , ev => { const t = ev . touches [ 0 ] ; onMove ( t . clientX , t . clientY ) ; } , { passive : true } ) ;
window . addEventListener ( 'mouseup' , ( ) => cropper . drag = null ) ;
window . addEventListener ( 'touchend' , ( ) => cropper . drag = null ) ;
}
function closeCropper ( ) {
document . getElementById ( 'cropperModal' ) . classList . add ( 'hidden' ) ;
document . getElementById ( 'cropBox' ) . classList . add ( 'hidden' ) ;
cropper . drag = null ;
}
async function applyCrop ( ) {
const b = cropper . box ;
// Convert display px → natural image px
const sx = ( b . x - cropper . offsetX ) / cropper . scale ;
const sy = ( b . y - cropper . offsetY ) / cropper . scale ;
const sw = b . w / cropper . scale ;
const sh = b . h / cropper . scale ;
const out = document . createElement ( 'canvas' ) ;
out . width = Math . round ( sw ) ; out . height = Math . round ( sh ) ;
out . getContext ( '2d' ) . drawImage ( cropper . img , sx , sy , sw , sh , 0 , 0 , out . width , out . height ) ;
2026-06-02 15:09:46 +03:30
// Capture targets BEFORE closing (closeCropper doesn't clear them, but be safe)
const _inputId = cropper . inputId , _previewId = cropper . previewId ;
2026-06-02 11:01:12 +03:30
out . toBlob ( async blob => {
closeCropper ( ) ;
2026-06-02 15:09:46 +03:30
const inputId = _inputId , previewId = _previewId ;
2026-05-31 00:42:08 +03:30
const btn = document . getElementById ( ` upload-btn- ${ inputId } ` ) ;
const orig = btn . innerHTML ;
btn . innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="animation:spin .8s linear infinite"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg> در حال آپلود...' ;
btn . disabled = true ;
try {
const fd = new FormData ( ) ;
2026-06-02 11:01:12 +03:30
fd . append ( 'file' , new File ( [ blob ] , 'crop.jpg' , { type : 'image/jpeg' } ) ) ;
const r = await fetch ( '/api/upload' , { method : 'POST' , headers : { 'Authorization' : ` Bearer ${ token } ` } , body : fd } ) ;
2026-05-31 00:42:08 +03:30
if ( ! r . ok ) throw new Error ( await r . text ( ) ) ;
const { url } = await r . json ( ) ;
document . getElementById ( inputId ) . value = url ;
showPreview ( previewId , url ) ;
toast ( 'تصویر با موفقیت آپلود شد ✓' ) ;
2026-06-02 11:01:12 +03:30
} catch ( e ) { toast ( 'خطا در آپلود: ' + e . message , 'error' ) ; }
finally { btn . innerHTML = orig ; btn . disabled = false ; }
} , 'image/jpeg' , 0.92 ) ;
}
async function uploadImage ( inputId , previewId ) {
const fileInput = document . createElement ( 'input' ) ;
fileInput . type = 'file' ;
fileInput . accept = 'image/jpeg,image/png,image/webp,image/gif' ;
fileInput . onchange = ( ) => {
const file = fileInput . files [ 0 ] ;
if ( ! file ) return ;
openCropper ( inputId , previewId , file ) ;
2026-05-31 00:42:08 +03:30
} ;
fileInput . click ( ) ;
}
function showPreview ( previewId , url ) {
const img = document . getElementById ( previewId ) ;
if ( ! img ) return ;
if ( url ) { img . src = url ; img . style . display = 'block' ; }
else { img . src = '' ; img . style . display = 'none' ; }
const rm = document . getElementById ( previewId . replace ( 'prev-' , 'rm-' ) ) ;
if ( rm ) rm . style . display = url ? 'inline-flex' : 'none' ;
}
function syncPreview ( inputId , previewId ) {
const val = ( document . getElementById ( inputId ) . value || '' ) . trim ( ) ;
showPreview ( previewId , ( val . startsWith ( 'http' ) || val . startsWith ( '/' ) ) ? val : '' ) ;
}
function removeImage ( inputId , previewId ) {
document . getElementById ( inputId ) . value = '' ;
showPreview ( previewId , '' ) ;
}
function clearGalleryPreviews ( ) {
[ 'prev-gal-img' , 'prev-gal-before' , 'prev-gal-after' ] . forEach ( id => showPreview ( id , '' ) ) ;
}
// ── Gallery ───────────────────────────────────────────────────────────────────
let gals = [ ] ;
async function loadGallery ( ) {
gals = await api ( '/api/gallery/all' ) || [ ] ;
document . getElementById ( 'galleryTable' ) . innerHTML = gals . map ( g => `
<tr>
<td><span class="badge badge-gold"> ${ g . category || '—' } </span></td>
<td> ${ g . caption || '—' } </td>
<td><span class="badge ${ g . isActive ? 'badge-green' : 'badge-red' } "> ${ g . isActive ? 'فعال' : 'غیرفعال' } </span></td>
<td><button class="btn btn-secondary btn-sm" onclick="editGallery( ${ g . id } )">ویرایش</button> <button class="btn btn-danger btn-sm" onclick="deleteGallery( ${ g . id } )">حذف</button></td>
</tr> ` ) . join ( '' ) ;
}
function openGalleryModal ( ) {
[ 'id' , 'img' , 'before' , 'after' , 'cat' , 'caption' ] . forEach ( k => document . getElementById ( ` gal- ${ k } ` ) . value = '' ) ;
document . getElementById ( 'gal-order' ) . value = gals . length + 1 ;
document . getElementById ( 'gal-active' ) . value = 'true' ;
clearGalleryPreviews ( ) ;
document . getElementById ( 'galleryModalTitle' ) . textContent = 'افزودن تصویر' ;
document . getElementById ( 'galleryModal' ) . classList . remove ( 'hidden' ) ;
}
function editGallery ( id ) {
const g = gals . find ( x => x . id === id ) ;
document . getElementById ( 'gal-id' ) . value = g . id ;
document . getElementById ( 'gal-img' ) . value = g . imageUrl || '' ;
document . getElementById ( 'gal-before' ) . value = g . beforeImageUrl || '' ;
document . getElementById ( 'gal-after' ) . value = g . afterImageUrl || '' ;
document . getElementById ( 'gal-cat' ) . value = g . category || '' ;
document . getElementById ( 'gal-caption' ) . value = g . caption || '' ;
document . getElementById ( 'gal-order' ) . value = g . order ;
clearGalleryPreviews ( ) ;
if ( g . imageUrl ) showPreview ( 'prev-gal-img' , g . imageUrl ) ;
if ( g . beforeImageUrl ) showPreview ( 'prev-gal-before' , g . beforeImageUrl ) ;
if ( g . afterImageUrl ) showPreview ( 'prev-gal-after' , g . afterImageUrl ) ;
document . getElementById ( 'galleryModalTitle' ) . textContent = 'ویرایش تصویر' ;
document . getElementById ( 'gal-active' ) . value = String ( g . isActive ) ;
document . getElementById ( 'galleryModal' ) . classList . remove ( 'hidden' ) ;
}
async function saveGallery ( ) {
const id = document . getElementById ( 'gal-id' ) . value ;
const body = { imageUrl : document . getElementById ( 'gal-img' ) . value , beforeImageUrl : document . getElementById ( 'gal-before' ) . value , afterImageUrl : document . getElementById ( 'gal-after' ) . value , category : document . getElementById ( 'gal-cat' ) . value , caption : document . getElementById ( 'gal-caption' ) . value , order : parseInt ( document . getElementById ( 'gal-order' ) . value ) , isActive : document . getElementById ( 'gal-active' ) . value === 'true' } ;
if ( id ) await api ( ` /api/gallery/ ${ id } ` , { method : 'PUT' , body : JSON . stringify ( body ) } ) ;
else await api ( '/api/gallery' , { method : 'POST' , body : JSON . stringify ( body ) } ) ;
closeModal ( 'galleryModal' ) ; toast ( 'ذخیره شد ✓' ) ; loadGallery ( ) ;
}
async function deleteGallery ( id ) { if ( ! confirm ( 'حذف؟' ) ) return ; await api ( ` /api/gallery/ ${ id } ` , { method : 'DELETE' } ) ; toast ( 'حذف شد' , 'error' ) ; loadGallery ( ) ; }
// ── Testimonials ──────────────────────────────────────────────────────────────
let testims = [ ] ;
async function loadTestimonials ( ) {
testims = await api ( '/api/testimonials/all' ) || [ ] ;
document . getElementById ( 'testimonialsTable' ) . innerHTML = testims . map ( t => `
<tr>
<td> ${ t . authorEmoji } ${ t . authorName } </td>
<td style="max-width:250px"> ${ t . text . substring ( 0 , 60 ) } ...</td>
<td> ${ '★' . repeat ( t . rating ) } </td>
<td><span class="badge ${ t . isActive ? 'badge-green' : 'badge-red' } "> ${ t . isActive ? 'فعال' : 'غیرفعال' } </span></td>
<td><button class="btn btn-secondary btn-sm" onclick="editTestim( ${ t . id } )">ویرایش</button> <button class="btn btn-danger btn-sm" onclick="deleteTestim( ${ t . id } )">حذف</button></td>
</tr> ` ) . join ( '' ) ;
}
function openTestimModal ( ) { [ 'id' , 'name' , 'emoji' , 'text' , 'date' ] . forEach ( k => document . getElementById ( ` testim- ${ k } ` ) . value = '' ) ; document . getElementById ( 'testim-emoji' ) . value = '👩' ; document . getElementById ( 'testim-rating' ) . value = 5 ; document . getElementById ( 'testim-active' ) . value = 'true' ; document . getElementById ( 'testimModal' ) . classList . remove ( 'hidden' ) ; }
function editTestim ( id ) { const t = testims . find ( x => x . id === id ) ; document . getElementById ( 'testim-id' ) . value = t . id ; document . getElementById ( 'testim-name' ) . value = t . authorName ; document . getElementById ( 'testim-emoji' ) . value = t . authorEmoji ; document . getElementById ( 'testim-text' ) . value = t . text ; document . getElementById ( 'testim-rating' ) . value = t . rating ; document . getElementById ( 'testim-date' ) . value = t . date ; document . getElementById ( 'testim-active' ) . value = String ( t . isActive ) ; document . getElementById ( 'testimModal' ) . classList . remove ( 'hidden' ) ; }
async function saveTestim ( ) { const id = document . getElementById ( 'testim-id' ) . value ; const body = { authorName : document . getElementById ( 'testim-name' ) . value , authorEmoji : document . getElementById ( 'testim-emoji' ) . value , text : document . getElementById ( 'testim-text' ) . value , rating : parseInt ( document . getElementById ( 'testim-rating' ) . value ) , date : document . getElementById ( 'testim-date' ) . value , isActive : document . getElementById ( 'testim-active' ) . value === 'true' } ; if ( id ) await api ( ` /api/testimonials/ ${ id } ` , { method : 'PUT' , body : JSON . stringify ( body ) } ) ; else await api ( '/api/testimonials' , { method : 'POST' , body : JSON . stringify ( body ) } ) ; closeModal ( 'testimModal' ) ; toast ( 'ذخیره شد ✓' ) ; loadTestimonials ( ) ; }
async function deleteTestim ( id ) { if ( ! confirm ( 'حذف؟' ) ) return ; await api ( ` /api/testimonials/ ${ id } ` , { method : 'DELETE' } ) ; toast ( 'حذف شد' , 'error' ) ; loadTestimonials ( ) ; }
// ── Blog Categories ───────────────────────────────────────────────────────────
let cats = [ ] ;
async function loadCategories ( ) {
cats = await api ( '/api/blog/categories' ) || [ ] ;
document . getElementById ( 'categoriesTable' ) . innerHTML = cats . map ( c => ` <tr><td><strong> ${ c . name } </strong></td><td dir="ltr" style="color:var(--light)"> ${ c . slug } </td><td><span class="badge badge-gold"> ${ c . postCount } </span></td><td><button class="btn btn-secondary btn-sm" onclick="editCat( ${ c . id } )">ویرایش</button> <button class="btn btn-danger btn-sm" onclick="deleteCat( ${ c . id } )">حذف</button></td></tr> ` ) . join ( '' ) ;
}
function openCatModal ( ) { document . getElementById ( 'catModalTitle' ) . textContent = 'دسته جدید' ; [ 'id' , 'name' , 'slug' , 'desc' ] . forEach ( k => document . getElementById ( ` cat- ${ k } ` ) . value = '' ) ; document . getElementById ( 'catModal' ) . classList . remove ( 'hidden' ) ; }
function editCat ( id ) { const c = cats . find ( x => x . id === id ) ; document . getElementById ( 'catModalTitle' ) . textContent = 'ویرایش دسته' ; document . getElementById ( 'cat-id' ) . value = c . id ; document . getElementById ( 'cat-name' ) . value = c . name ; document . getElementById ( 'cat-slug' ) . value = c . slug ; document . getElementById ( 'cat-desc' ) . value = c . description ; document . getElementById ( 'catModal' ) . classList . remove ( 'hidden' ) ; }
async function saveCat ( ) { const id = document . getElementById ( 'cat-id' ) . value ; const body = { name : document . getElementById ( 'cat-name' ) . value , slug : document . getElementById ( 'cat-slug' ) . value , description : document . getElementById ( 'cat-desc' ) . value } ; if ( id ) await api ( ` /api/blog/categories/ ${ id } ` , { method : 'PUT' , body : JSON . stringify ( body ) } ) ; else await api ( '/api/blog/categories' , { method : 'POST' , body : JSON . stringify ( body ) } ) ; closeModal ( 'catModal' ) ; toast ( 'ذخیره شد ✓' ) ; loadCategories ( ) ; }
async function deleteCat ( id ) { if ( ! confirm ( 'حذف؟' ) ) return ; await api ( ` /api/blog/categories/ ${ id } ` , { method : 'DELETE' } ) ; toast ( 'حذف شد' , 'error' ) ; loadCategories ( ) ; }
// ── Blog Posts ────────────────────────────────────────────────────────────────
let posts = [ ] ;
async function loadPosts ( ) {
posts = await api ( '/api/blog/posts/admin' ) || [ ] ;
document . getElementById ( 'postsTable' ) . innerHTML = posts . map ( p => `
<tr>
<td><strong> ${ p . title } </strong></td>
<td><span class="badge badge-gold"> ${ p . category ? . name || '—' } </span></td>
<td style="color:var(--gold);font-size:.8rem"> ${ p . focusKeyword || '—' } </td>
<td> ${ p . viewCount } </td>
<td><span class="badge ${ p . isPublished ? 'badge-green' : 'badge-red' } "> ${ p . isPublished ? 'منتشر' : 'پیشنویس' } </span></td>
<td>
<button class="btn btn-secondary btn-sm" onclick="editPost( ${ p . id } )">ویرایش</button>
<a href="/blog/ ${ p . slug } " target="_blank" class="btn btn-secondary btn-sm">مشاهده</a>
<button class="btn btn-danger btn-sm" onclick="deletePost( ${ p . id } )">حذف</button>
</td>
</tr> ` ) . join ( '' ) ;
}
async function openPostEditor ( ) {
if ( ! cats . length ) await loadCategories ( ) ;
const catSel = document . getElementById ( 'post-category' ) ;
catSel . innerHTML = '<option value="">انتخاب دسته...</option>' + cats . map ( c => ` <option value=" ${ c . id } "> ${ c . name } </option> ` ) . join ( '' ) ;
document . getElementById ( 'postModalTitle' ) . textContent = 'مقاله جدید' ;
[ 'id' , 'title' , 'slug' , 'focus' , 'metatitle' , 'metadesc' , 'keywords' , 'image' ] . forEach ( k => document . getElementById ( ` post- ${ k } ` ) . value = '' ) ;
document . getElementById ( 'post-content' ) . innerHTML = '' ;
showPreview ( 'prev-post-image' , '' ) ;
document . getElementById ( 'post-excerpt' ) . value = '' ;
document . getElementById ( 'post-published' ) . value = 'false' ;
document . getElementById ( 'post-schematype' ) . value = 'MedicalWebPage' ;
document . getElementById ( 'post-category' ) . value = '' ;
updateLen ( 'post-metatitle' , 'mtLen' , 70 ) ;
updateLen ( 'post-metadesc' , 'mdLen' , 160 ) ;
document . getElementById ( 'postModal' ) . classList . remove ( 'hidden' ) ;
}
async function editPost ( id ) {
await openPostEditor ( ) ;
const p = await api ( ` /api/blog/posts/id/ ${ id } ` ) ;
if ( ! p ) return ;
document . getElementById ( 'postModalTitle' ) . textContent = 'ویرایش مقاله' ;
document . getElementById ( 'post-id' ) . value = p . id ;
document . getElementById ( 'post-title' ) . value = p . title ;
document . getElementById ( 'post-slug' ) . value = p . slug ;
document . getElementById ( 'post-excerpt' ) . value = p . excerpt ;
document . getElementById ( 'post-content' ) . innerHTML = p . content ;
document . getElementById ( 'post-focus' ) . value = p . focusKeyword ;
document . getElementById ( 'post-metatitle' ) . value = p . metaTitle ;
document . getElementById ( 'post-metadesc' ) . value = p . metaDescription ;
document . getElementById ( 'post-keywords' ) . value = p . keywords ;
document . getElementById ( 'post-image' ) . value = p . featuredImage || '' ;
showPreview ( 'prev-post-image' , p . featuredImage || '' ) ;
document . getElementById ( 'post-published' ) . value = String ( p . isPublished ) ;
document . getElementById ( 'post-schematype' ) . value = p . articleType ;
document . getElementById ( 'post-category' ) . value = p . categoryId || '' ;
updateLen ( 'post-metatitle' , 'mtLen' , 70 ) ;
updateLen ( 'post-metadesc' , 'mdLen' , 160 ) ;
checkSeo ( ) ;
}
async function savePost ( ) {
const id = document . getElementById ( 'post-id' ) . value ;
const catId = document . getElementById ( 'post-category' ) . value ;
const body = {
title : document . getElementById ( 'post-title' ) . value ,
slug : document . getElementById ( 'post-slug' ) . value ,
excerpt : document . getElementById ( 'post-excerpt' ) . value ,
content : document . getElementById ( 'post-content' ) . innerHTML ,
featuredImage : document . getElementById ( 'post-image' ) . value ,
author : 'دکتر سوسن آلطه' ,
metaTitle : document . getElementById ( 'post-metatitle' ) . value ,
metaDescription : document . getElementById ( 'post-metadesc' ) . value ,
focusKeyword : document . getElementById ( 'post-focus' ) . value ,
keywords : document . getElementById ( 'post-keywords' ) . value ,
articleType : document . getElementById ( 'post-schematype' ) . value ,
isPublished : document . getElementById ( 'post-published' ) . value === 'true' ,
categoryId : catId ? parseInt ( catId ) : null ,
ogImage : document . getElementById ( 'post-image' ) . value ,
} ;
if ( id ) await api ( ` /api/blog/posts/ ${ id } ` , { method : 'PUT' , body : JSON . stringify ( body ) } ) ;
else await api ( '/api/blog/posts' , { method : 'POST' , body : JSON . stringify ( body ) } ) ;
closeModal ( 'postModal' ) ; toast ( 'مقاله ذخیره شد ✓' ) ; loadPosts ( ) ;
}
async function deletePost ( id ) { if ( ! confirm ( 'حذف مقاله؟' ) ) return ; await api ( ` /api/blog/posts/ ${ id } ` , { method : 'DELETE' } ) ; toast ( 'مقاله حذف شد' , 'error' ) ; loadPosts ( ) ; }
// ── FAQs ──────────────────────────────────────────────────────────────────────
let faqs = [ ] ;
async function loadFaqs ( ) {
faqs = await api ( '/api/faqs/all' ) || [ ] ;
document . getElementById ( 'faqsTable' ) . innerHTML = faqs . map ( f => `
<tr>
<td> ${ f . order } </td>
<td style="max-width:360px"><strong> ${ esc ( f . question ) } </strong></td>
<td><span class="badge ${ f . isActive ? 'badge-green' : 'badge-red' } "> ${ f . isActive ? 'فعال' : 'غیرفعال' } </span></td>
<td><button class="btn btn-secondary btn-sm" onclick="editFaq( ${ f . id } )">ویرایش</button> <button class="btn btn-danger btn-sm" onclick="deleteFaq( ${ f . id } )">حذف</button></td>
</tr> ` ) . join ( '' ) ;
}
function openFaqModal ( ) {
document . getElementById ( 'faqModalTitle' ) . textContent = 'افزودن سوال' ;
document . getElementById ( 'faq-id' ) . value = '' ;
document . getElementById ( 'faq-question' ) . value = '' ;
document . getElementById ( 'faq-answer' ) . value = '' ;
document . getElementById ( 'faq-order' ) . value = faqs . length + 1 ;
document . getElementById ( 'faq-active' ) . value = 'true' ;
document . getElementById ( 'faqModal' ) . classList . remove ( 'hidden' ) ;
}
function editFaq ( id ) {
const f = faqs . find ( x => x . id === id ) ;
document . getElementById ( 'faqModalTitle' ) . textContent = 'ویرایش سوال' ;
document . getElementById ( 'faq-id' ) . value = f . id ;
document . getElementById ( 'faq-question' ) . value = f . question ;
document . getElementById ( 'faq-answer' ) . value = f . answer ;
document . getElementById ( 'faq-order' ) . value = f . order ;
document . getElementById ( 'faq-active' ) . value = String ( f . isActive ) ;
document . getElementById ( 'faqModal' ) . classList . remove ( 'hidden' ) ;
}
async function saveFaq ( ) {
const id = document . getElementById ( 'faq-id' ) . value ;
const body = { question : document . getElementById ( 'faq-question' ) . value , answer : document . getElementById ( 'faq-answer' ) . value , order : parseInt ( document . getElementById ( 'faq-order' ) . value ) , isActive : document . getElementById ( 'faq-active' ) . value === 'true' } ;
if ( id ) await api ( ` /api/faqs/ ${ id } ` , { method : 'PUT' , body : JSON . stringify ( body ) } ) ;
else await api ( '/api/faqs' , { method : 'POST' , body : JSON . stringify ( body ) } ) ;
closeModal ( 'faqModal' ) ; toast ( 'ذخیره شد ✓' ) ; loadFaqs ( ) ;
}
async function deleteFaq ( id ) { if ( ! confirm ( 'این سوال حذف شود؟' ) ) return ; await api ( ` /api/faqs/ ${ id } ` , { method : 'DELETE' } ) ; toast ( 'حذف شد' , 'error' ) ; loadFaqs ( ) ; }
// ── SEO ───────────────────────────────────────────────────────────────────────
async function loadSeo ( ) {
const s = await api ( '/api/seo/stats' ) ;
if ( ! s ) return ;
document . getElementById ( 'seo-posts' ) . textContent = s . total ;
document . getElementById ( 'seo-views' ) . textContent = s . views ;
document . getElementById ( 'seo-nometa' ) . textContent = s . noMeta ;
document . getElementById ( 'seoTopPosts' ) . innerHTML = s . topPosts . map ( p => ` <tr><td> ${ p . title } </td><td><span class="badge badge-gold"> ${ p . viewCount } </span></td></tr> ` ) . join ( '' ) ;
}
// ── SEO helpers ───────────────────────────────────────────────────────────────
function autoSlug ( ) {
const t = document . getElementById ( 'post-title' ) . value ;
const s = t . trim ( ) . replace ( /\s+/g , '-' ) . replace ( /[^-ۿa-z0-9\-]/g , '' ) . toLowerCase ( ) ;
document . getElementById ( 'post-slug' ) . value = s ;
checkSeo ( ) ;
}
function updateLen ( inputId , labelId , max ) {
const el = document . getElementById ( inputId ) ;
const len = el . value . length ;
const label = document . getElementById ( labelId ) ;
label . textContent = ` ( ${ len } / ${ max } ) ` ;
label . style . color = len > max ? 'var(--danger)' : ( len > max * . 8 ? 'var(--gold)' : 'var(--light)' ) ;
}
function checkSeo ( ) {
const kw = document . getElementById ( 'post-focus' ) . value . trim ( ) ;
const title = document . getElementById ( 'post-title' ) . value ;
const mt = document . getElementById ( 'post-metatitle' ) . value ;
const md = document . getElementById ( 'post-metadesc' ) . value ;
const fb = document . getElementById ( 'seoFeedback' ) ;
if ( ! kw ) { fb . innerHTML = '' ; return ; }
const checks = [
{ ok : title . includes ( kw ) , msg : 'کلیدواژه در عنوان' } ,
{ ok : mt . includes ( kw ) , msg : 'کلیدواژه در Meta Title' } ,
{ ok : md . includes ( kw ) , msg : 'کلیدواژه در Meta Description' } ,
{ ok : mt . length <= 70 && mt . length > 0 , msg : 'طول Meta Title مناسب' } ,
{ ok : md . length <= 160 && md . length > 100 , msg : 'طول Meta Description مناسب' } ,
] ;
const pass = checks . filter ( c => c . ok ) . length ;
const cls = pass >= 4 ? 'good' : pass >= 2 ? 'ok' : 'bad' ;
const emoji = pass >= 4 ? '✅' : pass >= 2 ? '⚠️' : '❌' ;
fb . innerHTML = ` <div class="seo-score ${ cls } "> ${ emoji } امتیاز SEO: ${ pass } / ${ checks . length } — ${ checks . map ( c => ` <span style="opacity: ${ c . ok ? 1 : . 4 } "> ${ c . ok ? '✓' : '✗' } ${ c . msg } </span> ` ) . join ( ' | ' ) } </div> ` ;
}
// ── Editor helpers ────────────────────────────────────────────────────────────
function fmt ( cmd ) { document . getElementById ( 'post-content' ) . focus ( ) ; document . execCommand ( cmd ) ; }
function fmtBlock ( tag ) { document . getElementById ( 'post-content' ) . focus ( ) ; document . execCommand ( 'formatBlock' , false , tag ) ; }
function insLink ( ) { const url = prompt ( 'آدرس لینک:' ) ; if ( url ) { document . getElementById ( 'post-content' ) . focus ( ) ; document . execCommand ( 'createLink' , false , url ) ; } }
// ── Modal helpers ─────────────────────────────────────────────────────────────
function closeModal ( id ) { document . getElementById ( id ) . classList . add ( 'hidden' ) ; }
document . querySelectorAll ( '.modal-overlay' ) . forEach ( m => m . addEventListener ( 'click' , e => { if ( e . target === m ) m . classList . add ( 'hidden' ) ; } ) ) ;
// ── File Manager ──────────────────────────────────────────────────────────────
let _fmTarget = null ; // {inputId, previewId}
let _fmFiles = [ ] ;
function openFileMgr ( inputId , previewId ) {
_fmTarget = { inputId , previewId } ;
document . getElementById ( 'fileMgr' ) . classList . remove ( 'hidden' ) ;
document . getElementById ( 'fm-search' ) . value = '' ;
loadFmFiles ( ) ;
}
function closeFileMgr ( ) {
document . getElementById ( 'fileMgr' ) . classList . add ( 'hidden' ) ;
_fmTarget = null ;
}
async function loadFmFiles ( ) {
const grid = document . getElementById ( 'fm-grid' ) ;
grid . innerHTML = '<div class="fm-loading"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:32px;height:32px;animation:spin .8s linear infinite"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>در حال بارگذاری...</div>' ;
try {
const r = await fetch ( '/api/upload' , { headers : { Authorization : ` Bearer ${ token } ` } } ) ;
if ( ! r . ok ) throw new Error ( 'HTTP ' + r . status ) ;
_fmFiles = await r . json ( ) ;
renderFmGrid ( _fmFiles ) ;
} catch ( e ) {
grid . innerHTML = ` <div class="fm-empty" style="color:var(--danger)">⚠️ خطا در بارگذاری فایلها<br><small style="color:var(--mid)"> ${ e . message } </small><br><button class="btn btn-secondary btn-sm" style="margin-top:1rem" onclick="loadFmFiles()">تلاش مجدد</button></div> ` ;
}
}
function renderFmGrid ( files ) {
const cur = _fmTarget ? document . getElementById ( _fmTarget . inputId ) ? . value : '' ;
const grid = document . getElementById ( 'fm-grid' ) ;
if ( ! files . length ) {
grid . innerHTML = '<div class="fm-empty">هنوز هیچ فایلی آپلود نشده است<br><small>از دکمه «آپلود جدید» استفاده کنید</small></div>' ;
return ;
}
grid . innerHTML = ` <div class="fm-grid"> ${ files . map ( f => `
<div class="fm-tile ${ cur === f . url ? ' fm-selected' : '' } " data-url=" ${ f . url } " onclick="selectFmFile(' ${ f . url } ')">
<div class="fm-thumb" style="background-image:url(' ${ f . url } ')"></div>
<div class="fm-info">
<span class="fm-name"> ${ f . name } </span>
<span class="fm-meta"> ${ fmBytes ( f . size ) } </span>
</div>
<div class="fm-actions" onclick="event.stopPropagation()">
<button class="btn btn-primary btn-sm" onclick="selectFmFile(' ${ f . url } ')">انتخاب</button>
<button class="btn btn-danger btn-sm" onclick="deleteFmFile(' ${ f . name } ',this)">حذف</button>
</div>
</div> ` ) . join ( '' ) } </div> ` ;
}
function filterFmFiles ( ) {
const q = document . getElementById ( 'fm-search' ) . value . trim ( ) . toLowerCase ( ) ;
renderFmGrid ( q ? _fmFiles . filter ( f => f . name . toLowerCase ( ) . includes ( q ) ) : _fmFiles ) ;
}
function selectFmFile ( url ) {
if ( ! _fmTarget ) return ;
document . getElementById ( _fmTarget . inputId ) . value = url ;
showPreview ( _fmTarget . previewId , url ) ;
closeFileMgr ( ) ;
toast ( 'تصویر انتخاب شد ✓' ) ;
}
async function deleteFmFile ( name , btnEl ) {
if ( ! confirm ( ` فایل " ${ name } " برای همیشه حذف شود؟ ` ) ) return ;
try {
const r = await fetch ( ` /api/upload/ ${ encodeURIComponent ( name ) } ` , {
method : 'DELETE' , headers : { Authorization : ` Bearer ${ token } ` }
} ) ;
if ( ! r . ok ) throw new Error ( r . status ) ;
_fmFiles = _fmFiles . filter ( f => f . name !== name ) ;
btnEl . closest ( '.fm-tile' ) . remove ( ) ;
// clear the target input if it was pointing at the deleted file
if ( _fmTarget ) {
const el = document . getElementById ( _fmTarget . inputId ) ;
if ( el && el . value . endsWith ( '/' + name ) ) {
el . value = '' ;
showPreview ( _fmTarget . previewId , '' ) ;
}
}
if ( ! _fmFiles . length ) renderFmGrid ( [ ] ) ;
toast ( 'فایل حذف شد ✓' ) ;
} catch ( e ) { toast ( 'خطا در حذف فایل' , 'error' ) ; }
}
async function fmUploadNew ( ) {
const fi = document . createElement ( 'input' ) ;
fi . type = 'file' ; fi . accept = 'image/jpeg,image/png,image/webp,image/gif' ;
fi . onchange = async ( ) => {
const file = fi . files [ 0 ] ; if ( ! file ) return ;
const btn = document . getElementById ( 'fm-upload-btn' ) ;
btn . disabled = true ; btn . textContent = 'در حال آپلود...' ;
try {
const fd = new FormData ( ) ; fd . append ( 'file' , file ) ;
const r = await fetch ( '/api/upload' , { method : 'POST' , headers : { Authorization : ` Bearer ${ token } ` } , body : fd } ) ;
if ( ! r . ok ) throw new Error ( await r . text ( ) ) ;
await loadFmFiles ( ) ;
toast ( 'تصویر آپلود شد ✓' ) ;
} catch ( e ) { toast ( 'خطا در آپلود: ' + e . message , 'error' ) ; }
finally { btn . disabled = false ; btn . textContent = '+ آپلود جدید' ; }
} ;
fi . click ( ) ;
}
function fmBytes ( b ) {
if ( b < 1024 ) return b + ' B' ;
if ( b < 1048576 ) return ( b / 1024 ) . toFixed ( 1 ) + ' KB' ;
return ( b / 1048576 ) . toFixed ( 1 ) + ' MB' ;
}
// ── Auto-inject 📁 button next to every upload button ────────────────────────
document . addEventListener ( 'DOMContentLoaded' , ( ) => {
document . querySelectorAll ( '.upload-btn' ) . forEach ( btn => {
const m = btn . id . match ( /^upload-btn-(.+)$/ ) ;
if ( ! m ) return ;
const inputId = m [ 1 ] , previewId = ` prev- ${ inputId } ` ;
const fb = document . createElement ( 'button' ) ;
fb . type = 'button' ; fb . className = 'fm-open-btn' ; fb . title = 'انتخاب از فایلها' ;
fb . innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>' ;
fb . onclick = ( ) => openFileMgr ( inputId , previewId ) ;
btn . insertAdjacentElement ( 'afterend' , fb ) ;
} ) ;
} ) ;
// ── Init ──────────────────────────────────────────────────────────────────────
async function init ( ) {
if ( ! token ) { return ; }
document . getElementById ( 'loginScreen' ) . classList . add ( 'hidden' ) ;
loadDashboard ( ) ;
loadCategories ( ) ;
loadComments ( ) ; // populate pending badge on load
2026-06-02 12:27:16 +03:30
loadHealthRequests ( ) ; // populate health requests badge on load
2026-05-31 00:42:08 +03:30
}
// Auto-login if token exists
if ( token ) init ( ) ;
< / script >
2026-06-02 11:01:12 +03:30
<!-- ── Image Cropper Modal ──────────────────────────────────────────────────── -->
< div class = "cropper-overlay hidden" id = "cropperModal" >
< div class = "cropper-modal" >
< div class = "cropper-head" >
< span class = "cropper-head-title" > ✂️ برش تصویر< / span >
< div class = "cropper-ratio-btns" >
< button class = "cropper-ratio-btn active" onclick = "setCropRatio(1,1)" title = "مربع" > ۱ :۱ < / button >
< button class = "cropper-ratio-btn" onclick = "setCropRatio(4,3)" title = "افقی" > ۴:۳< / button >
< button class = "cropper-ratio-btn" onclick = "setCropRatio(16,9)" title = "پانورامیک" > ۱۶:۹< / button >
< button class = "cropper-ratio-btn" onclick = "setCropRatio(3,4)" title = "عمودی" > ۳:۴< / button >
< button class = "cropper-ratio-btn" onclick = "setCropRatio(0,0)" title = "آزاد" > آزاد< / button >
< / div >
< button class = "modal-close" style = "color:#ccc" onclick = "closeCropper()" > ✕< / button >
< / div >
< div class = "cropper-stage" id = "cropperStage" >
< canvas id = "cropperCanvas" > < / canvas >
< div class = "cropper-box hidden" id = "cropBox" >
< div class = "cropper-handle nw" data-h = "nw" > < / div >
< div class = "cropper-handle ne" data-h = "ne" > < / div >
< div class = "cropper-handle sw" data-h = "sw" > < / div >
< div class = "cropper-handle se" data-h = "se" > < / div >
< / div >
< / div >
< div class = "cropper-foot" >
< span class = "cropper-hint" > برای تنظیم ناحیه برش بکشید یا گوشهها را تغییر دهید< / span >
< button class = "btn btn-secondary btn-sm" onclick = "closeCropper()" > انصراف< / button >
< button class = "btn btn-primary btn-sm" onclick = "applyCrop()" > ✓ برش و آپلود< / button >
< / div >
< / div >
< / div >
2026-05-31 00:42:08 +03:30
<!-- ── File Manager Modal ─────────────────────────────────────────────────── -->
< div class = "fm-overlay hidden" id = "fileMgr" onclick = "if(event.target===this)closeFileMgr()" >
< div class = "fm-modal" >
< div class = "fm-head" >
< span class = "fm-head-title" > 📁 مدیریت فایلها< / span >
< input class = "fm-search" id = "fm-search" type = "text" placeholder = "جستجو..." oninput = "filterFmFiles()" / >
< button id = "fm-upload-btn" class = "btn btn-primary btn-sm" onclick = "fmUploadNew()" > + آپلود جدید< / button >
< button class = "modal-close" onclick = "closeFileMgr()" > ✕< / button >
< / div >
< div class = "fm-body" >
< div id = "fm-grid" > < / div >
< / div >
< / div >
< / div >
< / body >
< / html >