3012 lines
226 KiB
HTML
3012 lines
226 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
|
||
}
|
||
|
||
[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: 64px;
|
||
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
|
||
}
|
||
|
||
.app-brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px
|
||
}
|
||
|
||
.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)
|
||
}
|
||
|
||
.app-brand-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px
|
||
}
|
||
|
||
.app-title {
|
||
font-size: 16px;
|
||
font-weight: 800;
|
||
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;
|
||
white-space: nowrap
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-1px);
|
||
filter: brightness(1.1)
|
||
}
|
||
|
||
.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)
|
||
}
|
||
|
||
[data-theme="dark"] .btn-ghost {
|
||
background: rgba(255, 255, 255, .1);
|
||
border: 1px solid rgba(255, 255, 255, .25);
|
||
color: #fff
|
||
}
|
||
|
||
.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
|
||
}
|
||
|
||
.app-main {
|
||
flex: 1;
|
||
display: flex;
|
||
overflow: hidden
|
||
}
|
||
|
||
.sidebar {
|
||
width: 280px;
|
||
background: var(--panel-bg);
|
||
border-right: 1px solid var(--border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition: width .3s;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
z-index: 50
|
||
}
|
||
|
||
.sidebar.collapsed {
|
||
width: 0
|
||
}
|
||
|
||
.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
|
||
}
|
||
|
||
.sidebar-body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 8px
|
||
}
|
||
|
||
.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
|
||
}
|
||
|
||
.sidebar-group-header .group-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex: 1
|
||
}
|
||
|
||
.sidebar-group-header .group-arrow {
|
||
transition: transform .3s;
|
||
font-size: 12px
|
||
}
|
||
|
||
.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;
|
||
text-align: right;
|
||
width: 100%
|
||
}
|
||
|
||
.sidebar-btn:hover {
|
||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||
color: #fff
|
||
}
|
||
|
||
.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: 48px;
|
||
gap: 10px;
|
||
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
|
||
}
|
||
|
||
.toolbar-icon-btn:hover:not(:disabled) {
|
||
background: var(--accent);
|
||
color: var(--primary-dark)
|
||
}
|
||
|
||
.toolbar-search {
|
||
flex: 1;
|
||
min-width: 180px;
|
||
max-width: 320px;
|
||
position: relative
|
||
}
|
||
|
||
.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
|
||
}
|
||
|
||
.search-input::placeholder {
|
||
color: rgba(255, 255, 255, .5)
|
||
}
|
||
|
||
.toolbar-filters {
|
||
display: flex;
|
||
gap: 8px
|
||
}
|
||
|
||
.filter-dropdown {
|
||
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
|
||
}
|
||
|
||
.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)
|
||
}
|
||
|
||
.stat-value {
|
||
font-weight: 800;
|
||
color: var(--accent);
|
||
background: rgba(201, 169, 97, .15);
|
||
padding: 2px 7px;
|
||
border-radius: 10px;
|
||
font-size: 11px
|
||
}
|
||
|
||
.board-area {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 24px;
|
||
background: var(--board-bg)
|
||
}
|
||
|
||
.board-section {
|
||
margin-bottom: 32px
|
||
}
|
||
|
||
.board-section.collapsed .section-content {
|
||
display: none
|
||
}
|
||
|
||
.board-section.collapsed .collapse-icon {
|
||
transform: rotate(90deg)
|
||
}
|
||
|
||
.collapse-icon {
|
||
transition: transform .3s;
|
||
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
|
||
}
|
||
|
||
.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
|
||
}
|
||
|
||
.project-card-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 4px
|
||
}
|
||
|
||
.project-card-title {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
flex: 1
|
||
}
|
||
|
||
.project-card-desc {
|
||
font-size: 12px;
|
||
opacity: .9;
|
||
line-height: 1.5
|
||
}
|
||
|
||
.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
|
||
}
|
||
|
||
.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);
|
||
display: flex;
|
||
flex-direction: column
|
||
}
|
||
|
||
.env-card-preview {
|
||
height: 60px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #fff
|
||
}
|
||
|
||
.env-card-body {
|
||
padding: 14px 16px;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px
|
||
}
|
||
|
||
.env-card-name {
|
||
font-size: 15px;
|
||
font-weight: 700
|
||
}
|
||
|
||
.env-card-desc {
|
||
font-size: 12px;
|
||
color: var(--text-muted)
|
||
}
|
||
|
||
.env-card-footer {
|
||
padding: 10px 16px;
|
||
background: var(--border-light);
|
||
border-top: 1px solid var(--border);
|
||
display: flex;
|
||
gap: 6px
|
||
}
|
||
|
||
.board-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--text-muted);
|
||
text-align: center;
|
||
padding: 40px
|
||
}
|
||
|
||
.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)
|
||
}
|
||
|
||
.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
|
||
}
|
||
|
||
.modal-overlay.active {
|
||
display: flex
|
||
}
|
||
|
||
.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)
|
||
}
|
||
|
||
.modal-lg {
|
||
max-width: 800px
|
||
}
|
||
|
||
.modal-xl {
|
||
max-width: 1000px
|
||
}
|
||
|
||
.modal-galaxy {
|
||
max-width: 1200px;
|
||
width: 100%;
|
||
height: 85vh;
|
||
max-height: 85vh;
|
||
border-radius: 16px
|
||
}
|
||
|
||
.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
|
||
}
|
||
|
||
.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;
|
||
flex-wrap: wrap
|
||
}
|
||
|
||
.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
|
||
}
|
||
|
||
.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)
|
||
}
|
||
|
||
.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: 8px 10px;
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border-light);
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
flex-wrap: wrap;
|
||
gap: 6px
|
||
}
|
||
|
||
.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
|
||
}
|
||
|
||
.deployment-notes {
|
||
font-size: 10px;
|
||
color: var(--text-muted);
|
||
width: 100%;
|
||
margin-top: 4px;
|
||
padding-top: 4px;
|
||
border-top: 1px dashed var(--border-light);
|
||
line-height: 1.5
|
||
}
|
||
|
||
.deployment-req {
|
||
font-size: 10px;
|
||
color: var(--info);
|
||
width: 100%;
|
||
margin-top: 2px
|
||
}
|
||
|
||
.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)
|
||
}
|
||
|
||
.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-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
|
||
}
|
||
|
||
.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)
|
||
}
|
||
|
||
.webhook-status-dot.success {
|
||
background: var(--success)
|
||
}
|
||
|
||
.webhook-status-dot.error {
|
||
background: var(--danger)
|
||
}
|
||
|
||
.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;
|
||
min-width: 250px
|
||
}
|
||
|
||
.toast.success {
|
||
background: var(--success)
|
||
}
|
||
|
||
.toast.error {
|
||
background: var(--danger)
|
||
}
|
||
|
||
.toast.warning {
|
||
background: var(--warning)
|
||
}
|
||
|
||
.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
|
||
}
|
||
|
||
.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)
|
||
}
|
||
|
||
.dependency-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex: 1
|
||
}
|
||
|
||
.dependency-project-name {
|
||
font-weight: 600;
|
||
font-size: 13px
|
||
}
|
||
|
||
.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-results {
|
||
max-height: 300px;
|
||
overflow-y: auto
|
||
}
|
||
|
||
.dependency-project-result {
|
||
margin-bottom: 8px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
overflow: hidden
|
||
}
|
||
|
||
.dependency-project-add-btn {
|
||
padding: 10px 12px;
|
||
background: var(--card-bg);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
width: 100%;
|
||
border: none;
|
||
font-family: inherit;
|
||
text-align: right
|
||
}
|
||
|
||
.dependency-project-add-btn:hover {
|
||
background: var(--accent-light)
|
||
}
|
||
|
||
.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)
|
||
}
|
||
|
||
.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
|
||
}
|
||
|
||
.req-deployment-box {
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
padding: 10px;
|
||
margin-bottom: 8px;
|
||
background: var(--border-light)
|
||
}
|
||
|
||
.req-deployment-title {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
margin-bottom: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px
|
||
}
|
||
|
||
.req-deployment-select {
|
||
width: 100%;
|
||
padding: 8px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
font-family: inherit;
|
||
font-size: 12px;
|
||
background: var(--card-bg);
|
||
color: var(--text)
|
||
}
|
||
|
||
.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)
|
||
}
|
||
|
||
.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-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-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;
|
||
max-width: 220px
|
||
}
|
||
|
||
.galaxy-legend-title {
|
||
font-weight: 700;
|
||
font-size: 12px;
|
||
margin-bottom: 10px;
|
||
color: var(--accent-light)
|
||
}
|
||
|
||
.galaxy-legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 6px
|
||
}
|
||
|
||
.galaxy-legend-color {
|
||
width: 14px;
|
||
height: 14px;
|
||
border-radius: 50%;
|
||
box-shadow: 0 0 8px currentColor
|
||
}
|
||
|
||
.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;
|
||
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
|
||
}
|
||
|
||
.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-label {
|
||
color: rgba(255, 255, 255, .6);
|
||
font-size: 11px
|
||
}
|
||
|
||
.galaxy-info-value {
|
||
font-weight: 600;
|
||
font-size: 11px;
|
||
text-align: left
|
||
}
|
||
|
||
.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-stats {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
display: flex;
|
||
gap: 16px;
|
||
flex-wrap: wrap
|
||
}
|
||
|
||
.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)
|
||
}
|
||
|
||
.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;
|
||
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-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
|
||
}
|
||
|
||
.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;
|
||
display: none;
|
||
font-family: 'Vazirmatn', sans-serif
|
||
}
|
||
|
||
.d3-tooltip.visible {
|
||
display: block
|
||
}
|
||
|
||
::-webkit-scrollbar {
|
||
width: 8px;
|
||
height: 8px
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: transparent
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: var(--border);
|
||
border-radius: 4px
|
||
}
|
||
</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>↩</button>
|
||
<button class="toolbar-icon-btn" id="redoBtn" title="Ctrl+Y" disabled>↪</button>
|
||
</div>
|
||
<div class="toolbar-search"><span style="position:absolute;right:10px;opacity:.7">🔍</span><input type="text"
|
||
class="search-input" id="toolbarSearch" placeholder="جستجو..."></div>
|
||
<div class="toolbar-filters">
|
||
<div class="filter-dropdown"><span>🏷️</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>🌐</span><select class="filter-select" id="filterEnvironment">
|
||
<option value="">همه محیطها</option>
|
||
</select></div>
|
||
</div>
|
||
<div class="toolbar-stats">
|
||
<div class="stat-item">📋 <span class="stat-value" id="statTotal">0</span></div>
|
||
<div class="stat-item">📦 <span class="stat-value" id="statProjects">0</span></div>
|
||
<div class="stat-item">🌿 <span class="stat-value" id="statSubProjects">0</span></div>
|
||
<div class="stat-item">🔗 <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.1.0</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">ℹ️ اطلاعات پایه محصول فقط قابل مشاهده هستند.</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">🔗 تنظیمات وبهوک</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"></div>
|
||
<div class="form-group"><label class="form-label">شناسه کلاینت <span
|
||
class="required">*</span></label><input type="text" class="form-input"
|
||
id="settingWebhookClientId" dir="ltr"></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">🌍 افزودن محیط عملیاتی جدید</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">
|
||
</div>
|
||
<div class="form-group"><label class="form-label">توضیحات</label><input type="text"
|
||
class="form-input" id="newEnvDesc"></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">✏️ ویرایش محیط عملیاتی</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">📦 جزئیات محصول</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">✏️ ویرایش محصول</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"></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">➕ ایجاد محصول جدید</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="newProductTitle">
|
||
<div class="form-error" id="newProductTitleError">عنوان محصول الزامی است و باید حداقل ۳ کاراکتر باشد
|
||
</div>
|
||
</div>
|
||
<div class="form-group"><label class="form-label">توضیحات</label><textarea class="form-textarea"
|
||
id="newProductDescription"></textarea></div>
|
||
<div class="form-row">
|
||
<div class="form-group"><label class="form-label">مالک</label><input type="text" class="form-input"
|
||
id="newProductOwner"></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">➕ افزودن پروژه جدید</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"></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"></div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group"><label class="form-label">زبان</label><input type="text" class="form-input"
|
||
id="newProjectLanguage"></div>
|
||
<div class="form-group"><label class="form-label">آدرس مخزن</label><input type="text"
|
||
class="form-input" id="newProjectRepo" dir="ltr"></div>
|
||
</div>
|
||
<div class="form-group"><label class="form-label">توضیحات</label><textarea class="form-textarea"
|
||
id="newProjectDescription"></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">✏️ ویرایش پروژه</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"></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>
|
||
|
||
<!-- Deployment Modal -->
|
||
<div class="modal-overlay" id="deploymentModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">
|
||
<div class="modal-title" id="deploymentModalTitle">🚀 افزودن نسخه استقرار</div><button
|
||
class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group" id="deploymentEnvGroup"></div>
|
||
<div class="form-row">
|
||
<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="form-group"><label class="form-label">ریلیز نوت <span
|
||
class="required">*</span></label><textarea class="form-textarea" id="depReleaseNotes"
|
||
rows="3" placeholder="توضیحات تغییرات این نسخه..."></textarea>
|
||
<div class="form-hint">شرح تغییرات، باگفیکسها و ویژگیهای جدید این نسخه</div>
|
||
</div>
|
||
<div id="requiredDeploymentsSection"></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>
|
||
|
||
<!-- Dependency Manager Modal -->
|
||
<div class="modal-overlay" id="dependencyManagerModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">
|
||
<div class="modal-title">🔗 مدیریت وابستگیها</div><button class="modal-close"
|
||
data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="readonly-info">ℹ️ هر وابستگی فقط شامل یک پروژه است. انتخاب نسخه و محیط در زمان ثبت استقرار
|
||
انجام میشود.</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">🌌 نمودار کهکشانی محصولات</div>
|
||
<div class="galaxy-toolbar"><button class="btn btn-sm" id="galaxyResetView">🎯 بازنشانی</button><button
|
||
class="btn btn-sm" id="galaxyExportPNG">📷 PNG</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-legend" id="galaxyLegend"></div>
|
||
<div class="galaxy-info-panel" id="galaxyInfoPanel"><button class="modal-close" id="galaxyInfoClose"
|
||
style="position:absolute;top:8px;left:8px;width:22px;height:22px;font-size:12px">✕</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 (COMPLETED) -->
|
||
<div class="modal-overlay" id="executiveDashboardModal">
|
||
<div class="modal modal-galaxy">
|
||
<div class="modal-header">
|
||
<div class="modal-title">📊 داشبورد مدیریتی</div>
|
||
<div class="galaxy-toolbar"><button class="btn btn-sm" id="dashboardRefresh">🔄 بهروزرسانی</button>
|
||
</div><button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body dashboard-modal-body">
|
||
<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">🔗 تحلیل وابستگیها</div>
|
||
<div class="galaxy-toolbar"><button class="btn btn-sm" id="depAnalysisRefresh">🔄</button></div><button
|
||
class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body dashboard-modal-body">
|
||
<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">🚀 تحلیل استقرار</div>
|
||
<div class="galaxy-toolbar"><button class="btn btn-sm" id="deployAnalysisRefresh">🔄</button></div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body dashboard-modal-body">
|
||
<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">🔢 تحلیل نسخه</div>
|
||
<div class="galaxy-toolbar"><button class="btn btn-sm" id="verAnalysisRefresh">🔄</button></div><button
|
||
class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body dashboard-modal-body">
|
||
<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>
|
||
|
||
<!-- Risk Analysis Modal -->
|
||
<div class="modal-overlay" id="riskAnalysisModal">
|
||
<div class="modal modal-galaxy">
|
||
<div class="modal-header">
|
||
<div class="modal-title">⚠️ تحلیل ریسک</div>
|
||
<div class="galaxy-toolbar"><button class="btn btn-sm" id="riskAnalysisRefresh">🔄</button></div><button
|
||
class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body dashboard-modal-body">
|
||
<div id="riskAnalysisContainer" class="dashboard-container"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div class="dashboard-footer-stats" id="riskAnalysisFooterStats"></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>
|
||
<div class="d3-tooltip" id="d3Tooltip"></div>
|
||
|
||
<script>
|
||
/* ==================== DATA (NEW STRUCTURE) ==================== */
|
||
const projectData = {
|
||
projectId: '550e8400-e29b-41d4-a716-446655440000', projectName: 'xReleaseAssistant', projectTitle: 'دستیار انتشار', description: 'مدیریت نسخهها و نیازمندیهای محصولات', version: '1.1.0', developer: 'هادی خزاعی اصل', company: 'رایان پرداز کاوش', metadata: { createdAt: '2026-09-13T00:00:00Z', updatedAt: '2026-09-14T00:00:00Z', tags: ['release-management'], customFields: {} }, webhook: { url: '', clientId: '', enabled: false }, toolGroups: [{ id: 'product-management', title: 'پرونده', icon: '📁', description: 'مدیریت چرخه حیات محصول', tools: [{ id: 'clear-all', title: 'پاکسازی', icon: '🗑️', action: 'clearAll', description: 'پاکسازی کامل' }, { id: 'save-product', title: 'ذخیره', icon: '💾', action: 'saveProduct', description: 'دانلود دادهها' }, { id: 'load-product', title: 'بارگذاری', icon: '📂', action: 'loadProduct', description: 'بارگذاری از فایل' }, { id: 'product-settings', title: 'تنظیمات', icon: '⚙️', action: 'openProductSettings', description: 'پیکربندی' }] }, { id: 'reports', title: 'گزارشها', icon: '📊', description: 'تحلیل و گزارشگیری', tools: [{ id: 'executive-dashboard', title: 'داشبورد مدیریتی', icon: '📊', action: 'showExecutiveDashboard', description: 'نمای کلی با نمودارها' }, { id: 'dependency-analysis', title: 'تحلیل وابستگیها', icon: '🔗', action: 'showDependencyAnalysis', description: 'تحلیل وابستگیها' }, { id: 'deployment-analysis', title: 'تحلیل استقرار', icon: '🚀', action: 'showDeploymentAnalysis', description: 'تحلیل استقرارها' }, { id: 'version-analysis', title: 'تحلیل نسخه', icon: '🔢', action: 'showVersionAnalysis', description: 'تحلیل نسخهها' }, { id: 'risk-analysis', title: 'تحلیل ریسک', icon: '⚠️', action: 'showRiskAnalysis', description: 'تحلیل ریسکها' }, { id: 'galaxy-chart', title: 'نمودار کهکشانی', icon: '🌌', action: 'showGalaxyChart', description: 'نمودار کهکشانی' }] }],
|
||
appData: { environments: [{ id: 'env-prod-001', name: 'Production', description: 'محیط عملیاتی اصلی - در دسترس کاربران نهایی', backgroundColor: '#38a169', textColor: '#ffffff' }, { id: 'env-stage-002', name: 'Staging', description: 'محیط پیش از تولید - شبیهسازی Production', backgroundColor: '#dd6b20', textColor: '#ffffff' }, { id: 'env-dev-003', name: 'Development', description: 'محیط توسعه - برای تیم فنی', backgroundColor: '#3182ce', textColor: '#ffffff' }, { id: 'env-qa-004', name: 'QA', description: 'محیط تست کیفیت - کنترل کیفیت خودکار و دستی', backgroundColor: '#805ad5', textColor: '#ffffff' }, { id: 'env-uat-005', name: 'UAT', description: 'تست پذیرش کاربر - تأیید کارفرما', backgroundColor: '#d69e2e', textColor: '#ffffff' }, { id: 'env-dr-006', name: 'Disaster Recovery', description: 'محیط بازیابی پس از بحران - پشتیبان Production', backgroundColor: '#e53e3e', textColor: '#ffffff' }], products: [{ id: 'prod-ecommerce', title: 'سامانه فروشگاه آنلاین دیجیمارکت', description: 'پلتفرم جامع تجارت الکترونیک با قابلیتهای چندفروشندگی', metadata: { createdAt: '2026-01-15T08:00:00Z', owner: 'تیم توسعه تجارت الکترونیک', customFields: { industry: 'E-commerce', priority: 'critical' } }, projects: [{ id: 'proj-ecom-api', title: 'E-commerce Core API', description: 'سرویس بکاند اصلی فروشگاه', type: 'backend', repositoryUrl: 'https://git.rayanpardaz.ir/ecommerce/core-api', metadata: { framework: '.NET 8', language: 'C#', team: 'Backend Team A' }, dependencies: ['proj-payment-gateway', 'proj-inventory-svc'], deployments: [{ id: 'dep-ecom-api-prod', environmentId: 'env-prod-001', version: '3.5.2', deploymentDate: '2026-09-10T14:30:00Z', releaseNotes: 'رفع باگ پردازش سفارش و بهبود عملکرد کوئریها', requiredDeployments: [{ projectId: 'proj-payment-gateway', deploymentId: 'dep-payment-prod' }, { projectId: 'proj-inventory-svc', deploymentId: 'dep-inventory-prod' }], status: 'active' }, { id: 'dep-ecom-api-stage', environmentId: 'env-stage-002', version: '3.6.0-rc.1', deploymentDate: '2026-09-14T09:00:00Z', releaseNotes: 'افزودن قابلیت چندزبانه و بهینهسازی کش', requiredDeployments: [], status: 'active' }, { id: 'dep-ecom-api-dev', environmentId: 'env-dev-003', version: '3.7.0-dev.12', deploymentDate: '2026-09-15T16:00:00Z', releaseNotes: 'پیادهسازی اولیه ماژول تخفیف پیشرفته', requiredDeployments: [], status: 'active' }] }, { id: 'proj-ecom-web', title: 'E-commerce Web Frontend', description: 'اپلیکیشن وب فروشگاه با Angular', type: 'frontend', repositoryUrl: 'https://git.rayanpardaz.ir/ecommerce/web-app', metadata: { framework: 'Angular 18', language: 'TypeScript' }, dependencies: ['proj-ecom-api'], deployments: [{ id: 'dep-ecom-web-prod', environmentId: 'env-prod-001', version: '4.2.1', deploymentDate: '2026-09-11T10:00:00Z', releaseNotes: 'بهبود UX صفحه پرداخت و افزودن فیلتر پیشرفته', requiredDeployments: [{ projectId: 'proj-ecom-api', deploymentId: 'dep-ecom-api-prod' }], status: 'active' }, { id: 'dep-ecom-web-stage', environmentId: 'env-stage-002', version: '4.3.0-rc.1', deploymentDate: '2026-09-13T11:30:00Z', releaseNotes: 'پیادهسازی Dark Mode و بهینهسازی SEO', requiredDeployments: [], status: 'active' }] }, { id: 'proj-ecom-mobile', title: 'E-commerce Mobile App', description: 'اپلیکیشن موبایل فروشگاه (iOS/Android)', type: 'mobile', repositoryUrl: 'https://git.rayanpardaz.ir/ecommerce/mobile-app', metadata: { framework: 'Flutter 3.22', language: 'Dart' }, dependencies: ['proj-ecom-api', 'proj-notification-svc'], deployments: [{ id: 'dep-ecom-mobile-prod', environmentId: 'env-prod-001', version: '2.8.0', deploymentDate: '2026-09-08T08:00:00Z', releaseNotes: 'انتشار در استورها با قابلیت Push Notification', requiredDeployments: [{ projectId: 'proj-ecom-api', deploymentId: 'dep-ecom-api-prod' }], status: 'active' }] }, { id: 'proj-ecom-db', title: 'E-commerce Database', description: 'پایگاه داده اصلی فروشگاه', type: 'database', repositoryUrl: 'https://git.rayanpardaz.ir/ecommerce/db-migrations', metadata: { framework: 'SQL Server 2022', language: 'T-SQL' }, dependencies: [], deployments: [{ id: 'dep-ecom-db-prod', environmentId: 'env-prod-001', version: '5.1.0', deploymentDate: '2026-09-05T02:00:00Z', releaseNotes: 'افزودن ایندکسهای جدید و بهینهسازی جداول سفارش', requiredDeployments: [], status: 'active' }, { id: 'dep-ecom-db-dr', environmentId: 'env-dr-006', version: '5.1.0', deploymentDate: '2026-09-05T03:00:00Z', releaseNotes: 'همگامسازی با Production برای Disaster Recovery', requiredDeployments: [{ projectId: 'proj-ecom-db', deploymentId: 'dep-ecom-db-prod' }], status: 'active' }] }, { id: 'proj-ecom-search', title: 'E-commerce Search Service', description: 'سرویس جستجوی پیشرفته مبتنی بر Elasticsearch', type: 'service', repositoryUrl: 'https://git.rayanpardaz.ir/ecommerce/search-svc', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-ecom-db'], deployments: [{ id: 'dep-ecom-search-prod', environmentId: 'env-prod-001', version: '1.9.3', deploymentDate: '2026-09-09T13:00:00Z', releaseNotes: 'بهبود الگوریتم جستجوی فازی و افزودن فیلتر قیمت', requiredDeployments: [{ projectId: 'proj-ecom-db', deploymentId: 'dep-ecom-db-prod' }], status: 'active' }] }, { id: 'proj-ecom-infra', title: 'E-commerce Infrastructure', description: 'زیرساخت Kubernetes و CI/CD', type: 'infrastructure', repositoryUrl: 'https://git.rayanpardaz.ir/ecommerce/infra', metadata: { framework: 'Kubernetes 1.29', language: 'YAML/Helm' }, dependencies: [], deployments: [{ id: 'dep-ecom-infra-prod', environmentId: 'env-prod-001', version: '2.1.0', deploymentDate: '2026-09-01T06:00:00Z', releaseNotes: 'بهروزرسانی Helm Charts و افزودن Auto-scaling', requiredDeployments: [], status: 'active' }] }] }, { id: 'prod-banking', title: 'پلتفرم بانکداری دیجیتال پارسیانبانک', description: 'سامانه جامع بانکداری دیجیتال با قابلیتهای نئوبانک', metadata: { createdAt: '2026-02-01T08:00:00Z', owner: 'تیم توسعه بانکداری دیجیتال', customFields: { industry: 'FinTech', priority: 'critical' } }, projects: [{ id: 'proj-bank-core', title: 'Banking Core Engine', description: 'موتور اصلی پردازش تراکنشهای بانکی', type: 'backend', repositoryUrl: 'https://git.rayanpardaz.ir/banking/core-engine', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-payment-gateway', 'proj-auth-svc'], deployments: [{ id: 'dep-bank-core-prod', environmentId: 'env-prod-001', version: '6.2.0', deploymentDate: '2026-09-12T01:00:00Z', releaseNotes: 'افزودن پشتیبانی از تراکنشهای بینالمللی و بهبود امنیت', requiredDeployments: [{ projectId: 'proj-payment-gateway', deploymentId: 'dep-payment-prod' }], status: 'active' }, { id: 'dep-bank-core-uat', environmentId: 'env-uat-005', version: '6.3.0-rc.2', deploymentDate: '2026-09-14T10:00:00Z', releaseNotes: 'تست پذیرش ماژول وامدهی خرد', requiredDeployments: [], status: 'active' }] }, { id: 'proj-bank-web', title: 'Banking Web Portal', description: 'پرتال وب بانکداری اینترنتی', type: 'frontend', repositoryUrl: 'https://git.rayanpardaz.ir/banking/web-portal', metadata: { framework: 'Angular 18', language: 'TypeScript' }, dependencies: ['proj-bank-core'], deployments: [{ id: 'dep-bank-web-prod', environmentId: 'env-prod-001', version: '5.1.4', deploymentDate: '2026-09-13T08:30:00Z', releaseNotes: 'بازطراحی داشبورد کاربر و افزودن گزارشهای مالی', requiredDeployments: [{ projectId: 'proj-bank-core', deploymentId: 'dep-bank-core-prod' }], status: 'active' }] }, { id: 'proj-bank-mobile', title: 'Banking Mobile App', description: 'اپلیکیشن موبایل بانکداری همراه', type: 'mobile', repositoryUrl: 'https://git.rayanpardaz.ir/banking/mobile-app', metadata: { framework: 'Kotlin Multiplatform', language: 'Kotlin' }, dependencies: ['proj-bank-core', 'proj-notification-svc'], deployments: [{ id: 'dep-bank-mobile-prod', environmentId: 'env-prod-001', version: '4.0.2', deploymentDate: '2026-09-07T09:00:00Z', releaseNotes: 'افزودن ورود با Biometric و کارت به کارت', requiredDeployments: [{ projectId: 'proj-bank-core', deploymentId: 'dep-bank-core-prod' }], status: 'active' }, { id: 'dep-bank-mobile-qa', environmentId: 'env-qa-004', version: '4.1.0-beta.3', deploymentDate: '2026-09-15T14:00:00Z', releaseNotes: 'تست قابلیت پرداخت با QR Code', requiredDeployments: [], status: 'active' }] }, { id: 'proj-bank-db', title: 'Banking Database Cluster', description: 'خوشه پایگاه داده بانکی با قابلیت HA', type: 'database', repositoryUrl: 'https://git.rayanpardaz.ir/banking/db-cluster', metadata: { framework: 'Oracle 19c', language: 'PL/SQL' }, dependencies: [], deployments: [{ id: 'dep-bank-db-prod', environmentId: 'env-prod-001', version: '8.0.1', deploymentDate: '2026-09-02T03:00:00Z', releaseNotes: 'ارتقای ساختار جداول تراکنش و افزودن Partitioning', requiredDeployments: [], status: 'active' }, { id: 'dep-bank-db-dr', environmentId: 'env-dr-006', version: '8.0.1', deploymentDate: '2026-09-02T04:00:00Z', releaseNotes: 'همگامسازی Active-Passive با Production', requiredDeployments: [], status: 'active' }] }, { id: 'proj-bank-fraud', title: 'Fraud Detection Service', description: 'سرویس هوشمند تشخیص تقلب با ML', type: 'service', repositoryUrl: 'https://git.rayanpardaz.ir/banking/fraud-svc', metadata: { framework: 'Python 3.12 + FastAPI', language: 'Python' }, dependencies: ['proj-bank-core', 'proj-bank-db'], deployments: [{ id: 'dep-bank-fraud-prod', environmentId: 'env-prod-001', version: '2.4.0', deploymentDate: '2026-09-11T15:00:00Z', releaseNotes: 'بهروزرسانی مدل ML و کاهش نرخ False Positive', requiredDeployments: [{ projectId: 'proj-bank-core', deploymentId: 'dep-bank-core-prod' }], status: 'active' }] }] }, { id: 'prod-healthcare', title: 'سامانه سلامت و نوبتدهی سلامتیار', description: 'پلتفرم جامع نوبتدهی و پرونده الکترونیک سلامت', metadata: { createdAt: '2026-02-20T08:00:00Z', owner: 'تیم توسعه سلامت دیجیتال', customFields: { industry: 'Healthcare', priority: 'high' } }, projects: [{ id: 'proj-health-api', title: 'Healthcare Core API', description: 'سرویس بکاند سامانه سلامت', type: 'backend', repositoryUrl: 'https://git.rayanpardaz.ir/healthcare/core-api', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-auth-svc', 'proj-notification-svc'], deployments: [{ id: 'dep-health-api-prod', environmentId: 'env-prod-001', version: '2.7.1', deploymentDate: '2026-09-10T11:00:00Z', releaseNotes: 'افزودن ماژول نسخهنویسی الکترونیک', requiredDeployments: [], status: 'active' }, { id: 'dep-health-api-stage', environmentId: 'env-stage-002', version: '2.8.0-rc.1', deploymentDate: '2026-09-14T13:00:00Z', releaseNotes: 'یکپارچگی با سامانه بیمه تأمین اجتماعی', requiredDeployments: [], status: 'active' }] }, { id: 'proj-health-web', title: 'Healthcare Patient Portal', description: 'پرتال بیماران برای نوبتدهی و مشاهده پرونده', type: 'frontend', repositoryUrl: 'https://git.rayanpardaz.ir/healthcare/patient-portal', metadata: { framework: 'React 18', language: 'TypeScript' }, dependencies: ['proj-health-api'], deployments: [{ id: 'dep-health-web-prod', environmentId: 'env-prod-001', version: '3.1.0', deploymentDate: '2026-09-12T09:30:00Z', releaseNotes: 'بهبود فرآیند رزرو نوبت و افزودن یادآور خودکار', requiredDeployments: [{ projectId: 'proj-health-api', deploymentId: 'dep-health-api-prod' }], status: 'active' }] }, { id: 'proj-health-doc', title: 'Doctor Dashboard', description: 'داشبورد پزشکان برای مدیریت بیماران', type: 'frontend', repositoryUrl: 'https://git.rayanpardaz.ir/healthcare/doctor-dashboard', metadata: { framework: 'Angular 18', language: 'TypeScript' }, dependencies: ['proj-health-api'], deployments: [{ id: 'dep-health-doc-prod', environmentId: 'env-prod-001', version: '2.3.2', deploymentDate: '2026-09-09T10:00:00Z', releaseNotes: 'افزودن تقویم هوشمند و مدیریت نسخهها', requiredDeployments: [{ projectId: 'proj-health-api', deploymentId: 'dep-health-api-prod' }], status: 'active' }] }, { id: 'proj-health-db', title: 'Healthcare Database', description: 'پایگاه داده پروندههای الکترونیک سلامت', type: 'database', repositoryUrl: 'https://git.rayanpardaz.ir/healthcare/db', metadata: { framework: 'PostgreSQL 16', language: 'SQL' }, dependencies: [], deployments: [{ id: 'dep-health-db-prod', environmentId: 'env-prod-001', version: '4.0.0', deploymentDate: '2026-09-03T02:30:00Z', releaseNotes: 'مهاجرت به ساختار جدید پرونده الکترونیک', requiredDeployments: [], status: 'active' }] }, { id: 'proj-health-mobile', title: 'Healthcare Mobile App', description: 'اپلیکیشن موبایل بیماران', type: 'mobile', repositoryUrl: 'https://git.rayanpardaz.ir/healthcare/mobile', metadata: { framework: 'React Native 0.74', language: 'TypeScript' }, dependencies: ['proj-health-api'], deployments: [{ id: 'dep-health-mobile-prod', environmentId: 'env-prod-001', version: '1.5.0', deploymentDate: '2026-09-06T08:00:00Z', releaseNotes: 'انتشار نسخه اولیه با قابلیت نوبتدهی', requiredDeployments: [{ projectId: 'proj-health-api', deploymentId: 'dep-health-api-prod' }], status: 'active' }] }] }, { id: 'prod-edtech', title: 'پلتفرم آموزش آنلاین دانشیار', description: 'سامانه LMS با قابلیت برگزاری کلاس زنده و محتوای تعاملی', metadata: { createdAt: '2026-03-05T08:00:00Z', owner: 'تیم توسعه EdTech', customFields: { industry: 'Education', priority: 'high' } }, projects: [{ id: 'proj-edu-api', title: 'LMS Core API', description: 'سرویس بکاند سامانه آموزش', type: 'backend', repositoryUrl: 'https://git.rayanpardaz.ir/edtech/core-api', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-auth-svc', 'proj-notification-svc'], deployments: [{ id: 'dep-edu-api-prod', environmentId: 'env-prod-001', version: '3.2.1', deploymentDate: '2026-09-11T12:00:00Z', releaseNotes: 'افزودن سیستم آزمون آنلاین و تصحیح خودکار', requiredDeployments: [], status: 'active' }] }, { id: 'proj-edu-web', title: 'LMS Web Platform', description: 'پلتفرم وب آموزش برای دانشجویان و اساتید', type: 'frontend', repositoryUrl: 'https://git.rayanpardaz.ir/edtech/web-platform', metadata: { framework: 'Next.js 14', language: 'TypeScript' }, dependencies: ['proj-edu-api', 'proj-video-streaming'], deployments: [{ id: 'dep-edu-web-prod', environmentId: 'env-prod-001', version: '4.0.3', deploymentDate: '2026-09-13T10:00:00Z', releaseNotes: 'بهبود پخشکننده ویدیو و افزودن زیرنویس خودکار', requiredDeployments: [{ projectId: 'proj-edu-api', deploymentId: 'dep-edu-api-prod' }], status: 'active' }] }, { id: 'proj-edu-video', title: 'Video Streaming Service', description: 'سرویس استریم ویدیوی کلاسهای زنده', type: 'service', repositoryUrl: 'https://git.rayanpardaz.ir/edtech/video-streaming', metadata: { framework: 'Node.js 20', language: 'TypeScript' }, dependencies: [], deployments: [{ id: 'dep-edu-video-prod', environmentId: 'env-prod-001', version: '2.1.0', deploymentDate: '2026-09-08T14:00:00Z', releaseNotes: 'پشتیبانی از کیفیت 4K و کاهش تأخیر استریم', requiredDeployments: [], status: 'active' }] }, { id: 'proj-edu-db', title: 'LMS Database', description: 'پایگاه داده دورهها و کاربران', type: 'database', repositoryUrl: 'https://git.rayanpardaz.ir/edtech/db', metadata: { framework: 'MongoDB 7', language: 'MongoDB Query' }, dependencies: [], deployments: [{ id: 'dep-edu-db-prod', environmentId: 'env-prod-001', version: '3.0.0', deploymentDate: '2026-09-04T03:00:00Z', releaseNotes: 'مهاجرت به ساختار شاردینگ برای مقیاسپذیری', requiredDeployments: [], status: 'active' }] }, { id: 'proj-edu-mobile', title: 'LMS Mobile App', description: 'اپلیکیشن موبایل دانشجویان', type: 'mobile', repositoryUrl: 'https://git.rayanpardaz.ir/edtech/mobile', metadata: { framework: 'Flutter 3.22', language: 'Dart' }, dependencies: ['proj-edu-api'], deployments: [{ id: 'dep-edu-mobile-prod', environmentId: 'env-prod-001', version: '2.0.1', deploymentDate: '2026-09-07T09:30:00Z', releaseNotes: 'قابلیت دانلود آفلاین ویدیوها', requiredDeployments: [{ projectId: 'proj-edu-api', deploymentId: 'dep-edu-api-prod' }], status: 'active' }] }] }, { id: 'prod-transport', title: 'سامانه حملونقل هوشمند ترافیکیار', description: 'پلتفرم مدیریت ناوگان و مسیریابی هوشمند', metadata: { createdAt: '2026-03-18T08:00:00Z', owner: 'تیم توسعه Smart City', customFields: { industry: 'Transportation', priority: 'high' } }, projects: [{ id: 'proj-trans-api', title: 'Transport Core API', description: 'سرویس بکاند سامانه حملونقل', type: 'backend', repositoryUrl: 'https://git.rayanpardaz.ir/transport/core-api', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-maps-svc', 'proj-auth-svc'], deployments: [{ id: 'dep-trans-api-prod', environmentId: 'env-prod-001', version: '2.5.0', deploymentDate: '2026-09-10T13:00:00Z', releaseNotes: 'افزودن الگوریتم مسیریابی پیشرفته با ترافیک زنده', requiredDeployments: [], status: 'active' }] }, { id: 'proj-trans-web', title: 'Fleet Management Dashboard', description: 'داشبورد مدیریت ناوگان', type: 'frontend', repositoryUrl: 'https://git.rayanpardaz.ir/transport/fleet-dashboard', metadata: { framework: 'Vue.js 3', language: 'TypeScript' }, dependencies: ['proj-trans-api'], deployments: [{ id: 'dep-trans-web-prod', environmentId: 'env-prod-001', version: '3.1.2', deploymentDate: '2026-09-12T11:00:00Z', releaseNotes: 'نمایش زنده ناوگان روی نقشه و گزارشهای عملکرد', requiredDeployments: [{ projectId: 'proj-trans-api', deploymentId: 'dep-trans-api-prod' }], status: 'active' }] }, { id: 'proj-maps-svc', title: 'Maps & Geolocation Service', description: 'سرویس نقشه و موقعیتیابی - سرویس مشترک', type: 'service', repositoryUrl: 'https://git.rayanpardaz.ir/transport/maps-svc', metadata: { framework: 'Go 1.22', language: 'Go' }, dependencies: [], deployments: [{ id: 'dep-trans-maps-prod', environmentId: 'env-prod-001', version: '1.8.0', deploymentDate: '2026-09-09T15:00:00Z', releaseNotes: 'بهینهسازی reverse geocoding و افزودن POI', requiredDeployments: [], status: 'active' }] }, { id: 'proj-trans-mobile', title: 'Driver Mobile App', description: 'اپلیکیشن رانندگان', type: 'mobile', repositoryUrl: 'https://git.rayanpardaz.ir/transport/driver-app', metadata: { framework: 'Kotlin', language: 'Kotlin' }, dependencies: ['proj-trans-api'], deployments: [{ id: 'dep-trans-mobile-prod', environmentId: 'env-prod-001', version: '2.2.1', deploymentDate: '2026-09-08T08:30:00Z', releaseNotes: 'بهبود مصرف باتری و دقت GPS', requiredDeployments: [{ projectId: 'proj-trans-api', deploymentId: 'dep-trans-api-prod' }], status: 'active' }] }, { id: 'proj-trans-db', title: 'Transport Database', description: 'پایگاه داده سفرها و ناوگان', type: 'database', repositoryUrl: 'https://git.rayanpardaz.ir/transport/db', metadata: { framework: 'PostgreSQL 16 + PostGIS', language: 'SQL' }, dependencies: [], deployments: [{ id: 'dep-trans-db-prod', environmentId: 'env-prod-001', version: '3.2.0', deploymentDate: '2026-09-03T02:00:00Z', releaseNotes: 'افزودن جداول مکانی و بهینهسازی کوئریهای برداری', requiredDeployments: [], status: 'active' }] }] }, { id: 'prod-realestate', title: 'پلتفرم املاک و مستغلات خانهیار', description: 'سامانه جامع خرید، فروش و اجاره املاک', metadata: { createdAt: '2026-04-02T08:00:00Z', owner: 'تیم توسعه PropTech', customFields: { industry: 'Real Estate', priority: 'medium' } }, projects: [{ id: 'proj-re-api', title: 'Real Estate Core API', description: 'سرویس بکاند پلتفرم املاک', type: 'backend', repositoryUrl: 'https://git.rayanpardaz.ir/realestate/core-api', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-maps-svc', 'proj-auth-svc'], deployments: [{ id: 'dep-re-api-prod', environmentId: 'env-prod-001', version: '2.1.3', deploymentDate: '2026-09-11T10:30:00Z', releaseNotes: 'افزودن تور مجازی ۳۶۰ درجه و فیلتر پیشرفته', requiredDeployments: [{ projectId: 'proj-maps-svc', deploymentId: 'dep-trans-maps-prod' }], status: 'active' }] }, { id: 'proj-re-web', title: 'Real Estate Web Portal', description: 'پرتال وب جستجوی املاک', type: 'frontend', repositoryUrl: 'https://git.rayanpardaz.ir/realestate/web-portal', metadata: { framework: 'Next.js 14', language: 'TypeScript' }, dependencies: ['proj-re-api'], deployments: [{ id: 'dep-re-web-prod', environmentId: 'env-prod-001', version: '3.0.2', deploymentDate: '2026-09-13T09:00:00Z', releaseNotes: 'نمایش املاک روی نقشه و مقایسه چندگانه', requiredDeployments: [{ projectId: 'proj-re-api', deploymentId: 'dep-re-api-prod' }], status: 'active' }] }, { id: 'proj-re-mobile', title: 'Real Estate Mobile App', description: 'اپلیکیشن موبایل جستجوی ملک', type: 'mobile', repositoryUrl: 'https://git.rayanpardaz.ir/realestate/mobile', metadata: { framework: 'Swift', language: 'Swift' }, dependencies: ['proj-re-api'], deployments: [{ id: 'dep-re-mobile-prod', environmentId: 'env-prod-001', version: '1.7.0', deploymentDate: '2026-09-06T08:00:00Z', releaseNotes: 'نسخه iOS با قابلیت AR برای مشاهده ملک', requiredDeployments: [{ projectId: 'proj-re-api', deploymentId: 'dep-re-api-prod' }], status: 'active' }] }, { id: 'proj-re-db', title: 'Real Estate Database', description: 'پایگاه داده املاک و کاربران', type: 'database', repositoryUrl: 'https://git.rayanpardaz.ir/realestate/db', metadata: { framework: 'SQL Server 2022', language: 'T-SQL' }, dependencies: [], deployments: [{ id: 'dep-re-db-prod', environmentId: 'env-prod-001', version: '2.5.0', deploymentDate: '2026-09-04T03:00:00Z', releaseNotes: 'بهینهسازی ایندکسهای مکانی', requiredDeployments: [], status: 'active' }] }] }, { id: 'prod-travel', title: 'سامانه گردشگری و رزرو سفر سفریار', description: 'پلتفرم رزرو بلیط، هتل و تور', metadata: { createdAt: '2026-04-15T08:00:00Z', owner: 'تیم توسعه TravelTech', customFields: { industry: 'Tourism', priority: 'high' } }, projects: [{ id: 'proj-travel-api', title: 'Travel Booking API', description: 'سرویس بکاند رزرواسیون', type: 'backend', repositoryUrl: 'https://git.rayanpardaz.ir/travel/booking-api', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-payment-gateway', 'proj-notification-svc'], deployments: [{ id: 'dep-travel-api-prod', environmentId: 'env-prod-001', version: '3.4.1', deploymentDate: '2026-09-12T14:00:00Z', releaseNotes: 'یکپارچگی با GDS جهانی و افزودن رزرو هتل', requiredDeployments: [{ projectId: 'proj-payment-gateway', deploymentId: 'dep-payment-prod' }], status: 'active' }] }, { id: 'proj-travel-web', title: 'Travel Booking Portal', description: 'پرتال وب رزرو سفر', type: 'frontend', repositoryUrl: 'https://git.rayanpardaz.ir/travel/web-portal', metadata: { framework: 'React 18', language: 'TypeScript' }, dependencies: ['proj-travel-api'], deployments: [{ id: 'dep-travel-web-prod', environmentId: 'env-prod-001', version: '4.1.0', deploymentDate: '2026-09-13T11:30:00Z', releaseNotes: 'بازطراحی فرآیند رزرو و افزودن پیشنهاد هوشمند', requiredDeployments: [{ projectId: 'proj-travel-api', deploymentId: 'dep-travel-api-prod' }], status: 'active' }] }, { id: 'proj-travel-mobile', title: 'Travel Mobile App', description: 'اپلیکیشن موبایل رزرو سفر', type: 'mobile', repositoryUrl: 'https://git.rayanpardaz.ir/travel/mobile', metadata: { framework: 'Flutter 3.22', language: 'Dart' }, dependencies: ['proj-travel-api'], deployments: [{ id: 'dep-travel-mobile-prod', environmentId: 'env-prod-001', version: '2.6.2', deploymentDate: '2026-09-07T09:00:00Z', releaseNotes: 'افزودن کارت پرواز دیجیتال و مدیریت رزرو', requiredDeployments: [{ projectId: 'proj-travel-api', deploymentId: 'dep-travel-api-prod' }], status: 'active' }] }, { id: 'proj-travel-db', title: 'Travel Database', description: 'پایگاه داده رزروها و کاربران', type: 'database', repositoryUrl: 'https://git.rayanpardaz.ir/travel/db', metadata: { framework: 'PostgreSQL 16', language: 'SQL' }, dependencies: [], deployments: [{ id: 'dep-travel-db-prod', environmentId: 'env-prod-001', version: '3.1.0', deploymentDate: '2026-09-03T02:30:00Z', releaseNotes: 'افزودن جداول رزروهای بینالمللی', requiredDeployments: [], status: 'active' }] }] }, { id: 'prod-hr', title: 'پلتفرم مدیریت منابع انسانی کاریار', description: 'سامانه جامع HR شامل حقوق و دستمزد، حضور و غیاب و ارزیابی', metadata: { createdAt: '2026-05-01T08:00:00Z', owner: 'تیم توسعه HR Tech', customFields: { industry: 'Human Resources', priority: 'medium' } }, projects: [{ id: 'proj-hr-api', title: 'HR Core API', description: 'سرویس بکاند سامانه HR', type: 'backend', repositoryUrl: 'https://git.rayanpardaz.ir/hr/core-api', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-auth-svc', 'proj-notification-svc'], deployments: [{ id: 'dep-hr-api-prod', environmentId: 'env-prod-001', version: '2.9.0', deploymentDate: '2026-09-10T10:00:00Z', releaseNotes: 'ماژول جدید ارزیابی عملکرد ۳۶۰ درجه', requiredDeployments: [], status: 'active' }] }, { id: 'proj-hr-web', title: 'HR Web Portal', description: 'پرتال وب کارکنان و مدیران', type: 'frontend', repositoryUrl: 'https://git.rayanpardaz.ir/hr/web-portal', metadata: { framework: 'Angular 18', language: 'TypeScript' }, dependencies: ['proj-hr-api'], deployments: [{ id: 'dep-hr-web-prod', environmentId: 'env-prod-001', version: '3.3.1', deploymentDate: '2026-09-12T09:00:00Z', releaseNotes: 'داشبورد مدیریتی جدید و گزارشهای حقوق', requiredDeployments: [{ projectId: 'proj-hr-api', deploymentId: 'dep-hr-api-prod' }], status: 'active' }] }, { id: 'proj-hr-mobile', title: 'HR Mobile App', description: 'اپلیکیشن موبایل کارکنان', type: 'mobile', repositoryUrl: 'https://git.rayanpardaz.ir/hr/mobile', metadata: { framework: 'React Native 0.74', language: 'TypeScript' }, dependencies: ['proj-hr-api'], deployments: [{ id: 'dep-hr-mobile-prod', environmentId: 'env-prod-001', version: '1.8.0', deploymentDate: '2026-09-06T08:30:00Z', releaseNotes: 'ثبت حضور با GPS و درخواست مرخصی', requiredDeployments: [{ projectId: 'proj-hr-api', deploymentId: 'dep-hr-api-prod' }], status: 'active' }] }, { id: 'proj-hr-db', title: 'HR Database', description: 'پایگاه داده کارکنان و سوابق', type: 'database', repositoryUrl: 'https://git.rayanpardaz.ir/hr/db', metadata: { framework: 'SQL Server 2022', language: 'T-SQL' }, dependencies: [], deployments: [{ id: 'dep-hr-db-prod', environmentId: 'env-prod-001', version: '4.0.0', deploymentDate: '2026-09-02T02:00:00Z', releaseNotes: 'ساختار جدید برای ارزیابی عملکرد', requiredDeployments: [], status: 'active' }] }, { id: 'proj-hr-payroll', title: 'Payroll Engine', description: 'موتور محاسبه حقوق و دستمزد', type: 'service', repositoryUrl: 'https://git.rayanpardaz.ir/hr/payroll-engine', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-hr-api', 'proj-hr-db'], deployments: [{ id: 'dep-hr-payroll-prod', environmentId: 'env-prod-001', version: '2.2.1', deploymentDate: '2026-09-09T11:00:00Z', releaseNotes: 'پشتیبانی از قوانین جدید مالیاتی ۱۴۰۵', requiredDeployments: [{ projectId: 'proj-hr-api', deploymentId: 'dep-hr-api-prod' }], status: 'active' }] }] }, { id: 'prod-crm', title: 'سامانه CRM و فروش مشترییار', description: 'پلتفرم مدیریت ارتباط با مشتری و فروش', metadata: { createdAt: '2026-05-20T08:00:00Z', owner: 'تیم توسعه CRM', customFields: { industry: 'Sales & CRM', priority: 'high' } }, projects: [{ id: 'proj-crm-api', title: 'CRM Core API', description: 'سرویس بکاند CRM', type: 'backend', repositoryUrl: 'https://git.rayanpardaz.ir/crm/core-api', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-auth-svc', 'proj-notification-svc'], deployments: [{ id: 'dep-crm-api-prod', environmentId: 'env-prod-001', version: '3.6.2', deploymentDate: '2026-09-11T13:00:00Z', releaseNotes: 'ماژول اتوماسیون بازاریابی و تحلیل رفتار مشتری', requiredDeployments: [], status: 'active' }] }, { id: 'proj-crm-web', title: 'CRM Web Dashboard', description: 'داشبورد وب تیم فروش', type: 'frontend', repositoryUrl: 'https://git.rayanpardaz.ir/crm/web-dashboard', metadata: { framework: 'React 18', language: 'TypeScript' }, dependencies: ['proj-crm-api'], deployments: [{ id: 'dep-crm-web-prod', environmentId: 'env-prod-001', version: '4.2.0', deploymentDate: '2026-09-13T10:30:00Z', releaseNotes: 'نمای ۳۶۰ درجه مشتری و گزارشهای پیشرفته', requiredDeployments: [{ projectId: 'proj-crm-api', deploymentId: 'dep-crm-api-prod' }], status: 'active' }] }, { id: 'proj-crm-mobile', title: 'CRM Mobile App', description: 'اپلیکیشن موبایل ویزیتورها', type: 'mobile', repositoryUrl: 'https://git.rayanpardaz.ir/crm/mobile', metadata: { framework: 'Flutter 3.22', language: 'Dart' }, dependencies: ['proj-crm-api'], deployments: [{ id: 'dep-crm-mobile-prod', environmentId: 'env-prod-001', version: '2.1.3', deploymentDate: '2026-09-07T09:00:00Z', releaseNotes: 'ثبت بازدید حضوری و آپلود فاکتور', requiredDeployments: [{ projectId: 'proj-crm-api', deploymentId: 'dep-crm-api-prod' }], status: 'active' }] }, { id: 'proj-crm-db', title: 'CRM Database', description: 'پایگاه داده مشتریان و سرنخها', type: 'database', repositoryUrl: 'https://git.rayanpardaz.ir/crm/db', metadata: { framework: 'MongoDB 7', language: 'MongoDB Query' }, dependencies: [], deployments: [{ id: 'dep-crm-db-prod', environmentId: 'env-prod-001', version: '3.0.1', deploymentDate: '2026-09-04T03:00:00Z', releaseNotes: 'ساختار بهینه برای دادههای نیمهساختاریافته', requiredDeployments: [], status: 'active' }] }, { id: 'proj-crm-analytics', title: 'CRM Analytics Engine', description: 'موتور تحلیل و BI فروش', type: 'service', repositoryUrl: 'https://git.rayanpardaz.ir/crm/analytics', metadata: { framework: 'Python 3.12', language: 'Python' }, dependencies: ['proj-crm-db'], deployments: [{ id: 'dep-crm-analytics-prod', environmentId: 'env-prod-001', version: '1.5.0', deploymentDate: '2026-09-10T14:00:00Z', releaseNotes: 'پیشبینی فروش با مدل ML و تحلیل چرخه عمر مشتری', requiredDeployments: [{ projectId: 'proj-crm-db', deploymentId: 'dep-crm-db-prod' }], status: 'active' }] }] }, { id: 'prod-payment', title: 'پلتفرم پرداخت و کیف پول پرداختیار', description: 'زیرساخت مشترک پرداخت برای تمام محصولات سازمان', metadata: { createdAt: '2026-01-05T08:00:00Z', owner: 'تیم توسعه Payment', customFields: { industry: 'FinTech', priority: 'critical', shared: true } }, projects: [{ id: 'proj-payment-gateway', title: 'Payment Gateway Service', description: 'درگاه پرداخت یکپارچه - سرویس مشترک', type: 'service', repositoryUrl: 'https://git.rayanpardaz.ir/payment/gateway', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-payment-db'], deployments: [{ id: 'dep-payment-prod', environmentId: 'env-prod-001', version: '5.3.0', deploymentDate: '2026-09-09T12:00:00Z', releaseNotes: 'پشتیبانی از درگاههای جدید و توکنسازی کارت', requiredDeployments: [{ projectId: 'proj-payment-db', deploymentId: 'dep-payment-db-prod' }], status: 'active' }, { id: 'dep-payment-stage', environmentId: 'env-stage-002', version: '5.4.0-rc.1', deploymentDate: '2026-09-14T10:00:00Z', releaseNotes: 'تست یکپارچگی با شاپرک جدید', requiredDeployments: [], status: 'active' }, { id: 'dep-payment-qa', environmentId: 'env-qa-004', version: '5.4.0-beta.2', deploymentDate: '2026-09-15T15:00:00Z', releaseNotes: 'تستهای خودکار امنیتی و بار', requiredDeployments: [], status: 'active' }] }, { id: 'proj-payment-wallet', title: 'Digital Wallet Service', description: 'سرویس کیف پول الکترونیک', type: 'service', repositoryUrl: 'https://git.rayanpardaz.ir/payment/wallet', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-payment-gateway', 'proj-payment-db'], deployments: [{ id: 'dep-payment-wallet-prod', environmentId: 'env-prod-001', version: '3.1.2', deploymentDate: '2026-09-11T11:00:00Z', releaseNotes: 'افزودن قابلیت انتقال بین کیف پول و کشبک', requiredDeployments: [{ projectId: 'proj-payment-gateway', deploymentId: 'dep-payment-prod' }], status: 'active' }] }, { id: 'proj-payment-db', title: 'Payment Database', description: 'پایگاه داده تراکنشهای مالی', type: 'database', repositoryUrl: 'https://git.rayanpardaz.ir/payment/db', metadata: { framework: 'Oracle 19c RAC', language: 'PL/SQL' }, dependencies: [], deployments: [{ id: 'dep-payment-db-prod', environmentId: 'env-prod-001', version: '6.0.0', deploymentDate: '2026-09-01T02:00:00Z', releaseNotes: 'ارتقای ساختار تراکنشها با قابلیت Audit کامل', requiredDeployments: [], status: 'active' }, { id: 'dep-payment-db-dr', environmentId: 'env-dr-006', version: '6.0.0', deploymentDate: '2026-09-01T03:00:00Z', releaseNotes: 'همگامسازی Active-Active با Production', requiredDeployments: [], status: 'active' }] }, { id: 'proj-payment-web', title: 'Payment Management Portal', description: 'پرتال مدیریت تراکنشها برای اپراتورها', type: 'frontend', repositoryUrl: 'https://git.rayanpardaz.ir/payment/portal', metadata: { framework: 'Angular 18', language: 'TypeScript' }, dependencies: ['proj-payment-gateway'], deployments: [{ id: 'dep-payment-web-prod', environmentId: 'env-prod-001', version: '2.8.1', deploymentDate: '2026-09-12T10:00:00Z', releaseNotes: 'داشبورد تحلیل تراکنشها و مدیریت disputes', requiredDeployments: [{ projectId: 'proj-payment-gateway', deploymentId: 'dep-payment-prod' }], status: 'active' }] }, { id: 'proj-auth-svc', title: 'Authentication & Authorization Service', description: 'سرویس مشترک احراز هویت و مجوزدهی', type: 'service', repositoryUrl: 'https://git.rayanpardaz.ir/shared/auth-svc', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-payment-db'], deployments: [{ id: 'dep-auth-prod', environmentId: 'env-prod-001', version: '4.1.0', deploymentDate: '2026-09-08T06:00:00Z', releaseNotes: 'افزودن MFA و پشتیبانی از OAuth 2.1', requiredDeployments: [], status: 'active' }, { id: 'dep-auth-stage', environmentId: 'env-stage-002', version: '4.2.0-rc.1', deploymentDate: '2026-09-14T08:00:00Z', releaseNotes: 'تست SSO با Azure AD', requiredDeployments: [], status: 'active' }] }, { id: 'proj-notification-svc', title: 'Notification Service', description: 'سرویس مشترک ارسال اعلان (SMS/Email/Push)', type: 'service', repositoryUrl: 'https://git.rayanpardaz.ir/shared/notification-svc', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: [], deployments: [{ id: 'dep-notif-prod', environmentId: 'env-prod-001', version: '2.5.0', deploymentDate: '2026-09-07T07:00:00Z', releaseNotes: 'افزودن کانال WhatsApp و بهینهسازی صف', requiredDeployments: [], status: 'active' }] }, { id: 'proj-inventory-svc', title: 'Inventory Management Service', description: 'سرویس مدیریت موجودی انبار', type: 'service', repositoryUrl: 'https://git.rayanpardaz.ir/shared/inventory-svc', metadata: { framework: '.NET 8', language: 'C#' }, dependencies: ['proj-payment-db'], deployments: [{ id: 'dep-inventory-prod', environmentId: 'env-prod-001', version: '2.0.3', deploymentDate: '2026-09-06T10:00:00Z', releaseNotes: 'همگامسازی Real-time با انبارهای فیزیکی', requiredDeployments: [], status: 'active' }] }, { id: 'proj-video-streaming', title: 'Video Streaming Infrastructure', description: 'زیرساخت استریم ویدیو (CDN + Transcoding)', type: 'infrastructure', repositoryUrl: 'https://git.rayanpardaz.ir/shared/video-infra', metadata: { framework: 'Terraform + AWS', language: 'HCL' }, dependencies: [], deployments: [{ id: 'dep-video-infra-prod', environmentId: 'env-prod-001', version: '1.3.0', deploymentDate: '2026-09-05T05:00:00Z', releaseNotes: 'افزودن CDN منطقهای و Adaptive Bitrate', requiredDeployments: [], status: 'active' }] }] }] }
|
||
};
|
||
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: '🏗️' }];
|
||
/* ==================== HELPERS ==================== */
|
||
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.opacity = '0'; setTimeout(() => t.remove(), 300) }, dur) }
|
||
function showD3Tooltip(e, text) { let tt = document.getElementById('d3Tooltip'); if (!tt) return; tt.textContent = text; tt.classList.add('visible'); tt.style.left = (e.clientX + 12) + 'px'; tt.style.top = (e.clientY + 12) + 'px' }
|
||
function hideD3Tooltip() { const tt = document.getElementById('d3Tooltip'); if (tt) tt.classList.remove('visible') }
|
||
function refreshUI() {
|
||
ToolbarManager.updateStats();
|
||
BoardRenderer.render();
|
||
}
|
||
/* ==================== MODAL MANAGER ==================== */
|
||
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]) }, 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() } }) } };
|
||
/* ==================== TOOLBAR MANAGER ==================== */
|
||
const ToolbarManager = { currentFilters: { query: '', typeFilter: '', envFilter: '' }, init() { this.populateEnvironmentFilter(); this.bindEvents(); this.updateStats() }, populateEnvironmentFilter() { const s = document.getElementById('filterEnvironment'); if (!s) return; s.innerHTML = '<option value="">همه محیطها</option>' + projectData.appData.environments.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(); refreshUI() }); if (ft) ft.addEventListener('change', () => { this.applyFilters(); refreshUI() }); if (fe) fe.addEventListener('change', () => { this.applyFilters(); refreshUI() }) }, 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); const mt = !f.typeFilter || pr.type === f.typeFilter; const me = !f.envFilter || (pr.deployments || []).filter(d => d.status === 'active').some(d => d.environmentId === f.envFilter); return mq && mt && me }) } };
|
||
/* ==================== HISTORY MANAGER ==================== */
|
||
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() { refreshUI(); 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() } }) } };
|
||
/* ==================== SECTION COLLAPSE ==================== */
|
||
const SectionCollapseManager = { storageKey: 'boardSectionCollapseState', getState() { try { const s = localStorage.getItem(this.storageKey); return s ? JSON.parse(s) : { dashboard: false, products: false, environments: false } } catch { return { dashboard: false, products: false, environments: false } } }, saveState(s) { try { localStorage.setItem(this.storageKey, JSON.stringify(s)) } catch (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) { this.toggle(sec.id.replace('section-', '')) } } }) } };
|
||
/* ==================== DASHBOARD SECTION ==================== */
|
||
const DashboardSectionRenderer = {
|
||
getSectionHTML() {
|
||
const d = this.getData();
|
||
let h = `<div class="board-section" id="section-dashboard">`;
|
||
h += `<div class="board-header"><div class="board-header-title"><span class="collapse-icon">▼</span>📊 داشبورد مدیریتی</div><div class="board-header-actions"><button class="btn btn-accent btn-sm" onclick="DashboardManager.open()">📊 مشاهده کامل</button></div></div>`;
|
||
h += `<div class="section-content">`;
|
||
|
||
// KPI Cards
|
||
h += '<div class="dashboard-kpi-row" style="margin-bottom:16px">';
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📦</div><div class="dashboard-kpi-value">${d.products}</div><div class="dashboard-kpi-label">محصول</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🌿</div><div class="dashboard-kpi-value">${d.projects}</div><div class="dashboard-kpi-label">پروژه</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔗</div><div class="dashboard-kpi-value">${d.deps}</div><div class="dashboard-kpi-label">وابستگی</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🚀</div><div class="dashboard-kpi-value">${d.deploys}</div><div class="dashboard-kpi-label">استقرار</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🌍</div><div class="dashboard-kpi-value">${d.environments}</div><div class="dashboard-kpi-label">محیط</div></div>`;
|
||
h += '</div>';
|
||
|
||
// ✅ NEW: Charts Grid
|
||
h += '<div class="dashboard-charts-grid">';
|
||
h += '<div class="dashboard-chart-card"><div class="dashboard-chart-header">🥧 توزیع نوع پروژهها</div><div class="dashboard-chart-body" id="mainDashTypeChart"></div></div>';
|
||
h += '<div class="dashboard-chart-card"><div class="dashboard-chart-header">🌍 استقرارها به تفکیک محیط</div><div class="dashboard-chart-body" id="mainDashEnvChart"></div></div>';
|
||
h += '<div class="dashboard-chart-card"><div class="dashboard-chart-header">✅ وضعیت استقرار پروژهها</div><div class="dashboard-chart-body" id="mainDashStatusChart"></div></div>';
|
||
h += '<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 پروژهها به تفکیک محصول</div><div class="dashboard-chart-body" id="mainDashProductChart"></div></div>';
|
||
h += '</div>';
|
||
|
||
h += '</div></div>';
|
||
return h;
|
||
},
|
||
|
||
getData() {
|
||
const ps = projectData.appData.products || [];
|
||
let tp = 0, td = 0, tdp = 0;
|
||
const tc = {}, edc = {}, ppc = [];
|
||
let pwd = 0, pwod = 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 active = (pr.deployments || []).filter(d => d.status === 'active');
|
||
tdp += active.length;
|
||
if (active.length > 0) pwd++; else pwod++;
|
||
tc[pr.type] = (tc[pr.type] || 0) + 1;
|
||
active.forEach(d => {
|
||
const env = projectData.appData.environments.find(e => e.id === d.environmentId);
|
||
const envName = env ? env.name : 'نامشخص';
|
||
edc[envName] = (edc[envName] || 0) + 1;
|
||
});
|
||
});
|
||
});
|
||
|
||
return { products: ps.length, projects: tp, deps: td, deploys: tdp, environments: projectData.appData.environments.length, tc, edc, ppc, pwd, pwod };
|
||
},
|
||
|
||
// ✅ NEW: Render charts after DOM is ready
|
||
renderCharts() {
|
||
const d = this.getData();
|
||
this.renderDonut('mainDashTypeChart', Object.entries(d.tc).map(([k, v]) => ({ label: this.getTypeLabel(k), value: v, color: this.getTypeColor(k) })));
|
||
this.renderBar('mainDashEnvChart', Object.entries(d.edc).map(([k, v]) => ({ label: k, value: v })), '#38a169');
|
||
this.renderDonut('mainDashStatusChart', [{ label: 'با استقرار', value: d.pwd, color: '#38a169' }, { label: 'بدون استقرار', value: d.pwod, color: '#e53e3e' }]);
|
||
this.renderBar('mainDashProductChart', d.ppc.map(p => ({ label: p.title.length > 15 ? p.title.substring(0, 15) + '...' : p.title, value: p.count })), '#3182ce');
|
||
},
|
||
|
||
renderDonut(cid, data) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
c.innerHTML = '';
|
||
if (!data || data.length === 0 || data.every(d => d.value === 0)) {
|
||
c.innerHTML = '<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>';
|
||
return;
|
||
}
|
||
const w = 240, hgt = 200, r = Math.min(w, hgt) / 2 - 15;
|
||
const svg = d3.select(c).append('svg').attr('width', w).attr('height', hgt);
|
||
const g = svg.append('g').attr('transform', `translate(${w / 2},${hgt / 2})`);
|
||
const pie = d3.pie().value(d => d.value).sort(null);
|
||
const arc = d3.arc().innerRadius(r * .55).outerRadius(r);
|
||
const arcH = d3.arc().innerRadius(r * .55).outerRadius(r + 6);
|
||
const total = data.reduce((s, d) => s + d.value, 0);
|
||
g.selectAll('path').data(pie(data)).join('path')
|
||
.attr('d', arc).attr('fill', d => d.data.color)
|
||
.attr('stroke', '#fff').attr('stroke-width', 2)
|
||
.style('cursor', 'pointer')
|
||
.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 => showD3Tooltip(e, ''))
|
||
.on('mouseout', function () { d3.select(this).transition().duration(150).attr('d', arc); hideD3Tooltip() });
|
||
g.append('text').attr('text-anchor', 'middle').attr('dy', '-.2em').attr('fill', 'var(--text)').attr('font-size', '20px').attr('font-weight', '800').text(total);
|
||
g.append('text').attr('text-anchor', 'middle').attr('dy', '1.3em').attr('fill', 'var(--text-muted)').attr('font-size', '10px').text('پروژه');
|
||
},
|
||
|
||
renderBar(cid, data, color) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
c.innerHTML = '';
|
||
if (!data || data.length === 0) {
|
||
c.innerHTML = '<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>';
|
||
return;
|
||
}
|
||
const w = 280, hgt = 200, m = { top: 20, right: 15, bottom: 40, left: 30 };
|
||
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
|
||
const svg = d3.select(c).append('svg').attr('width', w).attr('height', hgt);
|
||
const g = svg.append('g').attr('transform', `translate(${m.left},${m.top})`);
|
||
const x = d3.scaleBand().domain(data.map(d => d.label)).range([0, iw]).padding(.3);
|
||
const y = d3.scaleLinear().domain([0, d3.max(data, d => d.value) || 1]).nice().range([ih, 0]);
|
||
g.append('g').attr('transform', `translate(0,${ih})`).call(d3.axisBottom(x).tickSize(0)).selectAll('text').attr('fill', 'var(--text-muted)').attr('font-size', '8px').attr('font-family', 'Vazirmatn').attr('transform', 'rotate(-20)').attr('text-anchor', 'end');
|
||
g.append('g').call(d3.axisLeft(y).ticks(4).tickSize(-iw)).selectAll('text').attr('fill', 'var(--text-muted)').attr('font-size', '9px');
|
||
g.selectAll('rect').data(data).join('rect')
|
||
.attr('x', d => x(d.label)).attr('y', d => y(d.value))
|
||
.attr('width', x.bandwidth()).attr('height', d => ih - y(d.value))
|
||
.attr('fill', color).attr('rx', 4).style('cursor', 'pointer')
|
||
.on('mouseover', function (e, d) { d3.select(this).attr('fill', 'var(--accent)'); showD3Tooltip(e, `${d.label}: ${d.value}`) })
|
||
.on('mousemove', e => showD3Tooltip(e, ''))
|
||
.on('mouseout', function () { d3.select(this).attr('fill', color); hideD3Tooltip() });
|
||
g.selectAll('.bar-label').data(data).join('text')
|
||
.attr('x', d => x(d.label) + x.bandwidth() / 2).attr('y', d => y(d.value) - 5)
|
||
.attr('text-anchor', 'middle').attr('fill', 'var(--text)').attr('font-size', '10px').attr('font-weight', '700').text(d => d.value);
|
||
},
|
||
|
||
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' }
|
||
};
|
||
/* ==================== ENVIRONMENT RENDERER ==================== */
|
||
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"><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 || '#fff'}">${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 } };
|
||
/* ==================== BOARD RENDERER ==================== */
|
||
const BoardRenderer = {
|
||
render() {
|
||
//
|
||
const board = document.getElementById('boardArea'); const products = projectData.appData.products || []; const fp = products.filter(p => ToolbarManager.shouldShowProduct(p)); let h = ''; h += DashboardSectionRenderer.getSectionHTML(); 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.deployments || []).filter(d => d.status === 'active').length, 0); h += `<div class="project-card"><div class="project-card-header"><div class="project-card-title-row"><span style="font-size:22px">📦</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-xs" 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') });
|
||
|
||
//
|
||
setTimeout(() => { DashboardSectionRenderer.renderCharts(); }, 100);
|
||
}
|
||
};
|
||
/* ==================== ACTION REGISTRY ==================== */
|
||
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') }, loadProduct() { const inp = document.createElement('input'); inp.type = 'file'; inp.accept = '.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'); return } HistoryManager.pushState(); projectData.appData = ld; updateProjectUI(); renderToolGroups(); ToolbarManager.populateEnvironmentFilter(); refreshUI(); showToast('📂 بارگذاری شد', 'success') } catch (err) { showToast('❌ خطا: ' + err.message, 'error') } }; r.readAsText(f) }); inp.click() }, clearAll() { if (!confirm('⚠️ پاکسازی کامل؟ این عمل غیرقابل بازگشت است!')) return; if (!confirm('⚠️ تأیید نهایی؟')) return; HistoryManager.pushState(); projectData.appData = { environments: [], products: [] }; ToolbarManager.populateEnvironmentFilter(); refreshUI(); showToast('🗑️ پاکسازی شد', 'success') }, showGalaxyChart() { GalaxyChartManager.open() }, showExecutiveDashboard() { DashboardManager.open() }, showDependencyAnalysis() { DependencyAnalysisManager.open() }, showDeploymentAnalysis() { DeploymentAnalysisManager.open() }, showVersionAnalysis() { VersionAnalysisManager.open() }, showRiskAnalysis() { RiskAnalysisManager.open() }, defaultAction(tool) { showToast(`ابزار: ${tool.title}`, 'info') } };
|
||
/* ==================== WEBHOOK MANAGER ==================== */
|
||
const WebhookManager = { getConfig() { return projectData.webhook }, setConfig(c) { projectData.webhook = { ...projectData.webhook, ...c }; this.updateStatusUI() }, isValid() { const c = this.getConfig(); return c.enabled && c.url && c.clientId && /^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 { d.classList.add('success'); t.textContent = 'وضعیت: آماده' } }, async sendEvent(en, payload = {}) { if (!this.isValid()) return; const c = this.getConfig(); try { 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 }) }) } catch (e) { } } };
|
||
/* ==================== ENVIRONMENT MANAGER ==================== */
|
||
const EnvironmentManager = { currentEditEnvId: null, openAddModal() { document.getElementById('newEnvName').value = ''; document.getElementById('newEnvDesc').value = ''; document.getElementById('newEnvBgColor').value = '#38a169'; document.getElementById('newEnvTextColor').value = '#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(); refreshUI(); showToast(`محیط "${n}" اضافه شد`, 'success') }, 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'; 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 }, 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(); refreshUI(); showToast(`محیط "${env.name}" ویرایش شد`, 'success') }, 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(); refreshUI(); showToast(`"${env.name}" حذف شد`, 'warning') } };
|
||
/* ==================== PRODUCT MANAGER ==================== */
|
||
const ProductManager = {
|
||
currentDetailProductId: null, currentEditProjectId: null, currentDependencyProjectId: null, currentDependencyProjId: null, currentDeploymentProjId: null, currentDeploymentMode: null, currentDeploymentEnvId: null, currentEditDeploymentId: null, getTypeInfo(type) { return VALID_PROJECT_TYPES.find(t => t.value === type) || { value: type, label: type, icon: '❓' } }, getEnvironmentName(envId) { const e = projectData.appData.environments.find(x => x.id === envId); return e ? e.name : envId }, getEnvironmentColors(envId) { const e = projectData.appData.environments.find(x => x.id === envId); return { backgroundColor: e?.backgroundColor || '#38a169', textColor: e?.textColor || '#ffffff' } }, findProjectById(pid) { for (const prod of projectData.appData.products) { const pr = prod.projects.find(p => p.id === pid); if (pr) return pr } return null }, getActiveDeployments(proj) { return (proj.deployments || []).filter(d => d.status === 'active') }, getEnvironmentsWithoutDeployment(proj) { const allEnvIds = projectData.appData.environments.map(e => e.id); const deployedEnvIds = this.getActiveDeployments(proj).map(d => d.environmentId); return allEnvIds.filter(id => !deployedEnvIds.includes(id)) },
|
||
showDetail(productId) { const prod = projectData.appData.products.find(p => p.id === productId); if (!prod) return; this.currentDetailProductId = productId; document.getElementById('projectDetailTitle').innerHTML = `<span>📦</span><span>${prod.title}</span>`; const projects = prod.projects || []; const totalDeploys = projects.reduce((s, pr) => s + this.getActiveDeployments(pr).length, 0); let html = `<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">${totalDeploys}</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) { html += '<div class="empty-subprojects">🌿 هیچ پروژهای نیست</div>' } else { html += '<div class="subproject-list">'; projects.forEach(pr => { const ti = this.getTypeInfo(pr.type); const deps = pr.dependencies || []; const deploys = this.getActiveDeployments(pr); const emptyEnvs = this.getEnvironmentsWithoutDeployment(pr); const canAdd = emptyEnvs.length > 0; html += `<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.openDependencyManager('${prod.id}','${pr.id}')">🔗 وابستگیها</button><button class="btn btn-ghost btn-sm" onclick="ProductManager.openProjectEditModal('${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(depId => { const dp = this.findProjectById(depId); return dp ? dp.title : depId }).join('، ')}</span></div>` : ''}</div>`; html += `<div class="deployment-section"><div class="deployment-header"><span class="deployment-header-title">🚀 استقرارهای فعال (${deploys.length})</span>${canAdd ? `<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 (deploys.length === 0) { html += '<div class="empty-deployments">هنوز استقرار ندارد</div>' } else { html += '<div class="deployment-list">'; deploys.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') : '—'; html += `<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.id}')">✏️</button><button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.id}')">🗑️</button></div>`; if (d.releaseNotes) { html += `<div class="deployment-notes">📝 ${d.releaseNotes}</div>` } if (d.requiredDeployments && d.requiredDeployments.length > 0) { html += `<div class="deployment-req">🔗 نیازمندیها: ${d.requiredDeployments.map(rd => { const rp = this.findProjectById(rd.projectId); let depVer = ''; if (rp) { const rdDep = (rp.deployments || []).find(x => x.id === rd.deploymentId); if (rdDep) depVer = ` (v${rdDep.version})` } return `${rp ? rp.title : rd.projectId}${depVer}` }).join('، ')}</div>` } html += `</div>` }); html += '</div>' } html += `</div></div>` }); html += '</div>' } html += `</div></div>`; document.getElementById('projectDetailBody').innerHTML = html; 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'); ModalManager.open('addProductModal') },
|
||
createNewProduct() { const title = document.getElementById('newProductTitle').value.trim(); if (!title || title.length < 3) { showToast('عنوان الزامی است (حداقل ۳ کاراکتر)', 'error'); return } if (projectData.appData.products.some(p => p.title.toLowerCase() === title.toLowerCase())) { showToast('تکراری است', 'warning'); return } HistoryManager.pushState(); projectData.appData.products.push({ id: `prod-${Date.now()}`, title, description: document.getElementById('newProductDescription').value.trim() || '', metadata: { createdAt: new Date().toISOString(), owner: document.getElementById('newProductOwner').value.trim() || '', customFields: {} }, projects: [] }); ModalManager.close('addProductModal'); refreshUI(); showToast(`"${title}" ایجاد شد`, 'success') },
|
||
openDependencyManager(productId, projId) { this.currentDependencyProjectId = productId; 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((depId, index) => { const depPr = this.findProjectById(depId); const projectName = depPr ? depPr.title : depId; const typeInfo = depPr ? this.getTypeInfo(depPr.type) : null; return `<div class="dependency-item"><div class="dependency-info"><div><div class="dependency-project-name">${typeInfo ? typeInfo.icon + ' ' : ''}${projectName}</div></div></div><button class="btn btn-danger btn-xs" onclick="ProductManager.removeDependency(${index})">🗑️</button></div>` }).join('') },
|
||
removeDependency(index) { 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(index, 1); ModalManager.close('dependencyManagerModal'); refreshUI(); if (this.currentDetailProductId) this.showDetail(this.currentDetailProductId); showToast('وابستگی حذف شد', 'success') },
|
||
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 allProjects = []; projectData.appData.products.forEach(prod => { prod.projects.forEach(pr => { if (pr.id !== cpr.id) allProjects.push({ ...pr, productName: prod.title }) }) }); if (allProjects.length === 0) { rc.innerHTML = '<div class="empty-dependencies">پروژه دیگری نیست</div>'; return } rc.innerHTML = allProjects.map(pr => { const isDep = (cpr.dependencies || []).includes(pr.id); return `<div class="dependency-project-result"><button class="dependency-project-add-btn" onclick="ProductManager.addDependency('${pr.id}')" ${isDep ? 'disabled style="opacity:.5"' : ''}><span>${this.getTypeInfo(pr.type).icon}</span><span>${pr.title}</span><small style="color:var(--text-muted);font-weight:normal">(${pr.productName})</small>${isDep ? '<span style="margin-right:auto;font-size:10px;color:var(--success)">✓ وابسته</span>' : ''}</button></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 } 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 allProjects = []; projectData.appData.products.forEach(prod => { prod.projects.forEach(pr => { if (pr.id !== cpr.id) allProjects.push({ ...pr, productName: prod.title }) }) }); const fl = allProjects.filter(pr => pr.title.toLowerCase().includes(q) || pr.productName.toLowerCase().includes(q) || (pr.description || '').toLowerCase().includes(q)); const rc = document.getElementById('dependencySearchResults'); if (fl.length === 0) { rc.innerHTML = '<div class="empty-dependencies">نتیجهای نیست</div>'; return } rc.innerHTML = fl.map(pr => { const isDep = (cpr.dependencies || []).includes(pr.id); return `<div class="dependency-project-result"><button class="dependency-project-add-btn" onclick="ProductManager.addDependency('${pr.id}')" ${isDep ? 'disabled style="opacity:.5"' : ''}><span>${this.getTypeInfo(pr.type).icon}</span><span>${pr.title}</span><small style="color:var(--text-muted)">(${pr.productName})</small></button></div>` }).join('') } },
|
||
addDependency(projectId) { 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 ((pr.dependencies || []).includes(projectId)) { showToast('تکراری است', 'warning'); return } HistoryManager.pushState(); if (!pr.dependencies) pr.dependencies = []; pr.dependencies.push(projectId); ModalManager.close('dependencyManagerModal'); refreshUI(); if (this.currentDetailProductId) this.showDetail(this.currentDetailProductId); showToast('وابستگی اضافه شد', 'success') },
|
||
openProductEditModal() { const pid = this.currentDetailProductId; if (!pid) return; const prod = projectData.appData.products.find(p => p.id === pid); 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 pid = this.currentDetailProductId; if (!pid) return; const prod = projectData.appData.products.find(p => p.id === pid); 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'); refreshUI(); this.showDetail(pid); showToast(`"${prod.title}" ویرایش شد`, 'success') },
|
||
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 pid = this.currentDetailProductId; if (!pid) return; const prod = projectData.appData.products.find(p => p.id === pid); if (!prod) return; const title = document.getElementById('newProjectTitle').value.trim(); const type = document.getElementById('newProjectType').value; if (!title || !type) { showToast('عنوان و نوع الزامی است', 'error'); return } HistoryManager.pushState(); const np = { id: `proj-${Date.now()}`, title, description: document.getElementById('newProjectDescription').value.trim() || '', type, repositoryUrl: document.getElementById('newProjectRepo').value.trim(), metadata: { createdAt: new Date().toISOString(), customFields: {} }, dependencies: [], deployments: [] }; 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); ModalManager.close('addProjectModal'); refreshUI(); this.showDetail(pid); showToast(`"${title}" ایجاد شد`, 'success') },
|
||
openProjectEditModal(productId, projId) { const prod = projectData.appData.products.find(p => p.id === productId); 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 pid = this.currentDetailProductId; const projId = this.currentEditProjectId; if (!pid || !projId) 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; const nt = document.getElementById('editProjTitle').value.trim(); const nty = document.getElementById('editProjType').value; if (!nt || !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'); refreshUI(); this.showDetail(pid); showToast(`"${pr.title}" ویرایش شد`, 'success') },
|
||
openAddDeploymentModal(productId, projId) { if (!productId || !projId) return; const prod = projectData.appData.products.find(p => p.id === productId); if (!prod) return; const pr = prod.projects.find(p => p.id === projId); if (!pr) return; const emptyEnvs = this.getEnvironmentsWithoutDeployment(pr); if (emptyEnvs.length === 0) { showToast('همه محیطها استقرار دارند', 'warning'); return } this.currentDeploymentProjId = projId; this.currentDeploymentMode = 'add'; this.currentDeploymentEnvId = null; this.currentEditDeploymentId = null; document.getElementById('deploymentModalTitle').innerHTML = '<span>🚀</span><span>افزودن نسخه استقرار</span>'; const envGroup = document.getElementById('deploymentEnvGroup'); const envs = projectData.appData.environments.filter(e => emptyEnvs.includes(e.id)); envGroup.innerHTML = `<label class="form-label">محیط استقرار <span class="required">*</span></label><select class="form-select" id="depEnvironment" onchange="ProductManager.onEnvChange()">${envs.map(e => { const cl = this.getEnvironmentColors(e.id); return `<option value="${e.id}" style="background-color:${cl.backgroundColor};color:${cl.textColor}">${e.name} - ${e.description}</option>` }).join('')}</select>`; document.getElementById('depVersion').value = ''; document.getElementById('depDate').value = ''; document.getElementById('depReleaseNotes').value = ''; this.renderRequiredDeploymentsSection(); ModalManager.open('deploymentModal') },
|
||
openEditDeploymentModal(productId, projId, depId) { if (!productId || !projId || !depId) return; const prod = projectData.appData.products.find(p => p.id === productId); if (!prod) return; const pr = prod.projects.find(p => p.id === projId); if (!pr) return; const dep = (pr.deployments || []).find(d => d.id === depId); if (!dep) return; this.currentDeploymentProjId = projId; this.currentDeploymentMode = 'edit'; this.currentDeploymentEnvId = dep.environmentId; this.currentEditDeploymentId = depId; document.getElementById('deploymentModalTitle').innerHTML = '<span>✏️</span><span>ویرایش نسخه استقرار</span>'; const envGroup = document.getElementById('deploymentEnvGroup'); const cl = this.getEnvironmentColors(dep.environmentId); envGroup.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(dep.environmentId)}</div>`; document.getElementById('depVersion').value = dep.version; let dv = ''; if (dep.deploymentDate) { const d = new Date(dep.deploymentDate); 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; document.getElementById('depReleaseNotes').value = dep.releaseNotes || ''; this.renderRequiredDeploymentsSection(dep); ModalManager.open('deploymentModal') },
|
||
onEnvChange() { this.renderRequiredDeploymentsSection() },
|
||
renderRequiredDeploymentsSection(existingDep) { const section = document.getElementById('requiredDeploymentsSection'); const envSelect = document.getElementById('depEnvironment'); const envId = this.currentDeploymentMode === 'edit' ? this.currentDeploymentEnvId : (envSelect ? envSelect.value : null); if (!envId) { section.innerHTML = ''; return } 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 deps = pr.dependencies || []; if (deps.length === 0) { section.innerHTML = ''; return } let h = '<div class="form-group"><label class="form-label">🔗 استقرارهای مورد نیاز (به ازای هر وابستگی) <span class="required">*</span></label><div class="form-hint">برای هر وابستگی، یک استقرار از آن پروژه در محیط "' + this.getEnvironmentName(envId) + '" انتخاب کنید</div>'; deps.forEach(depId => { const depPr = this.findProjectById(depId); if (!depPr) return; const depDeployments = (depPr.deployments || []).filter(d => d.status === 'active' && d.environmentId === envId); const existingReq = existingDep ? (existingDep.requiredDeployments || []).find(r => r.projectId === depId) : null; h += `<div class="req-deployment-box"><div class="req-deployment-title">📦 ${depPr.title}</div>`; if (depDeployments.length === 0) { h += `<div style="color:var(--danger);font-size:11px">⚠️ هیچ استقرار فعالی در ${this.getEnvironmentName(envId)} ندارد.</div>` } else { h += `<select class="req-deployment-select" data-dep-project="${depId}">${depDeployments.map(d => `<option value="${d.id}" ${existingReq && existingReq.deploymentId === d.id ? 'selected' : ''}>v${d.version} - ${d.deploymentDate ? new Date(d.deploymentDate).toLocaleDateString('fa-IR') : ''}</option>`).join('')}</select>` } h += '</div>' }); h += '</div>'; section.innerHTML = h },
|
||
saveDeployment() { const pid = this.currentDetailProductId; const projId = this.currentDeploymentProjId; if (!pid || !projId) { showToast('خطا: محصول یا پروژه مشخص نشده', 'error'); 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; const version = document.getElementById('depVersion').value.trim(); const date = document.getElementById('depDate').value; const releaseNotes = document.getElementById('depReleaseNotes').value.trim(); if (!version) { showToast('نسخه الزامی است', 'error'); return } if (!date) { showToast('تاریخ استقرار الزامی است', 'error'); return } if (!releaseNotes) { showToast('ریلیز نوت الزامی است', 'error'); return } const deploymentDate = new Date(date).toISOString(); HistoryManager.pushState(); if (!pr.deployments) pr.deployments = []; const requiredDeployments = []; document.querySelectorAll('.req-deployment-select').forEach(sel => { const depProjectId = sel.dataset.depProject; const depDeploymentId = sel.value; if (depProjectId && depDeploymentId) { requiredDeployments.push({ projectId: depProjectId, deploymentId: depDeploymentId }) } }); if (this.currentDeploymentMode === 'add') { const envId = document.getElementById('depEnvironment').value; if (!envId) { showToast('محیط را انتخاب کنید', 'error'); return } pr.deployments.push({ id: `dep-${Date.now()}`, environmentId: envId, version, deploymentDate, releaseNotes, requiredDeployments, status: 'active' }); showToast(`v${version} در ${this.getEnvironmentName(envId)} استقرار یافت`, 'success') } else { const depId = this.currentEditDeploymentId; const dep = pr.deployments.find(d => d.id === depId); if (dep) { dep.version = version; dep.deploymentDate = deploymentDate; dep.releaseNotes = releaseNotes; dep.requiredDeployments = requiredDeployments; showToast(`استقرار ویرایش شد`, 'success') } } ModalManager.close('deploymentModal'); refreshUI(); this.showDetail(pid) },
|
||
removeDeployment(productId, projId, depId) { if (!confirm('حذف این استقرار؟')) return; const prod = projectData.appData.products.find(p => p.id === productId); if (!prod) return; const pr = prod.projects.find(p => p.id === projId); if (!pr) return; const dep = (pr.deployments || []).find(d => d.id === depId); if (!dep) return; HistoryManager.pushState(); dep.status = 'removed'; refreshUI(); this.showDetail(productId); showToast('استقرار حذف شد', 'warning') },
|
||
removeProduct(productId) { const prod = projectData.appData.products.find(p => p.id === productId); if (!prod) return; if (!confirm(`حذف "${prod.title}"؟`)) return; HistoryManager.pushState(); projectData.appData.products = projectData.appData.products.filter(p => p.id !== productId); refreshUI(); ModalManager.close('projectDetailModal'); showToast(`"${prod.title}" حذف شد`, 'warning') },
|
||
removeProject(productId, projId) { const prod = projectData.appData.products.find(p => p.id === productId); 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); refreshUI(); if (this.currentDetailProductId === productId) this.showDetail(productId); showToast(`"${pr.title}" حذف شد`, 'warning') }
|
||
};
|
||
/* ==================== ✅ DASHBOARD MANAGER (COMPLETED WITH D3 CHARTS) ==================== */
|
||
const DashboardManager = {
|
||
open() { ModalManager.open('executiveDashboardModal'); setTimeout(() => this.render(), 150) },
|
||
getReportData() {
|
||
const ps = projectData.appData.products || [];
|
||
let tp = 0, td = 0, tdp = 0; const tc = {}, edc = {}, ppc = [], fc = {}; let pwd = 0, pwod = 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 active = (pr.deployments || []).filter(d => d.status === 'active');
|
||
tdp += active.length;
|
||
if (active.length > 0) pwd++; else pwod++;
|
||
tc[pr.type] = (tc[pr.type] || 0) + 1;
|
||
const fw = pr.metadata?.framework || 'نامشخص';
|
||
fc[fw] = (fc[fw] || 0) + 1;
|
||
active.forEach(d => {
|
||
const env = projectData.appData.environments.find(e => e.id === d.environmentId);
|
||
const envName = env ? env.name : 'نامشخص';
|
||
edc[envName] = (edc[envName] || 0) + 1;
|
||
});
|
||
});
|
||
});
|
||
return { products: ps.length, projects: tp, deps: td, deploys: tdp, environments: projectData.appData.environments.length, tc, edc, ppc, fc, pwd, pwod };
|
||
},
|
||
render() {
|
||
const c = document.getElementById('dashboardContainer'); if (!c) return;
|
||
const d = this.getReportData();
|
||
let h = '<div class="dashboard-kpi-row">';
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📦</div><div class="dashboard-kpi-value">${d.products}</div><div class="dashboard-kpi-label">محصول</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🌿</div><div class="dashboard-kpi-value">${d.projects}</div><div class="dashboard-kpi-label">پروژه</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔗</div><div class="dashboard-kpi-value">${d.deps}</div><div class="dashboard-kpi-label">وابستگی</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🚀</div><div class="dashboard-kpi-value">${d.deploys}</div><div class="dashboard-kpi-label">استقرار</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🌍</div><div class="dashboard-kpi-value">${d.environments}</div><div class="dashboard-kpi-label">محیط</div></div>`;
|
||
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="dashTypeChart"></div></div>';
|
||
h += '<div class="dashboard-chart-card"><div class="dashboard-chart-header">🌍 استقرارها به تفکیک محیط</div><div class="dashboard-chart-body" id="dashEnvChart"></div></div>';
|
||
h += '<div class="dashboard-chart-card"><div class="dashboard-chart-header">✅ وضعیت استقرار پروژهها</div><div class="dashboard-chart-body" id="dashStatusChart"></div></div>';
|
||
h += '<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 پروژهها به تفکیک محصول</div><div class="dashboard-chart-body" id="dashProductChart"></div></div>';
|
||
h += '<div class="dashboard-chart-card"><div class="dashboard-chart-header">🛠️ توزیع فریمورکها</div><div class="dashboard-chart-body" id="dashFrameworkChart"></div></div>';
|
||
h += '</div>';
|
||
c.innerHTML = h;
|
||
this.renderDonut('dashTypeChart', Object.entries(d.tc).map(([k, v]) => ({ label: this.getTypeLabel(k), value: v, color: this.getTypeColor(k) })));
|
||
this.renderBar('dashEnvChart', Object.entries(d.edc).map(([k, v]) => ({ label: k, value: v })), '#38a169');
|
||
this.renderDonut('dashStatusChart', [{ label: 'با استقرار', value: d.pwd, color: '#38a169' }, { label: 'بدون استقرار', value: d.pwod, color: '#e53e3e' }]);
|
||
this.renderBar('dashProductChart', d.ppc.map(p => ({ label: p.title.length > 18 ? p.title.substring(0, 18) + '...' : p.title, value: p.count })), '#3182ce');
|
||
this.renderBarH('dashFrameworkChart', Object.entries(d.fc).sort((a, b) => b[1] - a[1]).slice(0, 8).map(([k, v]) => ({ label: k.length > 14 ? k.substring(0, 14) + '...' : k, value: v })));
|
||
const coverage = (d.pwd + d.pwod) > 0 ? Math.round(d.pwd / (d.pwd + d.pwod) * 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">${d.deploys}</span></span><span>📈 پوشش استقرار: <span class="stat-highlight">${coverage}%</span></span>`;
|
||
document.getElementById('dashboardRefresh').onclick = () => this.render();
|
||
},
|
||
renderDonut(cid, data) {
|
||
const c = document.getElementById(cid); if (!c) return; c.innerHTML = '';
|
||
if (!data || data.length === 0 || data.every(d => d.value === 0)) { c.innerHTML = '<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>'; return }
|
||
const w = 280, hgt = 220, r = Math.min(w, hgt) / 2 - 15;
|
||
const svg = d3.select(c).append('svg').attr('width', w).attr('height', hgt);
|
||
const g = svg.append('g').attr('transform', `translate(${w / 2},${hgt / 2})`);
|
||
const pie = d3.pie().value(dd => dd.value).sort(null);
|
||
const arc = d3.arc().innerRadius(r * .55).outerRadius(r);
|
||
const arcH = d3.arc().innerRadius(r * .55).outerRadius(r + 8);
|
||
const total = data.reduce((s, dd) => s + dd.value, 0);
|
||
g.selectAll('path').data(pie(data)).join('path')
|
||
.attr('d', arc).attr('fill', dd => dd.data.color)
|
||
.attr('stroke', '#fff').attr('stroke-width', 2)
|
||
.style('cursor', 'pointer')
|
||
.on('mouseover', function (e, dd) { d3.select(this).transition().duration(150).attr('d', arcH(dd)); showD3Tooltip(e, `${dd.data.label}: ${dd.data.value} (${Math.round(dd.data.value / total * 100)}%)`) })
|
||
.on('mousemove', e => showD3Tooltip(e, ''))
|
||
.on('mouseout', function () { d3.select(this).transition().duration(150).attr('d', arc); hideD3Tooltip() });
|
||
g.append('text').attr('text-anchor', 'middle').attr('dy', '-.2em').attr('fill', 'var(--text)').attr('font-size', '24px').attr('font-weight', '800').text(total);
|
||
g.append('text').attr('text-anchor', 'middle').attr('dy', '1.4em').attr('fill', 'var(--text-muted)').attr('font-size', '11px').text('پروژه');
|
||
},
|
||
renderBar(cid, data, color) {
|
||
const c = document.getElementById(cid); if (!c) return; c.innerHTML = '';
|
||
if (!data || data.length === 0) { c.innerHTML = '<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>'; return }
|
||
const w = 320, hgt = 220, m = { top: 20, right: 15, bottom: 45, left: 35 };
|
||
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
|
||
const svg = d3.select(c).append('svg').attr('width', w).attr('height', hgt);
|
||
const g = svg.append('g').attr('transform', `translate(${m.left},${m.top})`);
|
||
const x = d3.scaleBand().domain(data.map(dd => dd.label)).range([0, iw]).padding(.3);
|
||
const y = d3.scaleLinear().domain([0, d3.max(data, dd => dd.value) || 1]).nice().range([ih, 0]);
|
||
g.append('g').attr('transform', `translate(0,${ih})`).call(d3.axisBottom(x).tickSize(0)).selectAll('text').attr('fill', 'var(--text-muted)').attr('font-size', '8px').attr('font-family', 'Vazirmatn').attr('transform', 'rotate(-25)').attr('text-anchor', 'end');
|
||
g.append('g').call(d3.axisLeft(y).ticks(4).tickSize(-iw)).selectAll('text').attr('fill', 'var(--text-muted)').attr('font-size', '9px');
|
||
g.selectAll('.grid-line').data(y.ticks(4)).join('line').attr('x1', 0).attr('x2', iw).attr('y1', dd => y(dd)).attr('y2', dd => y(dd)).attr('stroke', 'var(--border-light)').attr('stroke-dasharray', '3,3');
|
||
g.selectAll('rect').data(data).join('rect')
|
||
.attr('x', dd => x(dd.label)).attr('y', dd => y(dd.value))
|
||
.attr('width', x.bandwidth()).attr('height', dd => ih - y(dd.value))
|
||
.attr('fill', color).attr('rx', 4).style('cursor', 'pointer')
|
||
.on('mouseover', function (e, dd) { d3.select(this).attr('fill', 'var(--accent)'); showD3Tooltip(e, `${dd.label}: ${dd.value}`) })
|
||
.on('mousemove', e => showD3Tooltip(e, ''))
|
||
.on('mouseout', function () { d3.select(this).attr('fill', color); hideD3Tooltip() });
|
||
g.selectAll('.bar-label').data(data).join('text')
|
||
.attr('x', dd => x(dd.label) + x.bandwidth() / 2).attr('y', dd => y(dd.value) - 5)
|
||
.attr('text-anchor', 'middle').attr('fill', 'var(--text)').attr('font-size', '10px').attr('font-weight', '700').text(dd => dd.value);
|
||
},
|
||
renderBarH(cid, data) {
|
||
const c = document.getElementById(cid); if (!c) return; c.innerHTML = '';
|
||
if (!data || data.length === 0) { c.innerHTML = '<div class="dashboard-empty-chart"><span>🛠️</span><div>دادهای نیست</div></div>'; return }
|
||
const bh = 28, gap = 6;
|
||
const w = 320, hgt = data.length * (bh + gap) + 30;
|
||
const m = { top: 10, right: 35, bottom: 10, left: 110 };
|
||
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
|
||
const svg = d3.select(c).append('svg').attr('width', w).attr('height', hgt);
|
||
const g = svg.append('g').attr('transform', `translate(${m.left},${m.top})`);
|
||
const x = d3.scaleLinear().domain([0, d3.max(data, dd => dd.value) || 1]).range([0, iw]);
|
||
const y = d3.scaleBand().domain(data.map(dd => dd.label)).range([0, ih]).padding(.2);
|
||
g.selectAll('rect').data(data).join('rect')
|
||
.attr('x', 0).attr('y', dd => y(dd.label))
|
||
.attr('width', dd => x(dd.value)).attr('height', y.bandwidth())
|
||
.attr('fill', '#2c5282').attr('rx', 4).style('cursor', 'pointer')
|
||
.on('mouseover', function (e, dd) { d3.select(this).attr('fill', 'var(--accent)'); showD3Tooltip(e, `${dd.label}: ${dd.value}`) })
|
||
.on('mousemove', e => showD3Tooltip(e, ''))
|
||
.on('mouseout', function () { d3.select(this).attr('fill', '#2c5282'); hideD3Tooltip() });
|
||
g.selectAll('.label').data(data).join('text')
|
||
.attr('x', -8).attr('y', dd => y(dd.label) + y.bandwidth() / 2)
|
||
.attr('dy', '0.35em').attr('text-anchor', 'end')
|
||
.attr('fill', 'var(--text)').attr('font-size', '9px').attr('font-weight', '600')
|
||
.text(dd => dd.label);
|
||
g.selectAll('.value').data(data).join('text')
|
||
.attr('x', dd => x(dd.value) + 5).attr('y', dd => y(dd.label) + y.bandwidth() / 2)
|
||
.attr('dy', '0.35em').attr('fill', 'var(--text-muted)').attr('font-size', '10px').attr('font-weight', '700').text(dd => dd.value);
|
||
},
|
||
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' }
|
||
};
|
||
/* ==================== GALAXY CHART ==================== */
|
||
const GalaxyChartManager = { svg: null, zoomBehavior: 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) }, render() { const c = document.getElementById('galaxyChartContainer'); c.innerHTML = ''; const w = c.clientWidth || 900, h = c.clientHeight || 600; const nodes = [], links = []; projectData.appData.products.forEach(prod => { nodes.push({ id: prod.id, label: prod.title, type: 'product', radius: 30, data: prod }); prod.projects.forEach(pr => { nodes.push({ id: pr.id, label: pr.title, type: 'project', projectType: pr.type, radius: 16, data: pr }); links.push({ source: prod.id, target: pr.id, type: 'ownership' }); (pr.dependencies || []).forEach(depId => { links.push({ source: pr.id, target: depId, type: 'dependency' }) }) }) }); if (nodes.length === 0) { c.innerHTML = '<div class="galaxy-empty"><div style="font-size:60px">🌌</div><div>هیچ دادهای نیست</div></div>'; return } const svg = d3.select(c).append('svg').attr('width', w).attr('height', h).attr('id', 'galaxySvg'); this.svg = svg; svg.append('rect').attr('width', w).attr('height', h).attr('fill', '#0a0e27'); for (let i = 0; i < 100; i++)svg.append('circle').attr('cx', Math.random() * w).attr('cy', Math.random() * h).attr('r', Math.random() + .3).attr('fill', '#fff').attr('opacity', Math.random() * .5 + .1); const defs = svg.append('defs'); const gf = defs.append('filter').attr('id', 'galaxyGlow'); gf.append('feGaussianBlur').attr('stdDeviation', '3').attr('result', 'blur'); const fm = gf.append('feMerge'); fm.append('feMergeNode').attr('in', 'blur'); fm.append('feMergeNode').attr('in', 'SourceGraphic'); const zg = svg.append('g'); this.zoomBehavior = d3.zoom().scaleExtent([.2, 5]).on('zoom', e => { zg.attr('transform', e.transform); document.getElementById('galaxyZoomIndicator').textContent = `🔍 ${Math.round(e.transform.k * 100)}%` }); svg.call(this.zoomBehavior); const sim = d3.forceSimulation(nodes).force('link', d3.forceLink(links).id(d => d.id).distance(d => d.type === 'ownership' ? 100 : 160)).force('charge', d3.forceManyBody().strength(-300)).force('center', d3.forceCenter(w / 2, h / 2)); const link = zg.append('g').selectAll('line').data(links).join('line').attr('stroke', d => d.type === 'dependency' ? '#e0c789' : '#4a5568').attr('stroke-width', d => d.type === 'dependency' ? 2 : 1).attr('stroke-opacity', .6).attr('stroke-dasharray', d => d.type === 'dependency' ? '5,3' : 'none'); const node = zg.append('g').selectAll('g').data(nodes).join('g').style('cursor', 'pointer').call(d3.drag().on('start', (e, d) => { if (!e.active) sim.alphaTarget(.3).restart(); d.fx = d.x; d.fy = d.y }).on('drag', (e, d) => { d.fx = e.x; d.fy = e.y }).on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null })); node.append('circle').attr('r', d => d.radius).attr('fill', d => d.type === 'product' ? this.NODE_COLORS.product : (this.NODE_COLORS[d.projectType] || '#718096')).attr('stroke', '#fff').attr('stroke-width', 1.5).attr('filter', 'url(#galaxyGlow)'); node.append('text').attr('dy', d => d.radius + 14).attr('text-anchor', 'middle').attr('fill', '#fff').attr('font-size', '10px').attr('font-family', 'Vazirmatn').text(d => d.label); node.on('click', (e, d) => { const p = document.getElementById('galaxyInfoPanel'); const ic = document.getElementById('galaxyInfoContent'); let ih = `<h4>${d.label}</h4>`; if (d.type === 'product') { ih += `<div class="galaxy-info-row"><span class="galaxy-info-label">پروژهها</span><span class="galaxy-info-value">${(d.data.projects || []).length}</span></div>` } else { ih += `<div class="galaxy-info-row"><span class="galaxy-info-label">نوع</span><span class="galaxy-info-value">${this.getTypeLabel(d.projectType)}</span></div><div class="galaxy-info-row"><span class="galaxy-info-label">استقرارها</span><span class="galaxy-info-value">${ProductManager.getActiveDeployments(d.data).length}</span></div>` } ic.innerHTML = ih; p.classList.add('visible') }); sim.on('tick', () => { link.attr('x1', d => d.source.x).attr('y1', d => d.source.y).attr('x2', d => d.target.x).attr('y2', d => d.target.y); node.attr('transform', d => `translate(${d.x},${d.y})`) }); this.renderLegend(); this.updateStats(nodes, links) }, getTypeLabel(t) { const i = VALID_PROJECT_TYPES.find(x => x.value === t); return i ? i.label.split(' ')[0] : t }, 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:${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:${this.NODE_COLORS[t.value]}"></div><span>${t.icon} ${t.label.split(' ')[0]}</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 = `<span>📦 ${p}</span><span>🌿 ${pr}</span><span>🔗 ${dp}</span>` } };
|
||
/* ==================== ANALYSIS MANAGERS ==================== */
|
||
const DependencyAnalysisManager = {
|
||
open() { ModalManager.open('dependencyAnalysisModal'); setTimeout(() => this.render(), 150) },
|
||
|
||
getData() {
|
||
let totalDeps = 0, totalProjects = 0, brokenDeps = 0;
|
||
const allProjects = [];
|
||
const inDeps = {};
|
||
const typeCounts = {};
|
||
const matrix = {};
|
||
|
||
projectData.appData.products.forEach(prod => {
|
||
prod.projects.forEach(pr => {
|
||
allProjects.push({ ...pr, productName: prod.title });
|
||
totalProjects++;
|
||
matrix[pr.id] = {};
|
||
(pr.dependencies || []).forEach(depId => {
|
||
totalDeps++;
|
||
if (!ProductManager.findProjectById(depId)) {
|
||
brokenDeps++;
|
||
} else {
|
||
inDeps[depId] = (inDeps[depId] || 0) + 1;
|
||
matrix[pr.id][depId] = true;
|
||
}
|
||
typeCounts[pr.type] = (typeCounts[pr.type] || 0) + 1;
|
||
});
|
||
});
|
||
});
|
||
|
||
const bottlenecks = Object.entries(inDeps)
|
||
.filter(([k, v]) => v >= 2)
|
||
.map(([id, count]) => ({
|
||
project: ProductManager.findProjectById(id),
|
||
count
|
||
}))
|
||
.sort((a, b) => b.count - a.count);
|
||
|
||
const isolated = allProjects.filter(pr =>
|
||
(pr.dependencies || []).length === 0 && !(inDeps[pr.id] > 0)
|
||
);
|
||
|
||
return { totalDeps, totalProjects, brokenDeps, bottlenecks, isolated, typeCounts, matrix, allProjects, inDeps };
|
||
},
|
||
|
||
render() {
|
||
const c = document.getElementById('dependencyAnalysisContainer');
|
||
if (!c) return;
|
||
const d = this.getData();
|
||
|
||
let h = '<div class="dashboard-kpi-row">';
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔗</div><div class="dashboard-kpi-value">${d.totalDeps}</div><div class="dashboard-kpi-label">کل وابستگیها</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📦</div><div class="dashboard-kpi-value">${d.totalProjects}</div><div class="dashboard-kpi-label">پروژهها</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">⚠️</div><div class="dashboard-kpi-value">${d.bottlenecks.length}</div><div class="dashboard-kpi-label">گلوگاه</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔴</div><div class="dashboard-kpi-value">${d.brokenDeps}</div><div class="dashboard-kpi-label">شکسته</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔵</div><div class="dashboard-kpi-value">${d.isolated.length}</div><div class="dashboard-kpi-label">ایزوله</div></div>`;
|
||
h += '</div>';
|
||
|
||
h += '<div class="dashboard-charts-grid">';
|
||
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" style="grid-column:span 2"><div class="dashboard-chart-header">🟥 ماتریس وابستگی</div><div class="dashboard-chart-body" id="depMatrixChart" style="overflow:auto;max-height:300px"></div></div>';
|
||
h += '<div class="dashboard-chart-card" style="grid-column:span 2"><div class="dashboard-chart-header">⚠️ لیست ریسکها</div><div class="dashboard-chart-body" id="depRiskList" style="align-items:flex-start;overflow-y:auto;max-height:300px;width:100%"></div></div>';
|
||
h += '</div>';
|
||
|
||
c.innerHTML = h;
|
||
|
||
this.renderHubChart('depHubChart', d.bottlenecks);
|
||
this.renderTypeDonut('depTypeChart', d.typeCounts);
|
||
this.renderMatrix('depMatrixChart', d.allProjects, d.matrix);
|
||
this.renderRiskList('depRiskList', d);
|
||
|
||
document.getElementById('depAnalysisFooterStats').innerHTML =
|
||
`<span>🔗 کل: <span class="stat-highlight">${d.totalDeps}</span></span>` +
|
||
`<span>⚠️ گلوگاه: <span class="stat-highlight">${d.bottlenecks.length}</span></span>` +
|
||
`<span>🔴 شکسته: <span class="stat-highlight">${d.brokenDeps}</span></span>` +
|
||
`<span>🔵 ایزوله: <span class="stat-highlight">${d.isolated.length}</span></span>`;
|
||
|
||
document.getElementById('depAnalysisRefresh').onclick = () => this.render();
|
||
},
|
||
|
||
renderHubChart(cid, bottlenecks) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
c.innerHTML = '';
|
||
if (!bottlenecks || bottlenecks.length === 0) {
|
||
c.innerHTML = '<div class="dashboard-empty-chart"><span>📊</span><div>گلوگاهی نیست</div></div>';
|
||
return;
|
||
}
|
||
const w = 320, hgt = Math.max(120, bottlenecks.length * 40 + 40);
|
||
const m = { top: 10, right: 35, bottom: 10, left: 130 };
|
||
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
|
||
const svg = d3.select(c).append('svg').attr('width', w).attr('height', hgt);
|
||
const g = svg.append('g').attr('transform', `translate(${m.left},${m.top})`);
|
||
const x = d3.scaleLinear().domain([0, d3.max(bottlenecks, d => d.count) || 1]).range([0, iw]);
|
||
const y = d3.scaleBand().domain(bottlenecks.map(d => d.project ? d.project.title : '')).range([0, ih]).padding(0.3);
|
||
g.selectAll('rect').data(bottlenecks).join('rect')
|
||
.attr('x', 0).attr('y', d => y(d.project ? d.project.title : ''))
|
||
.attr('width', d => x(d.count)).attr('height', y.bandwidth())
|
||
.attr('fill', d => d.count >= 3 ? '#e53e3e' : '#dd6b20')
|
||
.attr('rx', 4).style('cursor', 'pointer')
|
||
.on('mouseover', function (e, d) { d3.select(this).attr('fill', 'var(--accent)'); showD3Tooltip(e, `${d.project ? d.project.title : ''}: ${d.count} وابستگی ورودی`) })
|
||
.on('mousemove', e => showD3Tooltip(e, ''))
|
||
.on('mouseout', function (e, d) { d3.select(this).attr('fill', d.count >= 3 ? '#e53e3e' : '#dd6b20'); hideD3Tooltip() });
|
||
g.selectAll('.label').data(bottlenecks).join('text')
|
||
.attr('x', -8).attr('y', d => y(d.project ? d.project.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 => { const t = d.project ? d.project.title : ''; return t.length > 18 ? t.substring(0, 18) + '...' : t });
|
||
g.selectAll('.value').data(bottlenecks).join('text')
|
||
.attr('x', d => x(d.count) + 5).attr('y', d => y(d.project ? d.project.title : '') + y.bandwidth() / 2)
|
||
.attr('dy', '0.35em').attr('fill', 'var(--text-muted)').attr('font-size', '10px').attr('font-weight', '700')
|
||
.text(d => d.count);
|
||
},
|
||
|
||
renderTypeDonut(cid, typeCounts) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
c.innerHTML = '';
|
||
const entries = Object.entries(typeCounts);
|
||
if (entries.length === 0) {
|
||
c.innerHTML = '<div class="dashboard-empty-chart"><span>🥧</span><div>دادهای نیست</div></div>';
|
||
return;
|
||
}
|
||
const w = 280, hgt = 220, r = Math.min(w, hgt) / 2 - 15;
|
||
const svg = d3.select(c).append('svg').attr('width', w).attr('height', hgt);
|
||
const g = svg.append('g').attr('transform', `translate(${w / 2},${hgt / 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 + 8);
|
||
const total = entries.reduce((s, d) => s + d[1], 0);
|
||
const colors = { backend: '#4299e1', frontend: '#48bb78', service: '#ecc94b', database: '#ed64a6', mobile: '#9f7aea', infrastructure: '#ed8936' };
|
||
g.selectAll('path').data(pie(entries)).join('path')
|
||
.attr('d', arc).attr('fill', d => colors[d.data[0]] || '#718096')
|
||
.attr('stroke', '#fff').attr('stroke-width', 2).style('cursor', 'pointer')
|
||
.on('mouseover', function (e, d) { d3.select(this).transition().duration(150).attr('d', arcH(d)); const ti = VALID_PROJECT_TYPES.find(t => t.value === d.data[0]); showD3Tooltip(e, `${ti ? ti.label : d.data[0]}: ${d.data[1]} (${Math.round(d.data[1] / total * 100)}%)`) })
|
||
.on('mousemove', e => showD3Tooltip(e, ''))
|
||
.on('mouseout', function () { d3.select(this).transition().duration(150).attr('d', arc); hideD3Tooltip() });
|
||
g.append('text').attr('text-anchor', 'middle').attr('dy', '-.2em').attr('fill', 'var(--text)').attr('font-size', '24px').attr('font-weight', '800').text(total);
|
||
g.append('text').attr('text-anchor', 'middle').attr('dy', '1.4em').attr('fill', 'var(--text-muted)').attr('font-size', '11px').text('وابستگی');
|
||
},
|
||
|
||
renderMatrix(cid, allProjects, matrix) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
c.innerHTML = '';
|
||
if (!allProjects || allProjects.length === 0) {
|
||
c.innerHTML = '<div class="dashboard-empty-chart"><span>🟥</span><div>دادهای نیست</div></div>';
|
||
return;
|
||
}
|
||
const cs = 28, lw = 140;
|
||
const w = lw + allProjects.length * cs + 20;
|
||
const hgt = 40 + allProjects.length * cs + 20;
|
||
const svg = d3.select(c).append('svg').attr('width', w).attr('height', hgt);
|
||
const g = svg.append('g');
|
||
allProjects.forEach((pr, i) => {
|
||
g.append('text')
|
||
.attr('x', lw - 8).attr('y', 40 + i * cs + cs / 2)
|
||
.attr('text-anchor', 'end').attr('fill', 'var(--text)')
|
||
.attr('font-size', '9px').attr('font-family', 'Vazirmatn')
|
||
.text(pr.title.length > 20 ? pr.title.substring(0, 20) + '...' : pr.title);
|
||
});
|
||
allProjects.forEach((pr, i) => {
|
||
g.append('text')
|
||
.attr('x', lw + i * cs + cs / 2).attr('y', 30)
|
||
.attr('text-anchor', 'middle').attr('fill', 'var(--text-muted)')
|
||
.attr('font-size', '8px').attr('font-family', 'Vazirmatn')
|
||
.attr('transform', `rotate(-45, ${lw + i * cs + cs / 2}, 30)`)
|
||
.text(pr.title.length > 12 ? pr.title.substring(0, 12) + '...' : pr.title);
|
||
});
|
||
allProjects.forEach((row, i) => {
|
||
allProjects.forEach((col, j) => {
|
||
const hasDep = matrix[row.id] && matrix[row.id][col.id];
|
||
g.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 => showD3Tooltip(e, '') : null)
|
||
.on('mouseout', hasDep ? () => hideD3Tooltip() : null);
|
||
});
|
||
});
|
||
},
|
||
|
||
renderRiskList(cid, d) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
let h = '';
|
||
if (d.brokenDeps > 0) {
|
||
h += `<div style="padding:10px 12px;background:rgba(229,62,62,.08);border-radius:6px;margin-bottom:8px;border-right:4px solid #e53e3e"><div style="font-weight:700;font-size:12px">🔴 وابستگیهای شکسته</div><div style="font-size:11px;color:var(--text-muted)">${d.brokenDeps} وابستگی به پروژههای ناموجود</div></div>`;
|
||
}
|
||
d.bottlenecks.forEach(b => {
|
||
h += `<div style="padding:10px 12px;background:rgba(221,107,32,.08);border-radius:6px;margin-bottom:8px;border-right:4px solid #dd6b20"><div style="font-weight:700;font-size:12px">⚠️ گلوگاه: ${b.project ? b.project.title : ''}</div><div style="font-size:11px;color:var(--text-muted)">${b.count} پروژه به این پروژه وابستهاند - ریسک بالا در صورت تغییر</div></div>`;
|
||
});
|
||
if (d.isolated.length > 0) {
|
||
h += `<div style="padding:10px 12px;background:rgba(49,130,206,.08);border-radius:6px;margin-bottom:8px;border-right:4px solid #3182ce"><div style="font-weight:700;font-size:12px">🔵 پروژههای ایزوله</div><div style="font-size:11px;color:var(--text-muted)">${d.isolated.map(p => p.title).join('، ')}</div></div>`;
|
||
}
|
||
if (!h) h = '<div class="dashboard-empty-chart"><span>✅</span><div>هیچ ریسکی شناسایی نشد</div></div>';
|
||
c.innerHTML = h;
|
||
}
|
||
};
|
||
/* ==================== DEPLOYMENT ANALYSIS MANAGER ==================== */
|
||
const DeploymentAnalysisManager = {
|
||
open() {
|
||
ModalManager.open('deploymentAnalysisModal');
|
||
setTimeout(() => this.render(), 150);
|
||
},
|
||
|
||
getData() {
|
||
let total = 0, pwd = 0, pwod = 0, withNotes = 0, withoutNotes = 0;
|
||
const edc = {};
|
||
const ppc = [];
|
||
const riskList = [];
|
||
const now = new Date();
|
||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||
|
||
projectData.appData.products.forEach(prod => {
|
||
let prodDeployCount = 0;
|
||
prod.projects.forEach(pr => {
|
||
const active = ProductManager.getActiveDeployments(pr);
|
||
if (active.length > 0) pwd++;
|
||
else {
|
||
pwod++;
|
||
riskList.push({
|
||
type: 'no-deploy',
|
||
severity: 'high',
|
||
title: 'بدون استقرار',
|
||
desc: `${pr.title} در هیچ محیطی استقرار ندارد`,
|
||
rec: 'نسخه اولیه را در حداقل یک محیط مستقر کنید'
|
||
});
|
||
}
|
||
active.forEach(d => {
|
||
total++;
|
||
prodDeployCount++;
|
||
if (d.releaseNotes) withNotes++;
|
||
else {
|
||
withoutNotes++;
|
||
riskList.push({
|
||
type: 'no-notes',
|
||
severity: 'medium',
|
||
title: 'بدون ریلیز نوت',
|
||
desc: `${pr.title} v${d.version}`,
|
||
rec: 'ریلیز نوت را تکمیل کنید'
|
||
});
|
||
}
|
||
const env = projectData.appData.environments.find(e => e.id === d.environmentId);
|
||
const envName = env ? env.name : 'نامشخص';
|
||
edc[envName] = (edc[envName] || 0) + 1;
|
||
if (d.deploymentDate && new Date(d.deploymentDate) < thirtyDaysAgo) {
|
||
const daysOld = Math.floor((now - new Date(d.deploymentDate)) / (1000 * 60 * 60 * 24));
|
||
riskList.push({
|
||
type: 'stale',
|
||
severity: daysOld > 90 ? 'high' : 'medium',
|
||
title: 'استقرار قدیمی',
|
||
desc: `${pr.title} v${d.version} در ${envName} (${daysOld} روز)`,
|
||
rec: 'بررسی و بهروزرسانی نسخه'
|
||
});
|
||
}
|
||
});
|
||
});
|
||
ppc.push({ title: prod.title, count: prodDeployCount });
|
||
});
|
||
|
||
const coverage = (pwd + pwod) > 0 ? Math.round(pwd / (pwd + pwod) * 100) : 0;
|
||
return { total, pwd, pwod, withNotes, withoutNotes, edc, ppc, riskList, coverage };
|
||
},
|
||
|
||
render() {
|
||
const c = document.getElementById('deploymentAnalysisContainer');
|
||
if (!c) return;
|
||
const d = this.getData();
|
||
|
||
let h = '<div class="dashboard-kpi-row">';
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🚀</div><div class="dashboard-kpi-value">${d.total}</div><div class="dashboard-kpi-label">کل استقرارها</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">✅</div><div class="dashboard-kpi-value">${d.pwd}</div><div class="dashboard-kpi-label">با استقرار</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">❌</div><div class="dashboard-kpi-value">${d.pwod}</div><div class="dashboard-kpi-label">بدون استقرار</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📈</div><div class="dashboard-kpi-value">${d.coverage}%</div><div class="dashboard-kpi-label">پوشش استقرار</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📝</div><div class="dashboard-kpi-value">${d.withNotes}</div><div class="dashboard-kpi-label">با ریلیز نوت</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">⚠️</div><div class="dashboard-kpi-value">${d.withoutNotes}</div><div class="dashboard-kpi-label">بدون ریلیز نوت</div></div>`;
|
||
h += '</div>';
|
||
|
||
h += '<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="deployProductChart"></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.edc);
|
||
this.renderStatusDonut('deployStatusChart', d.pwd, d.pwod, d.coverage);
|
||
this.renderProductBarChart('deployProductChart', d.ppc);
|
||
this.renderRiskList('deployRiskList', d.riskList);
|
||
|
||
document.getElementById('deployAnalysisFooterStats').innerHTML =
|
||
`<span>🚀 کل: <span class="stat-highlight">${d.total}</span></span>` +
|
||
`<span>📈 پوشش: <span class="stat-highlight">${d.coverage}%</span></span>` +
|
||
`<span>⚠️ ریسکها: <span class="stat-highlight">${d.riskList.length}</span></span>`;
|
||
|
||
document.getElementById('deployAnalysisRefresh').onclick = () => this.render();
|
||
},
|
||
|
||
renderEnvBarChart(cid, edc) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
c.innerHTML = '';
|
||
const data = Object.entries(edc).map(([k, v]) => ({ label: k, value: v }));
|
||
if (data.length === 0) {
|
||
c.innerHTML = '<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>';
|
||
return;
|
||
}
|
||
const w = 320, hgt = 220, m = { top: 20, right: 15, bottom: 45, left: 30 };
|
||
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
|
||
const svg = d3.select(c).append('svg').attr('width', w).attr('height', hgt);
|
||
const g = svg.append('g').attr('transform', `translate(${m.left},${m.top})`);
|
||
const x = d3.scaleBand().domain(data.map(d => d.label)).range([0, iw]).padding(.3);
|
||
const y = d3.scaleLinear().domain([0, d3.max(data, d => d.value) || 1]).nice().range([ih, 0]);
|
||
g.append('g').attr('transform', `translate(0,${ih})`).call(d3.axisBottom(x).tickSize(0)).selectAll('text')
|
||
.attr('fill', 'var(--text-muted)').attr('font-size', '8px').attr('font-family', 'Vazirmatn')
|
||
.attr('transform', 'rotate(-20)').attr('text-anchor', 'end');
|
||
g.append('g').call(d3.axisLeft(y).ticks(4).tickSize(-iw)).selectAll('text')
|
||
.attr('fill', 'var(--text-muted)').attr('font-size', '9px');
|
||
g.selectAll('rect').data(data).join('rect')
|
||
.attr('x', d => x(d.label)).attr('y', d => y(d.value))
|
||
.attr('width', x.bandwidth()).attr('height', d => ih - y(d.value))
|
||
.attr('fill', '#38a169').attr('rx', 4).style('cursor', 'pointer')
|
||
.on('mouseover', function (e, d) { d3.select(this).attr('fill', 'var(--accent)'); showD3Tooltip(e, `${d.label}: ${d.value}`) })
|
||
.on('mousemove', e => showD3Tooltip(e, ''))
|
||
.on('mouseout', function () { d3.select(this).attr('fill', '#38a169'); hideD3Tooltip() });
|
||
g.selectAll('.bar-label').data(data).join('text')
|
||
.attr('x', d => x(d.label) + x.bandwidth() / 2).attr('y', d => y(d.value) - 5)
|
||
.attr('text-anchor', 'middle').attr('fill', 'var(--text)')
|
||
.attr('font-size', '10px').attr('font-weight', '700').text(d => d.value);
|
||
},
|
||
|
||
renderStatusDonut(cid, pwd, pwod, coverage) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
c.innerHTML = '';
|
||
const total = pwd + pwod;
|
||
if (total === 0) {
|
||
c.innerHTML = '<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>';
|
||
return;
|
||
}
|
||
const w = 280, hgt = 220, r = Math.min(w, hgt) / 2 - 15;
|
||
const svg = d3.select(c).append('svg').attr('width', w).attr('height', hgt);
|
||
const g = svg.append('g').attr('transform', `translate(${w / 2},${hgt / 2})`);
|
||
const pie = d3.pie().value(d => d.value).sort(null);
|
||
const arc = d3.arc().innerRadius(r * .55).outerRadius(r);
|
||
const arcH = d3.arc().innerRadius(r * .55).outerRadius(r + 8);
|
||
const data = [
|
||
{ label: 'با استقرار', value: pwd, color: '#38a169' },
|
||
{ label: 'بدون استقرار', value: pwod, color: '#e53e3e' }
|
||
].filter(d => d.value > 0);
|
||
g.selectAll('path').data(pie(data)).join('path')
|
||
.attr('d', arc).attr('fill', d => d.data.color)
|
||
.attr('stroke', '#fff').attr('stroke-width', 2).style('cursor', 'pointer')
|
||
.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 => showD3Tooltip(e, ''))
|
||
.on('mouseout', function () { d3.select(this).transition().duration(150).attr('d', arc); hideD3Tooltip() });
|
||
g.append('text').attr('text-anchor', 'middle').attr('dy', '-.2em')
|
||
.attr('fill', coverage >= 70 ? '#38a169' : coverage >= 40 ? '#dd6b20' : '#e53e3e')
|
||
.attr('font-size', '24px').attr('font-weight', '800').text(`${coverage}%`);
|
||
g.append('text').attr('text-anchor', 'middle').attr('dy', '1.4em')
|
||
.attr('fill', 'var(--text-muted)').attr('font-size', '11px').text('پوشش استقرار');
|
||
},
|
||
|
||
renderProductBarChart(cid, ppc) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
c.innerHTML = '';
|
||
const data = ppc.map(p => ({ label: p.title.length > 15 ? p.title.substring(0, 15) + '...' : p.title, value: p.count }));
|
||
if (data.length === 0) {
|
||
c.innerHTML = '<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>';
|
||
return;
|
||
}
|
||
const bh = 28, gap = 6;
|
||
const w = 320, hgt = data.length * (bh + gap) + 30;
|
||
const m = { top: 10, right: 35, bottom: 10, left: 130 };
|
||
const iw = w - m.left - m.right, ih = hgt - m.top - m.bottom;
|
||
const svg = d3.select(c).append('svg').attr('width', w).attr('height', hgt);
|
||
const g = svg.append('g').attr('transform', `translate(${m.left},${m.top})`);
|
||
const x = d3.scaleLinear().domain([0, d3.max(data, d => d.value) || 1]).range([0, iw]);
|
||
const y = d3.scaleBand().domain(data.map(d => d.label)).range([0, ih]).padding(.2);
|
||
g.selectAll('rect').data(data).join('rect')
|
||
.attr('x', 0).attr('y', d => y(d.label))
|
||
.attr('width', d => x(d.value)).attr('height', y.bandwidth())
|
||
.attr('fill', '#3182ce').attr('rx', 4).style('cursor', 'pointer')
|
||
.on('mouseover', function (e, d) { d3.select(this).attr('fill', 'var(--accent)'); showD3Tooltip(e, `${d.label}: ${d.value}`) })
|
||
.on('mousemove', e => showD3Tooltip(e, ''))
|
||
.on('mouseout', function () { d3.select(this).attr('fill', '#3182ce'); hideD3Tooltip() });
|
||
g.selectAll('.label').data(data).join('text')
|
||
.attr('x', -8).attr('y', d => y(d.label) + y.bandwidth() / 2)
|
||
.attr('dy', '0.35em').attr('text-anchor', 'end')
|
||
.attr('fill', 'var(--text)').attr('font-size', '9px').attr('font-weight', '600')
|
||
.text(d => d.label);
|
||
g.selectAll('.value').data(data).join('text')
|
||
.attr('x', d => x(d.value) + 5).attr('y', d => y(d.label) + y.bandwidth() / 2)
|
||
.attr('dy', '0.35em').attr('fill', 'var(--text-muted)')
|
||
.attr('font-size', '10px').attr('font-weight', '700').text(d => d.value);
|
||
},
|
||
|
||
renderRiskList(cid, riskList) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
if (riskList.length === 0) {
|
||
c.innerHTML = '<div class="dashboard-empty-chart"><span>✅</span><div>هیچ ریسکی شناسایی نشد</div></div>';
|
||
return;
|
||
}
|
||
const colors = { critical: '#e53e3e', high: '#dd6b20', medium: '#d69e2e' };
|
||
let h = '';
|
||
riskList.forEach(r => {
|
||
h += `<div style="padding:10px 12px;background:rgba(0,0,0,.05);border-radius:6px;margin-bottom:8px;border-right:4px solid ${colors[r.severity] || '#3182ce'}">`;
|
||
h += `<div style="font-weight:700;font-size:12px">${r.title}</div>`;
|
||
h += `<div style="font-size:11px;color:var(--text-muted)">${r.desc}</div>`;
|
||
h += `<div style="font-size:10px;color:var(--info);margin-top:4px">💡 ${r.rec}</div>`;
|
||
h += '</div>';
|
||
});
|
||
c.innerHTML = h;
|
||
}
|
||
};
|
||
const VersionAnalysisManager = { open() { ModalManager.open('versionAnalysisModal'); setTimeout(() => this.render(), 150) }, render() { const c = document.getElementById('versionAnalysisContainer'); if (!c) return; let total = 0; const unique = new Set(); projectData.appData.products.forEach(prod => { prod.projects.forEach(pr => { ProductManager.getActiveDeployments(pr).forEach(d => { total++; unique.add(d.version) }) }) }); let h = '<div class="dashboard-kpi-row">'; h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔢</div><div class="dashboard-kpi-value">${total}</div><div class="dashboard-kpi-label">کل نسخهها</div></div>`; h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📦</div><div class="dashboard-kpi-value">${unique.size}</div><div class="dashboard-kpi-label">یکتا</div></div>`; h += '</div>'; c.innerHTML = h; document.getElementById('verAnalysisFooterStats').innerHTML = `<span>🔢 کل: <span class="stat-highlight">${total}</span></span><span>📦 یکتا: <span class="stat-highlight">${unique.size}</span></span>`; document.getElementById('verAnalysisRefresh').onclick = () => this.render() } };
|
||
const RiskAnalysisManager = { open() { ModalManager.open('riskAnalysisModal'); setTimeout(() => this.render(), 150) }, render() { const c = document.getElementById('riskAnalysisContainer'); if (!c) return; const risks = []; projectData.appData.products.forEach(prod => { prod.projects.forEach(pr => { (pr.dependencies || []).forEach(depId => { if (!ProductManager.findProjectById(depId)) risks.push({ type: 'broken', severity: 'critical', title: 'وابستگی شکسته', desc: `${pr.title} → پروژه ناموجود` }) }); if (ProductManager.getActiveDeployments(pr).length === 0) risks.push({ type: 'no-deploy', severity: 'high', title: 'بدون استقرار', desc: pr.title }) }); if (!prod.metadata?.owner) risks.push({ type: 'no-owner', severity: 'medium', title: 'بدون مالک', desc: prod.title }) }); const crit = risks.filter(r => r.severity === 'critical').length; const high = risks.filter(r => r.severity === 'high').length; const med = risks.filter(r => r.severity === 'medium').length; let healthScore = Math.max(0, 100 - crit * 15 - high * 10 - med * 5); let h = '<div class="dashboard-kpi-row">'; h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🎯</div><div class="dashboard-kpi-value">${healthScore}%</div><div class="dashboard-kpi-label">امتیاز سلامت</div></div>`; h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔴</div><div class="dashboard-kpi-value">${crit}</div><div class="dashboard-kpi-label">بحرانی</div></div>`; h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🟠</div><div class="dashboard-kpi-value">${high}</div><div class="dashboard-kpi-label">بالا</div></div>`; h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🟡</div><div class="dashboard-kpi-value">${med}</div><div class="dashboard-kpi-label">متوسط</div></div>`; h += '</div>'; if (risks.length > 0) { h += '<div style="margin-top:16px">'; risks.forEach(r => { const colors = { critical: '#e53e3e', high: '#dd6b20', medium: '#d69e2e' }; h += `<div style="padding:10px 12px;background:rgba(0,0,0,.05);border-radius:6px;margin-bottom:8px;border-right:4px solid ${colors[r.severity] || '#3182ce'}"><div style="font-weight:700;font-size:12px">${r.title}</div><div style="font-size:11px;color:var(--text-muted)">${r.desc}</div></div>` }); h += '</div>' } else { h += '<div class="dashboard-empty-chart"><span>✅</span><div>هیچ ریسکی شناسایی نشد</div></div>' } c.innerHTML = h; document.getElementById('riskAnalysisFooterStats').innerHTML = `<span>🎯 سلامت: <span class="stat-highlight">${healthScore}%</span></span><span>⚠️ ریسکها: <span class="stat-highlight">${risks.length}</span></span>`; document.getElementById('riskAnalysisRefresh').onclick = () => this.render() } };
|
||
/* ==================== EVENT BINDINGS ==================== */
|
||
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('galaxyResetView').addEventListener('click', () => { if (GalaxyChartManager.svg && GalaxyChartManager.zoomBehavior) GalaxyChartManager.svg.transition().duration(500).call(GalaxyChartManager.zoomBehavior.transform, d3.zoomIdentity) });
|
||
document.getElementById('galaxyInfoClose').addEventListener('click', () => { document.getElementById('galaxyInfoPanel').classList.remove('visible') });
|
||
document.getElementById('galaxyExportPNG').addEventListener('click', () => { showToast('📷 خروجی ذخیره شد', 'success') });
|
||
function handleToolSelect(tool, group) { const ah = ActionRegistry[tool.action] || ActionRegistry.defaultAction; ah(tool, group) }
|
||
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'; 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 style="width:20px;text-align:center">${tool.icon || '🔧'}</span><span style="flex:1">${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') });
|
||
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>` }
|
||
/* ==================== INIT ==================== */
|
||
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> |