1e51df406b
1. applyCrop() — mime variable was declared INSIDE toBlob callback but used as an argument to toBlob() (outer scope) → ReferenceError. Fix: declare _mime, _quality, _ext BEFORE out.toBlob() call. 2. loadSiteIdentity() — crashed when identity section had no rows (data was null/non-array). Fix: safe Array.isArray guard + catch. 3. Header logo: show logo image + | + site name side by side when logo is configured (was showing one OR the other). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2325 lines
146 KiB
HTML
2325 lines
146 KiB
HTML
<!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:240px;
|
||
}
|
||
body{font-family:'Vazirmatn',Tahoma,sans-serif;background:var(--bg);color:var(--dark);direction:rtl;display:flex;min-height:100vh}
|
||
|
||
/* ── 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.5rem 1.2rem;border-bottom:1px solid rgba(255,255,255,.08);font-size:.9rem;font-weight:600;color:var(--gold-l);line-height:1.4}
|
||
.sidebar-logo span{display:block;font-size:.7rem;color:rgba(255,255,255,.4);font-weight:400;margin-top:.2rem}
|
||
nav.sidebar-nav{flex:1;overflow-y:auto;padding:.8rem 0}
|
||
.nav-section{font-size:.65rem;font-weight:600;letter-spacing:.08em;color:rgba(255,255,255,.3);padding:.8rem 1.2rem .3rem;text-transform:uppercase}
|
||
.nav-item{display:flex;align-items:center;gap:.7rem;padding:.65rem 1.2rem;color:rgba(255,255,255,.65);cursor:pointer;font-size:.85rem;transition:background .2s,color .2s;border-right:3px 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:16px;height:16px;flex-shrink:0;opacity:.7}
|
||
.nav-item.active svg{opacity:1}
|
||
.sidebar-footer{padding:1rem 1.2rem;border-top:1px solid rgba(255,255,255,.08);font-size:.75rem;color:rgba(255,255,255,.35)}
|
||
|
||
/* ── Main ── */
|
||
.main{margin-right:var(--sidebar);flex:1;display:flex;flex-direction:column;min-height:100vh}
|
||
.topbar{background:var(--white);border-bottom:1px solid var(--border);padding:.75rem 2rem;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:40}
|
||
.topbar-title{font-size:1rem;font-weight:600;color:var(--dark)}
|
||
.topbar-actions{display:flex;gap:.7rem;align-items:center}
|
||
.page-content{padding:2rem;flex:1}
|
||
|
||
/* ── Cards ── */
|
||
.stat-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:1.2rem;margin-bottom:2rem}
|
||
.stat-card{background:var(--white);border-radius:14px;padding:1.4rem 1.6rem;border:1px solid var(--border);display:flex;flex-direction:column;gap:.4rem}
|
||
.stat-card .label{font-size:.78rem;color:var(--light)}
|
||
.stat-card .value{font-size:2rem;font-weight:700;color:var(--dark)}
|
||
.stat-card .icon{width:38px;height:38px;border-radius:10px;background:var(--gold-pale);display:flex;align-items:center;justify-content:center;margin-bottom:.4rem}
|
||
.stat-card .icon svg{width:18px;height:18px;color:var(--gold)}
|
||
|
||
.card{background:var(--white);border-radius:14px;border:1px solid var(--border);overflow:hidden;margin-bottom:1.5rem}
|
||
.card-header{padding:1.2rem 1.5rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||
.card-title{font-size:.95rem;font-weight:600}
|
||
.card-body{padding:1.5rem}
|
||
|
||
/* ── Buttons ── */
|
||
.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.55rem 1.1rem;border-radius:8px;font-family:inherit;font-size:.85rem;font-weight:500;cursor:pointer;border:1.5px solid transparent;transition:all .2s}
|
||
.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:.35rem .7rem;font-size:.78rem}
|
||
.btn svg{width:15px;height:15px}
|
||
|
||
/* ── Table ── */
|
||
.table-wrap{overflow-x:auto}
|
||
table{width:100%;border-collapse:collapse;font-size:.85rem}
|
||
th{background:#F8FAFC;padding:.75rem 1rem;text-align:right;font-weight:600;color:var(--mid);font-size:.78rem;border-bottom:1px solid var(--border)}
|
||
td{padding:.75rem 1rem;border-bottom:1px 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:.2rem .65rem;border-radius:50px;font-size:.72rem;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:1rem}
|
||
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
|
||
.form-group{display:flex;flex-direction:column;gap:.35rem}
|
||
.form-group label{font-size:.82rem;font-weight:500;color:var(--dark)}
|
||
.form-group input,.form-group select,.form-group textarea{
|
||
border:1.5px solid var(--border);border-radius:8px;padding:.65rem .9rem;
|
||
font-family:inherit;font-size:.88rem;color:var(--dark);direction:rtl;
|
||
transition:border-color .2s,box-shadow .2s;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 3px rgba(184,149,90,.1)}
|
||
.form-group textarea{resize:vertical;min-height:100px}
|
||
.form-hint{font-size:.72rem;color:var(--light);margin-top:.2rem}
|
||
/* ── Upload input group ── */
|
||
.input-upload-wrap{display:flex;gap:.45rem;align-items:center}
|
||
.input-upload-wrap input{flex:1;min-width:0}
|
||
.upload-btn{flex-shrink:0;display:inline-flex;align-items:center;gap:.3rem;padding:.55rem .8rem;border-radius:8px;border:1.5px dashed var(--gold);background:var(--gold-pale);color:var(--gold);font-family:inherit;font-size:.78rem;font-weight:600;cursor:pointer;transition:all .2s;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:14px;height:14px}
|
||
.upload-remove{flex-shrink:0;display:inline-flex;align-items:center;gap:.3rem;padding:.55rem .8rem;border-radius:8px;border:1.5px solid var(--border);background:#fff;color:#c0392b;font-family:inherit;font-size:.78rem;font-weight:600;cursor:pointer;transition:all .2s}
|
||
.upload-remove:hover{background:#c0392b;color:#fff;border-color:#c0392b}
|
||
.upload-preview{display:none;width:100%;max-height:120px;object-fit:cover;border-radius:8px;border:1.5px solid var(--border);margin-top:.4rem;background:var(--section-bg)}
|
||
/* ── File Manager button ── */
|
||
.fm-open-btn{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:8px;border:1.5px solid var(--border);background:var(--white);color:var(--mid);cursor:pointer;transition:all .2s;padding:0}
|
||
.fm-open-btn:hover{border-color:var(--gold);color:var(--gold);background:var(--gold-pale)}
|
||
.fm-open-btn svg{width:15px;height:15px;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:1rem}
|
||
.fm-modal{background:var(--white);border-radius:18px;width:100%;max-width:880px;height:82vh;display:flex;flex-direction:column;box-shadow:0 24px 80px rgba(0,0,0,.22);overflow:hidden}
|
||
.fm-head{padding:1rem 1.4rem;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:.8rem;flex-shrink:0}
|
||
.fm-head-title{font-size:.95rem;font-weight:600;flex:1}
|
||
.fm-search{flex:1;max-width:240px;border:1.5px solid var(--border);border-radius:8px;padding:.45rem .8rem;font-family:inherit;font-size:.83rem;direction:rtl;outline:none;transition:border-color .2s}
|
||
.fm-search:focus{border-color:var(--gold)}
|
||
.fm-body{flex:1;overflow-y:auto;padding:1.2rem}
|
||
.fm-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:.9rem}
|
||
.fm-tile{border:2px solid var(--border);border-radius:12px;overflow:hidden;cursor:pointer;transition:border-color .2s,box-shadow .2s;background:var(--bg)}
|
||
.fm-tile:hover{border-color:var(--gold);box-shadow:0 4px 18px rgba(184,149,90,.18)}
|
||
.fm-tile.fm-selected{border-color:var(--gold);box-shadow:0 0 0 3px 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:.5rem .6rem .3rem;border-top:1px solid var(--border)}
|
||
.fm-name{display:block;font-size:.7rem;color:var(--dark);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:ltr;text-align:left}
|
||
.fm-meta{font-size:.65rem;color:var(--light)}
|
||
.fm-actions{display:flex;gap:.35rem;padding:.4rem .6rem .6rem}
|
||
.fm-actions .btn{flex:1;padding:.35rem .4rem;font-size:.72rem;justify-content:center}
|
||
.fm-empty{text-align:center;padding:4rem 2rem;color:var(--mid);font-size:.9rem}
|
||
.fm-loading{text-align:center;padding:4rem 2rem;color:var(--gold);display:flex;flex-direction:column;align-items:center;gap:.8rem;font-size:.85rem}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
.seo-score{display:flex;align-items:center;gap:.5rem;padding:.6rem 1rem;border-radius:8px;font-size:.8rem}
|
||
.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:1fr 1fr;gap:.8rem}
|
||
.ba-pair .upload-preview{max-height:140px}
|
||
/* ── 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:1rem}
|
||
.cropper-modal{background:#1a1a1a;border-radius:18px;width:100%;max-width:680px;display:flex;flex-direction:column;box-shadow:0 24px 80px rgba(0,0,0,.5);overflow:hidden;max-height:92vh}
|
||
.cropper-head{padding:.85rem 1.2rem;display:flex;align-items:center;gap:.7rem;border-bottom:1px solid #333}
|
||
.cropper-head-title{color:#fff;font-size:.95rem;font-weight:600;flex:1}
|
||
.cropper-stage{flex:1;overflow:hidden;position:relative;background:#111;cursor:crosshair;user-select:none;min-height:320px}
|
||
.cropper-stage canvas{display:block;width:100%;height:100%;object-fit:contain}
|
||
.cropper-box{position:absolute;border:2px solid #B8955A;box-shadow:0 0 0 9999px rgba(0,0,0,.52);cursor:move;box-sizing:border-box}
|
||
.cropper-handle{position:absolute;width:10px;height:10px;background:#B8955A;border-radius:50%;z-index:2}
|
||
.cropper-handle.nw{top:-5px;left:-5px;cursor:nw-resize}
|
||
.cropper-handle.ne{top:-5px;right:-5px;cursor:ne-resize}
|
||
.cropper-handle.sw{bottom:-5px;left:-5px;cursor:sw-resize}
|
||
.cropper-handle.se{bottom:-5px;right:-5px;cursor:se-resize}
|
||
.cropper-foot{padding:.85rem 1.2rem;display:flex;gap:.6rem;align-items:center;border-top:1px solid #333;flex-wrap:wrap}
|
||
.cropper-hint{color:#888;font-size:.78rem;flex:1}
|
||
.cropper-ratio-btns{display:flex;gap:.4rem}
|
||
.cropper-ratio-btn{background:#2a2a2a;border:1.5px solid #444;color:#ccc;padding:.35rem .7rem;border-radius:8px;font-size:.75rem;cursor:pointer;transition:all .2s;font-family:inherit}
|
||
.cropper-ratio-btn.active,.cropper-ratio-btn:hover{background:#B8955A;border-color:#B8955A;color:#fff}
|
||
/* ── Icon Picker ── */
|
||
.icon-picker-section{margin-top:.9rem}
|
||
.icon-picker-section>label{display:block;font-size:.82rem;font-weight:600;color:var(--mid);margin-bottom:.55rem}
|
||
.icon-selected-preview{display:flex;align-items:center;gap:.7rem;padding:.6rem .9rem;border:1.5px solid var(--border);border-radius:8px;background:var(--section-bg);margin-bottom:.65rem;cursor:pointer;transition:border-color .2s}
|
||
.icon-selected-preview:hover{border-color:var(--gold)}
|
||
.icon-preview-box{width:40px;height:40px;display:flex;align-items:center;justify-content:center;background:var(--gold-pale);border-radius:10px;color:var(--gold);flex-shrink:0}
|
||
.icon-preview-box svg{width:22px;height:22px}
|
||
.icon-preview-label{font-size:.82rem;color:var(--mid)}
|
||
.icon-grid-wrap{border:1.5px solid var(--border);border-radius:10px;overflow:hidden;max-height:0;transition:max-height .3s ease}
|
||
.icon-grid-wrap.open{max-height:280px;overflow-y:auto}
|
||
.icon-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:0;padding:.5rem}
|
||
.icon-opt{display:flex;flex-direction:column;align-items:center;gap:.3rem;padding:.55rem .2rem;border-radius:8px;cursor:pointer;border:2px solid transparent;transition:all .15s;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:20px;height:20px}
|
||
.icon-opt span{font-size:.6rem;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:1rem}
|
||
.modal-overlay.hidden,.fm-overlay.hidden,.cropper-overlay.hidden{display:none}
|
||
.modal{background:var(--white);border-radius:16px;width:100%;max-width:720px;max-height:90vh;overflow-y:auto}
|
||
.modal-header{padding:1.2rem 1.5rem;border-bottom:1px 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:1rem;font-weight:600}
|
||
.modal-close{background:none;border:none;cursor:pointer;padding:.4rem;color:var(--light)}
|
||
.modal-body{padding:1.5rem}
|
||
.modal-footer{padding:1rem 1.5rem;border-top:1px solid var(--border);display:flex;gap:.7rem;justify-content:flex-start}
|
||
|
||
/* ── Toast ── */
|
||
.toast-container{position:fixed;bottom:1.5rem;left:1.5rem;z-index:300;display:flex;flex-direction:column;gap:.5rem}
|
||
.toast{background:var(--dark);color:#fff;padding:.7rem 1.2rem;border-radius:10px;font-size:.85rem;animation:slideIn .3s ease;box-shadow:0 4px 20px rgba(0,0,0,.2)}
|
||
.toast.success{border-left:4px solid #4CAF50}
|
||
.toast.error{border-left:4px solid var(--danger)}
|
||
@keyframes slideIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
|
||
|
||
/* ── Rich text editor minimal ── */
|
||
.editor-toolbar{border:1.5px solid var(--border);border-bottom:none;border-radius:8px 8px 0 0;padding:.4rem .6rem;display:flex;gap:.3rem;flex-wrap:wrap;background:#F8FAFC}
|
||
.editor-toolbar button{background:none;border:none;padding:.3rem .5rem;border-radius:4px;cursor:pointer;font-size:.82rem;font-family:inherit;color:var(--mid)}
|
||
.editor-toolbar button:hover{background:var(--gold-pale);color:var(--gold)}
|
||
.editor-content{border:1.5px solid var(--border);border-radius:0 0 8px 8px;padding:.8rem 1rem;min-height:250px;outline:none;font-size:.9rem;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:20px;padding:2.5rem;width:380px;text-align:center}
|
||
.login-logo{font-size:1.1rem;font-weight:700;color:var(--gold);margin-bottom:.3rem}
|
||
.login-sub{font-size:.8rem;color:var(--light);margin-bottom:2rem}
|
||
.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>
|
||
<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>
|
||
<div class="nav-section">تنظیمات</div>
|
||
<div class="nav-item" onclick="showPage('siteidentity',this)">
|
||
<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>
|
||
هویت سایت (لوگو / فاویکون)
|
||
</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>
|
||
<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>
|
||
</div>
|
||
<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>
|
||
</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 ══ -->
|
||
<!-- ══ SITE IDENTITY ══ -->
|
||
<div class="page" id="page-siteidentity">
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.2rem">
|
||
|
||
<!-- Logo -->
|
||
<div class="card">
|
||
<div class="card-header"><div class="card-title">🖼️ لوگوی سایت</div></div>
|
||
<div class="modal-body">
|
||
<p style="font-size:.83rem;color:var(--mid);margin-bottom:1.2rem;line-height:1.8">
|
||
لوگو در هدر سایت و صفحات وبلاگ نمایش داده میشود.<br>
|
||
<strong>فرمت پیشنهادی:</strong> PNG با پسزمینه شفاف (transparent) — ابعاد: ۲۰۰×۶۰ پیکسل
|
||
</p>
|
||
<input type="hidden" id="si-logo"/>
|
||
<div class="input-upload-wrap" style="flex-wrap:wrap;gap:.5rem">
|
||
<button class="upload-btn" id="upload-btn-si-logo" onclick="uploadImage('si-logo','prev-si-logo')">
|
||
<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-si-logo" onclick="removeImage('si-logo','prev-si-logo')" style="display:none">حذف</button>
|
||
</div>
|
||
<img id="prev-si-logo" class="upload-preview" alt="لوگو" style="max-height:80px;object-fit:contain;background:#f5f5f5;padding:.5rem"/>
|
||
<div style="margin-top:1.2rem">
|
||
<button class="btn btn-primary" onclick="saveSiteIdentity()">ذخیره لوگو</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Favicon -->
|
||
<div class="card">
|
||
<div class="card-header"><div class="card-title">⭐ فاویکون (Favicon)</div></div>
|
||
<div class="modal-body">
|
||
<p style="font-size:.83rem;color:var(--mid);margin-bottom:1.2rem;line-height:1.8">
|
||
آیکون کوچکی که در تب مرورگر نمایش داده میشود.<br>
|
||
<strong>فرمت پیشنهادی:</strong> PNG یا ICO — ابعاد: ۳۲×۳۲ یا ۶۴×۶۴ پیکسل
|
||
</p>
|
||
<input type="hidden" id="si-favicon"/>
|
||
<div class="input-upload-wrap" style="flex-wrap:wrap;gap:.5rem">
|
||
<button class="upload-btn" id="upload-btn-si-favicon" onclick="uploadImage('si-favicon','prev-si-favicon')">
|
||
<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-si-favicon" onclick="removeImage('si-favicon','prev-si-favicon')" style="display:none">حذف</button>
|
||
</div>
|
||
<img id="prev-si-favicon" class="upload-preview" alt="فاویکون" style="max-height:64px;max-width:64px;object-fit:contain;background:#f5f5f5;padding:.4rem;border-radius:8px"/>
|
||
<div style="margin-top:1.2rem">
|
||
<button class="btn btn-primary" onclick="saveSiteIdentity()">ذخیره فاویکون</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Preview -->
|
||
<div class="card" style="margin-top:1.2rem">
|
||
<div class="card-header"><div class="card-title">پیشنمایش</div></div>
|
||
<div class="modal-body">
|
||
<div style="display:flex;align-items:center;gap:2rem;flex-wrap:wrap">
|
||
<div>
|
||
<p style="font-size:.75rem;color:var(--light);margin-bottom:.5rem">هدر سایت</p>
|
||
<div style="background:#FAFAF7;border:1px solid var(--border);border-radius:10px;padding:.8rem 1.4rem;display:flex;align-items:center;gap:.8rem;min-width:220px">
|
||
<img id="preview-logo-header" src="" alt="" style="height:36px;object-fit:contain;display:none"/>
|
||
<span id="preview-logo-text" style="font-size:.95rem;font-weight:700;color:#B8955A">دکتر سوسن آلطه</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<p style="font-size:.75rem;color:var(--light);margin-bottom:.5rem">تب مرورگر</p>
|
||
<div style="background:#E8EAED;border-radius:8px 8px 0 0;padding:.4rem .9rem;display:inline-flex;align-items:center;gap:.5rem;font-size:.78rem;color:#333;min-width:180px">
|
||
<img id="preview-favicon-tab" src="" alt="" style="width:16px;height:16px;object-fit:contain;display:none"/>
|
||
<span id="preview-favicon-text">🌐</span>
|
||
<span style="color:#666;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">دکتر سوسن آلطه</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p style="font-size:.78rem;color:var(--light);margin-top:1rem">⚠️ پس از ذخیره، تغییرات فاویکون ممکن است نیاز به پاککردن کش مرورگر داشته باشد.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<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>
|
||
|
||
<!-- ══ 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>
|
||
|
||
<!-- ══ 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>
|
||
|
||
<!-- 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 -->
|
||
<!-- 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>
|
||
|
||
<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>
|
||
|
||
<!-- 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 ────────────────────────────────────────────────────────────────
|
||
const pageTitles = {dashboard:'داشبورد',hero:'صفحه اصلی',about:'درباره من',contact:'تماس',services:'خدمات',gallery:'گالری',testimonials:'نظرات',blogposts:'مقالات',categories:'دستهبندیها',faqs:'سوالات متداول',seo:'گزارش SEO',comments:'مدیریت نظرات',security:'تغییر رمز عبور',patients:'پرونده بیماران','patient-profile':'پرونده بیمار',healthrequests:'درخواستهای سلامت',siteidentity:'هویت سایت (لوگو / فاویکون)'};
|
||
|
||
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();
|
||
else if (name==='patients') loadPatients();
|
||
else if (name==='healthrequests') loadHealthRequests();
|
||
else if (name==='siteidentity') loadSiteIdentity();
|
||
}
|
||
|
||
// ── 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 ───────────────────────────────────────────────────────────
|
||
let _allReqs = [];
|
||
|
||
async function loadHealthRequests() {
|
||
const filter = document.getElementById('reqFilter').value;
|
||
const params = filter !== '' ? `?handled=${filter}` : '';
|
||
_allReqs = await api('/api/health-requests' + params) || [];
|
||
const pending = _allReqs.filter(r=>!r.isHandled).length;
|
||
const badge = document.getElementById('healthreqBadge');
|
||
if(pending>0){badge.textContent=pending;badge.style.display='inline';}else{badge.style.display='none';}
|
||
const catLabel = {beauty:'زیبایی پوست', health:'سلامت عمومی'};
|
||
document.getElementById('healthreqTable').innerHTML = _allReqs.map(r=>`
|
||
<tr style="${!r.isHandled?'font-weight:600':'opacity:.75'}">
|
||
<td>${r.fullName}</td>
|
||
<td dir="ltr">${r.phoneNumber}</td>
|
||
<td>${catLabel[r.category]||r.category}</td>
|
||
<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--mid);font-size:.85rem">${r.message||'—'}</td>
|
||
<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>
|
||
<td style="white-space:nowrap">
|
||
<button class="btn btn-secondary btn-sm" onclick="viewReq(${r.id})">مشاهده</button>
|
||
${!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>';
|
||
}
|
||
|
||
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');
|
||
}
|
||
|
||
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();}
|
||
|
||
// ── Comments ──────────────────────────────────────────────────────────────────
|
||
// ── Site Identity (logo / favicon) ────────────────────────────────────────────
|
||
async function loadSiteIdentity() {
|
||
const data = await api('/api/settings/identity').catch(()=>[]) || [];
|
||
const vals = {};
|
||
(Array.isArray(data) ? data : []).forEach(s => vals[s.key] = s.value);
|
||
// Set hidden inputs
|
||
document.getElementById('si-logo').value = vals.logo || '';
|
||
document.getElementById('si-favicon').value = vals.favicon || '';
|
||
// Show previews
|
||
showPreview('prev-si-logo', vals.logo || '');
|
||
showPreview('prev-si-favicon', vals.favicon || '');
|
||
// Update live preview panel
|
||
updateSiteIdentityPreview(vals.logo || '', vals.favicon || '');
|
||
}
|
||
|
||
function updateSiteIdentityPreview(logoUrl, faviconUrl) {
|
||
const logoImg = document.getElementById('preview-logo-header');
|
||
const logoTxt = document.getElementById('preview-logo-text');
|
||
if (logoUrl) { logoImg.src=logoUrl; logoImg.style.display='block'; logoTxt.style.display='none'; }
|
||
else { logoImg.style.display='none'; logoTxt.style.display=''; }
|
||
|
||
const favImg = document.getElementById('preview-favicon-tab');
|
||
const favTxt = document.getElementById('preview-favicon-text');
|
||
if (faviconUrl) { favImg.src=faviconUrl; favImg.style.display='inline'; favTxt.style.display='none'; }
|
||
else { favImg.style.display='none'; favTxt.style.display=''; }
|
||
}
|
||
|
||
async function saveSiteIdentity() {
|
||
const logo = document.getElementById('si-logo').value;
|
||
const favicon = document.getElementById('si-favicon').value;
|
||
await api('/api/settings/identity', {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ settings: { logo, favicon } })
|
||
});
|
||
updateSiteIdentityPreview(logo, favicon);
|
||
toast('ذخیره شد ✓ — برای اعمال فاویکون کش مرورگر را پاک کنید');
|
||
}
|
||
|
||
// Live preview while uploading
|
||
document.addEventListener('change', () => {
|
||
const logo = document.getElementById('si-logo')?.value || '';
|
||
const favicon = document.getElementById('si-favicon')?.value || '';
|
||
if (logo || favicon) updateSiteIdentityPreview(logo, favicon);
|
||
});
|
||
|
||
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() {
|
||
const [seo, testims, patients, reqs] = await Promise.all([
|
||
api('/api/seo/stats'),
|
||
api('/api/testimonials/all'),
|
||
api('/api/patients'),
|
||
api('/api/health-requests')
|
||
]);
|
||
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;
|
||
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';}
|
||
}
|
||
}
|
||
|
||
// ── 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 ───────────────────────────────────────────────────────
|
||
// ── 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;
|
||
// Preserve original file type so PNG stays PNG, WebP stays WebP, etc.
|
||
cropper.mimeType = file.type || 'image/jpeg';
|
||
cropper.fileName = file.name || 'upload';
|
||
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);
|
||
// Capture targets and mime type BEFORE the async callback
|
||
const _inputId = cropper.inputId;
|
||
const _previewId = cropper.previewId;
|
||
const _mime = cropper.mimeType || 'image/jpeg';
|
||
const _quality = (_mime === 'image/jpeg' || _mime === 'image/webp') ? 0.92 : undefined;
|
||
const _ext = _mime.split('/')[1].replace('jpeg','jpg').replace('vnd.microsoft.icon','ico').replace('x-icon','ico').replace('svg+xml','svg');
|
||
|
||
out.toBlob(async blob => {
|
||
closeCropper();
|
||
const inputId = _inputId, previewId = _previewId;
|
||
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();
|
||
fd.append('file', new File([blob], `crop.${_ext}`, {type: _mime}));
|
||
const r = await fetch('/api/upload', {method:'POST', headers:{'Authorization':`Bearer ${token}`}, body:fd});
|
||
if (!r.ok) throw new Error(await r.text());
|
||
const { url } = await r.json();
|
||
document.getElementById(inputId).value = url;
|
||
showPreview(previewId, url);
|
||
toast('تصویر با موفقیت آپلود شد ✓');
|
||
} catch(e) { toast('خطا در آپلود: '+e.message,'error'); }
|
||
finally { btn.innerHTML=orig; btn.disabled=false; }
|
||
}, _mime, _quality);
|
||
}
|
||
|
||
async function uploadImage(inputId, previewId) {
|
||
const fileInput = document.createElement('input');
|
||
fileInput.type = 'file';
|
||
fileInput.accept = 'image/jpeg,image/png,image/webp,image/gif,image/svg+xml,image/x-icon,image/vnd.microsoft.icon';
|
||
fileInput.onchange = () => {
|
||
const file = fileInput.files[0];
|
||
if (!file) return;
|
||
openCropper(inputId, previewId, file);
|
||
};
|
||
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
|
||
loadHealthRequests(); // populate health requests badge on load
|
||
}
|
||
|
||
// Auto-login if token exists
|
||
if(token) init();
|
||
</script>
|
||
|
||
<!-- ── 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>
|
||
|
||
<!-- ── 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>
|