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