8899 lines
299 KiB
HTML
8899 lines
299 KiB
HTML
<!doctype html>
|
||
<html lang="fa" dir="rtl">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>دستیار انتشار</title>
|
||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||
<style>
|
||
@import url("https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap");
|
||
|
||
:root {
|
||
--primary: #1a365d;
|
||
--primary-light: #2c5282;
|
||
--primary-dark: #0f2341;
|
||
--accent: #c9a961;
|
||
--accent-dark: #a8893f;
|
||
--accent-light: #e0c789;
|
||
--bg: #f5f7fa;
|
||
--board-bg: #e2e8f0;
|
||
--card-bg: #fff;
|
||
--panel-bg: #fff;
|
||
--text: #1a202c;
|
||
--text-muted: #4a5568;
|
||
--text-light: #718096;
|
||
--border: #cbd5e0;
|
||
--border-light: #e2e8f0;
|
||
--success: #38a169;
|
||
--danger: #e53e3e;
|
||
--warning: #dd6b20;
|
||
--info: #3182ce;
|
||
--column-bg: #edf2f7;
|
||
--shadow:
|
||
0 4px 6px rgba(0, 0, 0, 0.08), 0 10px 20px rgba(26, 54, 93, 0.1);
|
||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.06);
|
||
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||
--radius: 10px;
|
||
--radius-sm: 6px;
|
||
}
|
||
|
||
[data-theme="dark"] {
|
||
--primary: #2c5282;
|
||
--primary-light: #3b6fb5;
|
||
--primary-dark: #1a365d;
|
||
--accent: #e0c789;
|
||
--accent-dark: #c9a961;
|
||
--accent-light: #f0d9a0;
|
||
--bg: #1a202c;
|
||
--board-bg: #2d3748;
|
||
--card-bg: #2d3748;
|
||
--panel-bg: #1f2937;
|
||
--text: #e2e8f0;
|
||
--text-muted: #cbd5e0;
|
||
--text-light: #a0aec0;
|
||
--column-bg: #374151;
|
||
--border: #4a5568;
|
||
--border-light: #374151;
|
||
--shadow: 0 4px 6px rgba(0, 0, 0, 0.3), 0 10px 20px rgba(0, 0, 0, 0.4);
|
||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
body {
|
||
font-family: "Vazirmatn", Tahoma, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
font-size: 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition:
|
||
background 0.3s,
|
||
color 0.3s;
|
||
}
|
||
|
||
.app-header {
|
||
background: linear-gradient(
|
||
135deg,
|
||
var(--primary) 0%,
|
||
var(--primary-light) 100%
|
||
);
|
||
color: #fff;
|
||
padding: 0 20px;
|
||
height: 64px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 3px solid var(--accent);
|
||
box-shadow: var(--shadow);
|
||
z-index: 100;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.app-brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
|
||
.app-logo {
|
||
width: 44px;
|
||
height: 44px;
|
||
background: var(--accent);
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22px;
|
||
font-weight: 800;
|
||
color: var(--primary-dark);
|
||
}
|
||
|
||
.app-brand-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.app-title {
|
||
font-size: 16px;
|
||
font-weight: 800;
|
||
color: #fff;
|
||
}
|
||
|
||
.app-subtitle {
|
||
font-size: 11px;
|
||
opacity: 0.85;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
.btn {
|
||
background: linear-gradient(
|
||
135deg,
|
||
var(--primary-light),
|
||
var(--primary)
|
||
);
|
||
color: #fff;
|
||
border: none;
|
||
padding: 8px 14px;
|
||
border-radius: var(--radius-sm);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
transition: all 0.2s;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-1px);
|
||
filter: brightness(1.1);
|
||
}
|
||
|
||
.btn-accent {
|
||
background: linear-gradient(135deg, var(--accent), var(--accent-dark));
|
||
color: var(--primary-dark);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.btn-ghost {
|
||
background: rgba(26, 54, 93, 0.08);
|
||
border: 1px solid var(--border);
|
||
color: var(--primary);
|
||
}
|
||
|
||
[data-theme="dark"] .btn-ghost {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: linear-gradient(135deg, var(--danger), #c53030);
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-sm {
|
||
padding: 4px 10px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.btn-xs {
|
||
padding: 2px 6px;
|
||
font-size: 10px;
|
||
}
|
||
|
||
.app-main {
|
||
flex: 1;
|
||
display: flex;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.sidebar {
|
||
width: 280px;
|
||
background: var(--panel-bg);
|
||
border-right: 1px solid var(--border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition: width 0.3s;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
z-index: 50;
|
||
}
|
||
|
||
.sidebar.collapsed {
|
||
width: 0;
|
||
}
|
||
|
||
.sidebar-header {
|
||
background: linear-gradient(
|
||
135deg,
|
||
var(--primary),
|
||
var(--primary-light)
|
||
);
|
||
color: #fff;
|
||
padding: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 2px solid var(--accent);
|
||
min-height: 48px;
|
||
}
|
||
|
||
.sidebar-title {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.sidebar-body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 8px;
|
||
}
|
||
|
||
.sidebar-group {
|
||
margin-bottom: 6px;
|
||
border: 1px solid var(--border-light);
|
||
border-radius: var(--radius-sm);
|
||
overflow: hidden;
|
||
background: var(--card-bg);
|
||
}
|
||
|
||
.sidebar-group-header {
|
||
background: linear-gradient(
|
||
135deg,
|
||
var(--primary),
|
||
var(--primary-light)
|
||
);
|
||
color: #fff;
|
||
padding: 10px 12px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
user-select: none;
|
||
}
|
||
|
||
.sidebar-group-header .group-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex: 1;
|
||
}
|
||
|
||
.sidebar-group-header .group-arrow {
|
||
transition: transform 0.3s;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.sidebar-group.collapsed .group-arrow {
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.sidebar-group.collapsed .sidebar-group-body {
|
||
display: none;
|
||
}
|
||
|
||
.sidebar-group-body {
|
||
padding: 6px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.sidebar-btn {
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border-light);
|
||
color: var(--text);
|
||
padding: 8px 10px;
|
||
border-radius: var(--radius-sm);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
text-align: right;
|
||
width: 100%;
|
||
}
|
||
|
||
.sidebar-btn:hover {
|
||
background: linear-gradient(
|
||
135deg,
|
||
var(--primary),
|
||
var(--primary-light)
|
||
);
|
||
color: #fff;
|
||
}
|
||
|
||
.tabs-bar {
|
||
background: linear-gradient(135deg, #0f1724 0%, #1a2a44 100%);
|
||
border-bottom: 2px solid var(--accent);
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 12px;
|
||
min-height: 48px;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.toolbar-controls {
|
||
display: flex;
|
||
gap: 6px;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.toolbar-icon-btn {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
color: #fff;
|
||
width: 34px;
|
||
height: 34px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 15px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.toolbar-icon-btn:hover:not(:disabled) {
|
||
background: var(--accent);
|
||
color: var(--primary-dark);
|
||
}
|
||
|
||
.toolbar-search {
|
||
flex: 1;
|
||
min-width: 180px;
|
||
max-width: 320px;
|
||
position: relative;
|
||
}
|
||
|
||
.search-input {
|
||
width: 100%;
|
||
padding: 7px 32px 7px 12px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
border-radius: 6px;
|
||
color: #fff;
|
||
font-family: inherit;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.search-input::placeholder {
|
||
color: rgba(255, 255, 255, 0.5);
|
||
}
|
||
|
||
.toolbar-filters {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.filter-dropdown {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
border-radius: 6px;
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.filter-select {
|
||
background: transparent;
|
||
border: none;
|
||
color: #fff;
|
||
font-family: inherit;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
padding: 7px 4px;
|
||
cursor: pointer;
|
||
outline: none;
|
||
min-width: 110px;
|
||
}
|
||
|
||
.filter-select option {
|
||
background: var(--primary-dark);
|
||
color: #fff;
|
||
}
|
||
|
||
.toolbar-stats {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-right: auto;
|
||
padding-right: 8px;
|
||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 11px;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
}
|
||
|
||
.stat-value {
|
||
font-weight: 800;
|
||
color: var(--accent);
|
||
background: rgba(201, 169, 97, 0.15);
|
||
padding: 2px 7px;
|
||
border-radius: 10px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.board-area {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 24px;
|
||
background: var(--board-bg);
|
||
}
|
||
|
||
.board-section {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.board-section.collapsed .section-content {
|
||
display: none;
|
||
}
|
||
|
||
.board-section.collapsed .collapse-icon {
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.collapse-icon {
|
||
transition: transform 0.3s;
|
||
font-size: 14px;
|
||
display: inline-block;
|
||
margin-left: 6px;
|
||
}
|
||
|
||
.board-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 2px solid var(--border);
|
||
}
|
||
|
||
.board-header-title {
|
||
font-size: 18px;
|
||
font-weight: 800;
|
||
color: var(--primary);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
[data-theme="dark"] .board-header-title {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.board-header-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.projects-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||
gap: 20px;
|
||
}
|
||
|
||
.project-card {
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
box-shadow: var(--shadow-sm);
|
||
transition: all 0.25s;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.project-card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: var(--shadow-lg);
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.project-card-header {
|
||
background: linear-gradient(
|
||
135deg,
|
||
var(--primary) 0%,
|
||
var(--primary-light) 100%
|
||
);
|
||
color: #fff;
|
||
padding: 16px;
|
||
}
|
||
|
||
.project-card-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.project-card-title {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
flex: 1;
|
||
}
|
||
|
||
.project-card-desc {
|
||
font-size: 12px;
|
||
opacity: 0.9;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.project-card-body {
|
||
padding: 14px 16px;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.project-stat-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.project-stat-value {
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
}
|
||
|
||
[data-theme="dark"] .project-stat-value {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.project-type-badges {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.type-badge {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.type-backend {
|
||
background: #ebf4ff;
|
||
color: #2b6cb0;
|
||
}
|
||
|
||
.type-frontend {
|
||
background: #f0fff4;
|
||
color: #276749;
|
||
}
|
||
|
||
.type-service {
|
||
background: #fefcbf;
|
||
color: #975a16;
|
||
}
|
||
|
||
.type-database {
|
||
background: #fed7e2;
|
||
color: #97266d;
|
||
}
|
||
|
||
.type-mobile {
|
||
background: #e9d8fd;
|
||
color: #553c9a;
|
||
}
|
||
|
||
.type-infrastructure {
|
||
background: #feebc8;
|
||
color: #9c4221;
|
||
}
|
||
|
||
.project-card-footer {
|
||
padding: 12px 16px;
|
||
background: var(--border-light);
|
||
border-top: 1px solid var(--border);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.card-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.env-card-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.env-card-item {
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
box-shadow: var(--shadow-sm);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.env-card-preview {
|
||
height: 60px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
}
|
||
|
||
.env-card-body {
|
||
padding: 14px 16px;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.env-card-name {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.env-card-desc {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.env-card-footer {
|
||
padding: 10px 16px;
|
||
background: var(--border-light);
|
||
border-top: 1px solid var(--border);
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.board-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--text-muted);
|
||
text-align: center;
|
||
padding: 40px;
|
||
}
|
||
|
||
.board-empty-icon {
|
||
font-size: 80px;
|
||
opacity: 0.3;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.board-empty h2 {
|
||
font-size: 20px;
|
||
color: var(--primary);
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
[data-theme="dark"] .board-empty h2 {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.no-results {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: var(--text-muted);
|
||
background: var(--border-light);
|
||
border-radius: var(--radius);
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(26, 54, 93, 0.6);
|
||
backdrop-filter: blur(3px);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
padding: 20px;
|
||
}
|
||
|
||
.modal-overlay.active {
|
||
display: flex;
|
||
}
|
||
|
||
.modal {
|
||
background: var(--panel-bg);
|
||
color: var(--text);
|
||
border-radius: 12px;
|
||
width: 100%;
|
||
max-width: 600px;
|
||
max-height: 90vh;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: var(--shadow-lg);
|
||
}
|
||
|
||
.modal-lg {
|
||
max-width: 800px;
|
||
}
|
||
|
||
.modal-xl {
|
||
max-width: 1000px;
|
||
}
|
||
|
||
.modal-galaxy {
|
||
max-width: 1200px;
|
||
width: 100%;
|
||
height: 85vh;
|
||
max-height: 85vh;
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.galaxy-search {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
background: rgba(255, 255, 255, 0.12);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
border-radius: 6px;
|
||
padding: 0 8px;
|
||
height: 32px;
|
||
min-width: 230px;
|
||
}
|
||
|
||
.galaxy-search-icon {
|
||
font-size: 12px;
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.galaxy-search input {
|
||
flex: 1;
|
||
width: 140px;
|
||
background: transparent;
|
||
border: none;
|
||
color: #fff;
|
||
font-family: inherit;
|
||
font-size: 12px;
|
||
outline: none;
|
||
}
|
||
|
||
.galaxy-collapse-badge {
|
||
pointer-events: none;
|
||
}
|
||
|
||
.galaxy-search input::placeholder {
|
||
color: rgba(255, 255, 255, 0.55);
|
||
}
|
||
|
||
.galaxy-search-clear {
|
||
background: none;
|
||
border: none;
|
||
color: rgba(255, 255, 255, 0.75);
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.galaxy-search-clear.visible {
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.galaxy-node.galaxy-dim {
|
||
opacity: 0.12;
|
||
}
|
||
|
||
.galaxy-node.galaxy-match > circle:nth-of-type(2) {
|
||
stroke: #ffd700;
|
||
stroke-width: 3;
|
||
}
|
||
|
||
.galaxy-link.galaxy-dim {
|
||
stroke-opacity: 0.04 !important;
|
||
}
|
||
|
||
.modal-header {
|
||
background: linear-gradient(
|
||
135deg,
|
||
var(--primary),
|
||
var(--primary-light)
|
||
);
|
||
color: #fff;
|
||
padding: 16px 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 3px solid var(--accent);
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.modal-close {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
border: none;
|
||
color: #fff;
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.modal-close:hover {
|
||
background: var(--danger);
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 14px 20px;
|
||
border-top: 1px solid var(--border);
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
gap: 8px;
|
||
background: var(--border-light);
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.detail-section {
|
||
margin-bottom: 20px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.detail-section-header {
|
||
background: linear-gradient(
|
||
135deg,
|
||
var(--primary),
|
||
var(--primary-light)
|
||
);
|
||
color: #fff;
|
||
padding: 12px 16px;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.detail-section-header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.detail-section-body {
|
||
padding: 16px;
|
||
background: var(--card-bg);
|
||
}
|
||
|
||
.detail-info-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.detail-info-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.detail-info-label {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.detail-info-value {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.subproject-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.subproject-card {
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
padding: 12px;
|
||
background: var(--border-light);
|
||
}
|
||
|
||
.subproject-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.subproject-title {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.subproject-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
|
||
.subproject-meta {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.deployment-section {
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
border-top: 1px dashed var(--border);
|
||
}
|
||
|
||
.deployment-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.deployment-header-title {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.deployment-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.deployment-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 8px 10px;
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border-light);
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
.deployment-item-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.deployment-env-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 10px;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.deployment-version {
|
||
font-family: "Vazirmatn", monospace;
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
font-size: 11px;
|
||
}
|
||
|
||
[data-theme="dark"] .deployment-version {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.deployment-date {
|
||
font-size: 10px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.deployment-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
|
||
.deployment-notes {
|
||
font-size: 10px;
|
||
color: var(--text-muted);
|
||
width: 100%;
|
||
margin-top: 4px;
|
||
padding-top: 4px;
|
||
border-top: 1px dashed var(--border-light);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.deployment-req {
|
||
font-size: 10px;
|
||
color: var(--info);
|
||
width: 100%;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.empty-deployments {
|
||
text-align: center;
|
||
padding: 12px;
|
||
color: var(--text-muted);
|
||
font-size: 11px;
|
||
background: var(--border-light);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.empty-subprojects {
|
||
text-align: center;
|
||
padding: 30px;
|
||
color: var(--text-muted);
|
||
background: var(--border-light);
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.form-label {
|
||
display: block;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--primary);
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
[data-theme="dark"] .form-label {
|
||
color: var(--accent-light);
|
||
}
|
||
|
||
.form-label .required {
|
||
color: var(--danger);
|
||
}
|
||
|
||
.form-input,
|
||
.form-textarea,
|
||
.form-select {
|
||
width: 100%;
|
||
padding: 9px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
font-family: inherit;
|
||
font-size: 13px;
|
||
background: var(--card-bg);
|
||
color: var(--text);
|
||
}
|
||
|
||
.form-input:focus,
|
||
.form-textarea:focus,
|
||
.form-select:focus {
|
||
outline: none;
|
||
border-color: var(--accent);
|
||
box-shadow: 0 0 0 3px rgba(201, 169, 97, 0.15);
|
||
}
|
||
|
||
.form-textarea {
|
||
resize: vertical;
|
||
min-height: 70px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
}
|
||
|
||
.form-hint {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.form-error {
|
||
font-size: 11px;
|
||
color: var(--danger);
|
||
margin-top: 4px;
|
||
display: none;
|
||
}
|
||
|
||
.form-error.visible {
|
||
display: block;
|
||
}
|
||
|
||
.color-input-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 10px;
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
.color-input-wrapper input[type="color"] {
|
||
width: 32px;
|
||
height: 32px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
}
|
||
|
||
.color-preview-text {
|
||
font-size: 11px;
|
||
font-family: monospace;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.webhook-section {
|
||
background: var(--border-light);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
padding: 14px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.webhook-section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.webhook-section-title {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.webhook-toggle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.switch {
|
||
position: relative;
|
||
display: inline-block;
|
||
width: 36px;
|
||
height: 20px;
|
||
}
|
||
|
||
.switch input {
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
|
||
.slider {
|
||
position: absolute;
|
||
cursor: pointer;
|
||
inset: 0;
|
||
background: var(--border);
|
||
transition: 0.3s;
|
||
border-radius: 20px;
|
||
}
|
||
|
||
.slider::before {
|
||
position: absolute;
|
||
content: "";
|
||
height: 14px;
|
||
width: 14px;
|
||
right: 3px;
|
||
bottom: 3px;
|
||
background: #fff;
|
||
transition: 0.3s;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
input:checked + .slider {
|
||
background: var(--success);
|
||
}
|
||
|
||
input:checked + .slider::before {
|
||
transform: translateX(-16px);
|
||
}
|
||
|
||
.webhook-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
padding: 8px 10px;
|
||
background: var(--card-bg);
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.webhook-status-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: var(--text-muted);
|
||
}
|
||
|
||
.webhook-status-dot.success {
|
||
background: var(--success);
|
||
}
|
||
|
||
.webhook-status-dot.error {
|
||
background: var(--danger);
|
||
}
|
||
|
||
.toast-container {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
left: 20px;
|
||
z-index: 2000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.toast {
|
||
background: var(--primary);
|
||
color: #fff;
|
||
padding: 12px 18px;
|
||
border-radius: 8px;
|
||
box-shadow: var(--shadow-lg);
|
||
font-size: 13px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: 250px;
|
||
}
|
||
|
||
.toast.success {
|
||
background: var(--success);
|
||
}
|
||
|
||
.toast.error {
|
||
background: var(--danger);
|
||
}
|
||
|
||
.toast.warning {
|
||
background: var(--warning);
|
||
}
|
||
|
||
.app-footer {
|
||
background: linear-gradient(
|
||
135deg,
|
||
var(--primary),
|
||
var(--primary-light)
|
||
);
|
||
color: #fff;
|
||
padding: 8px 20px;
|
||
font-size: 11px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-top: 2px solid var(--accent);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.footer-brand {
|
||
color: var(--accent);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.form-input[readonly],
|
||
.form-textarea[readonly] {
|
||
background: var(--border-light);
|
||
color: var(--text-muted);
|
||
cursor: not-allowed;
|
||
opacity: 0.85;
|
||
border-style: dashed;
|
||
}
|
||
|
||
.readonly-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
background: var(--border-light);
|
||
border: 1px solid var(--border);
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.readonly-badge::before {
|
||
content: "🔒";
|
||
font-size: 10px;
|
||
}
|
||
|
||
.readonly-info {
|
||
background: linear-gradient(to left, #ebf8ff, #e6f6ff);
|
||
border: 1px solid #90cdf4;
|
||
border-right: 4px solid var(--info);
|
||
padding: 10px 14px;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
margin-bottom: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.dependency-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
margin-bottom: 20px;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.dependency-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 12px;
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
.dependency-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex: 1;
|
||
}
|
||
|
||
.dependency-project-name {
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.dependency-search-box {
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.dependency-search-input {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
font-family: inherit;
|
||
font-size: 13px;
|
||
background: var(--card-bg);
|
||
color: var(--text);
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.dependency-search-results {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.dependency-project-result {
|
||
margin-bottom: 8px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.dependency-project-add-btn {
|
||
padding: 10px 12px;
|
||
background: var(--card-bg);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
width: 100%;
|
||
border: none;
|
||
font-family: inherit;
|
||
text-align: right;
|
||
}
|
||
|
||
.dependency-project-add-btn:hover {
|
||
background: var(--accent-light);
|
||
}
|
||
|
||
.empty-dependencies {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
background: var(--border-light);
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
.dependency-section-title {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
[data-theme="dark"] .dependency-section-title {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.env-color-preview {
|
||
padding: 10px;
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
font-weight: 700;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.env-read-only-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
padding: 6px 10px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.req-deployment-box {
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
padding: 10px;
|
||
margin-bottom: 8px;
|
||
background: var(--border-light);
|
||
}
|
||
|
||
.req-deployment-title {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
margin-bottom: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.req-deployment-select {
|
||
width: 100%;
|
||
padding: 8px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
font-family: inherit;
|
||
font-size: 12px;
|
||
background: var(--card-bg);
|
||
color: var(--text);
|
||
}
|
||
|
||
.galaxy-toolbar {
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.galaxy-toolbar .btn {
|
||
background: rgba(255, 255, 255, 0.12);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
color: #fff;
|
||
font-size: 11px;
|
||
padding: 5px 10px;
|
||
}
|
||
|
||
.galaxy-toolbar .btn:hover {
|
||
background: var(--accent);
|
||
color: var(--primary-dark);
|
||
}
|
||
|
||
.galaxy-modal-body {
|
||
padding: 0;
|
||
position: relative;
|
||
overflow: hidden;
|
||
background: #0a0e27;
|
||
}
|
||
|
||
.galaxy-chart-container {
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: 400px;
|
||
position: relative;
|
||
}
|
||
|
||
.galaxy-chart-container svg {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
cursor: grab;
|
||
}
|
||
|
||
.galaxy-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
color: rgba(255, 255, 255, 0.5);
|
||
text-align: center;
|
||
font-size: 16px;
|
||
gap: 10px;
|
||
}
|
||
|
||
.galaxy-legend {
|
||
position: absolute;
|
||
bottom: 16px;
|
||
right: 16px;
|
||
background: rgba(10, 14, 39, 0.85);
|
||
border: 1px solid rgba(201, 169, 97, 0.3);
|
||
border-radius: 10px;
|
||
padding: 12px 16px;
|
||
color: #fff;
|
||
font-size: 11px;
|
||
z-index: 10;
|
||
max-width: 220px;
|
||
}
|
||
|
||
.galaxy-legend-title {
|
||
font-weight: 700;
|
||
font-size: 12px;
|
||
margin-bottom: 10px;
|
||
color: var(--accent-light);
|
||
}
|
||
|
||
.galaxy-legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.galaxy-legend-color {
|
||
width: 14px;
|
||
height: 14px;
|
||
border-radius: 50%;
|
||
box-shadow: 0 0 8px currentColor;
|
||
}
|
||
|
||
.galaxy-info-panel {
|
||
position: absolute;
|
||
top: 16px;
|
||
left: 16px;
|
||
background: rgba(10, 14, 39, 0.9);
|
||
border: 1px solid rgba(201, 169, 97, 0.3);
|
||
border-radius: 10px;
|
||
padding: 14px 18px;
|
||
color: #fff;
|
||
font-size: 12px;
|
||
z-index: 10;
|
||
max-width: 340px;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
display: none;
|
||
}
|
||
|
||
.galaxy-info-panel.visible {
|
||
display: block;
|
||
}
|
||
|
||
.galaxy-info-panel h4 {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: var(--accent-light);
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.galaxy-info-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 4px 0;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
gap: 12px;
|
||
}
|
||
|
||
.galaxy-info-label {
|
||
color: rgba(255, 255, 255, 0.6);
|
||
font-size: 11px;
|
||
}
|
||
|
||
.galaxy-info-value {
|
||
font-weight: 600;
|
||
font-size: 11px;
|
||
text-align: left;
|
||
}
|
||
|
||
.galaxy-zoom-indicator {
|
||
position: absolute;
|
||
bottom: 16px;
|
||
left: 16px;
|
||
background: rgba(10, 14, 39, 0.85);
|
||
border: 1px solid rgba(201, 169, 97, 0.3);
|
||
border-radius: 8px;
|
||
padding: 6px 12px;
|
||
color: #fff;
|
||
font-size: 12px;
|
||
z-index: 10;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.galaxy-stats {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
display: flex;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.galaxy-stat-value {
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
background: rgba(26, 54, 93, 0.1);
|
||
padding: 2px 8px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
[data-theme="dark"] .galaxy-stat-value {
|
||
color: var(--accent);
|
||
background: rgba(201, 169, 97, 0.15);
|
||
}
|
||
|
||
.galaxy-search {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
background: rgba(255, 255, 255, 0.12);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
border-radius: 6px;
|
||
padding: 0 8px;
|
||
height: 32px;
|
||
min-width: 230px;
|
||
}
|
||
.galaxy-search-icon {
|
||
font-size: 12px;
|
||
opacity: 0.85;
|
||
}
|
||
.galaxy-search input {
|
||
flex: 1;
|
||
width: 140px;
|
||
background: transparent;
|
||
border: none;
|
||
color: #fff;
|
||
font-family: inherit;
|
||
font-size: 12px;
|
||
outline: none;
|
||
}
|
||
.galaxy-search input::placeholder {
|
||
color: rgba(255, 255, 255, 0.55);
|
||
}
|
||
.galaxy-search-clear {
|
||
background: none;
|
||
border: none;
|
||
color: rgba(255, 255, 255, 0.75);
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
.galaxy-search-clear.visible {
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.galaxy-search-results {
|
||
position: absolute;
|
||
top: 60px;
|
||
right: 16px;
|
||
width: 320px;
|
||
max-height: 320px;
|
||
overflow-y: auto;
|
||
background: rgba(10, 14, 39, 0.95);
|
||
border: 1px solid rgba(201, 169, 97, 0.35);
|
||
border-radius: 10px;
|
||
color: #fff;
|
||
z-index: 20;
|
||
display: none;
|
||
padding: 6px;
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
.galaxy-search-results.visible {
|
||
display: block;
|
||
}
|
||
.galaxy-search-result-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
.galaxy-search-result-item:hover {
|
||
background: rgba(201, 169, 97, 0.25);
|
||
}
|
||
.galaxy-search-result-icon {
|
||
font-size: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
.galaxy-search-result-text {
|
||
min-width: 0;
|
||
}
|
||
.galaxy-search-result-title {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.galaxy-search-result-sub {
|
||
font-size: 10px;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.galaxy-search-empty {
|
||
padding: 14px;
|
||
text-align: center;
|
||
color: rgba(255, 255, 255, 0.6);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.galaxy-node.galaxy-dim {
|
||
opacity: 0.12;
|
||
}
|
||
.galaxy-node.galaxy-match > circle:nth-of-type(2) {
|
||
stroke: #ffd700;
|
||
stroke-width: 3;
|
||
}
|
||
.galaxy-link.galaxy-dim {
|
||
stroke-opacity: 0.04 !important;
|
||
}
|
||
.galaxy-node.galaxy-focus > circle:nth-of-type(2) {
|
||
stroke: #ffd700;
|
||
stroke-width: 4;
|
||
animation: galaxyPulse 0.9s ease-in-out 2;
|
||
}
|
||
.galaxy-node circle {
|
||
transform-box: fill-box;
|
||
transform-origin: center;
|
||
}
|
||
@keyframes galaxyPulse {
|
||
0%,
|
||
100% {
|
||
transform: scale(1);
|
||
}
|
||
50% {
|
||
transform: scale(1.15);
|
||
}
|
||
}
|
||
|
||
.dashboard-modal-body {
|
||
padding: 20px;
|
||
background: var(--bg);
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.dashboard-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.dashboard-kpi-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.dashboard-kpi-card {
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
padding: 16px;
|
||
text-align: center;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.dashboard-kpi-card::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
left: 0;
|
||
height: 3px;
|
||
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||
}
|
||
|
||
.dashboard-kpi-icon {
|
||
font-size: 28px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.dashboard-kpi-value {
|
||
font-size: 28px;
|
||
font-weight: 800;
|
||
color: var(--primary);
|
||
line-height: 1;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
[data-theme="dark"] .dashboard-kpi-value {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.dashboard-kpi-label {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.dashboard-charts-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||
gap: 16px;
|
||
}
|
||
|
||
.dashboard-chart-card {
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.dashboard-chart-header {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--border-light);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
}
|
||
|
||
[data-theme="dark"] .dashboard-chart-header {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.dashboard-chart-body {
|
||
padding: 16px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
min-height: 220px;
|
||
}
|
||
|
||
.dashboard-chart-body svg {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
.dashboard-empty-chart {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
gap: 8px;
|
||
text-align: center;
|
||
padding: 20px;
|
||
}
|
||
|
||
.dashboard-empty-chart span {
|
||
font-size: 32px;
|
||
opacity: 0.4;
|
||
}
|
||
|
||
.dashboard-footer-stats {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
display: flex;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.dashboard-footer-stats .stat-highlight {
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
}
|
||
|
||
[data-theme="dark"] .dashboard-footer-stats .stat-highlight {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.d3-tooltip {
|
||
position: fixed;
|
||
background: var(--primary-dark);
|
||
color: #fff;
|
||
padding: 8px 12px;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
pointer-events: none;
|
||
z-index: 3000;
|
||
display: none;
|
||
font-family: "Vazirmatn", sans-serif;
|
||
}
|
||
|
||
.d3-tooltip.visible {
|
||
display: block;
|
||
}
|
||
|
||
.rb-toolbar {
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
padding: 8px 12px;
|
||
background: linear-gradient(
|
||
135deg,
|
||
var(--primary-dark),
|
||
var(--primary)
|
||
);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.rb-toolbar .btn {
|
||
background: rgba(255, 255, 255, 0.12);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
color: #fff;
|
||
font-size: 11px;
|
||
padding: 5px 10px;
|
||
}
|
||
.rb-toolbar .btn:hover {
|
||
background: var(--accent);
|
||
color: var(--primary-dark);
|
||
}
|
||
.rb-toolbar .btn.btn-accent {
|
||
background: linear-gradient(135deg, var(--accent), var(--accent-dark));
|
||
color: var(--primary-dark);
|
||
font-weight: 700;
|
||
}
|
||
.rb-toolbar .btn.btn-danger {
|
||
background: linear-gradient(135deg, #f56565, #e53e3e);
|
||
color: #fff;
|
||
}
|
||
.rb-toolbar-separator {
|
||
width: 1px;
|
||
height: 24px;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
margin: 0 4px;
|
||
}
|
||
|
||
/* ✅ NEW: فیلتر تگ */
|
||
.rb-tag-filter {
|
||
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;
|
||
}
|
||
.rb-tag-filter-icon {
|
||
font-size: 12px;
|
||
opacity: 0.85;
|
||
}
|
||
.rb-tag-filter-select {
|
||
background: transparent;
|
||
border: none;
|
||
color: #fff;
|
||
font-family: inherit;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
outline: none;
|
||
cursor: pointer;
|
||
min-width: 110px;
|
||
padding: 4px 0;
|
||
}
|
||
.rb-tag-filter-select option {
|
||
background: var(--primary-dark);
|
||
color: #fff;
|
||
}
|
||
|
||
/* ✅ NEW: جستجوی کارت */
|
||
.rb-card-search {
|
||
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: 200px;
|
||
}
|
||
.rb-card-search-icon {
|
||
font-size: 12px;
|
||
opacity: 0.85;
|
||
}
|
||
.rb-card-search input {
|
||
flex: 1;
|
||
background: transparent;
|
||
border: none;
|
||
color: #fff;
|
||
font-family: inherit;
|
||
font-size: 12px;
|
||
outline: none;
|
||
width: 130px;
|
||
}
|
||
.rb-card-search input::placeholder {
|
||
color: rgba(255, 255, 255, 0.55);
|
||
}
|
||
.rb-card-search-clear {
|
||
background: none;
|
||
border: none;
|
||
color: rgba(255, 255, 255, 0.75);
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
.rb-card-search-clear.visible {
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
/* ✅ NEW: کارت مخفی با فیلتر */
|
||
.card.rb-hidden-by-filter {
|
||
display: none !important;
|
||
}
|
||
|
||
.rb-tabs-bar {
|
||
background: linear-gradient(
|
||
135deg,
|
||
var(--primary-dark) 0%,
|
||
var(--primary) 100%
|
||
);
|
||
border-bottom: 2px solid var(--accent);
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 10px;
|
||
min-height: 42px;
|
||
gap: 3px;
|
||
overflow-x: auto;
|
||
}
|
||
.rb-tabs-bar::-webkit-scrollbar {
|
||
height: 4px;
|
||
}
|
||
.rb-tabs-bar::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.rb-tab-item {
|
||
padding: 7px 14px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
border-radius: 6px 6px 0 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
white-space: nowrap;
|
||
transition: all 0.2s;
|
||
border-bottom: 2px solid transparent;
|
||
}
|
||
.rb-tab-item:hover {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
color: #fff;
|
||
}
|
||
.rb-tab-item.active {
|
||
background: var(--accent);
|
||
color: var(--primary-dark);
|
||
font-weight: 700;
|
||
border-bottom-color: var(--primary-dark);
|
||
}
|
||
.rb-tab-close {
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 50%;
|
||
background: transparent;
|
||
border: none;
|
||
cursor: pointer;
|
||
color: inherit;
|
||
font-size: 11px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
opacity: 0.6;
|
||
transition: all 0.2s;
|
||
}
|
||
.rb-tab-item:hover .rb-tab-close {
|
||
opacity: 1;
|
||
}
|
||
.rb-tab-close:hover {
|
||
background: var(--danger);
|
||
color: #fff;
|
||
}
|
||
|
||
.rb-tab-add {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border: 1px dashed rgba(255, 255, 255, 0.3);
|
||
color: rgba(255, 255, 255, 0.7);
|
||
padding: 7px 12px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
transition: all 0.2s;
|
||
}
|
||
.rb-tab-add:hover {
|
||
background: var(--accent);
|
||
color: var(--primary-dark);
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.rb-empty-tabs {
|
||
color: rgba(255, 255, 255, 0.5);
|
||
font-size: 12px;
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.rb-board-area {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 20px;
|
||
background: var(--board-bg);
|
||
}
|
||
.rb-board-wrapper {
|
||
display: flex;
|
||
gap: 15px;
|
||
align-items: flex-start;
|
||
min-height: calc(100% - 40px);
|
||
padding-bottom: 20px;
|
||
}
|
||
.rb-empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
color: var(--text-muted);
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
}
|
||
.rb-empty-state-icon {
|
||
font-size: 80px;
|
||
opacity: 0.3;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.rb-default-columns-switch {
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
padding: 12px;
|
||
background: var(--border-light);
|
||
}
|
||
|
||
.rb-switch-label {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.rb-switch-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.rb-switch-title {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.rb-switch-desc {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.rb-default-columns-preview {
|
||
display: flex;
|
||
gap: 6px;
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
border-top: 1px dashed var(--border);
|
||
flex-wrap: wrap;
|
||
animation: rbFadeIn 0.3s ease;
|
||
}
|
||
|
||
.rb-default-columns-preview.hidden {
|
||
display: none;
|
||
}
|
||
|
||
.rb-preview-column {
|
||
flex: 1;
|
||
min-width: 80px;
|
||
padding: 8px 10px;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
text-align: center;
|
||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||
box-shadow: var(--shadow-sm);
|
||
transition: transform 0.2s;
|
||
}
|
||
|
||
.rb-preview-column:hover {
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
@keyframes rbFadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-5px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.column {
|
||
border-radius: var(--radius);
|
||
min-width: 300px;
|
||
max-width: 300px;
|
||
max-height: calc(100% - 20px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: var(--shadow-sm);
|
||
border: 1px solid var(--border);
|
||
transition: all 0.2s;
|
||
background: var(--column-bg);
|
||
}
|
||
|
||
.column-header {
|
||
padding: 12px 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid var(--border);
|
||
background: linear-gradient(
|
||
to bottom,
|
||
rgba(255, 255, 255, 0.3),
|
||
transparent
|
||
);
|
||
border-radius: var(--radius) var(--radius) 0 0;
|
||
}
|
||
|
||
.column-title {
|
||
font-weight: 700;
|
||
font-size: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.column-count {
|
||
font-size: 11px;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
font-weight: 700;
|
||
background: rgba(0, 0, 0, 0.15);
|
||
color: inherit;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.column-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.column-action-btn {
|
||
background: transparent;
|
||
border: none;
|
||
cursor: pointer;
|
||
color: inherit;
|
||
opacity: 0.7;
|
||
padding: 4px 6px;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.column-action-btn:hover {
|
||
background: rgba(0, 0, 0, 0.1);
|
||
opacity: 1;
|
||
}
|
||
|
||
.column-body {
|
||
padding: 10px;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
min-height: 60px;
|
||
}
|
||
|
||
.column-body::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
.column-body::-webkit-scrollbar-thumb {
|
||
background: rgba(0, 0, 0, 0.15);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.column-footer {
|
||
padding: 8px;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.add-card-btn {
|
||
width: 100%;
|
||
background: transparent;
|
||
border: 1px dashed var(--border);
|
||
color: var(--text-muted);
|
||
padding: 8px;
|
||
border-radius: var(--radius-sm);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 13px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.add-card-btn:hover {
|
||
background: var(--card-bg);
|
||
border-color: var(--accent);
|
||
color: var(--accent-dark);
|
||
}
|
||
|
||
.add-column-btn {
|
||
background: rgba(255, 255, 255, 0.5);
|
||
border: 2px dashed var(--border);
|
||
border-radius: var(--radius);
|
||
min-width: 280px;
|
||
min-height: 100px;
|
||
cursor: pointer;
|
||
color: var(--text-muted);
|
||
font-family: inherit;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
transition: all 0.2s;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
[data-theme="dark"] .add-column-btn {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.add-column-btn:hover {
|
||
background: var(--card-bg);
|
||
border-color: var(--accent);
|
||
color: var(--accent-dark);
|
||
}
|
||
|
||
.card {
|
||
background: var(--card-bg);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 8px;
|
||
box-shadow: var(--shadow-sm);
|
||
cursor: pointer;
|
||
border: 1px solid var(--border);
|
||
transition: all 0.15s;
|
||
position: relative;
|
||
}
|
||
|
||
.card:hover {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||
transform: translateY(-1px);
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.card:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.card-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.card-tag {
|
||
font-size: 10px;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
font-weight: 600;
|
||
color: white;
|
||
display: inline-block;
|
||
}
|
||
|
||
.card-title {
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
margin-bottom: 6px;
|
||
line-height: 1.5;
|
||
color: var(--text);
|
||
}
|
||
|
||
.card-desc {
|
||
font-size: 12px;
|
||
margin-bottom: 8px;
|
||
line-height: 1.6;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
opacity: 0.85;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.card-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
align-items: center;
|
||
opacity: 0.9;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.card-meta-item {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 3px;
|
||
background: rgba(0, 0, 0, 0.08);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
[data-theme="dark"] .card-meta-item {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
color: var(--text);
|
||
}
|
||
|
||
.card-meta-item.overdue {
|
||
background: #fed7d7;
|
||
color: var(--danger);
|
||
font-weight: 600;
|
||
opacity: 1;
|
||
}
|
||
|
||
.card-meta-item.soon {
|
||
background: #fef5e7;
|
||
color: var(--warning);
|
||
font-weight: 600;
|
||
opacity: 1;
|
||
}
|
||
|
||
[data-theme="dark"] .card-meta-item.overdue {
|
||
background: rgba(229, 62, 62, 0.3);
|
||
color: #feb2b2;
|
||
}
|
||
|
||
[data-theme="dark"] .card-meta-item.soon {
|
||
background: rgba(221, 107, 32, 0.3);
|
||
color: #fbd38d;
|
||
}
|
||
|
||
.card-checklist-progress {
|
||
width: 100%;
|
||
height: 4px;
|
||
background: rgba(0, 0, 0, 0.1);
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
[data-theme="dark"] .card-checklist-progress {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
}
|
||
|
||
.card-checklist-bar {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, var(--success), #48bb78);
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
.tag-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 4px 10px;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: white;
|
||
transition: all 0.15s;
|
||
border: 2px solid transparent;
|
||
user-select: none;
|
||
}
|
||
|
||
.tag-chip.selected {
|
||
border-color: var(--primary);
|
||
transform: scale(1.05);
|
||
box-shadow:
|
||
0 0 0 2px var(--card-bg),
|
||
0 0 0 4px var(--primary);
|
||
}
|
||
|
||
.tag-chip:hover {
|
||
opacity: 0.85;
|
||
}
|
||
|
||
.rb-tag-add-form {
|
||
background: var(--border-light);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
padding: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.rb-tag-list-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
margin-bottom: 10px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.rb-tag-count {
|
||
background: var(--primary);
|
||
color: #fff;
|
||
font-size: 11px;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.rb-tag-list {
|
||
max-height: 320px;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.rb-tag-list::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
.rb-tag-list::-webkit-scrollbar-thumb {
|
||
background: rgba(0, 0, 0, 0.15);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.rb-tag-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 12px;
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
transition: all 0.15s;
|
||
}
|
||
|
||
.rb-tag-item:hover {
|
||
border-color: var(--accent);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.rb-tag-color-dot {
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
border: 2px solid rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.rb-tag-name {
|
||
flex: 1;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
|
||
.rb-tag-usage {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
background: var(--border-light);
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.rb-tag-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
|
||
.rb-tag-action-btn {
|
||
background: transparent;
|
||
border: none;
|
||
cursor: pointer;
|
||
padding: 4px 6px;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
opacity: 0.6;
|
||
transition: all 0.15s;
|
||
}
|
||
|
||
.rb-tag-action-btn:hover {
|
||
opacity: 1;
|
||
background: var(--border-light);
|
||
}
|
||
|
||
.rb-tag-action-btn.danger:hover {
|
||
background: #fed7d7;
|
||
}
|
||
|
||
.rb-tag-empty {
|
||
text-align: center;
|
||
padding: 30px 10px;
|
||
color: var(--text-muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.rb-tag-empty-icon {
|
||
font-size: 40px;
|
||
opacity: 0.3;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.rb-board-area .empty-state {
|
||
text-align: center;
|
||
padding: 20px 10px;
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* ✅ NEW: Drag & Drop */
|
||
.column.rb-dragging {
|
||
opacity: 0.4;
|
||
transform: scale(0.98);
|
||
}
|
||
.column.rb-drop-target {
|
||
border: 2px dashed var(--accent);
|
||
background: rgba(201, 169, 97, 0.08);
|
||
}
|
||
.card.rb-dragging {
|
||
opacity: 0.35;
|
||
transform: rotate(1.5deg) scale(0.98);
|
||
cursor: grabbing;
|
||
}
|
||
.card.rb-drop-above {
|
||
box-shadow: 0 -3px 0 0 var(--accent);
|
||
}
|
||
.card.rb-drop-below {
|
||
box-shadow: 0 3px 0 0 var(--accent);
|
||
}
|
||
.column-drag-handle {
|
||
cursor: grab;
|
||
padding: 4px 6px;
|
||
border-radius: 4px;
|
||
user-select: none;
|
||
font-size: 14px;
|
||
opacity: 0.5;
|
||
transition: opacity 0.15s;
|
||
flex-shrink: 0;
|
||
}
|
||
.column-drag-handle:hover {
|
||
opacity: 1;
|
||
background: rgba(0, 0, 0, 0.1);
|
||
}
|
||
.column-header:hover .column-drag-handle {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
/* ✅ NEW: Empty state فیلترشده */
|
||
.empty-state-filtered {
|
||
text-align: center;
|
||
padding: 20px 10px;
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
::-webkit-scrollbar {
|
||
width: 8px;
|
||
height: 8px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: var(--border);
|
||
border-radius: 4px;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<header class="app-header">
|
||
<div class="app-brand">
|
||
<div class="app-logo">📊</div>
|
||
<div class="app-brand-text">
|
||
<div class="app-title" id="headerTitle">دستیار انتشار</div>
|
||
<div class="app-subtitle" id="headerSubtitle">
|
||
مدیریت نسخهها و نیازمندیهای محصولات
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
<div class="tabs-bar" id="advancedToolbar">
|
||
<div class="toolbar-controls">
|
||
<button class="toolbar-icon-btn" id="sidebarToggle" title="سایدبار">
|
||
<span id="sidebarIcon">✕</span>
|
||
</button>
|
||
<button class="toolbar-icon-btn" id="themeToggle" title="تم">
|
||
<span id="themeIcon">🌙</span>
|
||
</button>
|
||
<button class="toolbar-icon-btn" id="undoBtn" title="Ctrl+Z" disabled>
|
||
↩
|
||
</button>
|
||
<button class="toolbar-icon-btn" id="redoBtn" title="Ctrl+Y" disabled>
|
||
↪
|
||
</button>
|
||
</div>
|
||
<div class="toolbar-search">
|
||
<span style="position: absolute; right: 10px; opacity: 0.7">🔍</span
|
||
><input
|
||
type="text"
|
||
class="search-input"
|
||
id="toolbarSearch"
|
||
placeholder="جستجو..."
|
||
/>
|
||
</div>
|
||
<div class="toolbar-filters">
|
||
<div class="filter-dropdown">
|
||
<span>🏷️</span
|
||
><select class="filter-select" id="filterType">
|
||
<option value="">همه انواع</option>
|
||
<option value="backend">بکاند</option>
|
||
<option value="frontend">فرانتاند</option>
|
||
<option value="service">میکروسرویس</option>
|
||
<option value="database">پایگاه داده</option>
|
||
<option value="mobile">موبایل</option>
|
||
<option value="infrastructure">زیرساخت</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-dropdown">
|
||
<span>🌐</span
|
||
><select class="filter-select" id="filterEnvironment">
|
||
<option value="">همه محیطها</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="toolbar-stats">
|
||
<div class="stat-item">
|
||
📋 <span class="stat-value" id="statTotal">0</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
📦 <span class="stat-value" id="statProjects">0</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
🌿 <span class="stat-value" id="statSubProjects">0</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
🔗 <span class="stat-value" id="statDependencies">0</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="app-main">
|
||
<aside class="sidebar" id="sidebar">
|
||
<div class="sidebar-header">
|
||
<div class="sidebar-title">🛠️ ابزارها</div>
|
||
</div>
|
||
<div class="sidebar-body" id="sidebarBody"></div>
|
||
</aside>
|
||
<main class="board-area" id="boardArea"></main>
|
||
</div>
|
||
<footer class="app-footer" id="appFooter">
|
||
<div>
|
||
<span class="footer-brand">طراح و توسعهدهنده:</span> هادی خزاعی اصل
|
||
<span style="margin: 0 10px">|</span
|
||
><span class="footer-brand">شرکت:</span> رایان پرداز کاوش
|
||
</div>
|
||
<div><span class="footer-brand">نسخه:</span> 1.1.0</div>
|
||
</footer>
|
||
|
||
<!-- Settings Modal -->
|
||
<div class="modal-overlay" id="settingsModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">
|
||
<div class="modal-title">
|
||
<span>⚙️</span><span>تنظیمات محصول</span>
|
||
</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="readonly-info">
|
||
ℹ️ اطلاعات پایه محصول فقط قابل مشاهده هستند.
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>عنوان محصول
|
||
<span class="readonly-badge">فقط خواندنی</span></label
|
||
><input
|
||
type="text"
|
||
class="form-input"
|
||
id="settingProjectTitle"
|
||
readonly
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>نسخه <span class="readonly-badge">فقط خواندنی</span></label
|
||
><input
|
||
type="text"
|
||
class="form-input"
|
||
id="settingVersion"
|
||
readonly
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>توضیحات <span class="readonly-badge">فقط خواندنی</span></label
|
||
><textarea
|
||
class="form-textarea"
|
||
id="settingDescription"
|
||
readonly
|
||
></textarea>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>توسعهدهنده
|
||
<span class="readonly-badge">فقط خواندنی</span></label
|
||
><input
|
||
type="text"
|
||
class="form-input"
|
||
id="settingDeveloper"
|
||
readonly
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>شرکت <span class="readonly-badge">فقط خواندنی</span></label
|
||
><input
|
||
type="text"
|
||
class="form-input"
|
||
id="settingCompany"
|
||
readonly
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="webhook-section">
|
||
<div class="webhook-section-header">
|
||
<div class="webhook-section-title">🔗 تنظیمات وبهوک</div>
|
||
<div class="webhook-toggle">
|
||
<span>فعالسازی</span
|
||
><label class="switch"
|
||
><input type="checkbox" id="settingWebhookEnabled" /><span
|
||
class="slider"
|
||
></span
|
||
></label>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>آدرس URL وبهوک <span class="required">*</span></label
|
||
><input
|
||
type="url"
|
||
class="form-input"
|
||
id="settingWebhookUrl"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>شناسه کلاینت <span class="required">*</span></label
|
||
><input
|
||
type="text"
|
||
class="form-input"
|
||
id="settingWebhookClientId"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
<div class="webhook-status">
|
||
<span class="webhook-status-dot" id="webhookStatusDot"></span
|
||
><span id="webhookStatusText">وضعیت: غیرفعال</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="saveSettingsBtn">💾 ذخیره</button
|
||
><button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Environment Modals -->
|
||
<div class="modal-overlay" id="envManagerModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">
|
||
<div class="modal-title">🌍 افزودن محیط عملیاتی جدید</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>نام محیط <span class="required">*</span></label
|
||
><input type="text" class="form-input" id="newEnvName" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">توضیحات</label
|
||
><input type="text" class="form-input" id="newEnvDesc" />
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">رنگ پسزمینه</label>
|
||
<div class="color-input-wrapper">
|
||
<input type="color" id="newEnvBgColor" value="#38a169" /><span
|
||
class="color-preview-text"
|
||
id="newEnvBgColorText"
|
||
>#38a169</span
|
||
>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">رنگ متن</label>
|
||
<div class="color-input-wrapper">
|
||
<input type="color" id="newEnvTextColor" value="#ffffff" /><span
|
||
class="color-preview-text"
|
||
id="newEnvTextColorText"
|
||
>#ffffff</span
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="saveNewEnvBtn">💾 ثبت</button
|
||
><button class="btn btn-ghost" data-close-modal>انصراف</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-overlay" id="envEditModal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<div class="modal-title">✏️ ویرایش محیط عملیاتی</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>شناسه <span class="readonly-badge">فقط خواندنی</span></label
|
||
><input type="text" class="form-input" id="editEnvId" readonly />
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">نام <span class="required">*</span></label
|
||
><input type="text" class="form-input" id="editEnvName" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">توضیحات</label
|
||
><input type="text" class="form-input" id="editEnvDesc" />
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">رنگ پسزمینه</label>
|
||
<div class="color-input-wrapper">
|
||
<input type="color" id="editEnvBgColor" /><span
|
||
class="color-preview-text"
|
||
id="editEnvBgColorText"
|
||
></span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">رنگ متن</label>
|
||
<div class="color-input-wrapper">
|
||
<input type="color" id="editEnvTextColor" /><span
|
||
class="color-preview-text"
|
||
id="editEnvTextColorText"
|
||
></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">پیشنمایش</label>
|
||
<div id="editEnvPreview" class="env-color-preview">نمونه</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="saveEnvEditBtn">💾 ذخیره</button
|
||
><button class="btn btn-ghost" data-close-modal>انصراف</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Product Modals -->
|
||
<div class="modal-overlay" id="projectDetailModal">
|
||
<div class="modal modal-xl">
|
||
<div class="modal-header">
|
||
<div class="modal-title" id="projectDetailTitle">📦 جزئیات محصول</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body" id="projectDetailBody"></div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-danger btn-sm" id="projectDetailDeleteBtn">
|
||
🗑️ حذف محصول</button
|
||
><button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-overlay" id="productEditModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">
|
||
<div class="modal-title">✏️ ویرایش محصول</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>شناسه <span class="readonly-badge">فقط خواندنی</span></label
|
||
><input
|
||
type="text"
|
||
class="form-input"
|
||
id="editProductId"
|
||
readonly
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>عنوان <span class="required">*</span></label
|
||
><input type="text" class="form-input" id="editProductTitle" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">توضیحات</label
|
||
><textarea
|
||
class="form-textarea"
|
||
id="editProductDescription"
|
||
></textarea>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">مالک</label
|
||
><input type="text" class="form-input" id="editProductOwner" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>تاریخ ایجاد
|
||
<span class="readonly-badge">فقط خواندنی</span></label
|
||
><input
|
||
type="text"
|
||
class="form-input"
|
||
id="editProductCreatedAt"
|
||
readonly
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="saveProductEditBtn">
|
||
💾 ذخیره</button
|
||
><button class="btn btn-ghost" data-close-modal>انصراف</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-overlay" id="addProductModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">
|
||
<div class="modal-title">➕ ایجاد محصول جدید</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>عنوان <span class="required">*</span></label
|
||
><input type="text" class="form-input" id="newProductTitle" />
|
||
<div class="form-error" id="newProductTitleError">
|
||
عنوان محصول الزامی است و باید حداقل ۳ کاراکتر باشد
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">توضیحات</label
|
||
><textarea
|
||
class="form-textarea"
|
||
id="newProductDescription"
|
||
></textarea>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">مالک</label
|
||
><input type="text" class="form-input" id="newProductOwner" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>تاریخ ایجاد
|
||
<span class="readonly-badge">فقط خواندنی</span></label
|
||
><input
|
||
type="text"
|
||
class="form-input"
|
||
id="newProductCreatedAt"
|
||
readonly
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="saveNewProductBtn">💾 ثبت</button
|
||
><button class="btn btn-ghost" data-close-modal>انصراف</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-overlay" id="addProjectModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">
|
||
<div class="modal-title">➕ افزودن پروژه جدید</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>عنوان <span class="required">*</span></label
|
||
><input type="text" class="form-input" id="newProjectTitle" />
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>نوع <span class="required">*</span></label
|
||
><select class="form-select" id="newProjectType">
|
||
<option value="">انتخاب کنید...</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">فریمورک</label
|
||
><input type="text" class="form-input" id="newProjectFramework" />
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">زبان</label
|
||
><input type="text" class="form-input" id="newProjectLanguage" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">آدرس مخزن</label
|
||
><input
|
||
type="text"
|
||
class="form-input"
|
||
id="newProjectRepo"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">توضیحات</label
|
||
><textarea
|
||
class="form-textarea"
|
||
id="newProjectDescription"
|
||
></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="saveNewProjectBtn">💾 ذخیره</button
|
||
><button class="btn btn-ghost" data-close-modal>انصراف</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-overlay" id="editProjectModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">
|
||
<div class="modal-title">✏️ ویرایش پروژه</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>شناسه <span class="readonly-badge">فقط خواندنی</span></label
|
||
><input type="text" class="form-input" id="editProjId" readonly />
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>عنوان <span class="required">*</span></label
|
||
><input type="text" class="form-input" id="editProjTitle" />
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>نوع <span class="required">*</span></label
|
||
><select class="form-select" id="editProjType">
|
||
<option value="">انتخاب کنید...</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">فریمورک</label
|
||
><input type="text" class="form-input" id="editProjFramework" />
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">زبان</label
|
||
><input type="text" class="form-input" id="editProjLanguage" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">آدرس مخزن</label
|
||
><input
|
||
type="text"
|
||
class="form-input"
|
||
id="editProjRepo"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">توضیحات</label
|
||
><textarea
|
||
class="form-textarea"
|
||
id="editProjDescription"
|
||
></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="saveProjEditBtn">💾 ذخیره</button
|
||
><button class="btn btn-ghost" data-close-modal>انصراف</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Deployment Modal -->
|
||
<div class="modal-overlay" id="deploymentModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">
|
||
<div class="modal-title" id="deploymentModalTitle">
|
||
🚀 افزودن نسخه استقرار
|
||
</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group" id="deploymentEnvGroup"></div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>نسخه <span class="required">*</span></label
|
||
><input
|
||
type="text"
|
||
class="form-input"
|
||
id="depVersion"
|
||
dir="ltr"
|
||
placeholder="مثلاً: 1.2.3"
|
||
/>
|
||
<div class="form-hint">از فرمت SemVer استفاده کنید</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>تاریخ و زمان استقرار <span class="required">*</span></label
|
||
><input
|
||
type="datetime-local"
|
||
class="form-input"
|
||
id="depDate"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>ریلیز نوت <span class="required">*</span></label
|
||
><textarea
|
||
class="form-textarea"
|
||
id="depReleaseNotes"
|
||
rows="3"
|
||
placeholder="توضیحات تغییرات این نسخه..."
|
||
></textarea>
|
||
<div class="form-hint">
|
||
شرح تغییرات، باگفیکسها و ویژگیهای جدید این نسخه
|
||
</div>
|
||
</div>
|
||
<div id="requiredDeploymentsSection"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="saveDeploymentBtn">💾 ذخیره</button
|
||
><button class="btn btn-ghost" data-close-modal>انصراف</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dependency Manager Modal -->
|
||
<div class="modal-overlay" id="dependencyManagerModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">
|
||
<div class="modal-title">🔗 مدیریت وابستگیها</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="readonly-info">
|
||
ℹ️ هر وابستگی فقط شامل یک پروژه است. انتخاب نسخه و محیط در زمان ثبت
|
||
استقرار انجام میشود.
|
||
</div>
|
||
<div class="dependency-section-title">📋 وابستگیهای فعلی</div>
|
||
<div id="currentDependenciesList" class="dependency-list"></div>
|
||
<div class="dependency-search-box">
|
||
<div class="dependency-section-title">🔍 افزودن وابستگی جدید</div>
|
||
<input
|
||
type="text"
|
||
class="dependency-search-input"
|
||
id="dependencySearch"
|
||
placeholder="جستجو در پروژهها..."
|
||
/>
|
||
<div
|
||
id="dependencySearchResults"
|
||
class="dependency-search-results"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Galaxy Chart Modal -->
|
||
<div class="modal-overlay" id="galaxyChartModal">
|
||
<div class="modal modal-galaxy">
|
||
<div class="modal-header">
|
||
<div class="modal-title">🌌 نمودار کهکشانی محصولات</div>
|
||
<div class="galaxy-toolbar">
|
||
<div class="galaxy-search">
|
||
<span class="galaxy-search-icon">🔍</span>
|
||
<input
|
||
type="text"
|
||
id="galaxySearchInput"
|
||
placeholder="جستجو در نمودار..."
|
||
autocomplete="off"
|
||
/>
|
||
<button
|
||
class="galaxy-search-clear"
|
||
id="galaxySearchClear"
|
||
title="پاک کردن"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<button
|
||
class="btn btn-sm"
|
||
id="galaxyCollapseAll"
|
||
title="کولاپس همه محصولات"
|
||
>
|
||
⊟ کولاپس همه
|
||
</button>
|
||
<button
|
||
class="btn btn-sm"
|
||
id="galaxyExpandAll"
|
||
title="باز کردن همه محصولات"
|
||
>
|
||
⊞ باز کردن همه
|
||
</button>
|
||
<button class="btn btn-sm" id="galaxyResetView">🎯 بازنشانی</button>
|
||
<button class="btn btn-sm" id="galaxyExportPNG">📷 PNG</button>
|
||
</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body galaxy-modal-body">
|
||
<div id="galaxyChartContainer" class="galaxy-chart-container"></div>
|
||
<div class="galaxy-search-results" id="galaxySearchResults"></div>
|
||
<div class="galaxy-legend" id="galaxyLegend"></div>
|
||
<div class="galaxy-info-panel" id="galaxyInfoPanel">
|
||
<button
|
||
class="modal-close"
|
||
id="galaxyInfoClose"
|
||
style="
|
||
position: absolute;
|
||
top: 8px;
|
||
left: 8px;
|
||
width: 22px;
|
||
height: 22px;
|
||
font-size: 12px;
|
||
"
|
||
>
|
||
✕
|
||
</button>
|
||
<div id="galaxyInfoContent"></div>
|
||
</div>
|
||
<div class="galaxy-zoom-indicator" id="galaxyZoomIndicator">
|
||
🔍 100%
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div class="galaxy-stats" id="galaxyStats"></div>
|
||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Executive Dashboard Modal -->
|
||
<div class="modal-overlay" id="executiveDashboardModal">
|
||
<div class="modal modal-galaxy">
|
||
<div class="modal-header">
|
||
<div class="modal-title">📊 داشبورد مدیریتی</div>
|
||
<div class="galaxy-toolbar">
|
||
<button class="btn btn-sm" id="dashboardRefresh">
|
||
🔄 بهروزرسانی
|
||
</button>
|
||
</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body dashboard-modal-body">
|
||
<div id="dashboardContainer" class="dashboard-container"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div class="dashboard-footer-stats" id="dashboardFooterStats"></div>
|
||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dependency Analysis Modal -->
|
||
<div class="modal-overlay" id="dependencyAnalysisModal">
|
||
<div class="modal modal-galaxy">
|
||
<div class="modal-header">
|
||
<div class="modal-title">🔗 تحلیل وابستگیها</div>
|
||
<div class="galaxy-toolbar">
|
||
<button class="btn btn-sm" id="depAnalysisRefresh">🔄</button>
|
||
</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body dashboard-modal-body">
|
||
<div
|
||
id="dependencyAnalysisContainer"
|
||
class="dashboard-container"
|
||
></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div class="dashboard-footer-stats" id="depAnalysisFooterStats"></div>
|
||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Deployment Analysis Modal -->
|
||
<div class="modal-overlay" id="deploymentAnalysisModal">
|
||
<div class="modal modal-galaxy">
|
||
<div class="modal-header">
|
||
<div class="modal-title">🚀 تحلیل استقرار</div>
|
||
<div class="galaxy-toolbar">
|
||
<button class="btn btn-sm" id="deployAnalysisRefresh">🔄</button>
|
||
</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body dashboard-modal-body">
|
||
<div
|
||
id="deploymentAnalysisContainer"
|
||
class="dashboard-container"
|
||
></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div
|
||
class="dashboard-footer-stats"
|
||
id="deployAnalysisFooterStats"
|
||
></div>
|
||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Version Analysis Modal -->
|
||
<div class="modal-overlay" id="versionAnalysisModal">
|
||
<div class="modal modal-galaxy">
|
||
<div class="modal-header">
|
||
<div class="modal-title">🔢 تحلیل نسخه</div>
|
||
<div class="galaxy-toolbar">
|
||
<button class="btn btn-sm" id="verAnalysisRefresh">🔄</button>
|
||
</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body dashboard-modal-body">
|
||
<div id="versionAnalysisContainer" class="dashboard-container"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div class="dashboard-footer-stats" id="verAnalysisFooterStats"></div>
|
||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Risk Analysis Modal -->
|
||
<div class="modal-overlay" id="riskAnalysisModal">
|
||
<div class="modal modal-galaxy">
|
||
<div class="modal-header">
|
||
<div class="modal-title">⚠️ تحلیل ریسک</div>
|
||
<div class="galaxy-toolbar">
|
||
<button class="btn btn-sm" id="riskAnalysisRefresh">🔄</button>
|
||
</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body dashboard-modal-body">
|
||
<div id="riskAnalysisContainer" class="dashboard-container"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div
|
||
class="dashboard-footer-stats"
|
||
id="riskAnalysisFooterStats"
|
||
></div>
|
||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Release Boards Modal -->
|
||
<div class="modal-overlay" id="releaseBoardsModal">
|
||
<div class="modal modal-galaxy">
|
||
<div class="modal-header">
|
||
<div class="modal-title">🚀 بردهای انتشار</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
|
||
<div class="rb-toolbar">
|
||
<button class="btn" id="rbEditBoard" title="ویرایش برد فعلی">
|
||
✏️ ویرایش
|
||
</button>
|
||
<button class="btn" id="rbManageTags" title="مدیریت تگها">
|
||
🏷️ تگها
|
||
</button>
|
||
|
||
<!-- ✅ NEW: فیلتر تگ -->
|
||
<div class="rb-tag-filter" title="فیلتر بر اساس تگ">
|
||
<span class="rb-tag-filter-icon">🔎</span>
|
||
<select id="rbTagFilter" class="rb-tag-filter-select">
|
||
<option value="">همه تگها</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- ✅ NEW: جستجوی کارت -->
|
||
<div class="rb-card-search" title="جستجوی کارتها">
|
||
<span class="rb-card-search-icon">🔍</span>
|
||
<input
|
||
type="text"
|
||
id="rbCardSearchInput"
|
||
placeholder="جستجوی کارت..."
|
||
autocomplete="off"
|
||
/>
|
||
<button
|
||
class="rb-card-search-clear"
|
||
id="rbCardSearchClear"
|
||
title="پاک کردن"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<div class="rb-toolbar-separator"></div>
|
||
<button class="btn" id="rbUndoBtn" title="بازگشت (Ctrl+Z)" disabled>
|
||
↩️ بازگشت
|
||
</button>
|
||
<button class="btn" id="rbRedoBtn" title="از نو (Ctrl+Y)" disabled>
|
||
↪️ از نو
|
||
</button>
|
||
</div>
|
||
|
||
<div class="rb-tabs-bar" id="rbTabsBar"></div>
|
||
|
||
<div
|
||
class="modal-body"
|
||
style="
|
||
padding: 0;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
"
|
||
>
|
||
<div class="rb-board-area" id="rbBoardArea">
|
||
<div class="rb-empty-state" id="rbEmptyState">
|
||
<div class="rb-empty-state-icon">📋</div>
|
||
<h2 style="color: var(--primary); margin-bottom: 10px">
|
||
هیچ بردی وجود ندارد
|
||
</h2>
|
||
<p style="margin-bottom: 20px">
|
||
برای شروع، یک برد جدید ایجاد کنید
|
||
</p>
|
||
<button
|
||
class="btn btn-accent"
|
||
onclick="ReleaseBoardsManager.openNewBoardModal()"
|
||
>
|
||
➕ ایجاد اولین برد
|
||
</button>
|
||
</div>
|
||
<div
|
||
class="rb-board-wrapper"
|
||
id="rbBoardWrapper"
|
||
style="display: none"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<div class="dashboard-footer-stats" id="rbFooterStats"></div>
|
||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: New/Edit Board -->
|
||
<div class="modal-overlay" id="rbBoardFormModal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<div class="modal-title">
|
||
📋 <span id="rbBoardFormTitle">ایجاد برد جدید</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="rbBoardTitle"
|
||
placeholder="مثلاً: اسپرینت جاری"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">توضیحات</label>
|
||
<textarea
|
||
class="form-textarea"
|
||
id="rbBoardDesc"
|
||
placeholder="توضیحات برد..."
|
||
></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">🎨 رنگ پسزمینه</label>
|
||
<input
|
||
type="color"
|
||
class="form-input"
|
||
id="rbBoardBgColor"
|
||
value="#e2e8f0"
|
||
style="height: 40px"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-group" id="rbDefaultColumnsGroup">
|
||
<label class="form-label">📑 ستونهای اولیه</label>
|
||
<div class="rb-default-columns-switch">
|
||
<label class="rb-switch-label">
|
||
<div class="rb-switch-info">
|
||
<div class="rb-switch-title">ایجاد با ستونهای پیشفرض</div>
|
||
<div class="rb-switch-desc">
|
||
۵ ستون استاندارد کانبان به صورت خودکار ایجاد میشود
|
||
</div>
|
||
</div>
|
||
<label class="switch">
|
||
<input type="checkbox" id="rbUseDefaultColumns" checked />
|
||
<span class="slider"></span>
|
||
</label>
|
||
</label>
|
||
<div
|
||
class="rb-default-columns-preview"
|
||
id="rbDefaultColumnsPreview"
|
||
>
|
||
<div
|
||
class="rb-preview-column"
|
||
style="background: #edf2f7; color: #1a202c"
|
||
>
|
||
📥 بک لاگ
|
||
</div>
|
||
<div
|
||
class="rb-preview-column"
|
||
style="background: #ebf8ff; color: #1a202c"
|
||
>
|
||
📋 برای انجام
|
||
</div>
|
||
<div
|
||
class="rb-preview-column"
|
||
style="background: #fef5e7; color: #1a202c"
|
||
>
|
||
🔨 در حال انجام
|
||
</div>
|
||
<div
|
||
class="rb-preview-column"
|
||
style="background: #faf5ff; color: #1a202c"
|
||
>
|
||
🔍 برای تست
|
||
</div>
|
||
<div
|
||
class="rb-preview-column"
|
||
style="background: #f0fff4; color: #1a202c"
|
||
>
|
||
✅ انجام شده
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="rbSaveBoardBtn">💾 ذخیره</button>
|
||
<button class="btn btn-ghost" data-close-modal>انصراف</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: New/Edit Column -->
|
||
<div class="modal-overlay" id="rbColumnFormModal">
|
||
<div class="modal" style="max-width: 500px">
|
||
<div class="modal-header">
|
||
<div class="modal-title">
|
||
📊 <span id="rbColumnFormTitle">ایجاد ستون جدید</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="rbColumnTitle"
|
||
placeholder="مثلاً: در حال انجام"
|
||
/>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">🎨 رنگ پسزمینه</label>
|
||
<input
|
||
type="color"
|
||
class="form-input"
|
||
id="rbColumnBgColor"
|
||
value="#edf2f7"
|
||
style="height: 40px"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">🖋️ رنگ متن</label>
|
||
<input
|
||
type="color"
|
||
class="form-input"
|
||
id="rbColumnTextColor"
|
||
value="#1a202c"
|
||
style="height: 40px"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="rbSaveColumnBtn">💾 ذخیره</button>
|
||
<button class="btn btn-ghost" data-close-modal>انصراف</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: New/Edit Card -->
|
||
<div class="modal-overlay" id="rbCardFormModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">
|
||
<div class="modal-title">
|
||
📝 <span id="rbCardFormTitle">ایجاد کارت جدید</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="rbCardTitle"
|
||
placeholder="عنوان وظیفه..."
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>توضیحات <span class="required">*</span></label
|
||
>
|
||
<textarea
|
||
class="form-textarea"
|
||
id="rbCardDesc"
|
||
placeholder="توضیحات کامل..."
|
||
></textarea>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">📅 تاریخ تخمینی</label>
|
||
<input
|
||
type="datetime-local"
|
||
class="form-input"
|
||
id="rbCardDate"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">🎨 رنگ کارت</label>
|
||
<input
|
||
type="color"
|
||
class="form-input"
|
||
id="rbCardBgColor"
|
||
value="#ffffff"
|
||
style="height: 40px"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">🏷️ برچسبها</label>
|
||
<div
|
||
id="rbCardTagsContainer"
|
||
style="
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
padding: 8px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
min-height: 50px;
|
||
background: var(--border-light);
|
||
"
|
||
></div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">✅ چکلیست</label>
|
||
<div
|
||
id="rbChecklistContainer"
|
||
style="
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
padding: 8px;
|
||
background: var(--border-light);
|
||
max-height: 180px;
|
||
overflow-y: auto;
|
||
"
|
||
></div>
|
||
<div style="display: flex; gap: 6px; margin-top: 6px">
|
||
<input
|
||
type="text"
|
||
class="form-input"
|
||
id="rbNewChecklistItem"
|
||
placeholder="ریزوظیفه جدید..."
|
||
/>
|
||
<button
|
||
class="btn btn-sm"
|
||
onclick="ReleaseBoardsManager.addChecklistItem()"
|
||
>
|
||
➕
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="rbSaveCardBtn">💾 ذخیره</button>
|
||
<button
|
||
class="btn btn-danger"
|
||
id="rbDeleteCardBtn"
|
||
style="display: none"
|
||
>
|
||
🗑️ حذف
|
||
</button>
|
||
<button class="btn btn-ghost" data-close-modal>انصراف</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: Tag Manager -->
|
||
<div class="modal-overlay" id="rbTagManagerModal">
|
||
<div class="modal" style="max-width: 550px">
|
||
<div class="modal-header">
|
||
<div class="modal-title">🏷️ مدیریت تگهای بردها</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="rb-tag-add-form">
|
||
<div class="form-row" style="gap: 8px; align-items: flex-end">
|
||
<div class="form-group" style="flex: 1; margin-bottom: 0">
|
||
<label class="form-label">نام تگ</label>
|
||
<input
|
||
type="text"
|
||
class="form-input"
|
||
id="rbNewTagName"
|
||
placeholder="مثلاً: فوری، باگ، ویژگی..."
|
||
/>
|
||
</div>
|
||
<div class="form-group" style="width: 80px; margin-bottom: 0">
|
||
<label class="form-label">رنگ</label>
|
||
<input
|
||
type="color"
|
||
class="form-input"
|
||
id="rbNewTagColor"
|
||
value="#4299e1"
|
||
style="height: 38px; padding: 2px"
|
||
/>
|
||
</div>
|
||
<button
|
||
class="btn btn-accent"
|
||
id="rbAddTagBtn"
|
||
style="height: 38px; white-space: nowrap"
|
||
>
|
||
➕ افزودن
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="rb-tag-list-header">
|
||
<span>تگهای موجود</span>
|
||
<span class="rb-tag-count" id="rbTagCount">0</span>
|
||
</div>
|
||
<div class="rb-tag-list" id="rbTagList"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div style="font-size: 11px; color: var(--text-muted)">
|
||
💡 تغییرات تگها به صورت خودکار در تمام کارتها اعمال میشود
|
||
</div>
|
||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: Edit Tag -->
|
||
<div class="modal-overlay" id="rbEditTagModal">
|
||
<div class="modal" style="max-width: 420px">
|
||
<div class="modal-header">
|
||
<div class="modal-title">✏️ ویرایش تگ</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label class="form-label"
|
||
>نام تگ <span class="required">*</span></label
|
||
>
|
||
<input
|
||
type="text"
|
||
class="form-input"
|
||
id="rbEditTagName"
|
||
placeholder="نام تگ..."
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">🎨 رنگ</label>
|
||
<input
|
||
type="color"
|
||
class="form-input"
|
||
id="rbEditTagColor"
|
||
value="#4299e1"
|
||
style="height: 40px"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">پیشنمایش</label>
|
||
<div
|
||
style="
|
||
padding: 10px;
|
||
background: var(--border-light);
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
"
|
||
>
|
||
<span
|
||
class="card-tag"
|
||
id="rbEditTagPreview"
|
||
style="font-size: 13px; padding: 4px 14px"
|
||
>نمونه</span
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="rbSaveTagBtn">💾 ذخیره</button>
|
||
<button class="btn btn-ghost" data-close-modal>انصراف</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal: Autosave Recovery -->
|
||
<div class="modal-overlay" id="rbAutosaveModal">
|
||
<div class="modal" style="max-width: 550px">
|
||
<div class="modal-header">
|
||
<div class="modal-title">💾 بازیابی خودکار دادهها</div>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div
|
||
style="
|
||
background: linear-gradient(to left, #ebf8ff, #e6f6ff);
|
||
border: 1px solid #90cdf4;
|
||
border-right: 4px solid var(--info);
|
||
padding: 14px 18px;
|
||
border-radius: 8px;
|
||
margin-bottom: 16px;
|
||
font-size: 13px;
|
||
line-height: 1.8;
|
||
"
|
||
>
|
||
<div
|
||
style="
|
||
font-weight: 700;
|
||
font-size: 14px;
|
||
margin-bottom: 8px;
|
||
color: var(--primary);
|
||
"
|
||
>
|
||
⚠️ داده ذخیرهشده خودکار یافت شد
|
||
</div>
|
||
<div style="color: var(--text-muted)">
|
||
به نظر میرسد در جلسه قبلی، تغییراتی در برنامه اعمال شده و به صورت
|
||
خودکار ذخیره شدهاند.
|
||
</div>
|
||
<div
|
||
style="
|
||
margin-top: 10px;
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
"
|
||
>
|
||
📅 تاریخ ذخیره:
|
||
<strong id="rbAutosaveDate" style="color: var(--primary)"
|
||
>—</strong
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
style="
|
||
background: var(--border-light);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 16px;
|
||
"
|
||
>
|
||
<div
|
||
style="
|
||
font-weight: 700;
|
||
font-size: 13px;
|
||
margin-bottom: 10px;
|
||
color: var(--primary);
|
||
"
|
||
>
|
||
📊 محتویات دادههای ذخیرهشده:
|
||
</div>
|
||
<div
|
||
style="
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
"
|
||
>
|
||
<div
|
||
style="
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 6px 10px;
|
||
background: var(--card-bg);
|
||
border-radius: 6px;
|
||
"
|
||
>
|
||
<span>🌍 محیطها:</span>
|
||
<strong id="rbAutosaveEnvCount" style="color: var(--primary)"
|
||
>0</strong
|
||
>
|
||
</div>
|
||
<div
|
||
style="
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 6px 10px;
|
||
background: var(--card-bg);
|
||
border-radius: 6px;
|
||
"
|
||
>
|
||
<span>📦 محصولات:</span>
|
||
<strong id="rbAutosaveProdCount" style="color: var(--primary)"
|
||
>0</strong
|
||
>
|
||
</div>
|
||
<div
|
||
style="
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 6px 10px;
|
||
background: var(--card-bg);
|
||
border-radius: 6px;
|
||
"
|
||
>
|
||
<span>🌿 پروژهها:</span>
|
||
<strong id="rbAutosaveProjCount" style="color: var(--primary)"
|
||
>0</strong
|
||
>
|
||
</div>
|
||
<div
|
||
style="
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 6px 10px;
|
||
background: var(--card-bg);
|
||
border-radius: 6px;
|
||
"
|
||
>
|
||
<span>🚀 استقرارها:</span>
|
||
<strong id="rbAutosaveDepCount" style="color: var(--primary)"
|
||
>0</strong
|
||
>
|
||
</div>
|
||
<div
|
||
style="
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 6px 10px;
|
||
background: var(--card-bg);
|
||
border-radius: 6px;
|
||
"
|
||
>
|
||
<span>📋 بردها:</span>
|
||
<strong id="rbAutosaveBoardCount" style="color: var(--primary)"
|
||
>0</strong
|
||
>
|
||
</div>
|
||
<div
|
||
style="
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 6px 10px;
|
||
background: var(--card-bg);
|
||
border-radius: 6px;
|
||
"
|
||
>
|
||
<span>🏷️ تگهای برد:</span>
|
||
<strong id="rbAutosaveTagCount" style="color: var(--primary)"
|
||
>0</strong
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="font-size: 13px; color: var(--text); line-height: 1.8">
|
||
آیا میخواهید این دادهها را بازیابی کنید؟
|
||
<ul
|
||
style="
|
||
margin-top: 8px;
|
||
padding-right: 20px;
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
"
|
||
>
|
||
<li>
|
||
<strong>بازیابی:</strong> تمام دادههای ذخیرهشده جایگزین
|
||
دادههای فعلی میشوند
|
||
</li>
|
||
<li>
|
||
<strong>نادیده گرفتن:</strong> دادههای خودکار حذف شده و
|
||
دادههای فعلی حفظ میشوند
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer" style="justify-content: space-between">
|
||
<button class="btn btn-accent" id="rbRecoverAutosaveBtn">
|
||
💾 بازیابی
|
||
</button>
|
||
<button class="btn btn-danger" id="rbDiscardAutosaveBtn">
|
||
🗑️ نادیده گرفتن
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Auto-save Indicator -->
|
||
<div
|
||
id="rbAutosaveIndicator"
|
||
style="
|
||
position: fixed;
|
||
bottom: 50px;
|
||
left: 20px;
|
||
background: var(--panel-bg);
|
||
color: var(--text);
|
||
padding: 6px 12px;
|
||
border-radius: 20px;
|
||
font-size: 11px;
|
||
box-shadow: var(--shadow);
|
||
border: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
z-index: 1500;
|
||
pointer-events: none;
|
||
"
|
||
>
|
||
<div
|
||
style="
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: var(--success);
|
||
animation: rbPulse 2s infinite;
|
||
"
|
||
></div>
|
||
<span id="rbAutosaveText">ذخیره خودکار انجام شد</span>
|
||
</div>
|
||
|
||
<style>
|
||
@keyframes rbPulse {
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
opacity: 0.4;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
<div class="galaxy-tooltip" id="galaxyTooltip"></div>
|
||
<div class="d3-tooltip" id="d3Tooltip"></div>
|
||
|
||
<script>
|
||
/* ==================== DATA ==================== */
|
||
const projectData = {
|
||
projectId: "550e8400-e29b-41d4-a716-446655440000",
|
||
projectName: "xReleaseAssistant",
|
||
projectTitle: "دستیار انتشار",
|
||
description: "مدیریت نسخهها و نیازمندیهای محصولات",
|
||
version: "1.2.0",
|
||
developer: "هادی خزاعی اصل",
|
||
company: "رایان پرداز کاوش",
|
||
metadata: {
|
||
createdAt: "2026-09-13T00:00:00Z",
|
||
updatedAt: "2026-09-14T00:00:00Z",
|
||
tags: ["release-management"],
|
||
customFields: {},
|
||
},
|
||
webhook: { url: "", clientId: "", enabled: false },
|
||
toolGroups: [
|
||
{
|
||
id: "product-management",
|
||
title: "پرونده",
|
||
icon: "📁",
|
||
description: "مدیریت چرخه حیات محصول",
|
||
tools: [
|
||
{
|
||
id: "clear-all",
|
||
title: "پاکسازی",
|
||
icon: "🗑️",
|
||
action: "clearAll",
|
||
description: "پاکسازی کامل",
|
||
},
|
||
{
|
||
id: "save-product",
|
||
title: "ذخیره",
|
||
icon: "💾",
|
||
action: "saveProduct",
|
||
description: "دانلود دادهها",
|
||
},
|
||
{
|
||
id: "load-product",
|
||
title: "بارگذاری",
|
||
icon: "📂",
|
||
action: "loadProduct",
|
||
description: "بارگذاری از فایل",
|
||
},
|
||
{
|
||
id: "product-settings",
|
||
title: "تنظیمات",
|
||
icon: "⚙️",
|
||
action: "openProductSettings",
|
||
description: "پیکربندی",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "boards",
|
||
title: "بردها",
|
||
icon: "📋",
|
||
description: "مدیریت بردهای انتشار",
|
||
tools: [
|
||
{
|
||
id: "release-boards",
|
||
title: "بردهای انتشار",
|
||
icon: "🚀",
|
||
action: "showReleaseBoards",
|
||
description: "مشاهده و مدیریت بردهای انتشار",
|
||
},
|
||
// ✅ NEW: گزینه مدیریت تگها
|
||
{
|
||
id: "manage-board-tags",
|
||
title: "مدیریت تگها",
|
||
icon: "🏷️",
|
||
action: "showBoardTagsManager",
|
||
description: "ایجاد، ویرایش و حذف تگهای بردها",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "reports",
|
||
title: "گزارشها",
|
||
icon: "📊",
|
||
description: "تحلیل و گزارشگیری",
|
||
tools: [
|
||
{
|
||
id: "executive-dashboard",
|
||
title: "داشبورد مدیریتی",
|
||
icon: "📊",
|
||
action: "showExecutiveDashboard",
|
||
description: "نمای کلی با نمودارها",
|
||
},
|
||
{
|
||
id: "dependency-analysis",
|
||
title: "تحلیل وابستگیها",
|
||
icon: "🔗",
|
||
action: "showDependencyAnalysis",
|
||
description: "تحلیل وابستگیها",
|
||
},
|
||
{
|
||
id: "deployment-analysis",
|
||
title: "تحلیل استقرار",
|
||
icon: "🚀",
|
||
action: "showDeploymentAnalysis",
|
||
description: "تحلیل استقرارها",
|
||
},
|
||
{
|
||
id: "version-analysis",
|
||
title: "تحلیل نسخه",
|
||
icon: "🔢",
|
||
action: "showVersionAnalysis",
|
||
description: "تحلیل نسخهها",
|
||
},
|
||
{
|
||
id: "risk-analysis",
|
||
title: "تحلیل ریسک",
|
||
icon: "⚠️",
|
||
action: "showRiskAnalysis",
|
||
description: "تحلیل ریسکها",
|
||
},
|
||
{
|
||
id: "galaxy-chart",
|
||
title: "نمودار کهکشانی",
|
||
icon: "🌌",
|
||
action: "showGalaxyChart",
|
||
description: "نمودار کهکشانی",
|
||
},
|
||
],
|
||
},
|
||
],
|
||
appData: {
|
||
environments: [
|
||
{
|
||
id: "env-prod-001",
|
||
name: "Production",
|
||
description: "محیط عملیاتی اصلی - در دسترس کاربران نهایی",
|
||
backgroundColor: "#38a169",
|
||
textColor: "#ffffff",
|
||
},
|
||
{
|
||
id: "env-stage-002",
|
||
name: "Staging",
|
||
description: "محیط پیش از تولید - شبیهسازی Production",
|
||
backgroundColor: "#dd6b20",
|
||
textColor: "#ffffff",
|
||
},
|
||
{
|
||
id: "env-dev-003",
|
||
name: "Development",
|
||
description: "محیط توسعه - برای تیم فنی",
|
||
backgroundColor: "#3182ce",
|
||
textColor: "#ffffff",
|
||
},
|
||
{
|
||
id: "env-qa-004",
|
||
name: "QA",
|
||
description: "محیط تست کیفیت - کنترل کیفیت خودکار و دستی",
|
||
backgroundColor: "#805ad5",
|
||
textColor: "#ffffff",
|
||
},
|
||
{
|
||
id: "env-uat-005",
|
||
name: "UAT",
|
||
description: "تست پذیرش کاربر - تأیید کارفرما",
|
||
backgroundColor: "#d69e2e",
|
||
textColor: "#ffffff",
|
||
},
|
||
{
|
||
id: "env-dr-006",
|
||
name: "Disaster Recovery",
|
||
description: "محیط بازیابی پس از بحران - پشتیبان Production",
|
||
backgroundColor: "#e53e3e",
|
||
textColor: "#ffffff",
|
||
},
|
||
],
|
||
products: [
|
||
{
|
||
id: "prod-ecommerce",
|
||
title: "سامانه فروشگاه آنلاین دیجیمارکت",
|
||
description:
|
||
"پلتفرم جامع تجارت الکترونیک با قابلیتهای چندفروشندگی",
|
||
metadata: {
|
||
createdAt: "2026-01-15T08:00:00Z",
|
||
owner: "تیم توسعه تجارت الکترونیک",
|
||
customFields: { industry: "E-commerce", priority: "critical" },
|
||
},
|
||
projects: [
|
||
{
|
||
id: "proj-ecom-api",
|
||
title: "E-commerce Core API",
|
||
description: "سرویس بکاند اصلی فروشگاه",
|
||
type: "backend",
|
||
repositoryUrl:
|
||
"https://git.rayanpardaz.ir/ecommerce/core-api",
|
||
metadata: {
|
||
framework: ".NET 8",
|
||
language: "C#",
|
||
team: "Backend Team A",
|
||
},
|
||
dependencies: ["proj-payment-gateway", "proj-inventory-svc"],
|
||
deployments: [
|
||
{
|
||
id: "dep-ecom-api-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "3.5.2",
|
||
deploymentDate: "2026-09-10T14:30:00Z",
|
||
releaseNotes:
|
||
"رفع باگ پردازش سفارش و بهبود عملکرد کوئریها",
|
||
requiredDeployments: [
|
||
{
|
||
projectId: "proj-payment-gateway",
|
||
deploymentId: "dep-payment-prod",
|
||
},
|
||
{
|
||
projectId: "proj-inventory-svc",
|
||
deploymentId: "dep-inventory-prod",
|
||
},
|
||
],
|
||
status: "active",
|
||
},
|
||
{
|
||
id: "dep-ecom-api-stage",
|
||
environmentId: "env-stage-002",
|
||
version: "3.6.0-rc.1",
|
||
deploymentDate: "2026-09-14T09:00:00Z",
|
||
releaseNotes: "افزودن قابلیت چندزبانه و بهینهسازی کش",
|
||
requiredDeployments: [],
|
||
status: "active",
|
||
},
|
||
{
|
||
id: "dep-ecom-api-dev",
|
||
environmentId: "env-dev-003",
|
||
version: "3.7.0-dev.12",
|
||
deploymentDate: "2026-09-15T16:00:00Z",
|
||
releaseNotes: "پیادهسازی اولیه ماژول تخفیف پیشرفته",
|
||
requiredDeployments: [],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "proj-ecom-web",
|
||
title: "E-commerce Web Frontend",
|
||
description: "اپلیکیشن وب فروشگاه با Angular",
|
||
type: "frontend",
|
||
repositoryUrl: "https://git.rayanpardaz.ir/ecommerce/web-app",
|
||
metadata: { framework: "Angular 18", language: "TypeScript" },
|
||
dependencies: ["proj-ecom-api"],
|
||
deployments: [
|
||
{
|
||
id: "dep-ecom-web-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "4.2.1",
|
||
deploymentDate: "2026-09-11T10:00:00Z",
|
||
releaseNotes:
|
||
"بهبود UX صفحه پرداخت و افزودن فیلتر پیشرفته",
|
||
requiredDeployments: [
|
||
{
|
||
projectId: "proj-ecom-api",
|
||
deploymentId: "dep-ecom-api-prod",
|
||
},
|
||
],
|
||
status: "active",
|
||
},
|
||
{
|
||
id: "dep-ecom-web-stage",
|
||
environmentId: "env-stage-002",
|
||
version: "4.3.0-rc.1",
|
||
deploymentDate: "2026-09-13T11:30:00Z",
|
||
releaseNotes: "پیادهسازی Dark Mode و بهینهسازی SEO",
|
||
requiredDeployments: [],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "proj-ecom-mobile",
|
||
title: "E-commerce Mobile App",
|
||
description: "اپلیکیشن موبایل فروشگاه (iOS/Android)",
|
||
type: "mobile",
|
||
repositoryUrl:
|
||
"https://git.rayanpardaz.ir/ecommerce/mobile-app",
|
||
metadata: { framework: "Flutter 3.22", language: "Dart" },
|
||
dependencies: ["proj-ecom-api", "proj-notification-svc"],
|
||
deployments: [
|
||
{
|
||
id: "dep-ecom-mobile-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "2.8.0",
|
||
deploymentDate: "2026-09-08T08:00:00Z",
|
||
releaseNotes:
|
||
"انتشار در استورها با قابلیت Push Notification",
|
||
requiredDeployments: [
|
||
{
|
||
projectId: "proj-ecom-api",
|
||
deploymentId: "dep-ecom-api-prod",
|
||
},
|
||
],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "proj-ecom-db",
|
||
title: "E-commerce Database",
|
||
description: "پایگاه داده اصلی فروشگاه",
|
||
type: "database",
|
||
repositoryUrl:
|
||
"https://git.rayanpardaz.ir/ecommerce/db-migrations",
|
||
metadata: { framework: "SQL Server 2022", language: "T-SQL" },
|
||
dependencies: [],
|
||
deployments: [
|
||
{
|
||
id: "dep-ecom-db-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "5.1.0",
|
||
deploymentDate: "2026-09-05T02:00:00Z",
|
||
releaseNotes:
|
||
"افزودن ایندکسهای جدید و بهینهسازی جداول سفارش",
|
||
requiredDeployments: [],
|
||
status: "active",
|
||
},
|
||
{
|
||
id: "dep-ecom-db-dr",
|
||
environmentId: "env-dr-006",
|
||
version: "5.1.0",
|
||
deploymentDate: "2026-09-05T03:00:00Z",
|
||
releaseNotes:
|
||
"همگامسازی با Production برای Disaster Recovery",
|
||
requiredDeployments: [
|
||
{
|
||
projectId: "proj-ecom-db",
|
||
deploymentId: "dep-ecom-db-prod",
|
||
},
|
||
],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "proj-ecom-search",
|
||
title: "E-commerce Search Service",
|
||
description: "سرویس جستجوی پیشرفته مبتنی بر Elasticsearch",
|
||
type: "service",
|
||
repositoryUrl:
|
||
"https://git.rayanpardaz.ir/ecommerce/search-svc",
|
||
metadata: { framework: ".NET 8", language: "C#" },
|
||
dependencies: ["proj-ecom-db"],
|
||
deployments: [
|
||
{
|
||
id: "dep-ecom-search-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "1.9.3",
|
||
deploymentDate: "2026-09-09T13:00:00Z",
|
||
releaseNotes:
|
||
"بهبود الگوریتم جستجوی فازی و افزودن فیلتر قیمت",
|
||
requiredDeployments: [
|
||
{
|
||
projectId: "proj-ecom-db",
|
||
deploymentId: "dep-ecom-db-prod",
|
||
},
|
||
],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "proj-ecom-infra",
|
||
title: "E-commerce Infrastructure",
|
||
description: "زیرساخت Kubernetes و CI/CD",
|
||
type: "infrastructure",
|
||
repositoryUrl: "https://git.rayanpardaz.ir/ecommerce/infra",
|
||
metadata: {
|
||
framework: "Kubernetes 1.29",
|
||
language: "YAML/Helm",
|
||
},
|
||
dependencies: [],
|
||
deployments: [
|
||
{
|
||
id: "dep-ecom-infra-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "2.1.0",
|
||
deploymentDate: "2026-09-01T06:00:00Z",
|
||
releaseNotes:
|
||
"بهروزرسانی Helm Charts و افزودن Auto-scaling",
|
||
requiredDeployments: [],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "prod-banking",
|
||
title: "پلتفرم بانکداری دیجیتال پارسیانبانک",
|
||
description: "سامانه جامع بانکداری دیجیتال با قابلیتهای نئوبانک",
|
||
metadata: {
|
||
createdAt: "2026-02-01T08:00:00Z",
|
||
owner: "تیم توسعه بانکداری دیجیتال",
|
||
customFields: { industry: "FinTech", priority: "critical" },
|
||
},
|
||
projects: [
|
||
{
|
||
id: "proj-bank-core",
|
||
title: "Banking Core Engine",
|
||
description: "موتور اصلی پردازش تراکنشهای بانکی",
|
||
type: "backend",
|
||
repositoryUrl:
|
||
"https://git.rayanpardaz.ir/banking/core-engine",
|
||
metadata: { framework: ".NET 8", language: "C#" },
|
||
dependencies: ["proj-payment-gateway", "proj-auth-svc"],
|
||
deployments: [
|
||
{
|
||
id: "dep-bank-core-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "6.2.0",
|
||
deploymentDate: "2026-09-12T01:00:00Z",
|
||
releaseNotes:
|
||
"افزودن پشتیبانی از تراکنشهای بینالمللی و بهبود امنیت",
|
||
requiredDeployments: [
|
||
{
|
||
projectId: "proj-payment-gateway",
|
||
deploymentId: "dep-payment-prod",
|
||
},
|
||
],
|
||
status: "active",
|
||
},
|
||
{
|
||
id: "dep-bank-core-uat",
|
||
environmentId: "env-uat-005",
|
||
version: "6.3.0-rc.2",
|
||
deploymentDate: "2026-09-14T10:00:00Z",
|
||
releaseNotes: "تست پذیرش ماژول وامدهی خرد",
|
||
requiredDeployments: [],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "proj-bank-web",
|
||
title: "Banking Web Portal",
|
||
description: "پرتال وب بانکداری اینترنتی",
|
||
type: "frontend",
|
||
repositoryUrl:
|
||
"https://git.rayanpardaz.ir/banking/web-portal",
|
||
metadata: { framework: "Angular 18", language: "TypeScript" },
|
||
dependencies: ["proj-bank-core"],
|
||
deployments: [
|
||
{
|
||
id: "dep-bank-web-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "5.1.4",
|
||
deploymentDate: "2026-09-13T08:30:00Z",
|
||
releaseNotes:
|
||
"بازطراحی داشبورد کاربر و افزودن گزارشهای مالی",
|
||
requiredDeployments: [
|
||
{
|
||
projectId: "proj-bank-core",
|
||
deploymentId: "dep-bank-core-prod",
|
||
},
|
||
],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "proj-bank-mobile",
|
||
title: "Banking Mobile App",
|
||
description: "اپلیکیشن موبایل بانکداری همراه",
|
||
type: "mobile",
|
||
repositoryUrl:
|
||
"https://git.rayanpardaz.ir/banking/mobile-app",
|
||
metadata: {
|
||
framework: "Kotlin Multiplatform",
|
||
language: "Kotlin",
|
||
},
|
||
dependencies: ["proj-bank-core", "proj-notification-svc"],
|
||
deployments: [
|
||
{
|
||
id: "dep-bank-mobile-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "4.0.2",
|
||
deploymentDate: "2026-09-07T09:00:00Z",
|
||
releaseNotes: "افزودن ورود با Biometric و کارت به کارت",
|
||
requiredDeployments: [
|
||
{
|
||
projectId: "proj-bank-core",
|
||
deploymentId: "dep-bank-core-prod",
|
||
},
|
||
],
|
||
status: "active",
|
||
},
|
||
{
|
||
id: "dep-bank-mobile-qa",
|
||
environmentId: "env-qa-004",
|
||
version: "4.1.0-beta.3",
|
||
deploymentDate: "2026-09-15T14:00:00Z",
|
||
releaseNotes: "تست قابلیت پرداخت با QR Code",
|
||
requiredDeployments: [],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "proj-bank-db",
|
||
title: "Banking Database Cluster",
|
||
description: "خوشه پایگاه داده بانکی با قابلیت HA",
|
||
type: "database",
|
||
repositoryUrl:
|
||
"https://git.rayanpardaz.ir/banking/db-cluster",
|
||
metadata: { framework: "Oracle 19c", language: "PL/SQL" },
|
||
dependencies: [],
|
||
deployments: [
|
||
{
|
||
id: "dep-bank-db-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "8.0.1",
|
||
deploymentDate: "2026-09-02T03:00:00Z",
|
||
releaseNotes:
|
||
"ارتقای ساختار جداول تراکنش و افزودن Partitioning",
|
||
requiredDeployments: [],
|
||
status: "active",
|
||
},
|
||
{
|
||
id: "dep-bank-db-dr",
|
||
environmentId: "env-dr-006",
|
||
version: "8.0.1",
|
||
deploymentDate: "2026-09-02T04:00:00Z",
|
||
releaseNotes: "همگامسازی Active-Passive با Production",
|
||
requiredDeployments: [],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "proj-bank-fraud",
|
||
title: "Fraud Detection Service",
|
||
description: "سرویس هوشمند تشخیص تقلب با ML",
|
||
type: "service",
|
||
repositoryUrl: "https://git.rayanpardaz.ir/banking/fraud-svc",
|
||
metadata: {
|
||
framework: "Python 3.12 + FastAPI",
|
||
language: "Python",
|
||
},
|
||
dependencies: ["proj-bank-core", "proj-bank-db"],
|
||
deployments: [
|
||
{
|
||
id: "dep-bank-fraud-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "2.4.0",
|
||
deploymentDate: "2026-09-11T15:00:00Z",
|
||
releaseNotes:
|
||
"بهروزرسانی مدل ML و کاهش نرخ False Positive",
|
||
requiredDeployments: [
|
||
{
|
||
projectId: "proj-bank-core",
|
||
deploymentId: "dep-bank-core-prod",
|
||
},
|
||
],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "prod-payment",
|
||
title: "پلتفرم پرداخت و کیف پول پرداختیار",
|
||
description: "زیرساخت مشترک پرداخت برای تمام محصولات سازمان",
|
||
metadata: {
|
||
createdAt: "2026-01-05T08:00:00Z",
|
||
owner: "تیم توسعه Payment",
|
||
customFields: {
|
||
industry: "FinTech",
|
||
priority: "critical",
|
||
shared: true,
|
||
},
|
||
},
|
||
projects: [
|
||
{
|
||
id: "proj-payment-gateway",
|
||
title: "Payment Gateway Service",
|
||
description: "درگاه پرداخت یکپارچه - سرویس مشترک",
|
||
type: "service",
|
||
repositoryUrl: "https://git.rayanpardaz.ir/payment/gateway",
|
||
metadata: { framework: ".NET 8", language: "C#" },
|
||
dependencies: ["proj-payment-db"],
|
||
deployments: [
|
||
{
|
||
id: "dep-payment-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "5.3.0",
|
||
deploymentDate: "2026-09-09T12:00:00Z",
|
||
releaseNotes:
|
||
"پشتیبانی از درگاههای جدید و توکنسازی کارت",
|
||
requiredDeployments: [
|
||
{
|
||
projectId: "proj-payment-db",
|
||
deploymentId: "dep-payment-db-prod",
|
||
},
|
||
],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "proj-payment-db",
|
||
title: "Payment Database",
|
||
description: "پایگاه داده تراکنشهای مالی",
|
||
type: "database",
|
||
repositoryUrl: "https://git.rayanpardaz.ir/payment/db",
|
||
metadata: { framework: "Oracle 19c RAC", language: "PL/SQL" },
|
||
dependencies: [],
|
||
deployments: [
|
||
{
|
||
id: "dep-payment-db-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "6.0.0",
|
||
deploymentDate: "2026-09-01T02:00:00Z",
|
||
releaseNotes:
|
||
"ارتقای ساختار تراکنشها با قابلیت Audit کامل",
|
||
requiredDeployments: [],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "proj-auth-svc",
|
||
title: "Authentication & Authorization Service",
|
||
description: "سرویس مشترک احراز هویت و مجوزدهی",
|
||
type: "service",
|
||
repositoryUrl: "https://git.rayanpardaz.ir/shared/auth-svc",
|
||
metadata: { framework: ".NET 8", language: "C#" },
|
||
dependencies: ["proj-payment-db"],
|
||
deployments: [
|
||
{
|
||
id: "dep-auth-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "4.1.0",
|
||
deploymentDate: "2026-09-08T06:00:00Z",
|
||
releaseNotes: "افزودن MFA و پشتیبانی از OAuth 2.1",
|
||
requiredDeployments: [],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "proj-notification-svc",
|
||
title: "Notification Service",
|
||
description: "سرویس مشترک ارسال اعلان (SMS/Email/Push)",
|
||
type: "service",
|
||
repositoryUrl:
|
||
"https://git.rayanpardaz.ir/shared/notification-svc",
|
||
metadata: { framework: ".NET 8", language: "C#" },
|
||
dependencies: [],
|
||
deployments: [
|
||
{
|
||
id: "dep-notif-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "2.5.0",
|
||
deploymentDate: "2026-09-07T07:00:00Z",
|
||
releaseNotes: "افزودن کانال WhatsApp و بهینهسازی صف",
|
||
requiredDeployments: [],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "proj-inventory-svc",
|
||
title: "Inventory Management Service",
|
||
description: "سرویس مدیریت موجودی انبار",
|
||
type: "service",
|
||
repositoryUrl:
|
||
"https://git.rayanpardaz.ir/shared/inventory-svc",
|
||
metadata: { framework: ".NET 8", language: "C#" },
|
||
dependencies: ["proj-payment-db"],
|
||
deployments: [
|
||
{
|
||
id: "dep-inventory-prod",
|
||
environmentId: "env-prod-001",
|
||
version: "2.0.3",
|
||
deploymentDate: "2026-09-06T10:00:00Z",
|
||
releaseNotes: "همگامسازی Real-time با انبارهای فیزیکی",
|
||
requiredDeployments: [],
|
||
status: "active",
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
};
|
||
|
||
const VALID_PROJECT_TYPES = [
|
||
{ value: "backend", label: "بکاند (Backend)", icon: "🖥️" },
|
||
{ value: "frontend", label: "فرانتاند (Frontend)", icon: "🌐" },
|
||
{ value: "service", label: "میکروسرویس (Service)", icon: "⚙️" },
|
||
{ value: "database", label: "پایگاه داده (Database)", icon: "🗄️" },
|
||
{ value: "mobile", label: "موبایل (Mobile)", icon: "📱" },
|
||
{
|
||
value: "infrastructure",
|
||
label: "زیرساخت (Infrastructure)",
|
||
icon: "🏗️",
|
||
},
|
||
];
|
||
|
||
/* ==================== HELPERS ==================== */
|
||
function validateAppDataStructure(d) {
|
||
return (
|
||
d &&
|
||
typeof d === "object" &&
|
||
Array.isArray(d.environments) &&
|
||
Array.isArray(d.products)
|
||
);
|
||
}
|
||
function populateProjectTypeDropdowns() {
|
||
["newProjectType", "editProjType"].forEach((id) => {
|
||
const s = document.getElementById(id);
|
||
if (!s) return;
|
||
const cv = s.value;
|
||
s.innerHTML =
|
||
'<option value="">انتخاب کنید...</option>' +
|
||
VALID_PROJECT_TYPES.map(
|
||
(t) => `<option value="${t.value}">${t.icon} ${t.label}</option>`,
|
||
).join("");
|
||
if (cv) s.value = cv;
|
||
});
|
||
}
|
||
function showToast(msg, type = "info", dur = 3000) {
|
||
const c = document.getElementById("toastContainer");
|
||
const t = document.createElement("div");
|
||
t.className = `toast ${type}`;
|
||
const ic = { success: "✅", error: "❌", warning: "⚠️", info: "ℹ️" };
|
||
t.innerHTML = `<span>${ic[type] || "ℹ️"}</span><span>${msg}</span>`;
|
||
c.appendChild(t);
|
||
setTimeout(() => {
|
||
t.style.opacity = "0";
|
||
setTimeout(() => t.remove(), 300);
|
||
}, dur);
|
||
}
|
||
function showD3Tooltip(e, text) {
|
||
let tt = document.getElementById("d3Tooltip");
|
||
if (!tt) return;
|
||
if (text) tt.textContent = text;
|
||
tt.classList.add("visible");
|
||
tt.style.left = e.clientX + 12 + "px";
|
||
tt.style.top = e.clientY + 12 + "px";
|
||
}
|
||
function hideD3Tooltip() {
|
||
const tt = document.getElementById("d3Tooltip");
|
||
if (tt) tt.classList.remove("visible");
|
||
}
|
||
function refreshUI() {
|
||
ToolbarManager.updateStats();
|
||
BoardRenderer.render();
|
||
}
|
||
|
||
/* ==================== MODAL MANAGER ==================== */
|
||
const ModalManager = {
|
||
stack: [],
|
||
open(id) {
|
||
const m = document.getElementById(id);
|
||
if (!m) return;
|
||
this.stack = this.stack.filter((x) => x !== id);
|
||
this.stack.push(id);
|
||
this.stack.forEach((mid, i) => {
|
||
const el = document.getElementById(mid);
|
||
if (!el) return;
|
||
if (mid === id) el.classList.add("active");
|
||
el.style.zIndex = 1000 + i + 1;
|
||
});
|
||
},
|
||
close(id) {
|
||
const m = document.getElementById(id);
|
||
if (!m) return;
|
||
m.classList.remove("active");
|
||
m.style.zIndex = "";
|
||
this.stack = this.stack.filter((x) => x !== id);
|
||
this.stack.forEach((mid, i) => {
|
||
const el = document.getElementById(mid);
|
||
if (el) el.style.zIndex = 1000 + i + 1;
|
||
});
|
||
},
|
||
closeTop() {
|
||
if (this.stack.length > 0)
|
||
this.close(this.stack[this.stack.length - 1]);
|
||
},
|
||
init() {
|
||
document.querySelectorAll("[data-close-modal]").forEach((btn) => {
|
||
btn.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
const m = e.target.closest(".modal-overlay");
|
||
if (m && m.id) this.close(m.id);
|
||
});
|
||
});
|
||
document.querySelectorAll(".modal-overlay").forEach((ov) => {
|
||
ov.addEventListener("click", (e) => {
|
||
if (e.target === ov && ov.id) this.close(ov.id);
|
||
});
|
||
});
|
||
document.addEventListener("keydown", (e) => {
|
||
if (e.key === "Escape") {
|
||
e.preventDefault();
|
||
this.closeTop();
|
||
}
|
||
});
|
||
},
|
||
};
|
||
|
||
/* ==================== TOOLBAR MANAGER ==================== */
|
||
const ToolbarManager = {
|
||
currentFilters: { query: "", typeFilter: "", envFilter: "" },
|
||
init() {
|
||
this.populateEnvironmentFilter();
|
||
this.bindEvents();
|
||
this.updateStats();
|
||
},
|
||
populateEnvironmentFilter() {
|
||
const s = document.getElementById("filterEnvironment");
|
||
if (!s) return;
|
||
s.innerHTML =
|
||
'<option value="">همه محیطها</option>' +
|
||
projectData.appData.environments
|
||
.map((e) => `<option value="${e.id}">${e.name}</option>`)
|
||
.join("");
|
||
},
|
||
bindEvents() {
|
||
const si = document.getElementById("toolbarSearch"),
|
||
ft = document.getElementById("filterType"),
|
||
fe = document.getElementById("filterEnvironment");
|
||
if (si)
|
||
si.addEventListener("input", () => {
|
||
this.applyFilters();
|
||
refreshUI();
|
||
});
|
||
if (ft)
|
||
ft.addEventListener("change", () => {
|
||
this.applyFilters();
|
||
refreshUI();
|
||
});
|
||
if (fe)
|
||
fe.addEventListener("change", () => {
|
||
this.applyFilters();
|
||
refreshUI();
|
||
});
|
||
},
|
||
applyFilters() {
|
||
this.currentFilters = {
|
||
query: (document.getElementById("toolbarSearch")?.value || "")
|
||
.trim()
|
||
.toLowerCase(),
|
||
typeFilter: document.getElementById("filterType")?.value || "",
|
||
envFilter:
|
||
document.getElementById("filterEnvironment")?.value || "",
|
||
};
|
||
this.updateStats();
|
||
},
|
||
updateStats() {
|
||
const ps = projectData.appData.products || [];
|
||
let tp = 0,
|
||
td = 0;
|
||
ps.forEach((p) => {
|
||
(p.projects || []).forEach((pr) => {
|
||
tp++;
|
||
td += (pr.dependencies || []).length;
|
||
});
|
||
});
|
||
const s = (id, v) => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.textContent = v;
|
||
};
|
||
s("statTotal", ps.length + tp);
|
||
s("statProjects", ps.length);
|
||
s("statSubProjects", tp);
|
||
s("statDependencies", td);
|
||
},
|
||
shouldShowProduct(product) {
|
||
const f = this.currentFilters;
|
||
if (!f.query && !f.typeFilter && !f.envFilter) return true;
|
||
return (product.projects || []).some((pr) => {
|
||
const mq =
|
||
!f.query ||
|
||
pr.title.toLowerCase().includes(f.query) ||
|
||
product.title.toLowerCase().includes(f.query);
|
||
const mt = !f.typeFilter || pr.type === f.typeFilter;
|
||
const me =
|
||
!f.envFilter ||
|
||
(pr.deployments || [])
|
||
.filter((d) => d.status === "active")
|
||
.some((d) => d.environmentId === f.envFilter);
|
||
return mq && mt && me;
|
||
});
|
||
},
|
||
};
|
||
|
||
/* ==================== HISTORY MANAGER ==================== */
|
||
const HistoryManager = {
|
||
undoStack: [],
|
||
redoStack: [],
|
||
maxSize: 50,
|
||
isUndoRedoInProgress: false,
|
||
_debounceTimer: null,
|
||
_pendingState: null,
|
||
|
||
pushState() {
|
||
if (this.isUndoRedoInProgress) return;
|
||
this._pendingState = projectData.appData;
|
||
if (this._debounceTimer) clearTimeout(this._debounceTimer);
|
||
this._debounceTimer = setTimeout(() => {
|
||
this._commitState();
|
||
}, 100);
|
||
},
|
||
|
||
_commitState() {
|
||
if (!this._pendingState) return;
|
||
let snapshot;
|
||
if (typeof structuredClone === "function") {
|
||
snapshot = structuredClone(this._pendingState);
|
||
} else {
|
||
snapshot = JSON.parse(JSON.stringify(this._pendingState));
|
||
}
|
||
this.undoStack.push(snapshot);
|
||
if (this.undoStack.length > this.maxSize) this.undoStack.shift();
|
||
this.redoStack = [];
|
||
this._pendingState = null;
|
||
this.updateButtons();
|
||
},
|
||
|
||
undo() {
|
||
if (this.undoStack.length === 0) {
|
||
showToast("هیچ تغییری نیست", "warning");
|
||
return;
|
||
}
|
||
if (this._pendingState) this._commitState();
|
||
this.redoStack.push(
|
||
typeof structuredClone === "function"
|
||
? structuredClone(projectData.appData)
|
||
: JSON.parse(JSON.stringify(projectData.appData)),
|
||
);
|
||
this.isUndoRedoInProgress = true;
|
||
projectData.appData = this.undoStack.pop();
|
||
this.isUndoRedoInProgress = false;
|
||
this.updateUI();
|
||
this.updateButtons();
|
||
showToast("↩️ بازگشت", "success");
|
||
},
|
||
|
||
redo() {
|
||
if (this.redoStack.length === 0) {
|
||
showToast("هیچ تغییری نیست", "warning");
|
||
return;
|
||
}
|
||
if (this._pendingState) this._commitState();
|
||
this.undoStack.push(
|
||
typeof structuredClone === "function"
|
||
? structuredClone(projectData.appData)
|
||
: JSON.parse(JSON.stringify(projectData.appData)),
|
||
);
|
||
this.isUndoRedoInProgress = true;
|
||
projectData.appData = this.redoStack.pop();
|
||
this.isUndoRedoInProgress = false;
|
||
this.updateUI();
|
||
this.updateButtons();
|
||
showToast("↪️ باز انجام", "success");
|
||
},
|
||
updateUI() {
|
||
refreshUI();
|
||
if (ProductManager.currentDetailProductId)
|
||
ProductManager.showDetail(ProductManager.currentDetailProductId);
|
||
},
|
||
updateButtons() {
|
||
const u = document.getElementById("undoBtn"),
|
||
r = document.getElementById("redoBtn");
|
||
if (u) u.disabled = this.undoStack.length === 0;
|
||
if (r) r.disabled = this.redoStack.length === 0;
|
||
if (ReleaseBoardsManager)
|
||
ReleaseBoardsManager.updateUndoRedoButtons();
|
||
},
|
||
init() {
|
||
this.pushState();
|
||
document
|
||
.getElementById("undoBtn")
|
||
.addEventListener("click", () => this.undo());
|
||
document
|
||
.getElementById("redoBtn")
|
||
.addEventListener("click", () => this.redo());
|
||
document.addEventListener("keydown", (e) => {
|
||
if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
|
||
e.preventDefault();
|
||
this.undo();
|
||
}
|
||
if (
|
||
(e.ctrlKey || e.metaKey) &&
|
||
(e.key === "y" || (e.key === "z" && e.shiftKey))
|
||
) {
|
||
e.preventDefault();
|
||
this.redo();
|
||
}
|
||
});
|
||
},
|
||
};
|
||
|
||
/* ==================== SECTION COLLAPSE ==================== */
|
||
const SectionCollapseManager = {
|
||
storageKey: "boardSectionCollapseState",
|
||
getState() {
|
||
try {
|
||
const s = localStorage.getItem(this.storageKey);
|
||
return s
|
||
? JSON.parse(s)
|
||
: { dashboard: false, products: false, environments: false };
|
||
} catch {
|
||
return { dashboard: false, products: false, environments: false };
|
||
}
|
||
},
|
||
saveState(s) {
|
||
try {
|
||
localStorage.setItem(this.storageKey, JSON.stringify(s));
|
||
} catch (e) {}
|
||
},
|
||
toggle(id) {
|
||
const s = this.getState();
|
||
s[id] = !s[id];
|
||
this.saveState(s);
|
||
const sec = document.getElementById(`section-${id}`);
|
||
if (sec) sec.classList.toggle("collapsed", s[id]);
|
||
return s[id];
|
||
},
|
||
init() {
|
||
const s = this.getState();
|
||
Object.keys(s).forEach((id) => {
|
||
const sec = document.getElementById(`section-${id}`);
|
||
if (sec && s[id]) sec.classList.add("collapsed");
|
||
});
|
||
document
|
||
.getElementById("boardArea")
|
||
.addEventListener("click", (e) => {
|
||
const ht = e.target.closest(".board-header-title");
|
||
if (ht) {
|
||
const sec = ht.closest(".board-section");
|
||
if (sec) {
|
||
this.toggle(sec.id.replace("section-", ""));
|
||
}
|
||
}
|
||
});
|
||
},
|
||
};
|
||
|
||
/* ==================== DASHBOARD SECTION ==================== */
|
||
const DashboardSectionRenderer = {
|
||
getSectionHTML() {
|
||
const d = this.getData();
|
||
let h = `<div class="board-section" id="section-dashboard">`;
|
||
h += `<div class="board-header"><div class="board-header-title"><span class="collapse-icon">▼</span>📊 داشبورد مدیریتی</div><div class="board-header-actions"><button class="btn btn-accent btn-sm" onclick="DashboardManager.open()">📊 مشاهده کامل</button></div></div>`;
|
||
h += `<div class="section-content">`;
|
||
h += '<div class="dashboard-kpi-row" style="margin-bottom:16px">';
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📦</div><div class="dashboard-kpi-value">${d.products}</div><div class="dashboard-kpi-label">محصول</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🌿</div><div class="dashboard-kpi-value">${d.projects}</div><div class="dashboard-kpi-label">پروژه</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔗</div><div class="dashboard-kpi-value">${d.deps}</div><div class="dashboard-kpi-label">وابستگی</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🚀</div><div class="dashboard-kpi-value">${d.deploys}</div><div class="dashboard-kpi-label">استقرار</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🌍</div><div class="dashboard-kpi-value">${d.environments}</div><div class="dashboard-kpi-label">محیط</div></div>`;
|
||
h += "</div>";
|
||
h += '<div class="dashboard-charts-grid">';
|
||
h +=
|
||
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🥧 توزیع نوع پروژهها</div><div class="dashboard-chart-body" id="mainDashTypeChart"></div></div>';
|
||
h +=
|
||
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🌍 استقرارها به تفکیک محیط</div><div class="dashboard-chart-body" id="mainDashEnvChart"></div></div>';
|
||
h +=
|
||
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">✅ وضعیت استقرار پروژهها</div><div class="dashboard-chart-body" id="mainDashStatusChart"></div></div>';
|
||
h +=
|
||
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 پروژهها به تفکیک محصول</div><div class="dashboard-chart-body" id="mainDashProductChart"></div></div>';
|
||
h += "</div>";
|
||
h += "</div></div>";
|
||
return h;
|
||
},
|
||
|
||
getData() {
|
||
const ps = projectData.appData.products || [];
|
||
let tp = 0,
|
||
td = 0,
|
||
tdp = 0;
|
||
const tc = {},
|
||
edc = {},
|
||
ppc = [];
|
||
let pwd = 0,
|
||
pwod = 0;
|
||
ps.forEach((prod) => {
|
||
const projects = prod.projects || [];
|
||
ppc.push({ title: prod.title, count: projects.length });
|
||
projects.forEach((pr) => {
|
||
tp++;
|
||
td += (pr.dependencies || []).length;
|
||
const active = (pr.deployments || []).filter(
|
||
(d) => d.status === "active",
|
||
);
|
||
tdp += active.length;
|
||
if (active.length > 0) pwd++;
|
||
else pwod++;
|
||
tc[pr.type] = (tc[pr.type] || 0) + 1;
|
||
active.forEach((d) => {
|
||
const env = projectData.appData.environments.find(
|
||
(e) => e.id === d.environmentId,
|
||
);
|
||
const envName = env ? env.name : "نامشخص";
|
||
edc[envName] = (edc[envName] || 0) + 1;
|
||
});
|
||
});
|
||
});
|
||
return {
|
||
products: ps.length,
|
||
projects: tp,
|
||
deps: td,
|
||
deploys: tdp,
|
||
environments: projectData.appData.environments.length,
|
||
tc,
|
||
edc,
|
||
ppc,
|
||
pwd,
|
||
pwod,
|
||
};
|
||
},
|
||
|
||
renderCharts() {
|
||
const d = this.getData();
|
||
this.renderDonut(
|
||
"mainDashTypeChart",
|
||
Object.entries(d.tc).map(([k, v]) => ({
|
||
label: this.getTypeLabel(k),
|
||
value: v,
|
||
color: this.getTypeColor(k),
|
||
})),
|
||
);
|
||
this.renderBar(
|
||
"mainDashEnvChart",
|
||
Object.entries(d.edc).map(([k, v]) => ({ label: k, value: v })),
|
||
"#38a169",
|
||
);
|
||
this.renderDonut("mainDashStatusChart", [
|
||
{ label: "با استقرار", value: d.pwd, color: "#38a169" },
|
||
{ label: "بدون استقرار", value: d.pwod, color: "#e53e3e" },
|
||
]);
|
||
this.renderBar(
|
||
"mainDashProductChart",
|
||
d.ppc.map((p) => ({
|
||
label:
|
||
p.title.length > 15
|
||
? p.title.substring(0, 15) + "..."
|
||
: p.title,
|
||
value: p.count,
|
||
})),
|
||
"#3182ce",
|
||
);
|
||
},
|
||
|
||
renderDonut(cid, data) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
c.innerHTML = "";
|
||
if (!data || data.length === 0 || data.every((d) => d.value === 0)) {
|
||
c.innerHTML =
|
||
'<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>';
|
||
return;
|
||
}
|
||
const w = 240,
|
||
hgt = 200,
|
||
r = Math.min(w, hgt) / 2 - 15;
|
||
const svg = d3
|
||
.select(c)
|
||
.append("svg")
|
||
.attr("width", w)
|
||
.attr("height", hgt);
|
||
const g = svg
|
||
.append("g")
|
||
.attr("transform", `translate(${w / 2},${hgt / 2})`);
|
||
const pie = d3
|
||
.pie()
|
||
.value((d) => d.value)
|
||
.sort(null);
|
||
const arc = d3
|
||
.arc()
|
||
.innerRadius(r * 0.55)
|
||
.outerRadius(r);
|
||
const arcH = d3
|
||
.arc()
|
||
.innerRadius(r * 0.55)
|
||
.outerRadius(r + 6);
|
||
const total = data.reduce((s, d) => s + d.value, 0);
|
||
g.selectAll("path")
|
||
.data(pie(data))
|
||
.join("path")
|
||
.attr("d", arc)
|
||
.attr("fill", (d) => d.data.color)
|
||
.attr("stroke", "#fff")
|
||
.attr("stroke-width", 2)
|
||
.style("cursor", "pointer")
|
||
.on("mouseover", function (e, d) {
|
||
d3.select(this).transition().duration(150).attr("d", arcH(d));
|
||
showD3Tooltip(
|
||
e,
|
||
`${d.data.label}: ${d.data.value} (${Math.round((d.data.value / total) * 100)}%)`,
|
||
);
|
||
})
|
||
.on("mousemove", (e) => showD3Tooltip(e, ""))
|
||
.on("mouseout", function () {
|
||
d3.select(this).transition().duration(150).attr("d", arc);
|
||
hideD3Tooltip();
|
||
});
|
||
g.append("text")
|
||
.attr("text-anchor", "middle")
|
||
.attr("dy", "-.2em")
|
||
.attr("fill", "var(--text)")
|
||
.attr("font-size", "20px")
|
||
.attr("font-weight", "800")
|
||
.text(total);
|
||
g.append("text")
|
||
.attr("text-anchor", "middle")
|
||
.attr("dy", "1.3em")
|
||
.attr("fill", "var(--text-muted)")
|
||
.attr("font-size", "10px")
|
||
.text("پروژه");
|
||
},
|
||
|
||
renderBar(cid, data, color) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
c.innerHTML = "";
|
||
if (!data || data.length === 0) {
|
||
c.innerHTML =
|
||
'<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>';
|
||
return;
|
||
}
|
||
const w = 280,
|
||
hgt = 200,
|
||
m = { top: 20, right: 15, bottom: 40, left: 30 };
|
||
const iw = w - m.left - m.right,
|
||
ih = hgt - m.top - m.bottom;
|
||
const svg = d3
|
||
.select(c)
|
||
.append("svg")
|
||
.attr("width", w)
|
||
.attr("height", hgt);
|
||
const g = svg
|
||
.append("g")
|
||
.attr("transform", `translate(${m.left},${m.top})`);
|
||
const x = d3
|
||
.scaleBand()
|
||
.domain(data.map((d) => d.label))
|
||
.range([0, iw])
|
||
.padding(0.3);
|
||
const y = d3
|
||
.scaleLinear()
|
||
.domain([0, d3.max(data, (d) => d.value) || 1])
|
||
.nice()
|
||
.range([ih, 0]);
|
||
g.append("g")
|
||
.attr("transform", `translate(0,${ih})`)
|
||
.call(d3.axisBottom(x).tickSize(0))
|
||
.selectAll("text")
|
||
.attr("fill", "var(--text-muted)")
|
||
.attr("font-size", "8px")
|
||
.attr("font-family", "Vazirmatn")
|
||
.attr("transform", "rotate(-20)")
|
||
.attr("text-anchor", "end");
|
||
g.append("g")
|
||
.call(d3.axisLeft(y).ticks(4).tickSize(-iw))
|
||
.selectAll("text")
|
||
.attr("fill", "var(--text-muted)")
|
||
.attr("font-size", "9px");
|
||
g.selectAll("rect")
|
||
.data(data)
|
||
.join("rect")
|
||
.attr("x", (d) => x(d.label))
|
||
.attr("y", (d) => y(d.value))
|
||
.attr("width", x.bandwidth())
|
||
.attr("height", (d) => ih - y(d.value))
|
||
.attr("fill", color)
|
||
.attr("rx", 4)
|
||
.style("cursor", "pointer")
|
||
.on("mouseover", function (e, d) {
|
||
d3.select(this).attr("fill", "var(--accent)");
|
||
showD3Tooltip(e, `${d.label}: ${d.value}`);
|
||
})
|
||
.on("mousemove", (e) => showD3Tooltip(e, ""))
|
||
.on("mouseout", function () {
|
||
d3.select(this).attr("fill", color);
|
||
hideD3Tooltip();
|
||
});
|
||
g.selectAll(".bar-label")
|
||
.data(data)
|
||
.join("text")
|
||
.attr("x", (d) => x(d.label) + x.bandwidth() / 2)
|
||
.attr("y", (d) => y(d.value) - 5)
|
||
.attr("text-anchor", "middle")
|
||
.attr("fill", "var(--text)")
|
||
.attr("font-size", "10px")
|
||
.attr("font-weight", "700")
|
||
.text((d) => d.value);
|
||
},
|
||
|
||
getTypeLabel(t) {
|
||
const i = VALID_PROJECT_TYPES.find((x) => x.value === t);
|
||
return i ? i.label.split(" ")[0] : t;
|
||
},
|
||
getTypeColor(t) {
|
||
const c = {
|
||
backend: "#4299e1",
|
||
frontend: "#48bb78",
|
||
service: "#ecc94b",
|
||
database: "#ed64a6",
|
||
mobile: "#9f7aea",
|
||
infrastructure: "#ed8936",
|
||
};
|
||
return c[t] || "#718096";
|
||
},
|
||
};
|
||
|
||
/* ==================== ENVIRONMENT RENDERER ==================== */
|
||
const EnvironmentRenderer = {
|
||
getSectionHTML() {
|
||
const envs = projectData.appData.environments || [];
|
||
let h = `<div class="board-section" id="section-environments"><div class="board-header"><div class="board-header-title"><span class="collapse-icon">▼</span>🌍 محیطهای عملیاتی (${envs.length})</div><div class="board-header-actions"><button class="btn btn-accent" onclick="EnvironmentManager.openAddModal()">➕ محیط جدید</button></div></div><div class="section-content">`;
|
||
if (envs.length === 0) {
|
||
h +=
|
||
'<div class="board-empty"><div class="board-empty-icon">🌍</div><h2>هیچ محیطی نیست</h2></div>';
|
||
} else {
|
||
h += '<div class="env-card-grid">';
|
||
envs.forEach((env) => {
|
||
h += `<div class="env-card-item"><div class="env-card-preview" style="background-color:${env.backgroundColor || "#38a169"};color:${env.textColor || "#fff"}">${env.name}</div><div class="env-card-body"><div class="env-card-name">${env.name}</div><div class="env-card-desc">${env.description || ""}</div></div><div class="env-card-footer"><button class="btn btn-ghost btn-sm" onclick="EnvironmentManager.openEditModal('${env.id}')">✏️ ویرایش</button><button class="btn btn-danger btn-sm" onclick="EnvironmentManager.remove('${env.id}')">🗑️ حذف</button></div></div>`;
|
||
});
|
||
h += "</div>";
|
||
}
|
||
h += "</div></div>";
|
||
return h;
|
||
},
|
||
};
|
||
|
||
/* ==================== BOARD RENDERER ==================== */
|
||
const BoardRenderer = {
|
||
render() {
|
||
const board = document.getElementById("boardArea");
|
||
const products = projectData.appData.products || [];
|
||
const fp = products.filter((p) =>
|
||
ToolbarManager.shouldShowProduct(p),
|
||
);
|
||
let h = "";
|
||
h += DashboardSectionRenderer.getSectionHTML();
|
||
h += `<div class="board-section" id="section-products"><div class="board-header"><div class="board-header-title"><span class="collapse-icon">▼</span>📦 محصولات (${fp.length} از ${products.length})</div><div class="board-header-actions"><button class="btn btn-accent" onclick="ProductManager.openAddProductModal()">➕ محصول جدید</button></div></div><div class="section-content">`;
|
||
if (products.length === 0) {
|
||
h +=
|
||
'<div class="board-empty"><div class="board-empty-icon">📦</div><h2>هیچ محصولی نیست</h2></div>';
|
||
} else if (fp.length === 0) {
|
||
h += '<div class="no-results">🔍 نتیجهای یافت نشد</div>';
|
||
} else {
|
||
h += '<div class="projects-grid">';
|
||
fp.forEach((prod) => {
|
||
const projects = prod.projects || [];
|
||
const tc = {};
|
||
projects.forEach((pr) => {
|
||
tc[pr.type] = (tc[pr.type] || 0) + 1;
|
||
});
|
||
const td = projects.reduce(
|
||
(s, pr) => s + (pr.dependencies || []).length,
|
||
0,
|
||
);
|
||
const tdp = projects.reduce(
|
||
(s, pr) =>
|
||
s +
|
||
(pr.deployments || []).filter((d) => d.status === "active")
|
||
.length,
|
||
0,
|
||
);
|
||
h += `<div class="project-card"><div class="project-card-header"><div class="project-card-title-row"><span style="font-size:22px">📦</span><div class="project-card-title">${prod.title}</div></div><div class="project-card-desc">${prod.description || ""}</div></div><div class="project-card-body"><div class="project-stat-row"><span>🌿 پروژهها:</span><span class="project-stat-value">${projects.length}</span></div><div class="project-stat-row"><span>🔗 وابستگیها:</span><span class="project-stat-value">${td}</span></div><div class="project-stat-row"><span>🚀 استقرارها:</span><span class="project-stat-value">${tdp}</span></div>${
|
||
Object.keys(tc).length > 0
|
||
? `<div class="project-type-badges">${Object.entries(tc)
|
||
.map(([type, count]) => {
|
||
const ti = VALID_PROJECT_TYPES.find(
|
||
(t) => t.value === type,
|
||
) || { label: type };
|
||
return `<span class="type-badge type-${type}">${ti.label}: ${count}</span>`;
|
||
})
|
||
.join("")}</div>`
|
||
: ""
|
||
}</div><div class="project-card-footer"><div class="card-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.showDetail('${prod.id}')">👁️ جزئیات</button></div><button class="btn btn-danger btn-xs" onclick="ProductManager.removeProduct('${prod.id}')">🗑️</button></div></div>`;
|
||
});
|
||
h += "</div>";
|
||
}
|
||
h += "</div></div>";
|
||
h += EnvironmentRenderer.getSectionHTML();
|
||
board.innerHTML = h;
|
||
const st = SectionCollapseManager.getState();
|
||
Object.keys(st).forEach((id) => {
|
||
const sec = document.getElementById(`section-${id}`);
|
||
if (sec && st[id]) sec.classList.add("collapsed");
|
||
});
|
||
setTimeout(() => {
|
||
DashboardSectionRenderer.renderCharts();
|
||
}, 100);
|
||
},
|
||
};
|
||
|
||
/* ==================== ACTION REGISTRY ==================== */
|
||
const ActionRegistry = {
|
||
openProductSettings() {
|
||
openSettingsModal();
|
||
},
|
||
saveProduct() {
|
||
projectData.metadata.updatedAt = new Date().toISOString();
|
||
const js = JSON.stringify(projectData.appData, null, 2);
|
||
const b = new Blob([js], { type: "application/json;charset=utf-8" });
|
||
const u = URL.createObjectURL(b);
|
||
const a = document.createElement("a");
|
||
a.href = u;
|
||
a.download = `${projectData.projectName}_v${projectData.version}_${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}.json`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(u);
|
||
showToast("💾 ذخیره شد", "success");
|
||
},
|
||
loadProduct() {
|
||
const inp = document.createElement("input");
|
||
inp.type = "file";
|
||
inp.accept = ".json";
|
||
inp.addEventListener("change", (e) => {
|
||
const f = e.target.files[0];
|
||
if (!f) return;
|
||
const r = new FileReader();
|
||
r.onload = (ev) => {
|
||
try {
|
||
const ld = JSON.parse(ev.target.result);
|
||
if (!validateAppDataStructure(ld)) {
|
||
showToast("❌ ساختار نامعتبر", "error");
|
||
return;
|
||
}
|
||
HistoryManager.pushState();
|
||
projectData.appData = ld;
|
||
AutosaveManager.clear();
|
||
AutosaveManager.save();
|
||
updateProjectUI();
|
||
renderToolGroups();
|
||
ToolbarManager.populateEnvironmentFilter();
|
||
refreshUI();
|
||
showToast("📂 بارگذاری شد", "success");
|
||
} catch (err) {
|
||
showToast("❌ خطا: " + err.message, "error");
|
||
}
|
||
};
|
||
r.readAsText(f);
|
||
});
|
||
inp.click();
|
||
},
|
||
clearAll() {
|
||
if (!confirm("⚠️ پاکسازی کامل؟ این عمل غیرقابل بازگشت است!")) return;
|
||
if (!confirm("⚠️ تأیید نهایی؟")) return;
|
||
HistoryManager.pushState();
|
||
projectData.appData = {
|
||
environments: [],
|
||
products: [],
|
||
boards: [],
|
||
boardTags: [],
|
||
};
|
||
AutosaveManager.clear();
|
||
ToolbarManager.populateEnvironmentFilter();
|
||
refreshUI();
|
||
showToast("🗑️ پاکسازی شد", "success");
|
||
},
|
||
showGalaxyChart() {
|
||
GalaxyChartManager.open();
|
||
},
|
||
showExecutiveDashboard() {
|
||
DashboardManager.open();
|
||
},
|
||
showDependencyAnalysis() {
|
||
DependencyAnalysisManager.open();
|
||
},
|
||
showDeploymentAnalysis() {
|
||
DeploymentAnalysisManager.open();
|
||
},
|
||
showVersionAnalysis() {
|
||
VersionAnalysisManager.open();
|
||
},
|
||
showRiskAnalysis() {
|
||
RiskAnalysisManager.open();
|
||
},
|
||
showReleaseBoards() {
|
||
ReleaseBoardsManager.open();
|
||
},
|
||
// ✅ NEW: مدیریت تگها از منوی ابزارها
|
||
showBoardTagsManager() {
|
||
if (!projectData.appData.boardTags) {
|
||
projectData.appData.boardTags = [];
|
||
}
|
||
ReleaseBoardsManager.openTagManager();
|
||
},
|
||
defaultAction(tool) {
|
||
showToast(`ابزار: ${tool.title}`, "info");
|
||
},
|
||
};
|
||
|
||
/* ==================== WEBHOOK MANAGER ==================== */
|
||
const WebhookManager = {
|
||
getConfig() {
|
||
return projectData.webhook;
|
||
},
|
||
setConfig(c) {
|
||
projectData.webhook = { ...projectData.webhook, ...c };
|
||
this.updateStatusUI();
|
||
},
|
||
isValid() {
|
||
const c = this.getConfig();
|
||
return (
|
||
c.enabled && c.url && c.clientId && /^https?:\/\//i.test(c.url)
|
||
);
|
||
},
|
||
updateStatusUI() {
|
||
const c = this.getConfig();
|
||
const d = document.getElementById("webhookStatusDot"),
|
||
t = document.getElementById("webhookStatusText");
|
||
if (!d || !t) return;
|
||
d.className = "webhook-status-dot";
|
||
if (!c.enabled) t.textContent = "وضعیت: غیرفعال";
|
||
else if (!c.url || !c.clientId) {
|
||
d.classList.add("pending");
|
||
t.textContent = "وضعیت: ناقص";
|
||
} else {
|
||
d.classList.add("success");
|
||
t.textContent = "وضعیت: آماده";
|
||
}
|
||
},
|
||
async sendEvent(en, payload = {}) {
|
||
if (!this.isValid()) return;
|
||
const c = this.getConfig();
|
||
try {
|
||
await fetch(c.url, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
"X-Client-Id": c.clientId,
|
||
"X-Event-Name": en,
|
||
},
|
||
body: JSON.stringify({
|
||
event: en,
|
||
timestamp: new Date().toISOString(),
|
||
projectId: projectData.projectId,
|
||
data: payload,
|
||
}),
|
||
});
|
||
} catch (e) {}
|
||
},
|
||
};
|
||
|
||
/* ==================== ENVIRONMENT MANAGER ==================== */
|
||
const EnvironmentManager = {
|
||
currentEditEnvId: null,
|
||
openAddModal() {
|
||
document.getElementById("newEnvName").value = "";
|
||
document.getElementById("newEnvDesc").value = "";
|
||
document.getElementById("newEnvBgColor").value = "#38a169";
|
||
document.getElementById("newEnvTextColor").value = "#ffffff";
|
||
ModalManager.open("envManagerModal");
|
||
},
|
||
saveNewEnv() {
|
||
const n = document.getElementById("newEnvName").value.trim();
|
||
if (!n) {
|
||
showToast("نام الزامی است", "error");
|
||
return;
|
||
}
|
||
if (
|
||
projectData.appData.environments.some(
|
||
(e) => e.name.toLowerCase() === n.toLowerCase(),
|
||
)
|
||
) {
|
||
showToast("تکراری است", "warning");
|
||
return;
|
||
}
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
projectData.appData.environments.push({
|
||
id: `env-${Date.now()}`,
|
||
name: n,
|
||
description:
|
||
document.getElementById("newEnvDesc").value.trim() || "",
|
||
backgroundColor: document.getElementById("newEnvBgColor").value,
|
||
textColor: document.getElementById("newEnvTextColor").value,
|
||
});
|
||
ModalManager.close("envManagerModal");
|
||
ToolbarManager.populateEnvironmentFilter();
|
||
refreshUI();
|
||
showToast(`محیط "${n}" اضافه شد`, "success");
|
||
},
|
||
openEditModal(envId) {
|
||
const env = projectData.appData.environments.find(
|
||
(e) => e.id === envId,
|
||
);
|
||
if (!env) return;
|
||
this.currentEditEnvId = envId;
|
||
document.getElementById("editEnvId").value = env.id;
|
||
document.getElementById("editEnvName").value = env.name;
|
||
document.getElementById("editEnvDesc").value = env.description || "";
|
||
document.getElementById("editEnvBgColor").value =
|
||
env.backgroundColor || "#38a169";
|
||
document.getElementById("editEnvTextColor").value =
|
||
env.textColor || "#ffffff";
|
||
this.updateEditPreview();
|
||
ModalManager.open("envEditModal");
|
||
},
|
||
updateEditPreview() {
|
||
const p = document.getElementById("editEnvPreview");
|
||
p.style.backgroundColor =
|
||
document.getElementById("editEnvBgColor").value;
|
||
p.style.color = document.getElementById("editEnvTextColor").value;
|
||
},
|
||
saveEdit() {
|
||
const env = projectData.appData.environments.find(
|
||
(e) => e.id === this.currentEditEnvId,
|
||
);
|
||
if (!env) return;
|
||
const nn = document.getElementById("editEnvName").value.trim();
|
||
if (!nn) {
|
||
showToast("نام الزامی است", "error");
|
||
return;
|
||
}
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
env.name = nn;
|
||
env.description = document.getElementById("editEnvDesc").value.trim();
|
||
env.backgroundColor = document.getElementById("editEnvBgColor").value;
|
||
env.textColor = document.getElementById("editEnvTextColor").value;
|
||
ModalManager.close("envEditModal");
|
||
ToolbarManager.populateEnvironmentFilter();
|
||
refreshUI();
|
||
showToast(`محیط "${env.name}" ویرایش شد`, "success");
|
||
},
|
||
remove(id) {
|
||
const env = projectData.appData.environments.find((e) => e.id === id);
|
||
if (!env) return;
|
||
if (!confirm(`حذف "${env.name}"؟`)) return;
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
projectData.appData.environments =
|
||
projectData.appData.environments.filter((e) => e.id !== id);
|
||
ToolbarManager.populateEnvironmentFilter();
|
||
refreshUI();
|
||
showToast(`"${env.name}" حذف شد`, "warning");
|
||
},
|
||
};
|
||
|
||
/* ==================== PRODUCT MANAGER ==================== */
|
||
const ProductManager = {
|
||
currentDetailProductId: null,
|
||
currentEditProjectId: null,
|
||
currentDependencyProjectId: null,
|
||
currentDependencyProjId: null,
|
||
currentDeploymentProjId: null,
|
||
currentDeploymentMode: null,
|
||
currentDeploymentEnvId: null,
|
||
currentEditDeploymentId: null,
|
||
getTypeInfo(type) {
|
||
return (
|
||
VALID_PROJECT_TYPES.find((t) => t.value === type) || {
|
||
value: type,
|
||
label: type,
|
||
icon: "❓",
|
||
}
|
||
);
|
||
},
|
||
getEnvironmentName(envId) {
|
||
const e = projectData.appData.environments.find(
|
||
(x) => x.id === envId,
|
||
);
|
||
return e ? e.name : envId;
|
||
},
|
||
getEnvironmentColors(envId) {
|
||
const e = projectData.appData.environments.find(
|
||
(x) => x.id === envId,
|
||
);
|
||
return {
|
||
backgroundColor: e?.backgroundColor || "#38a169",
|
||
textColor: e?.textColor || "#ffffff",
|
||
};
|
||
},
|
||
findProjectById(pid) {
|
||
for (const prod of projectData.appData.products) {
|
||
const pr = prod.projects.find((p) => p.id === pid);
|
||
if (pr) return pr;
|
||
}
|
||
return null;
|
||
},
|
||
getActiveDeployments(proj) {
|
||
return (proj.deployments || []).filter((d) => d.status === "active");
|
||
},
|
||
getEnvironmentsWithoutDeployment(proj) {
|
||
const allEnvIds = projectData.appData.environments.map((e) => e.id);
|
||
const deployedEnvIds = this.getActiveDeployments(proj).map(
|
||
(d) => d.environmentId,
|
||
);
|
||
return allEnvIds.filter((id) => !deployedEnvIds.includes(id));
|
||
},
|
||
showDetail(productId) {
|
||
const prod = projectData.appData.products.find(
|
||
(p) => p.id === productId,
|
||
);
|
||
if (!prod) return;
|
||
this.currentDetailProductId = productId;
|
||
document.getElementById("projectDetailTitle").innerHTML =
|
||
`<span>📦</span><span>${prod.title}</span>`;
|
||
const projects = prod.projects || [];
|
||
const totalDeploys = projects.reduce(
|
||
(s, pr) => s + this.getActiveDeployments(pr).length,
|
||
0,
|
||
);
|
||
let html = `<div class="detail-section"><div class="detail-section-header"><span>ℹ️ اطلاعات محصول</span><div class="detail-section-header-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.openProductEditModal()">✏️ ویرایش</button></div></div><div class="detail-section-body"><div class="detail-info-grid"><div class="detail-info-item"><span class="detail-info-label">عنوان</span><span class="detail-info-value">${prod.title}</span></div><div class="detail-info-item"><span class="detail-info-label">توضیحات</span><span class="detail-info-value">${prod.description || "—"}</span></div><div class="detail-info-item"><span class="detail-info-label">تاریخ ایجاد</span><span class="detail-info-value">${prod.metadata?.createdAt ? new Date(prod.metadata.createdAt).toLocaleDateString("fa-IR") : "—"}</span></div><div class="detail-info-item"><span class="detail-info-label">مالک</span><span class="detail-info-value">${prod.metadata?.owner || "—"}</span></div><div class="detail-info-item"><span class="detail-info-label">تعداد پروژه</span><span class="detail-info-value">${projects.length}</span></div><div class="detail-info-item"><span class="detail-info-label">استقرارهای فعال</span><span class="detail-info-value">${totalDeploys}</span></div></div></div></div><div class="detail-section"><div class="detail-section-header"><span>🌿 پروژهها (${projects.length})</span><div class="detail-section-header-actions"><button class="btn btn-accent btn-sm" onclick="ProductManager.openAddProjectModal()">➕ افزودن</button></div></div><div class="detail-section-body">`;
|
||
if (projects.length === 0) {
|
||
html += '<div class="empty-subprojects">🌿 هیچ پروژهای نیست</div>';
|
||
} else {
|
||
html += '<div class="subproject-list">';
|
||
projects.forEach((pr) => {
|
||
const ti = this.getTypeInfo(pr.type);
|
||
const deps = pr.dependencies || [];
|
||
const deploys = this.getActiveDeployments(pr);
|
||
const emptyEnvs = this.getEnvironmentsWithoutDeployment(pr);
|
||
const canAdd = emptyEnvs.length > 0;
|
||
html += `<div class="subproject-card"><div class="subproject-header"><div class="subproject-title"><span>${ti.icon}</span><span>${pr.title}</span><span class="type-badge type-${pr.type}">${ti.label}</span></div><div class="subproject-actions"><button class="btn btn-ghost btn-sm" onclick="ProductManager.openDependencyManager('${prod.id}','${pr.id}')">🔗 وابستگیها</button><button class="btn btn-ghost btn-sm" onclick="ProductManager.openProjectEditModal('${prod.id}','${pr.id}')">✏️</button><button class="btn btn-danger btn-sm" onclick="ProductManager.removeProject('${prod.id}','${pr.id}')">🗑️</button></div></div><div class="subproject-meta">${pr.description || ""}</div><div class="detail-info-grid" style="margin-bottom:8px">${pr.metadata?.framework ? `<div class="detail-info-item"><span class="detail-info-label">فریمورک</span><span class="detail-info-value">${pr.metadata.framework}</span></div>` : ""}${pr.metadata?.language ? `<div class="detail-info-item"><span class="detail-info-label">زبان</span><span class="detail-info-value">${pr.metadata.language}</span></div>` : ""}${
|
||
deps.length > 0
|
||
? `<div class="detail-info-item"><span class="detail-info-label">وابستگیها (${deps.length})</span><span class="detail-info-value">${deps
|
||
.map((depId) => {
|
||
const dp = this.findProjectById(depId);
|
||
return dp ? dp.title : depId;
|
||
})
|
||
.join("، ")}</span></div>`
|
||
: ""
|
||
}</div>`;
|
||
html += `<div class="deployment-section"><div class="deployment-header"><span class="deployment-header-title">🚀 استقرارهای فعال (${deploys.length})</span>${canAdd ? `<button class="btn btn-accent btn-xs" onclick="ProductManager.openAddDeploymentModal('${prod.id}','${pr.id}')">➕ افزودن</button>` : '<span style="font-size:10px;color:var(--text-muted)">✓ کامل</span>'}</div>`;
|
||
if (deploys.length === 0) {
|
||
html +=
|
||
'<div class="empty-deployments">هنوز استقرار ندارد</div>';
|
||
} else {
|
||
html += '<div class="deployment-list">';
|
||
deploys.forEach((d) => {
|
||
const en = this.getEnvironmentName(d.environmentId);
|
||
const cl = this.getEnvironmentColors(d.environmentId);
|
||
const fd = d.deploymentDate
|
||
? new Date(d.deploymentDate).toLocaleString("fa-IR")
|
||
: "—";
|
||
html += `<div class="deployment-item"><div class="deployment-item-info"><span class="deployment-env-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor}">${en}</span><span class="deployment-version">v${d.version}</span><span class="deployment-date">📅 ${fd}</span></div><div class="deployment-actions"><button class="btn btn-ghost btn-xs" onclick="ProductManager.openEditDeploymentModal('${prod.id}','${pr.id}','${d.id}')">✏️</button><button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.id}')">🗑️</button></div>`;
|
||
if (d.releaseNotes) {
|
||
html += `<div class="deployment-notes">📝 ${d.releaseNotes}</div>`;
|
||
}
|
||
if (
|
||
d.requiredDeployments &&
|
||
d.requiredDeployments.length > 0
|
||
) {
|
||
html += `<div class="deployment-req">🔗 نیازمندیها: ${d.requiredDeployments
|
||
.map((rd) => {
|
||
const rp = this.findProjectById(rd.projectId);
|
||
let depVer = "";
|
||
if (rp) {
|
||
const rdDep = (rp.deployments || []).find(
|
||
(x) => x.id === rd.deploymentId,
|
||
);
|
||
if (rdDep) depVer = ` (v${rdDep.version})`;
|
||
}
|
||
return `${rp ? rp.title : rd.projectId}${depVer}`;
|
||
})
|
||
.join("، ")}</div>`;
|
||
}
|
||
html += `</div>`;
|
||
});
|
||
html += "</div>";
|
||
}
|
||
html += `</div></div>`;
|
||
});
|
||
html += "</div>";
|
||
}
|
||
html += `</div></div>`;
|
||
document.getElementById("projectDetailBody").innerHTML = html;
|
||
ModalManager.open("projectDetailModal");
|
||
},
|
||
openAddProductModal() {
|
||
document.getElementById("newProductTitle").value = "";
|
||
document.getElementById("newProductDescription").value = "";
|
||
document.getElementById("newProductOwner").value = "";
|
||
document.getElementById("newProductCreatedAt").value =
|
||
new Date().toLocaleDateString("fa-IR");
|
||
ModalManager.open("addProductModal");
|
||
},
|
||
createNewProduct() {
|
||
const title = document.getElementById("newProductTitle").value.trim();
|
||
if (!title || title.length < 3) {
|
||
showToast("عنوان الزامی است (حداقل ۳ کاراکتر)", "error");
|
||
return;
|
||
}
|
||
if (
|
||
projectData.appData.products.some(
|
||
(p) => p.title.toLowerCase() === title.toLowerCase(),
|
||
)
|
||
) {
|
||
showToast("تکراری است", "warning");
|
||
return;
|
||
}
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
projectData.appData.products.push({
|
||
id: `prod-${Date.now()}`,
|
||
title,
|
||
description:
|
||
document.getElementById("newProductDescription").value.trim() ||
|
||
"",
|
||
metadata: {
|
||
createdAt: new Date().toISOString(),
|
||
owner:
|
||
document.getElementById("newProductOwner").value.trim() || "",
|
||
customFields: {},
|
||
},
|
||
projects: [],
|
||
});
|
||
ModalManager.close("addProductModal");
|
||
refreshUI();
|
||
showToast(`"${title}" ایجاد شد`, "success");
|
||
},
|
||
openDependencyManager(productId, projId) {
|
||
this.currentDependencyProjectId = productId;
|
||
this.currentDependencyProjId = projId;
|
||
this.renderCurrentDependencies();
|
||
this.renderAllProjectsList();
|
||
this.setupDependencySearch();
|
||
ModalManager.open("dependencyManagerModal");
|
||
},
|
||
renderCurrentDependencies() {
|
||
const prod = projectData.appData.products.find(
|
||
(p) => p.id === this.currentDependencyProjectId,
|
||
);
|
||
if (!prod) return;
|
||
const pr = prod.projects.find(
|
||
(p) => p.id === this.currentDependencyProjId,
|
||
);
|
||
if (!pr) return;
|
||
const c = document.getElementById("currentDependenciesList");
|
||
const deps = pr.dependencies || [];
|
||
if (deps.length === 0) {
|
||
c.innerHTML =
|
||
'<div class="empty-dependencies">هیچ وابستگیای نیست</div>';
|
||
return;
|
||
}
|
||
c.innerHTML = deps
|
||
.map((depId, index) => {
|
||
const depPr = this.findProjectById(depId);
|
||
const projectName = depPr ? depPr.title : depId;
|
||
const typeInfo = depPr ? this.getTypeInfo(depPr.type) : null;
|
||
return `<div class="dependency-item"><div class="dependency-info"><div><div class="dependency-project-name">${typeInfo ? typeInfo.icon + " " : ""}${projectName}</div></div></div><button class="btn btn-danger btn-xs" onclick="ProductManager.removeDependency(${index})">🗑️</button></div>`;
|
||
})
|
||
.join("");
|
||
},
|
||
removeDependency(index) {
|
||
const prod = projectData.appData.products.find(
|
||
(p) => p.id === this.currentDependencyProjectId,
|
||
);
|
||
if (!prod) return;
|
||
const pr = prod.projects.find(
|
||
(p) => p.id === this.currentDependencyProjId,
|
||
);
|
||
if (!pr) return;
|
||
if (!confirm("حذف این وابستگی؟")) return;
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
pr.dependencies.splice(index, 1);
|
||
ModalManager.close("dependencyManagerModal");
|
||
refreshUI();
|
||
if (this.currentDetailProductId)
|
||
this.showDetail(this.currentDetailProductId);
|
||
showToast("وابستگی حذف شد", "success");
|
||
},
|
||
renderAllProjectsList() {
|
||
const rc = document.getElementById("dependencySearchResults");
|
||
const cp = projectData.appData.products.find(
|
||
(p) => p.id === this.currentDependencyProjectId,
|
||
);
|
||
if (!cp) return;
|
||
const cpr = cp.projects.find(
|
||
(p) => p.id === this.currentDependencyProjId,
|
||
);
|
||
if (!cpr) return;
|
||
const allProjects = [];
|
||
projectData.appData.products.forEach((prod) => {
|
||
prod.projects.forEach((pr) => {
|
||
if (pr.id !== cpr.id)
|
||
allProjects.push({ ...pr, productName: prod.title });
|
||
});
|
||
});
|
||
if (allProjects.length === 0) {
|
||
rc.innerHTML =
|
||
'<div class="empty-dependencies">پروژه دیگری نیست</div>';
|
||
return;
|
||
}
|
||
rc.innerHTML = allProjects
|
||
.map((pr) => {
|
||
const isDep = (cpr.dependencies || []).includes(pr.id);
|
||
return `<div class="dependency-project-result"><button class="dependency-project-add-btn" onclick="ProductManager.addDependency('${pr.id}')" ${isDep ? 'disabled style="opacity:.5"' : ""}><span>${this.getTypeInfo(pr.type).icon}</span><span>${pr.title}</span><small style="color:var(--text-muted);font-weight:normal">(${pr.productName})</small>${isDep ? '<span style="margin-right:auto;font-size:10px;color:var(--success)">✓ وابسته</span>' : ""}</button></div>`;
|
||
})
|
||
.join("");
|
||
},
|
||
setupDependencySearch() {
|
||
const si = document.getElementById("dependencySearch");
|
||
si.value = "";
|
||
si.oninput = (e) => {
|
||
const q = e.target.value.trim().toLowerCase();
|
||
if (q.length === 0) {
|
||
this.renderAllProjectsList();
|
||
return;
|
||
}
|
||
const cp = projectData.appData.products.find(
|
||
(p) => p.id === this.currentDependencyProjectId,
|
||
);
|
||
if (!cp) return;
|
||
const cpr = cp.projects.find(
|
||
(p) => p.id === this.currentDependencyProjId,
|
||
);
|
||
if (!cpr) return;
|
||
const allProjects = [];
|
||
projectData.appData.products.forEach((prod) => {
|
||
prod.projects.forEach((pr) => {
|
||
if (pr.id !== cpr.id)
|
||
allProjects.push({ ...pr, productName: prod.title });
|
||
});
|
||
});
|
||
const fl = allProjects.filter(
|
||
(pr) =>
|
||
pr.title.toLowerCase().includes(q) ||
|
||
pr.productName.toLowerCase().includes(q) ||
|
||
(pr.description || "").toLowerCase().includes(q),
|
||
);
|
||
const rc = document.getElementById("dependencySearchResults");
|
||
if (fl.length === 0) {
|
||
rc.innerHTML =
|
||
'<div class="empty-dependencies">نتیجهای نیست</div>';
|
||
return;
|
||
}
|
||
rc.innerHTML = fl
|
||
.map((pr) => {
|
||
const isDep = (cpr.dependencies || []).includes(pr.id);
|
||
return `<div class="dependency-project-result"><button class="dependency-project-add-btn" onclick="ProductManager.addDependency('${pr.id}')" ${isDep ? 'disabled style="opacity:.5"' : ""}><span>${this.getTypeInfo(pr.type).icon}</span><span>${pr.title}</span><small style="color:var(--text-muted)">(${pr.productName})</small></button></div>`;
|
||
})
|
||
.join("");
|
||
};
|
||
},
|
||
addDependency(projectId) {
|
||
const prod = projectData.appData.products.find(
|
||
(p) => p.id === this.currentDependencyProjectId,
|
||
);
|
||
if (!prod) return;
|
||
const pr = prod.projects.find(
|
||
(p) => p.id === this.currentDependencyProjId,
|
||
);
|
||
if (!pr) return;
|
||
if ((pr.dependencies || []).includes(projectId)) {
|
||
showToast("تکراری است", "warning");
|
||
return;
|
||
}
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
if (!pr.dependencies) pr.dependencies = [];
|
||
pr.dependencies.push(projectId);
|
||
ModalManager.close("dependencyManagerModal");
|
||
refreshUI();
|
||
if (this.currentDetailProductId)
|
||
this.showDetail(this.currentDetailProductId);
|
||
showToast("وابستگی اضافه شد", "success");
|
||
},
|
||
openProductEditModal() {
|
||
const pid = this.currentDetailProductId;
|
||
if (!pid) return;
|
||
const prod = projectData.appData.products.find((p) => p.id === pid);
|
||
if (!prod) return;
|
||
document.getElementById("editProductId").value = prod.id;
|
||
document.getElementById("editProductTitle").value = prod.title;
|
||
document.getElementById("editProductDescription").value =
|
||
prod.description || "";
|
||
document.getElementById("editProductOwner").value =
|
||
prod.metadata?.owner || "";
|
||
document.getElementById("editProductCreatedAt").value = prod.metadata
|
||
?.createdAt
|
||
? new Date(prod.metadata.createdAt).toLocaleDateString("fa-IR")
|
||
: "—";
|
||
ModalManager.open("productEditModal");
|
||
},
|
||
saveProductEdit() {
|
||
const pid = this.currentDetailProductId;
|
||
if (!pid) return;
|
||
const prod = projectData.appData.products.find((p) => p.id === pid);
|
||
if (!prod) return;
|
||
const nt = document.getElementById("editProductTitle").value.trim();
|
||
if (!nt) {
|
||
showToast("عنوان الزامی است", "error");
|
||
return;
|
||
}
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
prod.title = nt;
|
||
prod.description = document
|
||
.getElementById("editProductDescription")
|
||
.value.trim();
|
||
if (!prod.metadata) prod.metadata = {};
|
||
prod.metadata.owner = document
|
||
.getElementById("editProductOwner")
|
||
.value.trim();
|
||
prod.metadata.updatedAt = new Date().toISOString();
|
||
ModalManager.close("productEditModal");
|
||
refreshUI();
|
||
this.showDetail(pid);
|
||
showToast(`"${prod.title}" ویرایش شد`, "success");
|
||
},
|
||
openAddProjectModal() {
|
||
if (!this.currentDetailProductId) {
|
||
showToast("ابتدا محصول انتخاب کنید", "warning");
|
||
return;
|
||
}
|
||
document.getElementById("newProjectTitle").value = "";
|
||
document.getElementById("newProjectType").value = "";
|
||
document.getElementById("newProjectFramework").value = "";
|
||
document.getElementById("newProjectLanguage").value = "";
|
||
document.getElementById("newProjectRepo").value = "";
|
||
document.getElementById("newProjectDescription").value = "";
|
||
populateProjectTypeDropdowns();
|
||
ModalManager.open("addProjectModal");
|
||
},
|
||
saveNewProject() {
|
||
const pid = this.currentDetailProductId;
|
||
if (!pid) return;
|
||
const prod = projectData.appData.products.find((p) => p.id === pid);
|
||
if (!prod) return;
|
||
const title = document.getElementById("newProjectTitle").value.trim();
|
||
const type = document.getElementById("newProjectType").value;
|
||
if (!title || !type) {
|
||
showToast("عنوان و نوع الزامی است", "error");
|
||
return;
|
||
}
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
const np = {
|
||
id: `proj-${Date.now()}`,
|
||
title,
|
||
description:
|
||
document.getElementById("newProjectDescription").value.trim() ||
|
||
"",
|
||
type,
|
||
repositoryUrl: document
|
||
.getElementById("newProjectRepo")
|
||
.value.trim(),
|
||
metadata: { createdAt: new Date().toISOString(), customFields: {} },
|
||
dependencies: [],
|
||
deployments: [],
|
||
};
|
||
const fw = document
|
||
.getElementById("newProjectFramework")
|
||
.value.trim();
|
||
if (fw) np.metadata.framework = fw;
|
||
const lg = document.getElementById("newProjectLanguage").value.trim();
|
||
if (lg) np.metadata.language = lg;
|
||
prod.projects.push(np);
|
||
ModalManager.close("addProjectModal");
|
||
refreshUI();
|
||
this.showDetail(pid);
|
||
showToast(`"${title}" ایجاد شد`, "success");
|
||
},
|
||
openProjectEditModal(productId, projId) {
|
||
const prod = projectData.appData.products.find(
|
||
(p) => p.id === productId,
|
||
);
|
||
if (!prod) return;
|
||
const pr = prod.projects.find((p) => p.id === projId);
|
||
if (!pr) return;
|
||
this.currentEditProjectId = projId;
|
||
populateProjectTypeDropdowns();
|
||
document.getElementById("editProjId").value = pr.id;
|
||
document.getElementById("editProjTitle").value = pr.title;
|
||
document.getElementById("editProjType").value = pr.type;
|
||
document.getElementById("editProjFramework").value =
|
||
pr.metadata?.framework || "";
|
||
document.getElementById("editProjLanguage").value =
|
||
pr.metadata?.language || "";
|
||
document.getElementById("editProjRepo").value =
|
||
pr.repositoryUrl || "";
|
||
document.getElementById("editProjDescription").value =
|
||
pr.description || "";
|
||
ModalManager.open("editProjectModal");
|
||
},
|
||
saveProjectEdit() {
|
||
const pid = this.currentDetailProductId;
|
||
const projId = this.currentEditProjectId;
|
||
if (!pid || !projId) return;
|
||
const prod = projectData.appData.products.find((p) => p.id === pid);
|
||
if (!prod) return;
|
||
const pr = prod.projects.find((p) => p.id === projId);
|
||
if (!pr) return;
|
||
const nt = document.getElementById("editProjTitle").value.trim();
|
||
const nty = document.getElementById("editProjType").value;
|
||
if (!nt || !nty) {
|
||
showToast("عنوان و نوع الزامی است", "error");
|
||
return;
|
||
}
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
pr.title = nt;
|
||
pr.type = nty;
|
||
pr.description =
|
||
document.getElementById("editProjDescription").value.trim() || "";
|
||
pr.repositoryUrl = document
|
||
.getElementById("editProjRepo")
|
||
.value.trim();
|
||
if (!pr.metadata) pr.metadata = {};
|
||
const fw = document.getElementById("editProjFramework").value.trim();
|
||
if (fw) pr.metadata.framework = fw;
|
||
else delete pr.metadata.framework;
|
||
const lg = document.getElementById("editProjLanguage").value.trim();
|
||
if (lg) pr.metadata.language = lg;
|
||
else delete pr.metadata.language;
|
||
pr.metadata.updatedAt = new Date().toISOString();
|
||
ModalManager.close("editProjectModal");
|
||
refreshUI();
|
||
this.showDetail(pid);
|
||
showToast(`"${pr.title}" ویرایش شد`, "success");
|
||
},
|
||
openAddDeploymentModal(productId, projId) {
|
||
if (!productId || !projId) return;
|
||
const prod = projectData.appData.products.find(
|
||
(p) => p.id === productId,
|
||
);
|
||
if (!prod) return;
|
||
const pr = prod.projects.find((p) => p.id === projId);
|
||
if (!pr) return;
|
||
const emptyEnvs = this.getEnvironmentsWithoutDeployment(pr);
|
||
if (emptyEnvs.length === 0) {
|
||
showToast("همه محیطها استقرار دارند", "warning");
|
||
return;
|
||
}
|
||
this.currentDeploymentProjId = projId;
|
||
this.currentDeploymentMode = "add";
|
||
this.currentDeploymentEnvId = null;
|
||
this.currentEditDeploymentId = null;
|
||
document.getElementById("deploymentModalTitle").innerHTML =
|
||
"<span>🚀</span><span>افزودن نسخه استقرار</span>";
|
||
const envGroup = document.getElementById("deploymentEnvGroup");
|
||
const envs = projectData.appData.environments.filter((e) =>
|
||
emptyEnvs.includes(e.id),
|
||
);
|
||
envGroup.innerHTML = `<label class="form-label">محیط استقرار <span class="required">*</span></label><select class="form-select" id="depEnvironment" onchange="ProductManager.onEnvChange()">${envs
|
||
.map((e) => {
|
||
const cl = this.getEnvironmentColors(e.id);
|
||
return `<option value="${e.id}" style="background-color:${cl.backgroundColor};color:${cl.textColor}">${e.name} - ${e.description}</option>`;
|
||
})
|
||
.join("")}</select>`;
|
||
document.getElementById("depVersion").value = "";
|
||
document.getElementById("depDate").value = "";
|
||
document.getElementById("depReleaseNotes").value = "";
|
||
this.renderRequiredDeploymentsSection();
|
||
ModalManager.open("deploymentModal");
|
||
},
|
||
openEditDeploymentModal(productId, projId, depId) {
|
||
if (!productId || !projId || !depId) return;
|
||
const prod = projectData.appData.products.find(
|
||
(p) => p.id === productId,
|
||
);
|
||
if (!prod) return;
|
||
const pr = prod.projects.find((p) => p.id === projId);
|
||
if (!pr) return;
|
||
const dep = (pr.deployments || []).find((d) => d.id === depId);
|
||
if (!dep) return;
|
||
this.currentDeploymentProjId = projId;
|
||
this.currentDeploymentMode = "edit";
|
||
this.currentDeploymentEnvId = dep.environmentId;
|
||
this.currentEditDeploymentId = depId;
|
||
document.getElementById("deploymentModalTitle").innerHTML =
|
||
"<span>✏️</span><span>ویرایش نسخه استقرار</span>";
|
||
const envGroup = document.getElementById("deploymentEnvGroup");
|
||
const cl = this.getEnvironmentColors(dep.environmentId);
|
||
envGroup.innerHTML = `<label class="form-label">محیط استقرار <span class="readonly-badge">فقط خواندنی</span></label><div class="env-read-only-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor}">${this.getEnvironmentName(dep.environmentId)}</div>`;
|
||
document.getElementById("depVersion").value = dep.version;
|
||
let dv = "";
|
||
if (dep.deploymentDate) {
|
||
const d = new Date(dep.deploymentDate);
|
||
const pad = (n) => n.toString().padStart(2, "0");
|
||
dv = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||
}
|
||
document.getElementById("depDate").value = dv;
|
||
document.getElementById("depReleaseNotes").value =
|
||
dep.releaseNotes || "";
|
||
this.renderRequiredDeploymentsSection(dep);
|
||
ModalManager.open("deploymentModal");
|
||
},
|
||
onEnvChange() {
|
||
this.renderRequiredDeploymentsSection();
|
||
},
|
||
renderRequiredDeploymentsSection(existingDep) {
|
||
const section = document.getElementById("requiredDeploymentsSection");
|
||
const envSelect = document.getElementById("depEnvironment");
|
||
const envId =
|
||
this.currentDeploymentMode === "edit"
|
||
? this.currentDeploymentEnvId
|
||
: envSelect
|
||
? envSelect.value
|
||
: null;
|
||
if (!envId) {
|
||
section.innerHTML = "";
|
||
return;
|
||
}
|
||
const prod = projectData.appData.products.find(
|
||
(p) => p.id === this.currentDetailProductId,
|
||
);
|
||
if (!prod) return;
|
||
const pr = prod.projects.find(
|
||
(p) => p.id === this.currentDeploymentProjId,
|
||
);
|
||
if (!pr) return;
|
||
const deps = pr.dependencies || [];
|
||
if (deps.length === 0) {
|
||
section.innerHTML = "";
|
||
return;
|
||
}
|
||
let h =
|
||
'<div class="form-group"><label class="form-label">🔗 استقرارهای مورد نیاز (به ازای هر وابستگی) <span class="required">*</span></label><div class="form-hint">برای هر وابستگی، یک استقرار از آن پروژه در محیط "' +
|
||
this.getEnvironmentName(envId) +
|
||
'" انتخاب کنید</div>';
|
||
deps.forEach((depId) => {
|
||
const depPr = this.findProjectById(depId);
|
||
if (!depPr) return;
|
||
const depDeployments = (depPr.deployments || []).filter(
|
||
(d) => d.status === "active" && d.environmentId === envId,
|
||
);
|
||
const existingReq = existingDep
|
||
? (existingDep.requiredDeployments || []).find(
|
||
(r) => r.projectId === depId,
|
||
)
|
||
: null;
|
||
h += `<div class="req-deployment-box"><div class="req-deployment-title">📦 ${depPr.title}</div>`;
|
||
if (depDeployments.length === 0) {
|
||
h += `<div style="color:var(--danger);font-size:11px">⚠️ هیچ استقرار فعالی در ${this.getEnvironmentName(envId)} ندارد.</div>`;
|
||
} else {
|
||
h += `<select class="req-deployment-select" data-dep-project="${depId}">${depDeployments.map((d) => `<option value="${d.id}" ${existingReq && existingReq.deploymentId === d.id ? "selected" : ""}>v${d.version} - ${d.deploymentDate ? new Date(d.deploymentDate).toLocaleDateString("fa-IR") : ""}</option>`).join("")}</select>`;
|
||
}
|
||
h += "</div>";
|
||
});
|
||
h += "</div>";
|
||
section.innerHTML = h;
|
||
},
|
||
saveDeployment() {
|
||
const pid = this.currentDetailProductId;
|
||
const projId = this.currentDeploymentProjId;
|
||
if (!pid || !projId) {
|
||
showToast("خطا: محصول یا پروژه مشخص نشده", "error");
|
||
return;
|
||
}
|
||
const prod = projectData.appData.products.find((p) => p.id === pid);
|
||
if (!prod) return;
|
||
const pr = prod.projects.find((p) => p.id === projId);
|
||
if (!pr) return;
|
||
const version = document.getElementById("depVersion").value.trim();
|
||
const date = document.getElementById("depDate").value;
|
||
const releaseNotes = document
|
||
.getElementById("depReleaseNotes")
|
||
.value.trim();
|
||
if (!version) {
|
||
showToast("نسخه الزامی است", "error");
|
||
return;
|
||
}
|
||
if (!date) {
|
||
showToast("تاریخ استقرار الزامی است", "error");
|
||
return;
|
||
}
|
||
if (!releaseNotes) {
|
||
showToast("ریلیز نوت الزامی است", "error");
|
||
return;
|
||
}
|
||
const deploymentDate = new Date(date).toISOString();
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
if (!pr.deployments) pr.deployments = [];
|
||
const requiredDeployments = [];
|
||
document.querySelectorAll(".req-deployment-select").forEach((sel) => {
|
||
const depProjectId = sel.dataset.depProject;
|
||
const depDeploymentId = sel.value;
|
||
if (depProjectId && depDeploymentId) {
|
||
requiredDeployments.push({
|
||
projectId: depProjectId,
|
||
deploymentId: depDeploymentId,
|
||
});
|
||
}
|
||
});
|
||
if (this.currentDeploymentMode === "add") {
|
||
const envId = document.getElementById("depEnvironment").value;
|
||
if (!envId) {
|
||
showToast("محیط را انتخاب کنید", "error");
|
||
return;
|
||
}
|
||
pr.deployments.push({
|
||
id: `dep-${Date.now()}`,
|
||
environmentId: envId,
|
||
version,
|
||
deploymentDate,
|
||
releaseNotes,
|
||
requiredDeployments,
|
||
status: "active",
|
||
});
|
||
showToast(
|
||
`v${version} در ${this.getEnvironmentName(envId)} استقرار یافت`,
|
||
"success",
|
||
);
|
||
} else {
|
||
const depId = this.currentEditDeploymentId;
|
||
const dep = pr.deployments.find((d) => d.id === depId);
|
||
if (dep) {
|
||
dep.version = version;
|
||
dep.deploymentDate = deploymentDate;
|
||
dep.releaseNotes = releaseNotes;
|
||
dep.requiredDeployments = requiredDeployments;
|
||
showToast(`استقرار ویرایش شد`, "success");
|
||
}
|
||
}
|
||
ModalManager.close("deploymentModal");
|
||
refreshUI();
|
||
this.showDetail(pid);
|
||
},
|
||
removeDeployment(productId, projId, depId) {
|
||
if (!confirm("حذف این استقرار؟")) return;
|
||
const prod = projectData.appData.products.find(
|
||
(p) => p.id === productId,
|
||
);
|
||
if (!prod) return;
|
||
const pr = prod.projects.find((p) => p.id === projId);
|
||
if (!pr) return;
|
||
const dep = (pr.deployments || []).find((d) => d.id === depId);
|
||
if (!dep) return;
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
dep.status = "removed";
|
||
refreshUI();
|
||
this.showDetail(productId);
|
||
showToast("استقرار حذف شد", "warning");
|
||
},
|
||
removeProduct(productId) {
|
||
const prod = projectData.appData.products.find(
|
||
(p) => p.id === productId,
|
||
);
|
||
if (!prod) return;
|
||
if (!confirm(`حذف "${prod.title}"؟`)) return;
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
projectData.appData.products = projectData.appData.products.filter(
|
||
(p) => p.id !== productId,
|
||
);
|
||
refreshUI();
|
||
ModalManager.close("projectDetailModal");
|
||
showToast(`"${prod.title}" حذف شد`, "warning");
|
||
},
|
||
removeProject(productId, projId) {
|
||
const prod = projectData.appData.products.find(
|
||
(p) => p.id === productId,
|
||
);
|
||
if (!prod) return;
|
||
const pr = prod.projects.find((p) => p.id === projId);
|
||
if (!pr) return;
|
||
if (!confirm(`حذف "${pr.title}"؟`)) return;
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
prod.projects = prod.projects.filter((p) => p.id !== projId);
|
||
refreshUI();
|
||
if (this.currentDetailProductId === productId)
|
||
this.showDetail(productId);
|
||
showToast(`"${pr.title}" حذف شد`, "warning");
|
||
},
|
||
};
|
||
|
||
/* ==================== DASHBOARD MANAGER ==================== */
|
||
const DashboardManager = {
|
||
open() {
|
||
ModalManager.open("executiveDashboardModal");
|
||
setTimeout(() => this.render(), 150);
|
||
},
|
||
getReportData() {
|
||
const ps = projectData.appData.products || [];
|
||
let tp = 0,
|
||
td = 0,
|
||
tdp = 0;
|
||
const tc = {},
|
||
edc = {},
|
||
ppc = [],
|
||
fc = {};
|
||
let pwd = 0,
|
||
pwod = 0;
|
||
ps.forEach((prod) => {
|
||
const projects = prod.projects || [];
|
||
ppc.push({ title: prod.title, count: projects.length });
|
||
projects.forEach((pr) => {
|
||
tp++;
|
||
td += (pr.dependencies || []).length;
|
||
const active = (pr.deployments || []).filter(
|
||
(d) => d.status === "active",
|
||
);
|
||
tdp += active.length;
|
||
if (active.length > 0) pwd++;
|
||
else pwod++;
|
||
tc[pr.type] = (tc[pr.type] || 0) + 1;
|
||
const fw = pr.metadata?.framework || "نامشخص";
|
||
fc[fw] = (fc[fw] || 0) + 1;
|
||
active.forEach((d) => {
|
||
const env = projectData.appData.environments.find(
|
||
(e) => e.id === d.environmentId,
|
||
);
|
||
const envName = env ? env.name : "نامشخص";
|
||
edc[envName] = (edc[envName] || 0) + 1;
|
||
});
|
||
});
|
||
});
|
||
return {
|
||
products: ps.length,
|
||
projects: tp,
|
||
deps: td,
|
||
deploys: tdp,
|
||
environments: projectData.appData.environments.length,
|
||
tc,
|
||
edc,
|
||
ppc,
|
||
fc,
|
||
pwd,
|
||
pwod,
|
||
};
|
||
},
|
||
render() {
|
||
const c = document.getElementById("dashboardContainer");
|
||
if (!c) return;
|
||
const d = this.getReportData();
|
||
let h = '<div class="dashboard-kpi-row">';
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">📦</div><div class="dashboard-kpi-value">${d.products}</div><div class="dashboard-kpi-label">محصول</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🌿</div><div class="dashboard-kpi-value">${d.projects}</div><div class="dashboard-kpi-label">پروژه</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🔗</div><div class="dashboard-kpi-value">${d.deps}</div><div class="dashboard-kpi-label">وابستگی</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🚀</div><div class="dashboard-kpi-value">${d.deploys}</div><div class="dashboard-kpi-label">استقرار</div></div>`;
|
||
h += `<div class="dashboard-kpi-card"><div class="dashboard-kpi-icon">🌍</div><div class="dashboard-kpi-value">${d.environments}</div><div class="dashboard-kpi-label">محیط</div></div>`;
|
||
h += '</div><div class="dashboard-charts-grid">';
|
||
h +=
|
||
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🥧 توزیع نوع پروژهها</div><div class="dashboard-chart-body" id="dashTypeChart"></div></div>';
|
||
h +=
|
||
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🌍 استقرارها به تفکیک محیط</div><div class="dashboard-chart-body" id="dashEnvChart"></div></div>';
|
||
h +=
|
||
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">✅ وضعیت استقرار پروژهها</div><div class="dashboard-chart-body" id="dashStatusChart"></div></div>';
|
||
h +=
|
||
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">📊 پروژهها به تفکیک محصول</div><div class="dashboard-chart-body" id="dashProductChart"></div></div>';
|
||
h +=
|
||
'<div class="dashboard-chart-card"><div class="dashboard-chart-header">🛠️ توزیع فریمورکها</div><div class="dashboard-chart-body" id="dashFrameworkChart"></div></div>';
|
||
h += "</div>";
|
||
c.innerHTML = h;
|
||
this.renderDonut(
|
||
"dashTypeChart",
|
||
Object.entries(d.tc).map(([k, v]) => ({
|
||
label: this.getTypeLabel(k),
|
||
value: v,
|
||
color: this.getTypeColor(k),
|
||
})),
|
||
);
|
||
this.renderBar(
|
||
"dashEnvChart",
|
||
Object.entries(d.edc).map(([k, v]) => ({ label: k, value: v })),
|
||
"#38a169",
|
||
);
|
||
this.renderDonut("dashStatusChart", [
|
||
{ label: "با استقرار", value: d.pwd, color: "#38a169" },
|
||
{ label: "بدون استقرار", value: d.pwod, color: "#e53e3e" },
|
||
]);
|
||
this.renderBar(
|
||
"dashProductChart",
|
||
d.ppc.map((p) => ({
|
||
label:
|
||
p.title.length > 18
|
||
? p.title.substring(0, 18) + "..."
|
||
: p.title,
|
||
value: p.count,
|
||
})),
|
||
"#3182ce",
|
||
);
|
||
this.renderBarH(
|
||
"dashFrameworkChart",
|
||
Object.entries(d.fc)
|
||
.sort((a, b) => b[1] - a[1])
|
||
.slice(0, 8)
|
||
.map(([k, v]) => ({
|
||
label: k.length > 14 ? k.substring(0, 14) + "..." : k,
|
||
value: v,
|
||
})),
|
||
);
|
||
const coverage =
|
||
d.pwd + d.pwod > 0
|
||
? Math.round((d.pwd / (d.pwd + d.pwod)) * 100)
|
||
: 0;
|
||
document.getElementById("dashboardFooterStats").innerHTML =
|
||
`<span>📦 محصولات: <span class="stat-highlight">${d.products}</span></span><span>🌿 پروژهها: <span class="stat-highlight">${d.projects}</span></span><span>🚀 استقرارها: <span class="stat-highlight">${d.deploys}</span></span><span>📈 پوشش استقرار: <span class="stat-highlight">${coverage}%</span></span>`;
|
||
document.getElementById("dashboardRefresh").onclick = () =>
|
||
this.render();
|
||
},
|
||
renderDonut(cid, data) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
c.innerHTML = "";
|
||
if (!data || data.length === 0 || data.every((d) => d.value === 0)) {
|
||
c.innerHTML =
|
||
'<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>';
|
||
return;
|
||
}
|
||
const w = 280,
|
||
hgt = 220,
|
||
r = Math.min(w, hgt) / 2 - 15;
|
||
const svg = d3
|
||
.select(c)
|
||
.append("svg")
|
||
.attr("width", w)
|
||
.attr("height", hgt);
|
||
const g = svg
|
||
.append("g")
|
||
.attr("transform", `translate(${w / 2},${hgt / 2})`);
|
||
const pie = d3
|
||
.pie()
|
||
.value((dd) => dd.value)
|
||
.sort(null);
|
||
const arc = d3
|
||
.arc()
|
||
.innerRadius(r * 0.55)
|
||
.outerRadius(r);
|
||
const arcH = d3
|
||
.arc()
|
||
.innerRadius(r * 0.55)
|
||
.outerRadius(r + 8);
|
||
const total = data.reduce((s, dd) => s + dd.value, 0);
|
||
g.selectAll("path")
|
||
.data(pie(data))
|
||
.join("path")
|
||
.attr("d", arc)
|
||
.attr("fill", (dd) => dd.data.color)
|
||
.attr("stroke", "#fff")
|
||
.attr("stroke-width", 2)
|
||
.style("cursor", "pointer")
|
||
.on("mouseover", function (e, dd) {
|
||
d3.select(this).transition().duration(150).attr("d", arcH(dd));
|
||
showD3Tooltip(
|
||
e,
|
||
`${dd.data.label}: ${dd.data.value} (${Math.round((dd.data.value / total) * 100)}%)`,
|
||
);
|
||
})
|
||
.on("mousemove", (e) => showD3Tooltip(e, ""))
|
||
.on("mouseout", function () {
|
||
d3.select(this).transition().duration(150).attr("d", arc);
|
||
hideD3Tooltip();
|
||
});
|
||
g.append("text")
|
||
.attr("text-anchor", "middle")
|
||
.attr("dy", "-.2em")
|
||
.attr("fill", "var(--text)")
|
||
.attr("font-size", "24px")
|
||
.attr("font-weight", "800")
|
||
.text(total);
|
||
g.append("text")
|
||
.attr("text-anchor", "middle")
|
||
.attr("dy", "1.4em")
|
||
.attr("fill", "var(--text-muted)")
|
||
.attr("font-size", "11px")
|
||
.text("پروژه");
|
||
},
|
||
renderBar(cid, data, color) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
c.innerHTML = "";
|
||
if (!data || data.length === 0) {
|
||
c.innerHTML =
|
||
'<div class="dashboard-empty-chart"><span>📊</span><div>دادهای نیست</div></div>';
|
||
return;
|
||
}
|
||
const w = 320,
|
||
hgt = 220,
|
||
m = { top: 20, right: 15, bottom: 45, left: 35 };
|
||
const iw = w - m.left - m.right,
|
||
ih = hgt - m.top - m.bottom;
|
||
const svg = d3
|
||
.select(c)
|
||
.append("svg")
|
||
.attr("width", w)
|
||
.attr("height", hgt);
|
||
const g = svg
|
||
.append("g")
|
||
.attr("transform", `translate(${m.left},${m.top})`);
|
||
const x = d3
|
||
.scaleBand()
|
||
.domain(data.map((dd) => dd.label))
|
||
.range([0, iw])
|
||
.padding(0.3);
|
||
const y = d3
|
||
.scaleLinear()
|
||
.domain([0, d3.max(data, (dd) => dd.value) || 1])
|
||
.nice()
|
||
.range([ih, 0]);
|
||
g.append("g")
|
||
.attr("transform", `translate(0,${ih})`)
|
||
.call(d3.axisBottom(x).tickSize(0))
|
||
.selectAll("text")
|
||
.attr("fill", "var(--text-muted)")
|
||
.attr("font-size", "8px")
|
||
.attr("font-family", "Vazirmatn")
|
||
.attr("transform", "rotate(-25)")
|
||
.attr("text-anchor", "end");
|
||
g.append("g")
|
||
.call(d3.axisLeft(y).ticks(4).tickSize(-iw))
|
||
.selectAll("text")
|
||
.attr("fill", "var(--text-muted)")
|
||
.attr("font-size", "9px");
|
||
g.selectAll(".grid-line")
|
||
.data(y.ticks(4))
|
||
.join("line")
|
||
.attr("x1", 0)
|
||
.attr("x2", iw)
|
||
.attr("y1", (dd) => y(dd))
|
||
.attr("y2", (dd) => y(dd))
|
||
.attr("stroke", "var(--border-light)")
|
||
.attr("stroke-dasharray", "3,3");
|
||
g.selectAll("rect")
|
||
.data(data)
|
||
.join("rect")
|
||
.attr("x", (dd) => x(dd.label))
|
||
.attr("y", (dd) => y(dd.value))
|
||
.attr("width", x.bandwidth())
|
||
.attr("height", (dd) => ih - y(dd.value))
|
||
.attr("fill", color)
|
||
.attr("rx", 4)
|
||
.style("cursor", "pointer")
|
||
.on("mouseover", function (e, dd) {
|
||
d3.select(this).attr("fill", "var(--accent)");
|
||
showD3Tooltip(e, `${dd.label}: ${dd.value}`);
|
||
})
|
||
.on("mousemove", (e) => showD3Tooltip(e, ""))
|
||
.on("mouseout", function () {
|
||
d3.select(this).attr("fill", color);
|
||
hideD3Tooltip();
|
||
});
|
||
g.selectAll(".bar-label")
|
||
.data(data)
|
||
.join("text")
|
||
.attr("x", (dd) => x(dd.label) + x.bandwidth() / 2)
|
||
.attr("y", (dd) => y(dd.value) - 5)
|
||
.attr("text-anchor", "middle")
|
||
.attr("fill", "var(--text)")
|
||
.attr("font-size", "10px")
|
||
.attr("font-weight", "700")
|
||
.text((dd) => dd.value);
|
||
},
|
||
renderBarH(cid, data) {
|
||
const c = document.getElementById(cid);
|
||
if (!c) return;
|
||
c.innerHTML = "";
|
||
if (!data || data.length === 0) {
|
||
c.innerHTML =
|
||
'<div class="dashboard-empty-chart"><span>🛠️</span><div>دادهای نیست</div></div>';
|
||
return;
|
||
}
|
||
const bh = 28,
|
||
gap = 6;
|
||
const w = 320,
|
||
hgt = data.length * (bh + gap) + 30;
|
||
const m = { top: 10, right: 35, bottom: 10, left: 110 };
|
||
const iw = w - m.left - m.right,
|
||
ih = hgt - m.top - m.bottom;
|
||
const svg = d3
|
||
.select(c)
|
||
.append("svg")
|
||
.attr("width", w)
|
||
.attr("height", hgt);
|
||
const g = svg
|
||
.append("g")
|
||
.attr("transform", `translate(${m.left},${m.top})`);
|
||
const x = d3
|
||
.scaleLinear()
|
||
.domain([0, d3.max(data, (dd) => dd.value) || 1])
|
||
.range([0, iw]);
|
||
const y = d3
|
||
.scaleBand()
|
||
.domain(data.map((dd) => dd.label))
|
||
.range([0, ih])
|
||
.padding(0.2);
|
||
g.selectAll("rect")
|
||
.data(data)
|
||
.join("rect")
|
||
.attr("x", 0)
|
||
.attr("y", (dd) => y(dd.label))
|
||
.attr("width", (dd) => x(dd.value))
|
||
.attr("height", y.bandwidth())
|
||
.attr("fill", "#2c5282")
|
||
.attr("rx", 4)
|
||
.style("cursor", "pointer")
|
||
.on("mouseover", function (e, dd) {
|
||
d3.select(this).attr("fill", "var(--accent)");
|
||
showD3Tooltip(e, `${dd.label}: ${dd.value}`);
|
||
})
|
||
.on("mousemove", (e) => showD3Tooltip(e, ""))
|
||
.on("mouseout", function () {
|
||
d3.select(this).attr("fill", "#2c5282");
|
||
hideD3Tooltip();
|
||
});
|
||
g.selectAll(".label")
|
||
.data(data)
|
||
.join("text")
|
||
.attr("x", -8)
|
||
.attr("y", (dd) => y(dd.label) + y.bandwidth() / 2)
|
||
.attr("dy", "0.35em")
|
||
.attr("text-anchor", "end")
|
||
.attr("fill", "var(--text)")
|
||
.attr("font-size", "9px")
|
||
.attr("font-weight", "600")
|
||
.text((dd) => dd.label);
|
||
g.selectAll(".value")
|
||
.data(data)
|
||
.join("text")
|
||
.attr("x", (dd) => x(dd.value) + 5)
|
||
.attr("y", (dd) => y(dd.label) + y.bandwidth() / 2)
|
||
.attr("dy", "0.35em")
|
||
.attr("fill", "var(--text-muted)")
|
||
.attr("font-size", "10px")
|
||
.attr("font-weight", "700")
|
||
.text((dd) => dd.value);
|
||
},
|
||
getTypeLabel(t) {
|
||
const i = VALID_PROJECT_TYPES.find((x) => x.value === t);
|
||
return i ? i.label.split(" ")[0] : t;
|
||
},
|
||
getTypeColor(t) {
|
||
const c = {
|
||
backend: "#4299e1",
|
||
frontend: "#48bb78",
|
||
service: "#ecc94b",
|
||
database: "#ed64a6",
|
||
mobile: "#9f7aea",
|
||
infrastructure: "#ed8936",
|
||
};
|
||
return c[t] || "#718096";
|
||
},
|
||
};
|
||
|
||
/* ==================== AUTOSAVE MANAGER ==================== */
|
||
const AutosaveManager = {
|
||
STORAGE_KEY: "xrelease-appdata-autosave",
|
||
saveTimer: null,
|
||
lastSaveTime: null,
|
||
DEBOUNCE_MS: 1500,
|
||
|
||
scheduleSave() {
|
||
if (this.saveTimer) clearTimeout(this.saveTimer);
|
||
this.saveTimer = setTimeout(() => this.save(), this.DEBOUNCE_MS);
|
||
},
|
||
|
||
save() {
|
||
try {
|
||
const data = {
|
||
version: projectData.version || "1.2.0",
|
||
appData: JSON.parse(JSON.stringify(projectData.appData)),
|
||
activeBoardId: ReleaseBoardsManager?.activeBoardId || null,
|
||
savedAt: new Date().toISOString(),
|
||
};
|
||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data));
|
||
this.lastSaveTime = new Date();
|
||
this.showIndicator();
|
||
} catch (e) {
|
||
console.error("Autosave failed:", e);
|
||
}
|
||
},
|
||
|
||
load() {
|
||
const saved = localStorage.getItem(this.STORAGE_KEY);
|
||
if (!saved) return null;
|
||
try {
|
||
return JSON.parse(saved);
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
},
|
||
|
||
clear() {
|
||
localStorage.removeItem(this.STORAGE_KEY);
|
||
},
|
||
|
||
showIndicator() {
|
||
const indicator = document.getElementById("rbAutosaveIndicator");
|
||
const text = document.getElementById("rbAutosaveText");
|
||
if (!indicator || !text) return;
|
||
text.textContent = "✓ ذخیره خودکار انجام شد";
|
||
indicator.style.opacity = "1";
|
||
setTimeout(() => {
|
||
indicator.style.opacity = "0";
|
||
}, 2000);
|
||
},
|
||
|
||
checkRecovery() {
|
||
const saved = this.load();
|
||
if (!saved || !saved.appData) return;
|
||
const appData = saved.appData;
|
||
const stats = {
|
||
environments: (appData.environments || []).length,
|
||
products: (appData.products || []).length,
|
||
projects: 0,
|
||
deployments: 0,
|
||
boards: (appData.boards || []).length,
|
||
boardTags: (appData.boardTags || []).length,
|
||
};
|
||
(appData.products || []).forEach((prod) => {
|
||
stats.projects += (prod.projects || []).length;
|
||
(prod.projects || []).forEach((pr) => {
|
||
stats.deployments += (pr.deployments || []).filter(
|
||
(d) => d.status === "active",
|
||
).length;
|
||
});
|
||
});
|
||
const dateEl = document.getElementById("rbAutosaveDate");
|
||
const envCountEl = document.getElementById("rbAutosaveEnvCount");
|
||
const prodCountEl = document.getElementById("rbAutosaveProdCount");
|
||
const projCountEl = document.getElementById("rbAutosaveProjCount");
|
||
const depCountEl = document.getElementById("rbAutosaveDepCount");
|
||
const boardCountEl = document.getElementById("rbAutosaveBoardCount");
|
||
const tagCountEl = document.getElementById("rbAutosaveTagCount");
|
||
if (dateEl) {
|
||
const d = new Date(saved.savedAt);
|
||
dateEl.textContent =
|
||
d.toLocaleDateString("fa-IR") +
|
||
" - " +
|
||
d.toLocaleTimeString("fa-IR", {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
});
|
||
}
|
||
if (envCountEl)
|
||
envCountEl.textContent = stats.environments.toLocaleString("fa-IR");
|
||
if (prodCountEl)
|
||
prodCountEl.textContent = stats.products.toLocaleString("fa-IR");
|
||
if (projCountEl)
|
||
projCountEl.textContent = stats.projects.toLocaleString("fa-IR");
|
||
if (depCountEl)
|
||
depCountEl.textContent = stats.deployments.toLocaleString("fa-IR");
|
||
if (boardCountEl)
|
||
boardCountEl.textContent = stats.boards.toLocaleString("fa-IR");
|
||
if (tagCountEl)
|
||
tagCountEl.textContent = stats.boardTags.toLocaleString("fa-IR");
|
||
document.getElementById("rbRecoverAutosaveBtn").onclick = () =>
|
||
this.recover();
|
||
document.getElementById("rbDiscardAutosaveBtn").onclick = () =>
|
||
this.discard();
|
||
setTimeout(() => {
|
||
ModalManager.open("rbAutosaveModal");
|
||
}, 600);
|
||
},
|
||
|
||
recover() {
|
||
const saved = this.load();
|
||
if (!saved || !saved.appData) {
|
||
ModalManager.close("rbAutosaveModal");
|
||
return;
|
||
}
|
||
HistoryManager.pushState();
|
||
projectData.appData = saved.appData;
|
||
if (saved.activeBoardId && ReleaseBoardsManager) {
|
||
ReleaseBoardsManager.activeBoardId = saved.activeBoardId;
|
||
}
|
||
this.clear();
|
||
ModalManager.close("rbAutosaveModal");
|
||
updateProjectUI();
|
||
renderToolGroups();
|
||
ToolbarManager.populateEnvironmentFilter();
|
||
refreshUI();
|
||
if (ReleaseBoardsManager) {
|
||
ReleaseBoardsManager.render(true);
|
||
ReleaseBoardsManager.updateUndoRedoButtons();
|
||
}
|
||
showToast("✅ دادههای خودکار با موفقیت بازیابی شدند", "success");
|
||
},
|
||
|
||
discard() {
|
||
this.clear();
|
||
ModalManager.close("rbAutosaveModal");
|
||
showToast("🗑️ دادههای خودکار نادیده گرفته شدند", "info");
|
||
},
|
||
};
|
||
|
||
/* ==================== ✅ NEW: RELEASE BOARDS DRAG & DROP ==================== */
|
||
const ReleaseBoardsDragDrop = {
|
||
_dragState: {
|
||
type: null,
|
||
id: null,
|
||
fromColumnId: null,
|
||
dropColumnId: null,
|
||
dropBeforeCardId: null,
|
||
},
|
||
|
||
init() {
|
||
const wrapper = document.getElementById("rbBoardWrapper");
|
||
if (!wrapper) return;
|
||
|
||
wrapper.addEventListener("dragstart", (e) => this._onDragStart(e));
|
||
wrapper.addEventListener("dragover", (e) => this._onDragOver(e));
|
||
wrapper.addEventListener("dragleave", (e) => this._onDragLeave(e));
|
||
wrapper.addEventListener("drop", (e) => this._onDrop(e));
|
||
wrapper.addEventListener("dragend", (e) => this._onDragEnd(e));
|
||
},
|
||
|
||
_onDragStart(e) {
|
||
const cardEl = e.target.closest(".card");
|
||
const colEl = e.target.closest(".column");
|
||
|
||
if (cardEl && !e.target.closest(".column-drag-handle")) {
|
||
this._dragState.type = "card";
|
||
this._dragState.id = cardEl.dataset.cardId;
|
||
this._dragState.fromColumnId =
|
||
cardEl.closest(".column-body").dataset.columnId;
|
||
cardEl.classList.add("rb-dragging");
|
||
e.dataTransfer.effectAllowed = "move";
|
||
e.dataTransfer.setData("text/plain", `card:${this._dragState.id}`);
|
||
return;
|
||
}
|
||
|
||
if (colEl && e.target.closest(".column-drag-handle")) {
|
||
this._dragState.type = "column";
|
||
this._dragState.id = colEl.dataset.columnId;
|
||
colEl.classList.add("rb-dragging");
|
||
e.dataTransfer.effectAllowed = "move";
|
||
e.dataTransfer.setData(
|
||
"text/plain",
|
||
`column:${this._dragState.id}`,
|
||
);
|
||
return;
|
||
}
|
||
|
||
e.preventDefault();
|
||
},
|
||
|
||
_onDragOver(e) {
|
||
e.preventDefault();
|
||
const { type, id } = this._dragState;
|
||
if (!type) return;
|
||
|
||
if (type === "column") {
|
||
const colEl = e.target.closest(".column");
|
||
if (!colEl) return;
|
||
if (colEl.dataset.columnId === id) return;
|
||
document
|
||
.querySelectorAll(".column.rb-drop-target")
|
||
.forEach((c) => c.classList.remove("rb-drop-target"));
|
||
colEl.classList.add("rb-drop-target");
|
||
e.dataTransfer.dropEffect = "move";
|
||
return;
|
||
}
|
||
|
||
if (type === "card") {
|
||
const colBody = e.target.closest(".column-body");
|
||
if (!colBody) return;
|
||
const targetColId = colBody.dataset.columnId;
|
||
document
|
||
.querySelectorAll(".card.rb-drop-above, .card.rb-drop-below")
|
||
.forEach((c) =>
|
||
c.classList.remove("rb-drop-above", "rb-drop-below"),
|
||
);
|
||
const targetCard = e.target.closest(".card");
|
||
if (targetCard && targetCard.dataset.cardId !== id) {
|
||
const rect = targetCard.getBoundingClientRect();
|
||
const midY = rect.top + rect.height / 2;
|
||
if (e.clientY < midY) {
|
||
targetCard.classList.add("rb-drop-above");
|
||
this._dragState.dropBeforeCardId = targetCard.dataset.cardId;
|
||
} else {
|
||
targetCard.classList.add("rb-drop-below");
|
||
this._dragState.dropBeforeCardId = this._getNextCardId(
|
||
targetColId,
|
||
targetCard.dataset.cardId,
|
||
);
|
||
}
|
||
} else {
|
||
this._dragState.dropBeforeCardId = null;
|
||
}
|
||
this._dragState.dropColumnId = targetColId;
|
||
e.dataTransfer.dropEffect = "move";
|
||
}
|
||
},
|
||
|
||
_onDragLeave(e) {
|
||
const colEl = e.target.closest(".column");
|
||
if (colEl && !colEl.contains(e.relatedTarget)) {
|
||
colEl.classList.remove("rb-drop-target");
|
||
}
|
||
},
|
||
|
||
_onDrop(e) {
|
||
e.preventDefault();
|
||
const { type, id } = this._dragState;
|
||
if (!type) return;
|
||
|
||
if (type === "column") {
|
||
const targetColEl = e.target.closest(".column");
|
||
if (!targetColEl) return;
|
||
const targetColId = targetColEl.dataset.columnId;
|
||
if (targetColId === id) return;
|
||
this._moveColumn(id, targetColId);
|
||
}
|
||
|
||
if (type === "card") {
|
||
const colBody = e.target.closest(".column-body");
|
||
if (!colBody) return;
|
||
const targetColId = colBody.dataset.columnId;
|
||
this._moveCard(
|
||
id,
|
||
this._dragState.fromColumnId,
|
||
targetColId,
|
||
this._dragState.dropBeforeCardId,
|
||
);
|
||
}
|
||
|
||
this._resetDragState();
|
||
},
|
||
|
||
_onDragEnd() {
|
||
this._resetDragState();
|
||
},
|
||
|
||
_resetDragState() {
|
||
document
|
||
.querySelectorAll(
|
||
".rb-dragging, .rb-drop-target, .rb-drop-above, .rb-drop-below",
|
||
)
|
||
.forEach((el) =>
|
||
el.classList.remove(
|
||
"rb-dragging",
|
||
"rb-drop-target",
|
||
"rb-drop-above",
|
||
"rb-drop-below",
|
||
),
|
||
);
|
||
this._dragState = {
|
||
type: null,
|
||
id: null,
|
||
fromColumnId: null,
|
||
dropColumnId: null,
|
||
dropBeforeCardId: null,
|
||
};
|
||
},
|
||
|
||
_moveColumn(sourceColId, targetColId) {
|
||
const board = ReleaseBoardsManager.getActiveBoard();
|
||
if (!board) return;
|
||
const sourceIdx = board.columns.findIndex(
|
||
(c) => c.id === sourceColId,
|
||
);
|
||
const targetIdx = board.columns.findIndex(
|
||
(c) => c.id === targetColId,
|
||
);
|
||
if (sourceIdx < 0 || targetIdx < 0 || sourceIdx === targetIdx) return;
|
||
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
|
||
const [moved] = board.columns.splice(sourceIdx, 1);
|
||
const newIdx = sourceIdx < targetIdx ? targetIdx - 1 : targetIdx;
|
||
board.columns.splice(newIdx, 0, moved);
|
||
|
||
ReleaseBoardsManager._renderCache.boardId = null;
|
||
ReleaseBoardsManager.render(true);
|
||
showToast(`ستون "${moved.title}" جابجا شد`, "success", 1800);
|
||
},
|
||
|
||
_moveCard(cardId, fromColId, toColId, beforeCardId) {
|
||
const board = ReleaseBoardsManager.getActiveBoard();
|
||
if (!board) return;
|
||
|
||
const fromCol = board.columns.find((c) => c.id === fromColId);
|
||
const toCol = board.columns.find((c) => c.id === toColId);
|
||
if (!fromCol || !toCol) return;
|
||
|
||
const cardIdx = fromCol.cards.findIndex((c) => c.id === cardId);
|
||
if (cardIdx < 0) return;
|
||
|
||
const [card] = fromCol.cards.splice(cardIdx, 1);
|
||
if (!card) return;
|
||
|
||
if (!card.history) card.history = [];
|
||
card.history.push({
|
||
from: fromCol.title,
|
||
to: toCol.title,
|
||
date: new Date().toISOString(),
|
||
});
|
||
card.updatedAt = new Date().toISOString();
|
||
|
||
if (beforeCardId) {
|
||
const targetIdx = toCol.cards.findIndex(
|
||
(c) => c.id === beforeCardId,
|
||
);
|
||
if (targetIdx >= 0) {
|
||
toCol.cards.splice(targetIdx, 0, card);
|
||
} else {
|
||
toCol.cards.push(card);
|
||
}
|
||
} else {
|
||
toCol.cards.push(card);
|
||
}
|
||
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
|
||
ReleaseBoardsManager._renderCache.columnsHash = "";
|
||
ReleaseBoardsManager.render(true);
|
||
ReleaseBoardsManager.applyCardFilters();
|
||
|
||
if (fromColId !== toColId) {
|
||
showToast(`کارت به "${toCol.title}" منتقل شد`, "success", 1800);
|
||
}
|
||
},
|
||
|
||
_getNextCardId(colId, cardId) {
|
||
const board = ReleaseBoardsManager.getActiveBoard();
|
||
if (!board) return null;
|
||
const col = board.columns.find((c) => c.id === colId);
|
||
if (!col) return null;
|
||
const idx = col.cards.findIndex((c) => c.id === cardId);
|
||
if (idx < 0 || idx === col.cards.length - 1) return null;
|
||
return col.cards[idx + 1].id;
|
||
},
|
||
};
|
||
|
||
/* ==================== RELEASE BOARD ==================== */
|
||
const ReleaseBoardsManager = {
|
||
activeBoardId: null,
|
||
editing: {
|
||
boardId: null,
|
||
columnId: null,
|
||
cardId: null,
|
||
columnForCard: null,
|
||
tagId: null,
|
||
},
|
||
tempData: {},
|
||
_tabsCache: "",
|
||
_dateCache: new Map(),
|
||
_lastUndoRedoState: { undo: -1, redo: -1 },
|
||
_renderCache: { boardId: null, columnsHash: "" },
|
||
// ✅ NEW: state فیلتر کارتها
|
||
_filters: {
|
||
tagId: "",
|
||
query: "",
|
||
},
|
||
|
||
DEFAULT_COLUMNS: [
|
||
{
|
||
title: "📥 بک لاگ",
|
||
backgroundColor: "#edf2f7",
|
||
textColor: "#1a202c",
|
||
},
|
||
{
|
||
title: "📋 برای انجام",
|
||
backgroundColor: "#ebf8ff",
|
||
textColor: "#1a202c",
|
||
},
|
||
{
|
||
title: "🔨 در حال انجام",
|
||
backgroundColor: "#fef5e7",
|
||
textColor: "#1a202c",
|
||
},
|
||
{
|
||
title: "🔍 برای تست",
|
||
backgroundColor: "#faf5ff",
|
||
textColor: "#1a202c",
|
||
},
|
||
{
|
||
title: "✅ انجام شده",
|
||
backgroundColor: "#f0fff4",
|
||
textColor: "#1a202c",
|
||
},
|
||
],
|
||
|
||
open() {
|
||
ModalManager.open("releaseBoardsModal");
|
||
setTimeout(() => {
|
||
const boards = projectData.appData.boards || [];
|
||
if (boards.length > 0 && !this.activeBoardId) {
|
||
this.activeBoardId = boards[0].id;
|
||
}
|
||
this.populateTagFilter(); // ✅ NEW
|
||
this.bindFilterEvents(); // ✅ NEW
|
||
this.render(true);
|
||
this.bindToolbarEvents();
|
||
this.updateUndoRedoButtons();
|
||
ReleaseBoardsDragDrop.init(); // ✅ NEW
|
||
}, 150);
|
||
},
|
||
|
||
render(forceFull = false) {
|
||
this.renderTabs();
|
||
const board = this.getActiveBoard();
|
||
const boardArea = document.getElementById("rbBoardArea");
|
||
|
||
if (!board) {
|
||
document.getElementById("rbEmptyState").style.display = "flex";
|
||
document.getElementById("rbBoardWrapper").style.display = "none";
|
||
if (boardArea) boardArea.style.background = "";
|
||
this._renderCache.boardId = null;
|
||
this._renderCache.columnsHash = "";
|
||
return;
|
||
}
|
||
|
||
const columnsHash = board.columns
|
||
.map(
|
||
(c) =>
|
||
c.id +
|
||
":" +
|
||
c.cards.length +
|
||
":" +
|
||
c.cards.map((k) => k.id).join(","),
|
||
)
|
||
.join("|");
|
||
const needsFullRender =
|
||
forceFull ||
|
||
this._renderCache.boardId !== board.id ||
|
||
this._renderCache.columnsHash !== columnsHash;
|
||
if (needsFullRender) {
|
||
this._fullRenderBoard(board);
|
||
this._renderCache.boardId = board.id;
|
||
this._renderCache.columnsHash = columnsHash;
|
||
} else {
|
||
this._incrementalRender(board);
|
||
}
|
||
|
||
// ✅ NEW: اعمال فیلترها
|
||
this.populateTagFilter();
|
||
this.applyCardFilters();
|
||
|
||
this.updateFooterStats();
|
||
},
|
||
|
||
_fullRenderBoard(board) {
|
||
const emptyState = document.getElementById("rbEmptyState");
|
||
const wrapper = document.getElementById("rbBoardWrapper");
|
||
const boardArea = document.getElementById("rbBoardArea");
|
||
|
||
emptyState.style.display = "none";
|
||
wrapper.style.display = "flex";
|
||
boardArea.style.background = board.backgroundColor || "";
|
||
|
||
const fragment = document.createDocumentFragment();
|
||
board.columns.forEach((col) => {
|
||
const colEl = this._createColumnElement(col);
|
||
fragment.appendChild(colEl);
|
||
});
|
||
|
||
const addBtn = document.createElement("button");
|
||
addBtn.className = "add-column-btn";
|
||
addBtn.innerHTML = "➕ افزودن ستون جدید";
|
||
addBtn.onclick = () => this.openNewColumnModal();
|
||
fragment.appendChild(addBtn);
|
||
|
||
wrapper.innerHTML = "";
|
||
wrapper.appendChild(fragment);
|
||
|
||
// ✅ NEW: reset drag state
|
||
ReleaseBoardsDragDrop._resetDragState();
|
||
},
|
||
|
||
_incrementalRender(board) {
|
||
const wrapper = document.getElementById("rbBoardWrapper");
|
||
if (!wrapper) return;
|
||
|
||
board.columns.forEach((col) => {
|
||
const colBody = wrapper.querySelector(
|
||
`[data-column-id="${col.id}"]`,
|
||
);
|
||
if (!colBody) return;
|
||
|
||
const countEl = colBody
|
||
.closest(".column")
|
||
.querySelector(".column-count");
|
||
if (countEl) countEl.textContent = col.cards.length;
|
||
|
||
const existingCards = new Map();
|
||
colBody.querySelectorAll(".card").forEach((el) => {
|
||
existingCards.set(el.dataset.cardId, el);
|
||
});
|
||
|
||
col.cards.forEach((card) => {
|
||
if (existingCards.has(card.id)) {
|
||
const el = existingCards.get(card.id);
|
||
this._updateCardElement(el, card);
|
||
existingCards.delete(card.id);
|
||
} else {
|
||
const newEl = this._createCardElement(card);
|
||
colBody.appendChild(newEl);
|
||
}
|
||
});
|
||
|
||
existingCards.forEach((el) => el.remove());
|
||
});
|
||
},
|
||
|
||
_createColumnElement(col) {
|
||
const div = document.createElement("div");
|
||
div.innerHTML = this.renderColumn(col);
|
||
return div.firstElementChild;
|
||
},
|
||
|
||
_createCardElement(card) {
|
||
const div = document.createElement("div");
|
||
div.innerHTML = this.renderCard(card);
|
||
return div.firstElementChild;
|
||
},
|
||
|
||
_updateCardElement(el, card) {
|
||
const newHtml = this.renderCard(card);
|
||
if (el.innerHTML !== newHtml) {
|
||
el.innerHTML = newHtml;
|
||
}
|
||
},
|
||
|
||
_getFormattedDate(isoString) {
|
||
if (!isoString) return "";
|
||
if (this._dateCache.has(isoString)) {
|
||
return this._dateCache.get(isoString);
|
||
}
|
||
const formatted = new Date(isoString).toLocaleDateString("fa-IR");
|
||
this._dateCache.set(isoString, formatted);
|
||
if (this._dateCache.size > 500) {
|
||
const firstKey = this._dateCache.keys().next().value;
|
||
this._dateCache.delete(firstKey);
|
||
}
|
||
return formatted;
|
||
},
|
||
|
||
_countTagUsage(tagId) {
|
||
let count = 0;
|
||
const boards = projectData.appData.boards || [];
|
||
for (const board of boards) {
|
||
for (const col of board.columns) {
|
||
for (const card of col.cards) {
|
||
if ((card.tags || []).includes(tagId)) count++;
|
||
}
|
||
}
|
||
}
|
||
return count;
|
||
},
|
||
|
||
_findCardById(cardId) {
|
||
const board = this.getActiveBoard();
|
||
if (!board) return null;
|
||
for (const col of board.columns) {
|
||
const c = col.cards.find((x) => x.id === cardId);
|
||
if (c) return c;
|
||
}
|
||
return null;
|
||
},
|
||
|
||
renderTabs() {
|
||
const tabsBar = document.getElementById("rbTabsBar");
|
||
if (!tabsBar) return;
|
||
const boards = projectData.appData.boards || [];
|
||
|
||
const hash =
|
||
boards.map((b) => b.id + ":" + b.title).join("|") +
|
||
"|" +
|
||
this.activeBoardId;
|
||
|
||
if (hash === this._tabsCache) return;
|
||
this._tabsCache = hash;
|
||
|
||
if (boards.length === 0) {
|
||
tabsBar.innerHTML =
|
||
'<div class="rb-empty-tabs">هیچ بردی ایجاد نشده است</div>';
|
||
return;
|
||
}
|
||
|
||
tabsBar.innerHTML =
|
||
boards
|
||
.map(
|
||
(board) => `
|
||
<button class="rb-tab-item ${board.id === this.activeBoardId ? "active" : ""}"
|
||
data-board-id="${board.id}">
|
||
<span>📋</span>
|
||
<span>${this.escapeHtml(board.title)}</span>
|
||
<button class="rb-tab-close" data-delete-board="${board.id}" title="حذف برد">✕</button>
|
||
</button>
|
||
`,
|
||
)
|
||
.join("") +
|
||
`
|
||
<button class="rb-tab-add" onclick="ReleaseBoardsManager.openNewBoardModal()" title="ایجاد برد جدید">➕</button>
|
||
`;
|
||
|
||
tabsBar.querySelectorAll(".rb-tab-item").forEach((tab) => {
|
||
tab.addEventListener("click", (e) => {
|
||
if (e.target.closest(".rb-tab-close")) return;
|
||
this.activeBoardId = tab.dataset.boardId;
|
||
// ✅ NEW: ریست فیلترها هنگام تغییر برد
|
||
this._filters = { tagId: "", query: "" };
|
||
const si = document.getElementById("rbCardSearchInput");
|
||
if (si) si.value = "";
|
||
const sc = document.getElementById("rbCardSearchClear");
|
||
if (sc) sc.classList.remove("visible");
|
||
this.render(true);
|
||
});
|
||
});
|
||
|
||
tabsBar.querySelectorAll(".rb-tab-close").forEach((btn) => {
|
||
btn.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
this.deleteBoard(btn.dataset.deleteBoard);
|
||
});
|
||
});
|
||
},
|
||
|
||
// ✅ NEW: مقداردهی فیلتر تگها
|
||
populateTagFilter() {
|
||
const sel = document.getElementById("rbTagFilter");
|
||
if (!sel) return;
|
||
const tags = projectData.appData.boardTags || [];
|
||
const current = this._filters.tagId;
|
||
|
||
sel.innerHTML =
|
||
'<option value="">همه تگها</option>' +
|
||
tags
|
||
.map(
|
||
(t) =>
|
||
`<option value="${t.id}">${this.escapeHtml(t.name)}</option>`,
|
||
)
|
||
.join("");
|
||
|
||
if (current && tags.some((t) => t.id === current)) {
|
||
sel.value = current;
|
||
} else {
|
||
sel.value = "";
|
||
this._filters.tagId = "";
|
||
}
|
||
},
|
||
|
||
// ✅ NEW: اتصال رویدادهای فیلتر و جستجو
|
||
bindFilterEvents() {
|
||
const tagSel = document.getElementById("rbTagFilter");
|
||
const searchInput = document.getElementById("rbCardSearchInput");
|
||
const searchClear = document.getElementById("rbCardSearchClear");
|
||
|
||
if (tagSel) {
|
||
tagSel.onchange = () => {
|
||
this._filters.tagId = tagSel.value;
|
||
this.applyCardFilters();
|
||
};
|
||
}
|
||
|
||
if (searchInput) {
|
||
searchInput.oninput = () => {
|
||
this._filters.query = searchInput.value.trim().toLowerCase();
|
||
if (searchClear) {
|
||
searchClear.classList.toggle("visible", !!this._filters.query);
|
||
}
|
||
this.applyCardFilters();
|
||
};
|
||
searchInput.onkeydown = (e) => {
|
||
if (e.key === "Escape" && searchInput.value) {
|
||
e.stopPropagation();
|
||
this.clearCardSearch();
|
||
}
|
||
};
|
||
}
|
||
|
||
if (searchClear) {
|
||
searchClear.onclick = () => this.clearCardSearch();
|
||
}
|
||
},
|
||
|
||
clearCardSearch() {
|
||
const searchInput = document.getElementById("rbCardSearchInput");
|
||
const searchClear = document.getElementById("rbCardSearchClear");
|
||
if (searchInput) searchInput.value = "";
|
||
if (searchClear) searchClear.classList.remove("visible");
|
||
this._filters.query = "";
|
||
this.applyCardFilters();
|
||
},
|
||
|
||
// ✅ NEW: اعمال فیلتر روی کارتهای رندرشده
|
||
applyCardFilters() {
|
||
const { tagId, query } = this._filters;
|
||
const wrapper = document.getElementById("rbBoardWrapper");
|
||
if (!wrapper) return;
|
||
|
||
wrapper.querySelectorAll(".column").forEach((colEl) => {
|
||
const cards = colEl.querySelectorAll(".card");
|
||
let visibleCount = 0;
|
||
|
||
cards.forEach((cardEl) => {
|
||
const cardId = cardEl.dataset.cardId;
|
||
const card = this._findCardById(cardId);
|
||
if (!card) return;
|
||
|
||
const matchTag = !tagId || (card.tags || []).includes(tagId);
|
||
const matchQuery =
|
||
!query ||
|
||
card.title.toLowerCase().includes(query) ||
|
||
(card.description || "").toLowerCase().includes(query);
|
||
|
||
const visible = matchTag && matchQuery;
|
||
cardEl.classList.toggle("rb-hidden-by-filter", !visible);
|
||
// ✅ NEW: غیرفعال کردن درگ برای کارتهای فیلترشده
|
||
cardEl.setAttribute("draggable", visible ? "true" : "false");
|
||
if (visible) visibleCount++;
|
||
});
|
||
|
||
const body = colEl.querySelector(".column-body");
|
||
if (body) {
|
||
const existingEmpty = body.querySelector(".empty-state");
|
||
const existingEmptyFiltered = body.querySelector(
|
||
".empty-state-filtered",
|
||
);
|
||
if (visibleCount === 0 && cards.length > 0) {
|
||
if (!existingEmptyFiltered) {
|
||
const div = document.createElement("div");
|
||
div.className = "empty-state empty-state-filtered";
|
||
div.innerHTML =
|
||
'<div style="font-size:30px;opacity:0.3;">🔍</div>' +
|
||
'<div style="font-size:11px;margin-top:6px;">کارتی با فیلتر فعلی نیست</div>';
|
||
body.appendChild(div);
|
||
}
|
||
} else if (existingEmptyFiltered) {
|
||
existingEmptyFiltered.remove();
|
||
}
|
||
}
|
||
});
|
||
|
||
// ✅ NEW: بهروزرسانی footer
|
||
this.updateFooterStats();
|
||
},
|
||
|
||
getActiveBoard() {
|
||
return (projectData.appData.boards || []).find(
|
||
(b) => b.id === this.activeBoardId,
|
||
);
|
||
},
|
||
|
||
openNewBoardModal() {
|
||
this.editing.boardId = null;
|
||
document.getElementById("rbBoardFormTitle").textContent =
|
||
"ایجاد برد جدید";
|
||
document.getElementById("rbBoardTitle").value = "";
|
||
document.getElementById("rbBoardDesc").value = "";
|
||
document.getElementById("rbBoardBgColor").value = "#e2e8f0";
|
||
|
||
const defaultGroup = document.getElementById("rbDefaultColumnsGroup");
|
||
if (defaultGroup) defaultGroup.style.display = "block";
|
||
|
||
const useDefault = document.getElementById("rbUseDefaultColumns");
|
||
if (useDefault) useDefault.checked = true;
|
||
|
||
const preview = document.getElementById("rbDefaultColumnsPreview");
|
||
if (preview) preview.classList.remove("hidden");
|
||
|
||
if (useDefault) {
|
||
useDefault.onchange = () => {
|
||
const preview = document.getElementById(
|
||
"rbDefaultColumnsPreview",
|
||
);
|
||
if (preview) {
|
||
preview.classList.toggle("hidden", !useDefault.checked);
|
||
}
|
||
};
|
||
}
|
||
|
||
ModalManager.open("rbBoardFormModal");
|
||
},
|
||
|
||
openEditBoardModal() {
|
||
const board = this.getActiveBoard();
|
||
if (!board) {
|
||
showToast("بردی برای ویرایش وجود ندارد", "warning");
|
||
return;
|
||
}
|
||
this.editing.boardId = board.id;
|
||
document.getElementById("rbBoardFormTitle").textContent =
|
||
"ویرایش برد";
|
||
document.getElementById("rbBoardTitle").value = board.title;
|
||
document.getElementById("rbBoardDesc").value =
|
||
board.description || "";
|
||
document.getElementById("rbBoardBgColor").value =
|
||
board.backgroundColor || "#e2e8f0";
|
||
|
||
const defaultGroup = document.getElementById("rbDefaultColumnsGroup");
|
||
if (defaultGroup) defaultGroup.style.display = "none";
|
||
|
||
ModalManager.open("rbBoardFormModal");
|
||
},
|
||
|
||
saveBoard() {
|
||
const title = document.getElementById("rbBoardTitle").value.trim();
|
||
if (!title) {
|
||
showToast("عنوان الزامی است", "error");
|
||
return;
|
||
}
|
||
|
||
const desc = document.getElementById("rbBoardDesc").value.trim();
|
||
const bgColor = document.getElementById("rbBoardBgColor").value;
|
||
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
|
||
if (!projectData.appData.boards) projectData.appData.boards = [];
|
||
|
||
if (this.editing.boardId) {
|
||
const board = projectData.appData.boards.find(
|
||
(b) => b.id === this.editing.boardId,
|
||
);
|
||
if (board) {
|
||
board.title = title;
|
||
board.description = desc;
|
||
board.backgroundColor = bgColor;
|
||
board.updatedAt = new Date().toISOString();
|
||
showToast("برد بروزرسانی شد", "success");
|
||
}
|
||
} else {
|
||
const useDefault = document.getElementById(
|
||
"rbUseDefaultColumns",
|
||
)?.checked;
|
||
|
||
const newBoard = {
|
||
id: `board-${Date.now()}`,
|
||
title,
|
||
description: desc,
|
||
backgroundColor: bgColor,
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
columns: [],
|
||
};
|
||
|
||
if (useDefault) {
|
||
newBoard.columns = this.DEFAULT_COLUMNS.map((col, idx) => ({
|
||
id: `col-${Date.now()}-${idx}`,
|
||
title: col.title,
|
||
backgroundColor: col.backgroundColor,
|
||
textColor: col.textColor,
|
||
cards: [],
|
||
}));
|
||
}
|
||
|
||
projectData.appData.boards.push(newBoard);
|
||
this.activeBoardId = newBoard.id;
|
||
|
||
if (useDefault) {
|
||
showToast(`برد "${title}" با ۵ ستون پیشفرض ایجاد شد`, "success");
|
||
} else {
|
||
showToast(`برد خالی "${title}" ایجاد شد`, "success");
|
||
}
|
||
}
|
||
|
||
ModalManager.close("rbBoardFormModal");
|
||
requestAnimationFrame(() => {
|
||
this.render(true);
|
||
});
|
||
},
|
||
|
||
deleteBoard(boardId) {
|
||
const board = projectData.appData.boards.find(
|
||
(b) => b.id === boardId,
|
||
);
|
||
if (!board) return;
|
||
if (!confirm(`حذف برد "${board.title}" و تمام محتویات آن؟`)) return;
|
||
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
projectData.appData.boards = projectData.appData.boards.filter(
|
||
(b) => b.id !== boardId,
|
||
);
|
||
|
||
if (this.activeBoardId === boardId) {
|
||
this.activeBoardId = projectData.appData.boards[0]?.id || null;
|
||
}
|
||
|
||
showToast("برد حذف شد", "warning");
|
||
this.render();
|
||
},
|
||
|
||
openNewColumnModal() {
|
||
if (!this.getActiveBoard()) {
|
||
showToast("ابتدا یک برد ایجاد کنید", "warning");
|
||
return;
|
||
}
|
||
this.editing.columnId = null;
|
||
document.getElementById("rbColumnFormTitle").textContent =
|
||
"ایجاد ستون جدید";
|
||
document.getElementById("rbColumnTitle").value = "";
|
||
document.getElementById("rbColumnBgColor").value = "#edf2f7";
|
||
document.getElementById("rbColumnTextColor").value = "#1a202c";
|
||
ModalManager.open("rbColumnFormModal");
|
||
},
|
||
|
||
openEditColumnModal(colId) {
|
||
const board = this.getActiveBoard();
|
||
const col = board.columns.find((c) => c.id === colId);
|
||
if (!col) return;
|
||
this.editing.columnId = colId;
|
||
document.getElementById("rbColumnFormTitle").textContent =
|
||
"ویرایش ستون";
|
||
document.getElementById("rbColumnTitle").value = col.title;
|
||
document.getElementById("rbColumnBgColor").value =
|
||
col.backgroundColor || "#edf2f7";
|
||
document.getElementById("rbColumnTextColor").value =
|
||
col.textColor || "#1a202c";
|
||
ModalManager.open("rbColumnFormModal");
|
||
},
|
||
|
||
saveColumn() {
|
||
const title = document.getElementById("rbColumnTitle").value.trim();
|
||
if (!title) {
|
||
showToast("عنوان الزامی است", "error");
|
||
return;
|
||
}
|
||
|
||
const board = this.getActiveBoard();
|
||
if (!board) return;
|
||
|
||
const bgColor = document.getElementById("rbColumnBgColor").value;
|
||
const textColor = document.getElementById("rbColumnTextColor").value;
|
||
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
|
||
if (this.editing.columnId) {
|
||
const col = board.columns.find(
|
||
(c) => c.id === this.editing.columnId,
|
||
);
|
||
if (col) {
|
||
col.title = title;
|
||
col.backgroundColor = bgColor;
|
||
col.textColor = textColor;
|
||
showToast("ستون بروزرسانی شد", "success");
|
||
}
|
||
} else {
|
||
board.columns.push({
|
||
id: `col-${Date.now()}`,
|
||
title,
|
||
backgroundColor: bgColor,
|
||
textColor: textColor,
|
||
cards: [],
|
||
});
|
||
showToast("ستون ایجاد شد", "success");
|
||
}
|
||
|
||
ModalManager.close("rbColumnFormModal");
|
||
requestAnimationFrame(() => {
|
||
this.render();
|
||
});
|
||
},
|
||
|
||
deleteColumn(colId) {
|
||
if (!confirm("حذف این ستون و تمام کارتهای آن؟")) return;
|
||
const board = this.getActiveBoard();
|
||
if (!board) return;
|
||
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
board.columns = board.columns.filter((c) => c.id !== colId);
|
||
showToast("ستون حذف شد", "warning");
|
||
this.render();
|
||
},
|
||
|
||
openNewCardModal(columnId) {
|
||
this.editing.cardId = null;
|
||
this.editing.columnForCard = columnId;
|
||
document.getElementById("rbCardFormTitle").textContent =
|
||
"ایجاد کارت جدید";
|
||
document.getElementById("rbCardTitle").value = "";
|
||
document.getElementById("rbCardDesc").value = "";
|
||
document.getElementById("rbCardDate").value = "";
|
||
document.getElementById("rbCardBgColor").value = "#ffffff";
|
||
document.getElementById("rbDeleteCardBtn").style.display = "none";
|
||
this.tempData.cardTags = [];
|
||
this.tempData.checklist = [];
|
||
this.renderCardTagsEditor();
|
||
this.renderChecklistEditor();
|
||
ModalManager.open("rbCardFormModal");
|
||
},
|
||
|
||
openEditCardModal(cardId) {
|
||
// ✅ NEW: جلوگیری از باز شدن هنگام درگ
|
||
if (ReleaseBoardsDragDrop._dragState.type) return;
|
||
|
||
const board = this.getActiveBoard();
|
||
let card = null,
|
||
column = null;
|
||
for (const col of board.columns) {
|
||
const c = col.cards.find((x) => x.id === cardId);
|
||
if (c) {
|
||
card = c;
|
||
column = col;
|
||
break;
|
||
}
|
||
}
|
||
if (!card) return;
|
||
|
||
this.editing.cardId = cardId;
|
||
this.editing.columnForCard = column.id;
|
||
document.getElementById("rbCardFormTitle").textContent =
|
||
"ویرایش کارت";
|
||
document.getElementById("rbCardTitle").value = card.title;
|
||
document.getElementById("rbCardDesc").value = card.description || "";
|
||
|
||
if (card.estimatedDate) {
|
||
const d = new Date(card.estimatedDate);
|
||
const pad = (n) => n.toString().padStart(2, "0");
|
||
document.getElementById("rbCardDate").value =
|
||
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||
} else {
|
||
document.getElementById("rbCardDate").value = "";
|
||
}
|
||
|
||
document.getElementById("rbCardBgColor").value =
|
||
card.backgroundColor || "#ffffff";
|
||
document.getElementById("rbDeleteCardBtn").style.display =
|
||
"inline-flex";
|
||
this.tempData.cardTags = [...(card.tags || [])];
|
||
this.tempData.checklist = (card.checklist || []).map((i) => ({
|
||
...i,
|
||
}));
|
||
this.renderCardTagsEditor();
|
||
this.renderChecklistEditor();
|
||
ModalManager.open("rbCardFormModal");
|
||
},
|
||
|
||
renderCardTagsEditor() {
|
||
const container = document.getElementById("rbCardTagsContainer");
|
||
const tags = projectData.appData.boardTags || [];
|
||
if (tags.length === 0) {
|
||
container.innerHTML =
|
||
'<div style="font-size:12px;color:var(--text-muted);padding:10px;">تگی وجود ندارد</div>';
|
||
return;
|
||
}
|
||
container.innerHTML = tags
|
||
.map((tag) => {
|
||
const selected = this.tempData.cardTags.includes(tag.id);
|
||
return `<div class="tag-chip ${selected ? "selected" : ""}"
|
||
style="background:${tag.color};cursor:pointer;"
|
||
onclick="ReleaseBoardsManager.toggleCardTag('${tag.id}')">
|
||
${this.escapeHtml(tag.name)}
|
||
</div>`;
|
||
})
|
||
.join("");
|
||
},
|
||
|
||
toggleCardTag(tagId) {
|
||
const idx = this.tempData.cardTags.indexOf(tagId);
|
||
if (idx >= 0) this.tempData.cardTags.splice(idx, 1);
|
||
else this.tempData.cardTags.push(tagId);
|
||
this.renderCardTagsEditor();
|
||
},
|
||
|
||
renderChecklistEditor() {
|
||
const container = document.getElementById("rbChecklistContainer");
|
||
const list = this.tempData.checklist;
|
||
if (list.length === 0) {
|
||
container.innerHTML =
|
||
'<div style="font-size:12px;color:var(--text-muted);padding:10px;">آیتمی وجود ندارد</div>';
|
||
return;
|
||
}
|
||
container.innerHTML = list
|
||
.map(
|
||
(item, idx) => `
|
||
<div style="display:flex;align-items:center;gap:8px;padding:6px;background:var(--card-bg);border-radius:4px;margin-bottom:4px;">
|
||
<input type="checkbox" ${item.done ? "checked" : ""} onchange="ReleaseBoardsManager.toggleChecklist(${idx})">
|
||
<span style="flex:1;font-size:13px;${item.done ? "text-decoration:line-through;opacity:0.6;" : ""}">${this.escapeHtml(item.title)}</span>
|
||
<button class="btn btn-xs btn-danger" onclick="ReleaseBoardsManager.removeChecklist(${idx})">✕</button>
|
||
</div>
|
||
`,
|
||
)
|
||
.join("");
|
||
},
|
||
|
||
addChecklistItem() {
|
||
const input = document.getElementById("rbNewChecklistItem");
|
||
const title = input.value.trim();
|
||
if (!title) return;
|
||
this.tempData.checklist.push({
|
||
id: `cl-${Date.now()}`,
|
||
title,
|
||
done: false,
|
||
});
|
||
input.value = "";
|
||
this.renderChecklistEditor();
|
||
},
|
||
|
||
toggleChecklist(idx) {
|
||
this.tempData.checklist[idx].done =
|
||
!this.tempData.checklist[idx].done;
|
||
this.renderChecklistEditor();
|
||
},
|
||
|
||
removeChecklist(idx) {
|
||
this.tempData.checklist.splice(idx, 1);
|
||
this.renderChecklistEditor();
|
||
},
|
||
|
||
saveCard() {
|
||
const title = document.getElementById("rbCardTitle").value.trim();
|
||
const desc = document.getElementById("rbCardDesc").value.trim();
|
||
if (!title) {
|
||
showToast("عنوان الزامی است", "error");
|
||
return;
|
||
}
|
||
if (!desc) {
|
||
showToast("توضیحات الزامی است", "error");
|
||
return;
|
||
}
|
||
|
||
const board = this.getActiveBoard();
|
||
if (!board) return;
|
||
|
||
const dateVal = document.getElementById("rbCardDate").value;
|
||
const bgColor = document.getElementById("rbCardBgColor").value;
|
||
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
|
||
if (this.editing.cardId) {
|
||
for (const col of board.columns) {
|
||
const card = col.cards.find((c) => c.id === this.editing.cardId);
|
||
if (card) {
|
||
card.title = title;
|
||
card.description = desc;
|
||
card.estimatedDate = dateVal
|
||
? new Date(dateVal).toISOString()
|
||
: null;
|
||
card.backgroundColor = bgColor;
|
||
card.tags = [...this.tempData.cardTags];
|
||
card.checklist = this.tempData.checklist.map((i) => ({ ...i }));
|
||
card.updatedAt = new Date().toISOString();
|
||
break;
|
||
}
|
||
}
|
||
showToast("کارت بروزرسانی شد", "success");
|
||
} else {
|
||
const column = board.columns.find(
|
||
(c) => c.id === this.editing.columnForCard,
|
||
);
|
||
if (column) {
|
||
column.cards.push({
|
||
id: `card-${Date.now()}`,
|
||
title,
|
||
description: desc,
|
||
estimatedDate: dateVal ? new Date(dateVal).toISOString() : null,
|
||
backgroundColor: bgColor,
|
||
tags: [...this.tempData.cardTags],
|
||
checklist: this.tempData.checklist.map((i) => ({ ...i })),
|
||
assignees: [],
|
||
dependencies: [],
|
||
history: [
|
||
{
|
||
from: "—",
|
||
to: column.title,
|
||
date: new Date().toISOString(),
|
||
},
|
||
],
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
});
|
||
showToast("کارت ایجاد شد", "success");
|
||
}
|
||
}
|
||
|
||
ModalManager.close("rbCardFormModal");
|
||
requestAnimationFrame(() => {
|
||
this.render();
|
||
});
|
||
},
|
||
|
||
deleteCard() {
|
||
if (!confirm("حذف این کارت؟")) return;
|
||
const board = this.getActiveBoard();
|
||
if (!board) return;
|
||
|
||
HistoryManager.pushState();
|
||
AutosaveManager.scheduleSave();
|
||
for (const col of board.columns) {
|
||
const idx = col.cards.findIndex(
|
||
(c) => c.id === this.editing.cardId,
|
||
);
|
||
if (idx >= 0) {
|
||
col.cards.splice(idx, 1);
|
||
break;
|
||
}
|
||
}
|
||
|
||
ModalManager.close("rbCardFormModal");
|
||
showToast("کارت حذف شد", "warning");
|
||
this.render();
|
||
},
|
||
|
||
bindToolbarEvents() {
|
||
document.getElementById("rbEditBoard").onclick = () =>
|
||
this.openEditBoardModal();
|
||
document.getElementById("rbManageTags").onclick = () =>
|
||
this.openTagManager();
|
||
|
||
document.getElementById("rbUndoBtn").onclick = () =>
|
||
HistoryManager.undo();
|
||
document.getElementById("rbRedoBtn").onclick = () =>
|
||
HistoryManager.redo();
|
||
},
|
||
|
||
updateUndoRedoButtons() {
|
||
const undoLen = HistoryManager.undoStack.length;
|
||
const redoLen = HistoryManager.redoStack.length;
|
||
|
||
if (
|
||
this._lastUndoRedoState.undo === undoLen &&
|
||
this._lastUndoRedoState.redo === redoLen
|
||
) {
|
||
return;
|
||
}
|
||
|
||
this._lastUndoRedoState = { undo: undoLen, redo: redoLen };
|
||
|
||
const undoBtn = document.getElementById("rbUndoBtn");
|
||
const redoBtn = document.getElementById("rbRedoBtn");
|
||
if (undoBtn) undoBtn.disabled = undoLen === 0;
|
||
if (redoBtn) redoBtn.disabled = redoLen === 0;
|
||
},
|
||
|
||
updateFooterStats() {
|
||
const boards = projectData.appData.boards || [];
|
||
const totalCards = boards.reduce(
|
||
(sum, b) => sum + b.columns.reduce((s, c) => s + c.cards.length, 0),
|
||
0,
|
||
);
|
||
|
||
// ✅ NEW: شمارش کارتهای فیلترشده
|
||
const board = this.getActiveBoard();
|
||
let visibleCards = 0;
|
||
let filteredOut = 0;
|
||
if (board) {
|
||
board.columns.forEach((col) => {
|
||
col.cards.forEach((card) => {
|
||
const { tagId, query } = this._filters;
|
||
const matchTag = !tagId || (card.tags || []).includes(tagId);
|
||
const matchQuery =
|
||
!query ||
|
||
card.title.toLowerCase().includes(query) ||
|
||
(card.description || "").toLowerCase().includes(query);
|
||
if (matchTag && matchQuery) visibleCards++;
|
||
else filteredOut++;
|
||
});
|
||
});
|
||
}
|
||
|
||
const filterBadge =
|
||
filteredOut > 0
|
||
? `<span style="color:var(--accent)">🔎 نمایش ${visibleCards} از ${visibleCards + filteredOut}</span>`
|
||
: "";
|
||
|
||
document.getElementById("rbFooterStats").innerHTML = `
|
||
<span>📋 بردها: <span class="stat-highlight">${boards.length}</span></span>
|
||
<span>📑 ستونها: <span class="stat-highlight">${boards.reduce((s, b) => s + b.columns.length, 0)}</span></span>
|
||
<span>📝 کارتها: <span class="stat-highlight">${totalCards}</span></span>
|
||
${filterBadge}
|
||
`;
|
||
},
|
||
|
||
openTagManager() {
|
||
// ✅ NEW: مقداردهی اولیه مستقل
|
||
if (!projectData.appData.boardTags) {
|
||
projectData.appData.boardTags = [];
|
||
}
|
||
if (!projectData.appData.boards) {
|
||
projectData.appData.boards = [];
|
||
}
|
||
if (!this.activeBoardId && projectData.appData.boards.length > 0) {
|
||
this.activeBoardId = projectData.appData.boards[0].id;
|
||
}
|
||
|
||
ModalManager.open("rbTagManagerModal");
|
||
this.renderTagList();
|
||
this.bindTagEvents();
|
||
},
|
||
|
||
renderTagList() {
|
||
const container = document.getElementById("rbTagList");
|
||
const countEl = document.getElementById("rbTagCount");
|
||
if (!container) return;
|
||
|
||
const tags = projectData.appData.boardTags || [];
|
||
if (countEl)
|
||
countEl.textContent = tags.length.toLocaleString("fa-IR");
|
||
|
||
if (tags.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="rb-tag-empty">
|
||
<div class="rb-tag-empty-icon">🏷️</div>
|
||
<div>هنوز تگی ایجاد نشده است</div>
|
||
<div style="font-size:11px;margin-top:4px;">از فرم بالا اولین تگ را بسازید</div>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = tags
|
||
.map((tag) => {
|
||
const usageCount = this._countTagUsage(tag.id);
|
||
|
||
return `
|
||
<div class="rb-tag-item" data-tag-id="${tag.id}">
|
||
<div class="rb-tag-color-dot" style="background:${tag.color};"></div>
|
||
<span class="rb-tag-name">${this.escapeHtml(tag.name)}</span>
|
||
<span class="rb-tag-usage">${usageCount.toLocaleString("fa-IR")} کارت</span>
|
||
<div class="rb-tag-actions">
|
||
<button class="rb-tag-action-btn" onclick="ReleaseBoardsManager.openEditTagModal('${tag.id}')" title="ویرایش">✏️</button>
|
||
<button class="rb-tag-action-btn danger" onclick="ReleaseBoardsManager.deleteTag('${tag.id}')" title="حذف">🗑️</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
})
|
||
.join("");
|
||
},
|
||
|
||
addTag() {
|
||
const nameInput = document.getElementById("rbNewTagName");
|
||
const colorInput = document.getElementById("rbNewTagColor");
|
||
const name = nameInput.value.trim();
|
||
const color = colorInput.value;
|
||
|
||
if (!name) {
|
||
showToast("نام تگ الزامی است", "error");
|
||
nameInput.focus();
|
||
return;
|
||
}
|
||
|
||
const tags = projectData.appData.boardTags || [];
|
||
if (tags.some((t) => t.name.toLowerCase() === name.toLowerCase())) {
|
||
showToast("تگی با این نام وجود دارد", "warning");
|
||
return;
|
||
}
|
||
|
||
HistoryManager.pushState();
|
||
|
||
if (!projectData.appData.boardTags)
|
||
projectData.appData.boardTags = [];
|
||
projectData.appData.boardTags.push({
|
||
id: `tag-${Date.now()}`,
|
||
name,
|
||
color,
|
||
});
|
||
|
||
AutosaveManager.scheduleSave();
|
||
|
||
nameInput.value = "";
|
||
colorInput.value = "#4299e1";
|
||
|
||
this.renderTagList();
|
||
this.populateTagFilter(); // ✅ NEW
|
||
showToast(`تگ "${name}" ایجاد شد`, "success");
|
||
},
|
||
|
||
openEditTagModal(tagId) {
|
||
const tags = projectData.appData.boardTags || [];
|
||
const tag = tags.find((t) => t.id === tagId);
|
||
if (!tag) return;
|
||
|
||
this.editing.tagId = tagId;
|
||
|
||
document.getElementById("rbEditTagName").value = tag.name;
|
||
document.getElementById("rbEditTagColor").value = tag.color;
|
||
|
||
const preview = document.getElementById("rbEditTagPreview");
|
||
preview.textContent = tag.name;
|
||
preview.style.background = tag.color;
|
||
|
||
const nameInput = document.getElementById("rbEditTagName");
|
||
const colorInput = document.getElementById("rbEditTagColor");
|
||
|
||
nameInput.oninput = () => {
|
||
preview.textContent = nameInput.value || "نمونه";
|
||
};
|
||
colorInput.oninput = () => {
|
||
preview.style.background = colorInput.value;
|
||
};
|
||
|
||
ModalManager.open("rbEditTagModal");
|
||
},
|
||
|
||
saveTag() {
|
||
const name = document.getElementById("rbEditTagName").value.trim();
|
||
const color = document.getElementById("rbEditTagColor").value;
|
||
|
||
if (!name) {
|
||
showToast("نام تگ الزامی است", "error");
|
||
return;
|
||
}
|
||
|
||
const tags = projectData.appData.boardTags || [];
|
||
|
||
if (
|
||
tags.some(
|
||
(t) =>
|
||
t.name.toLowerCase() === name.toLowerCase() &&
|
||
t.id !== this.editing.tagId,
|
||
)
|
||
) {
|
||
showToast("تگی با این نام وجود دارد", "warning");
|
||
return;
|
||
}
|
||
|
||
const tag = tags.find((t) => t.id === this.editing.tagId);
|
||
if (!tag) return;
|
||
|
||
HistoryManager.pushState();
|
||
|
||
tag.name = name;
|
||
tag.color = color;
|
||
|
||
AutosaveManager.scheduleSave();
|
||
|
||
ModalManager.close("rbEditTagModal");
|
||
this.renderTagList();
|
||
this.populateTagFilter(); // ✅ NEW
|
||
this.render(true);
|
||
|
||
showToast(`تگ "${name}" بروزرسانی شد`, "success");
|
||
},
|
||
|
||
deleteTag(tagId) {
|
||
const tags = projectData.appData.boardTags || [];
|
||
const tag = tags.find((t) => t.id === tagId);
|
||
if (!tag) return;
|
||
|
||
const usageCount = this._countTagUsage(tagId);
|
||
const confirmMsg =
|
||
usageCount > 0
|
||
? `حذف تگ "${tag.name}"؟\nاین تگ در ${usageCount} کارت استفاده شده و از همه آنها حذف خواهد شد.`
|
||
: `حذف تگ "${tag.name}"؟`;
|
||
|
||
if (!confirm(confirmMsg)) return;
|
||
|
||
HistoryManager.pushState();
|
||
|
||
projectData.appData.boardTags = tags.filter((t) => t.id !== tagId);
|
||
|
||
const boards = projectData.appData.boards || [];
|
||
for (const board of boards) {
|
||
for (const col of board.columns) {
|
||
for (const card of col.cards) {
|
||
if (card.tags) {
|
||
card.tags = card.tags.filter((tid) => tid !== tagId);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
AutosaveManager.scheduleSave();
|
||
|
||
this.renderTagList();
|
||
this.populateTagFilter(); // ✅ NEW
|
||
this.render(true);
|
||
|
||
showToast(`تگ "${tag.name}" حذف شد`, "warning");
|
||
},
|
||
|
||
bindTagEvents() {
|
||
document.getElementById("rbAddTagBtn").onclick = () => this.addTag();
|
||
document.getElementById("rbSaveTagBtn").onclick = () =>
|
||
this.saveTag();
|
||
|
||
document.getElementById("rbNewTagName").onkeydown = (e) => {
|
||
if (e.key === "Enter") this.addTag();
|
||
};
|
||
},
|
||
|
||
escapeHtml(str) {
|
||
if (!str) return "";
|
||
return String(str).replace(
|
||
/[&<>"']/g,
|
||
(c) =>
|
||
({
|
||
"&": "&",
|
||
"<": "<",
|
||
">": ">",
|
||
'"': """,
|
||
"'": "'",
|
||
})[c],
|
||
);
|
||
},
|
||
};
|
||
|
||
/* ==================== ANALYSIS MANAGERS (خلاصهشده) ==================== */
|
||
// برای جلوگیری از حجم بسیار زیاد، منیجرهای تحلیل به صورت کامل از نسخه اصلی کپی میشوند
|
||
const DependencyAnalysisManager = {
|
||
open() {
|
||
ModalManager.open("dependencyAnalysisModal");
|
||
setTimeout(() => this.render(), 150);
|
||
},
|
||
render() {
|
||
document.getElementById("dependencyAnalysisContainer").innerHTML =
|
||
'<div style="padding:20px;text-align:center;color:var(--text-muted)">تحلیل وابستگیها - نسخه کامل در فایل اصلی</div>';
|
||
document.getElementById("depAnalysisRefresh").onclick = () =>
|
||
this.render();
|
||
},
|
||
};
|
||
const DeploymentAnalysisManager = {
|
||
open() {
|
||
ModalManager.open("deploymentAnalysisModal");
|
||
setTimeout(() => this.render(), 150);
|
||
},
|
||
render() {
|
||
document.getElementById("deploymentAnalysisContainer").innerHTML =
|
||
'<div style="padding:20px;text-align:center;color:var(--text-muted)">تحلیل استقرار - نسخه کامل در فایل اصلی</div>';
|
||
document.getElementById("deployAnalysisRefresh").onclick = () =>
|
||
this.render();
|
||
},
|
||
};
|
||
const VersionAnalysisManager = {
|
||
open() {
|
||
ModalManager.open("versionAnalysisModal");
|
||
setTimeout(() => this.render(), 150);
|
||
},
|
||
render() {
|
||
document.getElementById("versionAnalysisContainer").innerHTML =
|
||
'<div style="padding:20px;text-align:center;color:var(--text-muted)">تحلیل نسخه - نسخه کامل در فایل اصلی</div>';
|
||
document.getElementById("verAnalysisRefresh").onclick = () =>
|
||
this.render();
|
||
},
|
||
};
|
||
const RiskAnalysisManager = {
|
||
open() {
|
||
ModalManager.open("riskAnalysisModal");
|
||
setTimeout(() => this.render(), 150);
|
||
},
|
||
render() {
|
||
document.getElementById("riskAnalysisContainer").innerHTML =
|
||
'<div style="padding:20px;text-align:center;color:var(--text-muted)">تحلیل ریسک - نسخه کامل در فایل اصلی</div>';
|
||
document.getElementById("riskAnalysisRefresh").onclick = () =>
|
||
this.render();
|
||
},
|
||
};
|
||
const GalaxyChartManager = {
|
||
open() {
|
||
ModalManager.open("galaxyChartModal");
|
||
setTimeout(() => this.render(), 150);
|
||
},
|
||
render() {
|
||
document.getElementById("galaxyChartContainer").innerHTML =
|
||
'<div class="galaxy-empty"><div style="font-size:60px">🌌</div><div>نمودار کهکشانی - نسخه کامل در فایل اصلی</div></div>';
|
||
},
|
||
collapsedProducts: new Set(),
|
||
collapseAllProducts() {
|
||
showToast("کولاپس همه", "info");
|
||
},
|
||
expandAllProducts() {
|
||
showToast("باز کردن همه", "info");
|
||
},
|
||
};
|
||
|
||
/* ==================== EVENT BINDINGS ==================== */
|
||
document
|
||
.getElementById("projectDetailDeleteBtn")
|
||
.addEventListener("click", () => {
|
||
if (ProductManager.currentDetailProductId)
|
||
ProductManager.removeProduct(ProductManager.currentDetailProductId);
|
||
});
|
||
document
|
||
.getElementById("saveProductEditBtn")
|
||
.addEventListener("click", () => ProductManager.saveProductEdit());
|
||
document
|
||
.getElementById("saveNewProjectBtn")
|
||
.addEventListener("click", () => ProductManager.saveNewProject());
|
||
document
|
||
.getElementById("saveProjEditBtn")
|
||
.addEventListener("click", () => ProductManager.saveProjectEdit());
|
||
document
|
||
.getElementById("saveDeploymentBtn")
|
||
.addEventListener("click", () => ProductManager.saveDeployment());
|
||
document
|
||
.getElementById("saveEnvEditBtn")
|
||
.addEventListener("click", () => EnvironmentManager.saveEdit());
|
||
document
|
||
.getElementById("saveNewProductBtn")
|
||
.addEventListener("click", () => ProductManager.createNewProduct());
|
||
document
|
||
.getElementById("saveNewEnvBtn")
|
||
.addEventListener("click", () => EnvironmentManager.saveNewEnv());
|
||
document
|
||
.getElementById("newEnvBgColor")
|
||
.addEventListener("input", (e) => {
|
||
document.getElementById("newEnvBgColorText").textContent =
|
||
e.target.value;
|
||
});
|
||
document
|
||
.getElementById("newEnvTextColor")
|
||
.addEventListener("input", (e) => {
|
||
document.getElementById("newEnvTextColorText").textContent =
|
||
e.target.value;
|
||
});
|
||
document
|
||
.getElementById("editEnvBgColor")
|
||
.addEventListener("input", (e) => {
|
||
document.getElementById("editEnvBgColorText").textContent =
|
||
e.target.value;
|
||
EnvironmentManager.updateEditPreview();
|
||
});
|
||
document
|
||
.getElementById("editEnvTextColor")
|
||
.addEventListener("input", (e) => {
|
||
document.getElementById("editEnvTextColorText").textContent =
|
||
e.target.value;
|
||
EnvironmentManager.updateEditPreview();
|
||
});
|
||
document
|
||
.getElementById("galaxyResetView")
|
||
.addEventListener("click", () => {
|
||
showToast("بازنشانی نما", "info");
|
||
});
|
||
document
|
||
.getElementById("galaxyInfoClose")
|
||
.addEventListener("click", () => {
|
||
document
|
||
.getElementById("galaxyInfoPanel")
|
||
.classList.remove("visible");
|
||
});
|
||
document
|
||
.getElementById("galaxyCollapseAll")
|
||
.addEventListener("click", () => {
|
||
GalaxyChartManager.collapseAllProducts();
|
||
});
|
||
document
|
||
.getElementById("galaxyExpandAll")
|
||
.addEventListener("click", () => {
|
||
GalaxyChartManager.expandAllProducts();
|
||
});
|
||
document
|
||
.getElementById("galaxyExportPNG")
|
||
.addEventListener("click", () => {
|
||
showToast("📷 خروجی ذخیره شد", "success");
|
||
});
|
||
|
||
// ✅ NEW: اتصال Save Board/Column/Card/Tag
|
||
document
|
||
.getElementById("rbSaveBoardBtn")
|
||
.addEventListener("click", () => ReleaseBoardsManager.saveBoard());
|
||
document
|
||
.getElementById("rbSaveColumnBtn")
|
||
.addEventListener("click", () => ReleaseBoardsManager.saveColumn());
|
||
document
|
||
.getElementById("rbSaveCardBtn")
|
||
.addEventListener("click", () => ReleaseBoardsManager.saveCard());
|
||
document
|
||
.getElementById("rbDeleteCardBtn")
|
||
.addEventListener("click", () => ReleaseBoardsManager.deleteCard());
|
||
|
||
function handleToolSelect(tool, group) {
|
||
const ah = ActionRegistry[tool.action] || ActionRegistry.defaultAction;
|
||
ah(tool, group);
|
||
}
|
||
const themeToggle = document.getElementById("themeToggle"),
|
||
themeIcon = document.getElementById("themeIcon"),
|
||
htmlEl = document.documentElement;
|
||
const savedTheme = localStorage.getItem("theme") || "light";
|
||
htmlEl.setAttribute("data-theme", savedTheme);
|
||
if (themeIcon)
|
||
themeIcon.textContent = savedTheme === "dark" ? "☀️" : "🌙";
|
||
if (themeToggle) {
|
||
themeToggle.addEventListener("click", () => {
|
||
const c = htmlEl.getAttribute("data-theme");
|
||
const n = c === "dark" ? "light" : "dark";
|
||
htmlEl.setAttribute("data-theme", n);
|
||
localStorage.setItem("theme", n);
|
||
if (themeIcon) themeIcon.textContent = n === "dark" ? "☀️" : "🌙";
|
||
});
|
||
}
|
||
const sidebarToggle = document.getElementById("sidebarToggle"),
|
||
sidebar = document.getElementById("sidebar"),
|
||
sidebarIcon = document.getElementById("sidebarIcon");
|
||
function updateSidebarIcon() {
|
||
if (!sidebarIcon) return;
|
||
sidebarIcon.textContent = sidebar.classList.contains("collapsed")
|
||
? "☰"
|
||
: "✕";
|
||
}
|
||
if (sidebarToggle) {
|
||
sidebarToggle.addEventListener("click", () => {
|
||
sidebar.classList.toggle("collapsed");
|
||
updateSidebarIcon();
|
||
});
|
||
}
|
||
updateSidebarIcon();
|
||
const sidebarBody = document.getElementById("sidebarBody");
|
||
function renderToolGroups() {
|
||
sidebarBody.innerHTML = "";
|
||
projectData.toolGroups.forEach((group) => {
|
||
const ge = document.createElement("div");
|
||
ge.className = "sidebar-group";
|
||
const he = document.createElement("div");
|
||
he.className = "sidebar-group-header";
|
||
he.innerHTML = `<div class="group-icon"><span>${group.icon || ""}</span><span>${group.title}</span></div><span class="group-arrow">▼</span>`;
|
||
const be = document.createElement("div");
|
||
be.className = "sidebar-group-body";
|
||
group.tools.forEach((tool) => {
|
||
const btn = document.createElement("button");
|
||
btn.className = "sidebar-btn";
|
||
btn.title = tool.description;
|
||
btn.innerHTML = `<span style="width:20px;text-align:center">${tool.icon || "🔧"}</span><span style="flex:1">${tool.title}</span>`;
|
||
btn.addEventListener("click", () => handleToolSelect(tool, group));
|
||
be.appendChild(btn);
|
||
});
|
||
he.addEventListener("click", () => ge.classList.toggle("collapsed"));
|
||
ge.appendChild(he);
|
||
ge.appendChild(be);
|
||
sidebarBody.appendChild(ge);
|
||
});
|
||
}
|
||
function openSettingsModal() {
|
||
document.getElementById("settingProjectTitle").value =
|
||
projectData.projectTitle;
|
||
document.getElementById("settingVersion").value = projectData.version;
|
||
document.getElementById("settingDescription").value =
|
||
projectData.description;
|
||
document.getElementById("settingDeveloper").value =
|
||
projectData.developer;
|
||
document.getElementById("settingCompany").value = projectData.company;
|
||
document.getElementById("settingWebhookUrl").value =
|
||
projectData.webhook.url;
|
||
document.getElementById("settingWebhookClientId").value =
|
||
projectData.webhook.clientId;
|
||
document.getElementById("settingWebhookEnabled").checked =
|
||
projectData.webhook.enabled;
|
||
WebhookManager.updateStatusUI();
|
||
ModalManager.open("settingsModal");
|
||
}
|
||
[
|
||
"settingWebhookUrl",
|
||
"settingWebhookClientId",
|
||
"settingWebhookEnabled",
|
||
].forEach((id) => {
|
||
document.getElementById(id).addEventListener("input", () => {
|
||
WebhookManager.setConfig({
|
||
url: document.getElementById("settingWebhookUrl").value,
|
||
clientId: document.getElementById("settingWebhookClientId").value,
|
||
enabled: document.getElementById("settingWebhookEnabled").checked,
|
||
});
|
||
});
|
||
});
|
||
document
|
||
.getElementById("saveSettingsBtn")
|
||
.addEventListener("click", () => {
|
||
WebhookManager.setConfig({
|
||
url: document.getElementById("settingWebhookUrl").value.trim(),
|
||
clientId: document
|
||
.getElementById("settingWebhookClientId")
|
||
.value.trim(),
|
||
enabled: document.getElementById("settingWebhookEnabled").checked,
|
||
});
|
||
ModalManager.close("settingsModal");
|
||
showToast("تنظیمات ذخیره شد", "success");
|
||
});
|
||
function updateProjectUI() {
|
||
document.title = projectData.projectTitle;
|
||
document.getElementById("headerTitle").textContent =
|
||
projectData.projectTitle;
|
||
document.getElementById("headerSubtitle").textContent =
|
||
projectData.description;
|
||
document.getElementById("appFooter").innerHTML =
|
||
`<div><span class="footer-brand">طراح و توسعهدهنده:</span> ${projectData.developer} <span style="margin:0 10px">|</span><span class="footer-brand">شرکت:</span> ${projectData.company}</div><div><span class="footer-brand">نسخه:</span> ${projectData.version}</div>`;
|
||
}
|
||
|
||
/* ==================== INIT ==================== */
|
||
ModalManager.init();
|
||
renderToolGroups();
|
||
updateProjectUI();
|
||
WebhookManager.updateStatusUI();
|
||
ToolbarManager.init();
|
||
SectionCollapseManager.init();
|
||
BoardRenderer.render();
|
||
HistoryManager.init();
|
||
AutosaveManager.checkRecovery();
|
||
|
||
window.addEventListener("beforeunload", () => {
|
||
AutosaveManager.save();
|
||
});
|
||
|
||
setTimeout(
|
||
() =>
|
||
showToast(
|
||
`${projectData.projectTitle} (نسخه ${projectData.version}) آماده است`,
|
||
"success",
|
||
),
|
||
500,
|
||
);
|
||
</script>
|
||
</body>
|
||
</html> |