Add project variants and UI updates
This commit is contained in:
@@ -4,23 +4,43 @@
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/projects" class="text-gray-500 hover:text-gray-700" title="Назад к проектам">
|
||||
<a href="/projects" class="text-gray-500 hover:text-gray-700" title="Все проекты">
|
||||
<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="M15 19l-7-7 7-7"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9.75L12 3l9 6.75v9A2.25 2.25 0 0118.75 21h-13.5A2.25 2.25 0 013 18.75v-9z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 21v-6h6v6"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold" id="project-title">Проект</h1>
|
||||
<div class="text-2xl font-bold flex items-center gap-2">
|
||||
<a id="project-code-link" href="/projects" class="text-blue-700 hover:underline">
|
||||
<span id="project-code">—</span>
|
||||
</a>
|
||||
<span class="text-gray-400">-</span>
|
||||
<div class="relative">
|
||||
<button id="project-variant-button" type="button" class="inline-flex items-center gap-2 text-base font-medium px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200 border border-gray-200">
|
||||
<span id="project-variant-label">main</span>
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="project-variant-menu" class="absolute left-0 mt-2 min-w-[10rem] rounded-lg border border-gray-200 bg-white shadow-lg hidden z-10">
|
||||
<div id="project-variant-list" class="py-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="action-buttons" class="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<button onclick="openCreateModal()" class="py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
|
||||
<div id="action-buttons" class="mt-4 grid grid-cols-1 sm:grid-cols-4 gap-3">
|
||||
<button onclick="openNewVariantModal()" class="py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 font-medium">
|
||||
+ Новый вариант
|
||||
</button>
|
||||
<button onclick="openCreateModal()" class="py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
|
||||
+ Создать новую квоту
|
||||
</button>
|
||||
<button onclick="openImportModal()" class="py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 font-medium">
|
||||
<button onclick="openImportModal()" class="py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 font-medium">
|
||||
Импорт квоты
|
||||
</button>
|
||||
<button onclick="openProjectSettingsModal()" class="py-3 bg-gray-700 text-white rounded-lg hover:bg-gray-800 font-medium">
|
||||
<button onclick="openProjectSettingsModal()" class="py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-800 font-medium">
|
||||
Параметры
|
||||
</button>
|
||||
</div>
|
||||
@@ -61,6 +81,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="new-variant-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-lg p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Новый вариант</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Код проекта</label>
|
||||
<div id="new-variant-code" class="px-3 py-2 bg-gray-50 border rounded text-sm text-gray-700">—</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="new-variant-name" class="block text-sm font-medium text-gray-700 mb-1">Название (необязательно)</label>
|
||||
<input id="new-variant-name" 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="new-variant-value" class="block text-sm font-medium text-gray-700 mb-1">Вариант</label>
|
||||
<input id="new-variant-value" 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 class="text-xs text-gray-500 mt-1">Оставьте пустым для main нельзя — нужно уникальное значение.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end gap-2">
|
||||
<button onclick="closeNewVariantModal()" class="px-4 py-2 text-gray-700 bg-gray-100 rounded hover:bg-gray-200">Отмена</button>
|
||||
<button onclick="createNewVariant()" class="px-4 py-2 text-white bg-purple-600 rounded hover:bg-purple-700">Создать</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rename-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>
|
||||
@@ -120,6 +167,16 @@
|
||||
<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 class="block text-sm font-medium text-gray-700 mb-1">Код проекта</label>
|
||||
<input type="text" id="project-settings-code"
|
||||
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Вариант (необязательно)</label>
|
||||
<input type="text" id="project-settings-variant"
|
||||
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Название проекта</label>
|
||||
<input type="text" id="project-settings-name"
|
||||
@@ -144,6 +201,8 @@ const projectUUID = '{{.ProjectUUID}}';
|
||||
let configStatusMode = 'active';
|
||||
let project = null;
|
||||
let allConfigs = [];
|
||||
let projectVariants = [];
|
||||
let variantMenuInitialized = false;
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
@@ -157,6 +216,91 @@ function resolveProjectTrackerURL(projectData) {
|
||||
return explicitURL;
|
||||
}
|
||||
|
||||
function formatProjectTitle(projectData) {
|
||||
if (!projectData) return 'Проект';
|
||||
const code = (projectData.code || '').trim();
|
||||
const name = (projectData.name || '').trim();
|
||||
const variant = (projectData.variant || '').trim();
|
||||
if (!code) return name || 'Проект';
|
||||
if (variant) {
|
||||
return code + ': (' + variant + ') ' + (name || '');
|
||||
}
|
||||
return code + ': ' + (name || '');
|
||||
}
|
||||
|
||||
function normalizeVariantLabel(variant) {
|
||||
const trimmed = (variant || '').trim();
|
||||
return trimmed === '' ? 'main' : trimmed;
|
||||
}
|
||||
|
||||
async function loadVariantsForCode(code) {
|
||||
if (!code) 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 || []);
|
||||
projectVariants = allProjects
|
||||
.filter(p => (p.code || '').trim() === code)
|
||||
.map(p => ({uuid: p.uuid, variant: (p.variant || '').trim()}));
|
||||
projectVariants.sort((a, b) => normalizeVariantLabel(a.variant).localeCompare(normalizeVariantLabel(b.variant)));
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function renderVariantSelect() {
|
||||
const list = document.getElementById('project-variant-list');
|
||||
const menu = document.getElementById('project-variant-menu');
|
||||
const button = document.getElementById('project-variant-button');
|
||||
const label = document.getElementById('project-variant-label');
|
||||
const codeLink = document.getElementById('project-code-link');
|
||||
if (!list || !menu || !button || !label) return;
|
||||
list.innerHTML = '';
|
||||
const variants = projectVariants.length ? projectVariants : [{uuid: projectUUID, variant: (project && project.variant) || ''}];
|
||||
let mainUUID = '';
|
||||
variants.forEach(item => {
|
||||
const variantLabel = normalizeVariantLabel(item.variant);
|
||||
if (variantLabel === 'main' && !mainUUID) {
|
||||
mainUUID = item.uuid;
|
||||
}
|
||||
const option = document.createElement('button');
|
||||
option.type = 'button';
|
||||
option.className = 'w-full text-left px-3 py-2 text-sm hover:bg-gray-50';
|
||||
if (item.uuid === projectUUID) {
|
||||
option.className += ' font-semibold text-gray-900';
|
||||
label.textContent = variantLabel;
|
||||
}
|
||||
option.textContent = variantLabel;
|
||||
option.onclick = function() {
|
||||
menu.classList.add('hidden');
|
||||
if (item.uuid && item.uuid !== projectUUID) {
|
||||
window.location.href = '/projects/' + item.uuid;
|
||||
}
|
||||
};
|
||||
list.appendChild(option);
|
||||
});
|
||||
|
||||
if (codeLink) {
|
||||
const targetMain = mainUUID || projectUUID;
|
||||
codeLink.href = '/projects/' + targetMain;
|
||||
}
|
||||
|
||||
if (!variantMenuInitialized) {
|
||||
button.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
menu.classList.toggle('hidden');
|
||||
};
|
||||
document.addEventListener('click', function() {
|
||||
menu.classList.add('hidden');
|
||||
});
|
||||
menu.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
variantMenuInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
function setConfigStatusMode(mode) {
|
||||
if (mode !== 'active' && mode !== 'archived') return;
|
||||
configStatusMode = mode;
|
||||
@@ -254,7 +398,9 @@ async function loadProject() {
|
||||
return false;
|
||||
}
|
||||
project = await resp.json();
|
||||
document.getElementById('project-title').textContent = project.name;
|
||||
document.getElementById('project-code').textContent = project.code || '—';
|
||||
await loadVariantsForCode(project.code || '');
|
||||
renderVariantSelect();
|
||||
const trackerLink = document.getElementById('tracker-link');
|
||||
if (trackerLink) {
|
||||
if (project && project.is_system) {
|
||||
@@ -297,6 +443,56 @@ function openCreateModal() {
|
||||
document.getElementById('create-name').focus();
|
||||
}
|
||||
|
||||
function openNewVariantModal() {
|
||||
if (!project) return;
|
||||
document.getElementById('new-variant-code').textContent = (project.code || '').trim() || '—';
|
||||
document.getElementById('new-variant-name').value = project.name || '';
|
||||
document.getElementById('new-variant-value').value = '';
|
||||
document.getElementById('new-variant-modal').classList.remove('hidden');
|
||||
document.getElementById('new-variant-modal').classList.add('flex');
|
||||
document.getElementById('new-variant-value').focus();
|
||||
}
|
||||
|
||||
function closeNewVariantModal() {
|
||||
document.getElementById('new-variant-modal').classList.add('hidden');
|
||||
document.getElementById('new-variant-modal').classList.remove('flex');
|
||||
}
|
||||
|
||||
async function createNewVariant() {
|
||||
if (!project) return;
|
||||
const code = (project.code || '').trim();
|
||||
const variant = (document.getElementById('new-variant-value').value || '').trim();
|
||||
const nameRaw = (document.getElementById('new-variant-name').value || '').trim();
|
||||
if (!code || !variant) {
|
||||
showToast('Укажите вариант', 'error');
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
code: code,
|
||||
variant: variant,
|
||||
name: nameRaw ? nameRaw : null
|
||||
};
|
||||
const resp = await fetch('/api/projects', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
showToast(data.error || 'Ошибка создания варианта', 'error');
|
||||
return;
|
||||
}
|
||||
const created = await resp.json().catch(() => null);
|
||||
closeNewVariantModal();
|
||||
showToast('Вариант создан', 'success');
|
||||
if (created && created.uuid) {
|
||||
window.location.href = '/projects/' + created.uuid;
|
||||
return;
|
||||
}
|
||||
loadProject();
|
||||
loadConfigs();
|
||||
}
|
||||
|
||||
function closeCreateModal() {
|
||||
document.getElementById('create-modal').classList.add('hidden');
|
||||
document.getElementById('create-modal').classList.remove('flex');
|
||||
@@ -429,6 +625,8 @@ function openProjectSettingsModal() {
|
||||
alert('Системный проект нельзя редактировать');
|
||||
return;
|
||||
}
|
||||
document.getElementById('project-settings-code').value = project.code || '';
|
||||
document.getElementById('project-settings-variant').value = project.variant || '';
|
||||
document.getElementById('project-settings-name').value = project.name || '';
|
||||
document.getElementById('project-settings-tracker-url').value = (project.tracker_url || '').trim();
|
||||
document.getElementById('project-settings-modal').classList.remove('hidden');
|
||||
@@ -442,27 +640,31 @@ function closeProjectSettingsModal() {
|
||||
|
||||
async function saveProjectSettings() {
|
||||
if (!project) return;
|
||||
const code = document.getElementById('project-settings-code').value.trim();
|
||||
const variant = document.getElementById('project-settings-variant').value.trim();
|
||||
const name = document.getElementById('project-settings-name').value.trim();
|
||||
const trackerURL = document.getElementById('project-settings-tracker-url').value.trim();
|
||||
if (!name) {
|
||||
alert('Введите название проекта');
|
||||
if (!code) {
|
||||
alert('Введите код проекта');
|
||||
return;
|
||||
}
|
||||
const resp = await fetch('/api/projects/' + projectUUID, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name: name, tracker_url: trackerURL})
|
||||
body: JSON.stringify({code: code, variant: variant, name: name, tracker_url: trackerURL})
|
||||
});
|
||||
if (!resp.ok) {
|
||||
if (resp.status === 409) {
|
||||
alert('Проект с таким названием уже существует');
|
||||
alert('Проект с таким кодом и вариантом уже существует');
|
||||
return;
|
||||
}
|
||||
alert('Не удалось сохранить параметры проекта');
|
||||
return;
|
||||
}
|
||||
project = await resp.json();
|
||||
document.getElementById('project-title').textContent = project.name;
|
||||
document.getElementById('project-code').textContent = project.code || '—';
|
||||
await loadVariantsForCode(project.code || '');
|
||||
renderVariantSelect();
|
||||
const trackerLink = document.getElementById('tracker-link');
|
||||
if (trackerLink) {
|
||||
const trackerURLResolved = resolveProjectTrackerURL(project);
|
||||
@@ -557,6 +759,7 @@ function wildcardMatch(value, pattern) {
|
||||
|
||||
document.getElementById('create-modal').addEventListener('click', function(e) { if (e.target === this) closeCreateModal(); });
|
||||
document.getElementById('rename-modal').addEventListener('click', function(e) { if (e.target === this) closeRenameModal(); });
|
||||
document.getElementById('new-variant-modal').addEventListener('click', function(e) { if (e.target === this) closeNewVariantModal(); });
|
||||
document.getElementById('clone-modal').addEventListener('click', function(e) { if (e.target === this) closeCloneModal(); });
|
||||
document.getElementById('import-modal').addEventListener('click', function(e) { if (e.target === this) closeImportModal(); });
|
||||
document.getElementById('project-settings-modal').addEventListener('click', function(e) { if (e.target === this) closeProjectSettingsModal(); });
|
||||
|
||||
Reference in New Issue
Block a user