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