Files
xReleaseAsistant/xDependencies_1.4.29.html
T
2026-09-16 17:52:23 +03:30

2798 lines
199 KiB
HTML
Raw Blame History

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