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

4375 lines
140 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: #ffffff;
--panel-bg: #ffffff;
--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;
--sidebar-width: 280px;
--header-height: 64px;
--tabs-height: 48px;
--footer-height: 36px;
}
[data-theme="dark"] {
--primary: #2c5282;
--primary-light: #3b6fb5;
--primary-dark: #1a365d;
--accent: #e0c789;
--accent-dark: #c9a961;
--accent-light: #f0d9a0;
--bg: #1a202c;
--board-bg: #2d3748;
--card-bg: #2d3748;
--panel-bg: #1f2937;
--text: #e2e8f0;
--text-muted: #cbd5e0;
--text-light: #a0aec0;
--border: #4a5568;
--border-light: #374151;
--shadow: 0 4px 6px rgba(0, 0, 0, 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: white;
padding: 0 20px;
height: var(--header-height);
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 3px solid var(--accent);
box-shadow: var(--shadow);
z-index: 100;
flex-shrink: 0;
position: relative;
overflow: hidden;
}
.app-header::before {
content: '';
position: absolute;
top: -50%;
left: -10%;
width: 300px;
height: 300px;
background: radial-gradient(circle, rgba(201, 169, 97, 0.15) 0%, transparent 70%);
border-radius: 50%;
pointer-events: none;
}
.app-brand {
display: flex;
align-items: center;
gap: 14px;
position: relative;
z-index: 1;
}
.app-logo {
width: 44px;
height: 44px;
background: var(--accent);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
font-weight: 800;
color: var(--primary-dark);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
flex-shrink: 0;
}
.app-brand-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.app-title {
font-size: 16px;
font-weight: 800;
letter-spacing: -0.3px;
color: white;
}
.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: white;
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;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
white-space: nowrap;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
filter: brightness(1.1);
}
.btn:active {
transform: translateY(0);
}
.btn-accent {
background: linear-gradient(135deg, var(--accent), var(--accent-dark));
color: var(--primary-dark);
font-weight: 700;
}
.btn-ghost {
background: rgba(26, 54, 93, 0.08);
border: 1px solid var(--border);
color: var(--primary);
font-weight: 600;
}
.btn-ghost:hover {
background: rgba(26, 54, 93, 0.15);
border-color: var(--primary-light);
}
[data-theme="dark"] .btn-ghost {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.25);
color: white;
}
[data-theme="dark"] .btn-ghost:hover {
background: rgba(255, 255, 255, 0.2);
}
.btn-danger {
background: linear-gradient(135deg, var(--danger), #c53030);
color: white;
}
.btn-sm {
padding: 4px 10px;
font-size: 11px;
}
.btn-xs {
padding: 2px 6px;
font-size: 10px;
}
.btn-icon {
width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.app-main {
flex: 1;
display: flex;
overflow: hidden;
position: relative;
flex-direction: row;
}
.sidebar {
order: -1;
width: var(--sidebar-width);
background: var(--panel-bg);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
transition: width 0.3s ease;
box-shadow: var(--shadow);
overflow: hidden;
flex-shrink: 0;
z-index: 50;
}
.sidebar.collapsed {
width: 0;
overflow: hidden;
}
.sidebar-header {
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
padding: 12px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 2px solid var(--accent);
min-height: 48px;
}
.sidebar-title {
font-size: 13px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
overflow: hidden;
}
.sidebar.collapsed .sidebar-title {
display: none;
}
.sidebar-body {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.sidebar.collapsed .sidebar-body {
display: none;
}
.sidebar-group {
margin-bottom: 6px;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
overflow: hidden;
background: var(--card-bg);
}
.sidebar-group-header {
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
padding: 10px 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 13px;
font-weight: 700;
user-select: none;
transition: all 0.2s;
position: relative;
}
.sidebar-group-header:hover {
filter: brightness(1.1);
}
.sidebar-group-header .group-icon {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
overflow: hidden;
}
.sidebar-group-header .group-arrow {
transition: transform 0.3s;
font-size: 12px;
flex-shrink: 0;
}
.sidebar-group.collapsed .group-arrow {
transform: rotate(90deg);
}
.sidebar-group.collapsed .sidebar-group-body {
display: none;
}
.sidebar-group-body {
padding: 6px;
display: flex;
flex-direction: column;
gap: 4px;
}
.sidebar-btn {
background: var(--card-bg);
border: 1px solid var(--border-light);
color: var(--text);
padding: 8px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
font-family: inherit;
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
text-align: right;
width: 100%;
position: relative;
}
.sidebar-btn:hover {
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
border-color: var(--primary);
transform: translateX(-2px);
}
.sidebar-btn .btn-icon-emoji {
font-size: 15px;
width: 20px;
text-align: center;
flex-shrink: 0;
}
.sidebar-btn .btn-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-tooltip] {
position: relative;
}
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
right: calc(100% + 10px);
top: 50%;
transform: translateY(-50%);
background: var(--primary-dark);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 11px;
font-weight: 500;
white-space: normal;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
max-width: 250px;
line-height: 1.5;
}
[data-tooltip]::before {
content: '';
position: absolute;
right: calc(100% + 5px);
top: 50%;
transform: translateY(-50%);
border: 5px solid transparent;
border-left-color: var(--primary-dark);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
z-index: 1000;
}
[data-tooltip]:hover::after,
[data-tooltip]:hover::before {
opacity: 1;
}
[data-theme="dark"] [data-tooltip]::after {
background: var(--accent-dark);
color: var(--primary-dark);
}
[data-theme="dark"] [data-tooltip]::before {
border-left-color: var(--accent-dark);
}
.tabs-bar {
background: linear-gradient(135deg, #0f1724 0%, #1a2a44 100%);
border-bottom: 2px solid var(--accent);
display: flex;
align-items: center;
padding: 0 12px;
min-height: var(--tabs-height);
box-shadow: var(--shadow-sm);
flex-shrink: 0;
gap: 10px;
position: relative;
flex-wrap: wrap;
}
.toolbar-controls {
display: flex;
gap: 6px;
margin-left: 8px;
}
.toolbar-icon-btn {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
color: white;
width: 34px;
height: 34px;
border-radius: 6px;
cursor: pointer;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
padding: 0;
}
.toolbar-icon-btn:hover:not(:disabled) {
background: var(--accent);
color: var(--primary-dark);
border-color: var(--accent);
transform: translateY(-1px);
}
.toolbar-icon-btn svg {
display: block;
}
.toolbar-search {
flex: 1;
min-width: 180px;
max-width: 320px;
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
right: 10px;
font-size: 13px;
opacity: 0.7;
pointer-events: none;
}
.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: white;
font-family: inherit;
font-size: 13px;
transition: all 0.2s;
}
.search-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.search-input:focus {
outline: none;
background: rgba(255, 255, 255, 0.12);
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(201, 169, 97, 0.2);
}
.toolbar-filters {
display: flex;
gap: 8px;
}
.filter-dropdown {
position: relative;
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;
transition: all 0.2s;
}
.filter-dropdown:hover {
border-color: rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.12);
}
.filter-icon {
font-size: 13px;
opacity: 0.9;
}
.filter-select {
background: transparent;
border: none;
color: white;
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: white;
}
.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);
white-space: nowrap;
}
.stat-icon {
font-size: 13px;
}
.stat-label {
font-weight: 500;
}
.stat-value {
font-weight: 800;
color: var(--accent);
background: rgba(201, 169, 97, 0.15);
padding: 2px 7px;
border-radius: 10px;
font-size: 11px;
min-width: 22px;
text-align: center;
}
@media (max-width: 900px) {
.tabs-bar {
flex-wrap: wrap;
padding: 8px;
gap: 8px;
}
.toolbar-stats {
order: 3;
width: 100%;
margin-right: 0;
padding-right: 0;
border-right: none;
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding-top: 8px;
justify-content: center;
}
.toolbar-search {
order: 2;
}
}
.board-area {
flex: 1;
overflow: auto;
padding: 24px;
background: var(--board-bg);
transition: background 0.3s;
}
.board-section {
margin-bottom: 32px;
}
.board-section:last-child {
margin-bottom: 0;
}
.board-section.collapsed .section-content {
display: none;
}
.board-section.collapsed .collapse-icon {
transform: rotate(90deg);
}
.collapse-icon {
transition: transform 0.3s ease;
font-size: 14px;
display: inline-block;
margin-left: 6px;
}
.board-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid var(--border);
}
.board-header-title {
font-size: 18px;
font-weight: 800;
color: var(--primary);
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}
[data-theme="dark"] .board-header-title {
color: var(--accent);
}
.board-header-actions {
display: flex;
gap: 8px;
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
}
.project-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: all 0.25s;
display: flex;
flex-direction: column;
position: relative;
}
.project-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--accent);
}
.project-card-header {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
color: white;
padding: 16px;
position: relative;
overflow: hidden;
}
.project-card-header::before {
content: '';
position: absolute;
top: -30px;
left: -30px;
width: 100px;
height: 100px;
background: radial-gradient(circle, rgba(201, 169, 97, 0.2) 0%, transparent 70%);
border-radius: 50%;
}
.project-card-title-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 4px;
position: relative;
z-index: 1;
}
.project-card-icon {
font-size: 22px;
flex-shrink: 0;
}
.project-card-title {
font-size: 16px;
font-weight: 700;
flex: 1;
}
.project-card-desc {
font-size: 12px;
opacity: 0.9;
position: relative;
z-index: 1;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.project-card-body {
padding: 14px 16px;
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
}
.project-stat-row {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--text-muted);
}
.project-stat-value {
font-weight: 700;
color: var(--primary);
}
[data-theme="dark"] .project-stat-value {
color: var(--accent);
}
.project-type-badges {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.type-badge {
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
border-radius: 4px;
display: inline-block;
}
.type-backend {
background: #ebf4ff;
color: #2b6cb0;
}
.type-frontend {
background: #f0fff4;
color: #276749;
}
.type-service {
background: #fefcbf;
color: #975a16;
}
.type-database {
background: #fed7e2;
color: #97266d;
}
.type-mobile {
background: #e9d8fd;
color: #553c9a;
}
.type-infrastructure {
background: #feebc8;
color: #9c4221;
}
[data-theme="dark"] .type-backend {
background: rgba(66, 153, 225, 0.2);
color: #90cdf4;
}
[data-theme="dark"] .type-frontend {
background: rgba(72, 187, 120, 0.2);
color: #9ae6b4;
}
[data-theme="dark"] .type-service {
background: rgba(236, 201, 75, 0.2);
color: #fefcbf;
}
[data-theme="dark"] .type-database {
background: rgba(237, 100, 166, 0.2);
color: #fbb6ce;
}
[data-theme="dark"] .type-mobile {
background: rgba(159, 122, 234, 0.2);
color: #d6bcfa;
}
[data-theme="dark"] .type-infrastructure {
background: rgba(237, 137, 54, 0.2);
color: #fbd38d;
}
.project-card-footer {
padding: 12px 16px;
background: var(--border-light);
border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
}
.card-actions {
display: flex;
gap: 6px;
}
.env-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.env-card-item {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: all 0.25s;
display: flex;
flex-direction: column;
}
.env-card-item:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
border-color: var(--accent);
}
.env-card-preview {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 700;
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.env-card-body {
padding: 14px 16px;
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.env-card-name {
font-size: 15px;
font-weight: 700;
color: var(--text);
}
.env-card-desc {
font-size: 12px;
color: var(--text-muted);
line-height: 1.5;
}
.env-card-footer {
padding: 10px 16px;
background: var(--border-light);
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-start;
gap: 6px;
}
.board-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted);
text-align: center;
}
.board-empty-icon {
font-size: 80px;
opacity: 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);
}
.board-empty p {
margin-bottom: 20px;
}
.no-results {
text-align: center;
padding: 40px;
color: var(--text-muted);
background: var(--border-light);
border-radius: var(--radius);
margin: 20px 0;
}
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(26, 54, 93, 0.6);
backdrop-filter: blur(3px);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
animation: fadeIn 0.2s;
}
.modal-overlay.active {
display: flex;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal {
background: var(--panel-bg);
color: var(--text);
border-radius: 12px;
width: 100%;
max-width: 600px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-lg);
animation: slideUp 0.25s;
}
.modal-lg {
max-width: 800px;
}
.modal-xl {
max-width: 1000px;
}
.modal-fullscreen {
max-width: 100%;
width: calc(100vw - 40px);
height: calc(100vh - 40px);
max-height: calc(100vh - 40px);
border-radius: 16px;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
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: white;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 16px;
transition: all 0.2s;
}
.modal-close:hover {
background: var(--danger);
}
.modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.modal-footer {
padding: 14px 20px;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-start;
gap: 8px;
background: var(--border-light);
align-items: center;
}
#projectDetailModal .modal-footer {
justify-content: flex-start;
}
.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: white;
padding: 12px 16px;
font-size: 14px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: space-between;
}
.detail-section-header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.detail-section-body {
padding: 16px;
background: var(--card-bg);
}
.detail-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.detail-info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-info-label {
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
}
.detail-info-value {
font-size: 13px;
font-weight: 600;
color: var(--text);
}
.subproject-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.subproject-card {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 12px;
background: var(--border-light);
transition: all 0.2s;
}
.subproject-card:hover {
border-color: var(--accent);
box-shadow: var(--shadow-sm);
}
.subproject-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
gap: 8px;
}
.subproject-title {
font-size: 14px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.subproject-actions {
display: flex;
gap: 4px;
}
.subproject-meta {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 8px;
}
.deployment-section {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed var(--border);
}
.deployment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.deployment-header-title {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
}
.deployment-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.deployment-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: var(--card-bg);
border: 1px solid var(--border-light);
border-radius: 6px;
font-size: 11px;
transition: all 0.2s;
}
.deployment-item:hover {
border-color: var(--accent);
box-shadow: var(--shadow-sm);
}
.deployment-item-info {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.deployment-env-tag {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
}
.deployment-version {
font-family: 'Vazirmatn', monospace;
font-weight: 700;
color: var(--primary);
font-size: 11px;
}
[data-theme="dark"] .deployment-version {
color: var(--accent);
}
.deployment-date {
font-size: 10px;
color: var(--text-muted);
}
.deployment-actions {
display: flex;
gap: 4px;
}
.empty-deployments {
text-align: center;
padding: 12px;
color: var(--text-muted);
font-size: 11px;
background: var(--border-light);
border-radius: 6px;
}
.empty-subprojects {
text-align: center;
padding: 30px;
color: var(--text-muted);
background: var(--border-light);
border-radius: var(--radius-sm);
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--primary);
margin-bottom: 6px;
}
[data-theme="dark"] .form-label {
color: var(--accent-light);
}
.form-label .required {
color: var(--danger);
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 9px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-family: inherit;
font-size: 13px;
background: var(--card-bg);
color: var(--text);
transition: all 0.2s;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(201, 169, 97, 0.15);
}
.form-input.error {
border-color: var(--danger);
box-shadow: 0 0 0 3px rgba(229, 62, 62, 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-row-3 {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 12px;
align-items: end;
}
.form-row-4 {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 12px;
align-items: end;
}
.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-input-wrapper input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-input-wrapper input[type="color"]::-webkit-color-swatch {
border: 1px solid var(--border);
border-radius: 4px;
}
.color-preview-text {
font-size: 11px;
font-family: monospace;
color: var(--text-muted);
}
.webhook-section {
background: var(--border-light);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 14px;
margin-top: 12px;
}
.webhook-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.webhook-section-title {
font-size: 13px;
font-weight: 700;
color: var(--primary);
display: flex;
align-items: center;
gap: 6px;
}
[data-theme="dark"] .webhook-section-title {
color: var(--accent-light);
}
.webhook-toggle {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-muted);
}
.switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
inset: 0;
background: var(--border);
transition: 0.3s;
border-radius: 20px;
}
.slider::before {
position: absolute;
content: "";
height: 14px;
width: 14px;
right: 3px;
bottom: 3px;
background: white;
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);
flex-shrink: 0;
}
.webhook-status-dot.success {
background: var(--success);
}
.webhook-status-dot.error {
background: var(--danger);
}
.webhook-status-dot.pending {
background: var(--warning);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
.toast-container {
position: fixed;
bottom: 20px;
left: 20px;
z-index: 2000;
display: flex;
flex-direction: column;
gap: 8px;
}
.toast {
background: var(--primary);
color: white;
padding: 12px 18px;
border-radius: 8px;
box-shadow: var(--shadow-lg);
font-size: 13px;
display: flex;
align-items: center;
gap: 10px;
animation: slideInLeft 0.3s;
min-width: 250px;
}
.toast.success {
background: var(--success);
}
.toast.error {
background: var(--danger);
}
.toast.warning {
background: var(--warning);
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.app-footer {
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
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;
}
[data-theme="dark"] .readonly-info {
background: rgba(49, 130, 206, 0.12);
border-color: rgba(49, 130, 206, 0.3);
border-right-color: var(--info);
}
.dependency-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
max-height: 200px;
overflow-y: auto;
}
.dependency-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
transition: all 0.2s;
}
.dependency-item:hover {
border-color: var(--accent);
box-shadow: var(--shadow-sm);
}
.dependency-info {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.dependency-project-name {
font-weight: 600;
font-size: 13px;
}
.dependency-version-info {
font-size: 11px;
color: var(--text-muted);
}
.dependency-search-box {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border);
}
.dependency-search-input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-family: inherit;
font-size: 13px;
background: var(--card-bg);
color: var(--text);
margin-bottom: 12px;
}
.dependency-search-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(201, 169, 97, 0.15);
}
.dependency-search-results {
max-height: 300px;
overflow-y: auto;
}
.dependency-project-result {
margin-bottom: 12px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
overflow: hidden;
transition: all 0.2s;
}
.dependency-project-result:hover {
border-color: var(--accent);
box-shadow: var(--shadow-sm);
}
.dependency-project-header {
background: var(--border-light);
padding: 10px 12px;
font-weight: 600;
font-size: 13px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s;
}
.dependency-project-header:hover {
background: var(--accent-light);
}
.dependency-versions-list {
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.dependency-version-item {
padding: 8px 10px;
background: var(--card-bg);
border: 1px solid var(--border-light);
border-radius: 4px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
transition: all 0.2s;
}
.dependency-version-item:hover {
background: var(--accent-light);
border-color: var(--accent);
}
.dependency-version-item.selected {
background: var(--accent);
color: var(--primary-dark);
border-color: var(--accent-dark);
}
.dependency-version-badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
background: var(--info);
color: white;
}
.dependency-version-badge.history {
background: var(--text-muted);
}
.empty-dependencies {
text-align: center;
padding: 20px;
color: var(--text-muted);
font-size: 12px;
background: var(--border-light);
border-radius: var(--radius-sm);
}
.dependency-section-title {
font-size: 13px;
font-weight: 700;
color: var(--primary);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 6px;
}
[data-theme="dark"] .dependency-section-title {
color: var(--accent);
}
.dependency-display-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.dependency-display-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: var(--card-bg);
border: 1px solid var(--border-light);
border-radius: 6px;
font-size: 12px;
transition: all 0.2s;
}
.dependency-display-item:hover {
border-color: var(--accent);
box-shadow: var(--shadow-sm);
}
.dependency-display-info {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.dependency-display-title {
font-weight: 600;
font-size: 12px;
color: var(--text);
}
.dependency-display-meta {
display: flex;
align-items: center;
gap: 6px;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* ============ Galaxy Chart Styles ============ */
.galaxy-toolbar {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
flex: 1;
}
.galaxy-toolbar .btn {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
font-size: 11px;
padding: 5px 10px;
}
.galaxy-toolbar .btn:hover {
background: var(--accent);
color: var(--primary-dark);
border-color: var(--accent);
}
/* Galaxy Search */
.galaxy-search-wrapper {
position: relative;
display: flex;
align-items: center;
flex: 1;
min-width: 180px;
max-width: 320px;
}
.galaxy-search-icon {
position: absolute;
right: 10px;
font-size: 12px;
opacity: 0.7;
pointer-events: none;
z-index: 2;
}
.galaxy-search-input {
width: 100%;
padding: 6px 32px 6px 28px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
color: white;
font-family: inherit;
font-size: 12px;
transition: all 0.2s;
}
.galaxy-search-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.galaxy-search-input:focus {
outline: none;
background: rgba(255, 255, 255, 0.18);
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(201, 169, 97, 0.2);
}
.galaxy-search-clear {
position: absolute;
left: 6px;
background: rgba(255, 255, 255, 0.15);
border: none;
color: white;
width: 18px;
height: 18px;
border-radius: 50%;
cursor: pointer;
font-size: 10px;
display: none;
align-items: center;
justify-content: center;
transition: all 0.2s;
z-index: 2;
padding: 0;
line-height: 1;
}
.galaxy-search-clear.visible {
display: flex;
}
.galaxy-search-clear:hover {
background: var(--danger);
}
.galaxy-search-count {
font-size: 11px;
color: var(--accent-light);
padding: 4px 10px;
background: rgba(201, 169, 97, 0.15);
border: 1px solid rgba(201, 169, 97, 0.3);
border-radius: 10px;
white-space: nowrap;
display: none;
font-weight: 600;
}
.galaxy-search-count.visible {
display: inline-block;
}
.galaxy-search-count.no-results {
background: rgba(229, 62, 62, 0.15);
border-color: rgba(229, 62, 62, 0.3);
color: #feb2b2;
}
.galaxy-modal-body {
padding: 0;
position: relative;
overflow: hidden;
background: #0a0e27;
}
.galaxy-chart-container {
width: 100%;
height: 100%;
min-height: 400px;
position: relative;
}
.galaxy-chart-container svg {
width: 100%;
height: 100%;
display: block;
cursor: grab;
}
.galaxy-chart-container svg:active {
cursor: grabbing;
}
.galaxy-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: rgba(255, 255, 255, 0.5);
text-align: center;
font-size: 16px;
gap: 10px;
}
.galaxy-empty-icon {
font-size: 60px;
opacity: 0.5;
}
.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: white;
font-size: 11px;
z-index: 10;
backdrop-filter: blur(10px);
max-width: 220px;
}
.galaxy-legend-title {
font-weight: 700;
font-size: 12px;
margin-bottom: 10px;
color: var(--accent-light);
display: flex;
align-items: center;
gap: 6px;
}
.galaxy-legend-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.galaxy-legend-color {
width: 14px;
height: 14px;
border-radius: 50%;
flex-shrink: 0;
box-shadow: 0 0 8px currentColor;
}
.galaxy-legend-line {
width: 24px;
height: 2px;
flex-shrink: 0;
}
.galaxy-info-panel {
position: absolute;
top: 16px;
left: 16px;
background: rgba(10, 14, 39, 0.9);
border: 1px solid rgba(201, 169, 97, 0.3);
border-radius: 10px;
padding: 14px 18px;
color: white;
font-size: 12px;
z-index: 10;
backdrop-filter: blur(10px);
max-width: 280px;
display: none;
}
.galaxy-info-panel.visible {
display: block;
}
.galaxy-info-panel h4 {
font-size: 14px;
font-weight: 700;
color: var(--accent-light);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.galaxy-info-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
gap: 12px;
}
.galaxy-info-row:last-child {
border-bottom: none;
}
.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-info-close {
position: absolute;
top: 8px;
left: 8px;
background: rgba(255, 255, 255, 0.1);
border: none;
color: white;
width: 22px;
height: 22px;
border-radius: 50%;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.galaxy-info-close:hover {
background: var(--danger);
}
.galaxy-tooltip {
position: fixed;
background: rgba(10, 14, 39, 0.95);
border: 1px solid var(--accent);
border-radius: 8px;
padding: 10px 14px;
color: white;
font-size: 12px;
pointer-events: none;
z-index: 3000;
max-width: 250px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
display: none;
}
.galaxy-tooltip.visible {
display: block;
}
.galaxy-tooltip-title {
font-weight: 700;
color: var(--accent-light);
margin-bottom: 4px;
}
.galaxy-tooltip-subtitle {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
}
.galaxy-stats {
font-size: 12px;
color: var(--text-muted);
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.galaxy-stat-item {
display: flex;
align-items: center;
gap: 6px;
}
.galaxy-stat-value {
font-weight: 700;
color: var(--primary);
background: rgba(26, 54, 93, 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-zoom-indicator {
position: absolute;
top: 16px;
right: 16px;
background: rgba(10, 14, 39, 0.85);
border: 1px solid rgba(201, 169, 97, 0.3);
border-radius: 8px;
padding: 6px 12px;
color: white;
font-size: 12px;
z-index: 10;
font-weight: 600;
}
/* Galaxy Node States (search) */
.galaxy-node {
transition: opacity 0.3s ease;
}
.galaxy-node.dimmed circle,
.galaxy-node.dimmed text {
opacity: 0.12 !important;
transition: opacity 0.3s ease;
}
.galaxy-node.highlighted circle:nth-of-type(2) {
stroke: #ffffff !important;
stroke-width: 4px !important;
stroke-opacity: 1 !important;
filter: url(#galaxyGlow);
}
.galaxy-node.highlighted .galaxy-highlight-ring {
opacity: 1 !important;
animation: galaxy-pulse 1.5s infinite;
}
.galaxy-link {
transition: opacity 0.3s ease;
}
.galaxy-link.dimmed {
opacity: 0.05 !important;
}
@keyframes galaxy-pulse {
0%, 100% {
r: attr(r);
opacity: 0.6;
}
50% {
opacity: 1;
}
}
</style>
</head>
<body>
<header class="app-header">
<div class="app-brand">
<div class="app-logo">📊</div>
<div class="app-brand-text">
<div class="app-title" id="headerTitle">مدیریت محصولات</div>
<div class="app-subtitle" id="headerSubtitle">مدیریت نسخه‌ها و نیازمندی‌های محصولات</div>
</div>
</div>
</header>
<div class="tabs-bar" id="advancedToolbar">
<div class="toolbar-controls">
<button class="toolbar-icon-btn" id="sidebarToggle" title="باز/بسته کردن سایدبار">
<span id="sidebarIcon">✕</span>
</button>
<button class="toolbar-icon-btn" id="themeToggle" title="تغییر تم">
<span id="themeIcon">🌙</span>
</button>
<button class="toolbar-icon-btn" id="undoBtn" title="بازگشت (Ctrl+Z)" disabled>
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
<path
d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z" />
</svg>
</button>
<button class="toolbar-icon-btn" id="redoBtn" title="باز انجام (Ctrl+Y)" disabled>
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
<path
d="M18.4 10.6C16.55 9 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z" />
</svg>
</button>
</div>
<div class="toolbar-search">
<span class="search-icon">🔍</span>
<input type="text" class="search-input" id="toolbarSearch" placeholder="جستجو...">
</div>
<div class="toolbar-filters">
<div class="filter-dropdown">
<span class="filter-icon">🏷️</span>
<select class="filter-select" id="filterType">
<option value="">همه انواع</option>
<option value="backend">بک‌اند</option>
<option value="frontend">فرانت‌اند</option>
<option value="service">میکروسرویس</option>
<option value="database">پایگاه داده</option>
<option value="mobile">موبایل</option>
<option value="infrastructure">زیرساخت</option>
</select>
</div>
<div class="filter-dropdown">
<span class="filter-icon">🌐</span>
<select class="filter-select" id="filterEnvironment">
<option value="">همه محیط‌ها</option>
</select>
</div>
</div>
<div class="toolbar-stats">
<div class="stat-item">
<span class="stat-icon">📋</span>
<span class="stat-label">کل:</span>
<span class="stat-value" id="statTotal">0</span>
</div>
<div class="stat-item">
<span class="stat-icon">📦</span>
<span class="stat-label">محصول:</span>
<span class="stat-value" id="statProjects">0</span>
</div>
<div class="stat-item">
<span class="stat-icon">🌿</span>
<span class="stat-label">پروژه:</span>
<span class="stat-value" id="statSubProjects">0</span>
</div>
<div class="stat-item">
<span class="stat-icon">🔗</span>
<span class="stat-label">وابستگی:</span>
<span class="stat-value" id="statDependencies">0</span>
</div>
</div>
</div>
<div class="app-main">
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="sidebar-title">🛠️ ابزارها</div>
</div>
<div class="sidebar-body" id="sidebarBody"></div>
</aside>
<main class="board-area" id="boardArea"></main>
</div>
<footer class="app-footer" id="appFooter">
<div>
<span class="footer-brand">طراح و توسعه‌دهنده:</span> هادی خزاعی اصل
<span style="margin: 0 10px;">|</span>
<span class="footer-brand">شرکت:</span> رایان پرداز کاوش
</div>
<div>
<span class="footer-brand">نسخه:</span> 1.4.20
</div>
</footer>
<!-- Settings Modal -->
<div class="modal-overlay" id="settingsModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title"><span>⚙️</span><span>تنظیمات محصول</span></div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="readonly-info">
<span>ℹ️</span>
<span>اطلاعات پایه محصول فقط قابل مشاهده هستند و از طریق فایل پیکربندی قابل تغییر می‌باشند.</span>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">عنوان محصول <span class="readonly-badge">فقط خواندنی</span></label>
<input type="text" class="form-input" id="settingProjectTitle" readonly>
</div>
<div class="form-group">
<label class="form-label">نسخه <span class="readonly-badge">فقط خواندنی</span></label>
<input type="text" class="form-input" id="settingVersion" readonly>
</div>
</div>
<div class="form-group">
<label class="form-label">توضیحات <span class="readonly-badge">فقط خواندنی</span></label>
<textarea class="form-textarea" id="settingDescription" readonly></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">توسعه‌دهنده <span class="readonly-badge">فقط خواندنی</span></label>
<input type="text" class="form-input" id="settingDeveloper" readonly>
</div>
<div class="form-group">
<label class="form-label">شرکت <span class="readonly-badge">فقط خواندنی</span></label>
<input type="text" class="form-input" id="settingCompany" readonly>
</div>
</div>
<div class="webhook-section">
<div class="webhook-section-header">
<div class="webhook-section-title"><span>🔗</span><span>تنظیمات وب‌هوک</span></div>
<div class="webhook-toggle">
<span>فعال‌سازی</span>
<label class="switch">
<input type="checkbox" id="settingWebhookEnabled">
<span class="slider"></span>
</label>
</div>
</div>
<div class="form-group">
<label class="form-label">آدرس URL وب‌هوک <span class="required">*</span></label>
<input type="url" class="form-input" id="settingWebhookUrl" dir="ltr"
placeholder="https://example.com/webhook">
</div>
<div class="form-group">
<label class="form-label">شناسه کلاینت (Client ID) <span class="required">*</span></label>
<input type="text" class="form-input" id="settingWebhookClientId" dir="ltr" placeholder="client-xxxx-xxxx">
</div>
<div class="webhook-status">
<span class="webhook-status-dot" id="webhookStatusDot"></span>
<span id="webhookStatusText">وضعیت: غیرفعال</span>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveSettingsBtn">💾 ذخیره</button>
<button class="btn btn-ghost" data-close-modal>بستن</button>
</div>
</div>
</div>
<!-- Environment Manager Modal -->
<div class="modal-overlay" id="envManagerModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title">
<span>🌍</span>
<span>افزودن محیط عملیاتی جدید</span>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="form-row">
<div class="form-group">
<label class="form-label">نام محیط <span class="required">*</span></label>
<input type="text" class="form-input" id="newEnvName" placeholder="مثلاً: QA">
</div>
<div class="form-group">
<label class="form-label">توضیحات</label>
<input type="text" class="form-input" id="newEnvDesc" placeholder="توضیحات محیط">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">رنگ پس‌زمینه</label>
<div class="color-input-wrapper">
<input type="color" id="newEnvBgColor" value="#38a169">
<span class="color-preview-text" id="newEnvBgColorText">#38a169</span>
</div>
</div>
<div class="form-group">
<label class="form-label">رنگ متن</label>
<div class="color-input-wrapper">
<input type="color" id="newEnvTextColor" value="#ffffff">
<span class="color-preview-text" id="newEnvTextColorText">#ffffff</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveNewEnvBtn">💾 ثبت محیط</button>
<button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<!-- Environment Edit Modal -->
<div class="modal-overlay" id="envEditModal">
<div class="modal">
<div class="modal-header">
<div class="modal-title">
<span>✏️</span>
<span>ویرایش محیط عملیاتی</span>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">شناسه محیط <span class="readonly-badge">فقط خواندنی</span></label>
<input type="text" class="form-input" id="editEnvId" readonly>
</div>
<div class="form-group">
<label class="form-label">نام محیط <span class="required">*</span></label>
<input type="text" class="form-input" id="editEnvName">
</div>
<div class="form-group">
<label class="form-label">توضیحات</label>
<input type="text" class="form-input" id="editEnvDesc">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">رنگ پس‌زمینه</label>
<div class="color-input-wrapper">
<input type="color" id="editEnvBgColor">
<span class="color-preview-text" id="editEnvBgColorText"></span>
</div>
</div>
<div class="form-group">
<label class="form-label">رنگ متن</label>
<div class="color-input-wrapper">
<input type="color" id="editEnvTextColor">
<span class="color-preview-text" id="editEnvTextColorText"></span>
</div>
</div>
</div>
<div class="form-group">
<label class="form-label">پیش‌نمایش</label>
<div id="editEnvPreview" class="env-color-preview">نمونه متن</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveEnvEditBtn">💾 ذخیره تغییرات</button>
<button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<!-- Product Hierarchy Manager Modal -->
<div class="modal-overlay" id="projectHierarchyModal">
<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 id="projectHierarchyTree" style="max-height: 60vh; overflow-y: auto; padding: 4px;"></div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" onclick="ProductManager.openAddProductModal()">➕ محصول جدید</button>
<button class="btn btn-ghost" data-close-modal>بستن</button>
</div>
</div>
</div>
<!-- Product Detail Modal -->
<div class="modal-overlay" id="projectDetailModal">
<div class="modal modal-xl">
<div class="modal-header">
<div class="modal-title" id="projectDetailTitle">
<span>📦</span>
<span>جزئیات محصول</span>
</div>
<div style="display: flex; gap: 8px;">
<button class="modal-close" data-close-modal>✕</button>
</div>
</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>
<!-- Product Edit Modal -->
<div class="modal-overlay" id="productEditModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title">
<span>✏️</span>
<span>ویرایش اطلاعات محصول</span>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">شناسه محصول <span class="readonly-badge">فقط خواندنی</span></label>
<input type="text" class="form-input" id="editProductId" readonly>
</div>
<div class="form-group">
<label class="form-label">عنوان محصول <span class="required">*</span></label>
<input type="text" class="form-input" id="editProductTitle">
</div>
<div class="form-group">
<label class="form-label">توضیحات</label>
<textarea class="form-textarea" id="editProductDescription" rows="3"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">مالک</label>
<input type="text" class="form-input" id="editProductOwner">
</div>
<div class="form-group">
<label class="form-label">تاریخ ایجاد <span class="readonly-badge">فقط خواندنی</span></label>
<input type="text" class="form-input" id="editProductCreatedAt" readonly>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveProductEditBtn">💾 ذخیره تغییرات</button>
<button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<!-- Add Product Modal -->
<div class="modal-overlay" id="addProductModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title">
<span>➕</span>
<span>ایجاد محصول جدید</span>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="readonly-info">
<span>ℹ️</span>
<span>لطفاً اطلاعات محصول جدید را با دقت وارد کنید. فیلدهای دارای <span style="color: var(--danger);">*</span>
الزامی هستند.</span>
</div>
<div class="form-group">
<label class="form-label">عنوان محصول <span class="required">*</span></label>
<input type="text" class="form-input" id="newProductTitle" placeholder="مثلاً: سامانه فروشگاه آنلاین">
<div class="form-error" id="newProductTitleError">عنوان محصول الزامی است و باید حداقل ۳ کاراکتر باشد</div>
</div>
<div class="form-group">
<label class="form-label">توضیحات</label>
<textarea class="form-textarea" id="newProductDescription" rows="3"
placeholder="توضیحات مختصر درباره محصول..."></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">مالک / تیم مسئول</label>
<input type="text" class="form-input" id="newProductOwner" placeholder="مثلاً: تیم توسعه محصول">
</div>
<div class="form-group">
<label class="form-label">تاریخ ایجاد <span class="readonly-badge">فقط خواندنی</span></label>
<input type="text" class="form-input" id="newProductCreatedAt" readonly>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveNewProductBtn">💾 ثبت محصول</button>
<button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<!-- Add Project Modal -->
<div class="modal-overlay" id="addProjectModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title">
<span>➕</span>
<span>افزودن پروژه جدید</span>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">عنوان پروژه <span class="required">*</span></label>
<input type="text" class="form-input" id="newProjectTitle" placeholder="مثلاً: Backend API Service">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">نوع پروژه <span class="required">*</span></label>
<select class="form-select" id="newProjectType">
<option value="">انتخاب کنید...</option>
</select>
</div>
<div class="form-group">
<label class="form-label">فریم‌ورک</label>
<input type="text" class="form-input" id="newProjectFramework" placeholder="مثلاً: .NET 8">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">زبان برنامه‌نویسی</label>
<input type="text" class="form-input" id="newProjectLanguage" placeholder="مثلاً: C#">
</div>
<div class="form-group">
<label class="form-label">آدرس مخزن</label>
<input type="text" class="form-input" id="newProjectRepo" dir="ltr" placeholder="https://github.com/...">
</div>
</div>
<div class="form-group">
<label class="form-label">توضیحات</label>
<textarea class="form-textarea" id="newProjectDescription" rows="3" placeholder="توضیحات پروژه..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveNewProjectBtn">💾 ذخیره</button>
<button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<!-- Edit Project Modal -->
<div class="modal-overlay" id="editProjectModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title">
<span>✏️</span>
<span>ویرایش اطلاعات پروژه</span>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">شناسه پروژه <span class="readonly-badge">فقط خواندنی</span></label>
<input type="text" class="form-input" id="editProjId" readonly>
</div>
<div class="form-group">
<label class="form-label">عنوان پروژه <span class="required">*</span></label>
<input type="text" class="form-input" id="editProjTitle">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">نوع پروژه <span class="required">*</span></label>
<select class="form-select" id="editProjType">
<option value="">انتخاب کنید...</option>
</select>
</div>
<div class="form-group">
<label class="form-label">فریم‌ورک</label>
<input type="text" class="form-input" id="editProjFramework">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">زبان برنامه‌نویسی</label>
<input type="text" class="form-input" id="editProjLanguage">
</div>
<div class="form-group">
<label class="form-label">آدرس مخزن</label>
<input type="text" class="form-input" id="editProjRepo" dir="ltr">
</div>
</div>
<div class="form-group">
<label class="form-label">توضیحات</label>
<textarea class="form-textarea" id="editProjDescription" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveProjEditBtn">💾 ذخیره تغییرات</button>
<button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<!-- Deployment Modal -->
<div class="modal-overlay" id="deploymentModal">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="deploymentModalTitle">
<span>🚀</span>
<span>افزودن نسخه استقرار</span>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="form-group" id="deploymentEnvGroup"></div>
<div class="form-group">
<label class="form-label">نسخه <span class="required">*</span></label>
<input type="text" class="form-input" id="depVersion" dir="ltr" placeholder="مثلاً: 1.2.3 یا 2.0.0-rc.1">
<div class="form-hint">از فرمت SemVer استفاده کنید (مثال: 1.2.3, 2.0.0-beta.1)</div>
</div>
<div class="form-group">
<label class="form-label">تاریخ و زمان استقرار <span class="required">*</span></label>
<input type="datetime-local" class="form-input" id="depDate" dir="ltr">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-accent" id="saveDeploymentBtn">💾 ذخیره</button>
<button class="btn btn-ghost" data-close-modal>انصراف</button>
</div>
</div>
</div>
<!-- Dependency Manager Modal -->
<div class="modal-overlay" id="dependencyManagerModal">
<div class="modal modal-lg">
<div class="modal-header">
<div class="modal-title">
<span>🔗</span>
<span>مدیریت وابستگی‌ها</span>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body">
<div class="readonly-info">
<span>ℹ️</span>
<span>وابستگی‌های پروژه فعلی را مدیریت کنید. هر وابستگی شامل یک پروژه و یک نسخه استقرار خاص از آن پروژه
است.</span>
</div>
<div class="dependency-section-title">📋 وابستگی‌های فعلی</div>
<div id="currentDependenciesList" class="dependency-list"></div>
<div class="dependency-search-box">
<div class="dependency-section-title">🔍 افزودن وابستگی جدید</div>
<input type="text" class="dependency-search-input" id="dependencySearch"
placeholder="جستجو در پروژه‌ها... (نام پروژه یا محصول)">
<div id="dependencySearchResults" class="dependency-search-results"></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" data-close-modal>بستن</button>
</div>
</div>
</div>
<!-- Galaxy Chart Modal -->
<div class="modal-overlay" id="galaxyChartModal">
<div class="modal modal-fullscreen">
<div class="modal-header">
<div class="modal-title"><span>🌌</span><span>نمودار کهکشانی محصولات</span></div>
<div class="galaxy-toolbar">
<div class="galaxy-search-wrapper">
<span class="galaxy-search-icon">🔍</span>
<input type="text" class="galaxy-search-input" id="galaxySearchInput" placeholder="جستجو در نمودار (نام، نوع، توضیحات...)">
<button class="galaxy-search-clear" id="galaxySearchClear" title="پاک کردن">✕</button>
</div>
<button class="btn btn-sm" id="galaxyZoomIn" title="بزرگنمایی">🔍+</button>
<button class="btn btn-sm" id="galaxyZoomOut" title="کوچک‌نمایی">🔍-</button>
<button class="btn btn-sm" id="galaxyResetView" title="بازنشانی نما">🎯</button>
<button class="btn btn-sm" id="galaxyToggleLabels" title="نمایش/پنهان برچسب‌ها">🏷️</button>
<span class="galaxy-search-count" id="galaxySearchCount"></span>
<button class="btn btn-sm" id="galaxyExportPNG" title="خروجی PNG">📷 PNG</button>
<button class="btn btn-sm" id="galaxyExportSVG" title="خروجی SVG">📐 SVG</button>
</div>
<button class="modal-close" data-close-modal>✕</button>
</div>
<div class="modal-body galaxy-modal-body">
<div id="galaxyChartContainer" class="galaxy-chart-container"></div>
<div class="galaxy-legend" id="galaxyLegend"></div>
<div class="galaxy-info-panel" id="galaxyInfoPanel">
<button class="galaxy-info-close" id="galaxyInfoClose">✕</button>
<div id="galaxyInfoContent"></div>
</div>
<div class="galaxy-zoom-indicator" id="galaxyZoomIndicator">🔍 100%</div>
</div>
<div class="modal-footer">
<div class="galaxy-stats" id="galaxyStats"></div>
<button class="btn btn-ghost" data-close-modal>بستن</button>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<div class="galaxy-tooltip" id="galaxyTooltip"></div>
<script>
const projectData = {
projectId: '550e8400-e29b-41d4-a716-446655440000',
projectName: 'xDependencies',
projectTitle: 'مدیریت محصولات',
description: 'مدیریت نسخه‌ها و نیازمندی‌های محصولات',
version: '1.4.20',
developer: 'هادی خزاعی اصل',
company: 'رایان پرداز کاوش',
metadata: {
createdAt: '2026-09-13T00:00:00Z',
updatedAt: '2026-09-14T00:00:00Z',
tags: ['release-management', 'architecture', 'product-management'],
customFields: {}
},
webhook: {url: '', clientId: '', enabled: false},
toolGroups: [
{
id: 'product-management', title: 'پرونده', icon: '📁', description: 'مدیریت چرخه حیات محصول',
tools: [
{id: 'clear-all', title: 'پاکسازی', icon: '🗑️', action: 'clearAll', shortcut: null, description: 'پاکسازی کامل همه موارد'},
{id: 'save-product', title: 'ذخیره', icon: '💾', action: 'saveProduct', shortcut: null, description: 'دانلود داده‌های محصول'},
{id: 'load-product', title: 'بارگذاری', icon: '📂', action: 'loadProduct', shortcut: null, description: 'بارگذاری از فایل JSON'},
{id: 'product-settings', title: 'تنظیمات', icon: '⚙️', action: 'openProductSettings', shortcut: null, description: 'پیکربندی تنظیمات'},
],
},
{
id: 'reports', title: 'گزارش‌ها', icon: '📊', description: 'تحلیل و گزارش‌گیری',
tools: [
{id: 'view-reports', title: 'نمایش گزارش‌ها', icon: '📈', action: 'viewReports', shortcut: null, description: 'آمار کلی محصول'},
{id: 'dependency-graph', title: 'نمودار وابستگی', icon: '🕸️', action: 'showDependencyGraph', shortcut: null, description: 'نمایش بصری وابستگی‌ها'},
{id: 'galaxy-chart', title: 'نمودار کهکشانی', icon: '🌌', action: 'showGalaxyChart', shortcut: null, description: 'نمایش کهکشانی ساختار محصولات و روابط بین آن‌ها'},
],
},
],
appData: {
environments: [
{id: 'env-prod-001', name: 'Production', description: 'محیط عملیاتی اصلی', backgroundColor: '#38a169', textColor: '#ffffff'},
{id: 'env-stage-002', name: 'Staging', description: 'محیط پیش از تولید', backgroundColor: '#dd6b20', textColor: '#ffffff'},
{id: 'env-dev-003', name: 'Development', description: 'محیط توسعه', backgroundColor: '#3182ce', textColor: '#ffffff'},
],
products: [
{
id: 'prod-1001', title: 'سامانه فروشگاه آنلاین', description: 'پلتفرم جامع فروش محصولات',
metadata: {createdAt: '2026-09-01T00:00:00Z', owner: 'تیم توسعه محصول', customFields: {}},
projects: [
{
id: 'proj-1001-01', title: 'Backend API Service', description: 'سرویس بک‌اند اصلی',
type: 'backend', repositoryUrl: '',
metadata: {framework: '.NET 8', language: 'C#'},
dependencies: [],
lastDeployedVersions: [
{environmentId: 'env-prod-001', version: '2.3.1', deploymentDate: '2026-09-10T14:30:00Z'},
{environmentId: 'env-stage-002', version: '2.4.0-rc.1', deploymentDate: '2026-09-12T09:00:00Z'},
],
deploymentHistory: [],
},
{
id: 'proj-1001-02', title: 'Frontend Web Application', description: 'اپلیکیشن فرانت‌اند',
type: 'frontend', repositoryUrl: '',
metadata: {framework: 'Angular 18', language: 'TypeScript'},
dependencies: ['proj-1001-01'],
lastDeployedVersions: [
{environmentId: 'env-prod-001', version: '3.1.0', deploymentDate: '2026-09-11T10:00:00Z'},
],
deploymentHistory: [],
},
{
id: 'proj-1001-03', title: 'Payment Gateway Service', description: 'سرویس درگاه پرداخت',
type: 'service', repositoryUrl: '',
metadata: {framework: '.NET 8', language: 'C#'},
dependencies: ['proj-1001-01'],
lastDeployedVersions: [
{environmentId: 'env-prod-001', version: '1.2.0', deploymentDate: '2026-09-08T16:00:00Z'},
],
deploymentHistory: [],
},
],
},
],
},
};
const VALID_PROJECT_TYPES = [
{value: 'backend', label: 'بک‌اند (Backend)', icon: '🖥️'},
{value: 'frontend', label: 'فرانت‌اند (Frontend)', icon: '🌐'},
{value: 'service', label: 'میکروسرویس (Service)', icon: '⚙️'},
{value: 'database', label: 'پایگاه داده (Database)', icon: '🗄️'},
{value: 'mobile', label: 'موبایل (Mobile)', icon: '📱'},
{value: 'infrastructure', label: 'زیرساخت (Infrastructure)', icon: '🏗️'}
];
function validateAppDataStructure(data) {
if (!data || typeof data !== 'object') return false;
if (!Array.isArray(data.environments)) return false;
if (!Array.isArray(data.products)) return false;
return true;
}
function populateProjectTypeDropdowns() {
const selects = ['newProjectType', 'editProjType'];
selects.forEach(selectId => {
const select = document.getElementById(selectId);
if (!select) return;
const currentValue = select.value;
select.innerHTML = '<option value="">انتخاب کنید...</option>' +
VALID_PROJECT_TYPES.map(t => `<option value="${t.value}">${t.icon} ${t.label}</option>`).join('');
if (currentValue) select.value = currentValue;
});
}
// ============ Modal Manager (Stack-based) ============
const ModalManager = {
openStack: [],
open(id) {
const m = document.getElementById(id);
if (!m) return;
m.classList.add('active');
if (!this.openStack.includes(id)) {
this.openStack.push(id);
}
},
close(id) {
const m = document.getElementById(id);
if (!m) return;
m.classList.remove('active');
this.openStack = this.openStack.filter(x => x !== id);
},
closeTopmost() {
if (this.openStack.length === 0) return false;
const topId = this.openStack[this.openStack.length - 1];
this.close(topId);
return true;
},
closeAll() {
const ids = [...this.openStack];
ids.forEach(id => this.close(id));
},
isAnyOpen() {
return this.openStack.length > 0;
},
init() {
document.querySelectorAll('[data-close-modal]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const modal = e.target.closest('.modal-overlay');
if (modal) this.close(modal.id);
});
});
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) this.close(overlay.id);
});
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
e.preventDefault();
this.closeTopmost();
}
});
}
};
const ToolbarManager = {
currentFilters: {query: '', typeFilter: '', envFilter: ''},
init() {
this.populateEnvironmentFilter();
this.bindEvents();
this.updateStats();
},
populateEnvironmentFilter() {
const select = document.getElementById('filterEnvironment');
if (!select) return;
const envs = projectData.appData.environments || [];
select.innerHTML = '<option value="">همه محیط‌ها</option>' +
envs.map(e => `<option value="${e.id}">${e.name}</option>`).join('');
},
bindEvents() {
const searchInput = document.getElementById('toolbarSearch');
const filterType = document.getElementById('filterType');
const filterEnv = document.getElementById('filterEnvironment');
if (searchInput) searchInput.addEventListener('input', () => {this.applyFilters(); BoardRenderer.render(); EnvironmentRenderer.render();});
if (filterType) filterType.addEventListener('change', () => {this.applyFilters(); BoardRenderer.render(); EnvironmentRenderer.render();});
if (filterEnv) filterEnv.addEventListener('change', () => {this.applyFilters(); BoardRenderer.render(); EnvironmentRenderer.render();});
},
applyFilters() {
this.currentFilters = {
query: (document.getElementById('toolbarSearch')?.value || '').trim().toLowerCase(),
typeFilter: document.getElementById('filterType')?.value || '',
envFilter: document.getElementById('filterEnvironment')?.value || ''
};
this.updateStats();
},
updateStats() {
const products = projectData.appData.products || [];
let totalProjects = 0;
let totalDeps = 0;
products.forEach(p => {
const projects = p.projects || [];
projects.forEach(pr => {
totalProjects++;
totalDeps += (pr.dependencies || []).length;
});
});
const set = (id, val) => {const el = document.getElementById(id); if (el) el.textContent = val;};
set('statTotal', products.length + totalProjects);
set('statProjects', products.length);
set('statSubProjects', totalProjects);
set('statDependencies', totalDeps);
},
shouldShowProduct(product) {
const f = this.currentFilters;
if (!f.query && !f.typeFilter && !f.envFilter) return true;
const projects = product.projects || [];
return projects.some(pr => {
const matchQuery = !f.query || pr.title.toLowerCase().includes(f.query) || product.title.toLowerCase().includes(f.query) || (pr.description || '').toLowerCase().includes(f.query);
const matchType = !f.typeFilter || pr.type === f.typeFilter;
const matchEnv = !f.envFilter || (pr.lastDeployedVersions || []).some(d => d.environmentId === f.envFilter);
return matchQuery && matchType && matchEnv;
});
}
};
// ============ History Manager (Undo/Redo) ============
const HistoryManager = {
undoStack: [],
redoStack: [],
maxSize: 50,
isUndoRedoInProgress: false,
pushState() {
if (this.isUndoRedoInProgress) return;
const state = JSON.parse(JSON.stringify(projectData.appData));
this.undoStack.push(state);
if (this.undoStack.length > this.maxSize) this.undoStack.shift();
this.redoStack = [];
this.updateButtons();
},
undo() {
if (this.undoStack.length === 0) {
showToast('هیچ تغییر برای بازگشت وجود ندارد', 'warning');
return;
}
const currentState = JSON.parse(JSON.stringify(projectData.appData));
this.redoStack.push(currentState);
const previousState = this.undoStack.pop();
this.isUndoRedoInProgress = true;
projectData.appData = previousState;
this.isUndoRedoInProgress = false;
this.updateUI();
this.updateButtons();
showToast('↩️ بازگشت به حالت قبلی', 'success');
},
redo() {
if (this.redoStack.length === 0) {
showToast('هیچ تغییر برای باز انجام وجود ندارد', 'warning');
return;
}
const currentState = JSON.parse(JSON.stringify(projectData.appData));
this.undoStack.push(currentState);
const nextState = this.redoStack.pop();
this.isUndoRedoInProgress = true;
projectData.appData = nextState;
this.isUndoRedoInProgress = false;
this.updateUI();
this.updateButtons();
showToast('↪️ باز انجام تغییر', 'success');
},
updateUI() {
BoardRenderer.render();
EnvironmentRenderer.render();
ToolbarManager.updateStats();
if (ProductManager.currentDetailProductId) {
ProductManager.showDetail(ProductManager.currentDetailProductId);
}
},
updateButtons() {
const undoBtn = document.getElementById('undoBtn');
const redoBtn = document.getElementById('redoBtn');
if (undoBtn) undoBtn.disabled = this.undoStack.length === 0;
if (redoBtn) redoBtn.disabled = this.redoStack.length === 0;
},
init() {
this.pushState();
document.getElementById('undoBtn').addEventListener('click', () => this.undo());
document.getElementById('redoBtn').addEventListener('click', () => this.redo());
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
this.undo();
}
if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
e.preventDefault();
this.redo();
}
});
}
};
// ============ Section Collapse Manager ============
const SectionCollapseManager = {
storageKey: 'boardSectionCollapseState',
getState() {
try {
const saved = localStorage.getItem(this.storageKey);
return saved ? JSON.parse(saved) : {products: false, environments: false};
} catch {
return {products: false, environments: false};
}
},
saveState(state) {
try {
localStorage.setItem(this.storageKey, JSON.stringify(state));
} catch (e) {
console.warn('Failed to save collapse state:', e);
}
},
toggle(sectionId) {
const state = this.getState();
state[sectionId] = !state[sectionId];
this.saveState(state);
const section = document.getElementById(`section-${sectionId}`);
if (section) {
section.classList.toggle('collapsed', state[sectionId]);
}
return state[sectionId];
},
init() {
const state = this.getState();
Object.keys(state).forEach(sectionId => {
const section = document.getElementById(`section-${sectionId}`);
if (section && state[sectionId]) {
section.classList.add('collapsed');
}
});
document.getElementById('boardArea').addEventListener('click', (e) => {
const headerTitle = e.target.closest('.board-header-title');
if (headerTitle) {
const section = headerTitle.closest('.board-section');
if (section) {
const sectionId = section.id.replace('section-', '');
const isCollapsed = this.toggle(sectionId);
showToast(isCollapsed ? 'بخش بسته شد' : 'بخش باز شد', 'info', 1500);
}
}
});
}
};
// ============ Board Renderer (Products) ============
const BoardRenderer = {
render() {
const board = document.getElementById('boardArea');
const products = projectData.appData.products || [];
const filteredProducts = products.filter(p => ToolbarManager.shouldShowProduct(p));
let html = '';
html += `<div class="board-section" id="section-products">`;
html += `<div class="board-header"><div class="board-header-title"><span class="collapse-icon">▼</span>📦 محصولات (${filteredProducts.length} از ${products.length})</div><div class="board-header-actions"><button class="btn btn-accent" onclick="ProductManager.openAddProductModal()">➕ محصول جدید</button></div></div>`;
html += `<div class="section-content">`;
if (products.length === 0) {
html += `<div class="board-empty"><div class="board-empty-icon">📦</div><h2>هیچ محصولی تعریف نشده است</h2><p>برای شروع، از دکمه "ایجاد محصول" استفاده کنید</p></div>`;
} else if (filteredProducts.length === 0) {
html += '<div class="no-results">🔍 نتیجه‌ای با فیلترهای فعلی یافت نشد</div>';
} else {
html += '<div class="projects-grid">';
filteredProducts.forEach(prod => {
const projects = prod.projects || [];
const typeCounts = {};
projects.forEach(pr => {typeCounts[pr.type] = (typeCounts[pr.type] || 0) + 1;});
const totalDeps = projects.reduce((sum, pr) => sum + (pr.dependencies || []).length, 0);
const totalDeploys = projects.reduce((sum, pr) => sum + (pr.lastDeployedVersions || []).length, 0);
html += `<div class="project-card" data-product-id="${prod.id}"><div class="project-card-header"><div class="project-card-title-row"><span class="project-card-icon">📦</span><div class="project-card-title">${prod.title}</div></div><div class="project-card-desc">${prod.description || 'بدون توضیحات'}</div></div><div class="project-card-body"><div class="project-stat-row"><span>🌿 پروژه‌ها:</span><span class="project-stat-value">${projects.length}</span></div><div class="project-stat-row"><span>🔗 وابستگی‌ها:</span><span class="project-stat-value">${totalDeps}</span></div><div class="project-stat-row"><span>🚀 استقرارها:</span><span class="project-stat-value">${totalDeploys}</span></div>${Object.keys(typeCounts).length > 0 ? `<div class="project-type-badges">${Object.entries(typeCounts).map(([type, count]) => {const typeInfo = VALID_PROJECT_TYPES.find(t => t.value === type) || {label: type}; return `<span class="type-badge type-${type}">${typeInfo.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}')" title="نمایش جزئیات">👁️ جزئیات</button></div><button class="btn btn-danger btn-icon" onclick="ProductManager.removeProduct('${prod.id}')" title="حذف">🗑️</button></div></div>`;
});
html += '</div>';
}
html += `</div></div>`;
html += EnvironmentRenderer.getSectionHTML();
board.innerHTML = html;
const state = SectionCollapseManager.getState();
Object.keys(state).forEach(sectionId => {
const section = document.getElementById(`section-${sectionId}`);
if (section && state[sectionId]) {
section.classList.add('collapsed');
}
});
}
};
// ============ Environment Renderer ============
const EnvironmentRenderer = {
getSectionHTML() {
const envs = projectData.appData.environments || [];
let html = `<div class="board-section" id="section-environments">`;
html += `<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>`;
html += `<div class="section-content">`;
if (envs.length === 0) {
html += `<div class="board-empty" style="height: auto; padding: 40px;"><div class="board-empty-icon">🌍</div><h2>هیچ محیطی تعریف نشده است</h2><p>برای شروع، از دکمه "محیط جدید" استفاده کنید</p></div>`;
} else {
html += '<div class="env-card-grid">';
envs.forEach(env => {
const bgColor = env.backgroundColor || '#38a169';
const textColor = env.textColor || '#ffffff';
html += `
<div class="env-card-item">
<div class="env-card-preview" style="background-color: ${bgColor}; color: ${textColor};">
${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}')" title="ویرایش">✏️ ویرایش</button>
<button class="btn btn-danger btn-sm" onclick="EnvironmentManager.remove('${env.id}')" title="حذف">🗑️ حذف</button>
</div>
</div>
`;
});
html += '</div>';
}
html += `</div></div>`;
return html;
},
render() {
const board = document.getElementById('boardArea');
if (board) {
BoardRenderer.render();
}
}
};
const ActionRegistry = {
openProductSettings() {openSettingsModal();},
saveProduct() {
projectData.metadata.updatedAt = new Date().toISOString();
const jsonString = JSON.stringify(projectData.appData, null, 2);
const blob = new Blob([jsonString], {type: 'application/json;charset=utf-8'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
a.download = `${projectData.projectName}_v${projectData.version}_${timestamp}.json`;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast(`💾 داده‌های محصول دانلود شد`, 'success', 4000);
WebhookManager.sendEvent('product_saved', {fileName: a.download});
},
loadProduct() {
const input = document.createElement('input');
input.type = 'file'; input.accept = '.json,application/json';
input.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const loaded = JSON.parse(ev.target.result);
if (!validateAppDataStructure(loaded)) return showToast('❌ ساختار فایل نامعتبر است', 'error', 5000);
HistoryManager.pushState();
projectData.appData = loaded;
projectData.metadata.updatedAt = new Date().toISOString();
updateProjectUI();
renderToolGroups();
WebhookManager.updateStatusUI();
ToolbarManager.populateEnvironmentFilter();
ToolbarManager.updateStats();
BoardRenderer.render();
showToast(`📂 محصول بارگذاری شد`, 'success', 4000);
WebhookManager.sendEvent('product_loaded', {fileName: file.name});
} catch (err) {showToast('❌ خطا در خواندن فایل: ' + err.message, 'error', 5000);}
};
reader.readAsText(file);
});
input.click();
},
clearAll() {
if (!confirm('⚠️ هشدار: آیا از پاکسازی کامل همه موارد (محصولات، پروژه‌ها، محیط‌ها و وابستگی‌ها) اطمینان دارید؟\nاین عمل غیرقابل بازگشت است!')) {
return;
}
if (!confirm('⚠️ تأیید نهایی: تمام داده‌ها حذف خواهند شد. آیا مطمئن هستید؟')) {
return;
}
HistoryManager.pushState();
projectData.appData = {
environments: [],
products: []
};
ToolbarManager.populateEnvironmentFilter();
ToolbarManager.updateStats();
BoardRenderer.render();
showToast('🗑️ تمام موارد با موفقیت پاکسازی شدند', 'success', 4000);
WebhookManager.sendEvent('all_cleared', {});
},
viewReports() {
const p = projectData.appData.products.length;
const s = projectData.appData.products.reduce((x, p) => x + p.projects.length, 0);
const e = projectData.appData.environments.length;
showToast(`📊 محصولات: ${p} | پروژه‌ها: ${s} | محیط‌ها: ${e}`, 'info', 5000);
},
showDependencyGraph() {showToast(`🕸️ نمودار وابستگی...`, 'info', 3000);},
showGalaxyChart() {GalaxyChartManager.open();},
defaultAction(tool) {showToast(`ابزار: ${tool.title}`, 'info', 3000);}
};
function showToast(message, type = 'info', duration = 3000) {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icons = {success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️'};
toast.innerHTML = `<span>${icons[type] || 'ℹ️'}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => {
toast.style.transition = 'opacity 0.3s, transform 0.3s';
toast.style.opacity = '0';
toast.style.transform = 'translateX(-30px)';
setTimeout(() => toast.remove(), 300);
}, duration);
}
const WebhookManager = {
getConfig() {return projectData.webhook;},
setConfig(config) {projectData.webhook = {...projectData.webhook, ...config}; this.updateStatusUI();},
isValid() {const cfg = this.getConfig(); if (!cfg.enabled || !cfg.url || !cfg.clientId) return false; return /^https?:\/\//i.test(cfg.url);},
updateStatusUI() {
const cfg = this.getConfig();
const dot = document.getElementById('webhookStatusDot');
const text = document.getElementById('webhookStatusText');
if (!dot || !text) return;
dot.className = 'webhook-status-dot';
if (!cfg.enabled) {text.textContent = 'وضعیت: غیرفعال';}
else if (!cfg.url || !cfg.clientId) {dot.classList.add('pending'); text.textContent = 'وضعیت: تنظیمات ناقص';}
else if (!/^https?:\/\//i.test(cfg.url)) {dot.classList.add('error'); text.textContent = 'وضعیت: آدرس نامعتبر';}
else {dot.classList.add('success'); text.textContent = 'وضعیت: آماده ارسال';}
},
async sendEvent(eventName, payload = {}) {
if (!this.isValid()) return {success: false, reason: 'invalid_config'};
const cfg = this.getConfig();
try {
const response = await fetch(cfg.url, {method: 'POST', headers: {'Content-Type': 'application/json', 'X-Client-Id': cfg.clientId, 'X-Event-Name': eventName}, body: JSON.stringify({event: eventName, timestamp: new Date().toISOString(), projectId: projectData.projectId, data: payload})});
return {success: response.ok, status: response.status};
} catch (err) {console.error('[Webhook] خطا:', err); return {success: false, reason: 'network_error'};}
}
};
const EnvironmentManager = {
currentEditEnvId: null,
openAddModal() {
document.getElementById('newEnvName').value = '';
document.getElementById('newEnvDesc').value = '';
document.getElementById('newEnvBgColor').value = '#38a169';
document.getElementById('newEnvTextColor').value = '#ffffff';
document.getElementById('newEnvBgColorText').textContent = '#38a169';
document.getElementById('newEnvTextColorText').textContent = '#ffffff';
ModalManager.open('envManagerModal');
},
saveNewEnv() {
const name = document.getElementById('newEnvName').value.trim();
const desc = document.getElementById('newEnvDesc').value.trim();
const bgColor = document.getElementById('newEnvBgColor').value;
const textColor = document.getElementById('newEnvTextColor').value;
if (!name) {
showToast('نام محیط الزامی است', 'error');
return;
}
if (projectData.appData.environments.some(e => e.name.toLowerCase() === name.toLowerCase())) {
showToast('محیطی با این نام وجود دارد', 'warning');
return;
}
HistoryManager.pushState();
projectData.appData.environments.push({
id: `env-${Date.now()}`,
name,
description: desc || 'بدون توضیحات',
backgroundColor: bgColor,
textColor: textColor
});
ModalManager.close('envManagerModal');
ToolbarManager.populateEnvironmentFilter();
ToolbarManager.updateStats();
BoardRenderer.render();
showToast(`محیط "${name}" اضافه شد`, 'success');
WebhookManager.sendEvent('environment_added', {envName: name});
},
openEditModal(envId) {
const env = projectData.appData.environments.find(e => e.id === envId);
if (!env) return;
this.currentEditEnvId = envId;
document.getElementById('editEnvId').value = env.id;
document.getElementById('editEnvName').value = env.name;
document.getElementById('editEnvDesc').value = env.description || '';
document.getElementById('editEnvBgColor').value = env.backgroundColor || '#38a169';
document.getElementById('editEnvTextColor').value = env.textColor || '#ffffff';
document.getElementById('editEnvBgColorText').textContent = env.backgroundColor || '#38a169';
document.getElementById('editEnvTextColorText').textContent = env.textColor || '#ffffff';
this.updateEditPreview();
ModalManager.open('envEditModal');
},
updateEditPreview() {
const bgColor = document.getElementById('editEnvBgColor').value;
const textColor = document.getElementById('editEnvTextColor').value;
const preview = document.getElementById('editEnvPreview');
preview.style.backgroundColor = bgColor;
preview.style.color = textColor;
preview.textContent = 'نمونه متن';
},
saveEdit() {
const envId = this.currentEditEnvId;
if (!envId) return;
const env = projectData.appData.environments.find(e => e.id === envId);
if (!env) return;
const newName = document.getElementById('editEnvName').value.trim();
const newDesc = document.getElementById('editEnvDesc').value.trim();
const newBgColor = document.getElementById('editEnvBgColor').value;
const newTextColor = document.getElementById('editEnvTextColor').value;
if (!newName) {
showToast('نام محیط الزامی است', 'error');
return;
}
HistoryManager.pushState();
env.name = newName;
env.description = newDesc;
env.backgroundColor = newBgColor;
env.textColor = newTextColor;
ModalManager.close('envEditModal');
ToolbarManager.populateEnvironmentFilter();
BoardRenderer.render();
showToast(`محیط "${env.name}" ویرایش شد`, 'success');
WebhookManager.sendEvent('environment_updated', {envId, name: env.name});
},
remove(id) {
const env = projectData.appData.environments.find(e => e.id === id);
if (!env) return;
if (!confirm(`حذف محیط "${env.name}"؟`)) return;
HistoryManager.pushState();
projectData.appData.environments = projectData.appData.environments.filter(e => e.id !== id);
ToolbarManager.populateEnvironmentFilter();
ToolbarManager.updateStats();
BoardRenderer.render();
showToast(`محیط "${env.name}" حذف شد`, 'warning');
WebhookManager.sendEvent('environment_removed', {envId: id});
}
};
// ============ Galaxy Chart Manager (D3.js) ============
const GalaxyChartManager = {
simulation: null,
svg: null,
zoomGroup: null,
zoomBehavior: null,
showLabels: true,
currentZoom: 1,
currentSearchQuery: '',
NODE_COLORS: {
product: '#c9a961',
backend: '#4299e1',
frontend: '#48bb78',
service: '#ecc94b',
database: '#ed64a6',
mobile: '#9f7aea',
infrastructure: '#ed8936'
},
open() {
ModalManager.open('galaxyChartModal');
this.currentSearchQuery = '';
const searchInput = document.getElementById('galaxySearchInput');
if (searchInput) searchInput.value = '';
setTimeout(() => this.render(), 150);
},
buildGraphData() {
const nodes = [];
const links = [];
const products = projectData.appData.products || [];
products.forEach(prod => {
nodes.push({
id: prod.id,
label: prod.title,
type: 'product',
radius: 32,
data: prod
});
(prod.projects || []).forEach(pr => {
nodes.push({
id: pr.id,
label: pr.title,
type: 'project',
projectType: pr.type,
radius: 18,
data: pr,
productId: prod.id
});
links.push({
source: prod.id,
target: pr.id,
type: 'ownership'
});
(pr.dependencies || []).forEach(dep => {
const normalized = ProductManager.normalizeDependency(dep);
links.push({
source: pr.id,
target: normalized.projectId,
type: 'dependency',
version: normalized.version,
environmentId: normalized.environmentId
});
});
});
});
return { nodes, links };
},
getNodeColor(d) {
if (d.type === 'product') return this.NODE_COLORS.product;
return this.NODE_COLORS[d.projectType] || '#718096';
},
getNodeIcon(d) {
if (d.type === 'product') return '📦';
const typeInfo = ProductManager.getTypeInfo(d.projectType);
return typeInfo.icon || '❓';
},
render() {
const container = document.getElementById('galaxyChartContainer');
container.innerHTML = '';
const width = container.clientWidth || 800;
const height = container.clientHeight || 600;
const { nodes, links } = this.buildGraphData();
if (nodes.length === 0) {
container.innerHTML = '<div class="galaxy-empty"><div class="galaxy-empty-icon">🌌</div><div>هیچ داده‌ای برای نمایش وجود ندارد</div><div style="font-size: 13px; opacity: 0.7;">ابتدا یک محصول ایجاد کنید</div></div>';
this.updateStats(0, 0);
return;
}
const svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', [0, 0, width, height])
.attr('id', 'galaxySvg');
this.svg = svg;
// Dark space background
svg.append('rect')
.attr('width', width)
.attr('height', height)
.attr('fill', '#0a0e27');
// Stars background
const starsGroup = svg.append('g').attr('class', 'stars');
for (let i = 0; i < 150; i++) {
starsGroup.append('circle')
.attr('cx', Math.random() * width)
.attr('cy', Math.random() * height)
.attr('r', Math.random() * 1.2 + 0.3)
.attr('fill', '#ffffff')
.attr('opacity', Math.random() * 0.6 + 0.1);
}
// Glow filter
const defs = svg.append('defs');
const glowFilter = defs.append('filter')
.attr('id', 'galaxyGlow')
.attr('x', '-50%')
.attr('y', '-50%')
.attr('width', '200%')
.attr('height', '200%');
glowFilter.append('feGaussianBlur')
.attr('stdDeviation', '4')
.attr('result', 'coloredBlur');
const feMerge = glowFilter.append('feMerge');
feMerge.append('feMergeNode').attr('in', 'coloredBlur');
feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
// Zoom group
this.zoomGroup = svg.append('g').attr('class', 'zoom-group');
// Zoom behavior
this.zoomBehavior = d3.zoom()
.scaleExtent([0.2, 5])
.on('zoom', (event) => {
this.zoomGroup.attr('transform', event.transform);
this.currentZoom = event.transform.k;
document.getElementById('galaxyZoomIndicator').textContent = `🔍 ${Math.round(this.currentZoom * 100)}%`;
});
svg.call(this.zoomBehavior);
// Force simulation
this.simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.id).distance(d => {
if (d.type === 'ownership') return 100;
return 180;
}).strength(d => d.type === 'ownership' ? 0.7 : 0.3))
.force('charge', d3.forceManyBody().strength(-400))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(d => d.radius + 15));
// Links
const link = this.zoomGroup.append('g')
.selectAll('line')
.data(links)
.join('line')
.attr('class', d => `galaxy-link galaxy-link-${d.type}`)
.attr('stroke', d => d.type === 'dependency' ? '#e0c789' : '#4a5568')
.attr('stroke-width', d => d.type === 'dependency' ? 2.5 : 1.5)
.attr('stroke-opacity', d => d.type === 'dependency' ? 0.8 : 0.5)
.attr('stroke-dasharray', d => d.type === 'dependency' ? '6,4' : 'none');
this.links = link;
// Node groups
const node = this.zoomGroup.append('g')
.selectAll('g')
.data(nodes)
.join('g')
.attr('class', 'galaxy-node')
.style('cursor', 'pointer')
.call(d3.drag()
.on('start', (event, d) => this.dragStarted(event, d))
.on('drag', (event, d) => this.dragged(event, d))
.on('end', (event, d) => this.dragEnded(event, d)));
// Highlight ring (for search)
node.append('circle')
.attr('class', 'galaxy-highlight-ring')
.attr('r', d => d.radius + 10)
.attr('fill', 'none')
.attr('stroke', '#ffffff')
.attr('stroke-width', 2)
.attr('opacity', 0);
// Outer glow circle
node.append('circle')
.attr('r', d => d.radius + 6)
.attr('fill', d => this.getNodeColor(d))
.attr('opacity', 0.25)
.attr('filter', 'url(#galaxyGlow)');
// Main circle
node.append('circle')
.attr('r', d => d.radius)
.attr('fill', d => this.getNodeColor(d))
.attr('stroke', '#ffffff')
.attr('stroke-width', d => d.type === 'product' ? 3 : 2)
.attr('stroke-opacity', 0.8)
.attr('filter', 'url(#galaxyGlow)');
// Icon text
node.append('text')
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.attr('font-size', d => d.type === 'product' ? '20px' : '14px')
.text(d => this.getNodeIcon(d))
.style('pointer-events', 'none');
// Labels
const labels = node.append('text')
.attr('class', 'galaxy-label')
.attr('dy', d => d.radius + 18)
.attr('text-anchor', 'middle')
.attr('fill', '#ffffff')
.attr('font-size', d => d.type === 'product' ? '13px' : '11px')
.attr('font-weight', d => d.type === 'product' ? '700' : '500')
.attr('font-family', 'Vazirmatn, sans-serif')
.style('pointer-events', 'none')
.style('text-shadow', '0 1px 3px rgba(0,0,0,0.8)')
.text(d => d.label)
.style('display', this.showLabels ? 'block' : 'none');
this.labels = labels;
this.nodes = node;
// Tooltip events
node.on('mouseover', (event, d) => this.showTooltip(event, d))
.on('mousemove', (event) => this.moveTooltip(event))
.on('mouseout', () => this.hideTooltip())
.on('click', (event, d) => {
event.stopPropagation();
this.showNodeInfo(d);
});
// Simulation tick
this.simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node.attr('transform', d => `translate(${d.x},${d.y})`);
});
this.renderLegend();
this.updateStats(nodes.length, links.length);
this.bindToolbarEvents();
// Reapply search if exists
if (this.currentSearchQuery) {
this.search(this.currentSearchQuery);
}
},
dragStarted(event, d) {
if (!event.active) this.simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
},
dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
},
dragEnded(event, d) {
if (!event.active) this.simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
},
showTooltip(event, d) {
const tooltip = document.getElementById('galaxyTooltip');
let subtitle = '';
if (d.type === 'product') {
const projects = d.data.projects || [];
subtitle = `${projects.length} پروژه | مالک: ${d.data.metadata?.owner || '—'}`;
} else {
const typeInfo = ProductManager.getTypeInfo(d.projectType);
subtitle = `${typeInfo.label} | ${d.data.metadata?.framework || 'بدون فریم‌ورک'}`;
}
tooltip.innerHTML = `
<div class="galaxy-tooltip-title">${this.getNodeIcon(d)} ${d.label}</div>
<div class="galaxy-tooltip-subtitle">${subtitle}</div>
`;
tooltip.classList.add('visible');
this.moveTooltip(event);
},
moveTooltip(event) {
const tooltip = document.getElementById('galaxyTooltip');
tooltip.style.left = (event.clientX + 15) + 'px';
tooltip.style.top = (event.clientY + 15) + 'px';
},
hideTooltip() {
document.getElementById('galaxyTooltip').classList.remove('visible');
},
showNodeInfo(d) {
const panel = document.getElementById('galaxyInfoPanel');
const content = document.getElementById('galaxyInfoContent');
let html = `<h4>${this.getNodeIcon(d)} ${d.label}</h4>`;
if (d.type === 'product') {
const projects = d.data.projects || [];
const totalDeps = projects.reduce((sum, p) => sum + (p.dependencies || []).length, 0);
const totalDeploys = projects.reduce((sum, p) => sum + (p.lastDeployedVersions || []).length, 0);
html += `
<div class="galaxy-info-row"><span class="galaxy-info-label">نوع</span><span class="galaxy-info-value">📦 محصول</span></div>
<div class="galaxy-info-row"><span class="galaxy-info-label">توضیحات</span><span class="galaxy-info-value">${d.data.description || '—'}</span></div>
<div class="galaxy-info-row"><span class="galaxy-info-label">مالک</span><span class="galaxy-info-value">${d.data.metadata?.owner || '—'}</span></div>
<div class="galaxy-info-row"><span class="galaxy-info-label">تعداد پروژه</span><span class="galaxy-info-value">${projects.length}</span></div>
<div class="galaxy-info-row"><span class="galaxy-info-label">وابستگی‌ها</span><span class="galaxy-info-value">${totalDeps}</span></div>
<div class="galaxy-info-row"><span class="galaxy-info-label">استقرارها</span><span class="galaxy-info-value">${totalDeploys}</span></div>
`;
} else {
const typeInfo = ProductManager.getTypeInfo(d.projectType);
const deploys = d.data.lastDeployedVersions || [];
const deps = d.data.dependencies || [];
html += `
<div class="galaxy-info-row"><span class="galaxy-info-label">نوع</span><span class="galaxy-info-value">${typeInfo.label}</span></div>
<div class="galaxy-info-row"><span class="galaxy-info-label">توضیحات</span><span class="galaxy-info-value">${d.data.description || '—'}</span></div>
<div class="galaxy-info-row"><span class="galaxy-info-label">فریم‌ورک</span><span class="galaxy-info-value">${d.data.metadata?.framework || '—'}</span></div>
<div class="galaxy-info-row"><span class="galaxy-info-label">زبان</span><span class="galaxy-info-value">${d.data.metadata?.language || '—'}</span></div>
<div class="galaxy-info-row"><span class="galaxy-info-label">وابستگی‌ها</span><span class="galaxy-info-value">${deps.length}</span></div>
<div class="galaxy-info-row"><span class="galaxy-info-label">استقرارها</span><span class="galaxy-info-value">${deploys.length}</span></div>
`;
}
content.innerHTML = html;
panel.classList.add('visible');
},
// ============ SEARCH ============
search(query) {
this.currentSearchQuery = query || '';
const q = (query || '').trim().toLowerCase();
const clearBtn = document.getElementById('galaxySearchClear');
const countEl = document.getElementById('galaxySearchCount');
if (!this.nodes || !this.links) return;
if (!q) {
this.nodes.classed('dimmed', false).classed('highlighted', false);
this.links.classed('dimmed', false);
if (countEl) {
countEl.classList.remove('visible', 'no-results');
countEl.textContent = '';
}
if (clearBtn) clearBtn.classList.remove('visible');
return;
}
if (clearBtn) clearBtn.classList.add('visible');
const matchIds = new Set();
this.nodes.each(function(d) {
const label = (d.label || '').toLowerCase();
const desc = ((d.data?.description) || '').toLowerCase();
const type = (d.projectType || d.type || '').toLowerCase();
const framework = ((d.data?.metadata?.framework) || '').toLowerCase();
const language = ((d.data?.metadata?.language) || '').toLowerCase();
const owner = ((d.data?.metadata?.owner) || '').toLowerCase();
if (label.includes(q) || desc.includes(q) || type.includes(q) ||
framework.includes(q) || language.includes(q) || owner.includes(q)) {
matchIds.add(d.id);
}
});
this.nodes
.classed('dimmed', d => !matchIds.has(d.id))
.classed('highlighted', d => matchIds.has(d.id));
this.links.classed('dimmed', d => {
const srcId = typeof d.source === 'object' ? d.source.id : d.source;
const tgtId = typeof d.target === 'object' ? d.target.id : d.target;
return !(matchIds.has(srcId) && matchIds.has(tgtId));
});
if (countEl) {
if (matchIds.size === 0) {
countEl.textContent = 'بدون نتیجه';
countEl.classList.add('visible', 'no-results');
} else {
countEl.textContent = `${matchIds.size} نتیجه`;
countEl.classList.add('visible');
countEl.classList.remove('no-results');
}
}
},
clearSearch() {
const searchInput = document.getElementById('galaxySearchInput');
if (searchInput) {
searchInput.value = '';
searchInput.focus();
}
this.search('');
},
renderLegend() {
const legend = document.getElementById('galaxyLegend');
let html = '<div class="galaxy-legend-title">🎨 راهنما</div>';
html += `<div class="galaxy-legend-item"><div class="galaxy-legend-color" style="background-color: ${this.NODE_COLORS.product};"></div><span>📦 محصول</span></div>`;
VALID_PROJECT_TYPES.forEach(t => {
html += `<div class="galaxy-legend-item"><div class="galaxy-legend-color" style="background-color: ${this.NODE_COLORS[t.value]};"></div><span>${t.icon} ${t.label.split(' ')[0]}</span></div>`;
});
html += `<div class="galaxy-legend-item" style="margin-top: 8px;"><div class="galaxy-legend-line" style="background: #4a5568;"></div><span>مالکیت</span></div>`;
html += `<div class="galaxy-legend-item"><div class="galaxy-legend-line" style="background: repeating-linear-gradient(90deg, #e0c789 0px, #e0c789 6px, transparent 6px, transparent 10px);"></div><span>وابستگی</span></div>`;
legend.innerHTML = html;
},
updateStats(nodeCount, linkCount) {
const products = projectData.appData.products.length;
const projects = projectData.appData.products.reduce((x, p) => x + p.projects.length, 0);
const deps = linkCount - projects;
document.getElementById('galaxyStats').innerHTML = `
<div class="galaxy-stat-item">📦 محصول: <span class="galaxy-stat-value">${products}</span></div>
<div class="galaxy-stat-item">🌿 پروژه: <span class="galaxy-stat-value">${projects}</span></div>
<div class="galaxy-stat-item">🔗 وابستگی: <span class="galaxy-stat-value">${Math.max(0, deps)}</span></div>
<div class="galaxy-stat-item">⭐ نودها: <span class="galaxy-stat-value">${nodeCount}</span></div>
<div class="galaxy-stat-item">🔗 یال‌ها: <span class="galaxy-stat-value">${linkCount}</span></div>
`;
},
bindToolbarEvents() {
document.getElementById('galaxyZoomIn').onclick = () => {
this.svg.transition().duration(300).call(this.zoomBehavior.scaleBy, 1.3);
};
document.getElementById('galaxyZoomOut').onclick = () => {
this.svg.transition().duration(300).call(this.zoomBehavior.scaleBy, 0.7);
};
document.getElementById('galaxyResetView').onclick = () => {
this.svg.transition().duration(500).call(this.zoomBehavior.transform, d3.zoomIdentity);
};
document.getElementById('galaxyToggleLabels').onclick = () => {
this.showLabels = !this.showLabels;
if (this.labels) {
this.labels.style('display', this.showLabels ? 'block' : 'none');
}
showToast(this.showLabels ? 'برچسب‌ها نمایش داده شدند' : 'برچسب‌ها پنهان شدند', 'info', 2000);
};
document.getElementById('galaxyExportPNG').onclick = () => this.exportPNG();
document.getElementById('galaxyExportSVG').onclick = () => this.exportSVG();
document.getElementById('galaxyInfoClose').onclick = () => {
document.getElementById('galaxyInfoPanel').classList.remove('visible');
};
// Search bindings
const searchInput = document.getElementById('galaxySearchInput');
const clearBtn = document.getElementById('galaxySearchClear');
if (searchInput) {
searchInput.oninput = (e) => this.search(e.target.value);
searchInput.onkeydown = (e) => {
if (e.key === 'Escape') {
e.stopPropagation();
this.clearSearch();
}
};
}
if (clearBtn) {
clearBtn.onclick = () => this.clearSearch();
}
},
exportPNG() {
const svgElement = document.getElementById('galaxySvg');
if (!svgElement) {
showToast('نموداری برای خروجی وجود ندارد', 'error');
return;
}
const svgData = new XMLSerializer().serializeToString(svgElement);
const svgBlob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(svgBlob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const scale = 2;
canvas.width = svgElement.clientWidth * scale;
canvas.height = svgElement.clientHeight * scale;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0a0e27';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
URL.revokeObjectURL(url);
const a = document.createElement('a');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
a.download = `galaxy-chart_${timestamp}.png`;
a.href = canvas.toDataURL('image/png');
a.click();
showToast('📷 خروجی PNG ذخیره شد', 'success');
WebhookManager.sendEvent('galaxy_chart_exported', {format: 'png'});
};
img.src = url;
},
exportSVG() {
const svgElement = document.getElementById('galaxySvg');
if (!svgElement) {
showToast('نموداری برای خروجی وجود ندارد', 'error');
return;
}
const svgData = new XMLSerializer().serializeToString(svgElement);
const blob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
a.download = `galaxy-chart_${timestamp}.svg`;
a.href = url;
a.click();
URL.revokeObjectURL(url);
showToast('📐 خروجی SVG ذخیره شد', 'success');
WebhookManager.sendEvent('galaxy_chart_exported', {format: 'svg'});
}
};
const ProductManager = {
currentDetailProductId: null,
currentEditProjectId: null,
currentDependencyProjectId: null,
currentDependencyProjId: null,
currentDeploymentProjId: null,
currentDeploymentMode: null,
currentDeploymentEnvId: null,
getTypeInfo(type) {
return VALID_PROJECT_TYPES.find(t => t.value === type) || {value: type, label: type, icon: '❓'};
},
getEnvironmentName(envId) {
const env = projectData.appData.environments.find(e => e.id === envId);
return env ? env.name : envId;
},
getEnvironmentColors(envId) {
const env = projectData.appData.environments.find(e => e.id === envId);
return {
backgroundColor: env?.backgroundColor || '#38a169',
textColor: env?.textColor || '#ffffff'
};
},
getEmptyEnvironmentIds(proj) {
const allEnvIds = projectData.appData.environments.map(e => e.id);
const deployedEnvIds = (proj.lastDeployedVersions || []).map(d => d.environmentId);
return allEnvIds.filter(id => !deployedEnvIds.includes(id));
},
normalizeDependency(dep) {
if (typeof dep === 'string') {
return {projectId: dep, environmentId: null, version: null};
}
return dep;
},
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((sum, pr) => sum + (pr.lastDeployedVersions || []).length, 0);
let html = `<div class="detail-section"><div class="detail-section-header"><span>ℹ️ اطلاعات محصول</span><div class="detail-section-header-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.openProductEditModal()" title="ویرایش اطلاعات محصول">✏️ ویرایش</button><span style="font-size: 11px; opacity: 0.8; display: none;">${prod.id}</span></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()" title="افزودن پروژه جدید">➕ افزودن</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 typeInfo = this.getTypeInfo(pr.type);
const deps = pr.dependencies || [];
const deploys = pr.lastDeployedVersions || [];
const emptyEnvIds = this.getEmptyEnvironmentIds(pr);
const canAddDeployment = emptyEnvIds.length > 0;
html += `<div class="subproject-card"><div class="subproject-header"><div class="subproject-title"><span>${typeInfo.icon}</span><span>${pr.title}</span><span class="type-badge type-${pr.type}">${typeInfo.label}</span></div><div class="subproject-actions"><button class="btn btn-ghost btn-sm" onclick="ProductManager.openProjectEditModal('${prod.id}', '${pr.id}')" title="ویرایش">✏️</button><button class="btn btn-danger btn-sm" onclick="ProductManager.removeProject('${prod.id}', '${pr.id}')" title="حذف">🗑️</button></div></div><div class="subproject-meta">${pr.description || 'بدون توضیحات'}</div><div class="detail-info-grid" style="margin-bottom: 8px;">${pr.metadata?.framework ? `<div class="detail-info-item"><span class="detail-info-label">فریم‌ورک</span><span class="detail-info-value">${pr.metadata.framework}</span></div>` : ''}${pr.metadata?.language ? `<div class="detail-info-item"><span class="detail-info-label">زبان</span><span class="detail-info-value">${pr.metadata.language}</span></div>` : ''}${deps.length > 0 ? `<div class="detail-info-item"><span class="detail-info-label">وابستگی‌ها (${deps.length})</span><span class="detail-info-value">${deps.map(d => {const normalized = this.normalizeDependency(d); const depPr = this.findProjectById(normalized.projectId); return depPr ? depPr.title : normalized.projectId;}).join('، ')}</span></div>` : ''}</div>`;
html += `<div class="deployment-section"><div class="deployment-header"><span class="deployment-header-title">🚀 نسخه‌های استقرار (${deploys.length})</span>`;
if (canAddDeployment) {
html += `<button class="btn btn-accent btn-xs" onclick="ProductManager.openAddDeploymentModal('${prod.id}', '${pr.id}')" title="افزودن نسخه استقرار جدید">➕ افزودن</button>`;
} else {
html += `<span style="font-size: 10px; color: var(--text-muted); font-style: italic;" title="همه محیط‌ها استقرار دارند">✓ کامل</span>`;
}
html += `</div>`;
if (deploys.length === 0) {
html += '<div class="empty-deployments">هنوز نسخه‌ای استقرار نیافته است</div>';
} else {
html += '<div class="deployment-list">';
deploys.forEach(d => {
const envName = this.getEnvironmentName(d.environmentId);
const colors = this.getEnvironmentColors(d.environmentId);
const formattedDate = 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: ${colors.backgroundColor}; color: ${colors.textColor};">${envName}</span><span class="deployment-version">v${d.version}</span><span class="deployment-date">📅 ${formattedDate}</span></div><div class="deployment-actions"><button class="btn btn-ghost btn-xs" onclick="ProductManager.openEditDeploymentModal('${prod.id}', '${pr.id}', '${d.environmentId}', '${d.version}', '${d.deploymentDate}')" title="ویرایش">✏️</button><button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}', '${pr.id}', '${d.environmentId}', '${d.version}')" title="حذف">🗑️</button></div></div>`;
});
html += '</div>';
}
html += `</div>`;
html += `</div>`;
});
html += '</div>';
}
html += `</div></div>`;
document.getElementById('projectDetailBody').innerHTML = html;
ModalManager.open('projectDetailModal');
},
findProjectById(projectId) {
for (const prod of projectData.appData.products) {
const pr = prod.projects.find(p => p.id === projectId);
if (pr) return pr;
}
return null;
},
openAddProductModal() {
document.getElementById('newProductTitle').value = '';
document.getElementById('newProductDescription').value = '';
document.getElementById('newProductOwner').value = '';
document.getElementById('newProductCreatedAt').value = new Date().toLocaleDateString('fa-IR');
this.clearProductFormErrors();
ModalManager.open('addProductModal');
setTimeout(() => {document.getElementById('newProductTitle').focus();}, 100);
},
clearProductFormErrors() {
const titleInput = document.getElementById('newProductTitle');
const titleError = document.getElementById('newProductTitleError');
if (titleInput) titleInput.classList.remove('error');
if (titleError) titleError.classList.remove('visible');
},
createNewProduct() {
const title = document.getElementById('newProductTitle').value.trim();
const description = document.getElementById('newProductDescription').value.trim();
const owner = document.getElementById('newProductOwner').value.trim();
if (!title) {
showToast('عنوان محصول الزامی است', 'error');
document.getElementById('newProductTitle').classList.add('error');
document.getElementById('newProductTitleError').classList.add('visible');
return;
}
if (title.length < 3) {
showToast('عنوان محصول باید حداقل ۳ کاراکتر باشد', 'error');
document.getElementById('newProductTitle').classList.add('error');
document.getElementById('newProductTitleError').classList.add('visible');
return;
}
const exists = projectData.appData.products.some(p => p.title.toLowerCase() === title.toLowerCase());
if (exists) {
showToast('محصولی با این عنوان قبلاً ثبت شده است', 'warning');
return;
}
HistoryManager.pushState();
const newProduct = {
id: `prod-${Date.now()}`,
title: title,
description: description || 'بدون توضیحات',
metadata: {createdAt: new Date().toISOString(), owner: owner || '', customFields: {}},
projects: []
};
projectData.appData.products.push(newProduct);
ModalManager.close('addProductModal');
ToolbarManager.updateStats();
BoardRenderer.render();
showToast(`محصول "${title}" با موفقیت ایجاد شد`, 'success');
WebhookManager.sendEvent('product_created', {productId: newProduct.id, title});
},
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 container = document.getElementById('currentDependenciesList');
const deps = pr.dependencies || [];
if (deps.length === 0) {
container.innerHTML = '<div class="empty-dependencies">هیچ وابستگی‌ای تعریف نشده است</div>';
return;
}
container.innerHTML = deps.map((dep, index) => {
const normalized = this.normalizeDependency(dep);
const depPr = this.findProjectById(normalized.projectId);
const projectName = depPr ? depPr.title : normalized.projectId;
const envName = normalized.environmentId ? this.getEnvironmentName(normalized.environmentId) : '—';
const version = normalized.version || '—';
return `
<div class="dependency-item">
<div class="dependency-info">
<div>
<div class="dependency-project-name">${projectName}</div>
<div class="dependency-version-info">
<span class="deployment-env-tag" style="background-color: ${this.getEnvironmentColors(normalized.environmentId).backgroundColor}; color: ${this.getEnvironmentColors(normalized.environmentId).textColor}; font-size: 9px; padding: 1px 6px;">${envName}</span>
<span style="font-family: monospace; font-weight: 600;">v${version}</span>
</div>
</div>
</div>
<button class="btn btn-danger btn-xs" onclick="ProductManager.removeDependency(${index})" title="حذف وابستگی">🗑️</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');
if (this.currentDetailProductId) this.showDetail(this.currentDetailProductId);
ToolbarManager.updateStats();
showToast('وابستگی حذف شد', 'success');
WebhookManager.sendEvent('dependency_removed', {projectId: this.currentDependencyProjId});
},
renderAllProjectsList() {
const resultsContainer = document.getElementById('dependencySearchResults');
const currentProd = projectData.appData.products.find(p => p.id === this.currentDependencyProjectId);
if (!currentProd) return;
const currentPr = currentProd.projects.find(p => p.id === this.currentDependencyProjId);
if (!currentPr) return;
const allProjects = [];
projectData.appData.products.forEach(prod => {
prod.projects.forEach(pr => {
if (pr.id !== currentPr.id) allProjects.push({...pr, productName: prod.title, productId: prod.id});
});
});
if (allProjects.length === 0) {
resultsContainer.innerHTML = '<div class="empty-dependencies">پروژه دیگری برای انتخاب وجود ندارد</div>';
return;
}
resultsContainer.innerHTML = allProjects.map(pr => {
const versions = this.getAvailableVersions(pr);
return `
<div class="dependency-project-result">
<div class="dependency-project-header">
<span>📦 ${pr.title} <small style="color: var(--text-muted); font-weight: normal;">(${pr.productName})</small></span>
<span style="font-size: 11px; color: var(--text-muted);">${versions.length} نسخه</span>
</div>
<div class="dependency-versions-list">
${versions.length > 0 ? versions.map(v => `
<div class="dependency-version-item" onclick="ProductManager.addDependency('${pr.id}', '${v.environmentId}', '${v.version}', '${v.source}')">
<div>
<span class="dependency-version-badge ${v.source === 'history' ? 'history' : ''}">${v.source === 'history' ? '📜 تاریخچه' : '🚀 فعلی'}</span>
<span style="font-family: monospace; font-weight: 600; margin-right: 8px;">v${v.version}</span>
<span style="font-size: 11px; color: var(--text-muted);">${this.getEnvironmentName(v.environmentId)}</span>
</div>
<span style="font-size: 10px; color: var(--text-muted);">${new Date(v.deploymentDate).toLocaleDateString('fa-IR')}</span>
</div>
`).join('') : '<div style="padding: 8px; text-align: center; color: var(--text-muted); font-size: 11px;">نسخه‌ای موجود نیست</div>'}
</div>
</div>
`;
}).join('');
},
setupDependencySearch() {
const searchInput = document.getElementById('dependencySearch');
const resultsContainer = document.getElementById('dependencySearchResults');
searchInput.value = '';
searchInput.addEventListener('input', (e) => {
const query = e.target.value.trim().toLowerCase();
if (query.length === 0) {this.renderAllProjectsList(); return;}
if (query.length < 2) {resultsContainer.innerHTML = '<div class="empty-dependencies">حداقل ۲ کاراکتر وارد کنید</div>'; return;}
const currentProd = projectData.appData.products.find(p => p.id === this.currentDependencyProjectId);
if (!currentProd) return;
const currentPr = currentProd.projects.find(p => p.id === this.currentDependencyProjId);
if (!currentPr) return;
const allProjects = [];
projectData.appData.products.forEach(prod => {
prod.projects.forEach(pr => {
if (pr.id !== currentPr.id) allProjects.push({...pr, productName: prod.title, productId: prod.id});
});
});
const filtered = allProjects.filter(pr =>
pr.title.toLowerCase().includes(query) ||
pr.productName.toLowerCase().includes(query) ||
pr.description.toLowerCase().includes(query)
);
if (filtered.length === 0) {
resultsContainer.innerHTML = '<div class="empty-dependencies">نتیجه‌ای یافت نشد</div>';
return;
}
resultsContainer.innerHTML = filtered.map(pr => {
const versions = this.getAvailableVersions(pr);
return `
<div class="dependency-project-result">
<div class="dependency-project-header">
<span>📦 ${pr.title} <small style="color: var(--text-muted); font-weight: normal;">(${pr.productName})</small></span>
<span style="font-size: 11px; color: var(--text-muted);">${versions.length} نسخه</span>
</div>
<div class="dependency-versions-list">
${versions.length > 0 ? versions.map(v => `
<div class="dependency-version-item" onclick="ProductManager.addDependency('${pr.id}', '${v.environmentId}', '${v.version}', '${v.source}')">
<div>
<span class="dependency-version-badge ${v.source === 'history' ? 'history' : ''}">${v.source === 'history' ? '📜 تاریخچه' : '🚀 فعلی'}</span>
<span style="font-family: monospace; font-weight: 600; margin-right: 8px;">v${v.version}</span>
<span style="font-size: 11px; color: var(--text-muted);">${this.getEnvironmentName(v.environmentId)}</span>
</div>
<span style="font-size: 10px; color: var(--text-muted);">${new Date(v.deploymentDate).toLocaleDateString('fa-IR')}</span>
</div>
`).join('') : '<div style="padding: 8px; text-align: center; color: var(--text-muted); font-size: 11px;">نسخه‌ای موجود نیست</div>'}
</div>
</div>
`;
}).join('');
});
},
getAvailableVersions(pr) {
const versions = [];
(pr.lastDeployedVersions || []).forEach(v => {
versions.push({environmentId: v.environmentId, version: v.version, deploymentDate: v.deploymentDate, source: 'current'});
});
(pr.deploymentHistory || []).forEach(v => {
versions.push({environmentId: v.environmentId, version: v.version, deploymentDate: v.deploymentDate || v.replacedAt, source: 'history'});
});
return versions;
},
addDependency(projectId, environmentId, version, source) {
const prod = projectData.appData.products.find(p => p.id === this.currentDependencyProjectId);
if (!prod) return;
const pr = prod.projects.find(p => p.id === this.currentDependencyProjId);
if (!pr) return;
const exists = (pr.dependencies || []).some(dep => {
const normalized = this.normalizeDependency(dep);
return normalized.projectId === projectId && normalized.environmentId === environmentId && normalized.version === version;
});
if (exists) {
showToast('این وابستگی قبلاً اضافه شده است (تکراری)', 'warning');
return;
}
HistoryManager.pushState();
if (!pr.dependencies) pr.dependencies = [];
pr.dependencies.push({projectId: projectId, environmentId: environmentId, version: version});
ModalManager.close('dependencyManagerModal');
if (this.currentDetailProductId) this.showDetail(this.currentDetailProductId);
ToolbarManager.updateStats();
showToast(`وابستگی به ${this.findProjectById(projectId)?.title || projectId} اضافه شد`, 'success');
WebhookManager.sendEvent('dependency_added', {projectId: this.currentDependencyProjId, dependencyProjectId: projectId});
},
openProductEditModal() {
const productId = this.currentDetailProductId;
if (!productId) return;
const prod = projectData.appData.products.find(p => p.id === productId);
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 productId = this.currentDetailProductId;
if (!productId) return;
const prod = projectData.appData.products.find(p => p.id === productId);
if (!prod) return;
const newTitle = document.getElementById('editProductTitle').value.trim();
if (!newTitle) {showToast('عنوان محصول الزامی است', 'error'); return;}
HistoryManager.pushState();
prod.title = newTitle;
prod.description = document.getElementById('editProductDescription').value.trim();
if (!prod.metadata) prod.metadata = {};
prod.metadata.owner = document.getElementById('editProductOwner').value.trim();
prod.metadata.updatedAt = new Date().toISOString();
ModalManager.close('productEditModal');
BoardRenderer.render();
this.showDetail(productId);
showToast(`محصول "${prod.title}" با موفقیت ویرایش شد`, 'success');
WebhookManager.sendEvent('product_edited', {productId, title: prod.title});
},
openAddProjectModal() {
if (!this.currentDetailProductId) {showToast('ابتدا یک محصول را انتخاب کنید', 'warning'); return;}
document.getElementById('newProjectTitle').value = '';
document.getElementById('newProjectType').value = '';
document.getElementById('newProjectFramework').value = '';
document.getElementById('newProjectLanguage').value = '';
document.getElementById('newProjectRepo').value = '';
document.getElementById('newProjectDescription').value = '';
populateProjectTypeDropdowns();
ModalManager.open('addProjectModal');
},
saveNewProject() {
const productId = this.currentDetailProductId;
if (!productId) return;
const prod = projectData.appData.products.find(p => p.id === productId);
if (!prod) return;
const title = document.getElementById('newProjectTitle').value.trim();
const type = document.getElementById('newProjectType').value;
const framework = document.getElementById('newProjectFramework').value.trim();
const language = document.getElementById('newProjectLanguage').value.trim();
const repo = document.getElementById('newProjectRepo').value.trim();
const description = document.getElementById('newProjectDescription').value.trim();
if (!title) {showToast('عنوان پروژه الزامی است', 'error'); return;}
if (!type) {showToast('نوع پروژه الزامی است', 'error'); return;}
HistoryManager.pushState();
const newProject = {
id: `proj-${Date.now()}`, title: title, description: description || 'توضیحات پیش‌فرض',
type: type, repositoryUrl: repo,
metadata: {createdAt: new Date().toISOString(), customFields: {}},
dependencies: [], lastDeployedVersions: [], deploymentHistory: []
};
if (framework) newProject.metadata.framework = framework;
if (language) newProject.metadata.language = language;
prod.projects.push(newProject);
ToolbarManager.updateStats();
ModalManager.close('addProjectModal');
this.showDetail(productId);
showToast(`پروژه "${title}" (${type}) ایجاد شد`, 'success');
WebhookManager.sendEvent('project_added', {productId, type, title});
},
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 productId = this.currentDetailProductId;
const projId = this.currentEditProjectId;
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 newTitle = document.getElementById('editProjTitle').value.trim();
const newType = document.getElementById('editProjType').value;
const newFramework = document.getElementById('editProjFramework').value.trim();
const newLanguage = document.getElementById('editProjLanguage').value.trim();
const newRepo = document.getElementById('editProjRepo').value.trim();
const newDesc = document.getElementById('editProjDescription').value.trim();
if (!newTitle) {showToast('عنوان پروژه الزامی است', 'error'); return;}
if (!newType) {showToast('نوع پروژه الزامی است', 'error'); return;}
HistoryManager.pushState();
pr.title = newTitle;
pr.type = newType;
pr.description = newDesc || 'توضیحات پیش‌فرض';
pr.repositoryUrl = newRepo;
if (!pr.metadata) pr.metadata = {};
if (newFramework) pr.metadata.framework = newFramework; else delete pr.metadata.framework;
if (newLanguage) pr.metadata.language = newLanguage; else delete pr.metadata.language;
pr.metadata.updatedAt = new Date().toISOString();
ModalManager.close('editProjectModal');
ToolbarManager.updateStats();
this.showDetail(productId);
showToast(`پروژه "${pr.title}" با موفقیت ویرایش شد`, 'success');
WebhookManager.sendEvent('project_edited', {productId, projId, title: pr.title, type: pr.type});
},
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 emptyEnvIds = this.getEmptyEnvironmentIds(pr);
if (emptyEnvIds.length === 0) {
showToast('همه محیط‌ها دارای نسخه استقرار هستند', 'warning');
return;
}
this.currentDeploymentProjId = projId;
this.currentDeploymentMode = 'add';
this.currentDeploymentEnvId = null;
document.getElementById('deploymentModalTitle').innerHTML = '<span>🚀</span><span>افزودن نسخه استقرار</span>';
const envGroup = document.getElementById('deploymentEnvGroup');
const envs = projectData.appData.environments.filter(e => emptyEnvIds.includes(e.id));
envGroup.innerHTML = `
<label class="form-label">محیط استقرار <span class="required">*</span></label>
<select class="form-select" id="depEnvironment">
<option value="">انتخاب محیط...</option>
${envs.map(e => {
const bgColor = e.backgroundColor || '#38a169';
const textColor = e.textColor || '#ffffff';
return `<option value="${e.id}" style="background-color: ${bgColor}; color: ${textColor};">${e.name} - ${e.description}</option>`;
}).join('')}
</select>
`;
document.getElementById('depVersion').value = '';
document.getElementById('depDate').value = '';
ModalManager.open('deploymentModal');
},
openEditDeploymentModal(productId, projId, envId, version, date) {
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;
this.currentDeploymentProjId = projId;
this.currentDeploymentMode = 'edit';
this.currentDeploymentEnvId = envId;
document.getElementById('deploymentModalTitle').innerHTML = '<span>✏️</span><span>ویرایش نسخه استقرار</span>';
const env = projectData.appData.environments.find(e => e.id === envId);
const envName = env ? env.name : envId;
const colors = this.getEnvironmentColors(envId);
const envGroup = document.getElementById('deploymentEnvGroup');
envGroup.innerHTML = `
<label class="form-label">محیط استقرار <span class="readonly-badge">فقط خواندنی</span></label>
<div class="env-read-only-tag" style="background-color: ${colors.backgroundColor}; color: ${colors.textColor};">
${envName}
</div>
`;
document.getElementById('depVersion').value = version;
let dateValue = '';
if (date) {
const d = new Date(date);
const pad = (n) => n.toString().padStart(2, '0');
dateValue = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
document.getElementById('depDate').value = dateValue;
ModalManager.open('deploymentModal');
},
saveDeployment() {
const productId = this.currentDetailProductId;
const projId = this.currentDeploymentProjId;
const mode = this.currentDeploymentMode;
if (!productId || !projId) {showToast('خطا: محصول یا پروژه مشخص نشده است', 'error'); return;}
const prod = projectData.appData.products.find(p => p.id === productId);
if (!prod) {showToast('خطا: محصول یافت نشد', 'error'); return;}
const pr = prod.projects.find(p => p.id === projId);
if (!pr) {showToast('خطا: پروژه یافت نشد', 'error'); return;}
const version = document.getElementById('depVersion').value.trim();
const date = document.getElementById('depDate').value;
if (!version) {showToast('نسخه الزامی است', 'error'); return;}
if (!date) {showToast('تاریخ استقرار الزامی است', 'error'); return;}
const deploymentDate = new Date(date).toISOString();
HistoryManager.pushState();
if (!pr.deploymentHistory) pr.deploymentHistory = [];
if (mode === 'add') {
const envId = document.getElementById('depEnvironment').value;
if (!envId) {showToast('انتخاب محیط الزامی است', 'error'); return;}
const existingIndex = pr.lastDeployedVersions.findIndex(d => d.environmentId === envId);
if (existingIndex >= 0) {
const old = pr.lastDeployedVersions[existingIndex];
pr.deploymentHistory.push({...old, replacedAt: deploymentDate, replacedByVersion: version});
pr.lastDeployedVersions[existingIndex] = {environmentId: envId, version: version, deploymentDate: deploymentDate};
showToast(`نسخه استقرار در ${this.getEnvironmentName(envId)} از v${old.version} به v${version} به‌روز شد`, 'success');
WebhookManager.sendEvent('deployment_updated', {productId, projId, envId, oldVersion: old.version, newVersion: version});
} else {
pr.lastDeployedVersions.push({environmentId: envId, version: version, deploymentDate: deploymentDate});
showToast(`نسخه v${version} در ${this.getEnvironmentName(envId)} استقرار یافت`, 'success');
WebhookManager.sendEvent('deployment_added', {productId, projId, envId, version});
}
} else if (mode === 'edit') {
const envId = this.currentDeploymentEnvId;
if (!envId) {showToast('خطا: محیط استقرار مشخص نشده است', 'error'); return;}
const currentIndex = pr.lastDeployedVersions.findIndex(d => d.environmentId === envId);
if (currentIndex >= 0) {
const old = pr.lastDeployedVersions[currentIndex];
if (old.version !== version) {
pr.deploymentHistory.push({...old, replacedAt: deploymentDate, replacedByVersion: version});
}
pr.lastDeployedVersions[currentIndex] = {environmentId: envId, version: version, deploymentDate: deploymentDate};
showToast(`نسخه استقرار در ${this.getEnvironmentName(envId)} به v${version} به‌روز شد`, 'success');
WebhookManager.sendEvent('deployment_updated', {productId, projId, envId, version});
} else {
showToast('خطا: نسخه استقرار یافت نشد', 'error');
return;
}
}
ModalManager.close('deploymentModal');
ToolbarManager.updateStats();
this.showDetail(productId);
},
removeDeployment(productId, projId, envId, version) {
if (!confirm(`حذف نسخه استقرار v${version} از ${this.getEnvironmentName(envId)}؟`)) 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;
HistoryManager.pushState();
if (!pr.deploymentHistory) pr.deploymentHistory = [];
const deployIndex = pr.lastDeployedVersions.findIndex(d => d.environmentId === envId && d.version === version);
if (deployIndex >= 0) {
const removed = pr.lastDeployedVersions[deployIndex];
pr.deploymentHistory.push({...removed, removedAt: new Date().toISOString(), status: 'removed'});
pr.lastDeployedVersions.splice(deployIndex, 1);
}
ToolbarManager.updateStats();
this.showDetail(productId);
showToast(`نسخه استقرار v${version} حذف شد`, 'warning');
WebhookManager.sendEvent('deployment_removed', {productId, projId, envId, version});
},
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);
ToolbarManager.updateStats();
BoardRenderer.render();
ModalManager.close('projectDetailModal');
showToast(`محصول "${prod.title}" حذف شد`, 'warning');
WebhookManager.sendEvent('product_removed', {productId});
},
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);
ToolbarManager.updateStats();
if (this.currentDetailProductId === productId) this.showDetail(productId);
showToast(`پروژه "${pr.title}" حذف شد`, 'warning');
WebhookManager.sendEvent('project_removed', {productId, projId});
}
};
document.getElementById('projectDetailDeleteBtn').addEventListener('click', () => {
if (ProductManager.currentDetailProductId) ProductManager.removeProduct(ProductManager.currentDetailProductId);
});
document.getElementById('saveProductEditBtn').addEventListener('click', () => ProductManager.saveProductEdit());
document.getElementById('saveNewProjectBtn').addEventListener('click', () => ProductManager.saveNewProject());
document.getElementById('saveProjEditBtn').addEventListener('click', () => ProductManager.saveProjectEdit());
document.getElementById('saveDeploymentBtn').addEventListener('click', () => ProductManager.saveDeployment());
document.getElementById('saveEnvEditBtn').addEventListener('click', () => EnvironmentManager.saveEdit());
document.getElementById('saveNewProductBtn').addEventListener('click', () => ProductManager.createNewProduct());
document.getElementById('saveNewEnvBtn').addEventListener('click', () => EnvironmentManager.saveNewEnv());
document.getElementById('newEnvBgColor').addEventListener('input', (e) => {
document.getElementById('newEnvBgColorText').textContent = e.target.value;
});
document.getElementById('newEnvTextColor').addEventListener('input', (e) => {
document.getElementById('newEnvTextColorText').textContent = e.target.value;
});
document.getElementById('editEnvBgColor').addEventListener('input', (e) => {
document.getElementById('editEnvBgColorText').textContent = e.target.value;
EnvironmentManager.updateEditPreview();
});
document.getElementById('editEnvTextColor').addEventListener('input', (e) => {
document.getElementById('editEnvTextColorText').textContent = e.target.value;
EnvironmentManager.updateEditPreview();
});
document.getElementById('newProductTitle').addEventListener('input', (e) => {
if (e.target.value.trim().length >= 3) {
e.target.classList.remove('error');
document.getElementById('newProductTitleError').classList.remove('visible');
}
});
function handleToolSelect(tool, group) {
const actionHandler = ActionRegistry[tool.action] || ActionRegistry.defaultAction;
actionHandler(tool, group);
WebhookManager.sendEvent('tool_selected', {toolId: tool.id, toolTitle: tool.title, toolAction: tool.action, groupId: group.id});
}
const themeToggle = document.getElementById('themeToggle');
const themeIcon = document.getElementById('themeIcon');
const 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 current = htmlEl.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
htmlEl.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
if (themeIcon) themeIcon.textContent = next === 'dark' ? '☀️' : '🌙';
});
}
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
const 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 groupEl = document.createElement('div');
groupEl.className = 'sidebar-group';
const headerEl = document.createElement('div');
headerEl.className = 'sidebar-group-header';
if (group.description) headerEl.setAttribute('data-tooltip', group.description);
headerEl.innerHTML = `<div class="group-icon"><span>${group.icon || ''}</span><span>${group.title}</span></div><span class="group-arrow">▼</span>`;
const bodyEl = document.createElement('div');
bodyEl.className = 'sidebar-group-body';
group.tools.forEach(tool => {
const btn = document.createElement('button');
btn.className = 'sidebar-btn';
btn.title = tool.description;
btn.innerHTML = `<span class="btn-icon-emoji">${tool.icon || '🔧'}</span><span class="btn-title">${tool.title}</span>`;
btn.addEventListener('click', () => handleToolSelect(tool, group));
bodyEl.appendChild(btn);
});
headerEl.addEventListener('click', () => groupEl.classList.toggle('collapsed'));
groupEl.appendChild(headerEl);
groupEl.appendChild(bodyEl);
sidebarBody.appendChild(groupEl);
});
}
function openSettingsModal() {
document.getElementById('settingProjectTitle').value = projectData.projectTitle;
document.getElementById('settingVersion').value = projectData.version;
document.getElementById('settingDescription').value = projectData.description;
document.getElementById('settingDeveloper').value = projectData.developer;
document.getElementById('settingCompany').value = projectData.company;
document.getElementById('settingWebhookUrl').value = projectData.webhook.url;
document.getElementById('settingWebhookClientId').value = projectData.webhook.clientId;
document.getElementById('settingWebhookEnabled').checked = projectData.webhook.enabled;
WebhookManager.updateStatusUI();
ModalManager.open('settingsModal');
}
['settingWebhookUrl', 'settingWebhookClientId', 'settingWebhookEnabled'].forEach(id => {
document.getElementById(id).addEventListener('input', () => {
WebhookManager.setConfig({url: document.getElementById('settingWebhookUrl').value, clientId: document.getElementById('settingWebhookClientId').value, enabled: document.getElementById('settingWebhookEnabled').checked});
});
});
document.getElementById('saveSettingsBtn').addEventListener('click', () => {
WebhookManager.setConfig({url: document.getElementById('settingWebhookUrl').value.trim(), clientId: document.getElementById('settingWebhookClientId').value.trim(), enabled: document.getElementById('settingWebhookEnabled').checked});
ModalManager.close('settingsModal');
showToast('تنظیمات ذخیره شد', 'success');
WebhookManager.sendEvent('settings_updated', {updatedFields: ['webhook']});
});
function updateProjectUI() {
document.title = projectData.projectTitle;
document.getElementById('headerTitle').textContent = projectData.projectTitle;
document.getElementById('headerSubtitle').textContent = projectData.description;
document.getElementById('appFooter').innerHTML = `<div><span class="footer-brand">طراح و توسعه‌دهنده:</span> ${projectData.developer}<span style="margin: 0 10px;">|</span><span class="footer-brand">شرکت:</span> ${projectData.company}</div><div><span class="footer-brand">نسخه:</span> ${projectData.version}</div>`;
}
document.addEventListener('keydown', (e) => {
if (!(e.ctrlKey || e.metaKey)) return;
const key = e.key.toLowerCase();
for (const group of projectData.toolGroups) {
const tool = group.tools.find(t => t.shortcut && t.shortcut.toLowerCase().replace('ctrl+', '') === key);
if (tool) {e.preventDefault(); handleToolSelect(tool, group); break;}
}
});
ModalManager.init();
renderToolGroups();
updateProjectUI();
WebhookManager.updateStatusUI();
ToolbarManager.init();
SectionCollapseManager.init();
BoardRenderer.render();
HistoryManager.init();
setTimeout(() => showToast(`${projectData.projectTitle} (نسخه ${projectData.version}) آماده است`, 'success'), 500);
</script>
</body>
</html>