416 lines
176 KiB
HTML
416 lines
176 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>
|
||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
|
||
:root{--primary:#1a365d;--primary-light:#2c5282;--primary-dark:#0f2341;--accent:#c9a961;--accent-dark:#a8893f;--accent-light:#e0c789;--bg:#f5f7fa;--board-bg:#e2e8f0;--card-bg:#fff;--panel-bg:#fff;--text:#1a202c;--text-muted:#4a5568;--text-light:#718096;--border:#cbd5e0;--border-light:#e2e8f0;--success:#38a169;--danger:#e53e3e;--warning:#dd6b20;--info:#3182ce;--shadow:0 4px 6px rgba(0,0,0,.08),0 10px 20px rgba(26,54,93,.1);--shadow-sm:0 2px 4px rgba(0,0,0,.06);--shadow-lg:0 10px 30px rgba(0,0,0,.15);--radius:10px;--radius-sm:6px;--sidebar-width:280px;--header-height:64px;--tabs-height:48px;--footer-height:36px}
|
||
[data-theme="dark"]{--primary:#2c5282;--primary-light:#3b6fb5;--primary-dark:#1a365d;--accent:#e0c789;--accent-dark:#c9a961;--accent-light:#f0d9a0;--bg:#1a202c;--board-bg:#2d3748;--card-bg:#2d3748;--panel-bg:#1f2937;--text:#e2e8f0;--text-muted:#cbd5e0;--text-light:#a0aec0;--border:#4a5568;--border-light:#374151;--shadow:0 4px 6px rgba(0,0,0,.3),0 10px 20px rgba(0,0,0,.4);--shadow-sm:0 2px 4px rgba(0,0,0,.2);--shadow-lg:0 10px 30px rgba(0,0,0,.5)}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font-family:'Vazirmatn',Tahoma,sans-serif;background:var(--bg);color:var(--text);height:100vh;overflow:hidden;font-size:14px;display:flex;flex-direction:column;transition:background .3s,color .3s}
|
||
.app-header{background:linear-gradient(135deg,var(--primary) 0%,var(--primary-light) 100%);color:#fff;padding:0 20px;height:var(--header-height);display:flex;align-items:center;justify-content:space-between;border-bottom:3px solid var(--accent);box-shadow:var(--shadow);z-index:100;flex-shrink:0;position:relative;overflow:hidden}
|
||
.app-header::before{content:'';position:absolute;top:-50%;left:-10%;width:300px;height:300px;background:radial-gradient(circle,rgba(201,169,97,.15) 0%,transparent 70%);border-radius:50%;pointer-events:none}
|
||
.app-brand{display:flex;align-items:center;gap:14px;position:relative;z-index:1}
|
||
.app-logo{width:44px;height:44px;background:var(--accent);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:800;color:var(--primary-dark);box-shadow:0 4px 12px rgba(0,0,0,.25);flex-shrink:0}
|
||
.app-brand-text{display:flex;flex-direction:column;gap:2px}
|
||
.app-title{font-size:16px;font-weight:800;letter-spacing:-.3px;color:#fff}
|
||
.app-subtitle{font-size:11px;opacity:.85;color:rgba(255,255,255,.9)}
|
||
.btn{background:linear-gradient(135deg,var(--primary-light),var(--primary));color:#fff;border:none;padding:8px 14px;border-radius:var(--radius-sm);cursor:pointer;font-family:inherit;font-size:13px;font-weight:600;display:inline-flex;align-items:center;gap:6px;transition:all .2s;box-shadow:0 2px 4px rgba(0,0,0,.15);white-space:nowrap}
|
||
.btn:hover{transform:translateY(-1px);box-shadow:0 4px 8px rgba(0,0,0,.2);filter:brightness(1.1)}
|
||
.btn:active{transform:translateY(0)}
|
||
.btn-accent{background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:var(--primary-dark);font-weight:700}
|
||
.btn-ghost{background:rgba(26,54,93,.08);border:1px solid var(--border);color:var(--primary);font-weight:600}
|
||
.btn-ghost:hover{background:rgba(26,54,93,.15);border-color:var(--primary-light)}
|
||
[data-theme="dark"] .btn-ghost{background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.25);color:#fff}
|
||
[data-theme="dark"] .btn-ghost:hover{background:rgba(255,255,255,.2)}
|
||
.btn-danger{background:linear-gradient(135deg,var(--danger),#c53030);color:#fff}
|
||
.btn-sm{padding:4px 10px;font-size:11px}
|
||
.btn-xs{padding:2px 6px;font-size:10px}
|
||
.btn-icon{width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;font-size:14px}
|
||
.app-main{flex:1;display:flex;overflow:hidden;position:relative;flex-direction:row}
|
||
.sidebar{order:-1;width:var(--sidebar-width);background:var(--panel-bg);border-right:1px solid var(--border);display:flex;flex-direction:column;transition:width .3s ease;box-shadow:var(--shadow);overflow:hidden;flex-shrink:0;z-index:50}
|
||
.sidebar.collapsed{width:0;overflow:hidden}
|
||
.sidebar-header{background:linear-gradient(135deg,var(--primary),var(--primary-light));color:#fff;padding:12px;display:flex;align-items:center;justify-content:space-between;border-bottom:2px solid var(--accent);min-height:48px}
|
||
.sidebar-title{font-size:13px;font-weight:700;display:flex;align-items:center;gap:8px;white-space:nowrap;overflow:hidden}
|
||
.sidebar.collapsed .sidebar-title{display:none}
|
||
.sidebar-body{flex:1;overflow-y:auto;padding:8px}
|
||
.sidebar.collapsed .sidebar-body{display:none}
|
||
.sidebar-group{margin-bottom:6px;border:1px solid var(--border-light);border-radius:var(--radius-sm);overflow:hidden;background:var(--card-bg)}
|
||
.sidebar-group-header{background:linear-gradient(135deg,var(--primary),var(--primary-light));color:#fff;padding:10px 12px;cursor:pointer;display:flex;align-items:center;justify-content:space-between;font-size:13px;font-weight:700;user-select:none;transition:all .2s;position:relative}
|
||
.sidebar-group-header:hover{filter:brightness(1.1)}
|
||
.sidebar-group-header .group-icon{display:flex;align-items:center;gap:8px;flex:1;overflow:hidden}
|
||
.sidebar-group-header .group-arrow{transition:transform .3s;font-size:12px;flex-shrink:0}
|
||
.sidebar-group.collapsed .group-arrow{transform:rotate(90deg)}
|
||
.sidebar-group.collapsed .sidebar-group-body{display:none}
|
||
.sidebar-group-body{padding:6px;display:flex;flex-direction:column;gap:4px}
|
||
.sidebar-btn{background:var(--card-bg);border:1px solid var(--border-light);color:var(--text);padding:8px 10px;border-radius:var(--radius-sm);cursor:pointer;font-family:inherit;font-size:12px;font-weight:500;display:flex;align-items:center;gap:8px;transition:all .2s;text-align:right;width:100%;position:relative}
|
||
.sidebar-btn:hover{background:linear-gradient(135deg,var(--primary),var(--primary-light));color:#fff;border-color:var(--primary);transform:translateX(-2px)}
|
||
.sidebar-btn .btn-icon-emoji{font-size:15px;width:20px;text-align:center;flex-shrink:0}
|
||
.sidebar-btn .btn-title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
[data-tooltip]{position:relative}
|
||
[data-tooltip]::after{content:attr(data-tooltip);position:absolute;right:calc(100% + 10px);top:50%;transform:translateY(-50%);background:var(--primary-dark);color:#fff;padding:8px 12px;border-radius:6px;font-size:11px;font-weight:500;white-space:normal;opacity:0;pointer-events:none;transition:opacity .2s;z-index:1000;box-shadow:0 4px 12px rgba(0,0,0,.3);max-width:250px;line-height:1.5}
|
||
[data-tooltip]::before{content:'';position:absolute;right:calc(100% + 5px);top:50%;transform:translateY(-50%);border:5px solid transparent;border-left-color:var(--primary-dark);opacity:0;pointer-events:none;transition:opacity .2s;z-index:1000}
|
||
[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}
|
||
[data-theme="dark"] [data-tooltip]::after{background:var(--accent-dark);color:var(--primary-dark)}
|
||
[data-theme="dark"] [data-tooltip]::before{border-left-color:var(--accent-dark)}
|
||
.tabs-bar{background:linear-gradient(135deg,#0f1724 0%,#1a2a44 100%);border-bottom:2px solid var(--accent);display:flex;align-items:center;padding:0 12px;min-height:var(--tabs-height);box-shadow:var(--shadow-sm);flex-shrink:0;gap:10px;position:relative;flex-wrap:wrap}
|
||
.toolbar-controls{display:flex;gap:6px;margin-left:8px}
|
||
.toolbar-icon-btn{background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);color:#fff;width:34px;height:34px;border-radius:6px;cursor:pointer;font-size:15px;display:flex;align-items:center;justify-content:center;transition:all .2s;padding:0}
|
||
.toolbar-icon-btn:hover:not(:disabled){background:var(--accent);color:var(--primary-dark);border-color:var(--accent);transform:translateY(-1px)}
|
||
.toolbar-icon-btn svg{display:block}
|
||
.toolbar-search{flex:1;min-width:180px;max-width:320px;position:relative;display:flex;align-items:center}
|
||
.search-icon{position:absolute;right:10px;font-size:13px;opacity:.7;pointer-events:none}
|
||
.search-input{width:100%;padding:7px 32px 7px 12px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);border-radius:6px;color:#fff;font-family:inherit;font-size:13px;transition:all .2s}
|
||
.search-input::placeholder{color:rgba(255,255,255,.5)}
|
||
.search-input:focus{outline:none;background:rgba(255,255,255,.12);border-color:var(--accent);box-shadow:0 0 0 2px rgba(201,169,97,.2)}
|
||
.toolbar-filters{display:flex;gap:8px}
|
||
.filter-dropdown{position:relative;display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);border-radius:6px;padding:0 10px;transition:all .2s}
|
||
.filter-dropdown:hover{border-color:rgba(255,255,255,.3);background:rgba(255,255,255,.12)}
|
||
.filter-icon{font-size:13px;opacity:.9}
|
||
.filter-select{background:transparent;border:none;color:#fff;font-family:inherit;font-size:12px;font-weight:600;padding:7px 4px;cursor:pointer;outline:none;min-width:110px}
|
||
.filter-select option{background:var(--primary-dark);color:#fff}
|
||
.toolbar-stats{display:flex;gap:12px;margin-right:auto;padding-right:8px;border-right:1px solid rgba(255,255,255,.1)}
|
||
.stat-item{display:flex;align-items:center;gap:4px;font-size:11px;color:rgba(255,255,255,.85);white-space:nowrap}
|
||
.stat-icon{font-size:13px}
|
||
.stat-label{font-weight:500}
|
||
.stat-value{font-weight:800;color:var(--accent);background:rgba(201,169,97,.15);padding:2px 7px;border-radius:10px;font-size:11px;min-width:22px;text-align:center}
|
||
@media(max-width:900px){.tabs-bar{flex-wrap:wrap;padding:8px;gap:8px}.toolbar-stats{order:3;width:100%;margin-right:0;padding-right:0;border-right:none;border-top:1px solid rgba(255,255,255,.1);padding-top:8px;justify-content:center}.toolbar-search{order:2}}
|
||
.board-area{flex:1;overflow:auto;padding:24px;background:var(--board-bg);transition:background .3s}
|
||
.board-section{margin-bottom:32px}
|
||
.board-section:last-child{margin-bottom:0}
|
||
.board-section.collapsed .section-content{display:none}
|
||
.board-section.collapsed .collapse-icon{transform:rotate(90deg)}
|
||
.collapse-icon{transition:transform .3s ease;font-size:14px;display:inline-block;margin-left:6px}
|
||
.board-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;padding-bottom:12px;border-bottom:2px solid var(--border)}
|
||
.board-header-title{font-size:18px;font-weight:800;color:var(--primary);display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none}
|
||
[data-theme="dark"] .board-header-title{color:var(--accent)}
|
||
.board-header-actions{display:flex;gap:8px}
|
||
.projects-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:20px}
|
||
.project-card{background:var(--card-bg);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow-sm);transition:all .25s;display:flex;flex-direction:column;position:relative}
|
||
.project-card:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg);border-color:var(--accent)}
|
||
.project-card-header{background:linear-gradient(135deg,var(--primary) 0%,var(--primary-light) 100%);color:#fff;padding:16px;position:relative;overflow:hidden}
|
||
.project-card-header::before{content:'';position:absolute;top:-30px;left:-30px;width:100px;height:100px;background:radial-gradient(circle,rgba(201,169,97,.2) 0%,transparent 70%);border-radius:50%}
|
||
.project-card-title-row{display:flex;align-items:center;gap:10px;margin-bottom:4px;position:relative;z-index:1}
|
||
.project-card-icon{font-size:22px;flex-shrink:0}
|
||
.project-card-title{font-size:16px;font-weight:700;flex:1}
|
||
.project-card-desc{font-size:12px;opacity:.9;position:relative;z-index:1;line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||
.project-card-body{padding:14px 16px;flex:1;display:flex;flex-direction:column;gap:10px}
|
||
.project-stat-row{display:flex;justify-content:space-between;font-size:12px;color:var(--text-muted)}
|
||
.project-stat-value{font-weight:700;color:var(--primary)}
|
||
[data-theme="dark"] .project-stat-value{color:var(--accent)}
|
||
.project-type-badges{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
|
||
.type-badge{font-size:10px;font-weight:700;padding:2px 8px;border-radius:4px;display:inline-block}
|
||
.type-backend{background:#ebf4ff;color:#2b6cb0}.type-frontend{background:#f0fff4;color:#276749}.type-service{background:#fefcbf;color:#975a16}.type-database{background:#fed7e2;color:#97266d}.type-mobile{background:#e9d8fd;color:#553c9a}.type-infrastructure{background:#feebc8;color:#9c4221}
|
||
[data-theme="dark"] .type-backend{background:rgba(66,153,225,.2);color:#90cdf4}[data-theme="dark"] .type-frontend{background:rgba(72,187,120,.2);color:#9ae6b4}[data-theme="dark"] .type-service{background:rgba(236,201,75,.2);color:#fefcbf}[data-theme="dark"] .type-database{background:rgba(237,100,166,.2);color:#fbb6ce}[data-theme="dark"] .type-mobile{background:rgba(159,122,234,.2);color:#d6bcfa}[data-theme="dark"] .type-infrastructure{background:rgba(237,137,54,.2);color:#fbd38d}
|
||
.project-card-footer{padding:12px 16px;background:var(--border-light);border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;gap:6px}
|
||
.card-actions{display:flex;gap:6px}
|
||
.env-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
|
||
.env-card-item{background:var(--card-bg);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow-sm);transition:all .25s;display:flex;flex-direction:column}
|
||
.env-card-item:hover{transform:translateY(-3px);box-shadow:var(--shadow-lg);border-color:var(--accent)}
|
||
.env-card-preview{height:60px;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:700;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.2)}
|
||
.env-card-body{padding:14px 16px;flex:1;display:flex;flex-direction:column;gap:8px}
|
||
.env-card-name{font-size:15px;font-weight:700;color:var(--text)}
|
||
.env-card-desc{font-size:12px;color:var(--text-muted);line-height:1.5}
|
||
.env-card-footer{padding:10px 16px;background:var(--border-light);border-top:1px solid var(--border);display:flex;justify-content:flex-start;gap:6px}
|
||
.board-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text-muted);text-align:center}
|
||
.board-empty-icon{font-size:80px;opacity:.3;margin-bottom:20px}
|
||
.board-empty h2{font-size:20px;color:var(--primary);margin-bottom:10px}
|
||
[data-theme="dark"] .board-empty h2{color:var(--accent)}
|
||
.board-empty p{margin-bottom:20px}
|
||
.no-results{text-align:center;padding:40px;color:var(--text-muted);background:var(--border-light);border-radius:var(--radius);margin:20px 0}
|
||
.modal-overlay{position:fixed;inset:0;background:rgba(26,54,93,.6);backdrop-filter:blur(3px);display:none;align-items:center;justify-content:center;z-index:1000;padding:20px;animation:fadeIn .2s}
|
||
.modal-overlay.active{display:flex}
|
||
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
||
.modal{background:var(--panel-bg);color:var(--text);border-radius:12px;width:100%;max-width:600px;max-height:90vh;overflow:hidden;display:flex;flex-direction:column;box-shadow:var(--shadow-lg);animation:slideUp .25s}
|
||
.modal-lg{max-width:800px}.modal-xl{max-width:1000px}
|
||
@keyframes slideUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
|
||
.modal-header{background:linear-gradient(135deg,var(--primary),var(--primary-light));color:#fff;padding:16px 20px;display:flex;align-items:center;justify-content:space-between;border-bottom:3px solid var(--accent);flex-wrap:wrap;gap:10px}
|
||
.modal-title{font-size:16px;font-weight:700;display:flex;align-items:center;gap:10px}
|
||
.modal-close{background:rgba(255,255,255,.15);border:none;color:#fff;width:30px;height:30px;border-radius:50%;cursor:pointer;font-size:16px;transition:all .2s}
|
||
.modal-close:hover{background:var(--danger)}
|
||
.modal-body{padding:20px;overflow-y:auto;flex:1}
|
||
.modal-footer{padding:14px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-start;gap:8px;background:var(--border-light);align-items:center}
|
||
.detail-section{margin-bottom:20px;border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden}
|
||
.detail-section-header{background:linear-gradient(135deg,var(--primary),var(--primary-light));color:#fff;padding:12px 16px;font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:space-between}
|
||
.detail-section-header-actions{display:flex;align-items:center;gap:8px}
|
||
.detail-section-body{padding:16px;background:var(--card-bg)}
|
||
.detail-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px}
|
||
.detail-info-item{display:flex;flex-direction:column;gap:4px}
|
||
.detail-info-label{font-size:11px;color:var(--text-muted);font-weight:600}
|
||
.detail-info-value{font-size:13px;font-weight:600;color:var(--text)}
|
||
.subproject-list{display:flex;flex-direction:column;gap:10px}
|
||
.subproject-card{border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px;background:var(--border-light);transition:all .2s}
|
||
.subproject-card:hover{border-color:var(--accent);box-shadow:var(--shadow-sm)}
|
||
.subproject-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;flex-wrap:wrap;gap:8px}
|
||
.subproject-title{font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px}
|
||
.subproject-actions{display:flex;gap:4px}
|
||
.subproject-meta{font-size:12px;color:var(--text-muted);margin-bottom:8px}
|
||
.deployment-section{margin-top:10px;padding-top:10px;border-top:1px dashed var(--border)}
|
||
.deployment-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
|
||
.deployment-header-title{font-size:11px;font-weight:600;color:var(--text-muted)}
|
||
.deployment-list{display:flex;flex-direction:column;gap:6px}
|
||
.deployment-item{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;background:var(--card-bg);border:1px solid var(--border-light);border-radius:6px;font-size:11px;transition:all .2s}
|
||
.deployment-item:hover{border-color:var(--accent);box-shadow:var(--shadow-sm)}
|
||
.deployment-item-info{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||
.deployment-env-tag{display:inline-flex;align-items:center;gap:4px;font-size:10px;padding:2px 8px;border-radius:4px;font-weight:600}
|
||
.deployment-version{font-family:'Vazirmatn',monospace;font-weight:700;color:var(--primary);font-size:11px}
|
||
[data-theme="dark"] .deployment-version{color:var(--accent)}
|
||
.deployment-date{font-size:10px;color:var(--text-muted)}
|
||
.deployment-actions{display:flex;gap:4px}
|
||
.empty-deployments{text-align:center;padding:12px;color:var(--text-muted);font-size:11px;background:var(--border-light);border-radius:6px}
|
||
.empty-subprojects{text-align:center;padding:30px;color:var(--text-muted);background:var(--border-light);border-radius:var(--radius-sm)}
|
||
.form-group{margin-bottom:16px}
|
||
.form-label{display:block;font-size:13px;font-weight:600;color:var(--primary);margin-bottom:6px}
|
||
[data-theme="dark"] .form-label{color:var(--accent-light)}
|
||
.form-label .required{color:var(--danger)}
|
||
.form-input,.form-textarea,.form-select{width:100%;padding:9px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);font-family:inherit;font-size:13px;background:var(--card-bg);color:var(--text);transition:all .2s}
|
||
.form-input:focus,.form-textarea:focus,.form-select:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(201,169,97,.15)}
|
||
.form-input.error{border-color:var(--danger);box-shadow:0 0 0 3px rgba(229,62,62,.15)}
|
||
.form-textarea{resize:vertical;min-height:70px;line-height:1.6}
|
||
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||
.form-hint{font-size:11px;color:var(--text-muted);margin-top:4px}
|
||
.form-error{font-size:11px;color:var(--danger);margin-top:4px;display:none}
|
||
.form-error.visible{display:block}
|
||
.color-input-wrapper{display:flex;align-items:center;gap:8px;padding:6px 10px;background:var(--card-bg);border:1px solid var(--border);border-radius:var(--radius-sm)}
|
||
.color-input-wrapper input[type="color"]{width:32px;height:32px;border:none;border-radius:4px;cursor:pointer;padding:0}
|
||
.color-preview-text{font-size:11px;font-family:monospace;color:var(--text-muted)}
|
||
.webhook-section{background:var(--border-light);border:1px solid var(--border);border-radius:var(--radius-sm);padding:14px;margin-top:12px}
|
||
.webhook-section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
|
||
.webhook-section-title{font-size:13px;font-weight:700;color:var(--primary);display:flex;align-items:center;gap:6px}
|
||
[data-theme="dark"] .webhook-section-title{color:var(--accent-light)}
|
||
.webhook-toggle{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text-muted)}
|
||
.switch{position:relative;display:inline-block;width:36px;height:20px}
|
||
.switch input{opacity:0;width:0;height:0}
|
||
.slider{position:absolute;cursor:pointer;inset:0;background:var(--border);transition:.3s;border-radius:20px}
|
||
.slider::before{position:absolute;content:"";height:14px;width:14px;right:3px;bottom:3px;background:#fff;transition:.3s;border-radius:50%}
|
||
input:checked+.slider{background:var(--success)}
|
||
input:checked+.slider::before{transform:translateX(-16px)}
|
||
.webhook-status{display:flex;align-items:center;gap:8px;margin-top:10px;padding:8px 10px;background:var(--card-bg);border-radius:4px;font-size:11px;color:var(--text-muted)}
|
||
.webhook-status-dot{width:8px;height:8px;border-radius:50%;background:var(--text-muted);flex-shrink:0}
|
||
.webhook-status-dot.success{background:var(--success)}
|
||
.webhook-status-dot.error{background:var(--danger)}
|
||
.webhook-status-dot.pending{background:var(--warning);animation:pulse 1.5s infinite}
|
||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||
.toast-container{position:fixed;bottom:20px;left:20px;z-index:2000;display:flex;flex-direction:column;gap:8px}
|
||
.toast{background:var(--primary);color:#fff;padding:12px 18px;border-radius:8px;box-shadow:var(--shadow-lg);font-size:13px;display:flex;align-items:center;gap:10px;animation:slideInLeft .3s;min-width:250px}
|
||
.toast.success{background:var(--success)}.toast.error{background:var(--danger)}.toast.warning{background:var(--warning)}
|
||
@keyframes slideInLeft{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}
|
||
.app-footer{background:linear-gradient(135deg,var(--primary),var(--primary-light));color:#fff;padding:8px 20px;font-size:11px;display:flex;justify-content:space-between;align-items:center;border-top:2px solid var(--accent);flex-shrink:0}
|
||
.footer-brand{color:var(--accent);font-weight:700}
|
||
.form-input[readonly],.form-textarea[readonly]{background:var(--border-light);color:var(--text-muted);cursor:not-allowed;opacity:.85;border-style:dashed}
|
||
.readonly-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;color:var(--text-muted);background:var(--border-light);border:1px solid var(--border);padding:2px 8px;border-radius:10px;margin-right:6px}
|
||
.readonly-badge::before{content:'🔒';font-size:10px}
|
||
.readonly-info{background:linear-gradient(to left,#ebf8ff,#e6f6ff);border:1px solid #90cdf4;border-right:4px solid var(--info);padding:10px 14px;border-radius:6px;font-size:11px;color:var(--text-muted);margin-bottom:14px;display:flex;align-items:center;gap:8px}
|
||
[data-theme="dark"] .readonly-info{background:rgba(49,130,206,.12);border-color:rgba(49,130,206,.3);border-right-color:var(--info)}
|
||
.dependency-list{display:flex;flex-direction:column;gap:8px;margin-bottom:20px;max-height:200px;overflow-y:auto}
|
||
.dependency-item{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--card-bg);border:1px solid var(--border);border-radius:var(--radius-sm);transition:all .2s}
|
||
.dependency-item:hover{border-color:var(--accent);box-shadow:var(--shadow-sm)}
|
||
.dependency-info{display:flex;align-items:center;gap:10px;flex:1}
|
||
.dependency-project-name{font-weight:600;font-size:13px}
|
||
.dependency-version-info{font-size:11px;color:var(--text-muted)}
|
||
.dependency-search-box{margin-top:16px;padding-top:16px;border-top:1px solid var(--border)}
|
||
.dependency-search-input{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);font-family:inherit;font-size:13px;background:var(--card-bg);color:var(--text);margin-bottom:12px}
|
||
.dependency-search-input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(201,169,97,.15)}
|
||
.dependency-search-results{max-height:300px;overflow-y:auto}
|
||
.dependency-project-result{margin-bottom:12px;border:1px solid var(--border);border-radius:var(--radius-sm);overflow:hidden;transition:all .2s}
|
||
.dependency-project-result:hover{border-color:var(--accent);box-shadow:var(--shadow-sm)}
|
||
.dependency-project-header{background:var(--border-light);padding:10px 12px;font-weight:600;font-size:13px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;transition:all .2s}
|
||
.dependency-project-header:hover{background:var(--accent-light)}
|
||
.dependency-versions-list{padding:8px;display:flex;flex-direction:column;gap:4px}
|
||
.dependency-version-item{padding:8px 10px;background:var(--card-bg);border:1px solid var(--border-light);border-radius:4px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;font-size:12px;transition:all .2s}
|
||
.dependency-version-item:hover{background:var(--accent-light);border-color:var(--accent)}
|
||
.dependency-version-badge{font-size:10px;padding:2px 6px;border-radius:4px;background:var(--info);color:#fff}
|
||
.dependency-version-badge.history{background:var(--text-muted)}
|
||
.empty-dependencies{text-align:center;padding:20px;color:var(--text-muted);font-size:12px;background:var(--border-light);border-radius:var(--radius-sm)}
|
||
.dependency-section-title{font-size:13px;font-weight:700;color:var(--primary);margin-bottom:10px;display:flex;align-items:center;gap:6px}
|
||
[data-theme="dark"] .dependency-section-title{color:var(--accent)}
|
||
::-webkit-scrollbar{width:8px;height:8px}
|
||
::-webkit-scrollbar-track{background:transparent}
|
||
::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
|
||
::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}
|
||
.env-color-preview{padding:10px;border-radius:6px;text-align:center;font-weight:700;border:1px solid var(--border)}
|
||
.env-read-only-tag{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;padding:6px 10px;border-radius:6px}
|
||
.modal-galaxy{max-width:1200px;width:100%;height:85vh;max-height:85vh;border-radius:16px}
|
||
.galaxy-toolbar{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
|
||
.galaxy-toolbar .btn{background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);color:#fff;font-size:11px;padding:5px 10px}
|
||
.galaxy-toolbar .btn:hover{background:var(--accent);color:var(--primary-dark);border-color:var(--accent)}
|
||
.galaxy-search{position:relative;display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);border-radius:6px;padding:0 8px;height:32px;min-width:230px}
|
||
.galaxy-search-icon{font-size:12px;opacity:.85}
|
||
.galaxy-search input{flex:1;width:140px;background:transparent;border:none;color:#fff;font-family:inherit;font-size:12px;outline:none}
|
||
.galaxy-search input::placeholder{color:rgba(255,255,255,.55)}
|
||
.galaxy-search-clear{background:none;border:none;color:rgba(255,255,255,.75);cursor:pointer;font-size:12px;padding:0 2px;opacity:0;pointer-events:none}
|
||
.galaxy-search-clear.visible{opacity:1;pointer-events:auto}
|
||
.galaxy-modal-body{padding:0;position:relative;overflow:hidden;background:#0a0e27}
|
||
.galaxy-chart-container{width:100%;height:100%;min-height:400px;position:relative}
|
||
.galaxy-chart-container svg{width:100%;height:100%;display:block;cursor:grab}
|
||
.galaxy-chart-container svg:active{cursor:grabbing}
|
||
.galaxy-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:rgba(255,255,255,.5);text-align:center;font-size:16px;gap:10px}
|
||
.galaxy-empty-icon{font-size:60px;opacity:.5}
|
||
.galaxy-legend{position:absolute;bottom:16px;right:16px;background:rgba(10,14,39,.85);border:1px solid rgba(201,169,97,.3);border-radius:10px;padding:12px 16px;color:#fff;font-size:11px;z-index:10;backdrop-filter:blur(10px);max-width:220px}
|
||
.galaxy-legend-title{font-weight:700;font-size:12px;margin-bottom:10px;color:var(--accent-light);display:flex;align-items:center;gap:6px}
|
||
.galaxy-legend-item{display:flex;align-items:center;gap:8px;margin-bottom:6px}
|
||
.galaxy-legend-color{width:14px;height:14px;border-radius:50%;flex-shrink:0;box-shadow:0 0 8px currentColor}
|
||
.galaxy-legend-line{width:24px;height:2px;flex-shrink:0}
|
||
.galaxy-info-panel{position:absolute;top:16px;left:16px;background:rgba(10,14,39,.9);border:1px solid rgba(201,169,97,.3);border-radius:10px;padding:14px 18px;color:#fff;font-size:12px;z-index:10;backdrop-filter:blur(10px);max-width:280px;display:none}
|
||
.galaxy-info-panel.visible{display:block}
|
||
.galaxy-info-panel h4{font-size:14px;font-weight:700;color:var(--accent-light);margin-bottom:10px;display:flex;align-items:center;gap:8px}
|
||
.galaxy-info-row{display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid rgba(255,255,255,.1);gap:12px}
|
||
.galaxy-info-row:last-child{border-bottom:none}
|
||
.galaxy-info-label{color:rgba(255,255,255,.6);font-size:11px}
|
||
.galaxy-info-value{font-weight:600;font-size:11px;text-align:left}
|
||
.galaxy-info-close{position:absolute;top:8px;left:8px;background:rgba(255,255,255,.1);border:none;color:#fff;width:22px;height:22px;border-radius:50%;cursor:pointer;font-size:12px;display:flex;align-items:center;justify-content:center;transition:all .2s}
|
||
.galaxy-info-close:hover{background:var(--danger)}
|
||
.galaxy-tooltip{position:fixed;background:rgba(10,14,39,.95);border:1px solid var(--accent);border-radius:8px;padding:10px 14px;color:#fff;font-size:12px;pointer-events:none;z-index:3000;max-width:250px;box-shadow:0 4px 20px rgba(0,0,0,.5);display:none}
|
||
.galaxy-tooltip.visible{display:block}
|
||
.galaxy-tooltip-title{font-weight:700;color:var(--accent-light);margin-bottom:4px}
|
||
.galaxy-tooltip-subtitle{font-size:11px;color:rgba(255,255,255,.7)}
|
||
.galaxy-stats{font-size:12px;color:var(--text-muted);display:flex;gap:16px;flex-wrap:wrap}
|
||
.galaxy-stat-item{display:flex;align-items:center;gap:6px}
|
||
.galaxy-stat-value{font-weight:700;color:var(--primary);background:rgba(26,54,93,.1);padding:2px 8px;border-radius:8px}
|
||
[data-theme="dark"] .galaxy-stat-value{color:var(--accent);background:rgba(201,169,97,.15)}
|
||
.galaxy-zoom-indicator{position:absolute;bottom:16px;left:16px;background:rgba(10,14,39,.85);border:1px solid rgba(201,169,97,.3);border-radius:8px;padding:6px 12px;color:#fff;font-size:12px;z-index:10;font-weight:600}
|
||
.galaxy-search-results{position:absolute;top:16px;right:16px;width:320px;max-height:320px;overflow-y:auto;background:rgba(10,14,39,.95);border:1px solid rgba(201,169,97,.35);border-radius:10px;color:#fff;z-index:20;display:none;padding:6px;backdrop-filter:blur(10px)}
|
||
.galaxy-search-results.visible{display:block}
|
||
.galaxy-search-results-header{padding:6px 8px;font-size:11px;color:var(--accent-light);border-bottom:1px solid rgba(255,255,255,.1);margin-bottom:4px}
|
||
.galaxy-search-result-item{display:flex;gap:8px;align-items:center;padding:8px;border-radius:6px;cursor:pointer;color:#fff}
|
||
.galaxy-search-result-item:hover{background:rgba(201,169,97,.2)}
|
||
.galaxy-search-result-text{min-width:0}
|
||
.galaxy-search-result-title{font-size:12px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.galaxy-search-result-sub{font-size:10px;color:rgba(255,255,255,.65);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.galaxy-search-empty{padding:10px;text-align:center;color:rgba(255,255,255,.6);font-size:12px}
|
||
.galaxy-node{transition:opacity .2s}
|
||
.galaxy-node.galaxy-dim{opacity:.12}
|
||
.galaxy-link{transition:stroke-opacity .2s}
|
||
.galaxy-link.galaxy-dim{stroke-opacity:.04!important}
|
||
.galaxy-node.galaxy-match>circle:nth-of-type(2){stroke:#ffd700;stroke-width:3}
|
||
.galaxy-node.galaxy-focus>circle:nth-of-type(2){stroke:#fff;stroke-width:4;animation:galaxyPulse .9s ease-in-out 2}
|
||
.galaxy-node circle{transform-box:fill-box;transform-origin:center}
|
||
@keyframes galaxyPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.15)}}
|
||
.dashboard-modal-body{padding:20px;background:var(--bg);overflow-y:auto}
|
||
.dashboard-container{display:flex;flex-direction:column;gap:20px}
|
||
.dashboard-kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px}
|
||
.dashboard-kpi-card{background:var(--card-bg);border:1px solid var(--border);border-radius:var(--radius);padding:16px;text-align:center;transition:all .25s;box-shadow:var(--shadow-sm);position:relative;overflow:hidden}
|
||
.dashboard-kpi-card::before{content:'';position:absolute;top:0;right:0;left:0;height:3px;background:linear-gradient(90deg,var(--primary),var(--accent))}
|
||
.dashboard-kpi-card:hover{transform:translateY(-3px);box-shadow:var(--shadow-lg);border-color:var(--accent)}
|
||
.dashboard-kpi-icon{font-size:28px;margin-bottom:8px}
|
||
.dashboard-kpi-value{font-size:28px;font-weight:800;color:var(--primary);line-height:1;margin-bottom:4px}
|
||
[data-theme="dark"] .dashboard-kpi-value{color:var(--accent)}
|
||
.dashboard-kpi-label{font-size:11px;color:var(--text-muted);font-weight:600}
|
||
.dashboard-charts-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px}
|
||
.dashboard-chart-card{background:var(--card-bg);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow-sm);transition:all .25s}
|
||
.dashboard-chart-card:hover{box-shadow:var(--shadow);border-color:var(--accent)}
|
||
.dashboard-chart-header{padding:12px 16px;border-bottom:1px solid var(--border-light);display:flex;align-items:center;gap:8px;font-size:13px;font-weight:700;color:var(--primary)}
|
||
[data-theme="dark"] .dashboard-chart-header{color:var(--accent)}
|
||
.dashboard-chart-body{padding:16px;display:flex;justify-content:center;align-items:center;min-height:220px}
|
||
.dashboard-chart-body svg{max-width:100%;height:auto}
|
||
.dashboard-empty-chart{display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text-muted);font-size:12px;gap:8px;text-align:center;padding:20px}
|
||
.dashboard-empty-chart span{font-size:32px;opacity:.4}
|
||
.dashboard-footer-stats{font-size:11px;color:var(--text-muted);display:flex;gap:16px;flex-wrap:wrap}
|
||
.dashboard-footer-stats .stat-highlight{font-weight:700;color:var(--primary)}
|
||
[data-theme="dark"] .dashboard-footer-stats .stat-highlight{color:var(--accent)}
|
||
.d3-tooltip{position:fixed;background:var(--primary-dark);color:#fff;padding:8px 12px;border-radius:6px;font-size:11px;font-weight:600;pointer-events:none;z-index:3000;box-shadow:0 4px 12px rgba(0,0,0,.3);display:none;font-family:'Vazirmatn',sans-serif}
|
||
.d3-tooltip.visible{display:block}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header class="app-header"><div class="app-brand"><div class="app-logo">📊</div><div class="app-brand-text"><div class="app-title" id="headerTitle">مدیریت محصولات</div><div class="app-subtitle" id="headerSubtitle">مدیریت نسخهها و نیازمندیهای محصولات</div></div></div></header>
|
||
<div class="tabs-bar" id="advancedToolbar"><div class="toolbar-controls"><button class="toolbar-icon-btn" id="sidebarToggle" title="باز/بسته کردن سایدبار"><span id="sidebarIcon">✕</span></button><button class="toolbar-icon-btn" id="themeToggle" title="تغییر تم"><span id="themeIcon">🌙</span></button><button class="toolbar-icon-btn" id="undoBtn" title="بازگشت (Ctrl+Z)" disabled><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg></button><button class="toolbar-icon-btn" id="redoBtn" title="باز انجام (Ctrl+Y)" disabled><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M18.4 10.6C16.55 9 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/></svg></button></div><div class="toolbar-search"><span class="search-icon">🔍</span><input type="text" class="search-input" id="toolbarSearch" placeholder="جستجو..."></div><div class="toolbar-filters"><div class="filter-dropdown"><span class="filter-icon">🏷️</span><select class="filter-select" id="filterType"><option value="">همه انواع</option><option value="backend">بکاند</option><option value="frontend">فرانتاند</option><option value="service">میکروسرویس</option><option value="database">پایگاه داده</option><option value="mobile">موبایل</option><option value="infrastructure">زیرساخت</option></select></div><div class="filter-dropdown"><span class="filter-icon">🌐</span><select class="filter-select" id="filterEnvironment"><option value="">همه محیطها</option></select></div></div><div class="toolbar-stats"><div class="stat-item"><span class="stat-icon">📋</span><span class="stat-label">کل:</span><span class="stat-value" id="statTotal">0</span></div><div class="stat-item"><span class="stat-icon">📦</span><span class="stat-label">محصول:</span><span class="stat-value" id="statProjects">0</span></div><div class="stat-item"><span class="stat-icon">🌿</span><span class="stat-label">پروژه:</span><span class="stat-value" id="statSubProjects">0</span></div><div class="stat-item"><span class="stat-icon">🔗</span><span class="stat-label">وابستگی:</span><span class="stat-value" id="statDependencies">0</span></div></div></div>
|
||
<div class="app-main"><aside class="sidebar" id="sidebar"><div class="sidebar-header"><div class="sidebar-title">🛠️ ابزارها</div></div><div class="sidebar-body" id="sidebarBody"></div></aside><main class="board-area" id="boardArea"></main></div>
|
||
<footer class="app-footer" id="appFooter"><div><span class="footer-brand">طراح و توسعهدهنده:</span> هادی خزاعی اصل <span style="margin:0 10px;">|</span> <span class="footer-brand">شرکت:</span> رایان پرداز کاوش</div><div><span class="footer-brand">نسخه:</span> 1.4.20</div></footer>
|
||
|
||
<!-- Settings Modal -->
|
||
<div class="modal-overlay" id="settingsModal"><div class="modal modal-lg"><div class="modal-header"><div class="modal-title"><span>⚙️</span><span>تنظیمات محصول</span></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body"><div class="readonly-info"><span>ℹ️</span><span>اطلاعات پایه محصول فقط قابل مشاهده هستند.</span></div><div class="form-row"><div class="form-group"><label class="form-label">عنوان محصول <span class="readonly-badge">فقط خواندنی</span></label><input type="text" class="form-input" id="settingProjectTitle" readonly></div><div class="form-group"><label class="form-label">نسخه <span class="readonly-badge">فقط خواندنی</span></label><input type="text" class="form-input" id="settingVersion" readonly></div></div><div class="form-group"><label class="form-label">توضیحات <span class="readonly-badge">فقط خواندنی</span></label><textarea class="form-textarea" id="settingDescription" readonly></textarea></div><div class="form-row"><div class="form-group"><label class="form-label">توسعهدهنده <span class="readonly-badge">فقط خواندنی</span></label><input type="text" class="form-input" id="settingDeveloper" readonly></div><div class="form-group"><label class="form-label">شرکت <span class="readonly-badge">فقط خواندنی</span></label><input type="text" class="form-input" id="settingCompany" readonly></div></div><div class="webhook-section"><div class="webhook-section-header"><div class="webhook-section-title"><span>🔗</span><span>تنظیمات وبهوک</span></div><div class="webhook-toggle"><span>فعالسازی</span><label class="switch"><input type="checkbox" id="settingWebhookEnabled"><span class="slider"></span></label></div></div><div class="form-group"><label class="form-label">آدرس URL وبهوک <span class="required">*</span></label><input type="url" class="form-input" id="settingWebhookUrl" dir="ltr" placeholder="https://example.com/webhook"></div><div class="form-group"><label class="form-label">شناسه کلاینت (Client ID) <span class="required">*</span></label><input type="text" class="form-input" id="settingWebhookClientId" dir="ltr" placeholder="client-xxxx-xxxx"></div><div class="webhook-status"><span class="webhook-status-dot" id="webhookStatusDot"></span><span id="webhookStatusText">وضعیت: غیرفعال</span></div></div></div><div class="modal-footer"><button class="btn btn-accent" id="saveSettingsBtn">💾 ذخیره</button><button class="btn btn-ghost" data-close-modal>بستن</button></div></div></div>
|
||
|
||
<!-- Environment Modals -->
|
||
<div class="modal-overlay" id="envManagerModal"><div class="modal modal-lg"><div class="modal-header"><div class="modal-title"><span>🌍</span><span>افزودن محیط عملیاتی جدید</span></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body"><div class="form-row"><div class="form-group"><label class="form-label">نام محیط <span class="required">*</span></label><input type="text" class="form-input" id="newEnvName" placeholder="مثلاً: QA"></div><div class="form-group"><label class="form-label">توضیحات</label><input type="text" class="form-input" id="newEnvDesc" placeholder="توضیحات محیط"></div></div><div class="form-row"><div class="form-group"><label class="form-label">رنگ پسزمینه</label><div class="color-input-wrapper"><input type="color" id="newEnvBgColor" value="#38a169"><span class="color-preview-text" id="newEnvBgColorText">#38a169</span></div></div><div class="form-group"><label class="form-label">رنگ متن</label><div class="color-input-wrapper"><input type="color" id="newEnvTextColor" value="#ffffff"><span class="color-preview-text" id="newEnvTextColorText">#ffffff</span></div></div></div></div><div class="modal-footer"><button class="btn btn-accent" id="saveNewEnvBtn">💾 ثبت محیط</button><button class="btn btn-ghost" data-close-modal>انصراف</button></div></div></div>
|
||
<div class="modal-overlay" id="envEditModal"><div class="modal"><div class="modal-header"><div class="modal-title"><span>✏️</span><span>ویرایش محیط عملیاتی</span></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body"><div class="form-group"><label class="form-label">شناسه محیط <span class="readonly-badge">فقط خواندنی</span></label><input type="text" class="form-input" id="editEnvId" readonly></div><div class="form-group"><label class="form-label">نام محیط <span class="required">*</span></label><input type="text" class="form-input" id="editEnvName"></div><div class="form-group"><label class="form-label">توضیحات</label><input type="text" class="form-input" id="editEnvDesc"></div><div class="form-row"><div class="form-group"><label class="form-label">رنگ پسزمینه</label><div class="color-input-wrapper"><input type="color" id="editEnvBgColor"><span class="color-preview-text" id="editEnvBgColorText"></span></div></div><div class="form-group"><label class="form-label">رنگ متن</label><div class="color-input-wrapper"><input type="color" id="editEnvTextColor"><span class="color-preview-text" id="editEnvTextColorText"></span></div></div></div><div class="form-group"><label class="form-label">پیشنمایش</label><div id="editEnvPreview" class="env-color-preview">نمونه متن</div></div></div><div class="modal-footer"><button class="btn btn-accent" id="saveEnvEditBtn">💾 ذخیره تغییرات</button><button class="btn btn-ghost" data-close-modal>انصراف</button></div></div></div>
|
||
|
||
<!-- Product Modals -->
|
||
<div class="modal-overlay" id="projectDetailModal"><div class="modal modal-xl"><div class="modal-header"><div class="modal-title" id="projectDetailTitle"><span>📦</span><span>جزئیات محصول</span></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body" id="projectDetailBody"></div><div class="modal-footer"><button class="btn btn-danger btn-sm" id="projectDetailDeleteBtn">🗑️ حذف محصول</button><button class="btn btn-ghost" data-close-modal>بستن</button></div></div></div>
|
||
<div class="modal-overlay" id="productEditModal"><div class="modal modal-lg"><div class="modal-header"><div class="modal-title"><span>✏️</span><span>ویرایش اطلاعات محصول</span></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body"><div class="form-group"><label class="form-label">شناسه محصول <span class="readonly-badge">فقط خواندنی</span></label><input type="text" class="form-input" id="editProductId" readonly></div><div class="form-group"><label class="form-label">عنوان محصول <span class="required">*</span></label><input type="text" class="form-input" id="editProductTitle"></div><div class="form-group"><label class="form-label">توضیحات</label><textarea class="form-textarea" id="editProductDescription" rows="3"></textarea></div><div class="form-row"><div class="form-group"><label class="form-label">مالک</label><input type="text" class="form-input" id="editProductOwner"></div><div class="form-group"><label class="form-label">تاریخ ایجاد <span class="readonly-badge">فقط خواندنی</span></label><input type="text" class="form-input" id="editProductCreatedAt" readonly></div></div></div><div class="modal-footer"><button class="btn btn-accent" id="saveProductEditBtn">💾 ذخیره تغییرات</button><button class="btn btn-ghost" data-close-modal>انصراف</button></div></div></div>
|
||
<div class="modal-overlay" id="addProductModal"><div class="modal modal-lg"><div class="modal-header"><div class="modal-title"><span>➕</span><span>ایجاد محصول جدید</span></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body"><div class="readonly-info"><span>ℹ️</span><span>فیلدهای دارای <span style="color:var(--danger)">*</span> الزامی هستند.</span></div><div class="form-group"><label class="form-label">عنوان محصول <span class="required">*</span></label><input type="text" class="form-input" id="newProductTitle" placeholder="مثلاً: سامانه فروشگاه آنلاین"><div class="form-error" id="newProductTitleError">عنوان محصول الزامی است و باید حداقل ۳ کاراکتر باشد</div></div><div class="form-group"><label class="form-label">توضیحات</label><textarea class="form-textarea" id="newProductDescription" rows="3" placeholder="توضیحات مختصر..."></textarea></div><div class="form-row"><div class="form-group"><label class="form-label">مالک / تیم مسئول</label><input type="text" class="form-input" id="newProductOwner" placeholder="مثلاً: تیم توسعه محصول"></div><div class="form-group"><label class="form-label">تاریخ ایجاد <span class="readonly-badge">فقط خواندنی</span></label><input type="text" class="form-input" id="newProductCreatedAt" readonly></div></div></div><div class="modal-footer"><button class="btn btn-accent" id="saveNewProductBtn">💾 ثبت محصول</button><button class="btn btn-ghost" data-close-modal>انصراف</button></div></div></div>
|
||
<div class="modal-overlay" id="addProjectModal"><div class="modal modal-lg"><div class="modal-header"><div class="modal-title"><span>➕</span><span>افزودن پروژه جدید</span></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body"><div class="form-group"><label class="form-label">عنوان پروژه <span class="required">*</span></label><input type="text" class="form-input" id="newProjectTitle" placeholder="مثلاً: Backend API Service"></div><div class="form-row"><div class="form-group"><label class="form-label">نوع پروژه <span class="required">*</span></label><select class="form-select" id="newProjectType"><option value="">انتخاب کنید...</option></select></div><div class="form-group"><label class="form-label">فریمورک</label><input type="text" class="form-input" id="newProjectFramework" placeholder="مثلاً: .NET 8"></div></div><div class="form-row"><div class="form-group"><label class="form-label">زبان برنامهنویسی</label><input type="text" class="form-input" id="newProjectLanguage" placeholder="مثلاً: C#"></div><div class="form-group"><label class="form-label">آدرس مخزن</label><input type="text" class="form-input" id="newProjectRepo" dir="ltr" placeholder="https://github.com/..."></div></div><div class="form-group"><label class="form-label">توضیحات</label><textarea class="form-textarea" id="newProjectDescription" rows="3" placeholder="توضیحات پروژه..."></textarea></div></div><div class="modal-footer"><button class="btn btn-accent" id="saveNewProjectBtn">💾 ذخیره</button><button class="btn btn-ghost" data-close-modal>انصراف</button></div></div></div>
|
||
<div class="modal-overlay" id="editProjectModal"><div class="modal modal-lg"><div class="modal-header"><div class="modal-title"><span>✏️</span><span>ویرایش اطلاعات پروژه</span></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body"><div class="form-group"><label class="form-label">شناسه پروژه <span class="readonly-badge">فقط خواندنی</span></label><input type="text" class="form-input" id="editProjId" readonly></div><div class="form-group"><label class="form-label">عنوان پروژه <span class="required">*</span></label><input type="text" class="form-input" id="editProjTitle"></div><div class="form-row"><div class="form-group"><label class="form-label">نوع پروژه <span class="required">*</span></label><select class="form-select" id="editProjType"><option value="">انتخاب کنید...</option></select></div><div class="form-group"><label class="form-label">فریمورک</label><input type="text" class="form-input" id="editProjFramework"></div></div><div class="form-row"><div class="form-group"><label class="form-label">زبان برنامهنویسی</label><input type="text" class="form-input" id="editProjLanguage"></div><div class="form-group"><label class="form-label">آدرس مخزن</label><input type="text" class="form-input" id="editProjRepo" dir="ltr"></div></div><div class="form-group"><label class="form-label">توضیحات</label><textarea class="form-textarea" id="editProjDescription" rows="3"></textarea></div></div><div class="modal-footer"><button class="btn btn-accent" id="saveProjEditBtn">💾 ذخیره تغییرات</button><button class="btn btn-ghost" data-close-modal>انصراف</button></div></div></div>
|
||
<div class="modal-overlay" id="deploymentModal"><div class="modal"><div class="modal-header"><div class="modal-title" id="deploymentModalTitle"><span>🚀</span><span>افزودن نسخه استقرار</span></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body"><div class="form-group" id="deploymentEnvGroup"></div><div class="form-group"><label class="form-label">نسخه <span class="required">*</span></label><input type="text" class="form-input" id="depVersion" dir="ltr" placeholder="مثلاً: 1.2.3"><div class="form-hint">از فرمت SemVer استفاده کنید</div></div><div class="form-group"><label class="form-label">تاریخ و زمان استقرار <span class="required">*</span></label><input type="datetime-local" class="form-input" id="depDate" dir="ltr"></div></div><div class="modal-footer"><button class="btn btn-accent" id="saveDeploymentBtn">💾 ذخیره</button><button class="btn btn-ghost" data-close-modal>انصراف</button></div></div></div>
|
||
<div class="modal-overlay" id="dependencyManagerModal"><div class="modal modal-lg"><div class="modal-header"><div class="modal-title"><span>🔗</span><span>مدیریت وابستگیها</span></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body"><div class="readonly-info"><span>ℹ️</span><span>وابستگیهای پروژه فعلی را مدیریت کنید.</span></div><div class="dependency-section-title">📋 وابستگیهای فعلی</div><div id="currentDependenciesList" class="dependency-list"></div><div class="dependency-search-box"><div class="dependency-section-title">🔍 افزودن وابستگی جدید</div><input type="text" class="dependency-search-input" id="dependencySearch" placeholder="جستجو در پروژهها..."><div id="dependencySearchResults" class="dependency-search-results"></div></div></div><div class="modal-footer"><button class="btn btn-ghost" data-close-modal>بستن</button></div></div></div>
|
||
|
||
<!-- Galaxy Chart Modal -->
|
||
<div class="modal-overlay" id="galaxyChartModal"><div class="modal modal-galaxy"><div class="modal-header"><div class="modal-title"><span>🌌</span><span>نمودار کهکشانی محصولات</span></div><div class="galaxy-toolbar"><div class="galaxy-search"><span class="galaxy-search-icon">🔍</span><input type="text" id="galaxySearchInput" placeholder="جستجو در نمودار..." autocomplete="off"><button class="galaxy-search-clear" id="galaxySearchClear" title="پاک کردن جستجو">✕</button></div><button class="btn btn-sm" id="galaxyZoomIn" title="بزرگنمایی">🔍+</button><button class="btn btn-sm" id="galaxyZoomOut" title="کوچکنمایی">🔍-</button><button class="btn btn-sm" id="galaxyResetView" title="بازنشانی نما">🎯</button><button class="btn btn-sm" id="galaxyToggleLabels" title="نمایش/پنهان برچسبها">🏷️</button><button class="btn btn-sm" id="galaxyExportPNG" title="خروجی PNG">📷 PNG</button><button class="btn btn-sm" id="galaxyExportSVG" title="خروجی SVG">📐 SVG</button></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body galaxy-modal-body"><div id="galaxyChartContainer" class="galaxy-chart-container"></div><div class="galaxy-search-results" id="galaxySearchResults"></div><div class="galaxy-legend" id="galaxyLegend"></div><div class="galaxy-info-panel" id="galaxyInfoPanel"><button class="galaxy-info-close" id="galaxyInfoClose">✕</button><div id="galaxyInfoContent"></div></div><div class="galaxy-zoom-indicator" id="galaxyZoomIndicator">🔍 100%</div></div><div class="modal-footer"><div class="galaxy-stats" id="galaxyStats"></div><button class="btn btn-ghost" data-close-modal>بستن</button></div></div></div>
|
||
|
||
<!-- Executive Dashboard Modal -->
|
||
<div class="modal-overlay" id="executiveDashboardModal"><div class="modal modal-galaxy"><div class="modal-header"><div class="modal-title"><span>📊</span><span>داشبورد مدیریتی</span></div><div class="galaxy-toolbar"><button class="btn btn-sm" id="dashboardExportPNG" title="خروجی PNG">📷 خروجی تصویر</button><button class="btn btn-sm" id="dashboardRefresh" title="بهروزرسانی">🔄 بهروزرسانی</button></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body dashboard-modal-body" id="dashboardModalBody"><div id="dashboardContainer" class="dashboard-container"></div></div><div class="modal-footer"><div class="dashboard-footer-stats" id="dashboardFooterStats"></div><button class="btn btn-ghost" data-close-modal>بستن</button></div></div></div>
|
||
|
||
<!-- Dependency Analysis Modal -->
|
||
<div class="modal-overlay" id="dependencyAnalysisModal"><div class="modal modal-galaxy"><div class="modal-header"><div class="modal-title"><span>🔗</span><span>تحلیل وابستگیها</span></div><div class="galaxy-toolbar"><button class="btn btn-sm" id="depAnalysisExportPNG" title="خروجی PNG">📷 خروجی تصویر</button><button class="btn btn-sm" id="depAnalysisRefresh" title="بهروزرسانی">🔄 بهروزرسانی</button></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body dashboard-modal-body" id="depAnalysisModalBody"><div id="dependencyAnalysisContainer" class="dashboard-container"></div></div><div class="modal-footer"><div class="dashboard-footer-stats" id="depAnalysisFooterStats"></div><button class="btn btn-ghost" data-close-modal>بستن</button></div></div></div>
|
||
|
||
<!-- Deployment Analysis Modal -->
|
||
<div class="modal-overlay" id="deploymentAnalysisModal"><div class="modal modal-galaxy"><div class="modal-header"><div class="modal-title"><span>🚀</span><span>تحلیل استقرار</span></div><div class="galaxy-toolbar"><button class="btn btn-sm" id="deployAnalysisExportPNG" title="خروجی PNG">📷 خروجی تصویر</button><button class="btn btn-sm" id="deployAnalysisRefresh" title="بهروزرسانی">🔄 بهروزرسانی</button></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body dashboard-modal-body" id="deployAnalysisModalBody"><div id="deploymentAnalysisContainer" class="dashboard-container"></div></div><div class="modal-footer"><div class="dashboard-footer-stats" id="deployAnalysisFooterStats"></div><button class="btn btn-ghost" data-close-modal>بستن</button></div></div></div>
|
||
|
||
<!-- Version Analysis Modal -->
|
||
<div class="modal-overlay" id="versionAnalysisModal"><div class="modal modal-galaxy"><div class="modal-header"><div class="modal-title"><span>🔢</span><span>تحلیل نسخه</span></div><div class="galaxy-toolbar"><button class="btn btn-sm" id="verAnalysisExportPNG" title="خروجی PNG">📷 خروجی تصویر</button><button class="btn btn-sm" id="verAnalysisRefresh" title="بهروزرسانی">🔄 بهروزرسانی</button></div><button class="modal-close" data-close-modal>✕</button></div><div class="modal-body dashboard-modal-body" id="versionAnalysisModalBody"><div id="versionAnalysisContainer" class="dashboard-container"></div></div><div class="modal-footer"><div class="dashboard-footer-stats" id="verAnalysisFooterStats"></div><button class="btn btn-ghost" data-close-modal>بستن</button></div></div></div>
|
||
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
<div class="galaxy-tooltip" id="galaxyTooltip"></div>
|
||
|
||
<script>
|
||
const projectData={projectId:'550e8400-e29b-41d4-a716-446655440000',projectName:'xDependencies',projectTitle:'مدیریت محصولات',description:'مدیریت نسخهها و نیازمندیهای محصولات',version:'1.4.20',developer:'هادی خزاعی اصل',company:'رایان پرداز کاوش',metadata:{createdAt:'2026-09-13T00:00:00Z',updatedAt:'2026-09-14T00:00:00Z',tags:['release-management','architecture','product-management'],customFields:{}},webhook:{url:'',clientId:'',enabled:false},toolGroups:[{id:'product-management',title:'پرونده',icon:'📁',description:'مدیریت چرخه حیات محصول',tools:[{id:'clear-all',title:'پاکسازی',icon:'🗑️',action:'clearAll',shortcut:null,description:'پاکسازی کامل همه موارد'},{id:'save-product',title:'ذخیره',icon:'💾',action:'saveProduct',shortcut:null,description:'دانلود دادههای محصول'},{id:'load-product',title:'بارگذاری',icon:'📂',action:'loadProduct',shortcut:null,description:'بارگذاری از فایل JSON'},{id:'product-settings',title:'تنظیمات',icon:'⚙️',action:'openProductSettings',shortcut:null,description:'پیکربندی تنظیمات'}]},{id:'reports',title:'گزارشها',icon:'📊',description:'تحلیل و گزارشگیری',tools:[{id:'executive-dashboard',title:'داشبورد مدیریتی',icon:'📊',action:'showExecutiveDashboard',shortcut:null,description:'نمای کلی وضعیت محصولات، پروژهها و استقرارها'},{id:'dependency-analysis',title:'تحلیل وابستگیها',icon:'🔗',action:'showDependencyAnalysis',shortcut:null,description:'تحلیل جامع وابستگیها، گلوگاهها و ریسکها'},{id:'deployment-analysis',title:'تحلیل استقرار',icon:'🚀',action:'showDeploymentAnalysis',shortcut:null,description:'تحلیل جامع استقرارها، پوشش محیطها و ریسکها'},{id:'version-analysis',title:'تحلیل نسخه',icon:'🔢',action:'showVersionAnalysis',shortcut:null,description:'تحلیل جامع نسخهها، توزیع و ریسکها'},{id:'galaxy-chart',title:'نمودار کهکشانی',icon:'🌌',action:'showGalaxyChart',shortcut:null,description:'نمایش کهکشانی ساختار محصولات و روابط'}]}],appData:{environments:[{id:'env-prod-001',name:'Production',description:'محیط عملیاتی اصلی',backgroundColor:'#38a169',textColor:'#ffffff'},{id:'env-stage-002',name:'Staging',description:'محیط پیش از تولید',backgroundColor:'#dd6b20',textColor:'#ffffff'},{id:'env-dev-003',name:'Development',description:'محیط توسعه',backgroundColor:'#3182ce',textColor:'#ffffff'}],products:[{id:'prod-1001',title:'سامانه فروشگاه آنلاین',description:'پلتفرم جامع فروش محصولات',metadata:{createdAt:'2026-09-01T00:00:00Z',owner:'تیم توسعه محصول',customFields:{}},projects:[{id:'proj-1001-01',title:'Backend API Service',description:'سرویس بکاند اصلی',type:'backend',repositoryUrl:'',metadata:{framework:'.NET 8',language:'C#'},dependencies:[],lastDeployedVersions:[{environmentId:'env-prod-001',version:'2.3.1',deploymentDate:'2026-09-10T14:30:00Z'},{environmentId:'env-stage-002',version:'2.4.0-rc.1',deploymentDate:'2026-09-12T09:00:00Z'}],deploymentHistory:[]},{id:'proj-1001-02',title:'Frontend Web Application',description:'اپلیکیشن فرانتاند',type:'frontend',repositoryUrl:'',metadata:{framework:'Angular 18',language:'TypeScript'},dependencies:['proj-1001-01'],lastDeployedVersions:[{environmentId:'env-prod-001',version:'3.1.0',deploymentDate:'2026-09-11T10:00:00Z'}],deploymentHistory:[]},{id:'proj-1001-03',title:'Payment Gateway Service',description:'سرویس درگاه پرداخت',type:'service',repositoryUrl:'',metadata:{framework:'.NET 8',language:'C#'},dependencies:['proj-1001-01'],lastDeployedVersions:[{environmentId:'env-prod-001',version:'1.2.0',deploymentDate:'2026-09-08T16:00:00Z'}],deploymentHistory:[]}]}]}};
|
||
const VALID_PROJECT_TYPES=[{value:'backend',label:'بکاند (Backend)',icon:'🖥️'},{value:'frontend',label:'فرانتاند (Frontend)',icon:'🌐'},{value:'service',label:'میکروسرویس (Service)',icon:'⚙️'},{value:'database',label:'پایگاه داده (Database)',icon:'🗄️'},{value:'mobile',label:'موبایل (Mobile)',icon:'📱'},{value:'infrastructure',label:'زیرساخت (Infrastructure)',icon:'🏗️'}];
|
||
function validateAppDataStructure(d){return d&&typeof d==='object'&&Array.isArray(d.environments)&&Array.isArray(d.products)}
|
||
function populateProjectTypeDropdowns(){['newProjectType','editProjType'].forEach(id=>{const s=document.getElementById(id);if(!s)return;const cv=s.value;s.innerHTML='<option value="">انتخاب کنید...</option>'+VALID_PROJECT_TYPES.map(t=>`<option value="${t.value}">${t.icon} ${t.label}</option>`).join('');if(cv)s.value=cv})}
|
||
function showToast(msg,type='info',dur=3000){const c=document.getElementById('toastContainer');const t=document.createElement('div');t.className=`toast ${type}`;const ic={success:'✅',error:'❌',warning:'⚠️',info:'ℹ️'};t.innerHTML=`<span>${ic[type]||'ℹ️'}</span><span>${msg}</span>`;c.appendChild(t);setTimeout(()=>{t.style.transition='opacity .3s,transform .3s';t.style.opacity='0';t.style.transform='translateX(-30px)';setTimeout(()=>t.remove(),300)},dur)}
|
||
function showD3Tooltip(e,text){let tt=document.querySelector('.d3-tooltip');if(!tt){tt=document.createElement('div');tt.className='d3-tooltip';document.body.appendChild(tt)}tt.textContent=text;tt.classList.add('visible');moveD3Tooltip(e)}
|
||
function moveD3Tooltip(e){const tt=document.querySelector('.d3-tooltip');if(tt){tt.style.left=(e.clientX+12)+'px';tt.style.top=(e.clientY+12)+'px'}}
|
||
function hideD3Tooltip(){const tt=document.querySelector('.d3-tooltip');if(tt)tt.classList.remove('visible')}
|
||
const ModalManager={stack:[],open(id){const m=document.getElementById(id);if(!m)return;this.stack=this.stack.filter(x=>x!==id);this.stack.push(id);this.stack.forEach((mid,i)=>{const el=document.getElementById(mid);if(!el)return;if(mid===id)el.classList.add('active');el.style.zIndex=1000+i+1})},close(id){const m=document.getElementById(id);if(!m)return;m.classList.remove('active');m.style.zIndex='';this.stack=this.stack.filter(x=>x!==id);this.stack.forEach((mid,i)=>{const el=document.getElementById(mid);if(el)el.style.zIndex=1000+i+1})},closeTop(){if(this.stack.length>0)this.close(this.stack[this.stack.length-1])},closeAll(){[...this.stack].reverse().forEach(id=>this.close(id))},init(){document.querySelectorAll('[data-close-modal]').forEach(btn=>{btn.addEventListener('click',e=>{e.stopPropagation();const m=e.target.closest('.modal-overlay');if(m&&m.id)this.close(m.id)})});document.querySelectorAll('.modal-overlay').forEach(ov=>{ov.addEventListener('click',e=>{if(e.target===ov&&ov.id)this.close(ov.id)})});document.addEventListener('keydown',e=>{if(e.key==='Escape'){e.preventDefault();this.closeTop()}})}};
|
||
const ToolbarManager={currentFilters:{query:'',typeFilter:'',envFilter:''},init(){this.populateEnvironmentFilter();this.bindEvents();this.updateStats()},populateEnvironmentFilter(){const s=document.getElementById('filterEnvironment');if(!s)return;const envs=projectData.appData.environments||[];s.innerHTML='<option value="">همه محیطها</option>'+envs.map(e=>`<option value="${e.id}">${e.name}</option>`).join('')},bindEvents(){const si=document.getElementById('toolbarSearch'),ft=document.getElementById('filterType'),fe=document.getElementById('filterEnvironment');if(si)si.addEventListener('input',()=>{this.applyFilters();BoardRenderer.render()});if(ft)ft.addEventListener('change',()=>{this.applyFilters();BoardRenderer.render()});if(fe)fe.addEventListener('change',()=>{this.applyFilters();BoardRenderer.render()})},applyFilters(){this.currentFilters={query:(document.getElementById('toolbarSearch')?.value||'').trim().toLowerCase(),typeFilter:document.getElementById('filterType')?.value||'',envFilter:document.getElementById('filterEnvironment')?.value||''};this.updateStats()},updateStats(){const ps=projectData.appData.products||[];let tp=0,td=0;ps.forEach(p=>{(p.projects||[]).forEach(pr=>{tp++;td+=(pr.dependencies||[]).length})});const s=(id,v)=>{const el=document.getElementById(id);if(el)el.textContent=v};s('statTotal',ps.length+tp);s('statProjects',ps.length);s('statSubProjects',tp);s('statDependencies',td)},shouldShowProduct(product){const f=this.currentFilters;if(!f.query&&!f.typeFilter&&!f.envFilter)return true;return(product.projects||[]).some(pr=>{const mq=!f.query||pr.title.toLowerCase().includes(f.query)||product.title.toLowerCase().includes(f.query)||(pr.description||'').toLowerCase().includes(f.query);const mt=!f.typeFilter||pr.type===f.typeFilter;const me=!f.envFilter||(pr.lastDeployedVersions||[]).some(d=>d.environmentId===f.envFilter);return mq&&mt&&me})}};
|
||
const HistoryManager={undoStack:[],redoStack:[],maxSize:50,isUndoRedoInProgress:false,pushState(){if(this.isUndoRedoInProgress)return;this.undoStack.push(JSON.parse(JSON.stringify(projectData.appData)));if(this.undoStack.length>this.maxSize)this.undoStack.shift();this.redoStack=[];this.updateButtons()},undo(){if(this.undoStack.length===0){showToast('هیچ تغییر برای بازگشت وجود ندارد','warning');return}this.redoStack.push(JSON.parse(JSON.stringify(projectData.appData)));this.isUndoRedoInProgress=true;projectData.appData=this.undoStack.pop();this.isUndoRedoInProgress=false;this.updateUI();this.updateButtons();showToast('↩️ بازگشت به حالت قبلی','success')},redo(){if(this.redoStack.length===0){showToast('هیچ تغییر برای باز انجام وجود ندارد','warning');return}this.undoStack.push(JSON.parse(JSON.stringify(projectData.appData)));this.isUndoRedoInProgress=true;projectData.appData=this.redoStack.pop();this.isUndoRedoInProgress=false;this.updateUI();this.updateButtons();showToast('↪️ باز انجام تغییر','success')},updateUI(){BoardRenderer.render();ToolbarManager.updateStats();if(ProductManager.currentDetailProductId)ProductManager.showDetail(ProductManager.currentDetailProductId)},updateButtons(){const u=document.getElementById('undoBtn'),r=document.getElementById('redoBtn');if(u)u.disabled=this.undoStack.length===0;if(r)r.disabled=this.redoStack.length===0},init(){this.pushState();document.getElementById('undoBtn').addEventListener('click',()=>this.undo());document.getElementById('redoBtn').addEventListener('click',()=>this.redo());document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='z'&&!e.shiftKey){e.preventDefault();this.undo()}if((e.ctrlKey||e.metaKey)&&(e.key==='y'||(e.key==='z'&&e.shiftKey))){e.preventDefault();this.redo()}})}};
|
||
const SectionCollapseManager={storageKey:'boardSectionCollapseState',getState(){try{const s=localStorage.getItem(this.storageKey);return s?JSON.parse(s):{products:false,environments:false}}catch{return{products:false,environments:false}}},saveState(s){try{localStorage.setItem(this.storageKey,JSON.stringify(s))}catch(e){console.warn(e)}},toggle(id){const s=this.getState();s[id]=!s[id];this.saveState(s);const sec=document.getElementById(`section-${id}`);if(sec)sec.classList.toggle('collapsed',s[id]);return s[id]},init(){const s=this.getState();Object.keys(s).forEach(id=>{const sec=document.getElementById(`section-${id}`);if(sec&&s[id])sec.classList.add('collapsed')});document.getElementById('boardArea').addEventListener('click',e=>{const ht=e.target.closest('.board-header-title');if(ht){const sec=ht.closest('.board-section');if(sec){const sid=sec.id.replace('section-','');this.toggle(sid)}}})}};
|
||
const EnvironmentRenderer={getSectionHTML(){const envs=projectData.appData.environments||[];let h=`<div class="board-section" id="section-environments"><div class="board-header"><div class="board-header-title"><span class="collapse-icon">▼</span>🌍 محیطهای عملیاتی (${envs.length})</div><div class="board-header-actions"><button class="btn btn-accent" onclick="EnvironmentManager.openAddModal()">➕ محیط جدید</button></div></div><div class="section-content">`;if(envs.length===0){h+='<div class="board-empty" style="height:auto;padding:40px;"><div class="board-empty-icon">🌍</div><h2>هیچ محیطی تعریف نشده است</h2></div>'}else{h+='<div class="env-card-grid">';envs.forEach(env=>{h+=`<div class="env-card-item"><div class="env-card-preview" style="background-color:${env.backgroundColor||'#38a169'};color:${env.textColor||'#ffffff'};">${env.name}</div><div class="env-card-body"><div class="env-card-name">${env.name}</div><div class="env-card-desc">${env.description||'بدون توضیحات'}</div></div><div class="env-card-footer"><button class="btn btn-ghost btn-sm" onclick="EnvironmentManager.openEditModal('${env.id}')">✏️ ویرایش</button><button class="btn btn-danger btn-sm" onclick="EnvironmentManager.remove('${env.id}')">🗑️ حذف</button></div></div>`});h+='</div>'}h+='</div></div>';return h}};
|
||
const BoardRenderer={render(){const board=document.getElementById('boardArea');const products=projectData.appData.products||[];const fp=products.filter(p=>ToolbarManager.shouldShowProduct(p));let h=`<div class="board-section" id="section-products"><div class="board-header"><div class="board-header-title"><span class="collapse-icon">▼</span>📦 محصولات (${fp.length} از ${products.length})</div><div class="board-header-actions"><button class="btn btn-accent" onclick="ProductManager.openAddProductModal()">➕ محصول جدید</button></div></div><div class="section-content">`;if(products.length===0)h+='<div class="board-empty"><div class="board-empty-icon">📦</div><h2>هیچ محصولی تعریف نشده است</h2></div>';else if(fp.length===0)h+='<div class="no-results">🔍 نتیجهای یافت نشد</div>';else{h+='<div class="projects-grid">';fp.forEach(prod=>{const projects=prod.projects||[];const tc={};projects.forEach(pr=>{tc[pr.type]=(tc[pr.type]||0)+1});const td=projects.reduce((s,pr)=>s+(pr.dependencies||[]).length,0);const tdp=projects.reduce((s,pr)=>s+(pr.lastDeployedVersions||[]).length,0);h+=`<div class="project-card"><div class="project-card-header"><div class="project-card-title-row"><span class="project-card-icon">📦</span><div class="project-card-title">${prod.title}</div></div><div class="project-card-desc">${prod.description||'بدون توضیحات'}</div></div><div class="project-card-body"><div class="project-stat-row"><span>🌿 پروژهها:</span><span class="project-stat-value">${projects.length}</span></div><div class="project-stat-row"><span>🔗 وابستگیها:</span><span class="project-stat-value">${td}</span></div><div class="project-stat-row"><span>🚀 استقرارها:</span><span class="project-stat-value">${tdp}</span></div>${Object.keys(tc).length>0?`<div class="project-type-badges">${Object.entries(tc).map(([type,count])=>{const ti=VALID_PROJECT_TYPES.find(t=>t.value===type)||{label:type};return`<span class="type-badge type-${type}">${ti.label}: ${count}</span>`}).join('')}</div>`:''}</div><div class="project-card-footer"><div class="card-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.showDetail('${prod.id}')">👁️ جزئیات</button></div><button class="btn btn-danger btn-icon" onclick="ProductManager.removeProduct('${prod.id}')">🗑️</button></div></div>`});h+='</div>'}h+='</div></div>';h+=EnvironmentRenderer.getSectionHTML();board.innerHTML=h;const st=SectionCollapseManager.getState();Object.keys(st).forEach(id=>{const sec=document.getElementById(`section-${id}`);if(sec&&st[id])sec.classList.add('collapsed')})}};
|
||
const ActionRegistry={openProductSettings(){openSettingsModal()},saveProduct(){projectData.metadata.updatedAt=new Date().toISOString();const js=JSON.stringify(projectData.appData,null,2);const b=new Blob([js],{type:'application/json;charset=utf-8'});const u=URL.createObjectURL(b);const a=document.createElement('a');a.href=u;a.download=`${projectData.projectName}_v${projectData.version}_${new Date().toISOString().replace(/[:.]/g,'-').slice(0,19)}.json`;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(u);showToast('💾 دادههای محصول دانلود شد','success',4000);WebhookManager.sendEvent('product_saved',{fileName:a.download})},loadProduct(){const inp=document.createElement('input');inp.type='file';inp.accept='.json,application/json';inp.addEventListener('change',e=>{const f=e.target.files[0];if(!f)return;const r=new FileReader();r.onload=ev=>{try{const ld=JSON.parse(ev.target.result);if(!validateAppDataStructure(ld)){showToast('❌ ساختار فایل نامعتبر است','error',5000);return}HistoryManager.pushState();projectData.appData=ld;projectData.metadata.updatedAt=new Date().toISOString();updateProjectUI();renderToolGroups();WebhookManager.updateStatusUI();ToolbarManager.populateEnvironmentFilter();ToolbarManager.updateStats();BoardRenderer.render();showToast('📂 محصول بارگذاری شد','success',4000);WebhookManager.sendEvent('product_loaded',{fileName:f.name})}catch(err){showToast('❌ خطا: '+err.message,'error',5000)}};r.readAsText(f)});inp.click()},clearAll(){if(!confirm('⚠️ آیا از پاکسازی کامل اطمینان دارید؟'))return;if(!confirm('⚠️ تأیید نهایی: تمام دادهها حذف خواهند شد.'))return;HistoryManager.pushState();projectData.appData={environments:[],products:[]};ToolbarManager.populateEnvironmentFilter();ToolbarManager.updateStats();BoardRenderer.render();showToast('🗑️ پاکسازی انجام شد','success',4000);WebhookManager.sendEvent('all_cleared',{})},showGalaxyChart(){GalaxyChartManager.open()},showExecutiveDashboard(){DashboardManager.open()},showDependencyAnalysis(){DependencyAnalysisManager.open()},showDeploymentAnalysis(){DeploymentAnalysisManager.open()},showVersionAnalysis(){VersionAnalysisManager.open()},defaultAction(tool){showToast(`ابزار: ${tool.title}`,'info',3000)}};
|
||
const WebhookManager={getConfig(){return projectData.webhook},setConfig(c){projectData.webhook={...projectData.webhook,...c};this.updateStatusUI()},isValid(){const c=this.getConfig();if(!c.enabled||!c.url||!c.clientId)return false;return/^https?:\/\//i.test(c.url)},updateStatusUI(){const c=this.getConfig();const d=document.getElementById('webhookStatusDot'),t=document.getElementById('webhookStatusText');if(!d||!t)return;d.className='webhook-status-dot';if(!c.enabled)t.textContent='وضعیت: غیرفعال';else if(!c.url||!c.clientId){d.classList.add('pending');t.textContent='وضعیت: تنظیمات ناقص'}else if(!/^https?:\/\//i.test(c.url)){d.classList.add('error');t.textContent='وضعیت: آدرس نامعتبر'}else{d.classList.add('success');t.textContent='وضعیت: آماده ارسال'}},async sendEvent(en,payload={}){if(!this.isValid())return{success:false};const c=this.getConfig();try{const r=await fetch(c.url,{method:'POST',headers:{'Content-Type':'application/json','X-Client-Id':c.clientId,'X-Event-Name':en},body:JSON.stringify({event:en,timestamp:new Date().toISOString(),projectId:projectData.projectId,data:payload})});return{success:r.ok,status:r.status}}catch(e){console.error('[Webhook]',e);return{success:false}}}};
|
||
const EnvironmentManager={currentEditEnvId:null,openAddModal(){document.getElementById('newEnvName').value='';document.getElementById('newEnvDesc').value='';document.getElementById('newEnvBgColor').value='#38a169';document.getElementById('newEnvTextColor').value='#ffffff';document.getElementById('newEnvBgColorText').textContent='#38a169';document.getElementById('newEnvTextColorText').textContent='#ffffff';ModalManager.open('envManagerModal')},saveNewEnv(){const n=document.getElementById('newEnvName').value.trim();if(!n){showToast('نام محیط الزامی است','error');return}if(projectData.appData.environments.some(e=>e.name.toLowerCase()===n.toLowerCase())){showToast('محیطی با این نام وجود دارد','warning');return}HistoryManager.pushState();projectData.appData.environments.push({id:`env-${Date.now()}`,name:n,description:document.getElementById('newEnvDesc').value.trim()||'بدون توضیحات',backgroundColor:document.getElementById('newEnvBgColor').value,textColor:document.getElementById('newEnvTextColor').value});ModalManager.close('envManagerModal');ToolbarManager.populateEnvironmentFilter();ToolbarManager.updateStats();BoardRenderer.render();showToast(`محیط "${n}" اضافه شد`,'success');WebhookManager.sendEvent('environment_added',{envName:n})},openEditModal(envId){const env=projectData.appData.environments.find(e=>e.id===envId);if(!env)return;this.currentEditEnvId=envId;document.getElementById('editEnvId').value=env.id;document.getElementById('editEnvName').value=env.name;document.getElementById('editEnvDesc').value=env.description||'';document.getElementById('editEnvBgColor').value=env.backgroundColor||'#38a169';document.getElementById('editEnvTextColor').value=env.textColor||'#ffffff';document.getElementById('editEnvBgColorText').textContent=env.backgroundColor||'#38a169';document.getElementById('editEnvTextColorText').textContent=env.textColor||'#ffffff';this.updateEditPreview();ModalManager.open('envEditModal')},updateEditPreview(){const p=document.getElementById('editEnvPreview');p.style.backgroundColor=document.getElementById('editEnvBgColor').value;p.style.color=document.getElementById('editEnvTextColor').value;p.textContent='نمونه متن'},saveEdit(){const env=projectData.appData.environments.find(e=>e.id===this.currentEditEnvId);if(!env)return;const nn=document.getElementById('editEnvName').value.trim();if(!nn){showToast('نام محیط الزامی است','error');return}HistoryManager.pushState();env.name=nn;env.description=document.getElementById('editEnvDesc').value.trim();env.backgroundColor=document.getElementById('editEnvBgColor').value;env.textColor=document.getElementById('editEnvTextColor').value;ModalManager.close('envEditModal');ToolbarManager.populateEnvironmentFilter();BoardRenderer.render();showToast(`محیط "${env.name}" ویرایش شد`,'success');WebhookManager.sendEvent('environment_updated',{envId:env.id,name:env.name})},remove(id){const env=projectData.appData.environments.find(e=>e.id===id);if(!env)return;if(!confirm(`حذف محیط "${env.name}"؟`))return;HistoryManager.pushState();projectData.appData.environments=projectData.appData.environments.filter(e=>e.id!==id);ToolbarManager.populateEnvironmentFilter();ToolbarManager.updateStats();BoardRenderer.render();showToast(`محیط "${env.name}" حذف شد`,'warning');WebhookManager.sendEvent('environment_removed',{envId:id})}};
|
||
const GalaxyChartManager={simulation:null,svg:null,zoomGroup:null,zoomBehavior:null,showLabels:true,currentZoom:1,nodes:[],links:[],nodeElements:null,linkElements:null,labelElements:null,NODE_COLORS:{product:'#c9a961',backend:'#4299e1',frontend:'#48bb78',service:'#ecc94b',database:'#ed64a6',mobile:'#9f7aea',infrastructure:'#ed8936'},open(){ModalManager.open('galaxyChartModal');setTimeout(()=>this.render(),150)},buildGraphData(){const nodes=[],links=[];(projectData.appData.products||[]).forEach(prod=>{nodes.push({id:prod.id,label:prod.title,type:'product',radius:32,data:prod,productTitle:prod.title});(prod.projects||[]).forEach(pr=>{nodes.push({id:pr.id,label:pr.title,type:'project',projectType:pr.type,radius:18,data:pr,productId:prod.id,productTitle:prod.title});links.push({source:prod.id,target:pr.id,type:'ownership'});(pr.dependencies||[]).forEach(dep=>{const n=ProductManager.normalizeDependency(dep);links.push({source:pr.id,target:n.projectId,type:'dependency',version:n.version,environmentId:n.environmentId})})})});return{nodes,links}},getNodeColor(d){if(d.type==='product')return this.NODE_COLORS.product;return this.NODE_COLORS[d.projectType]||'#718096'},getNodeIcon(d){if(d.type==='product')return'📦';const ti=ProductManager.getTypeInfo(d.projectType);return ti.icon||'❓'},render(){const c=document.getElementById('galaxyChartContainer');c.innerHTML='';this.nodeElements=null;this.linkElements=null;this.labelElements=null;const w=c.clientWidth||900,h=c.clientHeight||600;const g=this.buildGraphData();this.nodes=g.nodes;this.links=g.links;this.bindToolbarEvents();this.setupSearch();if(this.nodes.length===0){c.innerHTML='<div class="galaxy-empty"><div class="galaxy-empty-icon">🌌</div><div>هیچ دادهای نیست</div></div>';this.renderLegend();this.updateStats([],[]);this.clearSearch();return}const svg=d3.select(c).append('svg').attr('width',w).attr('height',h).attr('viewBox',[0,0,w,h]).attr('id','galaxySvg');this.svg=svg;svg.append('rect').attr('width',w).attr('height',h).attr('fill','#0a0e27');const sg=svg.append('g');for(let i=0;i<150;i++)sg.append('circle').attr('cx',Math.random()*w).attr('cy',Math.random()*h).attr('r',Math.random()*1.2+.3).attr('fill','#fff').attr('opacity',Math.random()*.6+.1);const defs=svg.append('defs');const gf=defs.append('filter').attr('id','galaxyGlow').attr('x','-50%').attr('y','-50%').attr('width','200%').attr('height','200%');gf.append('feGaussianBlur').attr('stdDeviation','4').attr('result','coloredBlur');const fm=gf.append('feMerge');fm.append('feMergeNode').attr('in','coloredBlur');fm.append('feMergeNode').attr('in','SourceGraphic');this.zoomGroup=svg.append('g');this.zoomBehavior=d3.zoom().scaleExtent([.2,5]).on('zoom',e=>{this.zoomGroup.attr('transform',e.transform);this.currentZoom=e.transform.k;document.getElementById('galaxyZoomIndicator').textContent=`🔍 ${Math.round(this.currentZoom*100)}%`});svg.call(this.zoomBehavior);svg.on('click',()=>{const r=document.getElementById('galaxySearchResults');if(r)r.classList.remove('visible')});this.simulation=d3.forceSimulation(this.nodes).force('link',d3.forceLink(this.links).id(d=>d.id).distance(d=>d.type==='ownership'?100:180).strength(d=>d.type==='ownership'?.7:.3)).force('charge',d3.forceManyBody().strength(-400)).force('center',d3.forceCenter(w/2,h/2)).force('collision',d3.forceCollide().radius(d=>d.radius+15));this.linkElements=this.zoomGroup.append('g').selectAll('line').data(this.links).join('line').attr('stroke',d=>d.type==='dependency'?'#e0c789':'#4a5568').attr('stroke-width',d=>d.type==='dependency'?2.5:1.5).attr('stroke-opacity',d=>d.type==='dependency'?.8:.5).attr('stroke-dasharray',d=>d.type==='dependency'?'6,4':'none');const node=this.zoomGroup.append('g').selectAll('g').data(this.nodes).join('g').attr('class','galaxy-node').style('cursor','pointer').call(d3.drag().on('start',(e,d)=>this.dragStarted(e,d)).on('drag',(e,d)=>this.dragged(e,d)).on('end',(e,d)=>this.dragEnded(e,d)));this.nodeElements=node;node.append('circle').attr('r',d=>d.radius+6).attr('fill',d=>this.getNodeColor(d)).attr('opacity',.25).attr('filter','url(#galaxyGlow)');node.append('circle').attr('r',d=>d.radius).attr('fill',d=>this.getNodeColor(d)).attr('stroke','#fff').attr('stroke-width',d=>d.type==='product'?3:2).attr('stroke-opacity',.8).attr('filter','url(#galaxyGlow)');node.append('text').attr('text-anchor','middle').attr('dominant-baseline','central').attr('font-size',d=>d.type==='product'?'20px':'14px').text(d=>this.getNodeIcon(d)).style('pointer-events','none');this.labelElements=node.append('text').attr('dy',d=>d.radius+18).attr('text-anchor','middle').attr('fill','#fff').attr('font-size',d=>d.type==='product'?'13px':'11px').attr('font-weight',d=>d.type==='product'?'700':'500').attr('font-family','Vazirmatn,sans-serif').style('pointer-events','none').style('display',this.showLabels?'inline':'none').text(d=>d.label);node.on('mouseover',(e,d)=>this.showTooltip(e,d)).on('mousemove',e=>this.moveTooltip(e)).on('mouseout',()=>this.hideTooltip()).on('click',(e,d)=>{e.stopPropagation();this.showNodeInfo(d)});this.simulation.on('tick',()=>{this.linkElements.attr('x1',d=>d.source.x).attr('y1',d=>d.source.y).attr('x2',d=>d.target.x).attr('y2',d=>d.target.y);this.nodeElements.attr('transform',d=>`translate(${d.x},${d.y})`)});this.renderLegend();this.updateStats(this.nodes,this.links);this.clearSearch()},dragStarted(e,d){if(!e.active)this.simulation.alphaTarget(.3).restart();d.fx=d.x;d.fy=d.y},dragged(e,d){d.fx=e.x;d.fy=e.y},dragEnded(e,d){if(!e.active)this.simulation.alphaTarget(0);d.fx=null;d.fy=null},showTooltip(e,d){const t=document.getElementById('galaxyTooltip');let sub=d.type==='product'?`${(d.data.projects||[]).length} پروژه | مالک: ${d.data.metadata?.owner||'—'}`:`${ProductManager.getTypeInfo(d.projectType).label} | ${d.data.metadata?.framework||'بدون فریمورک'}`;t.innerHTML=`<div class="galaxy-tooltip-title">${this.getNodeIcon(d)} ${d.label}</div><div class="galaxy-tooltip-subtitle">${sub}</div>`;t.classList.add('visible');this.moveTooltip(e)},moveTooltip(e){const t=document.getElementById('galaxyTooltip');t.style.left=(e.clientX+15)+'px';t.style.top=(e.clientY+15)+'px'},hideTooltip(){document.getElementById('galaxyTooltip').classList.remove('visible')},showNodeInfo(d){const p=document.getElementById('galaxyInfoPanel'),c=document.getElementById('galaxyInfoContent');let h=`<h4>${this.getNodeIcon(d)} ${d.label}</h4>`;if(d.type==='product'){h+=`<div class="galaxy-info-row"><span class="galaxy-info-label">نوع</span><span class="galaxy-info-value">📦 محصول</span></div><div class="galaxy-info-row"><span class="galaxy-info-label">توضیحات</span><span class="galaxy-info-value">${d.data.description||'—'}</span></div><div class="galaxy-info-row"><span class="galaxy-info-label">مالک</span><span class="galaxy-info-value">${d.data.metadata?.owner||'—'}</span></div><div class="galaxy-info-row"><span class="galaxy-info-label">تعداد پروژه</span><span class="galaxy-info-value">${(d.data.projects||[]).length}</span></div>`}else{h+=`<div class="galaxy-info-row"><span class="galaxy-info-label">نوع</span><span class="galaxy-info-value">${ProductManager.getTypeInfo(d.projectType).label}</span></div><div class="galaxy-info-row"><span class="galaxy-info-label">فریمورک</span><span class="galaxy-info-value">${d.data.metadata?.framework||'—'}</span></div><div class="galaxy-info-row"><span class="galaxy-info-label">زبان</span><span class="galaxy-info-value">${d.data.metadata?.language||'—'}</span></div>`}c.innerHTML=h;p.classList.add('visible')},renderLegend(){const l=document.getElementById('galaxyLegend');let h='<div class="galaxy-legend-title">🎨 راهنما</div>';h+=`<div class="galaxy-legend-item"><div class="galaxy-legend-color" style="background-color:${this.NODE_COLORS.product};"></div><span>📦 محصول</span></div>`;VALID_PROJECT_TYPES.forEach(t=>{h+=`<div class="galaxy-legend-item"><div class="galaxy-legend-color" style="background-color:${this.NODE_COLORS[t.value]};"></div><span>${t.icon} ${t.label.split(' ')[0]}</span></div>`});h+=`<div class="galaxy-legend-item" style="margin-top:8px;"><div class="galaxy-legend-line" style="background:#4a5568;"></div><span>مالکیت</span></div><div class="galaxy-legend-item"><div class="galaxy-legend-line" style="background:repeating-linear-gradient(90deg,#e0c789 0px,#e0c789 6px,transparent 6px,transparent 10px);"></div><span>وابستگی</span></div>`;l.innerHTML=h},updateStats(nodes=[],links=[]){const p=projectData.appData.products.length;const pr=projectData.appData.products.reduce((x,p)=>x+p.projects.length,0);const dp=links.filter(l=>l.type==='dependency').length;document.getElementById('galaxyStats').innerHTML=`<div class="galaxy-stat-item">📦 محصول: <span class="galaxy-stat-value">${p}</span></div><div class="galaxy-stat-item">🌿 پروژه: <span class="galaxy-stat-value">${pr}</span></div><div class="galaxy-stat-item">🔗 وابستگی: <span class="galaxy-stat-value">${dp}</span></div><div class="galaxy-stat-item">⭐ نودها: <span class="galaxy-stat-value">${nodes.length}</span></div>`},bindToolbarEvents(){document.getElementById('galaxyZoomIn').onclick=()=>{if(this.svg&&this.zoomBehavior)this.svg.transition().duration(300).call(this.zoomBehavior.scaleBy,1.3)};document.getElementById('galaxyZoomOut').onclick=()=>{if(this.svg&&this.zoomBehavior)this.svg.transition().duration(300).call(this.zoomBehavior.scaleBy,.7)};document.getElementById('galaxyResetView').onclick=()=>{if(this.svg&&this.zoomBehavior)this.svg.transition().duration(500).call(this.zoomBehavior.transform,d3.zoomIdentity)};document.getElementById('galaxyToggleLabels').onclick=()=>{this.showLabels=!this.showLabels;if(this.labelElements)this.labelElements.style('display',this.showLabels?'inline':'none');showToast(this.showLabels?'برچسبها نمایش داده شدند':'برچسبها پنهان شدند','info',2000)};document.getElementById('galaxyExportPNG').onclick=()=>this.exportPNG();document.getElementById('galaxyExportSVG').onclick=()=>this.exportSVG();document.getElementById('galaxyInfoClose').onclick=()=>{document.getElementById('galaxyInfoPanel').classList.remove('visible')}},setupSearch(){const inp=document.getElementById('galaxySearchInput'),clr=document.getElementById('galaxySearchClear'),res=document.getElementById('galaxySearchResults');if(!inp||!clr||!res)return;inp.value='';res.innerHTML='';res.classList.remove('visible');clr.classList.remove('visible');inp.oninput=()=>this.applySearch(inp.value.trim());inp.onkeydown=e=>{if(e.key==='Enter'){e.preventDefault();const f=res.querySelector('.galaxy-search-result-item');if(f)f.click()}if(e.key==='Escape'&&inp.value){e.stopPropagation();this.clearSearch();inp.focus()}};clr.onclick=()=>{this.clearSearch();inp.focus()}},getSearchHaystack(n){const tl=n.type==='product'?'محصول product':((ProductManager.getTypeInfo(n.projectType)?.label||'')+' پروژه project');return[n.label,n.productTitle||'',n.data?.description||'',n.data?.metadata?.framework||'',n.data?.metadata?.language||'',tl].join(' ').toLowerCase()},applySearch(q){const r=document.getElementById('galaxySearchResults'),c=document.getElementById('galaxySearchClear');if(!r||!c)return;c.classList.toggle('visible',!!q);if(!q){this.resetSearchHighlight();r.classList.remove('visible');r.innerHTML='';return}const ql=q.toLowerCase();const m=this.nodes.filter(n=>this.getSearchHaystack(n).includes(ql));const mi=new Set(m.map(x=>x.id));if(this.nodeElements){this.nodeElements.classed('galaxy-dim',d=>!mi.has(d.id)).classed('galaxy-match',d=>mi.has(d.id))}if(this.linkElements){this.linkElements.classed('galaxy-dim',l=>{const s=typeof l.source==='object'?l.source.id:l.source;const t=typeof l.target==='object'?l.target.id:l.target;return!(mi.has(s)||mi.has(t))})}if(m.length===0)r.innerHTML='<div class="galaxy-search-empty">نتیجهای یافت نشد</div>';else{r.innerHTML=`<div class="galaxy-search-results-header">${m.length} مورد یافت</div>`+m.slice(0,50).map(n=>{const tl=n.type==='product'?'محصول':(ProductManager.getTypeInfo(n.projectType)?.label||'پروژه');return`<div class="galaxy-search-result-item" data-node-id="${n.id}"><span>${this.getNodeIcon(n)}</span><div class="galaxy-search-result-text"><div class="galaxy-search-result-title">${n.label}</div><div class="galaxy-search-result-sub">${tl}${n.productTitle&&n.type!=='product'?' | '+n.productTitle:''}</div></div></div>`}).join('');r.querySelectorAll('.galaxy-search-result-item').forEach(item=>{item.addEventListener('click',()=>{this.focusNode(item.dataset.nodeId);r.classList.remove('visible')})})}r.classList.add('visible')},clearSearch(){const i=document.getElementById('galaxySearchInput'),r=document.getElementById('galaxySearchResults'),c=document.getElementById('galaxySearchClear');if(i)i.value='';if(r){r.innerHTML='';r.classList.remove('visible')}if(c)c.classList.remove('visible');this.resetSearchHighlight()},resetSearchHighlight(){if(this.nodeElements)this.nodeElements.classed('galaxy-dim',false).classed('galaxy-match',false).classed('galaxy-focus',false);if(this.linkElements)this.linkElements.classed('galaxy-dim',false)},focusNode(nid){const n=this.nodes.find(x=>x.id===nid);if(!n||!this.svg||!this.zoomBehavior)return;const c=document.getElementById('galaxyChartContainer');const w=c.clientWidth||900,h=c.clientHeight||600,s=1.6;const t=d3.zoomIdentity.translate(w/2-s*(n.x||0),h/2-s*(n.y||0)).scale(s);this.svg.transition().duration(600).call(this.zoomBehavior.transform,t);this.showNodeInfo(n);if(this.nodeElements){this.nodeElements.classed('galaxy-focus',d=>d.id===nid);setTimeout(()=>{if(this.nodeElements)this.nodeElements.classed('galaxy-focus',false)},2000)}},exportPNG(){const se=document.getElementById('galaxySvg');if(!se){showToast('نموداری نیست','error');return}const sd=new XMLSerializer().serializeToString(se);const sb=new Blob([sd],{type:'image/svg+xml;charset=utf-8'});const u=URL.createObjectURL(sb);const img=new Image();img.onload=()=>{const cv=document.createElement('canvas');const sc=2;cv.width=se.clientWidth*sc;cv.height=se.clientHeight*sc;const cx=cv.getContext('2d');cx.fillStyle='#0a0e27';cx.fillRect(0,0,cv.width,cv.height);cx.drawImage(img,0,0,cv.width,cv.height);URL.revokeObjectURL(u);const a=document.createElement('a');a.download=`galaxy-chart_${new Date().toISOString().replace(/[:.]/g,'-').slice(0,19)}.png`;a.href=cv.toDataURL('image/png');a.click();showToast('📷 خروجی PNG ذخیره شد','success');WebhookManager.sendEvent('galaxy_chart_exported',{format:'png'})};img.src=u},exportSVG(){const se=document.getElementById('galaxySvg');if(!se){showToast('نموداری نیست','error');return}const sd=new XMLSerializer().serializeToString(se);const b=new Blob([sd],{type:'image/svg+xml;charset=utf-8'});const u=URL.createObjectURL(b);const a=document.createElement('a');a.download=`galaxy-chart_${new Date().toISOString().replace(/[:.]/g,'-').slice(0,19)}.svg`;a.href=u;a.click();URL.revokeObjectURL(u);showToast('📐 خروجی SVG ذخیره شد','success');WebhookManager.sendEvent('galaxy_chart_exported',{format:'svg'})}};
|
||
const DashboardManager={open(){ModalManager.open('executiveDashboardModal');setTimeout(()=>this.render(),150)},getData(){const ps=projectData.appData.products||[];const envs=projectData.appData.environments||[];let tp=0,td=0,tdp=0;const tc={},ppc=[],fc={},edc={};let pwd=0,pwod=0;envs.forEach(e=>{edc[e.name]=0});ps.forEach(prod=>{const projects=prod.projects||[];ppc.push({title:prod.title,count:projects.length});projects.forEach(pr=>{tp++;td+=(pr.dependencies||[]).length;const dp=pr.lastDeployedVersions||[];tdp+=dp.length;if(dp.length>0)pwd++;else pwod++;tc[pr.type]=(tc[pr.type]||0)+1;const fw=pr.metadata?.framework||'نامشخص';fc[fw]=(fc[fw]||0)+1;dp.forEach(d=>{const en=this.getEnvName(d.environmentId);if(edc[en]!==undefined)edc[en]++;else edc[en]=1})})});return{products:ps.length,projects:tp,deps:td,deploys:tdp,environments:envs.length,typeCounts:tc,productProjectCounts:ppc,frameworkCounts:fc,envDeploymentCounts:edc,projectsWithDeployment:pwd,projectsWithoutDeployment:pwod}},getEnvName(eid){const e=projectData.appData.environments.find(x=>x.id===eid);return e?e.name:eid},getTypeLabel(t){const i=VALID_PROJECT_TYPES.find(x=>x.value===t);return i?i.label.split(' ')[0]:t},getTypeColor(t){const c={backend:'#4299e1',frontend:'#48bb78',service:'#ecc94b',database:'#ed64a6',mobile:'#9f7aea',infrastructure:'#ed8936'};return c[t]||'#718096'},render(){const c=document.getElementById('dashboardContainer');if(!c)return;const d=this.getData();let h='<div class="dashboard-kpi-row">';h+=this.kpiCard('📦',d.products,'محصول')+this.kpiCard('🌿',d.projects,'پروژه')+this.kpiCard('🔗',d.deps,'وابستگی')+this.kpiCard('🚀',d.deploys,'استقرار')+this.kpiCard('🌍',d.environments,'محیط');h+='</div><div class="dashboard-charts-grid">';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">🥧 توزیع نوع پروژهها</div><div class="dashboard-chart-body" id="chartTypeDistribution"></div></div>';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 پروژهها به تفکیک محصول</div><div class="dashboard-chart-body" id="chartProjectsByProduct"></div></div>';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">🌍 استقرارها به تفکیک محیط</div><div class="dashboard-chart-body" id="chartDeploymentsByEnv"></div></div>';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">✅ وضعیت استقرار پروژهها</div><div class="dashboard-chart-body" id="chartDeploymentStatus"></div></div>';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">🛠️ توزیع فریمورکها</div><div class="dashboard-chart-body" id="chartFrameworks"></div></div>';h+='</div>';c.innerHTML=h;this.renderDonutChart('chartTypeDistribution',d.typeCounts,k=>this.getTypeLabel(k),k=>this.getTypeColor(k));this.renderBarChart('chartProjectsByProduct',d.productProjectCounts,'title','count','#2c5282');this.renderBarChart('chartDeploymentsByEnv',Object.entries(d.envDeploymentCounts).map(([name,count])=>({name,count})),'name','count','#38a169');this.renderStatusDonut('chartDeploymentStatus',d.projectsWithDeployment,d.projectsWithoutDeployment);this.renderHorizontalBarChart('chartFrameworks',d.frameworkCounts);const cov=d.projects>0?Math.round((d.projectsWithDeployment/d.projects)*100):0;document.getElementById('dashboardFooterStats').innerHTML=`<span>📦 محصولات: <span class="stat-highlight">${d.products}</span></span><span>🌿 پروژهها: <span class="stat-highlight">${d.projects}</span></span><span>✅ پوشش استقرار: <span class="stat-highlight">${cov}%</span></span><span>📅 ${new Date().toLocaleTimeString('fa-IR')}</span>`;document.getElementById('dashboardExportPNG').onclick=()=>this.exportPNG();document.getElementById('dashboardRefresh').onclick=()=>{this.render();showToast('داشبورد بهروزرسانی شد','success',2000)}},kpiCard(icon,value,label){return`<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">${icon}</div><div class="dashboard-kpi-value">${value}</div><div class="dashboard-kpi-label">${label}</div></div>`},renderDonutChart(cid,dataObj,labelFn,colorFn){const c=document.getElementById(cid);if(!c)return;c.innerHTML='';const entries=Object.entries(dataObj);if(entries.length===0){c.innerHTML='<div class="dashboard-empty-chart"><span>🥧</span>دادهای نیست</div>';return}const w=260,h=200,r=Math.min(w,h)/2-10;const svg=d3.select(c).append('svg').attr('width',w).attr('height',h).append('g').attr('transform',`translate(${w/2},${h/2})`);const pie=d3.pie().value(d=>d[1]).sort(null);const arc=d3.arc().innerRadius(r*.55).outerRadius(r);const arcH=d3.arc().innerRadius(r*.55).outerRadius(r+6);const total=entries.reduce((s,e)=>s+e[1],0);const paths=svg.selectAll('path').data(pie(entries)).join('path').attr('d',arc).attr('fill',d=>colorFn(d.data[0])).attr('stroke','white').attr('stroke-width',2).style('cursor','pointer');paths.on('mouseover',function(e,d){d3.select(this).transition().duration(150).attr('d',arcH(d));showD3Tooltip(e,`${labelFn(d.data[0])}: ${d.data[1]} (${Math.round(d.data[1]/total*100)}%)`)}).on('mousemove',e=>moveD3Tooltip(e)).on('mouseout',function(){d3.select(this).transition().duration(150).attr('d',arc);hideD3Tooltip()});svg.append('text').attr('text-anchor','middle').attr('dy','-.2em').attr('fill','var(--text)').attr('font-size','22px').attr('font-weight','800').text(total);svg.append('text').attr('text-anchor','middle').attr('dy','1.2em').attr('fill','var(--text-muted)').attr('font-size','10px').text('پروژه')},renderBarChart(cid,dataArr,lk,vk,color){const c=document.getElementById(cid);if(!c)return;c.innerHTML='';if(!dataArr||dataArr.length===0){c.innerHTML='<div class="dashboard-empty-chart"><span>📊</span>دادهای نیست</div>';return}const w=300,h=200,m={top:10,right:10,bottom:40,left:30};const iw=w-m.left-m.right,ih=h-m.top-m.bottom;const svg=d3.select(c).append('svg').attr('width',w).attr('height',h).append('g').attr('transform',`translate(${m.left},${m.top})`);const x=d3.scaleBand().domain(dataArr.map(d=>d[lk])).range([0,iw]).padding(.3);const y=d3.scaleLinear().domain([0,d3.max(dataArr,d=>d[vk])||1]).nice().range([ih,0]);svg.append('g').attr('transform',`translate(0,${ih})`).call(d3.axisBottom(x).tickSize(0)).selectAll('text').attr('fill','var(--text-muted)').attr('font-size','9px').attr('font-family','Vazirmatn');svg.append('g').call(d3.axisLeft(y).ticks(4).tickSize(-iw)).selectAll('text').attr('fill','var(--text-muted)').attr('font-size','9px');svg.selectAll('.grid-line').data(y.ticks(4)).join('line').attr('x1',0).attr('x2',iw).attr('y1',d=>y(d)).attr('y2',d=>y(d)).attr('stroke','var(--border-light)').attr('stroke-dasharray','3,3');svg.selectAll('rect').data(dataArr).join('rect').attr('x',d=>x(d[lk])).attr('y',d=>y(d[vk])).attr('width',x.bandwidth()).attr('height',d=>ih-y(d[vk])).attr('fill',color).attr('rx',4).style('cursor','pointer').on('mouseover',function(e,d){d3.select(this).attr('fill','#c9a961');showD3Tooltip(e,`${d[lk]}: ${d[vk]}`)}).on('mousemove',e=>moveD3Tooltip(e)).on('mouseout',function(){d3.select(this).attr('fill',color);hideD3Tooltip()});svg.selectAll('.bar-label').data(dataArr).join('text').attr('x',d=>x(d[lk])+x.bandwidth()/2).attr('y',d=>y(d[vk])-5).attr('text-anchor','middle').attr('fill','var(--text)').attr('font-size','10px').attr('font-weight','700').text(d=>d[vk])},renderStatusDonut(cid,wd,wod){const c=document.getElementById(cid);if(!c)return;c.innerHTML='';const total=wd+wod;if(total===0){c.innerHTML='<div class="dashboard-empty-chart"><span>✅</span>دادهای نیست</div>';return}const w=260,h=200,r=Math.min(w,h)/2-10;const svg=d3.select(c).append('svg').attr('width',w).attr('height',h).append('g').attr('transform',`translate(${w/2},${h/2})`);const data=[{label:'دارای استقرار',value:wd,color:'#38a169'},{label:'بدون استقرار',value:wod,color:'#e53e3e'}];const pie=d3.pie().value(d=>d.value).sort(null);const arc=d3.arc().innerRadius(r*.6).outerRadius(r);const arcH=d3.arc().innerRadius(r*.6).outerRadius(r+6);const paths=svg.selectAll('path').data(pie(data)).join('path').attr('d',arc).attr('fill',d=>d.data.color).attr('stroke','white').attr('stroke-width',2).style('cursor','pointer');paths.on('mouseover',function(e,d){d3.select(this).transition().duration(150).attr('d',arcH(d));showD3Tooltip(e,`${d.data.label}: ${d.data.value} (${Math.round(d.data.value/total*100)}%)`)}).on('mousemove',e=>moveD3Tooltip(e)).on('mouseout',function(){d3.select(this).transition().duration(150).attr('d',arc);hideD3Tooltip()});const cov=Math.round((wd/total)*100);svg.append('text').attr('text-anchor','middle').attr('dy','-.2em').attr('fill',cov>=70?'#38a169':cov>=40?'#dd6b20':'#e53e3e').attr('font-size','24px').attr('font-weight','800').text(`${cov}%`);svg.append('text').attr('text-anchor','middle').attr('dy','1.2em').attr('fill','var(--text-muted)').attr('font-size','10px').text('پوشش استقرار')},renderHorizontalBarChart(cid,dataObj){const c=document.getElementById(cid);if(!c)return;c.innerHTML='';const entries=Object.entries(dataObj).sort((a,b)=>b[1]-a[1]);if(entries.length===0){c.innerHTML='<div class="dashboard-empty-chart"><span>🛠️</span>دادهای نیست</div>';return}const w=300,bh=24,gap=6,h=entries.length*(bh+gap)+20;const m={top:5,right:40,bottom:5,left:100};const iw=w-m.left-m.right;const svg=d3.select(c).append('svg').attr('width',w).attr('height',h).append('g').attr('transform',`translate(${m.left},${m.top})`);const x=d3.scaleLinear().domain([0,d3.max(entries,d=>d[1])||1]).range([0,iw]);const y=d3.scaleBand().domain(entries.map(d=>d[0])).range([0,entries.length*(bh+gap)]).padding(.2);svg.selectAll('rect').data(entries).join('rect').attr('x',0).attr('y',d=>y(d[0])).attr('width',d=>x(d[1])).attr('height',y.bandwidth()).attr('fill','#2c5282').attr('rx',4).style('cursor','pointer').on('mouseover',function(e,d){d3.select(this).attr('fill','#c9a961');showD3Tooltip(e,`${d[0]}: ${d[1]}`)}).on('mousemove',e=>moveD3Tooltip(e)).on('mouseout',function(){d3.select(this).attr('fill','#2c5282');hideD3Tooltip()});svg.selectAll('.label').data(entries).join('text').attr('x',-8).attr('y',d=>y(d[0])+y.bandwidth()/2).attr('dy','0.35em').attr('text-anchor','end').attr('fill','var(--text)').attr('font-size','10px').attr('font-weight','600').text(d=>d[0].length>15?d[0].substring(0,15)+'...':d[0]);svg.selectAll('.value').data(entries).join('text').attr('x',d=>x(d[1])+6).attr('y',d=>y(d[0])+y.bandwidth()/2).attr('dy','0.35em').attr('fill','var(--text-muted)').attr('font-size','10px').attr('font-weight','700').text(d=>d[1])},exportPNG(){showToast('در حال آمادهسازی خروجی...','info',2000);const mb=document.getElementById('dashboardModalBody');if(!mb){showToast('داشبوردی نیست','error');return}const svgs=mb.querySelectorAll('svg');if(svgs.length===0){showToast('نموداری نیست','warning');return}const cv=document.createElement('canvas');const sc=2;cv.width=1200*sc;cv.height=800*sc;const cx=cv.getContext('2d');const isDark=document.documentElement.getAttribute('data-theme')==='dark';cx.fillStyle=isDark?'#1a202c':'#f5f7fa';cx.fillRect(0,0,cv.width,cv.height);cx.fillStyle=isDark?'#e0c789':'#1a365d';cx.font='bold 24px Vazirmatn';cx.textAlign='center';cx.fillText('📊 داشبورد مدیریتی',cv.width/2,40*sc);let ld=0;const total=svgs.length;const pos=[{x:50,y:80},{x:650,y:80},{x:50,y:400},{x:650,y:400},{x:50,y:600}];svgs.forEach((se,i)=>{const sd=new XMLSerializer().serializeToString(se);const b=new Blob([sd],{type:'image/svg+xml;charset=utf-8'});const u=URL.createObjectURL(b);const img=new Image();img.onload=()=>{const p=pos[i]||{x:50,y:80+i*200};cx.drawImage(img,p.x*sc,p.y*sc,img.width*sc*.9,img.height*sc*.9);URL.revokeObjectURL(u);ld++;if(ld===total){const a=document.createElement('a');a.download=`dashboard_${new Date().toISOString().replace(/[:.]/g,'-').slice(0,19)}.png`;a.href=cv.toDataURL('image/png');a.click();showToast('📷 خروجی ذخیره شد','success')}};img.onerror=()=>{ld++;if(ld===total)showToast('خطا در خروجی','error')};img.src=u})}};
|
||
const DependencyAnalysisManager={open(){ModalManager.open('dependencyAnalysisModal');setTimeout(()=>this.render(),150)},getData(){const ps=projectData.appData.products||[];let totalDeps=0,totalProjects=0;const allProjects=[],depPairs=[],inDeps={},outDeps={},brokenDeps=[];ps.forEach(prod=>{(prod.projects||[]).forEach(pr=>{allProjects.push({...pr,productName:prod.title});totalProjects++})});const pidSet=new Set(allProjects.map(p=>p.id));allProjects.forEach(pr=>{const deps=pr.dependencies||[];totalDeps+=deps.length;outDeps[pr.id]=deps.length;deps.forEach(dep=>{const n=ProductManager.normalizeDependency(dep);const targetProj=ProductManager.findProjectById(n.projectId);depPairs.push({source:pr.id,sourceTitle:pr.title,target:n.projectId,targetTitle:targetProj?targetProj.title:'نامشخص',version:n.version,environmentId:n.environmentId});if(!pidSet.has(n.projectId)){brokenDeps.push({source:pr.title,target:n.projectId})}else{inDeps[n.projectId]=(inDeps[n.projectId]||0)+1}})});const hubProjects=allProjects.map(p=>({...p,incomingCount:inDeps[p.id]||0,outgoingCount:outDeps[p.id]||0})).filter(p=>p.incomingCount>0||p.outgoingCount>0).sort((a,b)=>b.incomingCount-a.incomingCount);const bottlenecks=hubProjects.filter(p=>p.incomingCount>=2);return{totalDeps,totalProjects,allProjects,depPairs,inDeps,outDeps,brokenDeps,hubProjects,bottlenecks}},render(){const d=this.getData();const c=document.getElementById('dependencyAnalysisContainer');if(!c)return;let h='<div class="dashboard-kpi-row">';h+=this.kpiCard('🔗',d.totalDeps,'کل وابستگیها');h+=this.kpiCard('📦',d.totalProjects,'پروژهها');h+=this.kpiCard('⚠️',d.bottlenecks.length,'گلوگاه');h+=this.kpiCard('🔴',d.brokenDeps.length,'وابستگی شکسته');h+='</div><div class="dashboard-charts-grid">';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">🟥 ماتریس وابستگی</div><div class="dashboard-chart-body" id="depMatrixChart"></div></div>';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 پروژههای گلوگاه</div><div class="dashboard-chart-body" id="depHubChart"></div></div>';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">🥧 توزیع نوع پروژههای وابسته</div><div class="dashboard-chart-body" id="depTypeChart"></div></div>';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">⚠️ لیست ریسکها</div><div class="dashboard-chart-body" id="depRiskList" style="align-items:flex-start;overflow-y:auto;max-height:250px;"></div></div>';h+='</div>';c.innerHTML=h;this.renderMatrix('depMatrixChart',d);this.renderHubChart('depHubChart',d);this.renderTypeDonut('depTypeChart',d);this.renderRiskList('depRiskList',d);const avg=d.totalProjects>0?(d.totalDeps/d.totalProjects).toFixed(1):0;document.getElementById('depAnalysisFooterStats').innerHTML=`<span>🔗 کل وابستگیها: <span class="stat-highlight">${d.totalDeps}</span></span><span>📦 پروژهها: <span class="stat-highlight">${d.totalProjects}</span></span><span>📈 میانگین: <span class="stat-highlight">${avg}</span></span><span>📅 ${new Date().toLocaleTimeString('fa-IR')}</span>`;document.getElementById('depAnalysisExportPNG').onclick=()=>this.exportPNG();document.getElementById('depAnalysisRefresh').onclick=()=>{this.render();showToast('تحلیل بهروزرسانی شد','success',2000)}},kpiCard(icon,value,label){return`<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">${icon}</div><div class="dashboard-kpi-value">${value}</div><div class="dashboard-kpi-label">${label}</div></div>`},renderMatrix(cid,data){const c=document.getElementById(cid);if(!c)return;c.innerHTML='';const projects=data.allProjects;if(projects.length===0){c.innerHTML='<div class="dashboard-empty-chart"><span>🟥</span>دادهای نیست</div>';return}const cs=30,lw=100,w=lw+projects.length*cs+20,h=40+projects.length*cs+20;const svg=d3.select(c).append('svg').attr('width',Math.min(w,400)).attr('height',Math.min(h,350)).attr('viewBox',`0 0 ${w} ${h}`);const matrix={};projects.forEach(p=>{matrix[p.id]={}});data.depPairs.forEach(pair=>{if(matrix[pair.source]&&matrix[pair.source][pair.target]!==undefined)matrix[pair.source][pair.target]=1});svg.selectAll('.row-label').data(projects).join('text').attr('x',lw-5).attr('y',(d,i)=>40+i*cs+cs/2).attr('text-anchor','end').attr('fill','var(--text)').attr('font-size','9px').attr('font-family','Vazirmatn').text(d=>d.title.length>12?d.title.substring(0,12)+'...':d.title);svg.selectAll('.col-label').data(projects).join('text').attr('x',lw+cs/2).attr('y',30).attr('text-anchor','start').attr('transform',(d,i)=>`rotate(45,${lw+i*cs+cs/2},30)`).attr('fill','var(--text-muted)').attr('font-size','8px').attr('font-family','Vazirmatn').text(d=>d.title.length>8?d.title.substring(0,8):d.title);projects.forEach((row,i)=>{projects.forEach((col,j)=>{const hasDep=matrix[row.id]?.[col.id]===1;svg.append('rect').attr('x',lw+j*cs).attr('y',40+i*cs).attr('width',cs-2).attr('height',cs-2).attr('rx',3).attr('fill',hasDep?'#e53e3e':'var(--border-light)').attr('opacity',hasDep?.9:.5).style('cursor',hasDep?'pointer':'default').on('mouseover',hasDep?e=>{showD3Tooltip(e,`${row.title} → ${col.title}`)}:null).on('mousemove',hasDep?e=>moveD3Tooltip(e):null).on('mouseout',hasDep?()=>hideD3Tooltip():null)})})},renderHubChart(cid,data){const c=document.getElementById(cid);if(!c)return;c.innerHTML='';const hubs=data.hubProjects.slice(0,8);if(hubs.length===0){c.innerHTML='<div class="dashboard-empty-chart"><span>📊</span>وابستگیای نیست</div>';return}const bh=28,gap=8,m={top:5,right:40,bottom:5,left:120};const w=350,iw=w-m.left-m.right,h=hubs.length*(bh+gap)+m.top+m.bottom;const svg=d3.select(c).append('svg').attr('width',w).attr('height',h).append('g').attr('transform',`translate(${m.left},${m.top})`);const x=d3.scaleLinear().domain([0,d3.max(hubs,d=>d.incomingCount)||1]).range([0,iw]);const y=d3.scaleBand().domain(hubs.map(d=>d.title)).range([0,hubs.length*(bh+gap)]).padding(.2);svg.selectAll('rect').data(hubs).join('rect').attr('x',0).attr('y',d=>y(d.title)).attr('width',d=>x(d.incomingCount)).attr('height',y.bandwidth()).attr('fill',d=>d.incomingCount>=2?'#e53e3e':'#dd6b20').attr('rx',4).style('cursor','pointer').on('mouseover',function(e,d){d3.select(this).attr('fill','#c9a961');showD3Tooltip(e,`${d.title}: ${d.incomingCount} وابستگی ورودی`)}).on('mousemove',e=>moveD3Tooltip(e)).on('mouseout',function(e,d){d3.select(this).attr('fill',d.incomingCount>=2?'#e53e3e':'#dd6b20');hideD3Tooltip()});svg.selectAll('.label').data(hubs).join('text').attr('x',-8).attr('y',d=>y(d.title)+y.bandwidth()/2).attr('dy','0.35em').attr('text-anchor','end').attr('fill','var(--text)').attr('font-size','10px').attr('font-weight','600').text(d=>d.title.length>15?d.title.substring(0,15)+'...':d.title);svg.selectAll('.value').data(hubs).join('text').attr('x',d=>x(d.incomingCount)+6).attr('y',d=>y(d.title)+y.bandwidth()/2).attr('dy','0.35em').attr('fill','var(--text-muted)').attr('font-size','10px').attr('font-weight','700').text(d=>d.incomingCount)},renderTypeDonut(cid,data){const c=document.getElementById(cid);if(!c)return;c.innerHTML='';const tc={};data.allProjects.forEach(pr=>{if((pr.dependencies||[]).length>0)tc[pr.type]=(tc[pr.type]||0)+1});const entries=Object.entries(tc);if(entries.length===0){c.innerHTML='<div class="dashboard-empty-chart"><span>🥧</span>وابستگیای نیست</div>';return}const w=260,h=200,r=Math.min(w,h)/2-10;const svg=d3.select(c).append('svg').attr('width',w).attr('height',h).append('g').attr('transform',`translate(${w/2},${h/2})`);const pie=d3.pie().value(d=>d[1]).sort(null);const arc=d3.arc().innerRadius(r*.55).outerRadius(r);const arcH=d3.arc().innerRadius(r*.55).outerRadius(r+6);const total=entries.reduce((s,e)=>s+e[1],0);const colors={backend:'#4299e1',frontend:'#48bb78',service:'#ecc94b',database:'#ed64a6',mobile:'#9f7aea',infrastructure:'#ed8936'};const paths=svg.selectAll('path').data(pie(entries)).join('path').attr('d',arc).attr('fill',d=>colors[d.data[0]]||'#718096').attr('stroke','white').attr('stroke-width',2).style('cursor','pointer');paths.on('mouseover',function(e,d){d3.select(this).transition().duration(150).attr('d',arcH(d));const ti=ProductManager.getTypeInfo(d.data[0]);showD3Tooltip(e,`${ti.label}: ${d.data[1]} (${Math.round(d.data[1]/total*100)}%)`)}).on('mousemove',e=>moveD3Tooltip(e)).on('mouseout',function(){d3.select(this).transition().duration(150).attr('d',arc);hideD3Tooltip()});svg.append('text').attr('text-anchor','middle').attr('dy','-.2em').attr('fill','var(--text)').attr('font-size','22px').attr('font-weight','800').text(total);svg.append('text').attr('text-anchor','middle').attr('dy','1.2em').attr('fill','var(--text-muted)').attr('font-size','10px').text('پروژه وابسته')},renderRiskList(cid,data){const c=document.getElementById(cid);if(!c)return;let h='';if(data.brokenDeps.length>0){h+='<div style="margin-bottom:12px;width:100%"><div style="font-size:12px;font-weight:700;color:var(--danger);margin-bottom:8px;">🔴 وابستگیهای شکسته</div>';data.brokenDeps.forEach(bd=>{h+=`<div style="font-size:11px;padding:6px 8px;background:rgba(229,62,62,.1);border-radius:4px;margin-bottom:4px;border-right:3px solid var(--danger);">${bd.source} → پروژه ناموجود (${bd.target})</div>`});h+='</div>'}if(data.bottlenecks.length>0){h+='<div style="margin-bottom:12px;width:100%"><div style="font-size:12px;font-weight:700;color:var(--warning);margin-bottom:8px;">⚠️ پروژههای گلوگاه</div>';data.bottlenecks.forEach(bn=>{h+=`<div style="font-size:11px;padding:6px 8px;background:rgba(221,107,32,.1);border-radius:4px;margin-bottom:4px;border-right:3px solid var(--warning);">${bn.title}: ${bn.incomingCount} پروژه وابسته</div>`});h+='</div>'}const isolated=data.allProjects.filter(p=>(p.dependencies||[]).length===0&&!(data.inDeps[p.id]>0));if(isolated.length>0){h+='<div style="margin-bottom:12px;width:100%"><div style="font-size:12px;font-weight:700;color:var(--info);margin-bottom:8px;">ℹ️ پروژههای ایزوله</div>';isolated.forEach(iso=>{h+=`<div style="font-size:11px;padding:6px 8px;background:rgba(49,130,206,.1);border-radius:4px;margin-bottom:4px;border-right:3px solid var(--info);">${iso.title}</div>`});h+='</div>'}if(!h)h='<div class="dashboard-empty-chart"><span>✅</span>هیچ ریسکی شناسایی نشد</div>';c.innerHTML=h},exportPNG(){showToast('در حال آمادهسازی خروجی...','info',2000);const mb=document.getElementById('depAnalysisModalBody');if(!mb){showToast('داشبوردی نیست','error');return}const svgs=mb.querySelectorAll('svg');if(svgs.length===0){showToast('نموداری نیست','warning');return}const cv=document.createElement('canvas');const sc=2;cv.width=1200*sc;cv.height=800*sc;const cx=cv.getContext('2d');const isDark=document.documentElement.getAttribute('data-theme')==='dark';cx.fillStyle=isDark?'#1a202c':'#f5f7fa';cx.fillRect(0,0,cv.width,cv.height);cx.fillStyle=isDark?'#e0c789':'#1a365d';cx.font='bold 24px Vazirmatn';cx.textAlign='center';cx.fillText('🔗 تحلیل وابستگیها',cv.width/2,40*sc);let ld=0;const total=svgs.length;const pos=[{x:50,y:80},{x:650,y:80},{x:50,y:400},{x:650,y:400}];svgs.forEach((se,i)=>{const sd=new XMLSerializer().serializeToString(se);const b=new Blob([sd],{type:'image/svg+xml;charset=utf-8'});const u=URL.createObjectURL(b);const img=new Image();img.onload=()=>{const p=pos[i]||{x:50,y:80+i*200};cx.drawImage(img,p.x*sc,p.y*sc,img.width*sc*.9,img.height*sc*.9);URL.revokeObjectURL(u);ld++;if(ld===total){const a=document.createElement('a');a.download=`dependency-analysis_${new Date().toISOString().replace(/[:.]/g,'-').slice(0,19)}.png`;a.href=cv.toDataURL('image/png');a.click();showToast('📷 خروجی ذخیره شد','success');WebhookManager.sendEvent('dependency_analysis_exported',{format:'png'})}};img.onerror=()=>{ld++;if(ld===total)showToast('خطا در خروجی','error')};img.src=u})}};
|
||
const DeploymentAnalysisManager={open(){ModalManager.open('deploymentAnalysisModal');setTimeout(()=>this.render(),150)},getData(){const ps=projectData.appData.products||[];const envs=projectData.appData.environments||[];let totalDeployments=0,projectsWithDeployment=0,projectsWithoutDeployment=0,totalProjects=0;const envDeploymentCounts={};envs.forEach(e=>{envDeploymentCounts[e.id]={name:e.name,count:0,color:e.backgroundColor}});const projectDeploymentMatrix=[];const oldDeployments=[];const projectsNoDeployment=[];const now=new Date();const thirtyDaysAgo=new Date(now.getTime()-30*24*60*60*1000);ps.forEach(prod=>{(prod.projects||[]).forEach(pr=>{totalProjects++;const deploys=pr.lastDeployedVersions||[];if(deploys.length>0)projectsWithDeployment++;else{projectsWithoutDeployment++;projectsNoDeployment.push({title:pr.title,productName:prod.title})}totalDeployments+=deploys.length;const row={projectTitle:pr.title,productName:prod.title,type:pr.type,envs:{}};deploys.forEach(d=>{if(envDeploymentCounts[d.environmentId])envDeploymentCounts[d.environmentId].count++;row.envs[d.environmentId]={version:d.version,date:d.deploymentDate};if(d.deploymentDate&&new Date(d.deploymentDate)<thirtyDaysAgo){oldDeployments.push({projectTitle:pr.title,environmentId:d.environmentId,version:d.version,date:d.deploymentDate,daysOld:Math.floor((now-new Date(d.deploymentDate))/(1000*60*60*24))})}});projectDeploymentMatrix.push(row)})});const envsNoDeployment=envs.filter(e=>envDeploymentCounts[e.id]?.count===0);const coverage=totalProjects>0?Math.round((projectsWithDeployment/totalProjects)*100):0;return{totalDeployments,totalProjects,projectsWithDeployment,projectsWithoutDeployment,coverage,envDeploymentCounts,projectDeploymentMatrix,oldDeployments,projectsNoDeployment,envsNoDeployment,environments:envs}},render(){const d=this.getData();const c=document.getElementById('deploymentAnalysisContainer');if(!c)return;let h='<div class="dashboard-kpi-row">';h+=this.kpiCard('🚀',d.totalDeployments,'کل استقرارها');h+=this.kpiCard('✅',d.projectsWithDeployment,'با استقرار');h+=this.kpiCard('❌',d.projectsWithoutDeployment,'بدون استقرار');h+=this.kpiCard('📈',d.coverage+'%','پوشش استقرار');h+='</div><div class="dashboard-charts-grid">';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">🌍 استقرارها به تفکیک محیط</div><div class="dashboard-chart-body" id="deployEnvChart"></div></div>';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 وضعیت استقرار پروژهها</div><div class="dashboard-chart-body" id="deployStatusChart"></div></div>';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">📋 مقایسه نسخهها بین محیطها</div><div class="dashboard-chart-body" id="deployMatrixTable" style="align-items:flex-start;overflow-y:auto;max-height:280px;width:100%;"></div></div>';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">⚠️ لیست ریسکها و هشدارها</div><div class="dashboard-chart-body" id="deployRiskList" style="align-items:flex-start;overflow-y:auto;max-height:280px;width:100%;"></div></div>';h+='</div>';c.innerHTML=h;this.renderEnvBarChart('deployEnvChart',d);this.renderStatusDonut('deployStatusChart',d);this.renderMatrixTable('deployMatrixTable',d);this.renderRiskList('deployRiskList',d);document.getElementById('deployAnalysisFooterStats').innerHTML=`<span>🚀 کل استقرارها: <span class="stat-highlight">${d.totalDeployments}</span></span><span>📦 پروژهها: <span class="stat-highlight">${d.totalProjects}</span></span><span>📈 پوشش: <span class="stat-highlight">${d.coverage}%</span></span><span>📅 ${new Date().toLocaleTimeString('fa-IR')}</span>`;document.getElementById('deployAnalysisExportPNG').onclick=()=>this.exportPNG();document.getElementById('deployAnalysisRefresh').onclick=()=>{this.render();showToast('تحلیل بهروزرسانی شد','success',2000)}},kpiCard(icon,value,label){return`<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">${icon}</div><div class="dashboard-kpi-value">${value}</div><div class="dashboard-kpi-label">${label}</div></div>`},renderEnvBarChart(cid,data){const c=document.getElementById(cid);if(!c)return;c.innerHTML='';const envData=Object.values(data.envDeploymentCounts);if(envData.length===0){c.innerHTML='<div class="dashboard-empty-chart"><span>🌍</span>محیطی نیست</div>';return}const w=300,h=200,m={top:10,right:10,bottom:40,left:30};const iw=w-m.left-m.right,ih=h-m.top-m.bottom;const svg=d3.select(c).append('svg').attr('width',w).attr('height',h).append('g').attr('transform',`translate(${m.left},${m.top})`);const x=d3.scaleBand().domain(envData.map(d=>d.name)).range([0,iw]).padding(.3);const y=d3.scaleLinear().domain([0,d3.max(envData,d=>d.count)||1]).nice().range([ih,0]);svg.append('g').attr('transform',`translate(0,${ih})`).call(d3.axisBottom(x).tickSize(0)).selectAll('text').attr('fill','var(--text-muted)').attr('font-size','9px').attr('font-family','Vazirmatn');svg.append('g').call(d3.axisLeft(y).ticks(4).tickSize(-iw)).selectAll('text').attr('fill','var(--text-muted)').attr('font-size','9px');svg.selectAll('.grid-line').data(y.ticks(4)).join('line').attr('x1',0).attr('x2',iw).attr('y1',d=>y(d)).attr('y2',d=>y(d)).attr('stroke','var(--border-light)').attr('stroke-dasharray','3,3');svg.selectAll('rect').data(envData).join('rect').attr('x',d=>x(d.name)).attr('y',d=>y(d.count)).attr('width',x.bandwidth()).attr('height',d=>ih-y(d.count)).attr('fill',d=>d.color||'#38a169').attr('rx',4).style('cursor','pointer').on('mouseover',function(e,d){d3.select(this).attr('fill','#c9a961');showD3Tooltip(e,`${d.name}: ${d.count} استقرار`)}).on('mousemove',e=>moveD3Tooltip(e)).on('mouseout',function(e,d){d3.select(this).attr('fill',d.color||'#38a169');hideD3Tooltip()});svg.selectAll('.bar-label').data(envData).join('text').attr('x',d=>x(d.name)+x.bandwidth()/2).attr('y',d=>y(d.count)-5).attr('text-anchor','middle').attr('fill','var(--text)').attr('font-size','10px').attr('font-weight','700').text(d=>d.count)},renderStatusDonut(cid,data){const c=document.getElementById(cid);if(!c)return;c.innerHTML='';const total=data.projectsWithDeployment+data.projectsWithoutDeployment;if(total===0){c.innerHTML='<div class="dashboard-empty-chart"><span>📊</span>دادهای نیست</div>';return}const w=260,h=200,r=Math.min(w,h)/2-10;const svg=d3.select(c).append('svg').attr('width',w).attr('height',h).append('g').attr('transform',`translate(${w/2},${h/2})`);const donutData=[{label:'با استقرار',value:data.projectsWithDeployment,color:'#38a169'},{label:'بدون استقرار',value:data.projectsWithoutDeployment,color:'#e53e3e'}];const pie=d3.pie().value(d=>d.value).sort(null);const arc=d3.arc().innerRadius(r*.6).outerRadius(r);const arcH=d3.arc().innerRadius(r*.6).outerRadius(r+6);const paths=svg.selectAll('path').data(pie(donutData)).join('path').attr('d',arc).attr('fill',d=>d.data.color).attr('stroke','white').attr('stroke-width',2).style('cursor','pointer');paths.on('mouseover',function(e,d){d3.select(this).transition().duration(150).attr('d',arcH(d));showD3Tooltip(e,`${d.data.label}: ${d.data.value} (${Math.round(d.data.value/total*100)}%)`)}).on('mousemove',e=>moveD3Tooltip(e)).on('mouseout',function(){d3.select(this).transition().duration(150).attr('d',arc);hideD3Tooltip()});svg.append('text').attr('text-anchor','middle').attr('dy','-.2em').attr('fill',data.coverage>=70?'#38a169':data.coverage>=40?'#dd6b20':'#e53e3e').attr('font-size','24px').attr('font-weight','800').text(`${data.coverage}%`);svg.append('text').attr('text-anchor','middle').attr('dy','1.2em').attr('fill','var(--text-muted)').attr('font-size','10px').text('پوشش استقرار')},renderMatrixTable(cid,data){const c=document.getElementById(cid);if(!c)return;if(data.projectDeploymentMatrix.length===0){c.innerHTML='<div class="dashboard-empty-chart"><span>📋</span>دادهای نیست</div>';return}const envs=data.environments;let html='<table style="width:100%;border-collapse:collapse;font-size:11px;">';html+='<thead><tr style="background:var(--border-light);">';html+='<th style="padding:8px;text-align:right;border-bottom:2px solid var(--border);font-weight:700;">پروژه</th>';envs.forEach(env=>{html+=`<th style="padding:8px;text-align:center;border-bottom:2px solid var(--border);font-weight:700;"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${env.backgroundColor};margin-left:4px;"></span>${env.name}</th>`});html+='</tr></thead><tbody>';data.projectDeploymentMatrix.forEach(row=>{html+='<tr style="border-bottom:1px solid var(--border-light);">';html+=`<td style="padding:6px 8px;font-weight:600;">${row.projectTitle}<br><small style="color:var(--text-muted);font-weight:400;">${row.productName}</small></td>`;envs.forEach(env=>{const cell=row.envs[env.id];if(cell){const dateStr=cell.date?new Date(cell.date).toLocaleDateString('fa-IR'):'';html+=`<td style="padding:6px 8px;text-align:center;"><span style="font-family:monospace;font-weight:700;color:var(--primary);">v${cell.version}</span><br><small style="color:var(--text-muted);">${dateStr}</small></td>`}else{html+='<td style="padding:6px 8px;text-align:center;color:var(--text-muted);">—</td>'}});html+='</tr>'});html+='</tbody></table>';c.innerHTML=html},renderRiskList(cid,data){const c=document.getElementById(cid);if(!c)return;let h='';if(data.projectsNoDeployment.length>0){h+='<div style="margin-bottom:12px;width:100%"><div style="font-size:12px;font-weight:700;color:var(--danger);margin-bottom:8px;">❌ پروژههای بدون استقرار</div>';data.projectsNoDeployment.forEach(p=>{h+=`<div style="font-size:11px;padding:6px 8px;background:rgba(229,62,62,.1);border-radius:4px;margin-bottom:4px;border-right:3px solid var(--danger);">${p.title} <small style="color:var(--text-muted);">(${p.productName})</small></div>`});h+='</div>'}if(data.oldDeployments.length>0){h+='<div style="margin-bottom:12px;width:100%"><div style="font-size:12px;font-weight:700;color:var(--warning);margin-bottom:8px;">⏰ استقرارهای قدیمی (بیش از ۳۰ روز)</div>';data.oldDeployments.forEach(od=>{const envName=data.environments.find(e=>e.id===od.environmentId)?.name||od.environmentId;h+=`<div style="font-size:11px;padding:6px 8px;background:rgba(221,107,32,.1);border-radius:4px;margin-bottom:4px;border-right:3px solid var(--warning);">${od.projectTitle} - v${od.version} در ${envName} <small style="color:var(--text-muted);">(${od.daysOld} روز پیش)</small></div>`});h+='</div>'}if(data.envsNoDeployment.length>0){h+='<div style="margin-bottom:12px;width:100%"><div style="font-size:12px;font-weight:700;color:var(--info);margin-bottom:8px;">ℹ️ محیطهای بدون استقرار</div>';data.envsNoDeployment.forEach(env=>{h+=`<div style="font-size:11px;padding:6px 8px;background:rgba(49,130,206,.1);border-radius:4px;margin-bottom:4px;border-right:3px solid var(--info);">${env.name}: هیچ پروژهای استقرار ندارد</div>`});h+='</div>'}if(!h)h='<div class="dashboard-empty-chart"><span>✅</span>هیچ ریسکی شناسایی نشد</div>';c.innerHTML=h},exportPNG(){showToast('در حال آمادهسازی خروجی...','info',2000);const mb=document.getElementById('deployAnalysisModalBody');if(!mb){showToast('داشبوردی نیست','error');return}const svgs=mb.querySelectorAll('svg');if(svgs.length===0){showToast('نموداری نیست','warning');return}const cv=document.createElement('canvas');const sc=2;cv.width=1200*sc;cv.height=800*sc;const cx=cv.getContext('2d');const isDark=document.documentElement.getAttribute('data-theme')==='dark';cx.fillStyle=isDark?'#1a202c':'#f5f7fa';cx.fillRect(0,0,cv.width,cv.height);cx.fillStyle=isDark?'#e0c789':'#1a365d';cx.font='bold 24px Vazirmatn';cx.textAlign='center';cx.fillText('🚀 تحلیل استقرار',cv.width/2,40*sc);let ld=0;const total=svgs.length;const pos=[{x:50,y:80},{x:650,y:80}];svgs.forEach((se,i)=>{const sd=new XMLSerializer().serializeToString(se);const b=new Blob([sd],{type:'image/svg+xml;charset=utf-8'});const u=URL.createObjectURL(b);const img=new Image();img.onload=()=>{const p=pos[i]||{x:50,y:80+i*200};cx.drawImage(img,p.x*sc,p.y*sc,img.width*sc*.9,img.height*sc*.9);URL.revokeObjectURL(u);ld++;if(ld===total){const a=document.createElement('a');a.download=`deployment-analysis_${new Date().toISOString().replace(/[:.]/g,'-').slice(0,19)}.png`;a.href=cv.toDataURL('image/png');a.click();showToast('📷 خروجی ذخیره شد','success');WebhookManager.sendEvent('deployment_analysis_exported',{format:'png'})}};img.onerror=()=>{ld++;if(ld===total)showToast('خطا در خروجی','error')};img.src=u})}};
|
||
const VersionAnalysisManager={open(){ModalManager.open('versionAnalysisModal');setTimeout(()=>this.render(),150)},parseVersion(v){const parts=v.split('.');return{major:parseInt(parts[0])||0,minor:parseInt(parts[1])||0,patch:parseInt(parts[2])||0,raw:v}},getData(){const ps=projectData.appData.products||[];const envs=projectData.appData.environments||[];let totalVersions=0;const uniqueVersions=new Set();const majorCounts={};const envVersionCounts={};const projectVersionMatrix=[];const oldVersions=[];const projectsNoVersion=[];envs.forEach(e=>{envVersionCounts[e.id]={name:e.name,count:0,color:e.backgroundColor}});const now=new Date();const thirtyDaysAgo=new Date(now.getTime()-30*24*60*60*1000);ps.forEach(prod=>{(prod.projects||[]).forEach(pr=>{const deploys=pr.lastDeployedVersions||[];if(deploys.length===0){projectsNoVersion.push({title:pr.title,productName:prod.title})}const row={projectTitle:pr.title,productName:prod.title,type:pr.type,envs:{}};deploys.forEach(d=>{totalVersions++;uniqueVersions.add(d.version);const parsed=this.parseVersion(d.version);const majorKey=`v${parsed.major}.x`;majorCounts[majorKey]=(majorCounts[majorKey]||0)+1;if(envVersionCounts[d.environmentId])envVersionCounts[d.environmentId].count++;row.envs[d.environmentId]={version:d.version,date:d.deploymentDate};if(d.deploymentDate&&new Date(d.deploymentDate)<thirtyDaysAgo){oldVersions.push({projectTitle:pr.title,environmentId:d.environmentId,version:d.version,date:d.deploymentDate,daysOld:Math.floor((now-new Date(d.deploymentDate))/(1000*60*60*24))})}});projectVersionMatrix.push(row)})});let avgMajor=0,avgMinor=0,avgPatch=0;if(uniqueVersions.size>0){let sm=0,smi=0,sp=0;uniqueVersions.forEach(v=>{const p=this.parseVersion(v);sm+=p.major;smi+=p.minor;sp+=p.patch});avgMajor=Math.round(sm/uniqueVersions.size);avgMinor=Math.round(smi/uniqueVersions.size);avgPatch=Math.round(sp/uniqueVersions.size)}const envsNoVersion=envs.filter(e=>envVersionCounts[e.id]?.count===0);return{totalVersions,uniqueVersions:uniqueVersions.size,envCount:envs.length,avgVersion:`${avgMajor}.${avgMinor}.${avgPatch}`,majorCounts,envVersionCounts,projectVersionMatrix,oldVersions,projectsNoVersion,envsNoVersion,environments:envs}},render(){const d=this.getData();const c=document.getElementById('versionAnalysisContainer');if(!c)return;let h='<div class="dashboard-kpi-row">';h+=this.kpiCard('🔢',d.totalVersions,'کل نسخهها');h+=this.kpiCard('📦',d.uniqueVersions,'نسخههای یکتا');h+=this.kpiCard('🌍',d.envCount,'محیطها');h+=this.kpiCard('📈',d.avgVersion,'میانگین نسخه');h+='</div><div class="dashboard-charts-grid">';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 توزیع نسخهها بر اساس Major</div><div class="dashboard-chart-body" id="verMajorChart"></div></div>';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">🌍 نسخهها به تفکیک محیط</div><div class="dashboard-chart-body" id="verEnvChart"></div></div>';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">📋 مقایسه نسخهها بین محیطها</div><div class="dashboard-chart-body" id="verMatrixTable" style="align-items:flex-start;overflow-y:auto;max-height:280px;width:100%;"></div></div>';h+='<div class="dashboard-chart-card"><div class="dashboard-chart-header">⚠️ لیست ریسکها و هشدارها</div><div class="dashboard-chart-body" id="verRiskList" style="align-items:flex-start;overflow-y:auto;max-height:280px;width:100%;"></div></div>';h+='</div>';c.innerHTML=h;this.renderMajorBarChart('verMajorChart',d);this.renderEnvDonut('verEnvChart',d);this.renderMatrixTable('verMatrixTable',d);this.renderRiskList('verRiskList',d);document.getElementById('verAnalysisFooterStats').innerHTML=`<span>🔢 کل نسخهها: <span class="stat-highlight">${d.totalVersions}</span></span><span>📦 یکتا: <span class="stat-highlight">${d.uniqueVersions}</span></span><span>📈 میانگین: <span class="stat-highlight">${d.avgVersion}</span></span><span>📅 ${new Date().toLocaleTimeString('fa-IR')}</span>`;document.getElementById('verAnalysisExportPNG').onclick=()=>this.exportPNG();document.getElementById('verAnalysisRefresh').onclick=()=>{this.render();showToast('تحلیل بهروزرسانی شد','success',2000)}},kpiCard(icon,value,label){return`<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">${icon}</div><div class="dashboard-kpi-value">${value}</div><div class="dashboard-kpi-label">${label}</div></div>`},renderMajorBarChart(cid,data){const c=document.getElementById(cid);if(!c)return;c.innerHTML='';const entries=Object.entries(data.majorCounts);if(entries.length===0){c.innerHTML='<div class="dashboard-empty-chart"><span>📊</span>دادهای نیست</div>';return}const w=300,h=200,m={top:10,right:10,bottom:40,left:30};const iw=w-m.left-m.right,ih=h-m.top-m.bottom;const svg=d3.select(c).append('svg').attr('width',w).attr('height',h).append('g').attr('transform',`translate(${m.left},${m.top})`);const x=d3.scaleBand().domain(entries.map(d=>d[0])).range([0,iw]).padding(.3);const y=d3.scaleLinear().domain([0,d3.max(entries,d=>d[1])||1]).nice().range([ih,0]);svg.append('g').attr('transform',`translate(0,${ih})`).call(d3.axisBottom(x).tickSize(0)).selectAll('text').attr('fill','var(--text-muted)').attr('font-size','9px').attr('font-family','Vazirmatn');svg.append('g').call(d3.axisLeft(y).ticks(4).tickSize(-iw)).selectAll('text').attr('fill','var(--text-muted)').attr('font-size','9px');svg.selectAll('.grid-line').data(y.ticks(4)).join('line').attr('x1',0).attr('x2',iw).attr('y1',d=>y(d)).attr('y2',d=>y(d)).attr('stroke','var(--border-light)').attr('stroke-dasharray','3,3');const colors=['#4299e1','#48bb78','#ecc94b','#ed64a6','#9f7aea','#ed8936'];svg.selectAll('rect').data(entries).join('rect').attr('x',d=>x(d[0])).attr('y',d=>y(d[1])).attr('width',x.bandwidth()).attr('height',d=>ih-y(d[1])).attr('fill',(d,i)=>colors[i%colors.length]).attr('rx',4).style('cursor','pointer').on('mouseover',function(e,d){d3.select(this).attr('fill','#c9a961');showD3Tooltip(e,`${d[0]}: ${d[1]} نسخه`)}).on('mousemove',e=>moveD3Tooltip(e)).on('mouseout',function(e,d){d3.select(this).attr('fill',colors[entries.indexOf(d)%colors.length]);hideD3Tooltip()});svg.selectAll('.bar-label').data(entries).join('text').attr('x',d=>x(d[0])+x.bandwidth()/2).attr('y',d=>y(d[1])-5).attr('text-anchor','middle').attr('fill','var(--text)').attr('font-size','10px').attr('font-weight','700').text(d=>d[1])},renderEnvDonut(cid,data){const c=document.getElementById(cid);if(!c)return;c.innerHTML='';const envData=Object.values(data.envVersionCounts).filter(e=>e.count>0);if(envData.length===0){c.innerHTML='<div class="dashboard-empty-chart"><span>🌍</span>دادهای نیست</div>';return}const w=260,h=200,r=Math.min(w,h)/2-10;const svg=d3.select(c).append('svg').attr('width',w).attr('height',h).append('g').attr('transform',`translate(${w/2},${h/2})`);const pie=d3.pie().value(d=>d.count).sort(null);const arc=d3.arc().innerRadius(r*.55).outerRadius(r);const arcH=d3.arc().innerRadius(r*.55).outerRadius(r+6);const total=envData.reduce((s,e)=>s+e.count,0);const paths=svg.selectAll('path').data(pie(envData)).join('path').attr('d',arc).attr('fill',d=>d.data.color||'#38a169').attr('stroke','white').attr('stroke-width',2).style('cursor','pointer');paths.on('mouseover',function(e,d){d3.select(this).transition().duration(150).attr('d',arcH(d));showD3Tooltip(e,`${d.data.name}: ${d.data.count} نسخه (${Math.round(d.data.count/total*100)}%)`)}).on('mousemove',e=>moveD3Tooltip(e)).on('mouseout',function(){d3.select(this).transition().duration(150).attr('d',arc);hideD3Tooltip()});svg.append('text').attr('text-anchor','middle').attr('dy','-.2em').attr('fill','var(--text)').attr('font-size','22px').attr('font-weight','800').text(total);svg.append('text').attr('text-anchor','middle').attr('dy','1.2em').attr('fill','var(--text-muted)').attr('font-size','10px').text('نسخه')},renderMatrixTable(cid,data){const c=document.getElementById(cid);if(!c)return;if(data.projectVersionMatrix.length===0){c.innerHTML='<div class="dashboard-empty-chart"><span>📋</span>دادهای نیست</div>';return}const envs=data.environments;let html='<table style="width:100%;border-collapse:collapse;font-size:11px;">';html+='<thead><tr style="background:var(--border-light);">';html+='<th style="padding:8px;text-align:right;border-bottom:2px solid var(--border);font-weight:700;">پروژه</th>';envs.forEach(env=>{html+=`<th style="padding:8px;text-align:center;border-bottom:2px solid var(--border);font-weight:700;"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${env.backgroundColor};margin-left:4px;"></span>${env.name}</th>`});html+='</tr></thead><tbody>';data.projectVersionMatrix.forEach(row=>{html+='<tr style="border-bottom:1px solid var(--border-light);">';html+=`<td style="padding:6px 8px;font-weight:600;">${row.projectTitle}<br><small style="color:var(--text-muted);font-weight:400;">${row.productName}</small></td>`;envs.forEach(env=>{const cell=row.envs[env.id];if(cell){const dateStr=cell.date?new Date(cell.date).toLocaleDateString('fa-IR'):'';html+=`<td style="padding:6px 8px;text-align:center;"><span style="font-family:monospace;font-weight:700;color:var(--primary);">v${cell.version}</span><br><small style="color:var(--text-muted);">${dateStr}</small></td>`}else{html+='<td style="padding:6px 8px;text-align:center;color:var(--text-muted);">—</td>'}});html+='</tr>'});html+='</tbody></table>';c.innerHTML=html},renderRiskList(cid,data){const c=document.getElementById(cid);if(!c)return;let h='';if(data.projectsNoVersion.length>0){h+='<div style="margin-bottom:12px;width:100%"><div style="font-size:12px;font-weight:700;color:var(--danger);margin-bottom:8px;">❌ پروژههای بدون نسخه</div>';data.projectsNoVersion.forEach(p=>{h+=`<div style="font-size:11px;padding:6px 8px;background:rgba(229,62,62,.1);border-radius:4px;margin-bottom:4px;border-right:3px solid var(--danger);">${p.title} <small style="color:var(--text-muted);">(${p.productName})</small></div>`});h+='</div>'}if(data.oldVersions.length>0){h+='<div style="margin-bottom:12px;width:100%"><div style="font-size:12px;font-weight:700;color:var(--warning);margin-bottom:8px;">⏰ نسخههای قدیمی (بیش از ۳۰ روز)</div>';data.oldVersions.forEach(ov=>{const envName=data.environments.find(e=>e.id===ov.environmentId)?.name||ov.environmentId;h+=`<div style="font-size:11px;padding:6px 8px;background:rgba(221,107,32,.1);border-radius:4px;margin-bottom:4px;border-right:3px solid var(--warning);">${ov.projectTitle} - v${ov.version} در ${envName} <small style="color:var(--text-muted);">(${ov.daysOld} روز پیش)</small></div>`});h+='</div>'}if(data.envsNoVersion.length>0){h+='<div style="margin-bottom:12px;width:100%"><div style="font-size:12px;font-weight:700;color:var(--info);margin-bottom:8px;">ℹ️ محیطهای بدون نسخه</div>';data.envsNoVersion.forEach(env=>{h+=`<div style="font-size:11px;padding:6px 8px;background:rgba(49,130,206,.1);border-radius:4px;margin-bottom:4px;border-right:3px solid var(--info);">${env.name}: هیچ نسخهای ندارد</div>`});h+='</div>'}if(!h)h='<div class="dashboard-empty-chart"><span>✅</span>هیچ ریسکی شناسایی نشد</div>';c.innerHTML=h},exportPNG(){showToast('در حال آمادهسازی خروجی...','info',2000);const mb=document.getElementById('versionAnalysisModalBody');if(!mb){showToast('داشبوردی نیست','error');return}const svgs=mb.querySelectorAll('svg');if(svgs.length===0){showToast('نموداری نیست','warning');return}const cv=document.createElement('canvas');const sc=2;cv.width=1200*sc;cv.height=800*sc;const cx=cv.getContext('2d');const isDark=document.documentElement.getAttribute('data-theme')==='dark';cx.fillStyle=isDark?'#1a202c':'#f5f7fa';cx.fillRect(0,0,cv.width,cv.height);cx.fillStyle=isDark?'#e0c789':'#1a365d';cx.font='bold 24px Vazirmatn';cx.textAlign='center';cx.fillText('🔢 تحلیل نسخه',cv.width/2,40*sc);let ld=0;const total=svgs.length;const pos=[{x:50,y:80},{x:650,y:80}];svgs.forEach((se,i)=>{const sd=new XMLSerializer().serializeToString(se);const b=new Blob([sd],{type:'image/svg+xml;charset=utf-8'});const u=URL.createObjectURL(b);const img=new Image();img.onload=()=>{const p=pos[i]||{x:50,y:80+i*200};cx.drawImage(img,p.x*sc,p.y*sc,img.width*sc*.9,img.height*sc*.9);URL.revokeObjectURL(u);ld++;if(ld===total){const a=document.createElement('a');a.download=`version-analysis_${new Date().toISOString().replace(/[:.]/g,'-').slice(0,19)}.png`;a.href=cv.toDataURL('image/png');a.click();showToast('📷 خروجی ذخیره شد','success');WebhookManager.sendEvent('version_analysis_exported',{format:'png'})}};img.onerror=()=>{ld++;if(ld===total)showToast('خطا در خروجی','error')};img.src=u})}};
|
||
const ProductManager={currentDetailProductId:null,currentEditProjectId:null,currentDependencyProjectId:null,currentDependencyProjId:null,currentDeploymentProjId:null,currentDeploymentMode:null,currentDeploymentEnvId:null,getTypeInfo(t){return VALID_PROJECT_TYPES.find(x=>x.value===t)||{value:t,label:t,icon:'❓'}},getEnvironmentName(eid){const e=projectData.appData.environments.find(x=>x.id===eid);return e?e.name:eid},getEnvironmentColors(eid){const e=projectData.appData.environments.find(x=>x.id===eid);return{backgroundColor:e?.backgroundColor||'#38a169',textColor:e?.textColor||'#ffffff'}},getEmptyEnvironmentIds(proj){const all=projectData.appData.environments.map(e=>e.id);const dep=(proj.lastDeployedVersions||[]).map(d=>d.environmentId);return all.filter(id=>!dep.includes(id))},normalizeDependency(dep){if(typeof dep==='string')return{projectId:dep,environmentId:null,version:null};return dep},findProjectById(pid){for(const prod of projectData.appData.products){const pr=prod.projects.find(p=>p.id===pid);if(pr)return pr}return null},showDetail(pid){const prod=projectData.appData.products.find(p=>p.id===pid);if(!prod)return;this.currentDetailProductId=pid;document.getElementById('projectDetailTitle').innerHTML=`<span>📦</span><span>${prod.title}</span>`;const projects=prod.projects||[];const tdp=projects.reduce((s,pr)=>s+(pr.lastDeployedVersions||[]).length,0);let h=`<div class="detail-section"><div class="detail-section-header"><span>ℹ️ اطلاعات محصول</span><div class="detail-section-header-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.openProductEditModal()">✏️ ویرایش</button></div></div><div class="detail-section-body"><div class="detail-info-grid"><div class="detail-info-item"><span class="detail-info-label">عنوان</span><span class="detail-info-value">${prod.title}</span></div><div class="detail-info-item"><span class="detail-info-label">توضیحات</span><span class="detail-info-value">${prod.description||'—'}</span></div><div class="detail-info-item"><span class="detail-info-label">تاریخ ایجاد</span><span class="detail-info-value">${prod.metadata?.createdAt?new Date(prod.metadata.createdAt).toLocaleDateString('fa-IR'):'—'}</span></div><div class="detail-info-item"><span class="detail-info-label">مالک</span><span class="detail-info-value">${prod.metadata?.owner||'—'}</span></div><div class="detail-info-item"><span class="detail-info-label">تعداد پروژه</span><span class="detail-info-value">${projects.length}</span></div><div class="detail-info-item"><span class="detail-info-label">استقرارها</span><span class="detail-info-value">${tdp}</span></div></div></div></div><div class="detail-section"><div class="detail-section-header"><span>🌿 پروژهها (${projects.length})</span><div class="detail-section-header-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.openAddProjectModal()">➕ افزودن</button></div></div><div class="detail-section-body">`;if(projects.length===0)h+='<div class="empty-subprojects">🌿 هیچ پروژهای نیست</div>';else{h+='<div class="subproject-list">';projects.forEach(pr=>{const ti=this.getTypeInfo(pr.type);const deps=pr.dependencies||[];const dp=pr.lastDeployedVersions||[];const eei=this.getEmptyEnvironmentIds(pr);h+=`<div class="subproject-card"><div class="subproject-header"><div class="subproject-title"><span>${ti.icon}</span><span>${pr.title}</span><span class="type-badge type-${pr.type}">${ti.label}</span></div><div class="subproject-actions"><button class="btn btn-ghost btn-sm" onclick="ProductManager.openProjectEditModal('${prod.id}','${pr.id}')">✏️</button><button class="btn btn-ghost btn-sm" onclick="ProductManager.openDependencyManager('${prod.id}','${pr.id}')">🔗</button><button class="btn btn-danger btn-sm" onclick="ProductManager.removeProject('${prod.id}','${pr.id}')">🗑️</button></div></div><div class="subproject-meta">${pr.description||''}</div><div class="detail-info-grid" style="margin-bottom:8px;">${pr.metadata?.framework?`<div class="detail-info-item"><span class="detail-info-label">فریمورک</span><span class="detail-info-value">${pr.metadata.framework}</span></div>`:''}${pr.metadata?.language?`<div class="detail-info-item"><span class="detail-info-label">زبان</span><span class="detail-info-value">${pr.metadata.language}</span></div>`:''}${deps.length>0?`<div class="detail-info-item"><span class="detail-info-label">وابستگیها (${deps.length})</span><span class="detail-info-value">${deps.map(d=>{const n=this.normalizeDependency(d);const dp2=this.findProjectById(n.projectId);return dp2?dp2.title:n.projectId}).join('، ')}</span></div>`:''}</div>`;h+=`<div class="deployment-section"><div class="deployment-header"><span class="deployment-header-title">🚀 استقرارها (${dp.length})</span>${eei.length>0?`<button class="btn btn-accent btn-xs" onclick="ProductManager.openAddDeploymentModal('${prod.id}','${pr.id}')">➕ افزودن</button>`:'<span style="font-size:10px;color:var(--text-muted);">✓ کامل</span>'}</div>`;if(dp.length===0)h+='<div class="empty-deployments">هنوز استقرار ندارد</div>';else{h+='<div class="deployment-list">';dp.forEach(d=>{const en=this.getEnvironmentName(d.environmentId);const cl=this.getEnvironmentColors(d.environmentId);const fd=d.deploymentDate?new Date(d.deploymentDate).toLocaleString('fa-IR'):'—';h+=`<div class="deployment-item"><div class="deployment-item-info"><span class="deployment-env-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor};">${en}</span><span class="deployment-version">v${d.version}</span><span class="deployment-date">📅 ${fd}</span></div><div class="deployment-actions"><button class="btn btn-ghost btn-xs" onclick="ProductManager.openEditDeploymentModal('${prod.id}','${pr.id}','${d.environmentId}','${d.version}','${d.deploymentDate}')">✏️</button><button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.environmentId}','${d.version}')">🗑️</button></div></div>`});h+='</div>'}h+='</div></div>'});h+='</div>'}h+='</div></div>';document.getElementById('projectDetailBody').innerHTML=h;ModalManager.open('projectDetailModal')},openAddProductModal(){document.getElementById('newProductTitle').value='';document.getElementById('newProductDescription').value='';document.getElementById('newProductOwner').value='';document.getElementById('newProductCreatedAt').value=new Date().toLocaleDateString('fa-IR');this.clearProductFormErrors();ModalManager.open('addProductModal');setTimeout(()=>{document.getElementById('newProductTitle').focus()},100)},clearProductFormErrors(){const ti=document.getElementById('newProductTitle'),te=document.getElementById('newProductTitleError');if(ti)ti.classList.remove('error');if(te)te.classList.remove('visible')},createNewProduct(){const t=document.getElementById('newProductTitle').value.trim();if(!t||t.length<3){showToast('عنوان محصول الزامی است (حداقل ۳ کاراکتر)','error');document.getElementById('newProductTitle').classList.add('error');document.getElementById('newProductTitleError').classList.add('visible');return}if(projectData.appData.products.some(p=>p.title.toLowerCase()===t.toLowerCase())){showToast('محصولی با این عنوان وجود دارد','warning');return}HistoryManager.pushState();projectData.appData.products.push({id:`prod-${Date.now()}`,title:t,description:document.getElementById('newProductDescription').value.trim()||'بدون توضیحات',metadata:{createdAt:new Date().toISOString(),owner:document.getElementById('newProductOwner').value.trim()||'',customFields:{}},projects:[]});ModalManager.close('addProductModal');ToolbarManager.updateStats();BoardRenderer.render();showToast(`محصول "${t}" ایجاد شد`,'success');WebhookManager.sendEvent('product_created',{title:t})},openProductEditModal(){const prod=projectData.appData.products.find(p=>p.id===this.currentDetailProductId);if(!prod)return;document.getElementById('editProductId').value=prod.id;document.getElementById('editProductTitle').value=prod.title;document.getElementById('editProductDescription').value=prod.description||'';document.getElementById('editProductOwner').value=prod.metadata?.owner||'';document.getElementById('editProductCreatedAt').value=prod.metadata?.createdAt?new Date(prod.metadata.createdAt).toLocaleDateString('fa-IR'):'—';ModalManager.open('productEditModal')},saveProductEdit(){const prod=projectData.appData.products.find(p=>p.id===this.currentDetailProductId);if(!prod)return;const nt=document.getElementById('editProductTitle').value.trim();if(!nt){showToast('عنوان الزامی است','error');return}HistoryManager.pushState();prod.title=nt;prod.description=document.getElementById('editProductDescription').value.trim();if(!prod.metadata)prod.metadata={};prod.metadata.owner=document.getElementById('editProductOwner').value.trim();prod.metadata.updatedAt=new Date().toISOString();ModalManager.close('productEditModal');BoardRenderer.render();this.showDetail(this.currentDetailProductId);showToast(`محصول "${prod.title}" ویرایش شد`,'success');WebhookManager.sendEvent('product_edited',{productId:this.currentDetailProductId,title:prod.title})},openAddProjectModal(){if(!this.currentDetailProductId){showToast('ابتدا یک محصول انتخاب کنید','warning');return}document.getElementById('newProjectTitle').value='';document.getElementById('newProjectType').value='';document.getElementById('newProjectFramework').value='';document.getElementById('newProjectLanguage').value='';document.getElementById('newProjectRepo').value='';document.getElementById('newProjectDescription').value='';populateProjectTypeDropdowns();ModalManager.open('addProjectModal')},saveNewProject(){const prod=projectData.appData.products.find(p=>p.id===this.currentDetailProductId);if(!prod)return;const t=document.getElementById('newProjectTitle').value.trim();const ty=document.getElementById('newProjectType').value;if(!t){showToast('عنوان پروژه الزامی است','error');return}if(!ty){showToast('نوع پروژه الزامی است','error');return}HistoryManager.pushState();const np={id:`proj-${Date.now()}`,title:t,description:document.getElementById('newProjectDescription').value.trim()||'بدون توضیحات',type:ty,repositoryUrl:document.getElementById('newProjectRepo').value.trim(),metadata:{createdAt:new Date().toISOString(),customFields:{}},dependencies:[],lastDeployedVersions:[],deploymentHistory:[]};const fw=document.getElementById('newProjectFramework').value.trim();if(fw)np.metadata.framework=fw;const lg=document.getElementById('newProjectLanguage').value.trim();if(lg)np.metadata.language=lg;prod.projects.push(np);ToolbarManager.updateStats();ModalManager.close('addProjectModal');this.showDetail(this.currentDetailProductId);showToast(`پروژه "${t}" ایجاد شد`,'success');WebhookManager.sendEvent('project_added',{productId:this.currentDetailProductId,type:ty,title:t})},openProjectEditModal(pid,projId){const prod=projectData.appData.products.find(p=>p.id===pid);if(!prod)return;const pr=prod.projects.find(p=>p.id===projId);if(!pr)return;this.currentEditProjectId=projId;populateProjectTypeDropdowns();document.getElementById('editProjId').value=pr.id;document.getElementById('editProjTitle').value=pr.title;document.getElementById('editProjType').value=pr.type;document.getElementById('editProjFramework').value=pr.metadata?.framework||'';document.getElementById('editProjLanguage').value=pr.metadata?.language||'';document.getElementById('editProjRepo').value=pr.repositoryUrl||'';document.getElementById('editProjDescription').value=pr.description||'';ModalManager.open('editProjectModal')},saveProjectEdit(){const prod=projectData.appData.products.find(p=>p.id===this.currentDetailProductId);if(!prod)return;const pr=prod.projects.find(p=>p.id===this.currentEditProjectId);if(!pr)return;const nt=document.getElementById('editProjTitle').value.trim();const nty=document.getElementById('editProjType').value;if(!nt){showToast('عنوان الزامی است','error');return}if(!nty){showToast('نوع الزامی است','error');return}HistoryManager.pushState();pr.title=nt;pr.type=nty;pr.description=document.getElementById('editProjDescription').value.trim()||'بدون توضیحات';pr.repositoryUrl=document.getElementById('editProjRepo').value.trim();if(!pr.metadata)pr.metadata={};const fw=document.getElementById('editProjFramework').value.trim();if(fw)pr.metadata.framework=fw;else delete pr.metadata.framework;const lg=document.getElementById('editProjLanguage').value.trim();if(lg)pr.metadata.language=lg;else delete pr.metadata.language;pr.metadata.updatedAt=new Date().toISOString();ModalManager.close('editProjectModal');ToolbarManager.updateStats();this.showDetail(this.currentDetailProductId);showToast(`پروژه "${pr.title}" ویرایش شد`,'success');WebhookManager.sendEvent('project_edited',{projId:this.currentEditProjectId,title:pr.title,type:pr.type})},openDependencyManager(pid,projId){this.currentDependencyProjectId=pid;this.currentDependencyProjId=projId;this.renderCurrentDependencies();this.renderAllProjectsList();this.setupDependencySearch();ModalManager.open('dependencyManagerModal')},renderCurrentDependencies(){const prod=projectData.appData.products.find(p=>p.id===this.currentDependencyProjectId);if(!prod)return;const pr=prod.projects.find(p=>p.id===this.currentDependencyProjId);if(!pr)return;const c=document.getElementById('currentDependenciesList');const deps=pr.dependencies||[];if(deps.length===0){c.innerHTML='<div class="empty-dependencies">هیچ وابستگیای نیست</div>';return}c.innerHTML=deps.map((dep,i)=>{const n=this.normalizeDependency(dep);const dp=this.findProjectById(n.projectId);const pn=dp?dp.title:n.projectId;const en=n.environmentId?this.getEnvironmentName(n.environmentId):'—';const v=n.version||'—';return`<div class="dependency-item"><div class="dependency-info"><div><div class="dependency-project-name">${pn}</div><div class="dependency-version-info"><span class="deployment-env-tag" style="background-color:${this.getEnvironmentColors(n.environmentId).backgroundColor};color:${this.getEnvironmentColors(n.environmentId).textColor};font-size:9px;padding:1px 6px;">${en}</span> <span style="font-family:monospace;font-weight:600;">v${v}</span></div></div></div><button class="btn btn-danger btn-xs" onclick="ProductManager.removeDependency(${i})">🗑️</button></div>`}).join('')},removeDependency(i){const prod=projectData.appData.products.find(p=>p.id===this.currentDependencyProjectId);if(!prod)return;const pr=prod.projects.find(p=>p.id===this.currentDependencyProjId);if(!pr)return;if(!confirm('حذف این وابستگی؟'))return;HistoryManager.pushState();pr.dependencies.splice(i,1);ModalManager.close('dependencyManagerModal');if(this.currentDetailProductId)this.showDetail(this.currentDetailProductId);ToolbarManager.updateStats();showToast('وابستگی حذف شد','success');WebhookManager.sendEvent('dependency_removed',{projectId:this.currentDependencyProjId})},renderAllProjectsList(){const rc=document.getElementById('dependencySearchResults');const cp=projectData.appData.products.find(p=>p.id===this.currentDependencyProjectId);if(!cp)return;const cpr=cp.projects.find(p=>p.id===this.currentDependencyProjId);if(!cpr)return;const ap=[];projectData.appData.products.forEach(prod=>{prod.projects.forEach(pr=>{if(pr.id!==cpr.id)ap.push({...pr,productName:prod.title})})});if(ap.length===0){rc.innerHTML='<div class="empty-dependencies">پروژه دیگری نیست</div>';return}rc.innerHTML=ap.map(pr=>{const vs=this.getAvailableVersions(pr);return`<div class="dependency-project-result"><div class="dependency-project-header"><span>📦 ${pr.title} <small style="color:var(--text-muted);font-weight:normal;">(${pr.productName})</small></span><span style="font-size:11px;color:var(--text-muted);">${vs.length} نسخه</span></div><div class="dependency-versions-list">${vs.length>0?vs.map(v=>`<div class="dependency-version-item" onclick="ProductManager.addDependency('${pr.id}','${v.environmentId}','${v.version}')"><div><span class="dependency-version-badge ${v.source==='history'?'history':''}">${v.source==='history'?'📜':'🚀'}</span> <span style="font-family:monospace;font-weight:600;">v${v.version}</span> <span style="font-size:11px;color:var(--text-muted);">${this.getEnvironmentName(v.environmentId)}</span></div><span style="font-size:10px;color:var(--text-muted);">${new Date(v.deploymentDate).toLocaleDateString('fa-IR')}</span></div>`).join(''):'<div style="padding:8px;text-align:center;color:var(--text-muted);font-size:11px;">نسخهای نیست</div>'}</div></div>`}).join('')},setupDependencySearch(){const si=document.getElementById('dependencySearch');si.value='';si.oninput=e=>{const q=e.target.value.trim().toLowerCase();if(q.length===0){this.renderAllProjectsList();return}if(q.length<2){document.getElementById('dependencySearchResults').innerHTML='<div class="empty-dependencies">حداقل ۲ کاراکتر</div>';return}const cp=projectData.appData.products.find(p=>p.id===this.currentDependencyProjectId);if(!cp)return;const cpr=cp.projects.find(p=>p.id===this.currentDependencyProjId);if(!cpr)return;const ap=[];projectData.appData.products.forEach(prod=>{prod.projects.forEach(pr=>{if(pr.id!==cpr.id)ap.push({...pr,productName:prod.title})})});const fl=ap.filter(pr=>pr.title.toLowerCase().includes(q)||pr.productName.toLowerCase().includes(q)||(pr.description||'').toLowerCase().includes(q));if(fl.length===0){document.getElementById('dependencySearchResults').innerHTML='<div class="empty-dependencies">نتیجهای نیست</div>';return}document.getElementById('dependencySearchResults').innerHTML=fl.map(pr=>{const vs=this.getAvailableVersions(pr);return`<div class="dependency-project-result"><div class="dependency-project-header"><span>📦 ${pr.title} <small style="color:var(--text-muted);font-weight:normal;">(${pr.productName})</small></span><span style="font-size:11px;color:var(--text-muted);">${vs.length} نسخه</span></div><div class="dependency-versions-list">${vs.length>0?vs.map(v=>`<div class="dependency-version-item" onclick="ProductManager.addDependency('${pr.id}','${v.environmentId}','${v.version}')"><div><span class="dependency-version-badge ${v.source==='history'?'history':''}">${v.source==='history'?'📜':'🚀'}</span> <span style="font-family:monospace;font-weight:600;">v${v.version}</span> <span style="font-size:11px;color:var(--text-muted);">${this.getEnvironmentName(v.environmentId)}</span></div><span style="font-size:10px;color:var(--text-muted);">${new Date(v.deploymentDate).toLocaleDateString('fa-IR')}</span></div>`).join(''):'<div style="padding:8px;text-align:center;color:var(--text-muted);font-size:11px;">نسخهای نیست</div>'}</div></div>`}).join('')}},getAvailableVersions(pr){const vs=[];(pr.lastDeployedVersions||[]).forEach(v=>{vs.push({environmentId:v.environmentId,version:v.version,deploymentDate:v.deploymentDate,source:'current'})});(pr.deploymentHistory||[]).forEach(v=>{vs.push({environmentId:v.environmentId,version:v.version,deploymentDate:v.deploymentDate||v.replacedAt,source:'history'})});return vs},addDependency(pid,eid,ver){const prod=projectData.appData.products.find(p=>p.id===this.currentDependencyProjectId);if(!prod)return;const pr=prod.projects.find(p=>p.id===this.currentDependencyProjId);if(!pr)return;const ex=(pr.dependencies||[]).some(dep=>{const n=this.normalizeDependency(dep);return n.projectId===pid&&n.environmentId===eid&&n.version===ver});if(ex){showToast('تکراری است','warning');return}HistoryManager.pushState();if(!pr.dependencies)pr.dependencies=[];pr.dependencies.push({projectId:pid,environmentId:eid,version:ver});ModalManager.close('dependencyManagerModal');if(this.currentDetailProductId)this.showDetail(this.currentDetailProductId);ToolbarManager.updateStats();showToast('وابستگی اضافه شد','success');WebhookManager.sendEvent('dependency_added',{projectId:this.currentDependencyProjId,dependencyProjectId:pid})},openAddDeploymentModal(pid,projId){const prod=projectData.appData.products.find(p=>p.id===pid);if(!prod)return;const pr=prod.projects.find(p=>p.id===projId);if(!pr)return;const eei=this.getEmptyEnvironmentIds(pr);if(eei.length===0){showToast('همه محیطها استقرار دارند','warning');return}this.currentDeploymentProjId=projId;this.currentDeploymentMode='add';this.currentDeploymentEnvId=null;document.getElementById('deploymentModalTitle').innerHTML='<span>🚀</span><span>افزودن نسخه استقرار</span>';const envs=projectData.appData.environments.filter(e=>eei.includes(e.id));document.getElementById('deploymentEnvGroup').innerHTML=`<label class="form-label">محیط استقرار <span class="required">*</span></label><select class="form-select" id="depEnvironment"><option value="">انتخاب...</option>${envs.map(e=>`<option value="${e.id}" style="background-color:${e.backgroundColor||'#38a169'};color:${e.textColor||'#ffffff'};">${e.name} - ${e.description}</option>`).join('')}</select>`;document.getElementById('depVersion').value='';document.getElementById('depDate').value='';ModalManager.open('deploymentModal')},openEditDeploymentModal(pid,projId,eid,ver,date){const prod=projectData.appData.products.find(p=>p.id===pid);if(!prod)return;const pr=prod.projects.find(p=>p.id===projId);if(!pr)return;this.currentDeploymentProjId=projId;this.currentDeploymentMode='edit';this.currentDeploymentEnvId=eid;document.getElementById('deploymentModalTitle').innerHTML='<span>✏️</span><span>ویرایش نسخه استقرار</span>';const cl=this.getEnvironmentColors(eid);document.getElementById('deploymentEnvGroup').innerHTML=`<label class="form-label">محیط استقرار <span class="readonly-badge">فقط خواندنی</span></label><div class="env-read-only-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor};">${this.getEnvironmentName(eid)}</div>`;document.getElementById('depVersion').value=ver;let dv='';if(date){const d=new Date(date);const pad=n=>n.toString().padStart(2,'0');dv=`${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`}document.getElementById('depDate').value=dv;ModalManager.open('deploymentModal')},saveDeployment(){const prod=projectData.appData.products.find(p=>p.id===this.currentDetailProductId);if(!prod)return;const pr=prod.projects.find(p=>p.id===this.currentDeploymentProjId);if(!pr)return;const ver=document.getElementById('depVersion').value.trim();const date=document.getElementById('depDate').value;if(!ver){showToast('نسخه الزامی است','error');return}if(!date){showToast('تاریخ الزامی است','error');return}const dd=new Date(date).toISOString();HistoryManager.pushState();if(!pr.deploymentHistory)pr.deploymentHistory=[];if(this.currentDeploymentMode==='add'){const eid=document.getElementById('depEnvironment').value;if(!eid){showToast('محیط را انتخاب کنید','error');return}const ei=pr.lastDeployedVersions.findIndex(d=>d.environmentId===eid);if(ei>=0){const old=pr.lastDeployedVersions[ei];pr.deploymentHistory.push({...old,replacedAt:dd,replacedByVersion:ver});pr.lastDeployedVersions[ei]={environmentId:eid,version:ver,deploymentDate:dd};showToast(`v${old.version} → v${ver} در ${this.getEnvironmentName(eid)}`,'success')}else{pr.lastDeployedVersions.push({environmentId:eid,version:ver,deploymentDate:dd});showToast(`v${ver} در ${this.getEnvironmentName(eid)} استقرار یافت`,'success')}}else{const eid=this.currentDeploymentEnvId;const ci=pr.lastDeployedVersions.findIndex(d=>d.environmentId===eid);if(ci>=0){const old=pr.lastDeployedVersions[ci];if(old.version!==ver)pr.deploymentHistory.push({...old,replacedAt:dd,replacedByVersion:ver});pr.lastDeployedVersions[ci]={environmentId:eid,version:ver,deploymentDate:dd};showToast(`v${ver} در ${this.getEnvironmentName(eid)} بهروز شد`,'success')}}ModalManager.close('deploymentModal');ToolbarManager.updateStats();this.showDetail(this.currentDetailProductId)},removeDeployment(pid,projId,eid,ver){if(!confirm(`حذف v${ver} از ${this.getEnvironmentName(eid)}؟`))return;const prod=projectData.appData.products.find(p=>p.id===pid);if(!prod)return;const pr=prod.projects.find(p=>p.id===projId);if(!pr)return;HistoryManager.pushState();if(!pr.deploymentHistory)pr.deploymentHistory=[];const di=pr.lastDeployedVersions.findIndex(d=>d.environmentId===eid&&d.version===ver);if(di>=0){pr.deploymentHistory.push({...pr.lastDeployedVersions[di],removedAt:new Date().toISOString(),status:'removed'});pr.lastDeployedVersions.splice(di,1)}ToolbarManager.updateStats();this.showDetail(pid);showToast(`v${ver} حذف شد`,'warning')},removeProduct(pid){const prod=projectData.appData.products.find(p=>p.id===pid);if(!prod)return;if(!confirm(`حذف "${prod.title}" و تمام پروژهها؟`))return;HistoryManager.pushState();projectData.appData.products=projectData.appData.products.filter(p=>p.id!==pid);ToolbarManager.updateStats();BoardRenderer.render();ModalManager.close('projectDetailModal');showToast(`"${prod.title}" حذف شد`,'warning');WebhookManager.sendEvent('product_removed',{productId:pid})},removeProject(pid,projId){const prod=projectData.appData.products.find(p=>p.id===pid);if(!prod)return;const pr=prod.projects.find(p=>p.id===projId);if(!pr)return;if(!confirm(`حذف "${pr.title}"؟`))return;HistoryManager.pushState();prod.projects=prod.projects.filter(p=>p.id!==projId);ToolbarManager.updateStats();if(this.currentDetailProductId===pid)this.showDetail(pid);showToast(`"${pr.title}" حذف شد`,'warning');WebhookManager.sendEvent('project_removed',{projectId:pid,projId})}};
|
||
document.getElementById('projectDetailDeleteBtn').addEventListener('click',()=>{if(ProductManager.currentDetailProductId)ProductManager.removeProduct(ProductManager.currentDetailProductId)});
|
||
document.getElementById('saveProductEditBtn').addEventListener('click',()=>ProductManager.saveProductEdit());
|
||
document.getElementById('saveNewProjectBtn').addEventListener('click',()=>ProductManager.saveNewProject());
|
||
document.getElementById('saveProjEditBtn').addEventListener('click',()=>ProductManager.saveProjectEdit());
|
||
document.getElementById('saveDeploymentBtn').addEventListener('click',()=>ProductManager.saveDeployment());
|
||
document.getElementById('saveEnvEditBtn').addEventListener('click',()=>EnvironmentManager.saveEdit());
|
||
document.getElementById('saveNewProductBtn').addEventListener('click',()=>ProductManager.createNewProduct());
|
||
document.getElementById('saveNewEnvBtn').addEventListener('click',()=>EnvironmentManager.saveNewEnv());
|
||
document.getElementById('newEnvBgColor').addEventListener('input',e=>{document.getElementById('newEnvBgColorText').textContent=e.target.value});
|
||
document.getElementById('newEnvTextColor').addEventListener('input',e=>{document.getElementById('newEnvTextColorText').textContent=e.target.value});
|
||
document.getElementById('editEnvBgColor').addEventListener('input',e=>{document.getElementById('editEnvBgColorText').textContent=e.target.value;EnvironmentManager.updateEditPreview()});
|
||
document.getElementById('editEnvTextColor').addEventListener('input',e=>{document.getElementById('editEnvTextColorText').textContent=e.target.value;EnvironmentManager.updateEditPreview()});
|
||
document.getElementById('newProductTitle').addEventListener('input',e=>{if(e.target.value.trim().length>=3){e.target.classList.remove('error');document.getElementById('newProductTitleError').classList.remove('visible')}});
|
||
function handleToolSelect(tool,group){const ah=ActionRegistry[tool.action]||ActionRegistry.defaultAction;ah(tool,group);WebhookManager.sendEvent('tool_selected',{toolId:tool.id,toolTitle:tool.title,toolAction:tool.action,groupId:group.id})}
|
||
const themeToggle=document.getElementById('themeToggle'),themeIcon=document.getElementById('themeIcon'),htmlEl=document.documentElement;
|
||
const savedTheme=localStorage.getItem('theme')||'light';
|
||
htmlEl.setAttribute('data-theme',savedTheme);
|
||
if(themeIcon)themeIcon.textContent=savedTheme==='dark'?'☀️':'🌙';
|
||
if(themeToggle)themeToggle.addEventListener('click',()=>{const c=htmlEl.getAttribute('data-theme');const n=c==='dark'?'light':'dark';htmlEl.setAttribute('data-theme',n);localStorage.setItem('theme',n);if(themeIcon)themeIcon.textContent=n==='dark'?'☀️':'🌙'});
|
||
const sidebarToggle=document.getElementById('sidebarToggle'),sidebar=document.getElementById('sidebar'),sidebarIcon=document.getElementById('sidebarIcon');
|
||
function updateSidebarIcon(){if(!sidebarIcon)return;sidebarIcon.textContent=sidebar.classList.contains('collapsed')?'☰':'✕'}
|
||
if(sidebarToggle)sidebarToggle.addEventListener('click',()=>{sidebar.classList.toggle('collapsed');updateSidebarIcon()});
|
||
updateSidebarIcon();
|
||
const sidebarBody=document.getElementById('sidebarBody');
|
||
function renderToolGroups(){sidebarBody.innerHTML='';projectData.toolGroups.forEach(group=>{const ge=document.createElement('div');ge.className='sidebar-group';const he=document.createElement('div');he.className='sidebar-group-header';if(group.description)he.setAttribute('data-tooltip',group.description);he.innerHTML=`<div class="group-icon"><span>${group.icon||''}</span><span>${group.title}</span></div><span class="group-arrow">▼</span>`;const be=document.createElement('div');be.className='sidebar-group-body';group.tools.forEach(tool=>{const btn=document.createElement('button');btn.className='sidebar-btn';btn.title=tool.description;btn.innerHTML=`<span class="btn-icon-emoji">${tool.icon||'🔧'}</span><span class="btn-title">${tool.title}</span>`;btn.addEventListener('click',()=>handleToolSelect(tool,group));be.appendChild(btn)});he.addEventListener('click',()=>ge.classList.toggle('collapsed'));ge.appendChild(he);ge.appendChild(be);sidebarBody.appendChild(ge)})}
|
||
function openSettingsModal(){document.getElementById('settingProjectTitle').value=projectData.projectTitle;document.getElementById('settingVersion').value=projectData.version;document.getElementById('settingDescription').value=projectData.description;document.getElementById('settingDeveloper').value=projectData.developer;document.getElementById('settingCompany').value=projectData.company;document.getElementById('settingWebhookUrl').value=projectData.webhook.url;document.getElementById('settingWebhookClientId').value=projectData.webhook.clientId;document.getElementById('settingWebhookEnabled').checked=projectData.webhook.enabled;WebhookManager.updateStatusUI();ModalManager.open('settingsModal')}
|
||
['settingWebhookUrl','settingWebhookClientId','settingWebhookEnabled'].forEach(id=>{document.getElementById(id).addEventListener('input',()=>{WebhookManager.setConfig({url:document.getElementById('settingWebhookUrl').value,clientId:document.getElementById('settingWebhookClientId').value,enabled:document.getElementById('settingWebhookEnabled').checked})})});
|
||
document.getElementById('saveSettingsBtn').addEventListener('click',()=>{WebhookManager.setConfig({url:document.getElementById('settingWebhookUrl').value.trim(),clientId:document.getElementById('settingWebhookClientId').value.trim(),enabled:document.getElementById('settingWebhookEnabled').checked});ModalManager.close('settingsModal');showToast('تنظیمات ذخیره شد','success');WebhookManager.sendEvent('settings_updated',{updatedFields:['webhook']})});
|
||
function updateProjectUI(){document.title=projectData.projectTitle;document.getElementById('headerTitle').textContent=projectData.projectTitle;document.getElementById('headerSubtitle').textContent=projectData.description;document.getElementById('appFooter').innerHTML=`<div><span class="footer-brand">طراح و توسعهدهنده:</span> ${projectData.developer} <span style="margin:0 10px;">|</span> <span class="footer-brand">شرکت:</span> ${projectData.company}</div><div><span class="footer-brand">نسخه:</span> ${projectData.version}</div>`}
|
||
document.addEventListener('keydown',e=>{if(!(e.ctrlKey||e.metaKey))return;const key=e.key.toLowerCase();for(const group of projectData.toolGroups){const tool=group.tools.find(t=>t.shortcut&&t.shortcut.toLowerCase().replace('ctrl+','')===key);if(tool){e.preventDefault();handleToolSelect(tool,group);break}}});
|
||
ModalManager.init();renderToolGroups();updateProjectUI();WebhookManager.updateStatusUI();ToolbarManager.init();SectionCollapseManager.init();BoardRenderer.render();HistoryManager.init();
|
||
setTimeout(()=>showToast(`${projectData.projectTitle} (نسخه ${projectData.version}) آماده است`,'success'),500);
|
||
</script>
|
||
</body>
|
||
</html> |