585 lines
26 KiB
HTML
585 lines
26 KiB
HTML
{{define "title"}}Мои проекты - QuoteForge{{end}}
|
||
|
||
{{define "content"}}
|
||
<div class="space-y-4">
|
||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||
<h1 class="text-2xl font-bold">Мои проекты</h1>
|
||
<div class="flex items-center gap-2">
|
||
<a href="/configs" class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">
|
||
Все конфигурации
|
||
</a>
|
||
<button onclick="openCreateProjectModal()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
||
+ Новый проект
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="inline-flex rounded-lg border border-gray-200 overflow-hidden">
|
||
<button id="status-active-btn" onclick="setStatus('active')" class="px-4 py-2 text-sm font-medium bg-blue-600 text-white">Активные</button>
|
||
<button id="status-archived-btn" onclick="setStatus('archived')" class="px-4 py-2 text-sm font-medium bg-white text-gray-700 hover:bg-gray-50 border-l border-gray-200">Архив</button>
|
||
</div>
|
||
|
||
<div class="max-w-md">
|
||
<input id="projects-search" type="text" placeholder="Поиск проекта по названию или коду"
|
||
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||
</div>
|
||
|
||
<div id="projects-table" class="bg-white rounded-lg shadow p-4 text-gray-500">Загрузка...</div>
|
||
</div>
|
||
|
||
<div id="create-project-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||
<div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4 p-6">
|
||
<h2 class="text-xl font-semibold mb-4">Новый проект</h2>
|
||
<div class="space-y-4">
|
||
<div>
|
||
<label for="create-project-name" class="block text-sm font-medium text-gray-700 mb-1">Название проекта</label>
|
||
<input id="create-project-name" type="text" placeholder="Например: Инфраструктура для OPS-123"
|
||
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||
</div>
|
||
<div>
|
||
<label for="create-project-code" class="block text-sm font-medium text-gray-700 mb-1">Код проекта</label>
|
||
<input id="create-project-code" type="text" placeholder="Например: OPS-123"
|
||
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||
</div>
|
||
<div>
|
||
<label for="create-project-variant" class="block text-sm font-medium text-gray-700 mb-1">Вариант (необязательно)</label>
|
||
<input id="create-project-variant" type="text" placeholder="Например: Lenovo"
|
||
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||
</div>
|
||
<div>
|
||
<label for="create-project-tracker-url" class="block text-sm font-medium text-gray-700 mb-1">Ссылка на трекер</label>
|
||
<input id="create-project-tracker-url" type="url" placeholder="https://tracker.yandex.ru/OPS-123"
|
||
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||
</div>
|
||
</div>
|
||
<div class="flex justify-end gap-2 mt-6">
|
||
<button type="button" onclick="closeCreateProjectModal()" class="px-4 py-2 text-gray-600 hover:text-gray-800">Отмена</button>
|
||
<button type="button" onclick="createProject()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Создать</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let status = 'active';
|
||
let projectsSearch = '';
|
||
let authorSearch = '';
|
||
let currentPage = 1;
|
||
let perPage = 10;
|
||
let sortField = 'created_at';
|
||
let sortDir = 'desc';
|
||
let createProjectTrackerManuallyEdited = false;
|
||
let createProjectLastAutoTrackerURL = '';
|
||
let variantsByCode = {};
|
||
let variantsLoaded = false;
|
||
|
||
const trackerBaseURL = 'https://tracker.yandex.ru/';
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text || '';
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function formatMoney(v) {
|
||
return '$' + (v || 0).toLocaleString('en-US', {minimumFractionDigits: 2});
|
||
}
|
||
|
||
function formatDateTime(value) {
|
||
if (!value) return '—';
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return '—';
|
||
return date.toLocaleString('ru-RU', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
|
||
function formatDateParts(value) {
|
||
if (!value) return null;
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) return null;
|
||
return {
|
||
date: date.toLocaleDateString('ru-RU', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit'
|
||
}),
|
||
time: date.toLocaleTimeString('ru-RU', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
})
|
||
};
|
||
}
|
||
|
||
function renderAuditCell(value, user) {
|
||
const parts = formatDateParts(value);
|
||
const safeUser = escapeHtml((user || '—').trim() || '—');
|
||
if (!parts) {
|
||
return '<div class="leading-tight">' +
|
||
'<div class="text-gray-400">—</div>' +
|
||
'<div class="text-gray-400">—</div>' +
|
||
'<div class="text-gray-500">@ ' + safeUser + '</div>' +
|
||
'</div>';
|
||
}
|
||
return '<div class="leading-tight whitespace-nowrap">' +
|
||
'<div>' + escapeHtml(parts.date) + '</div>' +
|
||
'<div class="text-gray-500">' + escapeHtml(parts.time) + '</div>' +
|
||
'<div class="text-gray-600">@ ' + safeUser + '</div>' +
|
||
'</div>';
|
||
}
|
||
|
||
function normalizeVariant(variant) {
|
||
const trimmed = (variant || '').trim();
|
||
return trimmed === '' ? 'main' : trimmed;
|
||
}
|
||
|
||
function renderVariantChips(code, fallbackVariant, fallbackUUID) {
|
||
const variants = variantsByCode[code || ''] || [];
|
||
if (!variants.length) {
|
||
const single = normalizeVariant(fallbackVariant);
|
||
const href = fallbackUUID ? ('/projects/' + fallbackUUID) : '/projects';
|
||
return '<a href="' + href + '" class="inline-flex items-center px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900">' + escapeHtml(single) + '</a>';
|
||
}
|
||
return variants.map(v => {
|
||
const href = v.uuid ? ('/projects/' + v.uuid) : '/projects';
|
||
return '<a href="' + href + '" class="inline-flex items-center px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-700 hover:bg-gray-200 hover:text-gray-900">' + escapeHtml(v.label) + '</a>';
|
||
}).join(' ');
|
||
}
|
||
|
||
async function loadVariantsIndex() {
|
||
if (variantsLoaded) return;
|
||
try {
|
||
const resp = await fetch('/api/projects/all');
|
||
if (!resp.ok) return;
|
||
const data = await resp.json();
|
||
const allProjects = Array.isArray(data) ? data : (data.projects || []);
|
||
variantsByCode = {};
|
||
allProjects.forEach(p => {
|
||
const code = (p.code || '').trim();
|
||
const variant = normalizeVariant(p.variant);
|
||
if (!variantsByCode[code]) {
|
||
variantsByCode[code] = [];
|
||
}
|
||
if (!variantsByCode[code].some(v => v.label === variant)) {
|
||
variantsByCode[code].push({label: variant, uuid: p.uuid});
|
||
}
|
||
});
|
||
Object.keys(variantsByCode).forEach(code => {
|
||
variantsByCode[code].sort((a, b) => {
|
||
if (a.label === 'main') return -1;
|
||
if (b.label === 'main') return 1;
|
||
return a.label.localeCompare(b.label);
|
||
});
|
||
});
|
||
variantsLoaded = true;
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
function toggleSort(field) {
|
||
if (sortField === field) {
|
||
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
sortField = field;
|
||
sortDir = field === 'name' ? 'asc' : 'desc';
|
||
}
|
||
currentPage = 1;
|
||
loadProjects();
|
||
}
|
||
|
||
function setStatus(value) {
|
||
status = value;
|
||
currentPage = 1;
|
||
document.getElementById('status-active-btn').className = value === 'active'
|
||
? 'px-4 py-2 text-sm font-medium bg-blue-600 text-white'
|
||
: 'px-4 py-2 text-sm font-medium bg-white text-gray-700 hover:bg-gray-50';
|
||
document.getElementById('status-archived-btn').className = value === 'archived'
|
||
? 'px-4 py-2 text-sm font-medium bg-blue-600 text-white border-l border-gray-200'
|
||
: 'px-4 py-2 text-sm font-medium bg-white text-gray-700 hover:bg-gray-50 border-l border-gray-200';
|
||
loadProjects();
|
||
}
|
||
|
||
async function loadProjects() {
|
||
const root = document.getElementById('projects-table');
|
||
root.innerHTML = '<div class="text-gray-500">Загрузка...</div>';
|
||
|
||
let rows = [];
|
||
let total = 0;
|
||
let totalPages = 0;
|
||
let page = currentPage;
|
||
try {
|
||
const params = new URLSearchParams({
|
||
status: status,
|
||
search: projectsSearch,
|
||
author: authorSearch,
|
||
page: String(currentPage),
|
||
per_page: String(perPage),
|
||
sort: sortField,
|
||
dir: sortDir
|
||
});
|
||
const resp = await fetch('/api/projects?' + params.toString());
|
||
if (!resp.ok) {
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
const data = await resp.json();
|
||
rows = data.projects || [];
|
||
if (Array.isArray(rows) && rows.length) {
|
||
const byCode = {};
|
||
rows.forEach(p => {
|
||
const codeKey = (p.code || '').trim();
|
||
if (!codeKey) {
|
||
const fallbackKey = p.uuid || Math.random().toString(36);
|
||
byCode[fallbackKey] = p;
|
||
return;
|
||
}
|
||
const variant = (p.variant || '').trim();
|
||
if (!byCode[codeKey]) {
|
||
byCode[codeKey] = p;
|
||
return;
|
||
}
|
||
const current = byCode[codeKey];
|
||
const currentVariant = (current.variant || '').trim();
|
||
if (currentVariant !== '' && variant === '') {
|
||
byCode[codeKey] = p;
|
||
}
|
||
});
|
||
rows = Object.values(byCode);
|
||
}
|
||
total = data.total || 0;
|
||
totalPages = data.total_pages || 0;
|
||
page = data.page || currentPage;
|
||
currentPage = page;
|
||
await loadVariantsIndex();
|
||
} catch (e) {
|
||
root.innerHTML = '<div class="text-red-600">Ошибка загрузки проектов: ' + escapeHtml(String(e.message || e)) + '</div>';
|
||
return;
|
||
}
|
||
|
||
let html = '<div class="overflow-x-auto"><table class="w-full table-fixed min-w-[980px]">';
|
||
html += '<thead class="bg-gray-50">';
|
||
html += '<tr>';
|
||
html += '<th class="w-28 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Код</th>';
|
||
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">';
|
||
html += '<button type="button" onclick="toggleSort(\'name\')" class="inline-flex items-center gap-1 hover:text-gray-700">Название';
|
||
if (sortField === 'name') {
|
||
html += sortDir === 'asc' ? ' <span>↑</span>' : ' <span>↓</span>';
|
||
}
|
||
html += '</button></th>';
|
||
html += '<th class="w-44 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Создан</th>';
|
||
html += '<th class="w-44 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Изменен</th>';
|
||
html += '<th class="w-36 px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Варианты</th>';
|
||
html += '<th class="w-36 px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Действия</th>';
|
||
html += '</tr>';
|
||
html += '<tr>';
|
||
html += '<th class="px-4 py-2"></th>';
|
||
html += '<th class="px-4 py-2"></th>';
|
||
html += '<th class="px-4 py-2"><input id="projects-author-filter" type="text" value="' + escapeHtml(authorSearch) + '" placeholder="Фильтр автора" class="w-full px-2 py-1 border rounded text-xs focus:ring-1 focus:ring-blue-500 focus:border-blue-500"></th>';
|
||
html += '<th class="px-4 py-2"></th>';
|
||
html += '<th class="px-4 py-2"></th>';
|
||
html += '<th class="px-4 py-2"></th>';
|
||
html += '</tr>';
|
||
html += '</thead><tbody class="divide-y">';
|
||
|
||
if (!rows.length) {
|
||
html += '<tr><td colspan="6" class="px-4 py-6 text-sm text-gray-500 text-center">Проектов нет</td></tr>';
|
||
}
|
||
|
||
rows.forEach(p => {
|
||
html += '<tr class="hover:bg-gray-50">';
|
||
const displayName = p.name || '';
|
||
const createdBy = p.owner_username || '—';
|
||
const updatedBy = '—';
|
||
const variantChips = renderVariantChips(p.code, p.variant, p.uuid);
|
||
html += '<td class="px-4 py-3 text-sm font-medium align-top"><a class="inline-block max-w-full text-blue-600 hover:underline whitespace-nowrap" href="/projects/' + p.uuid + '">' + escapeHtml(p.code || '—') + '</a></td>';
|
||
html += '<td class="px-4 py-3 text-sm text-gray-700 align-top"><div class="truncate" title="' + escapeHtml(displayName) + '">' + escapeHtml(displayName || '—') + '</div></td>';
|
||
html += '<td class="px-4 py-3 text-sm text-gray-600 align-top">' + renderAuditCell(p.created_at, createdBy) + '</td>';
|
||
html += '<td class="px-4 py-3 text-sm text-gray-600 align-top">' + renderAuditCell(p.updated_at, updatedBy) + '</td>';
|
||
html += '<td class="px-4 py-3 text-sm align-top"><div class="flex flex-wrap gap-1">' + variantChips + '</div></td>';
|
||
html += '<td class="px-4 py-3 text-sm text-right"><div class="inline-flex items-center gap-2">';
|
||
|
||
if (p.is_active) {
|
||
const safeName = escapeHtml(displayName).replace(/'/g, "\\'");
|
||
html += '<button onclick="copyProject(' + JSON.stringify(p.uuid) + ', ' + JSON.stringify(displayName) + ')" class="text-green-700 hover:text-green-900" title="Копировать">';
|
||
html += '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>';
|
||
html += '</button>';
|
||
|
||
html += '<button onclick="renameProject(' + JSON.stringify(p.uuid) + ', ' + JSON.stringify(displayName) + ')" class="text-blue-700 hover:text-blue-900" title="Переименовать">';
|
||
html += '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>';
|
||
html += '</button>';
|
||
|
||
html += '<button onclick="archiveProject(\'' + p.uuid + '\')" class="text-red-700 hover:text-red-900" title="Удалить (в архив)">';
|
||
html += '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>';
|
||
html += '</button>';
|
||
|
||
html += '<button onclick="addConfigToProject(\'' + p.uuid + '\')" class="text-indigo-700 hover:text-indigo-900" title="Добавить конфигурацию">';
|
||
html += '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>';
|
||
html += '</button>';
|
||
} else {
|
||
html += '<button onclick="reactivateProject(\'' + p.uuid + '\')" class="text-emerald-700 hover:text-emerald-900" title="Восстановить">';
|
||
html += '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>';
|
||
html += '</button>';
|
||
}
|
||
html += '</div></td>';
|
||
html += '</tr>';
|
||
});
|
||
|
||
html += '</tbody></table></div>';
|
||
|
||
if (totalPages > 1) {
|
||
html += '<div class="flex items-center justify-between mt-4 pt-4 border-t">';
|
||
html += '<div class="text-sm text-gray-600">Показано ' + rows.length + ' из ' + total + '</div>';
|
||
html += '<div class="inline-flex items-center gap-1">';
|
||
html += '<button type="button" onclick="goToPage(' + (page - 1) + ')" ' + (page <= 1 ? 'disabled' : '') + ' class="px-3 py-1 text-sm border rounded ' + (page <= 1 ? 'text-gray-300 border-gray-200 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-50') + '">←</button>';
|
||
const startPage = Math.max(1, page - 2);
|
||
const endPage = Math.min(totalPages, page + 2);
|
||
for (let i = startPage; i <= endPage; i++) {
|
||
html += '<button type="button" onclick="goToPage(' + i + ')" class="px-3 py-1 text-sm border rounded ' + (i === page ? 'bg-blue-600 text-white border-blue-600' : 'text-gray-700 border-gray-300 hover:bg-gray-50') + '">' + i + '</button>';
|
||
}
|
||
html += '<button type="button" onclick="goToPage(' + (page + 1) + ')" ' + (page >= totalPages ? 'disabled' : '') + ' class="px-3 py-1 text-sm border rounded ' + (page >= totalPages ? 'text-gray-300 border-gray-200 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-50') + '">→</button>';
|
||
html += '</div>';
|
||
html += '</div>';
|
||
}
|
||
|
||
root.innerHTML = html;
|
||
|
||
const authorInput = document.getElementById('projects-author-filter');
|
||
if (authorInput) {
|
||
authorInput.addEventListener('input', function(e) {
|
||
authorSearch = (e.target.value || '').trim();
|
||
currentPage = 1;
|
||
loadProjects();
|
||
});
|
||
}
|
||
}
|
||
|
||
function goToPage(page) {
|
||
if (page < 1) return;
|
||
currentPage = page;
|
||
loadProjects();
|
||
}
|
||
|
||
function buildTrackerURLFromProjectCode(projectCode) {
|
||
const code = (projectCode || '').trim();
|
||
if (!code) return '';
|
||
return trackerBaseURL + encodeURIComponent(code);
|
||
}
|
||
|
||
function openCreateProjectModal() {
|
||
const nameInput = document.getElementById('create-project-name');
|
||
const codeInput = document.getElementById('create-project-code');
|
||
const variantInput = document.getElementById('create-project-variant');
|
||
const trackerInput = document.getElementById('create-project-tracker-url');
|
||
nameInput.value = '';
|
||
codeInput.value = '';
|
||
variantInput.value = '';
|
||
trackerInput.value = '';
|
||
createProjectTrackerManuallyEdited = false;
|
||
createProjectLastAutoTrackerURL = '';
|
||
document.getElementById('create-project-modal').classList.remove('hidden');
|
||
document.getElementById('create-project-modal').classList.add('flex');
|
||
nameInput.focus();
|
||
}
|
||
|
||
function closeCreateProjectModal() {
|
||
document.getElementById('create-project-modal').classList.add('hidden');
|
||
document.getElementById('create-project-modal').classList.remove('flex');
|
||
}
|
||
|
||
function updateCreateProjectTrackerURL() {
|
||
const codeInput = document.getElementById('create-project-code');
|
||
const trackerInput = document.getElementById('create-project-tracker-url');
|
||
const generatedURL = buildTrackerURLFromProjectCode(codeInput.value);
|
||
if (!createProjectTrackerManuallyEdited || trackerInput.value.trim() === '' || trackerInput.value === createProjectLastAutoTrackerURL) {
|
||
trackerInput.value = generatedURL;
|
||
createProjectLastAutoTrackerURL = generatedURL;
|
||
}
|
||
}
|
||
|
||
async function createProject() {
|
||
const nameInput = document.getElementById('create-project-name');
|
||
const codeInput = document.getElementById('create-project-code');
|
||
const variantInput = document.getElementById('create-project-variant');
|
||
const trackerInput = document.getElementById('create-project-tracker-url');
|
||
const name = (nameInput.value || '').trim();
|
||
const code = (codeInput.value || '').trim();
|
||
const variant = (variantInput.value || '').trim();
|
||
if (!code) {
|
||
alert('Введите код проекта');
|
||
return;
|
||
}
|
||
const resp = await fetch('/api/projects', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
name: name,
|
||
code: code,
|
||
variant: variant,
|
||
tracker_url: (trackerInput.value || '').trim()
|
||
})
|
||
});
|
||
if (!resp.ok) {
|
||
if (resp.status === 409) {
|
||
alert('Проект с таким кодом и вариантом уже существует');
|
||
return;
|
||
}
|
||
alert('Не удалось создать проект');
|
||
return;
|
||
}
|
||
closeCreateProjectModal();
|
||
loadProjects();
|
||
}
|
||
|
||
async function renameProject(projectUUID, currentName) {
|
||
const name = prompt('Новое название проекта', currentName);
|
||
if (!name || !name.trim() || name.trim() === currentName) return;
|
||
const resp = await fetch('/api/projects/' + projectUUID, {
|
||
method: 'PUT',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name: name.trim()})
|
||
});
|
||
if (!resp.ok) {
|
||
if (resp.status === 409) {
|
||
alert('Проект с таким названием уже существует');
|
||
return;
|
||
}
|
||
alert('Не удалось переименовать проект');
|
||
return;
|
||
}
|
||
loadProjects();
|
||
}
|
||
|
||
async function archiveProject(projectUUID) {
|
||
if (!confirm('Переместить проект в архив?')) return;
|
||
const resp = await fetch('/api/projects/' + projectUUID + '/archive', {method: 'POST'});
|
||
if (!resp.ok) {
|
||
alert('Не удалось архивировать проект');
|
||
return;
|
||
}
|
||
loadProjects();
|
||
}
|
||
|
||
async function reactivateProject(projectUUID) {
|
||
const resp = await fetch('/api/projects/' + projectUUID + '/reactivate', {method: 'POST'});
|
||
if (!resp.ok) {
|
||
alert('Не удалось восстановить проект');
|
||
return;
|
||
}
|
||
loadProjects();
|
||
}
|
||
|
||
async function addConfigToProject(projectUUID) {
|
||
const name = prompt('Название новой конфигурации');
|
||
if (!name || !name.trim()) return;
|
||
const resp = await fetch('/api/projects/' + projectUUID + '/configs', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name: name.trim(), items: [], notes: '', server_count: 1})
|
||
});
|
||
if (!resp.ok) {
|
||
alert('Не удалось создать конфигурацию');
|
||
return;
|
||
}
|
||
loadProjects();
|
||
}
|
||
|
||
async function copyProject(projectUUID, projectName) {
|
||
const newName = prompt('Название копии проекта', projectName + ' (копия)');
|
||
if (!newName || !newName.trim()) return;
|
||
const newCode = prompt('Код проекта', '');
|
||
if (!newCode || !newCode.trim()) return;
|
||
const newVariant = prompt('Вариант (необязательно)', '');
|
||
|
||
const createResp = await fetch('/api/projects', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name: newName.trim(), code: newCode.trim(), variant: (newVariant || '').trim()})
|
||
});
|
||
if (!createResp.ok) {
|
||
if (createResp.status === 409) {
|
||
alert('Проект с таким кодом и вариантом уже существует');
|
||
return;
|
||
}
|
||
alert('Не удалось создать копию проекта');
|
||
return;
|
||
}
|
||
const newProject = await createResp.json();
|
||
|
||
const listResp = await fetch('/api/projects/' + projectUUID + '/configs');
|
||
if (!listResp.ok) {
|
||
alert('Проект скопирован без конфигураций (не удалось загрузить исходные конфигурации)');
|
||
loadProjects();
|
||
return;
|
||
}
|
||
const listData = await listResp.json();
|
||
const configs = listData.configurations || [];
|
||
|
||
for (const cfg of configs) {
|
||
await fetch('/api/projects/' + newProject.uuid + '/configs/' + cfg.uuid + '/clone', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name: cfg.name})
|
||
});
|
||
}
|
||
|
||
loadProjects();
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
loadProjects();
|
||
|
||
document.getElementById('projects-search').addEventListener('input', function(e) {
|
||
projectsSearch = (e.target.value || '').trim();
|
||
currentPage = 1;
|
||
loadProjects();
|
||
});
|
||
|
||
document.getElementById('create-project-code').addEventListener('input', function() {
|
||
updateCreateProjectTrackerURL();
|
||
});
|
||
|
||
document.getElementById('create-project-name').addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
createProject();
|
||
}
|
||
});
|
||
|
||
document.getElementById('create-project-tracker-url').addEventListener('input', function(e) {
|
||
createProjectTrackerManuallyEdited = (e.target.value || '').trim() !== createProjectLastAutoTrackerURL;
|
||
});
|
||
|
||
document.getElementById('create-project-code').addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
createProject();
|
||
}
|
||
});
|
||
|
||
document.getElementById('create-project-tracker-url').addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
createProject();
|
||
}
|
||
});
|
||
|
||
document.getElementById('create-project-modal').addEventListener('click', function(e) {
|
||
if (e.target === this) {
|
||
closeCreateProjectModal();
|
||
}
|
||
});
|
||
|
||
// Listen for sync completion events from navbar
|
||
window.addEventListener('sync-completed', function(e) {
|
||
// Reset pagination and reload projects list
|
||
loadProjects();
|
||
});
|
||
});
|
||
</script>
|
||
{{end}}
|
||
|
||
{{template "base" .}}
|