Files
xReleaseAsistant/BKP/14050626/xDependencies_1.4.33.html
2026-09-17 21:27:27 +03:30

8848 lines
329 KiB
HTML
Raw Permalink 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, 0.08), 0 10px 20px rgba(26, 54, 93, 0.1);
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.15);
--radius: 10px;
--radius-sm: 6px;
}
[data-theme="dark"] {
--primary: #2c5282;
--primary-light: #3b6fb5;
--primary-dark: #1a365d;
--accent: #e0c789;
--accent-dark: #c9a961;
--accent-light: #f0d9a0;
--bg: #1a202c;
--board-bg: #2d3748;
--card-bg: #2d3748;
--panel-bg: #1f2937;
--text: #e2e8f0;
--text-muted: #cbd5e0;
--text-light: #a0aec0;
--border: #4a5568;
--border-light: #374151;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.3), 0 10px 20px rgba(0, 0, 0, 0.4);
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.2);
--shadow-lg: 0 10px 30px rgba(0, 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 0.3s,
color 0.3s;
}
.app-header {
background: linear-gradient(
135deg,
var(--primary) 0%,
var(--primary-light) 100%
);
color: #fff;
padding: 0 20px;
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 3px solid var(--accent);
box-shadow: var(--shadow);
z-index: 100;
flex-shrink: 0;
}
.app-brand {
display: flex;
align-items: center;
gap: 14px;
}
.app-logo {
width: 44px;
height: 44px;
background: var(--accent);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
font-weight: 800;
color: var(--primary-dark);
}
.app-brand-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.app-title {
font-size: 16px;
font-weight: 800;
color: #fff;
}
.app-subtitle {
font-size: 11px;
opacity: 0.85;
color: rgba(255, 255, 255, 0.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 0.2s;
white-space: nowrap;
}
.btn:hover {
transform: translateY(-1px);
filter: brightness(1.1);
}
.btn-accent {
background: linear-gradient(135deg, var(--accent), var(--accent-dark));
color: var(--primary-dark);
font-weight: 700;
}
.btn-ghost {
background: rgba(26, 54, 93, 0.08);
border: 1px solid var(--border);
color: var(--primary);
}
[data-theme="dark"] .btn-ghost {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.25);
color: #fff;
}
.btn-danger {
background: linear-gradient(135deg, var(--danger), #c53030);
color: #fff;
}
.btn-sm {
padding: 4px 10px;
font-size: 11px;
}
.btn-xs {
padding: 2px 6px;
font-size: 10px;
}
.app-main {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 280px;
background: var(--panel-bg);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
transition: width 0.3s;
overflow: hidden;
flex-shrink: 0;
z-index: 50;
}
.sidebar.collapsed {
width: 0;
}
.sidebar-header {
background: linear-gradient(
135deg,
var(--primary),
var(--primary-light)
);
color: #fff;
padding: 12px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 2px solid var(--accent);
min-height: 48px;
}
.sidebar-title {
font-size: 13px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.sidebar-body {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.sidebar-group {
margin-bottom: 6px;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
overflow: hidden;
background: var(--card-bg);
}
.sidebar-group-header {
background: linear-gradient(
135deg,
var(--primary),
var(--primary-light)
);
color: #fff;
padding: 10px 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 13px;
font-weight: 700;
user-select: none;
}
.sidebar-group-header .group-icon {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.sidebar-group-header .group-arrow {
transition: transform 0.3s;
font-size: 12px;
}
.sidebar-group.collapsed .group-arrow {
transform: rotate(90deg);
}
.sidebar-group.collapsed .sidebar-group-body {
display: none;
}
.sidebar-group-body {
padding: 6px;
display: flex;
flex-direction: column;
gap: 4px;
}
.sidebar-btn {
background: var(--card-bg);
border: 1px solid var(--border-light);
color: var(--text);
padding: 8px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
font-family: inherit;
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
text-align: right;
width: 100%;
}
.sidebar-btn:hover {
background: linear-gradient(
135deg,
var(--primary),
var(--primary-light)
);
color: #fff;
}
.tabs-bar {
background: linear-gradient(135deg, #0f1724 0%, #1a2a44 100%);
border-bottom: 2px solid var(--accent);
display: flex;
align-items: center;
padding: 0 12px;
min-height: 48px;
gap: 10px;
flex-wrap: wrap;
}
.toolbar-controls {
display: flex;
gap: 6px;
margin-left: 8px;
}
.toolbar-icon-btn {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
color: #fff;
width: 34px;
height: 34px;
border-radius: 6px;
cursor: pointer;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
}
.toolbar-icon-btn:hover:not(:disabled) {
background: var(--accent);
color: var(--primary-dark);
}
.toolbar-search {
flex: 1;
min-width: 180px;
max-width: 320px;
position: relative;
}
.search-input {
width: 100%;
padding: 7px 32px 7px 12px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
color: #fff;
font-family: inherit;
font-size: 13px;
}
.search-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.toolbar-filters {
display: flex;
gap: 8px;
}
.filter-dropdown {
display: flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
padding: 0 10px;
}
.filter-select {
background: transparent;
border: none;
color: #fff;
font-family: inherit;
font-size: 12px;
font-weight: 600;
padding: 7px 4px;
cursor: pointer;
outline: none;
min-width: 110px;
}
.filter-select option {
background: var(--primary-dark);
color: #fff;
}
.toolbar-stats {
display: flex;
gap: 12px;
margin-right: auto;
padding-right: 8px;
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: rgba(255, 255, 255, 0.85);
}
.stat-value {
font-weight: 800;
color: var(--accent);
background: rgba(201, 169, 97, 0.15);
padding: 2px 7px;
border-radius: 10px;
font-size: 11px;
}
.board-area {
flex: 1;
overflow: auto;
padding: 24px;
background: var(--board-bg);
}
.board-section {
margin-bottom: 32px;
}
.board-section.collapsed .section-content {
display: none;
}
.board-section.collapsed .collapse-icon {
transform: rotate(90deg);
}
.collapse-icon {
transition: transform 0.3s;
font-size: 14px;
display: inline-block;
margin-left: 6px;
}
.board-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid var(--border);
}
.board-header-title {
font-size: 18px;
font-weight: 800;
color: var(--primary);
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}
[data-theme="dark"] .board-header-title {
color: var(--accent);
}
.board-header-actions {
display: flex;
gap: 8px;
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
}
.project-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: all 0.25s;
display: flex;
flex-direction: column;
}
.project-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--accent);
}
.project-card-header {
background: linear-gradient(
135deg,
var(--primary) 0%,
var(--primary-light) 100%
);
color: #fff;
padding: 16px;
}
.project-card-title-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 4px;
}
.project-card-title {
font-size: 16px;
font-weight: 700;
flex: 1;
}
.project-card-desc {
font-size: 12px;
opacity: 0.9;
line-height: 1.5;
}
.project-card-body {
padding: 14px 16px;
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
}
.project-stat-row {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--text-muted);
}
.project-stat-value {
font-weight: 700;
color: var(--primary);
}
[data-theme="dark"] .project-stat-value {
color: var(--accent);
}
.project-type-badges {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.type-badge {
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
border-radius: 4px;
display: inline-block;
}
.type-backend {
background: #ebf4ff;
color: #2b6cb0;
}
.type-frontend {
background: #f0fff4;
color: #276749;
}
.type-service {
background: #fefcbf;
color: #975a16;
}
.type-database {
background: #fed7e2;
color: #97266d;
}
.type-mobile {
background: #e9d8fd;
color: #553c9a;
}
.type-infrastructure {
background: #feebc8;
color: #9c4221;
}
.project-card-footer {
padding: 12px 16px;
background: var(--border-light);
border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
}
.card-actions {
display: flex;
gap: 6px;
}
.env-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.env-card-item {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
}
.env-card-preview {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 700;
color: #fff;
}
.env-card-body {
padding: 14px 16px;
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.env-card-name {
font-size: 15px;
font-weight: 700;
}
.env-card-desc {
font-size: 12px;
color: var(--text-muted);
}
.env-card-footer {
padding: 10px 16px;
background: var(--border-light);
border-top: 1px solid var(--border);
display: flex;
gap: 6px;
}
.board-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--text-muted);
text-align: center;
padding: 40px;
}
.board-empty-icon {
font-size: 80px;
opacity: 0.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, 0.6);
backdrop-filter: blur(3px);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.modal-overlay.active {
display: flex;
}
.modal {
background: var(--panel-bg);
color: var(--text);
border-radius: 12px;
width: 100%;
max-width: 600px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-lg);
}
.modal-lg {
max-width: 800px;
}
.modal-xl {
max-width: 1000px;
}
.modal-galaxy {
max-width: 1200px;
width: 100%;
height: 85vh;
max-height: 85vh;
border-radius: 16px;
}
.galaxy-search {
position: relative;
display: flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
padding: 0 8px;
height: 32px;
min-width: 230px;
}
.galaxy-search-icon {
font-size: 12px;
opacity: 0.85;
}
.galaxy-search input {
flex: 1;
width: 140px;
background: transparent;
border: none;
color: #fff;
font-family: inherit;
font-size: 12px;
outline: none;
}
.galaxy-collapse-badge {
pointer-events: none;
}
.galaxy-search input::placeholder {
color: rgba(255, 255, 255, 0.55);
}
.galaxy-search-clear {
background: none;
border: none;
color: rgba(255, 255, 255, 0.75);
cursor: pointer;
font-size: 12px;
opacity: 0;
pointer-events: none;
}
.galaxy-search-clear.visible {
opacity: 1;
pointer-events: auto;
}
.galaxy-node.galaxy-dim {
opacity: 0.12;
}
.galaxy-node.galaxy-match > circle:nth-of-type(2) {
stroke: #ffd700;
stroke-width: 3;
}
.galaxy-link.galaxy-dim {
stroke-opacity: 0.04 !important;
}
.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, 0.15);
border: none;
color: #fff;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 16px;
}
.modal-close:hover {
background: var(--danger);
}
.modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.modal-footer {
padding: 14px 20px;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-start;
gap: 8px;
background: var(--border-light);
align-items: center;
flex-wrap: wrap;
}
.detail-section {
margin-bottom: 20px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
overflow: hidden;
}
.detail-section-header {
background: linear-gradient(
135deg,
var(--primary),
var(--primary-light)
);
color: #fff;
padding: 12px 16px;
font-size: 14px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: space-between;
}
.detail-section-header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.detail-section-body {
padding: 16px;
background: var(--card-bg);
}
.detail-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.detail-info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-info-label {
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
}
.detail-info-value {
font-size: 13px;
font-weight: 600;
}
.subproject-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.subproject-card {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 12px;
background: var(--border-light);
}
.subproject-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
gap: 8px;
}
.subproject-title {
font-size: 14px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.subproject-actions {
display: flex;
gap: 4px;
}
.subproject-meta {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 8px;
}
.deployment-section {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed var(--border);
}
.deployment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.deployment-header-title {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
}
.deployment-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.deployment-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
background: var(--card-bg);
border: 1px solid var(--border-light);
border-radius: 6px;
font-size: 11px;
flex-wrap: wrap;
gap: 6px;
}
.deployment-item-info {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.deployment-env-tag {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
}
.deployment-version {
font-family: "Vazirmatn", monospace;
font-weight: 700;
color: var(--primary);
font-size: 11px;
}
[data-theme="dark"] .deployment-version {
color: var(--accent);
}
.deployment-date {
font-size: 10px;
color: var(--text-muted);
}
.deployment-actions {
display: flex;
gap: 4px;
}
.deployment-notes {
font-size: 10px;
color: var(--text-muted);
width: 100%;
margin-top: 4px;
padding-top: 4px;
border-top: 1px dashed var(--border-light);
line-height: 1.5;
}
.deployment-req {
font-size: 10px;
color: var(--info);
width: 100%;
margin-top: 2px;
}
.empty-deployments {
text-align: center;
padding: 12px;
color: var(--text-muted);
font-size: 11px;
background: var(--border-light);
border-radius: 6px;
}
.empty-subprojects {
text-align: center;
padding: 30px;
color: var(--text-muted);
background: var(--border-light);
border-radius: var(--radius-sm);
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--primary);
margin-bottom: 6px;
}
[data-theme="dark"] .form-label {
color: var(--accent-light);
}
.form-label .required {
color: var(--danger);
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 9px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-family: inherit;
font-size: 13px;
background: var(--card-bg);
color: var(--text);
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(201, 169, 97, 0.15);
}
.form-textarea {
resize: vertical;
min-height: 70px;
line-height: 1.6;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.form-hint {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
}
.form-error {
font-size: 11px;
color: var(--danger);
margin-top: 4px;
display: none;
}
.form-error.visible {
display: block;
}
.color-input-wrapper {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.color-input-wrapper input[type="color"] {
width: 32px;
height: 32px;
border: none;
border-radius: 4px;
cursor: pointer;
padding: 0;
}
.color-preview-text {
font-size: 11px;
font-family: monospace;
color: var(--text-muted);
}
.webhook-section {
background: var(--border-light);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 14px;
margin-top: 12px;
}
.webhook-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.webhook-section-title {
font-size: 13px;
font-weight: 700;
color: var(--primary);
display: flex;
align-items: center;
gap: 6px;
}
.webhook-toggle {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-muted);
}
.switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
inset: 0;
background: var(--border);
transition: 0.3s;
border-radius: 20px;
}
.slider::before {
position: absolute;
content: "";
height: 14px;
width: 14px;
right: 3px;
bottom: 3px;
background: #fff;
transition: 0.3s;
border-radius: 50%;
}
input:checked + .slider {
background: var(--success);
}
input:checked + .slider::before {
transform: translateX(-16px);
}
.webhook-status {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
padding: 8px 10px;
background: var(--card-bg);
border-radius: 4px;
font-size: 11px;
color: var(--text-muted);
}
.webhook-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted);
}
.webhook-status-dot.success {
background: var(--success);
}
.webhook-status-dot.error {
background: var(--danger);
}
.toast-container {
position: fixed;
bottom: 20px;
left: 20px;
z-index: 2000;
display: flex;
flex-direction: column;
gap: 8px;
}
.toast {
background: var(--primary);
color: #fff;
padding: 12px 18px;
border-radius: 8px;
box-shadow: var(--shadow-lg);
font-size: 13px;
display: flex;
align-items: center;
gap: 10px;
min-width: 250px;
}
.toast.success {
background: var(--success);
}
.toast.error {
background: var(--danger);
}
.toast.warning {
background: var(--warning);
}
.app-footer {
background: linear-gradient(
135deg,
var(--primary),
var(--primary-light)
);
color: #fff;
padding: 8px 20px;
font-size: 11px;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 2px solid var(--accent);
flex-shrink: 0;
}
.footer-brand {
color: var(--accent);
font-weight: 700;
}
.form-input[readonly],
.form-textarea[readonly] {
background: var(--border-light);
color: var(--text-muted);
cursor: not-allowed;
opacity: 0.85;
border-style: dashed;
}
.readonly-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
font-weight: 600;
color: var(--text-muted);
background: var(--border-light);
border: 1px solid var(--border);
padding: 2px 8px;
border-radius: 10px;
margin-right: 6px;
}
.readonly-badge::before {
content: "🔒";
font-size: 10px;
}
.readonly-info {
background: linear-gradient(to left, #ebf8ff, #e6f6ff);
border: 1px solid #90cdf4;
border-right: 4px solid var(--info);
padding: 10px 14px;
border-radius: 6px;
font-size: 11px;
color: var(--text-muted);
margin-bottom: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.dependency-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
max-height: 200px;
overflow-y: auto;
}
.dependency-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.dependency-info {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.dependency-project-name {
font-weight: 600;
font-size: 13px;
}
.dependency-search-box {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border);
}
.dependency-search-input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-family: inherit;
font-size: 13px;
background: var(--card-bg);
color: var(--text);
margin-bottom: 12px;
}
.dependency-search-results {
max-height: 300px;
overflow-y: auto;
}
.dependency-project-result {
margin-bottom: 8px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
overflow: hidden;
}
.dependency-project-add-btn {
padding: 10px 12px;
background: var(--card-bg);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
width: 100%;
border: none;
font-family: inherit;
text-align: right;
}
.dependency-project-add-btn:hover {
background: var(--accent-light);
}
.empty-dependencies {
text-align: center;
padding: 20px;
color: var(--text-muted);
font-size: 12px;
background: var(--border-light);
border-radius: var(--radius-sm);
}
.dependency-section-title {
font-size: 13px;
font-weight: 700;
color: var(--primary);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 6px;
}
[data-theme="dark"] .dependency-section-title {
color: var(--accent);
}
.env-color-preview {
padding: 10px;
border-radius: 6px;
text-align: center;
font-weight: 700;
border: 1px solid var(--border);
}
.env-read-only-tag {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 600;
padding: 6px 10px;
border-radius: 6px;
}
.req-deployment-box {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px;
margin-bottom: 8px;
background: var(--border-light);
}
.req-deployment-title {
font-size: 12px;
font-weight: 700;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 6px;
}
.req-deployment-select {
width: 100%;
padding: 8px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-family: inherit;
font-size: 12px;
background: var(--card-bg);
color: var(--text);
}
.galaxy-toolbar {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
}
.galaxy-toolbar .btn {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
font-size: 11px;
padding: 5px 10px;
}
.galaxy-toolbar .btn:hover {
background: var(--accent);
color: var(--primary-dark);
}
.galaxy-modal-body {
padding: 0;
position: relative;
overflow: hidden;
background: #0a0e27;
}
.galaxy-chart-container {
width: 100%;
height: 100%;
min-height: 400px;
position: relative;
}
.galaxy-chart-container svg {
width: 100%;
height: 100%;
display: block;
cursor: grab;
}
.galaxy-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: rgba(255, 255, 255, 0.5);
text-align: center;
font-size: 16px;
gap: 10px;
}
.galaxy-legend {
position: absolute;
bottom: 16px;
right: 16px;
background: rgba(10, 14, 39, 0.85);
border: 1px solid rgba(201, 169, 97, 0.3);
border-radius: 10px;
padding: 12px 16px;
color: #fff;
font-size: 11px;
z-index: 10;
max-width: 220px;
}
.galaxy-legend-title {
font-weight: 700;
font-size: 12px;
margin-bottom: 10px;
color: var(--accent-light);
}
.galaxy-legend-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.galaxy-legend-color {
width: 14px;
height: 14px;
border-radius: 50%;
box-shadow: 0 0 8px currentColor;
}
.galaxy-info-panel {
position: absolute;
top: 16px;
left: 16px;
background: rgba(10, 14, 39, 0.9);
border: 1px solid rgba(201, 169, 97, 0.3);
border-radius: 10px;
padding: 14px 18px;
color: #fff;
font-size: 12px;
z-index: 10;
max-width: 340px;
/* تغییر از 280px به 340px */
max-height: 400px;
/* اضافه شد */
overflow-y: auto;
/* اضافه شد */
display: none;
}
.galaxy-info-panel.visible {
display: block;
}
.galaxy-info-panel h4 {
font-size: 14px;
font-weight: 700;
color: var(--accent-light);
margin-bottom: 10px;
}
.galaxy-info-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
gap: 12px;
}
.galaxy-info-label {
color: rgba(255, 255, 255, 0.6);
font-size: 11px;
}
.galaxy-info-value {
font-weight: 600;
font-size: 11px;
text-align: left;
}
.galaxy-zoom-indicator {
position: absolute;
bottom: 16px;
left: 16px;
background: rgba(10, 14, 39, 0.85);
border: 1px solid rgba(201, 169, 97, 0.3);
border-radius: 8px;
padding: 6px 12px;
color: #fff;
font-size: 12px;
z-index: 10;
font-weight: 600;
}
.galaxy-stats {
font-size: 12px;
color: var(--text-muted);
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.galaxy-stat-value {
font-weight: 700;
color: var(--primary);
background: rgba(26, 54, 93, 0.1);
padding: 2px 8px;
border-radius: 8px;
}
[data-theme="dark"] .galaxy-stat-value {
color: var(--accent);
background: rgba(201, 169, 97, 0.15);
}
/* ── جستجو در نمودار کهکشانی ── */
.galaxy-search {
position: relative;
display: flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
padding: 0 8px;
height: 32px;
min-width: 230px;
}
.galaxy-search-icon {
font-size: 12px;
opacity: 0.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, 0.55);
}
.galaxy-search-clear {
background: none;
border: none;
color: rgba(255, 255, 255, 0.75);
cursor: pointer;
font-size: 12px;
opacity: 0;
pointer-events: none;
}
.galaxy-search-clear.visible {
opacity: 1;
pointer-events: auto;
}
/* ── پنل لیست نتایج جستجو ── */
.galaxy-search-results {
position: absolute;
top: 60px;
right: 16px;
width: 320px;
max-height: 320px;
overflow-y: auto;
background: rgba(10, 14, 39, 0.95);
border: 1px solid rgba(201, 169, 97, 0.35);
border-radius: 10px;
color: #fff;
z-index: 20;
display: none;
padding: 6px;
backdrop-filter: blur(10px);
}
.galaxy-search-results.visible {
display: block;
}
.galaxy-search-result-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
}
.galaxy-search-result-item:hover {
background: rgba(201, 169, 97, 0.25);
}
.galaxy-search-result-icon {
font-size: 16px;
flex-shrink: 0;
}
.galaxy-search-result-text {
min-width: 0;
}
.galaxy-search-result-title {
font-size: 12px;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.galaxy-search-result-sub {
font-size: 10px;
color: rgba(255, 255, 255, 0.6);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.galaxy-search-empty {
padding: 14px;
text-align: center;
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
}
/* ── هایلایت و کم‌رنگ‌سازی نودها ── */
.galaxy-node.galaxy-dim {
opacity: 0.12;
}
.galaxy-node.galaxy-match > circle:nth-of-type(2) {
stroke: #ffd700;
stroke-width: 3;
}
.galaxy-link.galaxy-dim {
stroke-opacity: 0.04 !important;
}
.galaxy-node.galaxy-focus > circle:nth-of-type(2) {
stroke: #ffd700;
stroke-width: 4;
animation: galaxyPulse 0.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;
position: relative;
overflow: hidden;
}
.dashboard-kpi-card::before {
content: "";
position: absolute;
top: 0;
right: 0;
left: 0;
height: 3px;
background: linear-gradient(90deg, var(--primary), var(--accent));
}
.dashboard-kpi-icon {
font-size: 28px;
margin-bottom: 8px;
}
.dashboard-kpi-value {
font-size: 28px;
font-weight: 800;
color: var(--primary);
line-height: 1;
margin-bottom: 4px;
}
[data-theme="dark"] .dashboard-kpi-value {
color: var(--accent);
}
.dashboard-kpi-label {
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
}
.dashboard-charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 16px;
}
.dashboard-chart-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.dashboard-chart-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border-light);
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 700;
color: var(--primary);
}
[data-theme="dark"] .dashboard-chart-header {
color: var(--accent);
}
.dashboard-chart-body {
padding: 16px;
display: flex;
justify-content: center;
align-items: center;
min-height: 220px;
}
.dashboard-chart-body svg {
max-width: 100%;
height: auto;
}
.dashboard-empty-chart {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 12px;
gap: 8px;
text-align: center;
padding: 20px;
}
.dashboard-empty-chart span {
font-size: 32px;
opacity: 0.4;
}
.dashboard-footer-stats {
font-size: 11px;
color: var(--text-muted);
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.dashboard-footer-stats .stat-highlight {
font-weight: 700;
color: var(--primary);
}
[data-theme="dark"] .dashboard-footer-stats .stat-highlight {
color: var(--accent);
}
.d3-tooltip {
position: fixed;
background: var(--primary-dark);
color: #fff;
padding: 8px 12px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
pointer-events: none;
z-index: 3000;
display: none;
font-family: "Vazirmatn", sans-serif;
}
.d3-tooltip.visible {
display: block;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
</style>
</head>
<body>
<header class="app-header">
<div class="app-brand">
<div class="app-logo">📊</div>
<div class="app-brand-text">
<div class="app-title" id="headerTitle">دستیار انتشار</div>
<div class="app-subtitle" id="headerSubtitle">
مدیریت نسخه‌ها و نیازمندی‌های محصولات
</div>
</div>
</div>
</header>
<div class="tabs-bar" id="advancedToolbar">
<div class="toolbar-controls">
<button class="toolbar-icon-btn" id="sidebarToggle" title="سایدبار">
<span id="sidebarIcon">✕</span>
</button>
<button class="toolbar-icon-btn" id="themeToggle" title="تم">
<span id="themeIcon">🌙</span>
</button>
<button class="toolbar-icon-btn" id="undoBtn" title="Ctrl+Z" disabled>
↩
</button>
<button class="toolbar-icon-btn" id="redoBtn" title="Ctrl+Y" disabled>
↪
</button>
</div>
<div class="toolbar-search">
<span style="position: absolute; right: 10px; opacity: 0.7">🔍</span
><input
type="text"
class="search-input"
id="toolbarSearch"
placeholder="جستجو..."
/>
</div>
<div class="toolbar-filters">
<div class="filter-dropdown">
<span>🏷️</span
><select class="filter-select" id="filterType">
<option value="">همه انواع</option>
<option value="backend">بک‌اند</option>
<option value="frontend">فرانت‌اند</option>
<option value="service">میکروسرویس</option>
<option value="database">پایگاه داده</option>
<option value="mobile">موبایل</option>
<option value="infrastructure">زیرساخت</option>
</select>
</div>
<div class="filter-dropdown">
<span>🌐</span
><select class="filter-select" id="filterEnvironment">
<option value="">همه محیط‌ها</option>
</select>
</div>
</div>
<div class="toolbar-stats">
<div class="stat-item">
📋 <span class="stat-value" id="statTotal">0</span>
</div>
<div class="stat-item">
📦 <span class="stat-value" id="statProjects">0</span>
</div>
<div class="stat-item">
🌿 <span class="stat-value" id="statSubProjects">0</span>
</div>
<div class="stat-item">
🔗 <span class="stat-value" id="statDependencies">0</span>
</div>
</div>
</div>
<div class="app-main">
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="sidebar-title">🛠️ ابزارها</div>
</div>
<div class="sidebar-body" id="sidebarBody"></div>
</aside>
<main class="board-area" id="boardArea"></main>
</div>
<footer class="app-footer" id="appFooter">
<div>
<span class="footer-brand">طراح و توسعه‌دهنده:</span> هادی خزاعی اصل
<span style="margin: 0 10px">|</span
><span class="footer-brand">شرکت:</span> رایان پرداز کاوش
</div>
<div><span class="footer-brand">نسخه:</span> 1.1.0</div>
</footer>
<!-- Settings Modal -->
<div class="modal-overlay" id="settingsModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title">
<span>⚙️</span><span>تنظیمات محصول</span>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="readonly-info">
ℹ️ اطلاعات پایه محصول فقط قابل مشاهده هستند.
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label"
>عنوان محصول
<span class="readonly-badge">فقط خواندنی</span></label
><input
type="text"
class="form-input"
id="settingProjectTitle"
readonly
/>
</div>
<div class="form-group">
<label class="form-label"
>نسخه <span class="readonly-badge">فقط خواندنی</span></label
><input
type="text"
class="form-input"
id="settingVersion"
readonly
/>
</div>
</div>
<div class="form-group">
<label class="form-label"
>توضیحات <span class="readonly-badge">فقط خواندنی</span></label
><textarea
class="form-textarea"
id="settingDescription"
readonly
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label"
>توسعه‌دهنده
<span class="readonly-badge">فقط خواندنی</span></label
><input
type="text"
class="form-input"
id="settingDeveloper"
readonly
/>
</div>
<div class="form-group">
<label class="form-label"
>شرکت <span class="readonly-badge">فقط خواندنی</span></label
><input
type="text"
class="form-input"
id="settingCompany"
readonly
/>
</div>
</div>
<div class="webhook-section">
<div class="webhook-section-header">
<div class="webhook-section-title">🔗 تنظیمات وب‌هوک</div>
<div class="webhook-toggle">
<span>فعال‌سازی</span
><label class="switch"
><input type="checkbox" id="settingWebhookEnabled" /><span
class="slider"
></span
></label>
</div>
</div>
<div class="form-group">
<label class="form-label"
>آدرس URL وب‌هوک <span class="required">*</span></label
><input
type="url"
class="form-input"
id="settingWebhookUrl"
dir="ltr"
/>
</div>
<div class="form-group">
<label class="form-label"
>شناسه کلاینت <span class="required">*</span></label
><input
type="text"
class="form-input"
id="settingWebhookClientId"
dir="ltr"
/>
</div>
<div class="webhook-status">
<span class="webhook-status-dot" id="webhookStatusDot"></span
><span id="webhookStatusText">وضعیت: غیرفعال</span>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveSettingsBtn">💾 ذخیره</button
><button class="btn btn-ghost" data-close-modal>بستن</button>
</div>
</div>
</div>
<!-- Environment Modals -->
<div class="modal-overlay" id="envManagerModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title">🌍 افزودن محیط عملیاتی جدید</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="form-row">
<div class="form-group">
<label class="form-label"
>نام محیط <span class="required">*</span></label
><input type="text" class="form-input" id="newEnvName" />
</div>
<div class="form-group">
<label class="form-label">توضیحات</label
><input type="text" class="form-input" id="newEnvDesc" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">رنگ پس‌زمینه</label>
<div class="color-input-wrapper">
<input type="color" id="newEnvBgColor" value="#38a169" /><span
class="color-preview-text"
id="newEnvBgColorText"
>#38a169</span
>
</div>
</div>
<div class="form-group">
<label class="form-label">رنگ متن</label>
<div class="color-input-wrapper">
<input type="color" id="newEnvTextColor" value="#ffffff" /><span
class="color-preview-text"
id="newEnvTextColorText"
>#ffffff</span
>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveNewEnvBtn">💾 ثبت</button
><button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<div class="modal-overlay" id="envEditModal">
<div class="modal">
<div class="modal-header">
<div class="modal-title">✏️ ویرایش محیط عملیاتی</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label"
>شناسه <span class="readonly-badge">فقط خواندنی</span></label
><input type="text" class="form-input" id="editEnvId" readonly />
</div>
<div class="form-group">
<label class="form-label">نام <span class="required">*</span></label
><input type="text" class="form-input" id="editEnvName" />
</div>
<div class="form-group">
<label class="form-label">توضیحات</label
><input type="text" class="form-input" id="editEnvDesc" />
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">رنگ پس‌زمینه</label>
<div class="color-input-wrapper">
<input type="color" id="editEnvBgColor" /><span
class="color-preview-text"
id="editEnvBgColorText"
></span>
</div>
</div>
<div class="form-group">
<label class="form-label">رنگ متن</label>
<div class="color-input-wrapper">
<input type="color" id="editEnvTextColor" /><span
class="color-preview-text"
id="editEnvTextColorText"
></span>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label">پیش‌نمایش</label>
<div id="editEnvPreview" class="env-color-preview">نمونه</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveEnvEditBtn">💾 ذخیره</button
><button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<!-- Product Modals -->
<div class="modal-overlay" id="projectDetailModal">
<div class="modal modal-xl">
<div class="modal-header">
<div class="modal-title" id="projectDetailTitle">📦 جزئیات محصول</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body" id="projectDetailBody"></div>
<div class="modal-footer">
<button class="btn btn-danger btn-sm" id="projectDetailDeleteBtn">
🗑️ حذف محصول</button
><button class="btn btn-ghost" data-close-modal>بستن</button>
</div>
</div>
</div>
<div class="modal-overlay" id="productEditModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title">✏️ ویرایش محصول</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label"
>شناسه <span class="readonly-badge">فقط خواندنی</span></label
><input
type="text"
class="form-input"
id="editProductId"
readonly
/>
</div>
<div class="form-group">
<label class="form-label"
>عنوان <span class="required">*</span></label
><input type="text" class="form-input" id="editProductTitle" />
</div>
<div class="form-group">
<label class="form-label">توضیحات</label
><textarea
class="form-textarea"
id="editProductDescription"
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">مالک</label
><input type="text" class="form-input" id="editProductOwner" />
</div>
<div class="form-group">
<label class="form-label"
>تاریخ ایجاد
<span class="readonly-badge">فقط خواندنی</span></label
><input
type="text"
class="form-input"
id="editProductCreatedAt"
readonly
/>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveProductEditBtn">
💾 ذخیره</button
><button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<div class="modal-overlay" id="addProductModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title">➕ ایجاد محصول جدید</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label"
>عنوان <span class="required">*</span></label
><input type="text" class="form-input" id="newProductTitle" />
<div class="form-error" id="newProductTitleError">
عنوان محصول الزامی است و باید حداقل ۳ کاراکتر باشد
</div>
</div>
<div class="form-group">
<label class="form-label">توضیحات</label
><textarea
class="form-textarea"
id="newProductDescription"
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">مالک</label
><input type="text" class="form-input" id="newProductOwner" />
</div>
<div class="form-group">
<label class="form-label"
>تاریخ ایجاد
<span class="readonly-badge">فقط خواندنی</span></label
><input
type="text"
class="form-input"
id="newProductCreatedAt"
readonly
/>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveNewProductBtn">💾 ثبت</button
><button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<div class="modal-overlay" id="addProjectModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title">➕ افزودن پروژه جدید</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label"
>عنوان <span class="required">*</span></label
><input type="text" class="form-input" id="newProjectTitle" />
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label"
>نوع <span class="required">*</span></label
><select class="form-select" id="newProjectType">
<option value="">انتخاب کنید...</option>
</select>
</div>
<div class="form-group">
<label class="form-label">فریم‌ورک</label
><input type="text" class="form-input" id="newProjectFramework" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">زبان</label
><input type="text" class="form-input" id="newProjectLanguage" />
</div>
<div class="form-group">
<label class="form-label">آدرس مخزن</label
><input
type="text"
class="form-input"
id="newProjectRepo"
dir="ltr"
/>
</div>
</div>
<div class="form-group">
<label class="form-label">توضیحات</label
><textarea
class="form-textarea"
id="newProjectDescription"
></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveNewProjectBtn">💾 ذخیره</button
><button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<div class="modal-overlay" id="editProjectModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title">✏️ ویرایش پروژه</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label"
>شناسه <span class="readonly-badge">فقط خواندنی</span></label
><input type="text" class="form-input" id="editProjId" readonly />
</div>
<div class="form-group">
<label class="form-label"
>عنوان <span class="required">*</span></label
><input type="text" class="form-input" id="editProjTitle" />
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label"
>نوع <span class="required">*</span></label
><select class="form-select" id="editProjType">
<option value="">انتخاب کنید...</option>
</select>
</div>
<div class="form-group">
<label class="form-label">فریم‌ورک</label
><input type="text" class="form-input" id="editProjFramework" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">زبان</label
><input type="text" class="form-input" id="editProjLanguage" />
</div>
<div class="form-group">
<label class="form-label">آدرس مخزن</label
><input
type="text"
class="form-input"
id="editProjRepo"
dir="ltr"
/>
</div>
</div>
<div class="form-group">
<label class="form-label">توضیحات</label
><textarea
class="form-textarea"
id="editProjDescription"
></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveProjEditBtn">💾 ذخیره</button
><button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<!-- Deployment Modal -->
<div class="modal-overlay" id="deploymentModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title" id="deploymentModalTitle">
🚀 افزودن نسخه استقرار
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="form-group" id="deploymentEnvGroup"></div>
<div class="form-row">
<div class="form-group">
<label class="form-label"
>نسخه <span class="required">*</span></label
><input
type="text"
class="form-input"
id="depVersion"
dir="ltr"
placeholder="مثلاً: 1.2.3"
/>
<div class="form-hint">از فرمت SemVer استفاده کنید</div>
</div>
<div class="form-group">
<label class="form-label"
>تاریخ و زمان استقرار <span class="required">*</span></label
><input
type="datetime-local"
class="form-input"
id="depDate"
dir="ltr"
/>
</div>
</div>
<div class="form-group">
<label class="form-label"
>ریلیز نوت <span class="required">*</span></label
><textarea
class="form-textarea"
id="depReleaseNotes"
rows="3"
placeholder="توضیحات تغییرات این نسخه..."
></textarea>
<div class="form-hint">
شرح تغییرات، باگ‌فیکس‌ها و ویژگی‌های جدید این نسخه
</div>
</div>
<div id="requiredDeploymentsSection"></div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveDeploymentBtn">💾 ذخیره</button
><button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<!-- Dependency Manager Modal -->
<div class="modal-overlay" id="dependencyManagerModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title">🔗 مدیریت وابستگی‌ها</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="readonly-info">
ℹ️ هر وابستگی فقط شامل یک پروژه است. انتخاب نسخه و محیط در زمان ثبت
استقرار انجام می‌شود.
</div>
<div class="dependency-section-title">📋 وابستگی‌های فعلی</div>
<div id="currentDependenciesList" class="dependency-list"></div>
<div class="dependency-search-box">
<div class="dependency-section-title">🔍 افزودن وابستگی جدید</div>
<input
type="text"
class="dependency-search-input"
id="dependencySearch"
placeholder="جستجو در پروژه‌ها..."
/>
<div
id="dependencySearchResults"
class="dependency-search-results"
></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" data-close-modal>بستن</button>
</div>
</div>
</div>
<!-- Galaxy Chart Modal -->
<div class="modal-overlay" id="galaxyChartModal">
<div class="modal modal-galaxy">
<div class="modal-header">
<div class="modal-title">🌌 نمودار کهکشانی محصولات</div>
<div class="galaxy-toolbar">
<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="galaxyCollapseAll"
title="کولاپس همه محصولات"
>
⊟ کولاپس همه
</button>
<button
class="btn btn-sm"
id="galaxyExpandAll"
title="باز کردن همه محصولات"
>
⊞ باز کردن همه
</button>
<button class="btn btn-sm" id="galaxyResetView">🎯 بازنشانی</button>
<button class="btn btn-sm" id="galaxyExportPNG">📷 PNG</button>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body galaxy-modal-body">
<div id="galaxyChartContainer" class="galaxy-chart-container"></div>
<div class="galaxy-search-results" id="galaxySearchResults"></div>
<div class="galaxy-legend" id="galaxyLegend"></div>
<div class="galaxy-info-panel" id="galaxyInfoPanel">
<button
class="modal-close"
id="galaxyInfoClose"
style="
position: absolute;
top: 8px;
left: 8px;
width: 22px;
height: 22px;
font-size: 12px;
"
>
✕
</button>
<div id="galaxyInfoContent"></div>
</div>
<div class="galaxy-zoom-indicator" id="galaxyZoomIndicator">
🔍 100%
</div>
</div>
<div class="modal-footer">
<div class="galaxy-stats" id="galaxyStats"></div>
<button class="btn btn-ghost" data-close-modal>بستن</button>
</div>
</div>
</div>
<!-- ✅ Executive Dashboard Modal (COMPLETED) -->
<div class="modal-overlay" id="executiveDashboardModal">
<div class="modal modal-galaxy">
<div class="modal-header">
<div class="modal-title">📊 داشبورد مدیریتی</div>
<div class="galaxy-toolbar">
<button class="btn btn-sm" id="dashboardRefresh">
🔄 به‌روزرسانی
</button>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body dashboard-modal-body">
<div id="dashboardContainer" class="dashboard-container"></div>
</div>
<div class="modal-footer">
<div class="dashboard-footer-stats" id="dashboardFooterStats"></div>
<button class="btn btn-ghost" data-close-modal>بستن</button>
</div>
</div>
</div>
<!-- Dependency Analysis Modal -->
<div class="modal-overlay" id="dependencyAnalysisModal">
<div class="modal modal-galaxy">
<div class="modal-header">
<div class="modal-title">🔗 تحلیل وابستگی‌ها</div>
<div class="galaxy-toolbar">
<button class="btn btn-sm" id="depAnalysisRefresh">🔄</button>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body dashboard-modal-body">
<div
id="dependencyAnalysisContainer"
class="dashboard-container"
></div>
</div>
<div class="modal-footer">
<div class="dashboard-footer-stats" id="depAnalysisFooterStats"></div>
<button class="btn btn-ghost" data-close-modal>بستن</button>
</div>
</div>
</div>
<!-- Deployment Analysis Modal -->
<div class="modal-overlay" id="deploymentAnalysisModal">
<div class="modal modal-galaxy">
<div class="modal-header">
<div class="modal-title">🚀 تحلیل استقرار</div>
<div class="galaxy-toolbar">
<button class="btn btn-sm" id="deployAnalysisRefresh">🔄</button>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body dashboard-modal-body">
<div
id="deploymentAnalysisContainer"
class="dashboard-container"
></div>
</div>
<div class="modal-footer">
<div
class="dashboard-footer-stats"
id="deployAnalysisFooterStats"
></div>
<button class="btn btn-ghost" data-close-modal>بستن</button>
</div>
</div>
</div>
<!-- Version Analysis Modal -->
<div class="modal-overlay" id="versionAnalysisModal">
<div class="modal modal-galaxy">
<div class="modal-header">
<div class="modal-title">🔢 تحلیل نسخه</div>
<div class="galaxy-toolbar">
<button class="btn btn-sm" id="verAnalysisRefresh">🔄</button>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body dashboard-modal-body">
<div id="versionAnalysisContainer" class="dashboard-container"></div>
</div>
<div class="modal-footer">
<div class="dashboard-footer-stats" id="verAnalysisFooterStats"></div>
<button class="btn btn-ghost" data-close-modal>بستن</button>
</div>
</div>
</div>
<!-- Risk Analysis Modal -->
<div class="modal-overlay" id="riskAnalysisModal">
<div class="modal modal-galaxy">
<div class="modal-header">
<div class="modal-title">⚠️ تحلیل ریسک</div>
<div class="galaxy-toolbar">
<button class="btn btn-sm" id="riskAnalysisRefresh">🔄</button>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body dashboard-modal-body">
<div id="riskAnalysisContainer" class="dashboard-container"></div>
</div>
<div class="modal-footer">
<div
class="dashboard-footer-stats"
id="riskAnalysisFooterStats"
></div>
<button class="btn btn-ghost" data-close-modal>بستن</button>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<div class="galaxy-tooltip" id="galaxyTooltip"></div>
<div class="d3-tooltip" id="d3Tooltip"></div>
<script>
/* ==================== DATA (NEW STRUCTURE) ==================== */
const projectData = {
projectId: "550e8400-e29b-41d4-a716-446655440000",
projectName: "xReleaseAssistant",
projectTitle: "دستیار انتشار",
description: "مدیریت نسخه‌ها و نیازمندی‌های محصولات",
version: "1.1.0",
developer: "هادی خزاعی اصل",
company: "رایان پرداز کاوش",
metadata: {
createdAt: "2026-09-13T00:00:00Z",
updatedAt: "2026-09-14T00:00:00Z",
tags: ["release-management"],
customFields: {},
},
webhook: { url: "", clientId: "", enabled: false },
toolGroups: [
{
id: "product-management",
title: "پرونده",
icon: "📁",
description: "مدیریت چرخه حیات محصول",
tools: [
{
id: "clear-all",
title: "پاکسازی",
icon: "🗑️",
action: "clearAll",
description: "پاکسازی کامل",
},
{
id: "save-product",
title: "ذخیره",
icon: "💾",
action: "saveProduct",
description: "دانلود داده‌ها",
},
{
id: "load-product",
title: "بارگذاری",
icon: "📂",
action: "loadProduct",
description: "بارگذاری از فایل",
},
{
id: "product-settings",
title: "تنظیمات",
icon: "⚙️",
action: "openProductSettings",
description: "پیکربندی",
},
],
},
{
id: "reports",
title: "گزارش‌ها",
icon: "📊",
description: "تحلیل و گزارش‌گیری",
tools: [
{
id: "executive-dashboard",
title: "داشبورد مدیریتی",
icon: "📊",
action: "showExecutiveDashboard",
description: "نمای کلی با نمودارها",
},
{
id: "dependency-analysis",
title: "تحلیل وابستگی‌ها",
icon: "🔗",
action: "showDependencyAnalysis",
description: "تحلیل وابستگی‌ها",
},
{
id: "deployment-analysis",
title: "تحلیل استقرار",
icon: "🚀",
action: "showDeploymentAnalysis",
description: "تحلیل استقرارها",
},
{
id: "version-analysis",
title: "تحلیل نسخه",
icon: "🔢",
action: "showVersionAnalysis",
description: "تحلیل نسخه‌ها",
},
{
id: "risk-analysis",
title: "تحلیل ریسک",
icon: "⚠️",
action: "showRiskAnalysis",
description: "تحلیل ریسک‌ها",
},
{
id: "galaxy-chart",
title: "نمودار کهکشانی",
icon: "🌌",
action: "showGalaxyChart",
description: "نمودار کهکشانی",
},
],
},
],
appData: {
environments: [
{
id: "env-prod-001",
name: "Production",
description: "محیط عملیاتی اصلی - در دسترس کاربران نهایی",
backgroundColor: "#38a169",
textColor: "#ffffff",
},
{
id: "env-stage-002",
name: "Staging",
description: "محیط پیش از تولید - شبیه‌سازی Production",
backgroundColor: "#dd6b20",
textColor: "#ffffff",
},
{
id: "env-dev-003",
name: "Development",
description: "محیط توسعه - برای تیم فنی",
backgroundColor: "#3182ce",
textColor: "#ffffff",
},
{
id: "env-qa-004",
name: "QA",
description: "محیط تست کیفیت - کنترل کیفیت خودکار و دستی",
backgroundColor: "#805ad5",
textColor: "#ffffff",
},
{
id: "env-uat-005",
name: "UAT",
description: "تست پذیرش کاربر - تأیید کارفرما",
backgroundColor: "#d69e2e",
textColor: "#ffffff",
},
{
id: "env-dr-006",
name: "Disaster Recovery",
description: "محیط بازیابی پس از بحران - پشتیبان Production",
backgroundColor: "#e53e3e",
textColor: "#ffffff",
},
],
products: [
{
id: "prod-ecommerce",
title: "سامانه فروشگاه آنلاین دیجی‌مارکت",
description:
"پلتفرم جامع تجارت الکترونیک با قابلیت‌های چندفروشندگی",
metadata: {
createdAt: "2026-01-15T08:00:00Z",
owner: "تیم توسعه تجارت الکترونیک",
customFields: { industry: "E-commerce", priority: "critical" },
},
projects: [
{
id: "proj-ecom-api",
title: "E-commerce Core API",
description: "سرویس بک‌اند اصلی فروشگاه",
type: "backend",
repositoryUrl:
"https://git.rayanpardaz.ir/ecommerce/core-api",
metadata: {
framework: ".NET 8",
language: "C#",
team: "Backend Team A",
},
dependencies: ["proj-payment-gateway", "proj-inventory-svc"],
deployments: [
{
id: "dep-ecom-api-prod",
environmentId: "env-prod-001",
version: "3.5.2",
deploymentDate: "2026-09-10T14:30:00Z",
releaseNotes:
"رفع باگ پردازش سفارش و بهبود عملکرد کوئری‌ها",
requiredDeployments: [
{
projectId: "proj-payment-gateway",
deploymentId: "dep-payment-prod",
},
{
projectId: "proj-inventory-svc",
deploymentId: "dep-inventory-prod",
},
],
status: "active",
},
{
id: "dep-ecom-api-stage",
environmentId: "env-stage-002",
version: "3.6.0-rc.1",
deploymentDate: "2026-09-14T09:00:00Z",
releaseNotes: "افزودن قابلیت چندزبانه و بهینه‌سازی کش",
requiredDeployments: [],
status: "active",
},
{
id: "dep-ecom-api-dev",
environmentId: "env-dev-003",
version: "3.7.0-dev.12",
deploymentDate: "2026-09-15T16:00:00Z",
releaseNotes: "پیاده‌سازی اولیه ماژول تخفیف پیشرفته",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-ecom-web",
title: "E-commerce Web Frontend",
description: "اپلیکیشن وب فروشگاه با Angular",
type: "frontend",
repositoryUrl: "https://git.rayanpardaz.ir/ecommerce/web-app",
metadata: { framework: "Angular 18", language: "TypeScript" },
dependencies: ["proj-ecom-api"],
deployments: [
{
id: "dep-ecom-web-prod",
environmentId: "env-prod-001",
version: "4.2.1",
deploymentDate: "2026-09-11T10:00:00Z",
releaseNotes:
"بهبود UX صفحه پرداخت و افزودن فیلتر پیشرفته",
requiredDeployments: [
{
projectId: "proj-ecom-api",
deploymentId: "dep-ecom-api-prod",
},
],
status: "active",
},
{
id: "dep-ecom-web-stage",
environmentId: "env-stage-002",
version: "4.3.0-rc.1",
deploymentDate: "2026-09-13T11:30:00Z",
releaseNotes: "پیاده‌سازی Dark Mode و بهینه‌سازی SEO",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-ecom-mobile",
title: "E-commerce Mobile App",
description: "اپلیکیشن موبایل فروشگاه (iOS/Android)",
type: "mobile",
repositoryUrl:
"https://git.rayanpardaz.ir/ecommerce/mobile-app",
metadata: { framework: "Flutter 3.22", language: "Dart" },
dependencies: ["proj-ecom-api", "proj-notification-svc"],
deployments: [
{
id: "dep-ecom-mobile-prod",
environmentId: "env-prod-001",
version: "2.8.0",
deploymentDate: "2026-09-08T08:00:00Z",
releaseNotes:
"انتشار در استورها با قابلیت Push Notification",
requiredDeployments: [
{
projectId: "proj-ecom-api",
deploymentId: "dep-ecom-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-ecom-db",
title: "E-commerce Database",
description: "پایگاه داده اصلی فروشگاه",
type: "database",
repositoryUrl:
"https://git.rayanpardaz.ir/ecommerce/db-migrations",
metadata: { framework: "SQL Server 2022", language: "T-SQL" },
dependencies: [],
deployments: [
{
id: "dep-ecom-db-prod",
environmentId: "env-prod-001",
version: "5.1.0",
deploymentDate: "2026-09-05T02:00:00Z",
releaseNotes:
"افزودن ایندکس‌های جدید و بهینه‌سازی جداول سفارش",
requiredDeployments: [],
status: "active",
},
{
id: "dep-ecom-db-dr",
environmentId: "env-dr-006",
version: "5.1.0",
deploymentDate: "2026-09-05T03:00:00Z",
releaseNotes:
"همگام‌سازی با Production برای Disaster Recovery",
requiredDeployments: [
{
projectId: "proj-ecom-db",
deploymentId: "dep-ecom-db-prod",
},
],
status: "active",
},
],
},
{
id: "proj-ecom-search",
title: "E-commerce Search Service",
description: "سرویس جستجوی پیشرفته مبتنی بر Elasticsearch",
type: "service",
repositoryUrl:
"https://git.rayanpardaz.ir/ecommerce/search-svc",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: ["proj-ecom-db"],
deployments: [
{
id: "dep-ecom-search-prod",
environmentId: "env-prod-001",
version: "1.9.3",
deploymentDate: "2026-09-09T13:00:00Z",
releaseNotes:
"بهبود الگوریتم جستجوی فازی و افزودن فیلتر قیمت",
requiredDeployments: [
{
projectId: "proj-ecom-db",
deploymentId: "dep-ecom-db-prod",
},
],
status: "active",
},
],
},
{
id: "proj-ecom-infra",
title: "E-commerce Infrastructure",
description: "زیرساخت Kubernetes و CI/CD",
type: "infrastructure",
repositoryUrl: "https://git.rayanpardaz.ir/ecommerce/infra",
metadata: {
framework: "Kubernetes 1.29",
language: "YAML/Helm",
},
dependencies: [],
deployments: [
{
id: "dep-ecom-infra-prod",
environmentId: "env-prod-001",
version: "2.1.0",
deploymentDate: "2026-09-01T06:00:00Z",
releaseNotes:
"به‌روزرسانی Helm Charts و افزودن Auto-scaling",
requiredDeployments: [],
status: "active",
},
],
},
],
},
{
id: "prod-banking",
title: "پلتفرم بانکداری دیجیتال پارسیان‌بانک",
description: "سامانه جامع بانکداری دیجیتال با قابلیت‌های نئوبانک",
metadata: {
createdAt: "2026-02-01T08:00:00Z",
owner: "تیم توسعه بانکداری دیجیتال",
customFields: { industry: "FinTech", priority: "critical" },
},
projects: [
{
id: "proj-bank-core",
title: "Banking Core Engine",
description: "موتور اصلی پردازش تراکنش‌های بانکی",
type: "backend",
repositoryUrl:
"https://git.rayanpardaz.ir/banking/core-engine",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: ["proj-payment-gateway", "proj-auth-svc"],
deployments: [
{
id: "dep-bank-core-prod",
environmentId: "env-prod-001",
version: "6.2.0",
deploymentDate: "2026-09-12T01:00:00Z",
releaseNotes:
"افزودن پشتیبانی از تراکنش‌های بین‌المللی و بهبود امنیت",
requiredDeployments: [
{
projectId: "proj-payment-gateway",
deploymentId: "dep-payment-prod",
},
],
status: "active",
},
{
id: "dep-bank-core-uat",
environmentId: "env-uat-005",
version: "6.3.0-rc.2",
deploymentDate: "2026-09-14T10:00:00Z",
releaseNotes: "تست پذیرش ماژول وام‌دهی خرد",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-bank-web",
title: "Banking Web Portal",
description: "پرتال وب بانکداری اینترنتی",
type: "frontend",
repositoryUrl:
"https://git.rayanpardaz.ir/banking/web-portal",
metadata: { framework: "Angular 18", language: "TypeScript" },
dependencies: ["proj-bank-core"],
deployments: [
{
id: "dep-bank-web-prod",
environmentId: "env-prod-001",
version: "5.1.4",
deploymentDate: "2026-09-13T08:30:00Z",
releaseNotes:
"بازطراحی داشبورد کاربر و افزودن گزارش‌های مالی",
requiredDeployments: [
{
projectId: "proj-bank-core",
deploymentId: "dep-bank-core-prod",
},
],
status: "active",
},
],
},
{
id: "proj-bank-mobile",
title: "Banking Mobile App",
description: "اپلیکیشن موبایل بانکداری همراه",
type: "mobile",
repositoryUrl:
"https://git.rayanpardaz.ir/banking/mobile-app",
metadata: {
framework: "Kotlin Multiplatform",
language: "Kotlin",
},
dependencies: ["proj-bank-core", "proj-notification-svc"],
deployments: [
{
id: "dep-bank-mobile-prod",
environmentId: "env-prod-001",
version: "4.0.2",
deploymentDate: "2026-09-07T09:00:00Z",
releaseNotes: "افزودن ورود با Biometric و کارت به کارت",
requiredDeployments: [
{
projectId: "proj-bank-core",
deploymentId: "dep-bank-core-prod",
},
],
status: "active",
},
{
id: "dep-bank-mobile-qa",
environmentId: "env-qa-004",
version: "4.1.0-beta.3",
deploymentDate: "2026-09-15T14:00:00Z",
releaseNotes: "تست قابلیت پرداخت با QR Code",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-bank-db",
title: "Banking Database Cluster",
description: "خوشه پایگاه داده بانکی با قابلیت HA",
type: "database",
repositoryUrl:
"https://git.rayanpardaz.ir/banking/db-cluster",
metadata: { framework: "Oracle 19c", language: "PL/SQL" },
dependencies: [],
deployments: [
{
id: "dep-bank-db-prod",
environmentId: "env-prod-001",
version: "8.0.1",
deploymentDate: "2026-09-02T03:00:00Z",
releaseNotes:
"ارتقای ساختار جداول تراکنش و افزودن Partitioning",
requiredDeployments: [],
status: "active",
},
{
id: "dep-bank-db-dr",
environmentId: "env-dr-006",
version: "8.0.1",
deploymentDate: "2026-09-02T04:00:00Z",
releaseNotes: "همگام‌سازی Active-Passive با Production",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-bank-fraud",
title: "Fraud Detection Service",
description: "سرویس هوشمند تشخیص تقلب با ML",
type: "service",
repositoryUrl: "https://git.rayanpardaz.ir/banking/fraud-svc",
metadata: {
framework: "Python 3.12 + FastAPI",
language: "Python",
},
dependencies: ["proj-bank-core", "proj-bank-db"],
deployments: [
{
id: "dep-bank-fraud-prod",
environmentId: "env-prod-001",
version: "2.4.0",
deploymentDate: "2026-09-11T15:00:00Z",
releaseNotes:
"به‌روزرسانی مدل ML و کاهش نرخ False Positive",
requiredDeployments: [
{
projectId: "proj-bank-core",
deploymentId: "dep-bank-core-prod",
},
],
status: "active",
},
],
},
],
},
{
id: "prod-healthcare",
title: "سامانه سلامت و نوبت‌دهی سلامت‌یار",
description: "پلتفرم جامع نوبت‌دهی و پرونده الکترونیک سلامت",
metadata: {
createdAt: "2026-02-20T08:00:00Z",
owner: "تیم توسعه سلامت دیجیتال",
customFields: { industry: "Healthcare", priority: "high" },
},
projects: [
{
id: "proj-health-api",
title: "Healthcare Core API",
description: "سرویس بک‌اند سامانه سلامت",
type: "backend",
repositoryUrl:
"https://git.rayanpardaz.ir/healthcare/core-api",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: ["proj-auth-svc", "proj-notification-svc"],
deployments: [
{
id: "dep-health-api-prod",
environmentId: "env-prod-001",
version: "2.7.1",
deploymentDate: "2026-09-10T11:00:00Z",
releaseNotes: "افزودن ماژول نسخه‌نویسی الکترونیک",
requiredDeployments: [],
status: "active",
},
{
id: "dep-health-api-stage",
environmentId: "env-stage-002",
version: "2.8.0-rc.1",
deploymentDate: "2026-09-14T13:00:00Z",
releaseNotes: "یکپارچگی با سامانه بیمه تأمین اجتماعی",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-health-web",
title: "Healthcare Patient Portal",
description: "پرتال بیماران برای نوبت‌دهی و مشاهده پرونده",
type: "frontend",
repositoryUrl:
"https://git.rayanpardaz.ir/healthcare/patient-portal",
metadata: { framework: "React 18", language: "TypeScript" },
dependencies: ["proj-health-api"],
deployments: [
{
id: "dep-health-web-prod",
environmentId: "env-prod-001",
version: "3.1.0",
deploymentDate: "2026-09-12T09:30:00Z",
releaseNotes:
"بهبود فرآیند رزرو نوبت و افزودن یادآور خودکار",
requiredDeployments: [
{
projectId: "proj-health-api",
deploymentId: "dep-health-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-health-doc",
title: "Doctor Dashboard",
description: "داشبورد پزشکان برای مدیریت بیماران",
type: "frontend",
repositoryUrl:
"https://git.rayanpardaz.ir/healthcare/doctor-dashboard",
metadata: { framework: "Angular 18", language: "TypeScript" },
dependencies: ["proj-health-api"],
deployments: [
{
id: "dep-health-doc-prod",
environmentId: "env-prod-001",
version: "2.3.2",
deploymentDate: "2026-09-09T10:00:00Z",
releaseNotes: "افزودن تقویم هوشمند و مدیریت نسخه‌ها",
requiredDeployments: [
{
projectId: "proj-health-api",
deploymentId: "dep-health-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-health-db",
title: "Healthcare Database",
description: "پایگاه داده پرونده‌های الکترونیک سلامت",
type: "database",
repositoryUrl: "https://git.rayanpardaz.ir/healthcare/db",
metadata: { framework: "PostgreSQL 16", language: "SQL" },
dependencies: [],
deployments: [
{
id: "dep-health-db-prod",
environmentId: "env-prod-001",
version: "4.0.0",
deploymentDate: "2026-09-03T02:30:00Z",
releaseNotes: "مهاجرت به ساختار جدید پرونده الکترونیک",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-health-mobile",
title: "Healthcare Mobile App",
description: "اپلیکیشن موبایل بیماران",
type: "mobile",
repositoryUrl: "https://git.rayanpardaz.ir/healthcare/mobile",
metadata: {
framework: "React Native 0.74",
language: "TypeScript",
},
dependencies: ["proj-health-api"],
deployments: [
{
id: "dep-health-mobile-prod",
environmentId: "env-prod-001",
version: "1.5.0",
deploymentDate: "2026-09-06T08:00:00Z",
releaseNotes: "انتشار نسخه اولیه با قابلیت نوبت‌دهی",
requiredDeployments: [
{
projectId: "proj-health-api",
deploymentId: "dep-health-api-prod",
},
],
status: "active",
},
],
},
],
},
{
id: "prod-edtech",
title: "پلتفرم آموزش آنلاین دانش‌یار",
description:
"سامانه LMS با قابلیت برگزاری کلاس زنده و محتوای تعاملی",
metadata: {
createdAt: "2026-03-05T08:00:00Z",
owner: "تیم توسعه EdTech",
customFields: { industry: "Education", priority: "high" },
},
projects: [
{
id: "proj-edu-api",
title: "LMS Core API",
description: "سرویس بک‌اند سامانه آموزش",
type: "backend",
repositoryUrl: "https://git.rayanpardaz.ir/edtech/core-api",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: ["proj-auth-svc", "proj-notification-svc"],
deployments: [
{
id: "dep-edu-api-prod",
environmentId: "env-prod-001",
version: "3.2.1",
deploymentDate: "2026-09-11T12:00:00Z",
releaseNotes: "افزودن سیستم آزمون آنلاین و تصحیح خودکار",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-edu-web",
title: "LMS Web Platform",
description: "پلتفرم وب آموزش برای دانشجویان و اساتید",
type: "frontend",
repositoryUrl:
"https://git.rayanpardaz.ir/edtech/web-platform",
metadata: { framework: "Next.js 14", language: "TypeScript" },
dependencies: ["proj-edu-api", "proj-video-streaming"],
deployments: [
{
id: "dep-edu-web-prod",
environmentId: "env-prod-001",
version: "4.0.3",
deploymentDate: "2026-09-13T10:00:00Z",
releaseNotes:
"بهبود پخش‌کننده ویدیو و افزودن زیرنویس خودکار",
requiredDeployments: [
{
projectId: "proj-edu-api",
deploymentId: "dep-edu-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-edu-video",
title: "Video Streaming Service",
description: "سرویس استریم ویدیوی کلاس‌های زنده",
type: "service",
repositoryUrl:
"https://git.rayanpardaz.ir/edtech/video-streaming",
metadata: { framework: "Node.js 20", language: "TypeScript" },
dependencies: [],
deployments: [
{
id: "dep-edu-video-prod",
environmentId: "env-prod-001",
version: "2.1.0",
deploymentDate: "2026-09-08T14:00:00Z",
releaseNotes: "پشتیبانی از کیفیت 4K و کاهش تأخیر استریم",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-edu-db",
title: "LMS Database",
description: "پایگاه داده دوره‌ها و کاربران",
type: "database",
repositoryUrl: "https://git.rayanpardaz.ir/edtech/db",
metadata: {
framework: "MongoDB 7",
language: "MongoDB Query",
},
dependencies: [],
deployments: [
{
id: "dep-edu-db-prod",
environmentId: "env-prod-001",
version: "3.0.0",
deploymentDate: "2026-09-04T03:00:00Z",
releaseNotes: "مهاجرت به ساختار شاردینگ برای مقیاس‌پذیری",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-edu-mobile",
title: "LMS Mobile App",
description: "اپلیکیشن موبایل دانشجویان",
type: "mobile",
repositoryUrl: "https://git.rayanpardaz.ir/edtech/mobile",
metadata: { framework: "Flutter 3.22", language: "Dart" },
dependencies: ["proj-edu-api"],
deployments: [
{
id: "dep-edu-mobile-prod",
environmentId: "env-prod-001",
version: "2.0.1",
deploymentDate: "2026-09-07T09:30:00Z",
releaseNotes: "قابلیت دانلود آفلاین ویدیوها",
requiredDeployments: [
{
projectId: "proj-edu-api",
deploymentId: "dep-edu-api-prod",
},
],
status: "active",
},
],
},
],
},
{
id: "prod-transport",
title: "سامانه حمل‌ونقل هوشمند ترافیک‌یار",
description: "پلتفرم مدیریت ناوگان و مسیریابی هوشمند",
metadata: {
createdAt: "2026-03-18T08:00:00Z",
owner: "تیم توسعه Smart City",
customFields: { industry: "Transportation", priority: "high" },
},
projects: [
{
id: "proj-trans-api",
title: "Transport Core API",
description: "سرویس بک‌اند سامانه حمل‌ونقل",
type: "backend",
repositoryUrl:
"https://git.rayanpardaz.ir/transport/core-api",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: ["proj-maps-svc", "proj-auth-svc"],
deployments: [
{
id: "dep-trans-api-prod",
environmentId: "env-prod-001",
version: "2.5.0",
deploymentDate: "2026-09-10T13:00:00Z",
releaseNotes:
"افزودن الگوریتم مسیریابی پیشرفته با ترافیک زنده",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-trans-web",
title: "Fleet Management Dashboard",
description: "داشبورد مدیریت ناوگان",
type: "frontend",
repositoryUrl:
"https://git.rayanpardaz.ir/transport/fleet-dashboard",
metadata: { framework: "Vue.js 3", language: "TypeScript" },
dependencies: ["proj-trans-api"],
deployments: [
{
id: "dep-trans-web-prod",
environmentId: "env-prod-001",
version: "3.1.2",
deploymentDate: "2026-09-12T11:00:00Z",
releaseNotes:
"نمایش زنده ناوگان روی نقشه و گزارش‌های عملکرد",
requiredDeployments: [
{
projectId: "proj-trans-api",
deploymentId: "dep-trans-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-maps-svc",
title: "Maps & Geolocation Service",
description: "سرویس نقشه و موقعیت‌یابی - سرویس مشترک",
type: "service",
repositoryUrl:
"https://git.rayanpardaz.ir/transport/maps-svc",
metadata: { framework: "Go 1.22", language: "Go" },
dependencies: [],
deployments: [
{
id: "dep-trans-maps-prod",
environmentId: "env-prod-001",
version: "1.8.0",
deploymentDate: "2026-09-09T15:00:00Z",
releaseNotes: "بهینه‌سازی reverse geocoding و افزودن POI",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-trans-mobile",
title: "Driver Mobile App",
description: "اپلیکیشن رانندگان",
type: "mobile",
repositoryUrl:
"https://git.rayanpardaz.ir/transport/driver-app",
metadata: { framework: "Kotlin", language: "Kotlin" },
dependencies: ["proj-trans-api"],
deployments: [
{
id: "dep-trans-mobile-prod",
environmentId: "env-prod-001",
version: "2.2.1",
deploymentDate: "2026-09-08T08:30:00Z",
releaseNotes: "بهبود مصرف باتری و دقت GPS",
requiredDeployments: [
{
projectId: "proj-trans-api",
deploymentId: "dep-trans-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-trans-db",
title: "Transport Database",
description: "پایگاه داده سفرها و ناوگان",
type: "database",
repositoryUrl: "https://git.rayanpardaz.ir/transport/db",
metadata: {
framework: "PostgreSQL 16 + PostGIS",
language: "SQL",
},
dependencies: [],
deployments: [
{
id: "dep-trans-db-prod",
environmentId: "env-prod-001",
version: "3.2.0",
deploymentDate: "2026-09-03T02:00:00Z",
releaseNotes:
"افزودن جداول مکانی و بهینه‌سازی کوئری‌های برداری",
requiredDeployments: [],
status: "active",
},
],
},
],
},
{
id: "prod-realestate",
title: "پلتفرم املاک و مستغلات خانه‌یار",
description: "سامانه جامع خرید، فروش و اجاره املاک",
metadata: {
createdAt: "2026-04-02T08:00:00Z",
owner: "تیم توسعه PropTech",
customFields: { industry: "Real Estate", priority: "medium" },
},
projects: [
{
id: "proj-re-api",
title: "Real Estate Core API",
description: "سرویس بک‌اند پلتفرم املاک",
type: "backend",
repositoryUrl:
"https://git.rayanpardaz.ir/realestate/core-api",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: ["proj-maps-svc", "proj-auth-svc"],
deployments: [
{
id: "dep-re-api-prod",
environmentId: "env-prod-001",
version: "2.1.3",
deploymentDate: "2026-09-11T10:30:00Z",
releaseNotes: "افزودن تور مجازی ۳۶۰ درجه و فیلتر پیشرفته",
requiredDeployments: [
{
projectId: "proj-maps-svc",
deploymentId: "dep-trans-maps-prod",
},
],
status: "active",
},
],
},
{
id: "proj-re-web",
title: "Real Estate Web Portal",
description: "پرتال وب جستجوی املاک",
type: "frontend",
repositoryUrl:
"https://git.rayanpardaz.ir/realestate/web-portal",
metadata: { framework: "Next.js 14", language: "TypeScript" },
dependencies: ["proj-re-api"],
deployments: [
{
id: "dep-re-web-prod",
environmentId: "env-prod-001",
version: "3.0.2",
deploymentDate: "2026-09-13T09:00:00Z",
releaseNotes: "نمایش املاک روی نقشه و مقایسه چندگانه",
requiredDeployments: [
{
projectId: "proj-re-api",
deploymentId: "dep-re-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-re-mobile",
title: "Real Estate Mobile App",
description: "اپلیکیشن موبایل جستجوی ملک",
type: "mobile",
repositoryUrl: "https://git.rayanpardaz.ir/realestate/mobile",
metadata: { framework: "Swift", language: "Swift" },
dependencies: ["proj-re-api"],
deployments: [
{
id: "dep-re-mobile-prod",
environmentId: "env-prod-001",
version: "1.7.0",
deploymentDate: "2026-09-06T08:00:00Z",
releaseNotes: "نسخه iOS با قابلیت AR برای مشاهده ملک",
requiredDeployments: [
{
projectId: "proj-re-api",
deploymentId: "dep-re-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-re-db",
title: "Real Estate Database",
description: "پایگاه داده املاک و کاربران",
type: "database",
repositoryUrl: "https://git.rayanpardaz.ir/realestate/db",
metadata: { framework: "SQL Server 2022", language: "T-SQL" },
dependencies: [],
deployments: [
{
id: "dep-re-db-prod",
environmentId: "env-prod-001",
version: "2.5.0",
deploymentDate: "2026-09-04T03:00:00Z",
releaseNotes: "بهینه‌سازی ایندکس‌های مکانی",
requiredDeployments: [],
status: "active",
},
],
},
],
},
{
id: "prod-travel",
title: "سامانه گردشگری و رزرو سفر سفریار",
description: "پلتفرم رزرو بلیط، هتل و تور",
metadata: {
createdAt: "2026-04-15T08:00:00Z",
owner: "تیم توسعه TravelTech",
customFields: { industry: "Tourism", priority: "high" },
},
projects: [
{
id: "proj-travel-api",
title: "Travel Booking API",
description: "سرویس بک‌اند رزرواسیون",
type: "backend",
repositoryUrl:
"https://git.rayanpardaz.ir/travel/booking-api",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: [
"proj-payment-gateway",
"proj-notification-svc",
],
deployments: [
{
id: "dep-travel-api-prod",
environmentId: "env-prod-001",
version: "3.4.1",
deploymentDate: "2026-09-12T14:00:00Z",
releaseNotes: "یکپارچگی با GDS جهانی و افزودن رزرو هتل",
requiredDeployments: [
{
projectId: "proj-payment-gateway",
deploymentId: "dep-payment-prod",
},
],
status: "active",
},
],
},
{
id: "proj-travel-web",
title: "Travel Booking Portal",
description: "پرتال وب رزرو سفر",
type: "frontend",
repositoryUrl: "https://git.rayanpardaz.ir/travel/web-portal",
metadata: { framework: "React 18", language: "TypeScript" },
dependencies: ["proj-travel-api"],
deployments: [
{
id: "dep-travel-web-prod",
environmentId: "env-prod-001",
version: "4.1.0",
deploymentDate: "2026-09-13T11:30:00Z",
releaseNotes:
"بازطراحی فرآیند رزرو و افزودن پیشنهاد هوشمند",
requiredDeployments: [
{
projectId: "proj-travel-api",
deploymentId: "dep-travel-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-travel-mobile",
title: "Travel Mobile App",
description: "اپلیکیشن موبایل رزرو سفر",
type: "mobile",
repositoryUrl: "https://git.rayanpardaz.ir/travel/mobile",
metadata: { framework: "Flutter 3.22", language: "Dart" },
dependencies: ["proj-travel-api"],
deployments: [
{
id: "dep-travel-mobile-prod",
environmentId: "env-prod-001",
version: "2.6.2",
deploymentDate: "2026-09-07T09:00:00Z",
releaseNotes: "افزودن کارت پرواز دیجیتال و مدیریت رزرو",
requiredDeployments: [
{
projectId: "proj-travel-api",
deploymentId: "dep-travel-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-travel-db",
title: "Travel Database",
description: "پایگاه داده رزروها و کاربران",
type: "database",
repositoryUrl: "https://git.rayanpardaz.ir/travel/db",
metadata: { framework: "PostgreSQL 16", language: "SQL" },
dependencies: [],
deployments: [
{
id: "dep-travel-db-prod",
environmentId: "env-prod-001",
version: "3.1.0",
deploymentDate: "2026-09-03T02:30:00Z",
releaseNotes: "افزودن جداول رزروهای بین‌المللی",
requiredDeployments: [],
status: "active",
},
],
},
],
},
{
id: "prod-hr",
title: "پلتفرم مدیریت منابع انسانی کاریار",
description:
"سامانه جامع HR شامل حقوق و دستمزد، حضور و غیاب و ارزیابی",
metadata: {
createdAt: "2026-05-01T08:00:00Z",
owner: "تیم توسعه HR Tech",
customFields: {
industry: "Human Resources",
priority: "medium",
},
},
projects: [
{
id: "proj-hr-api",
title: "HR Core API",
description: "سرویس بک‌اند سامانه HR",
type: "backend",
repositoryUrl: "https://git.rayanpardaz.ir/hr/core-api",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: ["proj-auth-svc", "proj-notification-svc"],
deployments: [
{
id: "dep-hr-api-prod",
environmentId: "env-prod-001",
version: "2.9.0",
deploymentDate: "2026-09-10T10:00:00Z",
releaseNotes: "ماژول جدید ارزیابی عملکرد ۳۶۰ درجه",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-hr-web",
title: "HR Web Portal",
description: "پرتال وب کارکنان و مدیران",
type: "frontend",
repositoryUrl: "https://git.rayanpardaz.ir/hr/web-portal",
metadata: { framework: "Angular 18", language: "TypeScript" },
dependencies: ["proj-hr-api"],
deployments: [
{
id: "dep-hr-web-prod",
environmentId: "env-prod-001",
version: "3.3.1",
deploymentDate: "2026-09-12T09:00:00Z",
releaseNotes: "داشبورد مدیریتی جدید و گزارش‌های حقوق",
requiredDeployments: [
{
projectId: "proj-hr-api",
deploymentId: "dep-hr-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-hr-mobile",
title: "HR Mobile App",
description: "اپلیکیشن موبایل کارکنان",
type: "mobile",
repositoryUrl: "https://git.rayanpardaz.ir/hr/mobile",
metadata: {
framework: "React Native 0.74",
language: "TypeScript",
},
dependencies: ["proj-hr-api"],
deployments: [
{
id: "dep-hr-mobile-prod",
environmentId: "env-prod-001",
version: "1.8.0",
deploymentDate: "2026-09-06T08:30:00Z",
releaseNotes: "ثبت حضور با GPS و درخواست مرخصی",
requiredDeployments: [
{
projectId: "proj-hr-api",
deploymentId: "dep-hr-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-hr-db",
title: "HR Database",
description: "پایگاه داده کارکنان و سوابق",
type: "database",
repositoryUrl: "https://git.rayanpardaz.ir/hr/db",
metadata: { framework: "SQL Server 2022", language: "T-SQL" },
dependencies: [],
deployments: [
{
id: "dep-hr-db-prod",
environmentId: "env-prod-001",
version: "4.0.0",
deploymentDate: "2026-09-02T02:00:00Z",
releaseNotes: "ساختار جدید برای ارزیابی عملکرد",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-hr-payroll",
title: "Payroll Engine",
description: "موتور محاسبه حقوق و دستمزد",
type: "service",
repositoryUrl: "https://git.rayanpardaz.ir/hr/payroll-engine",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: ["proj-hr-api", "proj-hr-db"],
deployments: [
{
id: "dep-hr-payroll-prod",
environmentId: "env-prod-001",
version: "2.2.1",
deploymentDate: "2026-09-09T11:00:00Z",
releaseNotes: "پشتیبانی از قوانین جدید مالیاتی ۱۴۰۵",
requiredDeployments: [
{
projectId: "proj-hr-api",
deploymentId: "dep-hr-api-prod",
},
],
status: "active",
},
],
},
],
},
{
id: "prod-crm",
title: "سامانه CRM و فروش مشتری‌یار",
description: "پلتفرم مدیریت ارتباط با مشتری و فروش",
metadata: {
createdAt: "2026-05-20T08:00:00Z",
owner: "تیم توسعه CRM",
customFields: { industry: "Sales & CRM", priority: "high" },
},
projects: [
{
id: "proj-crm-api",
title: "CRM Core API",
description: "سرویس بک‌اند CRM",
type: "backend",
repositoryUrl: "https://git.rayanpardaz.ir/crm/core-api",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: ["proj-auth-svc", "proj-notification-svc"],
deployments: [
{
id: "dep-crm-api-prod",
environmentId: "env-prod-001",
version: "3.6.2",
deploymentDate: "2026-09-11T13:00:00Z",
releaseNotes:
"ماژول اتوماسیون بازاریابی و تحلیل رفتار مشتری",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-crm-web",
title: "CRM Web Dashboard",
description: "داشبورد وب تیم فروش",
type: "frontend",
repositoryUrl: "https://git.rayanpardaz.ir/crm/web-dashboard",
metadata: { framework: "React 18", language: "TypeScript" },
dependencies: ["proj-crm-api"],
deployments: [
{
id: "dep-crm-web-prod",
environmentId: "env-prod-001",
version: "4.2.0",
deploymentDate: "2026-09-13T10:30:00Z",
releaseNotes: "نمای ۳۶۰ درجه مشتری و گزارش‌های پیشرفته",
requiredDeployments: [
{
projectId: "proj-crm-api",
deploymentId: "dep-crm-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-crm-mobile",
title: "CRM Mobile App",
description: "اپلیکیشن موبایل ویزیتورها",
type: "mobile",
repositoryUrl: "https://git.rayanpardaz.ir/crm/mobile",
metadata: { framework: "Flutter 3.22", language: "Dart" },
dependencies: ["proj-crm-api"],
deployments: [
{
id: "dep-crm-mobile-prod",
environmentId: "env-prod-001",
version: "2.1.3",
deploymentDate: "2026-09-07T09:00:00Z",
releaseNotes: "ثبت بازدید حضوری و آپلود فاکتور",
requiredDeployments: [
{
projectId: "proj-crm-api",
deploymentId: "dep-crm-api-prod",
},
],
status: "active",
},
],
},
{
id: "proj-crm-db",
title: "CRM Database",
description: "پایگاه داده مشتریان و سرنخ‌ها",
type: "database",
repositoryUrl: "https://git.rayanpardaz.ir/crm/db",
metadata: {
framework: "MongoDB 7",
language: "MongoDB Query",
},
dependencies: [],
deployments: [
{
id: "dep-crm-db-prod",
environmentId: "env-prod-001",
version: "3.0.1",
deploymentDate: "2026-09-04T03:00:00Z",
releaseNotes:
"ساختار بهینه برای داده‌های نیمه‌ساختاریافته",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-crm-analytics",
title: "CRM Analytics Engine",
description: "موتور تحلیل و BI فروش",
type: "service",
repositoryUrl: "https://git.rayanpardaz.ir/crm/analytics",
metadata: { framework: "Python 3.12", language: "Python" },
dependencies: ["proj-crm-db"],
deployments: [
{
id: "dep-crm-analytics-prod",
environmentId: "env-prod-001",
version: "1.5.0",
deploymentDate: "2026-09-10T14:00:00Z",
releaseNotes:
"پیش‌بینی فروش با مدل ML و تحلیل چرخه عمر مشتری",
requiredDeployments: [
{
projectId: "proj-crm-db",
deploymentId: "dep-crm-db-prod",
},
],
status: "active",
},
],
},
],
},
{
id: "prod-payment",
title: "پلتفرم پرداخت و کیف پول پرداخت‌یار",
description: "زیرساخت مشترک پرداخت برای تمام محصولات سازمان",
metadata: {
createdAt: "2026-01-05T08:00:00Z",
owner: "تیم توسعه Payment",
customFields: {
industry: "FinTech",
priority: "critical",
shared: true,
},
},
projects: [
{
id: "proj-payment-gateway",
title: "Payment Gateway Service",
description: "درگاه پرداخت یکپارچه - سرویس مشترک",
type: "service",
repositoryUrl: "https://git.rayanpardaz.ir/payment/gateway",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: ["proj-payment-db"],
deployments: [
{
id: "dep-payment-prod",
environmentId: "env-prod-001",
version: "5.3.0",
deploymentDate: "2026-09-09T12:00:00Z",
releaseNotes:
"پشتیبانی از درگاه‌های جدید و توکن‌سازی کارت",
requiredDeployments: [
{
projectId: "proj-payment-db",
deploymentId: "dep-payment-db-prod",
},
],
status: "active",
},
{
id: "dep-payment-stage",
environmentId: "env-stage-002",
version: "5.4.0-rc.1",
deploymentDate: "2026-09-14T10:00:00Z",
releaseNotes: "تست یکپارچگی با شاپرک جدید",
requiredDeployments: [],
status: "active",
},
{
id: "dep-payment-qa",
environmentId: "env-qa-004",
version: "5.4.0-beta.2",
deploymentDate: "2026-09-15T15:00:00Z",
releaseNotes: "تست‌های خودکار امنیتی و بار",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-payment-wallet",
title: "Digital Wallet Service",
description: "سرویس کیف پول الکترونیک",
type: "service",
repositoryUrl: "https://git.rayanpardaz.ir/payment/wallet",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: ["proj-payment-gateway", "proj-payment-db"],
deployments: [
{
id: "dep-payment-wallet-prod",
environmentId: "env-prod-001",
version: "3.1.2",
deploymentDate: "2026-09-11T11:00:00Z",
releaseNotes: "افزودن قابلیت انتقال بین کیف پول و کش‌بک",
requiredDeployments: [
{
projectId: "proj-payment-gateway",
deploymentId: "dep-payment-prod",
},
],
status: "active",
},
],
},
{
id: "proj-payment-db",
title: "Payment Database",
description: "پایگاه داده تراکنش‌های مالی",
type: "database",
repositoryUrl: "https://git.rayanpardaz.ir/payment/db",
metadata: { framework: "Oracle 19c RAC", language: "PL/SQL" },
dependencies: [],
deployments: [
{
id: "dep-payment-db-prod",
environmentId: "env-prod-001",
version: "6.0.0",
deploymentDate: "2026-09-01T02:00:00Z",
releaseNotes:
"ارتقای ساختار تراکنش‌ها با قابلیت Audit کامل",
requiredDeployments: [],
status: "active",
},
{
id: "dep-payment-db-dr",
environmentId: "env-dr-006",
version: "6.0.0",
deploymentDate: "2026-09-01T03:00:00Z",
releaseNotes: "همگام‌سازی Active-Active با Production",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-payment-web",
title: "Payment Management Portal",
description: "پرتال مدیریت تراکنش‌ها برای اپراتورها",
type: "frontend",
repositoryUrl: "https://git.rayanpardaz.ir/payment/portal",
metadata: { framework: "Angular 18", language: "TypeScript" },
dependencies: ["proj-payment-gateway"],
deployments: [
{
id: "dep-payment-web-prod",
environmentId: "env-prod-001",
version: "2.8.1",
deploymentDate: "2026-09-12T10:00:00Z",
releaseNotes: "داشبورد تحلیل تراکنش‌ها و مدیریت disputes",
requiredDeployments: [
{
projectId: "proj-payment-gateway",
deploymentId: "dep-payment-prod",
},
],
status: "active",
},
],
},
{
id: "proj-auth-svc",
title: "Authentication & Authorization Service",
description: "سرویس مشترک احراز هویت و مجوزدهی",
type: "service",
repositoryUrl: "https://git.rayanpardaz.ir/shared/auth-svc",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: ["proj-payment-db"],
deployments: [
{
id: "dep-auth-prod",
environmentId: "env-prod-001",
version: "4.1.0",
deploymentDate: "2026-09-08T06:00:00Z",
releaseNotes: "افزودن MFA و پشتیبانی از OAuth 2.1",
requiredDeployments: [],
status: "active",
},
{
id: "dep-auth-stage",
environmentId: "env-stage-002",
version: "4.2.0-rc.1",
deploymentDate: "2026-09-14T08:00:00Z",
releaseNotes: "تست SSO با Azure AD",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-notification-svc",
title: "Notification Service",
description: "سرویس مشترک ارسال اعلان (SMS/Email/Push)",
type: "service",
repositoryUrl:
"https://git.rayanpardaz.ir/shared/notification-svc",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: [],
deployments: [
{
id: "dep-notif-prod",
environmentId: "env-prod-001",
version: "2.5.0",
deploymentDate: "2026-09-07T07:00:00Z",
releaseNotes: "افزودن کانال WhatsApp و بهینه‌سازی صف",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-inventory-svc",
title: "Inventory Management Service",
description: "سرویس مدیریت موجودی انبار",
type: "service",
repositoryUrl:
"https://git.rayanpardaz.ir/shared/inventory-svc",
metadata: { framework: ".NET 8", language: "C#" },
dependencies: ["proj-payment-db"],
deployments: [
{
id: "dep-inventory-prod",
environmentId: "env-prod-001",
version: "2.0.3",
deploymentDate: "2026-09-06T10:00:00Z",
releaseNotes: "همگام‌سازی Real-time با انبارهای فیزیکی",
requiredDeployments: [],
status: "active",
},
],
},
{
id: "proj-video-streaming",
title: "Video Streaming Infrastructure",
description: "زیرساخت استریم ویدیو (CDN + Transcoding)",
type: "infrastructure",
repositoryUrl:
"https://git.rayanpardaz.ir/shared/video-infra",
metadata: { framework: "Terraform + AWS", language: "HCL" },
dependencies: [],
deployments: [
{
id: "dep-video-infra-prod",
environmentId: "env-prod-001",
version: "1.3.0",
deploymentDate: "2026-09-05T05:00:00Z",
releaseNotes: "افزودن CDN منطقه‌ای و Adaptive Bitrate",
requiredDeployments: [],
status: "active",
},
],
},
],
},
],
},
};
const VALID_PROJECT_TYPES = [
{ value: "backend", label: "بک‌اند (Backend)", icon: "🖥️" },
{ value: "frontend", label: "فرانت‌اند (Frontend)", icon: "🌐" },
{ value: "service", label: "میکروسرویس (Service)", icon: "⚙️" },
{ value: "database", label: "پایگاه داده (Database)", icon: "🗄️" },
{ value: "mobile", label: "موبایل (Mobile)", icon: "📱" },
{
value: "infrastructure",
label: "زیرساخت (Infrastructure)",
icon: "🏗️",
},
];
/* ==================== HELPERS ==================== */
function validateAppDataStructure(d) {
return (
d &&
typeof d === "object" &&
Array.isArray(d.environments) &&
Array.isArray(d.products)
);
}
function populateProjectTypeDropdowns() {
["newProjectType", "editProjType"].forEach((id) => {
const s = document.getElementById(id);
if (!s) return;
const cv = s.value;
s.innerHTML =
'<option value="">انتخاب کنید...</option>' +
VALID_PROJECT_TYPES.map(
(t) => `<option value="${t.value}">${t.icon} ${t.label}</option>`,
).join("");
if (cv) s.value = cv;
});
}
function showToast(msg, type = "info", dur = 3000) {
const c = document.getElementById("toastContainer");
const t = document.createElement("div");
t.className = `toast ${type}`;
const ic = { success: "✅", error: "❌", warning: "⚠️", info: "ℹ️" };
t.innerHTML = `<span>${ic[type] || "ℹ️"}</span><span>${msg}</span>`;
c.appendChild(t);
setTimeout(() => {
t.style.opacity = "0";
setTimeout(() => t.remove(), 300);
}, dur);
}
function showD3Tooltip(e, text) {
let tt = document.getElementById("d3Tooltip");
if (!tt) return;
tt.textContent = text;
tt.classList.add("visible");
tt.style.left = e.clientX + 12 + "px";
tt.style.top = e.clientY + 12 + "px";
}
function hideD3Tooltip() {
const tt = document.getElementById("d3Tooltip");
if (tt) tt.classList.remove("visible");
}
function refreshUI() {
ToolbarManager.updateStats();
BoardRenderer.render();
}
/* ==================== MODAL MANAGER ==================== */
const ModalManager = {
stack: [],
open(id) {
const m = document.getElementById(id);
if (!m) return;
this.stack = this.stack.filter((x) => x !== id);
this.stack.push(id);
this.stack.forEach((mid, i) => {
const el = document.getElementById(mid);
if (!el) return;
if (mid === id) el.classList.add("active");
el.style.zIndex = 1000 + i + 1;
});
},
close(id) {
const m = document.getElementById(id);
if (!m) return;
m.classList.remove("active");
m.style.zIndex = "";
this.stack = this.stack.filter((x) => x !== id);
this.stack.forEach((mid, i) => {
const el = document.getElementById(mid);
if (el) el.style.zIndex = 1000 + i + 1;
});
},
closeTop() {
if (this.stack.length > 0)
this.close(this.stack[this.stack.length - 1]);
},
init() {
document.querySelectorAll("[data-close-modal]").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const m = e.target.closest(".modal-overlay");
if (m && m.id) this.close(m.id);
});
});
document.querySelectorAll(".modal-overlay").forEach((ov) => {
ov.addEventListener("click", (e) => {
if (e.target === ov && ov.id) this.close(ov.id);
});
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
e.preventDefault();
this.closeTop();
}
});
},
};
/* ==================== TOOLBAR MANAGER ==================== */
const ToolbarManager = {
currentFilters: { query: "", typeFilter: "", envFilter: "" },
init() {
this.populateEnvironmentFilter();
this.bindEvents();
this.updateStats();
},
populateEnvironmentFilter() {
const s = document.getElementById("filterEnvironment");
if (!s) return;
s.innerHTML =
'<option value="">همه محیط‌ها</option>' +
projectData.appData.environments
.map((e) => `<option value="${e.id}">${e.name}</option>`)
.join("");
},
bindEvents() {
const si = document.getElementById("toolbarSearch"),
ft = document.getElementById("filterType"),
fe = document.getElementById("filterEnvironment");
if (si)
si.addEventListener("input", () => {
this.applyFilters();
refreshUI();
});
if (ft)
ft.addEventListener("change", () => {
this.applyFilters();
refreshUI();
});
if (fe)
fe.addEventListener("change", () => {
this.applyFilters();
refreshUI();
});
},
applyFilters() {
this.currentFilters = {
query: (document.getElementById("toolbarSearch")?.value || "")
.trim()
.toLowerCase(),
typeFilter: document.getElementById("filterType")?.value || "",
envFilter:
document.getElementById("filterEnvironment")?.value || "",
};
this.updateStats();
},
updateStats() {
const ps = projectData.appData.products || [];
let tp = 0,
td = 0;
ps.forEach((p) => {
(p.projects || []).forEach((pr) => {
tp++;
td += (pr.dependencies || []).length;
});
});
const s = (id, v) => {
const el = document.getElementById(id);
if (el) el.textContent = v;
};
s("statTotal", ps.length + tp);
s("statProjects", ps.length);
s("statSubProjects", tp);
s("statDependencies", td);
},
shouldShowProduct(product) {
const f = this.currentFilters;
if (!f.query && !f.typeFilter && !f.envFilter) return true;
return (product.projects || []).some((pr) => {
const mq =
!f.query ||
pr.title.toLowerCase().includes(f.query) ||
product.title.toLowerCase().includes(f.query);
const mt = !f.typeFilter || pr.type === f.typeFilter;
const me =
!f.envFilter ||
(pr.deployments || [])
.filter((d) => d.status === "active")
.some((d) => d.environmentId === f.envFilter);
return mq && mt && me;
});
},
};
/* ==================== HISTORY MANAGER ==================== */
const HistoryManager = {
undoStack: [],
redoStack: [],
maxSize: 50,
isUndoRedoInProgress: false,
pushState() {
if (this.isUndoRedoInProgress) return;
this.undoStack.push(JSON.parse(JSON.stringify(projectData.appData)));
if (this.undoStack.length > this.maxSize) this.undoStack.shift();
this.redoStack = [];
this.updateButtons();
},
undo() {
if (this.undoStack.length === 0) {
showToast("هیچ تغییری نیست", "warning");
return;
}
this.redoStack.push(JSON.parse(JSON.stringify(projectData.appData)));
this.isUndoRedoInProgress = true;
projectData.appData = this.undoStack.pop();
this.isUndoRedoInProgress = false;
this.updateUI();
this.updateButtons();
showToast("↩️ بازگشت", "success");
},
redo() {
if (this.redoStack.length === 0) {
showToast("هیچ تغییری نیست", "warning");
return;
}
this.undoStack.push(JSON.parse(JSON.stringify(projectData.appData)));
this.isUndoRedoInProgress = true;
projectData.appData = this.redoStack.pop();
this.isUndoRedoInProgress = false;
this.updateUI();
this.updateButtons();
showToast("↪️ باز انجام", "success");
},
updateUI() {
refreshUI();
if (ProductManager.currentDetailProductId)
ProductManager.showDetail(ProductManager.currentDetailProductId);
},
updateButtons() {
const u = document.getElementById("undoBtn"),
r = document.getElementById("redoBtn");
if (u) u.disabled = this.undoStack.length === 0;
if (r) r.disabled = this.redoStack.length === 0;
},
init() {
this.pushState();
document
.getElementById("undoBtn")
.addEventListener("click", () => this.undo());
document
.getElementById("redoBtn")
.addEventListener("click", () => this.redo());
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
e.preventDefault();
this.undo();
}
if (
(e.ctrlKey || e.metaKey) &&
(e.key === "y" || (e.key === "z" && e.shiftKey))
) {
e.preventDefault();
this.redo();
}
});
},
};
/* ==================== SECTION COLLAPSE ==================== */
const SectionCollapseManager = {
storageKey: "boardSectionCollapseState",
getState() {
try {
const s = localStorage.getItem(this.storageKey);
return s
? JSON.parse(s)
: { dashboard: false, products: false, environments: false };
} catch {
return { dashboard: false, products: false, environments: false };
}
},
saveState(s) {
try {
localStorage.setItem(this.storageKey, JSON.stringify(s));
} catch (e) {}
},
toggle(id) {
const s = this.getState();
s[id] = !s[id];
this.saveState(s);
const sec = document.getElementById(`section-${id}`);
if (sec) sec.classList.toggle("collapsed", s[id]);
return s[id];
},
init() {
const s = this.getState();
Object.keys(s).forEach((id) => {
const sec = document.getElementById(`section-${id}`);
if (sec && s[id]) sec.classList.add("collapsed");
});
document
.getElementById("boardArea")
.addEventListener("click", (e) => {
const ht = e.target.closest(".board-header-title");
if (ht) {
const sec = ht.closest(".board-section");
if (sec) {
this.toggle(sec.id.replace("section-", ""));
}
}
});
},
};
/* ==================== DASHBOARD SECTION ==================== */
const DashboardSectionRenderer = {
getSectionHTML() {
const d = this.getData();
let h = `<div class="board-section" id="section-dashboard">`;
h += `<div class="board-header"><div class="board-header-title"><span class="collapse-icon">▼</span>📊 داشبورد مدیریتی</div><div class="board-header-actions"><button class="btn btn-accent btn-sm" onclick="DashboardManager.open()">📊 مشاهده کامل</button></div></div>`;
h += `<div class="section-content">`;
// KPI Cards
h += '<div class="dashboard-kpi-row" style="margin-bottom:16px">';
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📦</div><div class="dashboard-kpi-value">${d.products}</div><div class="dashboard-kpi-label">محصول</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🌿</div><div class="dashboard-kpi-value">${d.projects}</div><div class="dashboard-kpi-label">پروژه</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔗</div><div class="dashboard-kpi-value">${d.deps}</div><div class="dashboard-kpi-label">وابستگی</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🚀</div><div class="dashboard-kpi-value">${d.deploys}</div><div class="dashboard-kpi-label">استقرار</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🌍</div><div class="dashboard-kpi-value">${d.environments}</div><div class="dashboard-kpi-label">محیط</div></div>`;
h += "</div>";
// ✅ NEW: Charts Grid
h += '<div class="dashboard-charts-grid">';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🥧 توزیع نوع پروژه‌ها</div><div class="dashboard-chart-body" id="mainDashTypeChart"></div></div>';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🌍 استقرارها به تفکیک محیط</div><div class="dashboard-chart-body" id="mainDashEnvChart"></div></div>';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">✅ وضعیت استقرار پروژه‌ها</div><div class="dashboard-chart-body" id="mainDashStatusChart"></div></div>';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 پروژه‌ها به تفکیک محصول</div><div class="dashboard-chart-body" id="mainDashProductChart"></div></div>';
h += "</div>";
h += "</div></div>";
return h;
},
getData() {
const ps = projectData.appData.products || [];
let tp = 0,
td = 0,
tdp = 0;
const tc = {},
edc = {},
ppc = [];
let pwd = 0,
pwod = 0;
ps.forEach((prod) => {
const projects = prod.projects || [];
ppc.push({ title: prod.title, count: projects.length });
projects.forEach((pr) => {
tp++;
td += (pr.dependencies || []).length;
const active = (pr.deployments || []).filter(
(d) => d.status === "active",
);
tdp += active.length;
if (active.length > 0) pwd++;
else pwod++;
tc[pr.type] = (tc[pr.type] || 0) + 1;
active.forEach((d) => {
const env = projectData.appData.environments.find(
(e) => e.id === d.environmentId,
);
const envName = env ? env.name : "نامشخص";
edc[envName] = (edc[envName] || 0) + 1;
});
});
});
return {
products: ps.length,
projects: tp,
deps: td,
deploys: tdp,
environments: projectData.appData.environments.length,
tc,
edc,
ppc,
pwd,
pwod,
};
},
// ✅ NEW: Render charts after DOM is ready
renderCharts() {
const d = this.getData();
this.renderDonut(
"mainDashTypeChart",
Object.entries(d.tc).map(([k, v]) => ({
label: this.getTypeLabel(k),
value: v,
color: this.getTypeColor(k),
})),
);
this.renderBar(
"mainDashEnvChart",
Object.entries(d.edc).map(([k, v]) => ({ label: k, value: v })),
"#38a169",
);
this.renderDonut("mainDashStatusChart", [
{ label: "با استقرار", value: d.pwd, color: "#38a169" },
{ label: "بدون استقرار", value: d.pwod, color: "#e53e3e" },
]);
this.renderBar(
"mainDashProductChart",
d.ppc.map((p) => ({
label:
p.title.length > 15
? p.title.substring(0, 15) + "..."
: p.title,
value: p.count,
})),
"#3182ce",
);
},
renderDonut(cid, data) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
if (!data || data.length === 0 || data.every((d) => d.value === 0)) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
return;
}
const w = 240,
hgt = 200,
r = Math.min(w, hgt) / 2 - 15;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${w / 2},${hgt / 2})`);
const pie = d3
.pie()
.value((d) => d.value)
.sort(null);
const arc = d3
.arc()
.innerRadius(r * 0.55)
.outerRadius(r);
const arcH = d3
.arc()
.innerRadius(r * 0.55)
.outerRadius(r + 6);
const total = data.reduce((s, d) => s + d.value, 0);
g.selectAll("path")
.data(pie(data))
.join("path")
.attr("d", arc)
.attr("fill", (d) => d.data.color)
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.style("cursor", "pointer")
.on("mouseover", function (e, d) {
d3.select(this).transition().duration(150).attr("d", arcH(d));
showD3Tooltip(
e,
`${d.data.label}: ${d.data.value} (${Math.round((d.data.value / total) * 100)}%)`,
);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function () {
d3.select(this).transition().duration(150).attr("d", arc);
hideD3Tooltip();
});
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "-.2em")
.attr("fill", "var(--text)")
.attr("font-size", "20px")
.attr("font-weight", "800")
.text(total);
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "1.3em")
.attr("fill", "var(--text-muted)")
.attr("font-size", "10px")
.text("پروژه");
},
renderBar(cid, data, color) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
if (!data || data.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
return;
}
const w = 280,
hgt = 200,
m = { top: 20, right: 15, bottom: 40, left: 30 };
const iw = w - m.left - m.right,
ih = hgt - m.top - m.bottom;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${m.left},${m.top})`);
const x = d3
.scaleBand()
.domain(data.map((d) => d.label))
.range([0, iw])
.padding(0.3);
const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value) || 1])
.nice()
.range([ih, 0]);
g.append("g")
.attr("transform", `translate(0,${ih})`)
.call(d3.axisBottom(x).tickSize(0))
.selectAll("text")
.attr("fill", "var(--text-muted)")
.attr("font-size", "8px")
.attr("font-family", "Vazirmatn")
.attr("transform", "rotate(-20)")
.attr("text-anchor", "end");
g.append("g")
.call(d3.axisLeft(y).ticks(4).tickSize(-iw))
.selectAll("text")
.attr("fill", "var(--text-muted)")
.attr("font-size", "9px");
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d) => x(d.label))
.attr("y", (d) => y(d.value))
.attr("width", x.bandwidth())
.attr("height", (d) => ih - y(d.value))
.attr("fill", color)
.attr("rx", 4)
.style("cursor", "pointer")
.on("mouseover", function (e, d) {
d3.select(this).attr("fill", "var(--accent)");
showD3Tooltip(e, `${d.label}: ${d.value}`);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function () {
d3.select(this).attr("fill", color);
hideD3Tooltip();
});
g.selectAll(".bar-label")
.data(data)
.join("text")
.attr("x", (d) => x(d.label) + x.bandwidth() / 2)
.attr("y", (d) => y(d.value) - 5)
.attr("text-anchor", "middle")
.attr("fill", "var(--text)")
.attr("font-size", "10px")
.attr("font-weight", "700")
.text((d) => d.value);
},
getTypeLabel(t) {
const i = VALID_PROJECT_TYPES.find((x) => x.value === t);
return i ? i.label.split(" ")[0] : t;
},
getTypeColor(t) {
const c = {
backend: "#4299e1",
frontend: "#48bb78",
service: "#ecc94b",
database: "#ed64a6",
mobile: "#9f7aea",
infrastructure: "#ed8936",
};
return c[t] || "#718096";
},
};
/* ==================== ENVIRONMENT RENDERER ==================== */
const EnvironmentRenderer = {
getSectionHTML() {
const envs = projectData.appData.environments || [];
let h = `<div class="board-section" id="section-environments"><div class="board-header"><div class="board-header-title"><span class="collapse-icon">▼</span>🌍 محیط‌های عملیاتی (${envs.length})</div><div class="board-header-actions"><button class="btn btn-accent" onclick="EnvironmentManager.openAddModal()">➕ محیط جدید</button></div></div><div class="section-content">`;
if (envs.length === 0) {
h +=
'<div class="board-empty"><div class="board-empty-icon">🌍</div><h2>هیچ محیطی نیست</h2></div>';
} else {
h += '<div class="env-card-grid">';
envs.forEach((env) => {
h += `<div class="env-card-item"><div class="env-card-preview" style="background-color:${env.backgroundColor || "#38a169"};color:${env.textColor || "#fff"}">${env.name}</div><div class="env-card-body"><div class="env-card-name">${env.name}</div><div class="env-card-desc">${env.description || ""}</div></div><div class="env-card-footer"><button class="btn btn-ghost btn-sm" onclick="EnvironmentManager.openEditModal('${env.id}')">✏️ ویرایش</button><button class="btn btn-danger btn-sm" onclick="EnvironmentManager.remove('${env.id}')">🗑️ حذف</button></div></div>`;
});
h += "</div>";
}
h += "</div></div>";
return h;
},
};
/* ==================== BOARD RENDERER ==================== */
const BoardRenderer = {
render() {
//
const board = document.getElementById("boardArea");
const products = projectData.appData.products || [];
const fp = products.filter((p) =>
ToolbarManager.shouldShowProduct(p),
);
let h = "";
h += DashboardSectionRenderer.getSectionHTML();
h += `<div class="board-section" id="section-products"><div class="board-header"><div class="board-header-title"><span class="collapse-icon">▼</span>📦 محصولات (${fp.length} از ${products.length})</div><div class="board-header-actions"><button class="btn btn-accent" onclick="ProductManager.openAddProductModal()">➕ محصول جدید</button></div></div><div class="section-content">`;
if (products.length === 0) {
h +=
'<div class="board-empty"><div class="board-empty-icon">📦</div><h2>هیچ محصولی نیست</h2></div>';
} else if (fp.length === 0) {
h += '<div class="no-results">🔍 نتیجه‌ای یافت نشد</div>';
} else {
h += '<div class="projects-grid">';
fp.forEach((prod) => {
const projects = prod.projects || [];
const tc = {};
projects.forEach((pr) => {
tc[pr.type] = (tc[pr.type] || 0) + 1;
});
const td = projects.reduce(
(s, pr) => s + (pr.dependencies || []).length,
0,
);
const tdp = projects.reduce(
(s, pr) =>
s +
(pr.deployments || []).filter((d) => d.status === "active")
.length,
0,
);
h += `<div class="project-card"><div class="project-card-header"><div class="project-card-title-row"><span style="font-size:22px">📦</span><div class="project-card-title">${prod.title}</div></div><div class="project-card-desc">${prod.description || ""}</div></div><div class="project-card-body"><div class="project-stat-row"><span>🌿 پروژه‌ها:</span><span class="project-stat-value">${projects.length}</span></div><div class="project-stat-row"><span>🔗 وابستگی‌ها:</span><span class="project-stat-value">${td}</span></div><div class="project-stat-row"><span>🚀 استقرارها:</span><span class="project-stat-value">${tdp}</span></div>${
Object.keys(tc).length > 0
? `<div class="project-type-badges">${Object.entries(tc)
.map(([type, count]) => {
const ti = VALID_PROJECT_TYPES.find(
(t) => t.value === type,
) || { label: type };
return `<span class="type-badge type-${type}">${ti.label}: ${count}</span>`;
})
.join("")}</div>`
: ""
}</div><div class="project-card-footer"><div class="card-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.showDetail('${prod.id}')">👁️ جزئیات</button></div><button class="btn btn-danger btn-xs" onclick="ProductManager.removeProduct('${prod.id}')">🗑️</button></div></div>`;
});
h += "</div>";
}
h += "</div></div>";
h += EnvironmentRenderer.getSectionHTML();
board.innerHTML = h;
const st = SectionCollapseManager.getState();
Object.keys(st).forEach((id) => {
const sec = document.getElementById(`section-${id}`);
if (sec && st[id]) sec.classList.add("collapsed");
});
//
setTimeout(() => {
DashboardSectionRenderer.renderCharts();
}, 100);
},
};
/* ==================== ACTION REGISTRY ==================== */
const ActionRegistry = {
openProductSettings() {
openSettingsModal();
},
saveProduct() {
projectData.metadata.updatedAt = new Date().toISOString();
const js = JSON.stringify(projectData.appData, null, 2);
const b = new Blob([js], { type: "application/json;charset=utf-8" });
const u = URL.createObjectURL(b);
const a = document.createElement("a");
a.href = u;
a.download = `${projectData.projectName}_v${projectData.version}_${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(u);
showToast("💾 ذخیره شد", "success");
},
loadProduct() {
const inp = document.createElement("input");
inp.type = "file";
inp.accept = ".json";
inp.addEventListener("change", (e) => {
const f = e.target.files[0];
if (!f) return;
const r = new FileReader();
r.onload = (ev) => {
try {
const ld = JSON.parse(ev.target.result);
if (!validateAppDataStructure(ld)) {
showToast("❌ ساختار نامعتبر", "error");
return;
}
HistoryManager.pushState();
projectData.appData = ld;
updateProjectUI();
renderToolGroups();
ToolbarManager.populateEnvironmentFilter();
refreshUI();
showToast("📂 بارگذاری شد", "success");
} catch (err) {
showToast("❌ خطا: " + err.message, "error");
}
};
r.readAsText(f);
});
inp.click();
},
clearAll() {
if (!confirm("⚠️ پاکسازی کامل؟ این عمل غیرقابل بازگشت است!")) return;
if (!confirm("⚠️ تأیید نهایی؟")) return;
HistoryManager.pushState();
projectData.appData = { environments: [], products: [] };
ToolbarManager.populateEnvironmentFilter();
refreshUI();
showToast("🗑️ پاکسازی شد", "success");
},
showGalaxyChart() {
GalaxyChartManager.open();
},
showExecutiveDashboard() {
DashboardManager.open();
},
showDependencyAnalysis() {
DependencyAnalysisManager.open();
},
showDeploymentAnalysis() {
DeploymentAnalysisManager.open();
},
showVersionAnalysis() {
VersionAnalysisManager.open();
},
showRiskAnalysis() {
RiskAnalysisManager.open();
},
defaultAction(tool) {
showToast(`ابزار: ${tool.title}`, "info");
},
};
/* ==================== WEBHOOK MANAGER ==================== */
const WebhookManager = {
getConfig() {
return projectData.webhook;
},
setConfig(c) {
projectData.webhook = { ...projectData.webhook, ...c };
this.updateStatusUI();
},
isValid() {
const c = this.getConfig();
return (
c.enabled && c.url && c.clientId && /^https?:\/\//i.test(c.url)
);
},
updateStatusUI() {
const c = this.getConfig();
const d = document.getElementById("webhookStatusDot"),
t = document.getElementById("webhookStatusText");
if (!d || !t) return;
d.className = "webhook-status-dot";
if (!c.enabled) t.textContent = "وضعیت: غیرفعال";
else if (!c.url || !c.clientId) {
d.classList.add("pending");
t.textContent = "وضعیت: ناقص";
} else {
d.classList.add("success");
t.textContent = "وضعیت: آماده";
}
},
async sendEvent(en, payload = {}) {
if (!this.isValid()) return;
const c = this.getConfig();
try {
await fetch(c.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Client-Id": c.clientId,
"X-Event-Name": en,
},
body: JSON.stringify({
event: en,
timestamp: new Date().toISOString(),
projectId: projectData.projectId,
data: payload,
}),
});
} catch (e) {}
},
};
/* ==================== ENVIRONMENT MANAGER ==================== */
const EnvironmentManager = {
currentEditEnvId: null,
openAddModal() {
document.getElementById("newEnvName").value = "";
document.getElementById("newEnvDesc").value = "";
document.getElementById("newEnvBgColor").value = "#38a169";
document.getElementById("newEnvTextColor").value = "#ffffff";
ModalManager.open("envManagerModal");
},
saveNewEnv() {
const n = document.getElementById("newEnvName").value.trim();
if (!n) {
showToast("نام الزامی است", "error");
return;
}
if (
projectData.appData.environments.some(
(e) => e.name.toLowerCase() === n.toLowerCase(),
)
) {
showToast("تکراری است", "warning");
return;
}
HistoryManager.pushState();
projectData.appData.environments.push({
id: `env-${Date.now()}`,
name: n,
description:
document.getElementById("newEnvDesc").value.trim() || "",
backgroundColor: document.getElementById("newEnvBgColor").value,
textColor: document.getElementById("newEnvTextColor").value,
});
ModalManager.close("envManagerModal");
ToolbarManager.populateEnvironmentFilter();
refreshUI();
showToast(`محیط "${n}" اضافه شد`, "success");
},
openEditModal(envId) {
const env = projectData.appData.environments.find(
(e) => e.id === envId,
);
if (!env) return;
this.currentEditEnvId = envId;
document.getElementById("editEnvId").value = env.id;
document.getElementById("editEnvName").value = env.name;
document.getElementById("editEnvDesc").value = env.description || "";
document.getElementById("editEnvBgColor").value =
env.backgroundColor || "#38a169";
document.getElementById("editEnvTextColor").value =
env.textColor || "#ffffff";
this.updateEditPreview();
ModalManager.open("envEditModal");
},
updateEditPreview() {
const p = document.getElementById("editEnvPreview");
p.style.backgroundColor =
document.getElementById("editEnvBgColor").value;
p.style.color = document.getElementById("editEnvTextColor").value;
},
saveEdit() {
const env = projectData.appData.environments.find(
(e) => e.id === this.currentEditEnvId,
);
if (!env) return;
const nn = document.getElementById("editEnvName").value.trim();
if (!nn) {
showToast("نام الزامی است", "error");
return;
}
HistoryManager.pushState();
env.name = nn;
env.description = document.getElementById("editEnvDesc").value.trim();
env.backgroundColor = document.getElementById("editEnvBgColor").value;
env.textColor = document.getElementById("editEnvTextColor").value;
ModalManager.close("envEditModal");
ToolbarManager.populateEnvironmentFilter();
refreshUI();
showToast(`محیط "${env.name}" ویرایش شد`, "success");
},
remove(id) {
const env = projectData.appData.environments.find((e) => e.id === id);
if (!env) return;
if (!confirm(`حذف "${env.name}"؟`)) return;
HistoryManager.pushState();
projectData.appData.environments =
projectData.appData.environments.filter((e) => e.id !== id);
ToolbarManager.populateEnvironmentFilter();
refreshUI();
showToast(`"${env.name}" حذف شد`, "warning");
},
};
/* ==================== PRODUCT MANAGER ==================== */
const ProductManager = {
currentDetailProductId: null,
currentEditProjectId: null,
currentDependencyProjectId: null,
currentDependencyProjId: null,
currentDeploymentProjId: null,
currentDeploymentMode: null,
currentDeploymentEnvId: null,
currentEditDeploymentId: null,
getTypeInfo(type) {
return (
VALID_PROJECT_TYPES.find((t) => t.value === type) || {
value: type,
label: type,
icon: "❓",
}
);
},
getEnvironmentName(envId) {
const e = projectData.appData.environments.find(
(x) => x.id === envId,
);
return e ? e.name : envId;
},
getEnvironmentColors(envId) {
const e = projectData.appData.environments.find(
(x) => x.id === envId,
);
return {
backgroundColor: e?.backgroundColor || "#38a169",
textColor: e?.textColor || "#ffffff",
};
},
findProjectById(pid) {
for (const prod of projectData.appData.products) {
const pr = prod.projects.find((p) => p.id === pid);
if (pr) return pr;
}
return null;
},
getActiveDeployments(proj) {
return (proj.deployments || []).filter((d) => d.status === "active");
},
getEnvironmentsWithoutDeployment(proj) {
const allEnvIds = projectData.appData.environments.map((e) => e.id);
const deployedEnvIds = this.getActiveDeployments(proj).map(
(d) => d.environmentId,
);
return allEnvIds.filter((id) => !deployedEnvIds.includes(id));
},
showDetail(productId) {
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
if (!prod) return;
this.currentDetailProductId = productId;
document.getElementById("projectDetailTitle").innerHTML =
`<span>📦</span><span>${prod.title}</span>`;
const projects = prod.projects || [];
const totalDeploys = projects.reduce(
(s, pr) => s + this.getActiveDeployments(pr).length,
0,
);
let html = `<div class="detail-section"><div class="detail-section-header"><span>ℹ️ اطلاعات محصول</span><div class="detail-section-header-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.openProductEditModal()">✏️ ویرایش</button></div></div><div class="detail-section-body"><div class="detail-info-grid"><div class="detail-info-item"><span class="detail-info-label">عنوان</span><span class="detail-info-value">${prod.title}</span></div><div class="detail-info-item"><span class="detail-info-label">توضیحات</span><span class="detail-info-value">${prod.description || "—"}</span></div><div class="detail-info-item"><span class="detail-info-label">تاریخ ایجاد</span><span class="detail-info-value">${prod.metadata?.createdAt ? new Date(prod.metadata.createdAt).toLocaleDateString("fa-IR") : "—"}</span></div><div class="detail-info-item"><span class="detail-info-label">مالک</span><span class="detail-info-value">${prod.metadata?.owner || "—"}</span></div><div class="detail-info-item"><span class="detail-info-label">تعداد پروژه</span><span class="detail-info-value">${projects.length}</span></div><div class="detail-info-item"><span class="detail-info-label">استقرارهای فعال</span><span class="detail-info-value">${totalDeploys}</span></div></div></div></div><div class="detail-section"><div class="detail-section-header"><span>🌿 پروژه‌ها (${projects.length})</span><div class="detail-section-header-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.openAddProjectModal()">➕ افزودن</button></div></div><div class="detail-section-body">`;
if (projects.length === 0) {
html += '<div class="empty-subprojects">🌿 هیچ پروژه‌ای نیست</div>';
} else {
html += '<div class="subproject-list">';
projects.forEach((pr) => {
const ti = this.getTypeInfo(pr.type);
const deps = pr.dependencies || [];
const deploys = this.getActiveDeployments(pr);
const emptyEnvs = this.getEnvironmentsWithoutDeployment(pr);
const canAdd = emptyEnvs.length > 0;
html += `<div class="subproject-card"><div class="subproject-header"><div class="subproject-title"><span>${ti.icon}</span><span>${pr.title}</span><span class="type-badge type-${pr.type}">${ti.label}</span></div><div class="subproject-actions"><button class="btn btn-ghost btn-sm" onclick="ProductManager.openDependencyManager('${prod.id}','${pr.id}')">🔗 وابستگی‌ها</button><button class="btn btn-ghost btn-sm" onclick="ProductManager.openProjectEditModal('${prod.id}','${pr.id}')">✏️</button><button class="btn btn-danger btn-sm" onclick="ProductManager.removeProject('${prod.id}','${pr.id}')">🗑️</button></div></div><div class="subproject-meta">${pr.description || ""}</div><div class="detail-info-grid" style="margin-bottom:8px">${pr.metadata?.framework ? `<div class="detail-info-item"><span class="detail-info-label">فریم‌ورک</span><span class="detail-info-value">${pr.metadata.framework}</span></div>` : ""}${pr.metadata?.language ? `<div class="detail-info-item"><span class="detail-info-label">زبان</span><span class="detail-info-value">${pr.metadata.language}</span></div>` : ""}${
deps.length > 0
? `<div class="detail-info-item"><span class="detail-info-label">وابستگی‌ها (${deps.length})</span><span class="detail-info-value">${deps
.map((depId) => {
const dp = this.findProjectById(depId);
return dp ? dp.title : depId;
})
.join("، ")}</span></div>`
: ""
}</div>`;
html += `<div class="deployment-section"><div class="deployment-header"><span class="deployment-header-title">🚀 استقرارهای فعال (${deploys.length})</span>${canAdd ? `<button class="btn btn-accent btn-xs" onclick="ProductManager.openAddDeploymentModal('${prod.id}','${pr.id}')">➕ افزودن</button>` : '<span style="font-size:10px;color:var(--text-muted)">✓ کامل</span>'}</div>`;
if (deploys.length === 0) {
html +=
'<div class="empty-deployments">هنوز استقرار ندارد</div>';
} else {
html += '<div class="deployment-list">';
deploys.forEach((d) => {
const en = this.getEnvironmentName(d.environmentId);
const cl = this.getEnvironmentColors(d.environmentId);
const fd = d.deploymentDate
? new Date(d.deploymentDate).toLocaleString("fa-IR")
: "—";
html += `<div class="deployment-item"><div class="deployment-item-info"><span class="deployment-env-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor}">${en}</span><span class="deployment-version">v${d.version}</span><span class="deployment-date">📅 ${fd}</span></div><div class="deployment-actions"><button class="btn btn-ghost btn-xs" onclick="ProductManager.openEditDeploymentModal('${prod.id}','${pr.id}','${d.id}')">✏️</button><button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.id}')">🗑️</button></div>`;
if (d.releaseNotes) {
html += `<div class="deployment-notes">📝 ${d.releaseNotes}</div>`;
}
if (
d.requiredDeployments &&
d.requiredDeployments.length > 0
) {
html += `<div class="deployment-req">🔗 نیازمندی‌ها: ${d.requiredDeployments
.map((rd) => {
const rp = this.findProjectById(rd.projectId);
let depVer = "";
if (rp) {
const rdDep = (rp.deployments || []).find(
(x) => x.id === rd.deploymentId,
);
if (rdDep) depVer = ` (v${rdDep.version})`;
}
return `${rp ? rp.title : rd.projectId}${depVer}`;
})
.join("، ")}</div>`;
}
html += `</div>`;
});
html += "</div>";
}
html += `</div></div>`;
});
html += "</div>";
}
html += `</div></div>`;
document.getElementById("projectDetailBody").innerHTML = html;
ModalManager.open("projectDetailModal");
},
openAddProductModal() {
document.getElementById("newProductTitle").value = "";
document.getElementById("newProductDescription").value = "";
document.getElementById("newProductOwner").value = "";
document.getElementById("newProductCreatedAt").value =
new Date().toLocaleDateString("fa-IR");
ModalManager.open("addProductModal");
},
createNewProduct() {
const title = document.getElementById("newProductTitle").value.trim();
if (!title || title.length < 3) {
showToast("عنوان الزامی است (حداقل ۳ کاراکتر)", "error");
return;
}
if (
projectData.appData.products.some(
(p) => p.title.toLowerCase() === title.toLowerCase(),
)
) {
showToast("تکراری است", "warning");
return;
}
HistoryManager.pushState();
projectData.appData.products.push({
id: `prod-${Date.now()}`,
title,
description:
document.getElementById("newProductDescription").value.trim() ||
"",
metadata: {
createdAt: new Date().toISOString(),
owner:
document.getElementById("newProductOwner").value.trim() || "",
customFields: {},
},
projects: [],
});
ModalManager.close("addProductModal");
refreshUI();
showToast(`"${title}" ایجاد شد`, "success");
},
openDependencyManager(productId, projId) {
this.currentDependencyProjectId = productId;
this.currentDependencyProjId = projId;
this.renderCurrentDependencies();
this.renderAllProjectsList();
this.setupDependencySearch();
ModalManager.open("dependencyManagerModal");
},
renderCurrentDependencies() {
const prod = projectData.appData.products.find(
(p) => p.id === this.currentDependencyProjectId,
);
if (!prod) return;
const pr = prod.projects.find(
(p) => p.id === this.currentDependencyProjId,
);
if (!pr) return;
const c = document.getElementById("currentDependenciesList");
const deps = pr.dependencies || [];
if (deps.length === 0) {
c.innerHTML =
'<div class="empty-dependencies">هیچ وابستگی‌ای نیست</div>';
return;
}
c.innerHTML = deps
.map((depId, index) => {
const depPr = this.findProjectById(depId);
const projectName = depPr ? depPr.title : depId;
const typeInfo = depPr ? this.getTypeInfo(depPr.type) : null;
return `<div class="dependency-item"><div class="dependency-info"><div><div class="dependency-project-name">${typeInfo ? typeInfo.icon + " " : ""}${projectName}</div></div></div><button class="btn btn-danger btn-xs" onclick="ProductManager.removeDependency(${index})">🗑️</button></div>`;
})
.join("");
},
removeDependency(index) {
const prod = projectData.appData.products.find(
(p) => p.id === this.currentDependencyProjectId,
);
if (!prod) return;
const pr = prod.projects.find(
(p) => p.id === this.currentDependencyProjId,
);
if (!pr) return;
if (!confirm("حذف این وابستگی؟")) return;
HistoryManager.pushState();
pr.dependencies.splice(index, 1);
ModalManager.close("dependencyManagerModal");
refreshUI();
if (this.currentDetailProductId)
this.showDetail(this.currentDetailProductId);
showToast("وابستگی حذف شد", "success");
},
renderAllProjectsList() {
const rc = document.getElementById("dependencySearchResults");
const cp = projectData.appData.products.find(
(p) => p.id === this.currentDependencyProjectId,
);
if (!cp) return;
const cpr = cp.projects.find(
(p) => p.id === this.currentDependencyProjId,
);
if (!cpr) return;
const allProjects = [];
projectData.appData.products.forEach((prod) => {
prod.projects.forEach((pr) => {
if (pr.id !== cpr.id)
allProjects.push({ ...pr, productName: prod.title });
});
});
if (allProjects.length === 0) {
rc.innerHTML =
'<div class="empty-dependencies">پروژه دیگری نیست</div>';
return;
}
rc.innerHTML = allProjects
.map((pr) => {
const isDep = (cpr.dependencies || []).includes(pr.id);
return `<div class="dependency-project-result"><button class="dependency-project-add-btn" onclick="ProductManager.addDependency('${pr.id}')" ${isDep ? 'disabled style="opacity:.5"' : ""}><span>${this.getTypeInfo(pr.type).icon}</span><span>${pr.title}</span><small style="color:var(--text-muted);font-weight:normal">(${pr.productName})</small>${isDep ? '<span style="margin-right:auto;font-size:10px;color:var(--success)">✓ وابسته</span>' : ""}</button></div>`;
})
.join("");
},
setupDependencySearch() {
const si = document.getElementById("dependencySearch");
si.value = "";
si.oninput = (e) => {
const q = e.target.value.trim().toLowerCase();
if (q.length === 0) {
this.renderAllProjectsList();
return;
}
const cp = projectData.appData.products.find(
(p) => p.id === this.currentDependencyProjectId,
);
if (!cp) return;
const cpr = cp.projects.find(
(p) => p.id === this.currentDependencyProjId,
);
if (!cpr) return;
const allProjects = [];
projectData.appData.products.forEach((prod) => {
prod.projects.forEach((pr) => {
if (pr.id !== cpr.id)
allProjects.push({ ...pr, productName: prod.title });
});
});
const fl = allProjects.filter(
(pr) =>
pr.title.toLowerCase().includes(q) ||
pr.productName.toLowerCase().includes(q) ||
(pr.description || "").toLowerCase().includes(q),
);
const rc = document.getElementById("dependencySearchResults");
if (fl.length === 0) {
rc.innerHTML =
'<div class="empty-dependencies">نتیجه‌ای نیست</div>';
return;
}
rc.innerHTML = fl
.map((pr) => {
const isDep = (cpr.dependencies || []).includes(pr.id);
return `<div class="dependency-project-result"><button class="dependency-project-add-btn" onclick="ProductManager.addDependency('${pr.id}')" ${isDep ? 'disabled style="opacity:.5"' : ""}><span>${this.getTypeInfo(pr.type).icon}</span><span>${pr.title}</span><small style="color:var(--text-muted)">(${pr.productName})</small></button></div>`;
})
.join("");
};
},
addDependency(projectId) {
const prod = projectData.appData.products.find(
(p) => p.id === this.currentDependencyProjectId,
);
if (!prod) return;
const pr = prod.projects.find(
(p) => p.id === this.currentDependencyProjId,
);
if (!pr) return;
if ((pr.dependencies || []).includes(projectId)) {
showToast("تکراری است", "warning");
return;
}
HistoryManager.pushState();
if (!pr.dependencies) pr.dependencies = [];
pr.dependencies.push(projectId);
ModalManager.close("dependencyManagerModal");
refreshUI();
if (this.currentDetailProductId)
this.showDetail(this.currentDetailProductId);
showToast("وابستگی اضافه شد", "success");
},
openProductEditModal() {
const pid = this.currentDetailProductId;
if (!pid) return;
const prod = projectData.appData.products.find((p) => p.id === pid);
if (!prod) return;
document.getElementById("editProductId").value = prod.id;
document.getElementById("editProductTitle").value = prod.title;
document.getElementById("editProductDescription").value =
prod.description || "";
document.getElementById("editProductOwner").value =
prod.metadata?.owner || "";
document.getElementById("editProductCreatedAt").value = prod.metadata
?.createdAt
? new Date(prod.metadata.createdAt).toLocaleDateString("fa-IR")
: "—";
ModalManager.open("productEditModal");
},
saveProductEdit() {
const pid = this.currentDetailProductId;
if (!pid) return;
const prod = projectData.appData.products.find((p) => p.id === pid);
if (!prod) return;
const nt = document.getElementById("editProductTitle").value.trim();
if (!nt) {
showToast("عنوان الزامی است", "error");
return;
}
HistoryManager.pushState();
prod.title = nt;
prod.description = document
.getElementById("editProductDescription")
.value.trim();
if (!prod.metadata) prod.metadata = {};
prod.metadata.owner = document
.getElementById("editProductOwner")
.value.trim();
prod.metadata.updatedAt = new Date().toISOString();
ModalManager.close("productEditModal");
refreshUI();
this.showDetail(pid);
showToast(`"${prod.title}" ویرایش شد`, "success");
},
openAddProjectModal() {
if (!this.currentDetailProductId) {
showToast("ابتدا محصول انتخاب کنید", "warning");
return;
}
document.getElementById("newProjectTitle").value = "";
document.getElementById("newProjectType").value = "";
document.getElementById("newProjectFramework").value = "";
document.getElementById("newProjectLanguage").value = "";
document.getElementById("newProjectRepo").value = "";
document.getElementById("newProjectDescription").value = "";
populateProjectTypeDropdowns();
ModalManager.open("addProjectModal");
},
saveNewProject() {
const pid = this.currentDetailProductId;
if (!pid) return;
const prod = projectData.appData.products.find((p) => p.id === pid);
if (!prod) return;
const title = document.getElementById("newProjectTitle").value.trim();
const type = document.getElementById("newProjectType").value;
if (!title || !type) {
showToast("عنوان و نوع الزامی است", "error");
return;
}
HistoryManager.pushState();
const np = {
id: `proj-${Date.now()}`,
title,
description:
document.getElementById("newProjectDescription").value.trim() ||
"",
type,
repositoryUrl: document
.getElementById("newProjectRepo")
.value.trim(),
metadata: { createdAt: new Date().toISOString(), customFields: {} },
dependencies: [],
deployments: [],
};
const fw = document
.getElementById("newProjectFramework")
.value.trim();
if (fw) np.metadata.framework = fw;
const lg = document.getElementById("newProjectLanguage").value.trim();
if (lg) np.metadata.language = lg;
prod.projects.push(np);
ModalManager.close("addProjectModal");
refreshUI();
this.showDetail(pid);
showToast(`"${title}" ایجاد شد`, "success");
},
openProjectEditModal(productId, projId) {
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
if (!prod) return;
const pr = prod.projects.find((p) => p.id === projId);
if (!pr) return;
this.currentEditProjectId = projId;
populateProjectTypeDropdowns();
document.getElementById("editProjId").value = pr.id;
document.getElementById("editProjTitle").value = pr.title;
document.getElementById("editProjType").value = pr.type;
document.getElementById("editProjFramework").value =
pr.metadata?.framework || "";
document.getElementById("editProjLanguage").value =
pr.metadata?.language || "";
document.getElementById("editProjRepo").value =
pr.repositoryUrl || "";
document.getElementById("editProjDescription").value =
pr.description || "";
ModalManager.open("editProjectModal");
},
saveProjectEdit() {
const pid = this.currentDetailProductId;
const projId = this.currentEditProjectId;
if (!pid || !projId) return;
const prod = projectData.appData.products.find((p) => p.id === pid);
if (!prod) return;
const pr = prod.projects.find((p) => p.id === projId);
if (!pr) return;
const nt = document.getElementById("editProjTitle").value.trim();
const nty = document.getElementById("editProjType").value;
if (!nt || !nty) {
showToast("عنوان و نوع الزامی است", "error");
return;
}
HistoryManager.pushState();
pr.title = nt;
pr.type = nty;
pr.description =
document.getElementById("editProjDescription").value.trim() || "";
pr.repositoryUrl = document
.getElementById("editProjRepo")
.value.trim();
if (!pr.metadata) pr.metadata = {};
const fw = document.getElementById("editProjFramework").value.trim();
if (fw) pr.metadata.framework = fw;
else delete pr.metadata.framework;
const lg = document.getElementById("editProjLanguage").value.trim();
if (lg) pr.metadata.language = lg;
else delete pr.metadata.language;
pr.metadata.updatedAt = new Date().toISOString();
ModalManager.close("editProjectModal");
refreshUI();
this.showDetail(pid);
showToast(`"${pr.title}" ویرایش شد`, "success");
},
openAddDeploymentModal(productId, projId) {
if (!productId || !projId) return;
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
if (!prod) return;
const pr = prod.projects.find((p) => p.id === projId);
if (!pr) return;
const emptyEnvs = this.getEnvironmentsWithoutDeployment(pr);
if (emptyEnvs.length === 0) {
showToast("همه محیط‌ها استقرار دارند", "warning");
return;
}
this.currentDeploymentProjId = projId;
this.currentDeploymentMode = "add";
this.currentDeploymentEnvId = null;
this.currentEditDeploymentId = null;
document.getElementById("deploymentModalTitle").innerHTML =
"<span>🚀</span><span>افزودن نسخه استقرار</span>";
const envGroup = document.getElementById("deploymentEnvGroup");
const envs = projectData.appData.environments.filter((e) =>
emptyEnvs.includes(e.id),
);
envGroup.innerHTML = `<label class="form-label">محیط استقرار <span class="required">*</span></label><select class="form-select" id="depEnvironment" onchange="ProductManager.onEnvChange()">${envs
.map((e) => {
const cl = this.getEnvironmentColors(e.id);
return `<option value="${e.id}" style="background-color:${cl.backgroundColor};color:${cl.textColor}">${e.name} - ${e.description}</option>`;
})
.join("")}</select>`;
document.getElementById("depVersion").value = "";
document.getElementById("depDate").value = "";
document.getElementById("depReleaseNotes").value = "";
this.renderRequiredDeploymentsSection();
ModalManager.open("deploymentModal");
},
openEditDeploymentModal(productId, projId, depId) {
if (!productId || !projId || !depId) return;
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
if (!prod) return;
const pr = prod.projects.find((p) => p.id === projId);
if (!pr) return;
const dep = (pr.deployments || []).find((d) => d.id === depId);
if (!dep) return;
this.currentDeploymentProjId = projId;
this.currentDeploymentMode = "edit";
this.currentDeploymentEnvId = dep.environmentId;
this.currentEditDeploymentId = depId;
document.getElementById("deploymentModalTitle").innerHTML =
"<span>✏️</span><span>ویرایش نسخه استقرار</span>";
const envGroup = document.getElementById("deploymentEnvGroup");
const cl = this.getEnvironmentColors(dep.environmentId);
envGroup.innerHTML = `<label class="form-label">محیط استقرار <span class="readonly-badge">فقط خواندنی</span></label><div class="env-read-only-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor}">${this.getEnvironmentName(dep.environmentId)}</div>`;
document.getElementById("depVersion").value = dep.version;
let dv = "";
if (dep.deploymentDate) {
const d = new Date(dep.deploymentDate);
const pad = (n) => n.toString().padStart(2, "0");
dv = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
document.getElementById("depDate").value = dv;
document.getElementById("depReleaseNotes").value =
dep.releaseNotes || "";
this.renderRequiredDeploymentsSection(dep);
ModalManager.open("deploymentModal");
},
onEnvChange() {
this.renderRequiredDeploymentsSection();
},
renderRequiredDeploymentsSection(existingDep) {
const section = document.getElementById("requiredDeploymentsSection");
const envSelect = document.getElementById("depEnvironment");
const envId =
this.currentDeploymentMode === "edit"
? this.currentDeploymentEnvId
: envSelect
? envSelect.value
: null;
if (!envId) {
section.innerHTML = "";
return;
}
const prod = projectData.appData.products.find(
(p) => p.id === this.currentDetailProductId,
);
if (!prod) return;
const pr = prod.projects.find(
(p) => p.id === this.currentDeploymentProjId,
);
if (!pr) return;
const deps = pr.dependencies || [];
if (deps.length === 0) {
section.innerHTML = "";
return;
}
let h =
'<div class="form-group"><label class="form-label">🔗 استقرارهای مورد نیاز (به ازای هر وابستگی) <span class="required">*</span></label><div class="form-hint">برای هر وابستگی، یک استقرار از آن پروژه در محیط "' +
this.getEnvironmentName(envId) +
'" انتخاب کنید</div>';
deps.forEach((depId) => {
const depPr = this.findProjectById(depId);
if (!depPr) return;
const depDeployments = (depPr.deployments || []).filter(
(d) => d.status === "active" && d.environmentId === envId,
);
const existingReq = existingDep
? (existingDep.requiredDeployments || []).find(
(r) => r.projectId === depId,
)
: null;
h += `<div class="req-deployment-box"><div class="req-deployment-title">📦 ${depPr.title}</div>`;
if (depDeployments.length === 0) {
h += `<div style="color:var(--danger);font-size:11px">⚠️ هیچ استقرار فعالی در ${this.getEnvironmentName(envId)} ندارد.</div>`;
} else {
h += `<select class="req-deployment-select" data-dep-project="${depId}">${depDeployments.map((d) => `<option value="${d.id}" ${existingReq && existingReq.deploymentId === d.id ? "selected" : ""}>v${d.version} - ${d.deploymentDate ? new Date(d.deploymentDate).toLocaleDateString("fa-IR") : ""}</option>`).join("")}</select>`;
}
h += "</div>";
});
h += "</div>";
section.innerHTML = h;
},
saveDeployment() {
const pid = this.currentDetailProductId;
const projId = this.currentDeploymentProjId;
if (!pid || !projId) {
showToast("خطا: محصول یا پروژه مشخص نشده", "error");
return;
}
const prod = projectData.appData.products.find((p) => p.id === pid);
if (!prod) return;
const pr = prod.projects.find((p) => p.id === projId);
if (!pr) return;
const version = document.getElementById("depVersion").value.trim();
const date = document.getElementById("depDate").value;
const releaseNotes = document
.getElementById("depReleaseNotes")
.value.trim();
if (!version) {
showToast("نسخه الزامی است", "error");
return;
}
if (!date) {
showToast("تاریخ استقرار الزامی است", "error");
return;
}
if (!releaseNotes) {
showToast("ریلیز نوت الزامی است", "error");
return;
}
const deploymentDate = new Date(date).toISOString();
HistoryManager.pushState();
if (!pr.deployments) pr.deployments = [];
const requiredDeployments = [];
document.querySelectorAll(".req-deployment-select").forEach((sel) => {
const depProjectId = sel.dataset.depProject;
const depDeploymentId = sel.value;
if (depProjectId && depDeploymentId) {
requiredDeployments.push({
projectId: depProjectId,
deploymentId: depDeploymentId,
});
}
});
if (this.currentDeploymentMode === "add") {
const envId = document.getElementById("depEnvironment").value;
if (!envId) {
showToast("محیط را انتخاب کنید", "error");
return;
}
pr.deployments.push({
id: `dep-${Date.now()}`,
environmentId: envId,
version,
deploymentDate,
releaseNotes,
requiredDeployments,
status: "active",
});
showToast(
`v${version} در ${this.getEnvironmentName(envId)} استقرار یافت`,
"success",
);
} else {
const depId = this.currentEditDeploymentId;
const dep = pr.deployments.find((d) => d.id === depId);
if (dep) {
dep.version = version;
dep.deploymentDate = deploymentDate;
dep.releaseNotes = releaseNotes;
dep.requiredDeployments = requiredDeployments;
showToast(`استقرار ویرایش شد`, "success");
}
}
ModalManager.close("deploymentModal");
refreshUI();
this.showDetail(pid);
},
removeDeployment(productId, projId, depId) {
if (!confirm("حذف این استقرار؟")) return;
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
if (!prod) return;
const pr = prod.projects.find((p) => p.id === projId);
if (!pr) return;
const dep = (pr.deployments || []).find((d) => d.id === depId);
if (!dep) return;
HistoryManager.pushState();
dep.status = "removed";
refreshUI();
this.showDetail(productId);
showToast("استقرار حذف شد", "warning");
},
removeProduct(productId) {
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
if (!prod) return;
if (!confirm(`حذف "${prod.title}"؟`)) return;
HistoryManager.pushState();
projectData.appData.products = projectData.appData.products.filter(
(p) => p.id !== productId,
);
refreshUI();
ModalManager.close("projectDetailModal");
showToast(`"${prod.title}" حذف شد`, "warning");
},
removeProject(productId, projId) {
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
if (!prod) return;
const pr = prod.projects.find((p) => p.id === projId);
if (!pr) return;
if (!confirm(`حذف "${pr.title}"؟`)) return;
HistoryManager.pushState();
prod.projects = prod.projects.filter((p) => p.id !== projId);
refreshUI();
if (this.currentDetailProductId === productId)
this.showDetail(productId);
showToast(`"${pr.title}" حذف شد`, "warning");
},
};
/* ==================== ✅ DASHBOARD MANAGER (COMPLETED WITH D3 CHARTS) ==================== */
const DashboardManager = {
open() {
ModalManager.open("executiveDashboardModal");
setTimeout(() => this.render(), 150);
},
getReportData() {
const ps = projectData.appData.products || [];
let tp = 0,
td = 0,
tdp = 0;
const tc = {},
edc = {},
ppc = [],
fc = {};
let pwd = 0,
pwod = 0;
ps.forEach((prod) => {
const projects = prod.projects || [];
ppc.push({ title: prod.title, count: projects.length });
projects.forEach((pr) => {
tp++;
td += (pr.dependencies || []).length;
const active = (pr.deployments || []).filter(
(d) => d.status === "active",
);
tdp += active.length;
if (active.length > 0) pwd++;
else pwod++;
tc[pr.type] = (tc[pr.type] || 0) + 1;
const fw = pr.metadata?.framework || "نامشخص";
fc[fw] = (fc[fw] || 0) + 1;
active.forEach((d) => {
const env = projectData.appData.environments.find(
(e) => e.id === d.environmentId,
);
const envName = env ? env.name : "نامشخص";
edc[envName] = (edc[envName] || 0) + 1;
});
});
});
return {
products: ps.length,
projects: tp,
deps: td,
deploys: tdp,
environments: projectData.appData.environments.length,
tc,
edc,
ppc,
fc,
pwd,
pwod,
};
},
render() {
const c = document.getElementById("dashboardContainer");
if (!c) return;
const d = this.getReportData();
let h = '<div class="dashboard-kpi-row">';
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📦</div><div class="dashboard-kpi-value">${d.products}</div><div class="dashboard-kpi-label">محصول</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🌿</div><div class="dashboard-kpi-value">${d.projects}</div><div class="dashboard-kpi-label">پروژه</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔗</div><div class="dashboard-kpi-value">${d.deps}</div><div class="dashboard-kpi-label">وابستگی</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🚀</div><div class="dashboard-kpi-value">${d.deploys}</div><div class="dashboard-kpi-label">استقرار</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🌍</div><div class="dashboard-kpi-value">${d.environments}</div><div class="dashboard-kpi-label">محیط</div></div>`;
h += '</div><div class="dashboard-charts-grid">';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🥧 توزیع نوع پروژه‌ها</div><div class="dashboard-chart-body" id="dashTypeChart"></div></div>';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🌍 استقرارها به تفکیک محیط</div><div class="dashboard-chart-body" id="dashEnvChart"></div></div>';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">✅ وضعیت استقرار پروژه‌ها</div><div class="dashboard-chart-body" id="dashStatusChart"></div></div>';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 پروژه‌ها به تفکیک محصول</div><div class="dashboard-chart-body" id="dashProductChart"></div></div>';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🛠️ توزیع فریم‌ورک‌ها</div><div class="dashboard-chart-body" id="dashFrameworkChart"></div></div>';
h += "</div>";
c.innerHTML = h;
this.renderDonut(
"dashTypeChart",
Object.entries(d.tc).map(([k, v]) => ({
label: this.getTypeLabel(k),
value: v,
color: this.getTypeColor(k),
})),
);
this.renderBar(
"dashEnvChart",
Object.entries(d.edc).map(([k, v]) => ({ label: k, value: v })),
"#38a169",
);
this.renderDonut("dashStatusChart", [
{ label: "با استقرار", value: d.pwd, color: "#38a169" },
{ label: "بدون استقرار", value: d.pwod, color: "#e53e3e" },
]);
this.renderBar(
"dashProductChart",
d.ppc.map((p) => ({
label:
p.title.length > 18
? p.title.substring(0, 18) + "..."
: p.title,
value: p.count,
})),
"#3182ce",
);
this.renderBarH(
"dashFrameworkChart",
Object.entries(d.fc)
.sort((a, b) => b[1] - a[1])
.slice(0, 8)
.map(([k, v]) => ({
label: k.length > 14 ? k.substring(0, 14) + "..." : k,
value: v,
})),
);
const coverage =
d.pwd + d.pwod > 0
? Math.round((d.pwd / (d.pwd + d.pwod)) * 100)
: 0;
document.getElementById("dashboardFooterStats").innerHTML =
`<span>📦 محصولات: <span class="stat-highlight">${d.products}</span></span><span>🌿 پروژه‌ها: <span class="stat-highlight">${d.projects}</span></span><span>🚀 استقرارها: <span class="stat-highlight">${d.deploys}</span></span><span>📈 پوشش استقرار: <span class="stat-highlight">${coverage}%</span></span>`;
document.getElementById("dashboardRefresh").onclick = () =>
this.render();
},
renderDonut(cid, data) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
if (!data || data.length === 0 || data.every((d) => d.value === 0)) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
return;
}
const w = 280,
hgt = 220,
r = Math.min(w, hgt) / 2 - 15;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${w / 2},${hgt / 2})`);
const pie = d3
.pie()
.value((dd) => dd.value)
.sort(null);
const arc = d3
.arc()
.innerRadius(r * 0.55)
.outerRadius(r);
const arcH = d3
.arc()
.innerRadius(r * 0.55)
.outerRadius(r + 8);
const total = data.reduce((s, dd) => s + dd.value, 0);
g.selectAll("path")
.data(pie(data))
.join("path")
.attr("d", arc)
.attr("fill", (dd) => dd.data.color)
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.style("cursor", "pointer")
.on("mouseover", function (e, dd) {
d3.select(this).transition().duration(150).attr("d", arcH(dd));
showD3Tooltip(
e,
`${dd.data.label}: ${dd.data.value} (${Math.round((dd.data.value / total) * 100)}%)`,
);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function () {
d3.select(this).transition().duration(150).attr("d", arc);
hideD3Tooltip();
});
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "-.2em")
.attr("fill", "var(--text)")
.attr("font-size", "24px")
.attr("font-weight", "800")
.text(total);
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "1.4em")
.attr("fill", "var(--text-muted)")
.attr("font-size", "11px")
.text("پروژه");
},
renderBar(cid, data, color) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
if (!data || data.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
return;
}
const w = 320,
hgt = 220,
m = { top: 20, right: 15, bottom: 45, left: 35 };
const iw = w - m.left - m.right,
ih = hgt - m.top - m.bottom;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${m.left},${m.top})`);
const x = d3
.scaleBand()
.domain(data.map((dd) => dd.label))
.range([0, iw])
.padding(0.3);
const y = d3
.scaleLinear()
.domain([0, d3.max(data, (dd) => dd.value) || 1])
.nice()
.range([ih, 0]);
g.append("g")
.attr("transform", `translate(0,${ih})`)
.call(d3.axisBottom(x).tickSize(0))
.selectAll("text")
.attr("fill", "var(--text-muted)")
.attr("font-size", "8px")
.attr("font-family", "Vazirmatn")
.attr("transform", "rotate(-25)")
.attr("text-anchor", "end");
g.append("g")
.call(d3.axisLeft(y).ticks(4).tickSize(-iw))
.selectAll("text")
.attr("fill", "var(--text-muted)")
.attr("font-size", "9px");
g.selectAll(".grid-line")
.data(y.ticks(4))
.join("line")
.attr("x1", 0)
.attr("x2", iw)
.attr("y1", (dd) => y(dd))
.attr("y2", (dd) => y(dd))
.attr("stroke", "var(--border-light)")
.attr("stroke-dasharray", "3,3");
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (dd) => x(dd.label))
.attr("y", (dd) => y(dd.value))
.attr("width", x.bandwidth())
.attr("height", (dd) => ih - y(dd.value))
.attr("fill", color)
.attr("rx", 4)
.style("cursor", "pointer")
.on("mouseover", function (e, dd) {
d3.select(this).attr("fill", "var(--accent)");
showD3Tooltip(e, `${dd.label}: ${dd.value}`);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function () {
d3.select(this).attr("fill", color);
hideD3Tooltip();
});
g.selectAll(".bar-label")
.data(data)
.join("text")
.attr("x", (dd) => x(dd.label) + x.bandwidth() / 2)
.attr("y", (dd) => y(dd.value) - 5)
.attr("text-anchor", "middle")
.attr("fill", "var(--text)")
.attr("font-size", "10px")
.attr("font-weight", "700")
.text((dd) => dd.value);
},
renderBarH(cid, data) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
if (!data || data.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>🛠️</span><div>داده‌ای نیست</div></div>';
return;
}
const bh = 28,
gap = 6;
const w = 320,
hgt = data.length * (bh + gap) + 30;
const m = { top: 10, right: 35, bottom: 10, left: 110 };
const iw = w - m.left - m.right,
ih = hgt - m.top - m.bottom;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${m.left},${m.top})`);
const x = d3
.scaleLinear()
.domain([0, d3.max(data, (dd) => dd.value) || 1])
.range([0, iw]);
const y = d3
.scaleBand()
.domain(data.map((dd) => dd.label))
.range([0, ih])
.padding(0.2);
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", 0)
.attr("y", (dd) => y(dd.label))
.attr("width", (dd) => x(dd.value))
.attr("height", y.bandwidth())
.attr("fill", "#2c5282")
.attr("rx", 4)
.style("cursor", "pointer")
.on("mouseover", function (e, dd) {
d3.select(this).attr("fill", "var(--accent)");
showD3Tooltip(e, `${dd.label}: ${dd.value}`);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function () {
d3.select(this).attr("fill", "#2c5282");
hideD3Tooltip();
});
g.selectAll(".label")
.data(data)
.join("text")
.attr("x", -8)
.attr("y", (dd) => y(dd.label) + y.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.attr("fill", "var(--text)")
.attr("font-size", "9px")
.attr("font-weight", "600")
.text((dd) => dd.label);
g.selectAll(".value")
.data(data)
.join("text")
.attr("x", (dd) => x(dd.value) + 5)
.attr("y", (dd) => y(dd.label) + y.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("fill", "var(--text-muted)")
.attr("font-size", "10px")
.attr("font-weight", "700")
.text((dd) => dd.value);
},
getTypeLabel(t) {
const i = VALID_PROJECT_TYPES.find((x) => x.value === t);
return i ? i.label.split(" ")[0] : t;
},
getTypeColor(t) {
const c = {
backend: "#4299e1",
frontend: "#48bb78",
service: "#ecc94b",
database: "#ed64a6",
mobile: "#9f7aea",
infrastructure: "#ed8936",
};
return c[t] || "#718096";
},
};
/* ==================== GALAXY CHART ==================== */
const GalaxyChartManager = {
svg: null,
zoomBehavior: null,
zoomGroup: null,
nodeElements: null,
linkElements: null,
labelElements: null,
allNodes: [],
collapsedProducts: new Set(), // ← NEW: وضعیت کولاپس محصولات
NODE_COLORS: {
product: "#c9a961",
backend: "#4299e1",
frontend: "#48bb78",
service: "#ecc94b",
database: "#ed64a6",
mobile: "#9f7aea",
infrastructure: "#ed8936",
},
open() {
ModalManager.open("galaxyChartModal");
setTimeout(() => this.render(), 150);
},
/* ── NEW: ساخت گراف با منطق کولاپس (راهبرد الف) ── */
buildGraphData() {
const nodes = [];
const links = [];
const projectToProduct = new Map();
// ساخت نگاشت پروژه -> محصول
projectData.appData.products.forEach((prod) => {
(prod.projects || []).forEach((pr) => {
projectToProduct.set(pr.id, prod.id);
});
});
// ساخت نودها
projectData.appData.products.forEach((prod) => {
const isCollapsed = this.collapsedProducts.has(prod.id);
nodes.push({
id: prod.id,
label: prod.title,
type: "product",
radius: 30,
data: prod,
collapsed: isCollapsed,
collapsedCount: isCollapsed ? (prod.projects || []).length : 0,
});
// اگر محصول کولاپس نیست، پروژه‌هایش را اضافه کن
if (!isCollapsed) {
(prod.projects || []).forEach((pr) => {
nodes.push({
id: pr.id,
label: pr.title,
type: "project",
projectType: pr.type,
radius: 16,
data: pr,
productTitle: prod.title,
productId: prod.id,
});
links.push({
source: prod.id,
target: pr.id,
type: "ownership",
});
});
}
});
// ساخت یال‌های وابستگی با هدایت به نود محصول کولاپس‌شده (راهبرد الف)
projectData.appData.products.forEach((prod) => {
(prod.projects || []).forEach((pr) => {
(pr.dependencies || []).forEach((depId) => {
let sourceId = pr.id;
let targetId = depId;
// اگر پروژه مبدأ کولاپس است، وابستگی را به نود محصول هدایت کن
const sourceProductId = projectToProduct.get(pr.id);
if (
sourceProductId &&
this.collapsedProducts.has(sourceProductId)
) {
sourceId = sourceProductId;
}
// اگر پروژه مقصد کولاپس است، وابستگی را به نود محصول هدایت کن
const targetProductId = projectToProduct.get(depId);
if (
targetProductId &&
this.collapsedProducts.has(targetProductId)
) {
targetId = targetProductId;
}
// جلوگیری از یال به خود و یال‌های تکراری
if (sourceId === targetId) return;
const exists = links.some((l) => {
const s =
typeof l.source === "object" ? l.source.id : l.source;
const t =
typeof l.target === "object" ? l.target.id : l.target;
return (
s === sourceId && t === targetId && l.type === "dependency"
);
});
if (!exists) {
links.push({
source: sourceId,
target: targetId,
type: "dependency",
});
}
});
});
});
return { nodes, links };
},
/* ── NEW: کولاپس/بازکردن پروژه‌های یک محصول ── */
toggleCollapseProduct(productId) {
if (this.collapsedProducts.has(productId)) {
this.collapsedProducts.delete(productId);
} else {
this.collapsedProducts.add(productId);
}
this.render(); // بازسازی کامل گراف
},
/* ── کولاپس همه محصولات ── */
collapseAllProducts() {
projectData.appData.products.forEach((prod) => {
this.collapsedProducts.add(prod.id);
});
this.render();
showToast(
`⊟ ${projectData.appData.products.length} محصول کولاپس شدند`,
"info",
2000,
);
},
/* ── باز کردن همه محصولات ── */
expandAllProducts() {
const count = this.collapsedProducts.size;
this.collapsedProducts.clear();
this.render();
showToast(`⊞ ${count} محصول از حالت کولاپس خارج شدند`, "info", 2000);
},
/* ── رندر اصلی ── */
render() {
const c = document.getElementById("galaxyChartContainer");
c.innerHTML = "";
const w = c.clientWidth || 900,
h = c.clientHeight || 600;
// استفاده از متد buildGraphData
const { nodes, links } = this.buildGraphData();
this.allNodes = nodes;
if (nodes.length === 0) {
c.innerHTML =
'<div class="galaxy-empty"><div style="font-size:60px">🌌</div><div>هیچ داده‌ای نیست</div></div>';
return;
}
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("id", "galaxySvg");
this.svg = svg;
svg
.append("rect")
.attr("width", w)
.attr("height", h)
.attr("fill", "#0a0e27");
for (let i = 0; i < 100; i++) {
svg
.append("circle")
.attr("cx", Math.random() * w)
.attr("cy", Math.random() * h)
.attr("r", Math.random() + 0.3)
.attr("fill", "#fff")
.attr("opacity", Math.random() * 0.5 + 0.1);
}
const defs = svg.append("defs");
const gf = defs.append("filter").attr("id", "galaxyGlow");
gf.append("feGaussianBlur")
.attr("stdDeviation", "3")
.attr("result", "blur");
const fm = gf.append("feMerge");
fm.append("feMergeNode").attr("in", "blur");
fm.append("feMergeNode").attr("in", "SourceGraphic");
this.zoomGroup = svg.append("g");
this.zoomBehavior = d3
.zoom()
.scaleExtent([0.2, 5])
.on("zoom", (e) => {
this.zoomGroup.attr("transform", e.transform);
document.getElementById("galaxyZoomIndicator").textContent =
`🔍 ${Math.round(e.transform.k * 100)}%`;
});
svg.call(this.zoomBehavior);
const sim = d3
.forceSimulation(nodes)
.force(
"link",
d3
.forceLink(links)
.id((d) => d.id)
.distance((d) => (d.type === "ownership" ? 100 : 160)),
)
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(w / 2, h / 2));
this.linkElements = this.zoomGroup
.append("g")
.selectAll("line")
.data(links)
.join("line")
.attr("stroke", (d) =>
d.type === "dependency" ? "#e0c789" : "#4a5568",
)
.attr("stroke-width", (d) => (d.type === "dependency" ? 2 : 1))
.attr("stroke-opacity", 0.6)
.attr("stroke-dasharray", (d) =>
d.type === "dependency" ? "5,3" : "none",
);
const node = this.zoomGroup
.append("g")
.selectAll("g")
.data(nodes)
.join("g")
.style("cursor", "pointer")
.call(
d3
.drag()
.on("start", (e, d) => {
if (!e.active) sim.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
})
.on("drag", (e, d) => {
d.fx = e.x;
d.fy = e.y;
})
.on("end", (e, d) => {
if (!e.active) sim.alphaTarget(0);
d.fx = null;
d.fy = null;
}),
);
this.nodeElements = node;
node
.append("circle")
.attr("r", (d) => d.radius)
.attr("fill", (d) =>
d.type === "product"
? this.NODE_COLORS.product
: this.NODE_COLORS[d.projectType] || "#718096",
)
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.attr("filter", "url(#galaxyGlow)");
node
.append("text")
.attr("dy", (d) => d.radius + 14)
.attr("text-anchor", "middle")
.attr("fill", "#fff")
.attr("font-size", "10px")
.attr("font-family", "Vazirmatn")
.text((d) => d.label);
// NEW: نشانگر تعداد پروژه‌ها روی نود کولاپس‌شده
node
.filter((d) => d.type === "product" && d.collapsed)
.append("text")
.attr("class", "galaxy-collapse-badge")
.attr("dy", 4)
.attr("text-anchor", "middle")
.attr("fill", "#fff")
.attr("font-size", "11px")
.attr("font-weight", "700")
.text((d) => `+${d.collapsedCount}`);
// کلیک = نمایش پنل اطلاعات / دوبارکلیک = کولاپس
node.on("click", (e, d) => {
this.showNodeInfo(d);
});
node.on("dblclick", (e, d) => {
if (d.type === "product") {
e.stopPropagation();
this.toggleCollapseProduct(d.id);
}
});
sim.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);
node.attr("transform", (d) => `translate(${d.x},${d.y})`);
});
this.renderLegend();
this.updateStats(nodes, links);
this.setupSearch();
s;
},
getTypeLabel(t) {
const i = VALID_PROJECT_TYPES.find((x) => x.value === t);
return i ? i.label.split(" ")[0] : t;
},
renderLegend() {
const l = document.getElementById("galaxyLegend");
let h = '<div class="galaxy-legend-title">🎨 راهنما</div>';
h += `<div class="galaxy-legend-item"><div class="galaxy-legend-color" style="background:${this.NODE_COLORS.product}"></div><span>📦 محصول</span></div>`;
VALID_PROJECT_TYPES.forEach((t) => {
h += `<div class="galaxy-legend-item"><div class="galaxy-legend-color" style="background:${this.NODE_COLORS[t.value]}"></div><span>${t.icon} ${t.label.split(" ")[0]}</span></div>`;
});
l.innerHTML = h;
},
updateStats(nodes, links) {
const p = projectData.appData.products.length;
const pr = projectData.appData.products.reduce(
(x, p) => x + p.projects.length,
0,
);
const dp = links.filter((l) => l.type === "dependency").length;
document.getElementById("galaxyStats").innerHTML =
`<span>📦 ${p}</span><span>🌿 ${pr}</span><span>🔗 ${dp}</span>`;
},
/* ── NEW: پنل اطلاعات دقیق با دکمه کولاپس ── */
showNodeInfo(d) {
const p = document.getElementById("galaxyInfoPanel");
const ic = document.getElementById("galaxyInfoContent");
let ih = "";
if (d.type === "product") {
const projects = d.data.projects || [];
const isCollapsed = this.collapsedProducts.has(d.id);
ih += `<h4>📦 ${d.label}</h4>`;
ih += `<div class="galaxy-info-row"><span class="galaxy-info-label">توضیحات</span><span class="galaxy-info-value">${d.data.description || "—"}</span></div>`;
ih += `<div class="galaxy-info-row"><span class="galaxy-info-label">مالک</span><span class="galaxy-info-value">${d.data.metadata?.owner || "—"}</span></div>`;
ih += `<div class="galaxy-info-row"><span class="galaxy-info-label">تعداد پروژه</span><span class="galaxy-info-value">${projects.length}</span></div>`;
if (projects.length > 0) {
ih += `<div style="margin-top:10px;border-top:1px solid rgba(255,255,255,.1);padding-top:8px"><div style="font-weight:700;font-size:11px;margin-bottom:6px">🌿 پروژه‌ها:</div>`;
projects.forEach((pr) => {
const ti = this.getTypeInfo(pr.type);
ih += `<div style="font-size:11px;padding:3px 0;color:rgba(255,255,255,.8)">${ti.icon} ${pr.title}</div>`;
});
ih += `</div>`;
}
ih += `<div style="margin-top:12px;border-top:1px solid rgba(255,255,255,.1);padding-top:10px">`;
// ih += `<button onclick="GalaxyChartManager.toggleCollapseProduct('${d.id}')" style="width:100%;padding:8px;background:${isCollapsed ? '#38a169' : '#dd6b20'};color:#fff;border:none;border-radius:6px;cursor:pointer;font-family:inherit;font-size:12px;font-weight:600">${isCollapsed ? '📂 بازکردن پروژه‌ها' : '📁 جمع‌کردن پروژه‌ها'}</button>`;
ih += `</div>`;
} else {
const ti = this.getTypeInfo(d.projectType);
const deploys = ProductManager.getActiveDeployments(d.data);
ih += `<h4>${ti.icon} ${d.label}</h4>`;
ih += `<div class="galaxy-info-row"><span class="galaxy-info-label">نوع</span><span class="galaxy-info-value">${ti.label}</span></div>`;
ih += `<div class="galaxy-info-row"><span class="galaxy-info-label">فریم‌ورک</span><span class="galaxy-info-value">${d.data.metadata?.framework || "—"}</span></div>`;
ih += `<div class="galaxy-info-row"><span class="galaxy-info-label">استقرارها</span><span class="galaxy-info-value">${deploys.length}</span></div>`;
}
ic.innerHTML = ih;
p.classList.add("visible");
},
getTypeInfo(type) {
return (
VALID_PROJECT_TYPES.find((t) => t.value === type) || {
value: type,
label: type,
icon: "❓",
}
);
},
/* ── جستجو در نمودار ── */
setupSearch() {
const inp = document.getElementById("galaxySearchInput");
const clr = document.getElementById("galaxySearchClear");
if (!inp || !clr) return;
inp.value = "";
clr.classList.remove("visible");
inp.oninput = () => this.applySearch(inp.value.trim());
inp.onkeydown = (e) => {
if (e.key === "Escape" && inp.value) {
e.stopPropagation();
this.clearSearch();
inp.focus();
}
};
clr.onclick = () => {
this.clearSearch();
inp.focus();
};
},
applySearch(query) {
const clr = document.getElementById("galaxySearchClear");
const results = document.getElementById("galaxySearchResults");
if (clr) clr.classList.toggle("visible", !!query);
if (!query) {
this.clearSearch();
return;
}
const q = query.toLowerCase();
const matches = this.allNodes.filter((n) => {
const hay = [
n.label,
n.productTitle || "",
n.data?.description || "",
n.data?.metadata?.framework || "",
n.data?.metadata?.language || "",
]
.join(" ")
.toLowerCase();
return hay.includes(q);
});
// هایلایت نودهای منطبق
const matchIds = new Set(matches.map((m) => m.id));
if (this.nodeElements) {
this.nodeElements
.classed("galaxy-dim", (d) => !matchIds.has(d.id))
.classed("galaxy-match", (d) => matchIds.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 !(matchIds.has(s) || matchIds.has(t));
});
}
// نمایش لیست نتایج
if (!results) return;
if (matches.length === 0) {
results.innerHTML =
'<div class="galaxy-search-empty">نتیجه‌ای یافت نشد</div>';
} else {
results.innerHTML = matches
.slice(0, 30)
.map((n) => {
const icon =
n.type === "product"
? "📦"
: ProductManager.getTypeInfo(n.projectType)?.icon || "❓";
const sub =
n.type === "product"
? "محصول"
: ProductManager.getTypeInfo(n.projectType)?.label || "";
return `<div class="galaxy-search-result-item" data-node-id="${n.id}">
<span class="galaxy-search-result-icon">${icon}</span>
<div class="galaxy-search-result-text">
<div class="galaxy-search-result-title">${n.label}</div>
<div class="galaxy-search-result-sub">${sub}</div>
</div>
</div>`;
})
.join("");
results
.querySelectorAll(".galaxy-search-result-item")
.forEach((item) => {
item.addEventListener("click", () => {
this.focusNode(item.dataset.nodeId);
results.classList.remove("visible");
});
});
}
results.classList.add("visible");
},
clearSearch() {
const inp = document.getElementById("galaxySearchInput");
const clr = document.getElementById("galaxySearchClear");
const results = document.getElementById("galaxySearchResults");
if (inp) inp.value = "";
if (clr) clr.classList.remove("visible");
if (results) {
results.innerHTML = "";
results.classList.remove("visible");
}
this.allNodes.forEach((n) => (n._matched = false));
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(nodeId) {
const n = this.allNodes.find((x) => x.id === nodeId);
if (!n || !this.svg || !this.zoomBehavior) return;
const c = document.getElementById("galaxyChartContainer");
const w = c.clientWidth || 900,
h = c.clientHeight || 600;
const scale = 1.6;
const t = d3.zoomIdentity
.translate(w / 2 - scale * (n.x || 0), h / 2 - scale * (n.y || 0))
.scale(scale);
this.svg
.transition()
.duration(600)
.call(this.zoomBehavior.transform, t);
this.showNodeInfo(n);
if (this.nodeElements) {
this.nodeElements.classed("galaxy-focus", (d) => d.id === nodeId);
setTimeout(() => {
if (this.nodeElements)
this.nodeElements.classed("galaxy-focus", false);
}, 2000);
}
},
};
/* ==================== ANALYSIS MANAGERS ==================== */
const DependencyAnalysisManager = {
open() {
ModalManager.open("dependencyAnalysisModal");
setTimeout(() => this.render(), 150);
},
getData() {
let totalDeps = 0,
totalProjects = 0,
brokenDeps = 0;
const allProjects = [];
const inDeps = {};
const typeCounts = {};
const matrix = {};
projectData.appData.products.forEach((prod) => {
prod.projects.forEach((pr) => {
allProjects.push({ ...pr, productName: prod.title });
totalProjects++;
matrix[pr.id] = {};
(pr.dependencies || []).forEach((depId) => {
totalDeps++;
if (!ProductManager.findProjectById(depId)) {
brokenDeps++;
} else {
inDeps[depId] = (inDeps[depId] || 0) + 1;
matrix[pr.id][depId] = true;
}
typeCounts[pr.type] = (typeCounts[pr.type] || 0) + 1;
});
});
});
const bottlenecks = Object.entries(inDeps)
.filter(([k, v]) => v >= 2)
.map(([id, count]) => ({
project: ProductManager.findProjectById(id),
count,
}))
.sort((a, b) => b.count - a.count);
const isolated = allProjects.filter(
(pr) =>
(pr.dependencies || []).length === 0 && !(inDeps[pr.id] > 0),
);
return {
totalDeps,
totalProjects,
brokenDeps,
bottlenecks,
isolated,
typeCounts,
matrix,
allProjects,
inDeps,
};
},
render() {
const c = document.getElementById("dependencyAnalysisContainer");
if (!c) return;
const d = this.getData();
let h = '<div class="dashboard-kpi-row">';
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔗</div><div class="dashboard-kpi-value">${d.totalDeps}</div><div class="dashboard-kpi-label">کل وابستگی‌ها</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📦</div><div class="dashboard-kpi-value">${d.totalProjects}</div><div class="dashboard-kpi-label">پروژه‌ها</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">⚠️</div><div class="dashboard-kpi-value">${d.bottlenecks.length}</div><div class="dashboard-kpi-label">گلوگاه</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔴</div><div class="dashboard-kpi-value">${d.brokenDeps}</div><div class="dashboard-kpi-label">شکسته</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔵</div><div class="dashboard-kpi-value">${d.isolated.length}</div><div class="dashboard-kpi-label">ایزوله</div></div>`;
h += "</div>";
h += '<div class="dashboard-charts-grid">';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 گلوگاه‌ها (بیشترین وابستگی ورودی)</div><div class="dashboard-chart-body" id="depHubChart"></div></div>';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🥧 توزیع نوع پروژه‌های وابسته</div><div class="dashboard-chart-body" id="depTypeChart"></div></div>';
h +=
'<div class="dashboard-chart-card" style="grid-column:span 2"><div class="dashboard-chart-header">🟥 ماتریس وابستگی</div><div class="dashboard-chart-body" id="depMatrixChart" style="overflow:auto;max-height:300px"></div></div>';
h +=
'<div class="dashboard-chart-card" style="grid-column:span 2"><div class="dashboard-chart-header">⚠️ لیست ریسک‌ها</div><div class="dashboard-chart-body" id="depRiskList" style="align-items:flex-start;overflow-y:auto;max-height:300px;width:100%"></div></div>';
h += "</div>";
c.innerHTML = h;
this.renderHubChart("depHubChart", d.bottlenecks);
this.renderTypeDonut("depTypeChart", d.typeCounts);
this.renderMatrix("depMatrixChart", d.allProjects, d.matrix);
this.renderRiskList("depRiskList", d);
document.getElementById("depAnalysisFooterStats").innerHTML =
`<span>🔗 کل: <span class="stat-highlight">${d.totalDeps}</span></span>` +
`<span>⚠️ گلوگاه: <span class="stat-highlight">${d.bottlenecks.length}</span></span>` +
`<span>🔴 شکسته: <span class="stat-highlight">${d.brokenDeps}</span></span>` +
`<span>🔵 ایزوله: <span class="stat-highlight">${d.isolated.length}</span></span>`;
document.getElementById("depAnalysisRefresh").onclick = () =>
this.render();
},
renderHubChart(cid, bottlenecks) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
if (!bottlenecks || bottlenecks.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📊</span><div>گلوگاهی نیست</div></div>';
return;
}
const w = 320,
hgt = Math.max(120, bottlenecks.length * 40 + 40);
const m = { top: 10, right: 35, bottom: 10, left: 130 };
const iw = w - m.left - m.right,
ih = hgt - m.top - m.bottom;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${m.left},${m.top})`);
const x = d3
.scaleLinear()
.domain([0, d3.max(bottlenecks, (d) => d.count) || 1])
.range([0, iw]);
const y = d3
.scaleBand()
.domain(bottlenecks.map((d) => (d.project ? d.project.title : "")))
.range([0, ih])
.padding(0.3);
g.selectAll("rect")
.data(bottlenecks)
.join("rect")
.attr("x", 0)
.attr("y", (d) => y(d.project ? d.project.title : ""))
.attr("width", (d) => x(d.count))
.attr("height", y.bandwidth())
.attr("fill", (d) => (d.count >= 3 ? "#e53e3e" : "#dd6b20"))
.attr("rx", 4)
.style("cursor", "pointer")
.on("mouseover", function (e, d) {
d3.select(this).attr("fill", "var(--accent)");
showD3Tooltip(
e,
`${d.project ? d.project.title : ""}: ${d.count} وابستگی ورودی`,
);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function (e, d) {
d3.select(this).attr(
"fill",
d.count >= 3 ? "#e53e3e" : "#dd6b20",
);
hideD3Tooltip();
});
g.selectAll(".label")
.data(bottlenecks)
.join("text")
.attr("x", -8)
.attr(
"y",
(d) => y(d.project ? d.project.title : "") + y.bandwidth() / 2,
)
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.attr("fill", "var(--text)")
.attr("font-size", "10px")
.attr("font-weight", "600")
.text((d) => {
const t = d.project ? d.project.title : "";
return t.length > 18 ? t.substring(0, 18) + "..." : t;
});
g.selectAll(".value")
.data(bottlenecks)
.join("text")
.attr("x", (d) => x(d.count) + 5)
.attr(
"y",
(d) => y(d.project ? d.project.title : "") + y.bandwidth() / 2,
)
.attr("dy", "0.35em")
.attr("fill", "var(--text-muted)")
.attr("font-size", "10px")
.attr("font-weight", "700")
.text((d) => d.count);
},
renderTypeDonut(cid, typeCounts) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
const entries = Object.entries(typeCounts);
if (entries.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>🥧</span><div>داده‌ای نیست</div></div>';
return;
}
const w = 280,
hgt = 220,
r = Math.min(w, hgt) / 2 - 15;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${w / 2},${hgt / 2})`);
const pie = d3
.pie()
.value((d) => d[1])
.sort(null);
const arc = d3
.arc()
.innerRadius(r * 0.55)
.outerRadius(r);
const arcH = d3
.arc()
.innerRadius(r * 0.55)
.outerRadius(r + 8);
const total = entries.reduce((s, d) => s + d[1], 0);
const colors = {
backend: "#4299e1",
frontend: "#48bb78",
service: "#ecc94b",
database: "#ed64a6",
mobile: "#9f7aea",
infrastructure: "#ed8936",
};
g.selectAll("path")
.data(pie(entries))
.join("path")
.attr("d", arc)
.attr("fill", (d) => colors[d.data[0]] || "#718096")
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.style("cursor", "pointer")
.on("mouseover", function (e, d) {
d3.select(this).transition().duration(150).attr("d", arcH(d));
const ti = VALID_PROJECT_TYPES.find((t) => t.value === d.data[0]);
showD3Tooltip(
e,
`${ti ? ti.label : d.data[0]}: ${d.data[1]} (${Math.round((d.data[1] / total) * 100)}%)`,
);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function () {
d3.select(this).transition().duration(150).attr("d", arc);
hideD3Tooltip();
});
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "-.2em")
.attr("fill", "var(--text)")
.attr("font-size", "24px")
.attr("font-weight", "800")
.text(total);
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "1.4em")
.attr("fill", "var(--text-muted)")
.attr("font-size", "11px")
.text("وابستگی");
},
renderMatrix(cid, allProjects, matrix) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
if (!allProjects || allProjects.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>🟥</span><div>داده‌ای نیست</div></div>';
return;
}
const cs = 28,
lw = 140;
const w = lw + allProjects.length * cs + 20;
const hgt = 40 + allProjects.length * cs + 20;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg.append("g");
allProjects.forEach((pr, i) => {
g.append("text")
.attr("x", lw - 8)
.attr("y", 40 + i * cs + cs / 2)
.attr("text-anchor", "end")
.attr("fill", "var(--text)")
.attr("font-size", "9px")
.attr("font-family", "Vazirmatn")
.text(
pr.title.length > 20
? pr.title.substring(0, 20) + "..."
: pr.title,
);
});
allProjects.forEach((pr, i) => {
g.append("text")
.attr("x", lw + i * cs + cs / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("fill", "var(--text-muted)")
.attr("font-size", "8px")
.attr("font-family", "Vazirmatn")
.attr("transform", `rotate(-45, ${lw + i * cs + cs / 2}, 30)`)
.text(
pr.title.length > 12
? pr.title.substring(0, 12) + "..."
: pr.title,
);
});
allProjects.forEach((row, i) => {
allProjects.forEach((col, j) => {
const hasDep = matrix[row.id] && matrix[row.id][col.id];
g.append("rect")
.attr("x", lw + j * cs)
.attr("y", 40 + i * cs)
.attr("width", cs - 2)
.attr("height", cs - 2)
.attr("rx", 3)
.attr("fill", hasDep ? "#e53e3e" : "var(--border-light)")
.attr("opacity", hasDep ? 0.9 : 0.5)
.style("cursor", hasDep ? "pointer" : "default")
.on(
"mouseover",
hasDep
? (e) => showD3Tooltip(e, `${row.title} → ${col.title}`)
: null,
)
.on("mousemove", hasDep ? (e) => showD3Tooltip(e, "") : null)
.on("mouseout", hasDep ? () => hideD3Tooltip() : null);
});
});
},
renderRiskList(cid, d) {
const c = document.getElementById(cid);
if (!c) return;
let h = "";
if (d.brokenDeps > 0) {
h += `<div style="padding:10px 12px;background:rgba(229,62,62,.08);border-radius:6px;margin-bottom:8px;border-right:4px solid #e53e3e"><div style="font-weight:700;font-size:12px">🔴 وابستگی‌های شکسته</div><div style="font-size:11px;color:var(--text-muted)">${d.brokenDeps} وابستگی به پروژه‌های ناموجود</div></div>`;
}
d.bottlenecks.forEach((b) => {
h += `<div style="padding:10px 12px;background:rgba(221,107,32,.08);border-radius:6px;margin-bottom:8px;border-right:4px solid #dd6b20"><div style="font-weight:700;font-size:12px">⚠️ گلوگاه: ${b.project ? b.project.title : ""}</div><div style="font-size:11px;color:var(--text-muted)">${b.count} پروژه به این پروژه وابسته‌اند - ریسک بالا در صورت تغییر</div></div>`;
});
if (d.isolated.length > 0) {
h += `<div style="padding:10px 12px;background:rgba(49,130,206,.08);border-radius:6px;margin-bottom:8px;border-right:4px solid #3182ce"><div style="font-weight:700;font-size:12px">🔵 پروژه‌های ایزوله</div><div style="font-size:11px;color:var(--text-muted)">${d.isolated.map((p) => p.title).join("، ")}</div></div>`;
}
if (!h)
h =
'<div class="dashboard-empty-chart"><span>✅</span><div>هیچ ریسکی شناسایی نشد</div></div>';
c.innerHTML = h;
},
};
const DeploymentAnalysisManager = {
open() {
ModalManager.open("deploymentAnalysisModal");
setTimeout(() => this.render(), 150);
},
getData() {
let total = 0,
pwd = 0,
pwod = 0,
withNotes = 0,
withoutNotes = 0;
const edc = {};
const ppc = [];
const riskList = [];
const now = new Date();
const thirtyDaysAgo = new Date(
now.getTime() - 30 * 24 * 60 * 60 * 1000,
);
projectData.appData.products.forEach((prod) => {
let prodDeployCount = 0;
prod.projects.forEach((pr) => {
const active = ProductManager.getActiveDeployments(pr);
if (active.length > 0) pwd++;
else {
pwod++;
riskList.push({
type: "no-deploy",
severity: "high",
title: "بدون استقرار",
desc: `${pr.title} در هیچ محیطی استقرار ندارد`,
rec: "نسخه اولیه را در حداقل یک محیط مستقر کنید",
});
}
active.forEach((d) => {
total++;
prodDeployCount++;
if (d.releaseNotes) withNotes++;
else {
withoutNotes++;
riskList.push({
type: "no-notes",
severity: "medium",
title: "بدون ریلیز نوت",
desc: `${pr.title} v${d.version}`,
rec: "ریلیز نوت را تکمیل کنید",
});
}
const env = projectData.appData.environments.find(
(e) => e.id === d.environmentId,
);
const envName = env ? env.name : "نامشخص";
edc[envName] = (edc[envName] || 0) + 1;
if (
d.deploymentDate &&
new Date(d.deploymentDate) < thirtyDaysAgo
) {
const daysOld = Math.floor(
(now - new Date(d.deploymentDate)) / (1000 * 60 * 60 * 24),
);
riskList.push({
type: "stale",
severity: daysOld > 90 ? "high" : "medium",
title: "استقرار قدیمی",
desc: `${pr.title} v${d.version} در ${envName} (${daysOld} روز)`,
rec: "بررسی و به‌روزرسانی نسخه",
});
}
});
});
ppc.push({ title: prod.title, count: prodDeployCount });
});
const coverage =
pwd + pwod > 0 ? Math.round((pwd / (pwd + pwod)) * 100) : 0;
return {
total,
pwd,
pwod,
withNotes,
withoutNotes,
edc,
ppc,
riskList,
coverage,
};
},
render() {
const c = document.getElementById("deploymentAnalysisContainer");
if (!c) return;
const d = this.getData();
let h = '<div class="dashboard-kpi-row">';
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🚀</div><div class="dashboard-kpi-value">${d.total}</div><div class="dashboard-kpi-label">کل استقرارها</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">✅</div><div class="dashboard-kpi-value">${d.pwd}</div><div class="dashboard-kpi-label">با استقرار</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">❌</div><div class="dashboard-kpi-value">${d.pwod}</div><div class="dashboard-kpi-label">بدون استقرار</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📈</div><div class="dashboard-kpi-value">${d.coverage}%</div><div class="dashboard-kpi-label">پوشش استقرار</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📝</div><div class="dashboard-kpi-value">${d.withNotes}</div><div class="dashboard-kpi-label">با ریلیز نوت</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">⚠️</div><div class="dashboard-kpi-value">${d.withoutNotes}</div><div class="dashboard-kpi-label">بدون ریلیز نوت</div></div>`;
h += "</div>";
h += '<div class="dashboard-charts-grid">';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🌍 استقرارها به تفکیک محیط</div><div class="dashboard-chart-body" id="deployEnvChart"></div></div>';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">✅ وضعیت استقرار پروژه‌ها</div><div class="dashboard-chart-body" id="deployStatusChart"></div></div>';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 استقرارها به تفکیک محصول</div><div class="dashboard-chart-body" id="deployProductChart"></div></div>';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">⚠️ لیست ریسک‌ها</div><div class="dashboard-chart-body" id="deployRiskList" style="align-items:flex-start;overflow-y:auto;max-height:280px;width:100%"></div></div>';
h += "</div>";
c.innerHTML = h;
this.renderEnvBarChart("deployEnvChart", d.edc);
this.renderStatusDonut(
"deployStatusChart",
d.pwd,
d.pwod,
d.coverage,
);
this.renderProductBarChart("deployProductChart", d.ppc);
this.renderRiskList("deployRiskList", d.riskList);
document.getElementById("deployAnalysisFooterStats").innerHTML =
`<span>🚀 کل: <span class="stat-highlight">${d.total}</span></span>` +
`<span>📈 پوشش: <span class="stat-highlight">${d.coverage}%</span></span>` +
`<span>⚠️ ریسک‌ها: <span class="stat-highlight">${d.riskList.length}</span></span>`;
document.getElementById("deployAnalysisRefresh").onclick = () =>
this.render();
},
renderEnvBarChart(cid, edc) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
const data = Object.entries(edc).map(([k, v]) => ({
label: k,
value: v,
}));
if (data.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
return;
}
const w = 320,
hgt = 220,
m = { top: 20, right: 15, bottom: 45, left: 30 };
const iw = w - m.left - m.right,
ih = hgt - m.top - m.bottom;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${m.left},${m.top})`);
const x = d3
.scaleBand()
.domain(data.map((d) => d.label))
.range([0, iw])
.padding(0.3);
const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value) || 1])
.nice()
.range([ih, 0]);
g.append("g")
.attr("transform", `translate(0,${ih})`)
.call(d3.axisBottom(x).tickSize(0))
.selectAll("text")
.attr("fill", "var(--text-muted)")
.attr("font-size", "8px")
.attr("font-family", "Vazirmatn")
.attr("transform", "rotate(-20)")
.attr("text-anchor", "end");
g.append("g")
.call(d3.axisLeft(y).ticks(4).tickSize(-iw))
.selectAll("text")
.attr("fill", "var(--text-muted)")
.attr("font-size", "9px");
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d) => x(d.label))
.attr("y", (d) => y(d.value))
.attr("width", x.bandwidth())
.attr("height", (d) => ih - y(d.value))
.attr("fill", "#38a169")
.attr("rx", 4)
.style("cursor", "pointer")
.on("mouseover", function (e, d) {
d3.select(this).attr("fill", "var(--accent)");
showD3Tooltip(e, `${d.label}: ${d.value}`);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function () {
d3.select(this).attr("fill", "#38a169");
hideD3Tooltip();
});
g.selectAll(".bar-label")
.data(data)
.join("text")
.attr("x", (d) => x(d.label) + x.bandwidth() / 2)
.attr("y", (d) => y(d.value) - 5)
.attr("text-anchor", "middle")
.attr("fill", "var(--text)")
.attr("font-size", "10px")
.attr("font-weight", "700")
.text((d) => d.value);
},
renderStatusDonut(cid, pwd, pwod, coverage) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
const total = pwd + pwod;
if (total === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
return;
}
const w = 280,
hgt = 220,
r = Math.min(w, hgt) / 2 - 15;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${w / 2},${hgt / 2})`);
const pie = d3
.pie()
.value((d) => d.value)
.sort(null);
const arc = d3
.arc()
.innerRadius(r * 0.55)
.outerRadius(r);
const arcH = d3
.arc()
.innerRadius(r * 0.55)
.outerRadius(r + 8);
const data = [
{ label: "با استقرار", value: pwd, color: "#38a169" },
{ label: "بدون استقرار", value: pwod, color: "#e53e3e" },
].filter((d) => d.value > 0);
g.selectAll("path")
.data(pie(data))
.join("path")
.attr("d", arc)
.attr("fill", (d) => d.data.color)
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.style("cursor", "pointer")
.on("mouseover", function (e, d) {
d3.select(this).transition().duration(150).attr("d", arcH(d));
showD3Tooltip(
e,
`${d.data.label}: ${d.data.value} (${Math.round((d.data.value / total) * 100)}%)`,
);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function () {
d3.select(this).transition().duration(150).attr("d", arc);
hideD3Tooltip();
});
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "-.2em")
.attr(
"fill",
coverage >= 70
? "#38a169"
: coverage >= 40
? "#dd6b20"
: "#e53e3e",
)
.attr("font-size", "24px")
.attr("font-weight", "800")
.text(`${coverage}%`);
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "1.4em")
.attr("fill", "var(--text-muted)")
.attr("font-size", "11px")
.text("پوشش استقرار");
},
renderProductBarChart(cid, ppc) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
const data = ppc.map((p) => ({
label:
p.title.length > 15 ? p.title.substring(0, 15) + "..." : p.title,
value: p.count,
}));
if (data.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
return;
}
const bh = 28,
gap = 6;
const w = 320,
hgt = data.length * (bh + gap) + 30;
const m = { top: 10, right: 35, bottom: 10, left: 130 };
const iw = w - m.left - m.right,
ih = hgt - m.top - m.bottom;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${m.left},${m.top})`);
const x = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value) || 1])
.range([0, iw]);
const y = d3
.scaleBand()
.domain(data.map((d) => d.label))
.range([0, ih])
.padding(0.2);
g.selectAll("rect")
.data(data)
.join("rect")
.attr("x", 0)
.attr("y", (d) => y(d.label))
.attr("width", (d) => x(d.value))
.attr("height", y.bandwidth())
.attr("fill", "#3182ce")
.attr("rx", 4)
.style("cursor", "pointer")
.on("mouseover", function (e, d) {
d3.select(this).attr("fill", "var(--accent)");
showD3Tooltip(e, `${d.label}: ${d.value}`);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function () {
d3.select(this).attr("fill", "#3182ce");
hideD3Tooltip();
});
g.selectAll(".label")
.data(data)
.join("text")
.attr("x", -8)
.attr("y", (d) => y(d.label) + y.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.attr("fill", "var(--text)")
.attr("font-size", "9px")
.attr("font-weight", "600")
.text((d) => d.label);
g.selectAll(".value")
.data(data)
.join("text")
.attr("x", (d) => x(d.value) + 5)
.attr("y", (d) => y(d.label) + y.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("fill", "var(--text-muted)")
.attr("font-size", "10px")
.attr("font-weight", "700")
.text((d) => d.value);
},
renderRiskList(cid, riskList) {
const c = document.getElementById(cid);
if (!c) return;
if (riskList.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>✅</span><div>هیچ ریسکی شناسایی نشد</div></div>';
return;
}
const colors = {
critical: "#e53e3e",
high: "#dd6b20",
medium: "#d69e2e",
};
let h = "";
riskList.forEach((r) => {
h += `<div style="padding:10px 12px;background:rgba(0,0,0,.05);border-radius:6px;margin-bottom:8px;border-right:4px solid ${colors[r.severity] || "#3182ce"}">`;
h += `<div style="font-weight:700;font-size:12px">${r.title}</div>`;
h += `<div style="font-size:11px;color:var(--text-muted)">${r.desc}</div>`;
h += `<div style="font-size:10px;color:var(--info);margin-top:4px">💡 ${r.rec}</div>`;
h += "</div>";
});
c.innerHTML = h;
},
};
const VersionAnalysisManager = {
open() {
ModalManager.open("versionAnalysisModal");
setTimeout(() => this.render(), 150);
},
/* ── جمع‌آوری داده‌های جامع نسخه‌ها ── */
getData() {
let total = 0;
const uniqueVersions = new Set();
const majorCounts = {};
const envCounts = {};
const envMatrix = {};
const riskList = [];
const now = new Date();
const thirtyDaysAgo = new Date(
now.getTime() - 30 * 24 * 60 * 60 * 1000,
);
const ninetyDaysAgo = new Date(
now.getTime() - 90 * 24 * 60 * 60 * 1000,
);
const prereleaseRegex = /-(rc|beta|dev|alpha)/i;
// مقداردهی اولیه محیط‌ها
projectData.appData.environments.forEach((e) => {
envCounts[e.name] = 0;
});
projectData.appData.products.forEach((prod) => {
(prod.projects || []).forEach((pr) => {
const active = ProductManager.getActiveDeployments(pr);
// مقداردهی اولیه ماتریس
envMatrix[pr.title] = {};
active.forEach((d) => {
total++;
uniqueVersions.add(d.version);
// استخراج Major
const major = parseInt(d.version.split(".")[0]) || 0;
const majorKey = `v${major}.x`;
majorCounts[majorKey] = (majorCounts[majorKey] || 0) + 1;
// شمارش به تفکیک محیط
const env = projectData.appData.environments.find(
(e) => e.id === d.environmentId,
);
const envName = env ? env.name : "نامشخص";
envCounts[envName] = (envCounts[envName] || 0) + 1;
// پر کردن ماتریس
envMatrix[pr.title][d.environmentId] = d.version;
// ریسک ۱: نسخه‌های بسیار قدیمی (+۹۰ روز)
if (
d.deploymentDate &&
new Date(d.deploymentDate) < ninetyDaysAgo
) {
const daysOld = Math.floor(
(now - new Date(d.deploymentDate)) / (1000 * 60 * 60 * 24),
);
riskList.push({
type: "stale-critical",
severity: "high",
icon: "⏰",
title: "نسخه بسیار قدیمی",
desc: `${pr.title} v${d.version} در ${envName} (${daysOld} روز)`,
rec: "فوراً بررسی و به‌روزرسانی کنید",
});
}
// ریسک ۲: نسخه‌های قدیمی (۳۰-۹۰ روز)
else if (
d.deploymentDate &&
new Date(d.deploymentDate) < thirtyDaysAgo
) {
const daysOld = Math.floor(
(now - new Date(d.deploymentDate)) / (1000 * 60 * 60 * 24),
);
riskList.push({
type: "stale-medium",
severity: "medium",
icon: "⏰",
title: "نسخه قدیمی",
desc: `${pr.title} v${d.version} در ${envName} (${daysOld} روز)`,
rec: "برنامه‌ریزی به‌روزرسانی",
});
}
// ریسک ۳: بدون ریلیز نوت
if (!d.releaseNotes) {
riskList.push({
type: "no-notes",
severity: "medium",
icon: "📝",
title: "بدون ریلیز نوت",
desc: `${pr.title} v${d.version} در ${envName}`,
rec: "ریلیز نوت را تکمیل کنید",
});
}
// ریسک ۴: نسخه پیش‌نویس در Production
if (prereleaseRegex.test(d.version)) {
if (d.environmentId === "env-prod-001") {
riskList.push({
type: "prerelease-prod",
severity: "high",
icon: "🧪",
title: "نسخه پیش‌نویس در Production",
desc: `${pr.title} v${d.version} در ${envName}`,
rec: "نسخه پایدار را در Production مستقر کنید",
});
} else {
riskList.push({
type: "prerelease-other",
severity: "low",
icon: "🧪",
title: "نسخه پیش‌نویس",
desc: `${pr.title} v${d.version} در ${envName}`,
rec: "پس از تست، نسخه پایدار را منتشر کنید",
});
}
}
});
});
});
// محاسبه میانگین نسخه
let avgMajor = 0,
avgMinor = 0,
avgPatch = 0;
if (uniqueVersions.size > 0) {
let sumMajor = 0,
sumMinor = 0,
sumPatch = 0;
uniqueVersions.forEach((v) => {
const parts = v.split(".");
sumMajor += parseInt(parts[0]) || 0;
sumMinor += parseInt(parts[1]) || 0;
sumPatch += parseInt(parts[2]) || 0;
});
avgMajor = Math.round(sumMajor / uniqueVersions.size);
avgMinor = Math.round(sumMinor / uniqueVersions.size);
avgPatch = Math.round(sumPatch / uniqueVersions.size);
}
const prereleaseCount = [...uniqueVersions].filter((v) =>
prereleaseRegex.test(v),
).length;
return {
total,
uniqueVersions: uniqueVersions.size,
avgVersion: `${avgMajor}.${avgMinor}.${avgPatch}`,
prereleaseCount,
majorCounts,
envCounts,
envMatrix,
riskList,
};
},
/* ── رندر اصلی مودال ── */
render() {
const c = document.getElementById("versionAnalysisContainer");
if (!c) return;
const d = this.getData();
// ۵ کارت KPI
let h = '<div class="dashboard-kpi-row">';
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔢</div><div class="dashboard-kpi-value">${d.total}</div><div class="dashboard-kpi-label">کل نسخه‌ها</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📦</div><div class="dashboard-kpi-value">${d.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" style="font-size:20px">${d.avgVersion}</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.prereleaseCount}</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.riskList.length}</div><div class="dashboard-kpi-label">ریسک‌ها</div></div>`;
h += "</div>";
// نمودارها
h += '<div class="dashboard-charts-grid">';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 توزیع نسخه‌ها بر اساس Major</div><div class="dashboard-chart-body" id="verMajorChart"></div></div>';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🌍 نسخه‌ها به تفکیک محیط</div><div class="dashboard-chart-body" id="verEnvChart"></div></div>';
h +=
'<div class="dashboard-chart-card" style="grid-column:span 2"><div class="dashboard-chart-header">📋 مقایسه نسخه‌ها بین محیط‌ها</div><div class="dashboard-chart-body" id="verEnvMatrix" style="overflow:auto;max-height:300px;width:100%"></div></div>';
h +=
'<div class="dashboard-chart-card" style="grid-column:span 2"><div class="dashboard-chart-header">⚠️ لیست ریسک‌ها</div><div class="dashboard-chart-body" id="verRiskList" style="align-items:flex-start;overflow-y:auto;max-height:300px;width:100%"></div></div>';
h += "</div>";
c.innerHTML = h;
this.renderMajorBarChart("verMajorChart", d.majorCounts);
this.renderEnvBarChart("verEnvChart", d.envCounts);
this.renderEnvMatrix("verEnvMatrix", d.envMatrix);
this.renderRiskList("verRiskList", d.riskList);
document.getElementById("verAnalysisFooterStats").innerHTML =
`<span>🔢 کل: <span class="stat-highlight">${d.total}</span></span>` +
`<span>📦 یکتا: <span class="stat-highlight">${d.uniqueVersions}</span></span>` +
`<span>📈 میانگین: <span class="stat-highlight">${d.avgVersion}</span></span>` +
`<span>⚠️ ریسک‌ها: <span class="stat-highlight">${d.riskList.length}</span></span>`;
document.getElementById("verAnalysisRefresh").onclick = () =>
this.render();
},
/* ── نمودار میله‌ای توزیع بر اساس Major ── */
renderMajorBarChart(cid, majorCounts) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
const entries = Object.entries(majorCounts).sort((a, b) =>
a[0].localeCompare(b[0]),
);
if (entries.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
return;
}
const w = 320,
hgt = 220,
m = { top: 20, right: 15, bottom: 40, left: 30 };
const iw = w - m.left - m.right,
ih = hgt - m.top - m.bottom;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${m.left},${m.top})`);
const x = d3
.scaleBand()
.domain(entries.map((x) => x[0]))
.range([0, iw])
.padding(0.3);
const y = d3
.scaleLinear()
.domain([0, d3.max(entries, (x) => x[1]) || 1])
.nice()
.range([ih, 0]);
const colors = [
"#4299e1",
"#48bb78",
"#ecc94b",
"#ed64a6",
"#9f7aea",
"#ed8936",
];
g.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");
g.append("g")
.call(d3.axisLeft(y).ticks(4).tickSize(-iw))
.selectAll("text")
.attr("fill", "var(--text-muted)")
.attr("font-size", "9px");
g.selectAll("rect")
.data(entries)
.join("rect")
.attr("x", (x) => x(x[0]))
.attr("y", (x) => y(x[1]))
.attr("width", x.bandwidth())
.attr("height", (x) => ih - y(x[1]))
.attr("fill", (x, i) => colors[i % colors.length])
.attr("rx", 4)
.style("cursor", "pointer")
.on("mouseover", function (e, x) {
d3.select(this).attr("fill", "var(--accent)");
showD3Tooltip(e, `${x[0]}: ${x[1]}`);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function (e, x) {
d3.select(this).attr(
"fill",
colors[entries.indexOf(x) % colors.length],
);
hideD3Tooltip();
});
g.selectAll(".bar-label")
.data(entries)
.join("text")
.attr("x", (x) => x(x[0]) + x.bandwidth() / 2)
.attr("y", (x) => y(x[1]) - 5)
.attr("text-anchor", "middle")
.attr("fill", "var(--text)")
.attr("font-size", "10px")
.attr("font-weight", "700")
.text((x) => x[1]);
},
/* ── نمودار میله‌ای نسخه‌ها به تفکیک محیط ── */
renderEnvBarChart(cid, envCounts) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
const entries = Object.entries(envCounts).filter((x) => x[1] > 0);
if (entries.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📊</span><div>داده‌ای نیست</div></div>';
return;
}
const w = 320,
hgt = 220,
m = { top: 20, right: 15, bottom: 45, left: 30 };
const iw = w - m.left - m.right,
ih = hgt - m.top - m.bottom;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${m.left},${m.top})`);
const x = d3
.scaleBand()
.domain(entries.map((x) => x[0]))
.range([0, iw])
.padding(0.3);
const y = d3
.scaleLinear()
.domain([0, d3.max(entries, (x) => x[1]) || 1])
.nice()
.range([ih, 0]);
g.append("g")
.attr("transform", `translate(0,${ih})`)
.call(d3.axisBottom(x).tickSize(0))
.selectAll("text")
.attr("fill", "var(--text-muted)")
.attr("font-size", "8px")
.attr("font-family", "Vazirmatn")
.attr("transform", "rotate(-20)")
.attr("text-anchor", "end");
g.append("g")
.call(d3.axisLeft(y).ticks(4).tickSize(-iw))
.selectAll("text")
.attr("fill", "var(--text-muted)")
.attr("font-size", "9px");
g.selectAll("rect")
.data(entries)
.join("rect")
.attr("x", (x) => x(x[0]))
.attr("y", (x) => y(x[1]))
.attr("width", x.bandwidth())
.attr("height", (x) => ih - y(x[1]))
.attr("fill", "#38a169")
.attr("rx", 4)
.style("cursor", "pointer")
.on("mouseover", function (e, x) {
d3.select(this).attr("fill", "var(--accent)");
showD3Tooltip(e, `${x[0]}: ${x[1]}`);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function () {
d3.select(this).attr("fill", "#38a169");
hideD3Tooltip();
});
g.selectAll(".bar-label")
.data(entries)
.join("text")
.attr("x", (x) => x(x[0]) + x.bandwidth() / 2)
.attr("y", (x) => y(x[1]) - 5)
.attr("text-anchor", "middle")
.attr("fill", "var(--text)")
.attr("font-size", "10px")
.attr("font-weight", "700")
.text((x) => x[1]);
},
/* ── جدول مقایسه نسخه‌ها بین محیط‌ها ── */
renderEnvMatrix(cid, envMatrix) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
const projects = Object.keys(envMatrix);
if (projects.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>📋</span><div>داده‌ای نیست</div></div>';
return;
}
const envs = projectData.appData.environments || [];
let html =
'<table style="width:100%;border-collapse:collapse;font-size:11px">';
html += '<thead><tr style="background:var(--border-light)">';
html +=
'<th style="padding:8px;text-align:right;border-bottom:2px solid var(--border);font-weight:700">پروژه</th>';
envs.forEach((env) => {
html += `<th style="padding:8px;text-align:center;border-bottom:2px solid var(--border);font-weight:700"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${env.backgroundColor};margin-left:4px"></span>${env.name}</th>`;
});
html += "</tr></thead><tbody>";
projects.forEach((prTitle) => {
html += `<tr style="border-bottom:1px solid var(--border-light)">`;
html += `<td style="padding:6px 8px;font-weight:600">${prTitle}</td>`;
envs.forEach((env) => {
const version = envMatrix[prTitle][env.id];
if (version) {
html += `<td style="padding:6px 8px;text-align:center"><span style="font-family:monospace;font-weight:700;color:var(--primary)">v${version}</span></td>`;
} else {
html +=
'<td style="padding:6px 8px;text-align:center;color:var(--text-muted)">—</td>';
}
});
html += "</tr>";
});
html += "</tbody></table>";
c.innerHTML = html;
},
/* ── لیست ریسک‌ها (مرتب بر اساس شدت) ── */
renderRiskList(cid, riskList) {
const c = document.getElementById(cid);
if (!c) return;
if (riskList.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>✅</span><div>هیچ ریسکی شناسایی نشد</div></div>';
return;
}
const severityOrder = { high: 0, medium: 1, low: 2 };
const sorted = [...riskList].sort(
(a, b) => severityOrder[a.severity] - severityOrder[b.severity],
);
const colors = { high: "#dd6b20", medium: "#d69e2e", low: "#3182ce" };
let h = "";
sorted.forEach((r) => {
h += `<div style="padding:10px 12px;background:rgba(0,0,0,.05);border-radius:6px;margin-bottom:8px;border-right:4px solid ${colors[r.severity] || "#3182ce"}">`;
h += `<div style="font-weight:700;font-size:12px">${r.icon} ${r.title}</div>`;
h += `<div style="font-size:11px;color:var(--text-muted)">${r.desc}</div>`;
h += `<div style="font-size:10px;color:var(--info);margin-top:4px">💡 ${r.rec}</div>`;
h += "</div>";
});
c.innerHTML = h;
},
};
const RiskAnalysisManager = {
open() {
ModalManager.open("riskAnalysisModal");
setTimeout(() => this.render(), 150);
},
/* ── تحلیل جامع ریسک‌ها (۸ نوع) ── */
analyzeRisks() {
const risks = [];
const now = new Date();
const thirtyDaysAgo = new Date(
now.getTime() - 30 * 24 * 60 * 60 * 1000,
);
const ninetyDaysAgo = new Date(
now.getTime() - 90 * 24 * 60 * 60 * 1000,
);
const envs = projectData.appData.environments || [];
const envIdsWithDeployment = new Set();
projectData.appData.products.forEach((prod) => {
// ۱. بدون مالک (medium)
if (!prod.metadata?.owner) {
risks.push({
type: "no-owner",
severity: "medium",
icon: "👤",
title: "بدون مالک",
desc: `محصول "${prod.title}" مالک مشخصی ندارد`,
rec: "مالک محصول را مشخص کنید",
});
}
(prod.projects || []).forEach((pr) => {
// ۲. وابستگی شکسته (critical)
(pr.dependencies || []).forEach((depId) => {
if (!ProductManager.findProjectById(depId)) {
risks.push({
type: "broken",
severity: "critical",
icon: "🔴",
title: "وابستگی شکسته",
desc: `${pr.title} به پروژه ناموجود (${depId}) وابسته است`,
rec: "وابستگی را حذف یا پروژه مقصد را ایجاد کنید",
});
}
});
const active = ProductManager.getActiveDeployments(pr);
// ۳. بدون استقرار (high)
if (active.length === 0) {
risks.push({
type: "no-deploy",
severity: "high",
icon: "❌",
title: "بدون استقرار",
desc: `${pr.title} در هیچ محیطی استقرار ندارد`,
rec: "نسخه اولیه را در حداقل یک محیط مستقر کنید",
});
}
active.forEach((d) => {
envIdsWithDeployment.add(d.environmentId);
// ۴. نیازمندی شکسته (critical)
if (d.requiredDeployments && d.requiredDeployments.length > 0) {
d.requiredDeployments.forEach((rd) => {
if (!ProductManager.findProjectById(rd.projectId)) {
risks.push({
type: "broken-req",
severity: "critical",
icon: "🔴",
title: "نیازمندی شکسته",
desc: `${pr.title} به پروژه ناموجود در نیازمندی‌ها ارجاع دارد`,
rec: "نیازمندی را بررسی و اصلاح کنید",
});
}
});
}
// ۵. بدون ریلیز نوت (medium)
if (!d.releaseNotes) {
const envName = ProductManager.getEnvironmentName(
d.environmentId,
);
risks.push({
type: "no-notes",
severity: "medium",
icon: "📝",
title: "بدون ریلیز نوت",
desc: `${pr.title} v${d.version} در ${envName}`,
rec: "ریلیز نوت را تکمیل کنید",
});
}
// ۶ و ۷. استقرار قدیمی
if (d.deploymentDate) {
const depDate = new Date(d.deploymentDate);
const daysOld = Math.floor(
(now - depDate) / (1000 * 60 * 60 * 24),
);
const envName = ProductManager.getEnvironmentName(
d.environmentId,
);
if (depDate < ninetyDaysAgo) {
risks.push({
type: "stale-critical",
severity: "high",
icon: "⏰",
title: "استقرار بسیار قدیمی",
desc: `${pr.title} v${d.version} در ${envName} (${daysOld} روز)`,
rec: "فوراً بررسی و به‌روزرسانی کنید",
});
} else if (depDate < thirtyDaysAgo) {
risks.push({
type: "stale-medium",
severity: "medium",
icon: "⏰",
title: "استقرار قدیمی",
desc: `${pr.title} v${d.version} در ${envName} (${daysOld} روز)`,
rec: "برنامه‌ریزی به‌روزرسانی",
});
}
}
});
});
});
// ۸. محیط خالی (low)
envs.forEach((env) => {
if (!envIdsWithDeployment.has(env.id)) {
risks.push({
type: "empty-environment",
severity: "low",
icon: "🌍",
title: "محیط خالی",
desc: `محیط "${env.name}" هیچ استقراری ندارد`,
rec: "در صورت عدم نیاز، محیط را حذف کنید",
});
}
});
// شمارش بر اساس شدت
const crit = risks.filter((r) => r.severity === "critical").length;
const high = risks.filter((r) => r.severity === "high").length;
const med = risks.filter((r) => r.severity === "medium").length;
const low = risks.filter((r) => r.severity === "low").length;
// فرمول اصلاح‌شده Health Score (لحاظ ریسک‌های کم)
let healthScore = 100 - crit * 15 - high * 10 - med * 5 - low * 2;
healthScore = Math.max(0, healthScore);
return {
risks,
crit,
high,
med,
low,
healthScore,
total: risks.length,
};
},
/* ── رندر اصلی مودال ── */
render() {
const c = document.getElementById("riskAnalysisContainer");
if (!c) return;
const d = this.analyzeRisks();
// ۵ کارت KPI
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.crit}</div><div class="dashboard-kpi-label">بحرانی</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🟠</div><div class="dashboard-kpi-value">${d.high}</div><div class="dashboard-kpi-label">بالا</div></div>`;
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🟡</div><div class="dashboard-kpi-value">${d.med}</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.low}</div><div class="dashboard-kpi-label">کم</div></div>`;
h += "</div>";
// نمودارها و لیست ریسک‌ها
h += '<div class="dashboard-charts-grid">';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🍩 توزیع شدت ریسک‌ها</div><div class="dashboard-chart-body" id="riskSeverityChart"></div></div>';
h +=
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 توزیع نوع ریسک</div><div class="dashboard-chart-body" id="riskTypeChart"></div></div>';
h +=
'<div class="dashboard-chart-card" style="grid-column:span 2"><div class="dashboard-chart-header">⚠️ لیست ریسک‌ها و پیشنهادات</div><div class="dashboard-chart-body" id="riskDetailList" style="align-items:flex-start;overflow-y:auto;max-height:350px;width:100%"></div></div>';
h += "</div>";
c.innerHTML = h;
this.renderSeverityDonut("riskSeverityChart", d);
this.renderTypeBarChart("riskTypeChart", d.risks);
this.renderRiskList("riskDetailList", d.risks);
document.getElementById("riskAnalysisFooterStats").innerHTML =
`<span>🎯 سلامت: <span class="stat-highlight">${d.healthScore}%</span></span>` +
`<span>⚠️ ریسک‌ها: <span class="stat-highlight">${d.total}</span></span>` +
`<span>🔴 بحرانی: <span class="stat-highlight">${d.crit}</span></span>` +
`<span>🟠 بالا: <span class="stat-highlight">${d.high}</span></span>`;
document.getElementById("riskAnalysisRefresh").onclick = () =>
this.render();
},
/* ── نمودار دونات توزیع شدت ── */
renderSeverityDonut(cid, d) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
const data = [
{ label: "بحرانی", value: d.crit, color: "#e53e3e" },
{ label: "بالا", value: d.high, color: "#dd6b20" },
{ label: "متوسط", value: d.med, color: "#d69e2e" },
{ label: "کم", value: d.low, color: "#3182ce" },
].filter((x) => x.value > 0);
if (data.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>✅</span><div>هیچ ریسکی نیست</div></div>';
return;
}
const w = 280,
hgt = 220,
r = Math.min(w, hgt) / 2 - 15;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${w / 2},${hgt / 2})`);
const pie = d3
.pie()
.value((x) => x.value)
.sort(null);
const arc = d3
.arc()
.innerRadius(r * 0.55)
.outerRadius(r);
const arcH = d3
.arc()
.innerRadius(r * 0.55)
.outerRadius(r + 8);
const total = data.reduce((s, x) => s + x.value, 0);
g.selectAll("path")
.data(pie(data))
.join("path")
.attr("d", arc)
.attr("fill", (x) => x.data.color)
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.style("cursor", "pointer")
.on("mouseover", function (e, x) {
d3.select(this).transition().duration(150).attr("d", arcH(x));
showD3Tooltip(
e,
`${x.data.label}: ${x.data.value} (${Math.round((x.data.value / total) * 100)}%)`,
);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function () {
d3.select(this).transition().duration(150).attr("d", arc);
hideD3Tooltip();
});
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "-.2em")
.attr(
"fill",
d.healthScore >= 70
? "#38a169"
: d.healthScore >= 40
? "#dd6b20"
: "#e53e3e",
)
.attr("font-size", "24px")
.attr("font-weight", "800")
.text(`${d.healthScore}%`);
g.append("text")
.attr("text-anchor", "middle")
.attr("dy", "1.4em")
.attr("fill", "var(--text-muted)")
.attr("font-size", "11px")
.text("امتیاز سلامت");
},
/* ── نمودار میله‌ای افقی توزیع نوع ریسک ── */
renderTypeBarChart(cid, risks) {
const c = document.getElementById(cid);
if (!c) return;
c.innerHTML = "";
const typeLabels = {
broken: "وابستگی شکسته",
"broken-req": "نیازمندی شکسته",
"no-deploy": "بدون استقرار",
"stale-critical": "استقرار بسیار قدیمی",
"stale-medium": "استقرار قدیمی",
"no-notes": "بدون ریلیز نوت",
"no-owner": "بدون مالک",
"empty-environment": "محیط خالی",
};
const typeColors = {
broken: "#e53e3e",
"broken-req": "#e53e3e",
"no-deploy": "#dd6b20",
"stale-critical": "#dd6b20",
"stale-medium": "#d69e2e",
"no-notes": "#d69e2e",
"no-owner": "#d69e2e",
"empty-environment": "#3182ce",
};
const typeCounts = {};
risks.forEach((r) => {
typeCounts[r.type] = (typeCounts[r.type] || 0) + 1;
});
const entries = Object.entries(typeCounts).sort(
(a, b) => b[1] - a[1],
);
if (entries.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>✅</span><div>ریسکی نیست</div></div>';
return;
}
const bh = 28,
gap = 6;
const w = 320,
hgt = entries.length * (bh + gap) + 30;
const m = { top: 10, right: 35, bottom: 10, left: 130 };
const iw = w - m.left - m.right,
ih = hgt - m.top - m.bottom;
const svg = d3
.select(c)
.append("svg")
.attr("width", w)
.attr("height", hgt);
const g = svg
.append("g")
.attr("transform", `translate(${m.left},${m.top})`);
const x = d3
.scaleLinear()
.domain([0, d3.max(entries, (x) => x[1]) || 1])
.range([0, iw]);
const y = d3
.scaleBand()
.domain(entries.map((x) => x[0]))
.range([0, ih])
.padding(0.2);
g.selectAll("rect")
.data(entries)
.join("rect")
.attr("x", 0)
.attr("y", (x) => y(x[0]))
.attr("width", (x) => x(x[1]))
.attr("height", y.bandwidth())
.attr("fill", (x) => typeColors[x[0]] || "#718096")
.attr("rx", 4)
.style("cursor", "pointer")
.on("mouseover", function (e, x) {
d3.select(this).attr("fill", "var(--accent)");
showD3Tooltip(e, `${typeLabels[x[0]] || x[0]}: ${x[1]}`);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function (e, x) {
d3.select(this).attr("fill", typeColors[x[0]] || "#718096");
hideD3Tooltip();
});
g.selectAll(".label")
.data(entries)
.join("text")
.attr("x", -8)
.attr("y", (x) => y(x[0]) + y.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "end")
.attr("fill", "var(--text)")
.attr("font-size", "9px")
.attr("font-weight", "600")
.text((x) => typeLabels[x[0]] || x[0]);
g.selectAll(".value")
.data(entries)
.join("text")
.attr("x", (x) => x(x[1]) + 5)
.attr("y", (x) => y(x[0]) + y.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("fill", "var(--text-muted)")
.attr("font-size", "10px")
.attr("font-weight", "700")
.text((x) => x[1]);
},
/* ── لیست ریسک‌ها با پیشنهاد رفع (مرتب بر اساس شدت) ── */
renderRiskList(cid, risks) {
const c = document.getElementById(cid);
if (!c) return;
if (risks.length === 0) {
c.innerHTML =
'<div class="dashboard-empty-chart"><span>✅</span><div>هیچ ریسکی شناسایی نشد</div></div>';
return;
}
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
const sorted = [...risks].sort(
(a, b) => severityOrder[a.severity] - severityOrder[b.severity],
);
const colors = {
critical: "#e53e3e",
high: "#dd6b20",
medium: "#d69e2e",
low: "#3182ce",
};
let h = "";
sorted.forEach((r) => {
h += `<div style="padding:10px 12px;background:rgba(0,0,0,.05);border-radius:6px;margin-bottom:8px;border-right:4px solid ${colors[r.severity] || "#3182ce"}">`;
h += `<div style="font-weight:700;font-size:12px">${r.icon} ${r.title}</div>`;
h += `<div style="font-size:11px;color:var(--text-muted)">${r.desc}</div>`;
h += `<div style="font-size:10px;color:var(--info);margin-top:4px">💡 ${r.rec}</div>`;
h += "</div>";
});
c.innerHTML = h;
},
};
/* ==================== EVENT BINDINGS ==================== */
document
.getElementById("projectDetailDeleteBtn")
.addEventListener("click", () => {
if (ProductManager.currentDetailProductId)
ProductManager.removeProduct(ProductManager.currentDetailProductId);
});
document
.getElementById("saveProductEditBtn")
.addEventListener("click", () => ProductManager.saveProductEdit());
document
.getElementById("saveNewProjectBtn")
.addEventListener("click", () => ProductManager.saveNewProject());
document
.getElementById("saveProjEditBtn")
.addEventListener("click", () => ProductManager.saveProjectEdit());
document
.getElementById("saveDeploymentBtn")
.addEventListener("click", () => ProductManager.saveDeployment());
document
.getElementById("saveEnvEditBtn")
.addEventListener("click", () => EnvironmentManager.saveEdit());
document
.getElementById("saveNewProductBtn")
.addEventListener("click", () => ProductManager.createNewProduct());
document
.getElementById("saveNewEnvBtn")
.addEventListener("click", () => EnvironmentManager.saveNewEnv());
document
.getElementById("newEnvBgColor")
.addEventListener("input", (e) => {
document.getElementById("newEnvBgColorText").textContent =
e.target.value;
});
document
.getElementById("newEnvTextColor")
.addEventListener("input", (e) => {
document.getElementById("newEnvTextColorText").textContent =
e.target.value;
});
document
.getElementById("editEnvBgColor")
.addEventListener("input", (e) => {
document.getElementById("editEnvBgColorText").textContent =
e.target.value;
EnvironmentManager.updateEditPreview();
});
document
.getElementById("editEnvTextColor")
.addEventListener("input", (e) => {
document.getElementById("editEnvTextColorText").textContent =
e.target.value;
EnvironmentManager.updateEditPreview();
});
document
.getElementById("galaxyResetView")
.addEventListener("click", () => {
if (GalaxyChartManager.svg && GalaxyChartManager.zoomBehavior)
GalaxyChartManager.svg
.transition()
.duration(500)
.call(GalaxyChartManager.zoomBehavior.transform, d3.zoomIdentity);
});
document
.getElementById("galaxyInfoClose")
.addEventListener("click", () => {
document
.getElementById("galaxyInfoPanel")
.classList.remove("visible");
});
document
.getElementById("galaxyCollapseAll")
.addEventListener("click", () => {
GalaxyChartManager.collapseAllProducts();
});
document
.getElementById("galaxyExpandAll")
.addEventListener("click", () => {
GalaxyChartManager.expandAllProducts();
});
document
.getElementById("galaxyExportPNG")
.addEventListener("click", () => {
showToast("📷 خروجی ذخیره شد", "success");
});
function handleToolSelect(tool, group) {
const ah = ActionRegistry[tool.action] || ActionRegistry.defaultAction;
ah(tool, group);
}
const themeToggle = document.getElementById("themeToggle"),
themeIcon = document.getElementById("themeIcon"),
htmlEl = document.documentElement;
const savedTheme = localStorage.getItem("theme") || "light";
htmlEl.setAttribute("data-theme", savedTheme);
if (themeIcon)
themeIcon.textContent = savedTheme === "dark" ? "☀️" : "🌙";
if (themeToggle) {
themeToggle.addEventListener("click", () => {
const c = htmlEl.getAttribute("data-theme");
const n = c === "dark" ? "light" : "dark";
htmlEl.setAttribute("data-theme", n);
localStorage.setItem("theme", n);
if (themeIcon) themeIcon.textContent = n === "dark" ? "☀️" : "🌙";
});
}
const sidebarToggle = document.getElementById("sidebarToggle"),
sidebar = document.getElementById("sidebar"),
sidebarIcon = document.getElementById("sidebarIcon");
function updateSidebarIcon() {
if (!sidebarIcon) return;
sidebarIcon.textContent = sidebar.classList.contains("collapsed")
? "☰"
: "✕";
}
if (sidebarToggle) {
sidebarToggle.addEventListener("click", () => {
sidebar.classList.toggle("collapsed");
updateSidebarIcon();
});
}
updateSidebarIcon();
const sidebarBody = document.getElementById("sidebarBody");
function renderToolGroups() {
sidebarBody.innerHTML = "";
projectData.toolGroups.forEach((group) => {
const ge = document.createElement("div");
ge.className = "sidebar-group";
const he = document.createElement("div");
he.className = "sidebar-group-header";
he.innerHTML = `<div class="group-icon"><span>${group.icon || ""}</span><span>${group.title}</span></div><span class="group-arrow">▼</span>`;
const be = document.createElement("div");
be.className = "sidebar-group-body";
group.tools.forEach((tool) => {
const btn = document.createElement("button");
btn.className = "sidebar-btn";
btn.title = tool.description;
btn.innerHTML = `<span style="width:20px;text-align:center">${tool.icon || "🔧"}</span><span style="flex:1">${tool.title}</span>`;
btn.addEventListener("click", () => handleToolSelect(tool, group));
be.appendChild(btn);
});
he.addEventListener("click", () => ge.classList.toggle("collapsed"));
ge.appendChild(he);
ge.appendChild(be);
sidebarBody.appendChild(ge);
});
}
function openSettingsModal() {
document.getElementById("settingProjectTitle").value =
projectData.projectTitle;
document.getElementById("settingVersion").value = projectData.version;
document.getElementById("settingDescription").value =
projectData.description;
document.getElementById("settingDeveloper").value =
projectData.developer;
document.getElementById("settingCompany").value = projectData.company;
document.getElementById("settingWebhookUrl").value =
projectData.webhook.url;
document.getElementById("settingWebhookClientId").value =
projectData.webhook.clientId;
document.getElementById("settingWebhookEnabled").checked =
projectData.webhook.enabled;
WebhookManager.updateStatusUI();
ModalManager.open("settingsModal");
}
[
"settingWebhookUrl",
"settingWebhookClientId",
"settingWebhookEnabled",
].forEach((id) => {
document.getElementById(id).addEventListener("input", () => {
WebhookManager.setConfig({
url: document.getElementById("settingWebhookUrl").value,
clientId: document.getElementById("settingWebhookClientId").value,
enabled: document.getElementById("settingWebhookEnabled").checked,
});
});
});
document
.getElementById("saveSettingsBtn")
.addEventListener("click", () => {
WebhookManager.setConfig({
url: document.getElementById("settingWebhookUrl").value.trim(),
clientId: document
.getElementById("settingWebhookClientId")
.value.trim(),
enabled: document.getElementById("settingWebhookEnabled").checked,
});
ModalManager.close("settingsModal");
showToast("تنظیمات ذخیره شد", "success");
});
function updateProjectUI() {
document.title = projectData.projectTitle;
document.getElementById("headerTitle").textContent =
projectData.projectTitle;
document.getElementById("headerSubtitle").textContent =
projectData.description;
document.getElementById("appFooter").innerHTML =
`<div><span class="footer-brand">طراح و توسعه‌دهنده:</span> ${projectData.developer} <span style="margin:0 10px">|</span><span class="footer-brand">شرکت:</span> ${projectData.company}</div><div><span class="footer-brand">نسخه:</span> ${projectData.version}</div>`;
}
/* ==================== INIT ==================== */
ModalManager.init();
renderToolGroups();
updateProjectUI();
WebhookManager.updateStatusUI();
ToolbarManager.init();
SectionCollapseManager.init();
BoardRenderer.render();
HistoryManager.init();
setTimeout(
() =>
showToast(
`${projectData.projectTitle} (نسخه ${projectData.version}) آماده است`,
"success",
),
500,
);
</script>
</body>
</html>