This commit is contained in:
2026-09-20 17:32:00 +03:30
parent 036f7e0e02
commit 6c4a8233c7
6 changed files with 2467 additions and 170 deletions
BIN
View File
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
+226
View File
@@ -0,0 +1,226 @@
{
"environments": [
{
"id": "env-1789757606914",
"name": "کاوش - توسعه",
"description": "این محیط فقط برای تست عملیاتی توسعه دهندگان است",
"backgroundColor": "#3f94ee",
"textColor": "#ffffff"
},
{
"id": "env-1789757658261",
"name": "کاوش تست",
"description": "این محیط برای تست محصول توسط تیم های تضمین کیفیت و تحلیل است",
"backgroundColor": "#df3416",
"textColor": "#ffffff"
},
{
"id": "env-1789757730121",
"name": "کانون - قرمز",
"description": "محیطی برای تست محصولات در فضای کانون",
"backgroundColor": "#ff0000",
"textColor": "#ffffff"
},
{
"id": "env-1789757791901",
"name": "کانون - آبی",
"description": "محیط انتشار عمومی محصولات در کانون",
"backgroundColor": "#0000ff",
"textColor": "#ffffff"
},
{
"id": "env-1789757853333",
"name": "سازمان - قرمز",
"description": "محیطی برای تست محصولات در سازمان",
"backgroundColor": "#ff0000",
"textColor": "#ffffff"
},
{
"id": "env-1789757900213",
"name": "سازمان - آبی",
"description": "محیط انتشار عمومی محصولات در سازمان",
"backgroundColor": "#0000ff",
"textColor": "#ffffff"
}
],
"products": [
{
"id": "prod-1789906178306",
"title": "زیرساخت",
"description": "کلیه پروژه هایی که بعنوان زیرساخت برای سایر محصولات شرکت مورد استفاده قرار می گیرند ...",
"metadata": {
"createdAt": "2026-09-20T12:09:38.306Z",
"owner": "تیم آتنا",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789906450942",
"title": "کنترل سنتر",
"description": "مدیریت تنظیمات و ارائه اطلاعات پایه برای پروژه های مختلف ...",
"metadata": {
"createdAt": "2026-09-20T12:14:10.942Z",
"owner": "تیم آتنا",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789906580440",
"title": "مانا",
"description": "مدیریت ارجاع نقشه برداران ایران",
"metadata": {
"createdAt": "2026-09-20T12:16:20.440Z",
"owner": "تیم کاداستر",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789906612889",
"title": "مدام",
"description": "مدیریت داده های مکانی",
"metadata": {
"createdAt": "2026-09-20T12:16:52.889Z",
"owner": "تیم کاداستر",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789906649432",
"title": "مدیریت قرارداد ها",
"description": "",
"metadata": {
"createdAt": "2026-09-20T12:17:29.432Z",
"owner": "تیم کاداستر",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789907070679",
"title": "پیام رسان",
"description": "",
"metadata": {
"createdAt": "2026-09-20T12:24:30.679Z",
"owner": "تیم کاداستر",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789907660064",
"title": "تفکیک آپارتمانی - کاداستر",
"description": "",
"metadata": {
"createdAt": "2026-09-20T12:34:20.064Z",
"owner": "تیم کاداستر",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789907676416",
"title": "ابزار WebGIS",
"description": "",
"metadata": {
"createdAt": "2026-09-20T12:34:36.416Z",
"owner": "تیم کاداستر",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789907707481",
"title": "سرویس های املاک",
"description": "",
"metadata": {
"createdAt": "2026-09-20T12:35:07.481Z",
"owner": "تیم کاداستر",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789907737585",
"title": "ساماندهی",
"description": "",
"metadata": {
"createdAt": "2026-09-20T12:35:37.585Z",
"owner": "تیم املاک",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789907760114",
"title": "تعیین تکلیف",
"description": "",
"metadata": {
"createdAt": "2026-09-20T12:36:00.114Z",
"owner": "تیم املاک",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789907772450",
"title": "دفترچه ای",
"description": "",
"metadata": {
"createdAt": "2026-09-20T12:36:12.450Z",
"owner": "تیم املاک",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789907792986",
"title": "تفکیک آپارتمانی",
"description": "",
"metadata": {
"createdAt": "2026-09-20T12:36:32.986Z",
"owner": "تیم املاک",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789907850075",
"title": "ثبت اطلاعات سامانه املاک",
"description": "",
"metadata": {
"createdAt": "2026-09-20T12:37:30.075Z",
"owner": "تیم املاک",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789907867139",
"title": "املاک من",
"description": "",
"metadata": {
"createdAt": "2026-09-20T12:37:47.139Z",
"owner": "تیم املاک",
"customFields": {}
},
"projects": []
},
{
"id": "prod-1789907887203",
"title": "شروع از کارگزاری",
"description": "",
"metadata": {
"createdAt": "2026-09-20T12:38:07.203Z",
"owner": "تیم املاک",
"customFields": {}
},
"projects": []
}
],
"boards": [],
"boardTags": []
}
+279 -85
View File
@@ -3017,6 +3017,12 @@
<option value="">همه محیط‌ها</option>
</select>
</div>
<div class="filter-dropdown">
<span>👤</span>
<select class="filter-select" id="filterOwner">
<option value="">همه مالکان</option>
</select>
</div>
</div>
<div class="toolbar-stats">
<div class="stat-item">
@@ -3296,32 +3302,38 @@
</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" />
<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">
<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" />
<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" />
<label class="form-label">مالک پروژه</label>
<input type="text" class="form-input" id="newProjectOwner" placeholder="نام مسئول پروژه..." />
</div>
<div class="form-group">
<label class="form-label">آدرس مخزن</label><input type="text" class="form-input" id="newProjectRepo"
dir="ltr" />
<label class="form-label">زبان</label>
<input type="text" class="form-input" id="newProjectLanguage" />
</div>
</div>
<div class="form-group">
<label class="form-label">توضیحات</label><textarea class="form-textarea"
id="newProjectDescription"></textarea>
<label class="form-label">آدرس مخزن</label>
<input type="text" class="form-input" id="newProjectRepo" dir="ltr" />
</div>
<div class="form-group">
<label class="form-label">توضیحات</label>
<textarea class="form-textarea" id="newProjectDescription"></textarea>
</div>
</div>
<div class="modal-footer">
@@ -3338,35 +3350,42 @@
</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 />
<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" />
<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">
<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" />
<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" />
<label class="form-label">مالک پروژه</label>
<input type="text" class="form-input" id="editProjOwner" placeholder="نام مسئول پروژه..." />
</div>
<div class="form-group">
<label class="form-label">آدرس مخزن</label><input type="text" class="form-input" id="editProjRepo"
dir="ltr" />
<label class="form-label">زبان</label>
<input type="text" class="form-input" id="editProjLanguage" />
</div>
</div>
<div class="form-group">
<label class="form-label">توضیحات</label><textarea class="form-textarea" id="editProjDescription"></textarea>
<label class="form-label">آدرس مخزن</label>
<input type="text" class="form-input" id="editProjRepo" dir="ltr" />
</div>
<div class="form-group">
<label class="form-label">توضیحات</label>
<textarea class="form-textarea" id="editProjDescription"></textarea>
</div>
</div>
<div class="modal-footer">
@@ -3486,8 +3505,8 @@
</div>
</div>
<div class="modal-footer">
<div class="galaxy-stats" id="galaxyStats"></div>
<button class="btn btn-ghost" data-close-modal>بستن</button>
<div class="galaxy-stats" id="galaxyStats"></div>
</div>
</div>
</div>
@@ -3505,8 +3524,8 @@
<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 class="dashboard-footer-stats" id="dashboardFooterStats"></div>
</div>
</div>
</div>
@@ -3524,8 +3543,8 @@
<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 class="dashboard-footer-stats" id="depAnalysisFooterStats"></div>
</div>
</div>
</div>
@@ -3543,8 +3562,8 @@
<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 class="dashboard-footer-stats" id="deployAnalysisFooterStats"></div>
</div>
</div>
</div>
@@ -3581,8 +3600,8 @@
<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 class="dashboard-footer-stats" id="riskAnalysisFooterStats"></div>
</div>
</div>
</div>
@@ -4283,9 +4302,10 @@
};
/* ==================== TOOLBAR MANAGER ==================== */
const ToolbarManager = {
currentFilters: { query: "", typeFilter: "", envFilter: "" },
currentFilters: { query: "", typeFilter: "", envFilter: "", ownerFilter: "" },
init() {
this.populateEnvironmentFilter();
this.populateOwnerFilter();
this.bindEvents();
this.updateStats();
},
@@ -4298,10 +4318,28 @@
.map((e) => `<option value="${e.id}">${e.name}</option>`)
.join("");
},
populateOwnerFilter() {
const s = document.getElementById("filterOwner");
if (!s) return;
const owners = new Set();
(projectData.appData.products || []).forEach((p) => {
const owner = p.metadata?.owner?.trim();
if (owner) owners.add(owner);
});
const sortedOwners = Array.from(owners).sort((a, b) =>
a.localeCompare(b, "fa"),
);
s.innerHTML =
'<option value="">همه مالکان</option>' +
sortedOwners
.map((o) => `<option value="${o}">${o}</option>`)
.join("");
},
bindEvents() {
const si = document.getElementById("toolbarSearch"),
ft = document.getElementById("filterType"),
fe = document.getElementById("filterEnvironment");
fe = document.getElementById("filterEnvironment"),
fo = document.getElementById("filterOwner");
if (si)
si.addEventListener("input", () => {
this.applyFilters();
@@ -4317,6 +4355,11 @@
this.applyFilters();
refreshUI();
});
if (fo)
fo.addEventListener("change", () => {
this.applyFilters();
refreshUI();
});
},
applyFilters() {
this.currentFilters = {
@@ -4324,8 +4367,8 @@
.trim()
.toLowerCase(),
typeFilter: document.getElementById("filterType")?.value || "",
envFilter:
document.getElementById("filterEnvironment")?.value || "",
envFilter: document.getElementById("filterEnvironment")?.value || "",
ownerFilter: document.getElementById("filterOwner")?.value || "",
};
this.updateStats();
},
@@ -4350,7 +4393,15 @@
},
shouldShowProduct(product) {
const f = this.currentFilters;
if (!f.query && !f.typeFilter && !f.envFilter && !f.ownerFilter) return true;
if (f.ownerFilter) {
const prodOwner = product.metadata?.owner?.trim();
if (prodOwner !== f.ownerFilter) return false;
}
if (!f.query && !f.typeFilter && !f.envFilter) return true;
return (product.projects || []).some((pr) => {
const mq =
!f.query ||
@@ -4362,6 +4413,7 @@
(pr.deployments || [])
.filter((d) => d.status === "active")
.some((d) => d.environmentId === f.envFilter);
return mq && mt && me;
});
},
@@ -4891,6 +4943,7 @@
updateProjectUI();
renderToolGroups();
ToolbarManager.populateEnvironmentFilter();
ToolbarManager.populateOwnerFilter();
refreshUI();
showToast("📂 بارگذاری شد", "success");
} catch (err) {
@@ -4913,6 +4966,7 @@
};
AutosaveManager.clear();
ToolbarManager.populateEnvironmentFilter();
ToolbarManager.populateOwnerFilter();
refreshUI();
showToast("🗑️ پاکسازی شد", "success");
},
@@ -5132,7 +5186,13 @@
return null;
},
getActiveDeployments(proj) {
return (proj.deployments || []).filter((d) => d.status === "active");
return (proj.deployments || [])
.filter((d) => d.status === "active")
.sort((a, b) => {
const dateA = a.deploymentDate ? new Date(a.deploymentDate).getTime() : 0;
const dateB = b.deploymentDate ? new Date(b.deploymentDate).getTime() : 0;
return dateB - dateA;
});
},
getEnvironmentsWithoutDeployment(proj) {
const allEnvIds = projectData.appData.environments.map((e) => e.id);
@@ -5154,7 +5214,9 @@
(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 {
@@ -5163,21 +5225,43 @@
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
const archivedDeploys = (pr.deployments || [])
.filter((d) => d.status === "archived")
.sort((a, b) => {
const dateA = a.archivedAt ? new Date(a.archivedAt).getTime() : 0;
const dateB = b.archivedAt ? new Date(b.archivedAt).getTime() : 0;
return dateB - dateA;
});
const hasEnvs = projectData.appData.environments.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">`;
if (pr.metadata?.owner) {
html += `<div class="detail-info-item"><span class="detail-info-label">👤 مالک</span><span class="detail-info-value">${pr.metadata.owner}</span></div>`;
}
if (pr.metadata?.framework) {
html += `<div class="detail-info-item"><span class="detail-info-label">فریم‌ورک</span><span class="detail-info-value">${pr.metadata.framework}</span></div>`;
}
if (pr.metadata?.language) {
html += `<div class="detail-info-item"><span class="detail-info-label">زبان</span><span class="detail-info-value">${pr.metadata.language}</span></div>`;
}
if (deps.length > 0) {
html += `<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>`;
.join("، ")}</span></div>`;
}
html += `</div>`;
html += `<div class="deployment-section"><div class="deployment-header"><span class="deployment-header-title">🚀 استقرارهای فعال (${deploys.length})</span>${hasEnvs ? `<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>';
html += '<div class="empty-deployments">هنوز استقرار ندارد</div>';
} else {
html += '<div class="deployment-list">';
deploys.forEach((d) => {
@@ -5186,13 +5270,22 @@
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-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">`;
html += `<button class="btn btn-ghost btn-xs" onclick="ProductManager.openEditDeploymentModal('${prod.id}','${pr.id}','${d.id}')" title="ویرایش">✏️</button>`;
html += `<button class="btn btn-xs" style="background:#dd6b20;color:#fff" onclick="ProductManager.archiveDeployment('${prod.id}','${pr.id}','${d.id}')" title="انتقال به تاریخچه">📦 آرشیو</button>`;
html += `<button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.id}')" title="حذف کامل">🗑️</button>`;
html += `</div>`;
if (d.releaseNotes) {
html += `<div class="deployment-notes">📝 ${d.releaseNotes}</div>`;
if (
d.requiredDeployments &&
d.requiredDeployments.length > 0
) {
}
if (d.requiredDeployments && d.requiredDeployments.length > 0) {
html += `<div class="deployment-req">🔗 نیازمندی‌ها: ${d.requiredDeployments
.map((rd) => {
const rp = this.findProjectById(rd.projectId);
@@ -5207,14 +5300,62 @@
})
.join("، ")}</div>`;
}
html += `</div>`;
});
html += "</div>";
}
html += `<div class="deployment-section" style="margin-top:12px;border-top:2px dashed var(--border)">`;
html += `<div class="deployment-header">`;
html += `<span class="deployment-header-title">📜 تاریخچه استقرارها (${archivedDeploys.length})</span>`;
if (archivedDeploys.length > 0) {
html += `<button class="btn btn-ghost btn-xs" onclick="ProductManager.toggleArchiveVisibility('${pr.id}')" id="archiveToggle-${pr.id}">👁️ نمایش/مخفی</button>`;
}
html += `</div>`;
if (archivedDeploys.length === 0) {
html += `<div class="empty-deployments" style="opacity:0.6">هنوز استقراری آرشیو نشده است</div>`;
} else {
html += `<div class="deployment-list" id="archiveList-${pr.id}" style="display:none">`;
archivedDeploys.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")
: "—";
const archivedDate = d.archivedAt
? new Date(d.archivedAt).toLocaleString("fa-IR")
: "—";
html += `<div class="deployment-item" style="opacity:0.75;background:var(--border-light)">`;
html += `<div class="deployment-item-info">`;
html += `<span class="deployment-env-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor};opacity:0.7">${en}</span>`;
html += `<span class="deployment-version" style="opacity:0.7">v${d.version}</span>`;
html += `<span class="deployment-date">📅 ${fd}</span>`;
html += `<span class="deployment-date" style="color:var(--warning)">📦 آرشیو: ${archivedDate}</span>`;
html += `</div>`;
html += `<div class="deployment-actions">`;
html += `<button class="btn btn-xs" style="background:#3182ce;color:#fff" onclick="ProductManager.restoreDeployment('${prod.id}','${pr.id}','${d.id}')" title="بازگرداندن به لیست فعال">↩️ بازگردانی</button>`;
html += `<button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.id}')" title="حذف کامل">🗑️</button>`;
html += `</div>`;
if (d.releaseNotes) {
html += `<div class="deployment-notes" style="opacity:0.7">📝 ${d.releaseNotes}</div>`;
}
html += `</div>`;
});
html += `</div>`;
}
html += `</div>`;
html += `</div></div>`;
});
html += "</div>";
}
html += `</div></div>`;
document.getElementById("projectDetailBody").innerHTML = html;
ModalManager.open("projectDetailModal");
@@ -5259,6 +5400,7 @@
});
ModalManager.close("addProductModal");
refreshUI();
ToolbarManager.populateOwnerFilter();
showToast(`"${title}" ایجاد شد`, "success");
},
openDependencyManager(productId, projId) {
@@ -5449,6 +5591,7 @@
prod.metadata.updatedAt = new Date().toISOString();
ModalManager.close("productEditModal");
refreshUI();
ToolbarManager.populateOwnerFilter();
this.showDetail(pid);
showToast(`"${prod.title}" ویرایش شد`, "success");
},
@@ -5460,6 +5603,7 @@
document.getElementById("newProjectTitle").value = "";
document.getElementById("newProjectType").value = "";
document.getElementById("newProjectFramework").value = "";
document.getElementById("newProjectOwner").value = ""; // 🆕
document.getElementById("newProjectLanguage").value = "";
document.getElementById("newProjectRepo").value = "";
document.getElementById("newProjectDescription").value = "";
@@ -5482,20 +5626,18 @@
const np = {
id: `proj-${Date.now()}`,
title,
description:
document.getElementById("newProjectDescription").value.trim() ||
"",
description: document.getElementById("newProjectDescription").value.trim() || "",
type,
repositoryUrl: document
.getElementById("newProjectRepo")
.value.trim(),
metadata: { createdAt: new Date().toISOString(), customFields: {} },
repositoryUrl: document.getElementById("newProjectRepo").value.trim(),
metadata: {
createdAt: new Date().toISOString(),
owner: document.getElementById("newProjectOwner").value.trim() || "", // 🆕
customFields: {}
},
dependencies: [],
deployments: [],
};
const fw = document
.getElementById("newProjectFramework")
.value.trim();
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;
@@ -5506,9 +5648,7 @@
showToast(`"${title}" ایجاد شد`, "success");
},
openProjectEditModal(productId, projId) {
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
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;
@@ -5517,14 +5657,11 @@
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 || "";
document.getElementById("editProjFramework").value = pr.metadata?.framework || "";
document.getElementById("editProjOwner").value = pr.metadata?.owner || ""; // 🆕
document.getElementById("editProjLanguage").value = pr.metadata?.language || "";
document.getElementById("editProjRepo").value = pr.repositoryUrl || "";
document.getElementById("editProjDescription").value = pr.description || "";
ModalManager.open("editProjectModal");
},
saveProjectEdit() {
@@ -5545,15 +5682,15 @@
AutosaveManager.scheduleSave();
pr.title = nt;
pr.type = nty;
pr.description =
document.getElementById("editProjDescription").value.trim() || "";
pr.repositoryUrl = document
.getElementById("editProjRepo")
.value.trim();
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 owner = document.getElementById("editProjOwner").value.trim(); // 🆕
if (owner) pr.metadata.owner = owner;
else delete pr.metadata.owner;
const lg = document.getElementById("editProjLanguage").value.trim();
if (lg) pr.metadata.language = lg;
else delete pr.metadata.language;
@@ -5565,36 +5702,39 @@
},
openAddDeploymentModal(productId, projId) {
if (!productId || !projId) return;
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
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");
const allEnvs = projectData.appData.environments;
if (allEnvs.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),
);
const envs = allEnvs;
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");
},
@@ -5762,7 +5902,26 @@
this.showDetail(pid);
},
removeDeployment(productId, projId, depId) {
if (!confirm("حذف این استقرار؟")) return;
if (!confirm("⚠️ حذف کامل این استقرار؟\n\nاین عمل غیرقابل بازگشت است.\nاگر می‌خواهید فقط از لیست فعال خارج شود، از دکمه «آرشیو» استفاده کنید.")) return;
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
if (!prod) return;
const pr = prod.projects.find((p) => p.id === projId);
if (!pr) return;
HistoryManager.pushState();
AutosaveManager.scheduleSave();
pr.deployments = (pr.deployments || []).filter((d) => d.id !== depId);
refreshUI();
this.showDetail(productId);
showToast("🗑️ استقرار به‌طور کامل حذف شد", "warning");
},
archiveDeployment(productId, projId, depId) {
if (!confirm("📦 انتقال این استقرار به تاریخچه؟\n\nاستقرار از لیست فعال خارج شده و در بخش تاریخچه نگهداری می‌شود.")) return;
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
@@ -5771,12 +5930,16 @@
if (!pr) return;
const dep = (pr.deployments || []).find((d) => d.id === depId);
if (!dep) return;
HistoryManager.pushState();
AutosaveManager.scheduleSave();
dep.status = "removed";
dep.status = "archived";
dep.archivedAt = new Date().toISOString();
refreshUI();
this.showDetail(productId);
showToast("استقرار حذف شد", "warning");
showToast("📦 استقرار به تاریخچه منتقل شد", "success");
},
removeProduct(productId) {
const prod = projectData.appData.products.find(
@@ -5790,6 +5953,7 @@
(p) => p.id !== productId,
);
refreshUI();
ToolbarManager.populateOwnerFilter();
ModalManager.close("projectDetailModal");
showToast(`"${prod.title}" حذف شد`, "warning");
},
@@ -5809,6 +5973,35 @@
this.showDetail(productId);
showToast(`"${pr.title}" حذف شد`, "warning");
},
toggleArchiveVisibility(projId) {
const list = document.getElementById(`archiveList-${projId}`);
if (!list) return;
const isHidden = list.style.display === "none";
list.style.display = isHidden ? "flex" : "none";
},
restoreDeployment(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();
// ✅ بازگرداندن به وضعیت active
dep.status = "active";
delete dep.archivedAt;
refreshUI();
this.showDetail(productId);
showToast("↩️ استقرار به لیست فعال بازگردانده شد", "success");
},
};
/* ==================== DASHBOARD MANAGER ==================== */
const DashboardManager = {
@@ -6710,6 +6903,7 @@
updateProjectUI();
renderToolGroups();
ToolbarManager.populateEnvironmentFilter();
ToolbarManager.populateOwnerFilter();
refreshUI();
if (ReleaseBoardsManager) {
ReleaseBoardsManager.render(true);
+368 -85
View File
@@ -3017,6 +3017,12 @@
<option value="">همه محیط‌ها</option>
</select>
</div>
<div class="filter-dropdown">
<span>👤</span>
<select class="filter-select" id="filterOwner">
<option value="">همه مالکان</option>
</select>
</div>
</div>
<div class="toolbar-stats">
<div class="stat-item">
@@ -3296,32 +3302,38 @@
</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" />
<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">
<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" />
<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" />
<label class="form-label">مالک پروژه</label>
<input type="text" class="form-input" id="newProjectOwner" placeholder="نام مسئول پروژه..." />
</div>
<div class="form-group">
<label class="form-label">آدرس مخزن</label><input type="text" class="form-input" id="newProjectRepo"
dir="ltr" />
<label class="form-label">زبان</label>
<input type="text" class="form-input" id="newProjectLanguage" />
</div>
</div>
<div class="form-group">
<label class="form-label">توضیحات</label><textarea class="form-textarea"
id="newProjectDescription"></textarea>
<label class="form-label">آدرس مخزن</label>
<input type="text" class="form-input" id="newProjectRepo" dir="ltr" />
</div>
<div class="form-group">
<label class="form-label">توضیحات</label>
<textarea class="form-textarea" id="newProjectDescription"></textarea>
</div>
</div>
<div class="modal-footer">
@@ -3338,35 +3350,42 @@
</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 />
<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" />
<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">
<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" />
<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" />
<label class="form-label">مالک پروژه</label>
<input type="text" class="form-input" id="editProjOwner" placeholder="نام مسئول پروژه..." />
</div>
<div class="form-group">
<label class="form-label">آدرس مخزن</label><input type="text" class="form-input" id="editProjRepo"
dir="ltr" />
<label class="form-label">زبان</label>
<input type="text" class="form-input" id="editProjLanguage" />
</div>
</div>
<div class="form-group">
<label class="form-label">توضیحات</label><textarea class="form-textarea" id="editProjDescription"></textarea>
<label class="form-label">آدرس مخزن</label>
<input type="text" class="form-input" id="editProjRepo" dir="ltr" />
</div>
<div class="form-group">
<label class="form-label">توضیحات</label>
<textarea class="form-textarea" id="editProjDescription"></textarea>
</div>
</div>
<div class="modal-footer">
@@ -4283,9 +4302,10 @@
};
/* ==================== TOOLBAR MANAGER ==================== */
const ToolbarManager = {
currentFilters: { query: "", typeFilter: "", envFilter: "" },
currentFilters: { query: "", typeFilter: "", envFilter: "", ownerFilter: "" },
init() {
this.populateEnvironmentFilter();
this.populateOwnerFilter();
this.bindEvents();
this.updateStats();
},
@@ -4298,10 +4318,28 @@
.map((e) => `<option value="${e.id}">${e.name}</option>`)
.join("");
},
populateOwnerFilter() {
const s = document.getElementById("filterOwner");
if (!s) return;
const owners = new Set();
(projectData.appData.products || []).forEach((p) => {
const owner = p.metadata?.owner?.trim();
if (owner) owners.add(owner);
});
const sortedOwners = Array.from(owners).sort((a, b) =>
a.localeCompare(b, "fa"),
);
s.innerHTML =
'<option value="">همه مالکان</option>' +
sortedOwners
.map((o) => `<option value="${o}">${o}</option>`)
.join("");
},
bindEvents() {
const si = document.getElementById("toolbarSearch"),
ft = document.getElementById("filterType"),
fe = document.getElementById("filterEnvironment");
fe = document.getElementById("filterEnvironment"),
fo = document.getElementById("filterOwner");
if (si)
si.addEventListener("input", () => {
this.applyFilters();
@@ -4317,6 +4355,11 @@
this.applyFilters();
refreshUI();
});
if (fo)
fo.addEventListener("change", () => {
this.applyFilters();
refreshUI();
});
},
applyFilters() {
this.currentFilters = {
@@ -4324,8 +4367,8 @@
.trim()
.toLowerCase(),
typeFilter: document.getElementById("filterType")?.value || "",
envFilter:
document.getElementById("filterEnvironment")?.value || "",
envFilter: document.getElementById("filterEnvironment")?.value || "",
ownerFilter: document.getElementById("filterOwner")?.value || "",
};
this.updateStats();
},
@@ -4350,7 +4393,15 @@
},
shouldShowProduct(product) {
const f = this.currentFilters;
if (!f.query && !f.typeFilter && !f.envFilter && !f.ownerFilter) return true;
if (f.ownerFilter) {
const prodOwner = product.metadata?.owner?.trim();
if (prodOwner !== f.ownerFilter) return false;
}
if (!f.query && !f.typeFilter && !f.envFilter) return true;
return (product.projects || []).some((pr) => {
const mq =
!f.query ||
@@ -4362,6 +4413,7 @@
(pr.deployments || [])
.filter((d) => d.status === "active")
.some((d) => d.environmentId === f.envFilter);
return mq && mt && me;
});
},
@@ -4891,6 +4943,7 @@
updateProjectUI();
renderToolGroups();
ToolbarManager.populateEnvironmentFilter();
ToolbarManager.populateOwnerFilter();
refreshUI();
showToast("📂 بارگذاری شد", "success");
} catch (err) {
@@ -4913,6 +4966,7 @@
};
AutosaveManager.clear();
ToolbarManager.populateEnvironmentFilter();
ToolbarManager.populateOwnerFilter();
refreshUI();
showToast("🗑️ پاکسازی شد", "success");
},
@@ -5132,7 +5186,13 @@
return null;
},
getActiveDeployments(proj) {
return (proj.deployments || []).filter((d) => d.status === "active");
return (proj.deployments || [])
.filter((d) => d.status === "active")
.sort((a, b) => {
const dateA = a.deploymentDate ? new Date(a.deploymentDate).getTime() : 0;
const dateB = b.deploymentDate ? new Date(b.deploymentDate).getTime() : 0;
return dateB - dateA;
});
},
getEnvironmentsWithoutDeployment(proj) {
const allEnvIds = projectData.appData.environments.map((e) => e.id);
@@ -5154,7 +5214,9 @@
(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 {
@@ -5163,21 +5225,43 @@
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
const archivedDeploys = (pr.deployments || [])
.filter((d) => d.status === "archived")
.sort((a, b) => {
const dateA = a.archivedAt ? new Date(a.archivedAt).getTime() : 0;
const dateB = b.archivedAt ? new Date(b.archivedAt).getTime() : 0;
return dateB - dateA;
});
const hasEnvs = projectData.appData.environments.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">`;
if (pr.metadata?.owner) {
html += `<div class="detail-info-item"><span class="detail-info-label">👤 مالک</span><span class="detail-info-value">${pr.metadata.owner}</span></div>`;
}
if (pr.metadata?.framework) {
html += `<div class="detail-info-item"><span class="detail-info-label">فریم‌ورک</span><span class="detail-info-value">${pr.metadata.framework}</span></div>`;
}
if (pr.metadata?.language) {
html += `<div class="detail-info-item"><span class="detail-info-label">زبان</span><span class="detail-info-value">${pr.metadata.language}</span></div>`;
}
if (deps.length > 0) {
html += `<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>`;
.join("، ")}</span></div>`;
}
html += `</div>`;
html += `<div class="deployment-section"><div class="deployment-header"><span class="deployment-header-title">🚀 استقرارهای فعال (${deploys.length})</span>${hasEnvs ? `<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>';
html += '<div class="empty-deployments">هنوز استقرار ندارد</div>';
} else {
html += '<div class="deployment-list">';
deploys.forEach((d) => {
@@ -5186,13 +5270,22 @@
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-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">`;
html += `<button class="btn btn-ghost btn-xs" onclick="ProductManager.openEditDeploymentModal('${prod.id}','${pr.id}','${d.id}')" title="ویرایش">✏️</button>`;
html += `<button class="btn btn-xs" style="background:#dd6b20;color:#fff" onclick="ProductManager.archiveDeployment('${prod.id}','${pr.id}','${d.id}')" title="انتقال به تاریخچه">📦 آرشیو</button>`;
html += `<button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.id}')" title="حذف کامل">🗑️</button>`;
html += `</div>`;
if (d.releaseNotes) {
html += `<div class="deployment-notes">📝 ${d.releaseNotes}</div>`;
if (
d.requiredDeployments &&
d.requiredDeployments.length > 0
) {
}
if (d.requiredDeployments && d.requiredDeployments.length > 0) {
html += `<div class="deployment-req">🔗 نیازمندی‌ها: ${d.requiredDeployments
.map((rd) => {
const rp = this.findProjectById(rd.projectId);
@@ -5207,14 +5300,62 @@
})
.join("، ")}</div>`;
}
html += `</div>`;
});
html += "</div>";
}
html += `<div class="deployment-section" style="margin-top:12px;border-top:2px dashed var(--border)">`;
html += `<div class="deployment-header">`;
html += `<span class="deployment-header-title">📜 تاریخچه استقرارها (${archivedDeploys.length})</span>`;
if (archivedDeploys.length > 0) {
html += `<button class="btn btn-ghost btn-xs" onclick="ProductManager.toggleArchiveVisibility('${pr.id}')" id="archiveToggle-${pr.id}">👁️ نمایش/مخفی</button>`;
}
html += `</div>`;
if (archivedDeploys.length === 0) {
html += `<div class="empty-deployments" style="opacity:0.6">هنوز استقراری آرشیو نشده است</div>`;
} else {
html += `<div class="deployment-list" id="archiveList-${pr.id}" style="display:none">`;
archivedDeploys.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")
: "—";
const archivedDate = d.archivedAt
? new Date(d.archivedAt).toLocaleString("fa-IR")
: "—";
html += `<div class="deployment-item" style="opacity:0.75;background:var(--border-light)">`;
html += `<div class="deployment-item-info">`;
html += `<span class="deployment-env-tag" style="background-color:${cl.backgroundColor};color:${cl.textColor};opacity:0.7">${en}</span>`;
html += `<span class="deployment-version" style="opacity:0.7">v${d.version}</span>`;
html += `<span class="deployment-date">📅 ${fd}</span>`;
html += `<span class="deployment-date" style="color:var(--warning)">📦 آرشیو: ${archivedDate}</span>`;
html += `</div>`;
html += `<div class="deployment-actions">`;
html += `<button class="btn btn-xs" style="background:#3182ce;color:#fff" onclick="ProductManager.restoreDeployment('${prod.id}','${pr.id}','${d.id}')" title="بازگرداندن به لیست فعال">↩️ بازگردانی</button>`;
html += `<button class="btn btn-danger btn-xs" onclick="ProductManager.removeDeployment('${prod.id}','${pr.id}','${d.id}')" title="حذف کامل">🗑️</button>`;
html += `</div>`;
if (d.releaseNotes) {
html += `<div class="deployment-notes" style="opacity:0.7">📝 ${d.releaseNotes}</div>`;
}
html += `</div>`;
});
html += `</div>`;
}
html += `</div>`;
html += `</div></div>`;
});
html += "</div>";
}
html += `</div></div>`;
document.getElementById("projectDetailBody").innerHTML = html;
ModalManager.open("projectDetailModal");
@@ -5259,6 +5400,7 @@
});
ModalManager.close("addProductModal");
refreshUI();
ToolbarManager.populateOwnerFilter();
showToast(`"${title}" ایجاد شد`, "success");
},
openDependencyManager(productId, projId) {
@@ -5449,6 +5591,7 @@
prod.metadata.updatedAt = new Date().toISOString();
ModalManager.close("productEditModal");
refreshUI();
ToolbarManager.populateOwnerFilter();
this.showDetail(pid);
showToast(`"${prod.title}" ویرایش شد`, "success");
},
@@ -5460,6 +5603,7 @@
document.getElementById("newProjectTitle").value = "";
document.getElementById("newProjectType").value = "";
document.getElementById("newProjectFramework").value = "";
document.getElementById("newProjectOwner").value = ""; // 🆕
document.getElementById("newProjectLanguage").value = "";
document.getElementById("newProjectRepo").value = "";
document.getElementById("newProjectDescription").value = "";
@@ -5482,20 +5626,18 @@
const np = {
id: `proj-${Date.now()}`,
title,
description:
document.getElementById("newProjectDescription").value.trim() ||
"",
description: document.getElementById("newProjectDescription").value.trim() || "",
type,
repositoryUrl: document
.getElementById("newProjectRepo")
.value.trim(),
metadata: { createdAt: new Date().toISOString(), customFields: {} },
repositoryUrl: document.getElementById("newProjectRepo").value.trim(),
metadata: {
createdAt: new Date().toISOString(),
owner: document.getElementById("newProjectOwner").value.trim() || "", // 🆕
customFields: {}
},
dependencies: [],
deployments: [],
};
const fw = document
.getElementById("newProjectFramework")
.value.trim();
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;
@@ -5506,9 +5648,7 @@
showToast(`"${title}" ایجاد شد`, "success");
},
openProjectEditModal(productId, projId) {
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
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;
@@ -5517,14 +5657,11 @@
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 || "";
document.getElementById("editProjFramework").value = pr.metadata?.framework || "";
document.getElementById("editProjOwner").value = pr.metadata?.owner || ""; // 🆕
document.getElementById("editProjLanguage").value = pr.metadata?.language || "";
document.getElementById("editProjRepo").value = pr.repositoryUrl || "";
document.getElementById("editProjDescription").value = pr.description || "";
ModalManager.open("editProjectModal");
},
saveProjectEdit() {
@@ -5545,15 +5682,15 @@
AutosaveManager.scheduleSave();
pr.title = nt;
pr.type = nty;
pr.description =
document.getElementById("editProjDescription").value.trim() || "";
pr.repositoryUrl = document
.getElementById("editProjRepo")
.value.trim();
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 owner = document.getElementById("editProjOwner").value.trim(); // 🆕
if (owner) pr.metadata.owner = owner;
else delete pr.metadata.owner;
const lg = document.getElementById("editProjLanguage").value.trim();
if (lg) pr.metadata.language = lg;
else delete pr.metadata.language;
@@ -5565,36 +5702,39 @@
},
openAddDeploymentModal(productId, projId) {
if (!productId || !projId) return;
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
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");
const allEnvs = projectData.appData.environments;
if (allEnvs.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),
);
const envs = allEnvs;
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");
},
@@ -5762,7 +5902,26 @@
this.showDetail(pid);
},
removeDeployment(productId, projId, depId) {
if (!confirm("حذف این استقرار؟")) return;
if (!confirm("⚠️ حذف کامل این استقرار؟\n\nاین عمل غیرقابل بازگشت است.\nاگر می‌خواهید فقط از لیست فعال خارج شود، از دکمه «آرشیو» استفاده کنید.")) return;
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
if (!prod) return;
const pr = prod.projects.find((p) => p.id === projId);
if (!pr) return;
HistoryManager.pushState();
AutosaveManager.scheduleSave();
pr.deployments = (pr.deployments || []).filter((d) => d.id !== depId);
refreshUI();
this.showDetail(productId);
showToast("🗑️ استقرار به‌طور کامل حذف شد", "warning");
},
archiveDeployment(productId, projId, depId) {
if (!confirm("📦 انتقال این استقرار به تاریخچه؟\n\nاستقرار از لیست فعال خارج شده و در بخش تاریخچه نگهداری می‌شود.")) return;
const prod = projectData.appData.products.find(
(p) => p.id === productId,
);
@@ -5771,12 +5930,16 @@
if (!pr) return;
const dep = (pr.deployments || []).find((d) => d.id === depId);
if (!dep) return;
HistoryManager.pushState();
AutosaveManager.scheduleSave();
dep.status = "removed";
dep.status = "archived";
dep.archivedAt = new Date().toISOString();
refreshUI();
this.showDetail(productId);
showToast("استقرار حذف شد", "warning");
showToast("📦 استقرار به تاریخچه منتقل شد", "success");
},
removeProduct(productId) {
const prod = projectData.appData.products.find(
@@ -5790,6 +5953,7 @@
(p) => p.id !== productId,
);
refreshUI();
ToolbarManager.populateOwnerFilter();
ModalManager.close("projectDetailModal");
showToast(`"${prod.title}" حذف شد`, "warning");
},
@@ -5809,6 +5973,35 @@
this.showDetail(productId);
showToast(`"${pr.title}" حذف شد`, "warning");
},
toggleArchiveVisibility(projId) {
const list = document.getElementById(`archiveList-${projId}`);
if (!list) return;
const isHidden = list.style.display === "none";
list.style.display = isHidden ? "flex" : "none";
},
restoreDeployment(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();
// ✅ بازگرداندن به وضعیت active
dep.status = "active";
delete dep.archivedAt;
refreshUI();
this.showDetail(productId);
showToast("↩️ استقرار به لیست فعال بازگردانده شد", "success");
},
};
/* ==================== DASHBOARD MANAGER ==================== */
const DashboardManager = {
@@ -6340,6 +6533,35 @@
const fm = gf.append("feMerge");
fm.append("feMergeNode").attr("in", "blur");
fm.append("feMergeNode").attr("in", "SourceGraphic");
defs.append("marker")
.attr("id", "arrowDependency")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10) // نقطه مرجع در نوک فلش
.attr("refY", 0)
.attr("markerWidth", 8) // عرض فلش
.attr("markerHeight", 8) // ارتفاع فلش
.attr("orient", "auto")
.attr("markerUnits", "userSpaceOnUse")
.append("path")
.attr("d", "M0,-5L10,0L0,5") // شکل مثلثی فلش
.attr("fill", "#e0c789"); // رنگ طلایی هماهنگ با لینک وابستگی
// 🆕 Marker برای لینک‌های ownership (اختیاری - بدون فلش یا فلش کوچکتر)
defs.append("marker")
.attr("id", "arrowOwnership")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.attr("markerUnits", "userSpaceOnUse")
.append("path")
.attr("d", "M0,-4L10,0L0,4")
.attr("fill", "#4a5568")
.attr("opacity", 0.7);
this.zoomGroup = svg.append("g");
this.zoomBehavior = d3
.zoom()
@@ -6370,10 +6592,38 @@
d.type === "dependency" ? "#e0c789" : "#4a5568",
)
.attr("stroke-width", (d) => (d.type === "dependency" ? 2 : 1))
.attr("stroke-opacity", 0.6)
.attr("stroke-opacity", (d) => (d.type === "dependency" ? 0.85 : 0.4))
.attr("stroke-dasharray", (d) =>
d.type === "dependency" ? "5,3" : "none",
)
.attr("marker-end", (d) =>
d.type === "dependency" ? "url(#arrowDependency)" : "url(#arrowOwnership)"
);
this.linkElements
.style("cursor", "pointer")
.on("mouseover", function (e, d) {
d3.select(this)
.attr("stroke-opacity", 1)
.attr("stroke-width", d.type === "dependency" ? 3 : 2);
const sourceNode = this.__data__.source;
const targetNode = this.__data__.target;
const sourceLabel = sourceNode.label || sourceNode.id;
const targetLabel = targetNode.label || targetNode.id;
const tooltipText = d.type === "dependency"
? `🔗 ${sourceLabel} → ${targetLabel}`
: `📦 ${targetLabel} شامل ${sourceLabel}`;
showD3Tooltip(e, tooltipText);
})
.on("mousemove", (e) => showD3Tooltip(e, ""))
.on("mouseout", function (e, d) {
d3.select(this)
.attr("stroke-opacity", d.type === "dependency" ? 0.85 : 0.4)
.attr("stroke-width", d.type === "dependency" ? 2 : 1);
hideD3Tooltip();
});
const node = this.zoomGroup
.append("g")
.selectAll("g")
@@ -6437,14 +6687,46 @@
this.toggleCollapseProduct(d.id);
}
});
const getEdgePoint = (source, target, targetRadius) => {
const dx = target.x - source.x;
const dy = target.y - source.y;
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
const offset = targetRadius + 10;
return {
x: target.x - (dx / distance) * offset,
y: target.y - (dy / distance) * offset,
};
};
sim.on("tick", () => {
this.linkElements
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
.attr("x1", (d) => {
// نقطه شروع: روی مرز گره منبع
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
return d.source.x + (dx / distance) * d.source.radius;
})
.attr("y1", (d) => {
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
return d.source.y + (dy / distance) * d.source.radius;
})
.attr("x2", (d) => {
// نقطه پایان: روی مرز گره هدف (با offset برای فلش)
const edge = getEdgePoint(d.source, d.target, d.target.radius);
return edge.x;
})
.attr("y2", (d) => {
const edge = getEdgePoint(d.source, d.target, d.target.radius);
return edge.y;
});
node.attr("transform", (d) => `translate(${d.x},${d.y})`);
});
this.renderLegend();
this.updateStats(nodes, links);
this.setupSearch();
@@ -6710,6 +6992,7 @@
updateProjectUI();
renderToolGroups();
ToolbarManager.populateEnvironmentFilter();
ToolbarManager.populateOwnerFilter();
refreshUI();
if (ReleaseBoardsManager) {
ReleaseBoardsManager.render(true);