Add project variants and UI updates

This commit is contained in:
Mikhail Chusavitin
2026-02-13 19:27:48 +03:00
parent 4e1a46bd71
commit 9b5d57902d
23 changed files with 1113 additions and 147 deletions

View File

@@ -62,7 +62,7 @@
<label class="block text-sm font-medium text-gray-700 mb-1">Код проекта</label>
<input id="create-project-input"
list="create-project-options"
placeholder="Начните вводить название проекта"
placeholder="Например: OPS-123 (Lenovo)"
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<datalist id="create-project-options"></datalist>
<div class="mt-2 flex justify-between items-center gap-3">
@@ -147,7 +147,7 @@
<label class="block text-sm font-medium text-gray-700 mb-1">Проект</label>
<input id="move-project-input"
list="move-project-options"
placeholder="Начните вводить название проекта"
placeholder="Например: OPS-123 (Lenovo)"
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<datalist id="move-project-options"></datalist>
<div class="mt-2 flex justify-between items-center gap-3">
@@ -174,7 +174,17 @@
<div id="create-project-on-move-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-3">Проект не найден</h2>
<p class="text-sm text-gray-600 mb-4">Проект "<span id="create-project-on-move-name" class="font-medium text-gray-900"></span>" не найден. <span id="create-project-on-move-description">Создать и привязать квоту?</span></p>
<p class="text-sm text-gray-600 mb-4">Проект с кодом "<span id="create-project-on-move-code" class="font-medium text-gray-900"></span>" не найден. <span id="create-project-on-move-description">Создать и привязать квоту?</span></p>
<div class="mb-4">
<label for="create-project-on-move-name" class="block text-sm font-medium text-gray-700 mb-1">Название проекта</label>
<input id="create-project-on-move-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 class="mb-4">
<label for="create-project-on-move-variant" class="block text-sm font-medium text-gray-700 mb-1">Вариант (необязательно)</label>
<input id="create-project-on-move-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 class="flex justify-end space-x-3">
<button onclick="closeCreateProjectOnMoveModal()" class="px-4 py-2 text-gray-600 hover:text-gray-800">Отмена</button>
<button id="create-project-on-move-confirm-btn" onclick="confirmCreateProjectOnMove()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Создать и привязать</button>
@@ -191,10 +201,12 @@ let configStatusMode = 'active';
let configsSearch = '';
let projectsCache = [];
let projectNameByUUID = {};
let projectCodeByUUID = {};
let projectVariantByUUID = {};
let pendingMoveConfigUUID = '';
let pendingMoveProjectName = '';
let pendingMoveProjectCode = '';
let pendingCreateConfigName = '';
let pendingCreateProjectName = '';
let pendingCreateProjectCode = '';
function renderConfigs(configs) {
const emptyText = configStatusMode === 'archived'
@@ -307,6 +319,30 @@ function renderConfigs(configs) {
document.getElementById('configs-list').innerHTML = html;
}
function projectDisplayKey(project) {
const code = (project.code || '').trim();
const variant = (project.variant || '').trim();
if (!code) return '';
return variant ? (code + ' (' + variant + ')') : code;
}
function findProjectByInput(input) {
const trimmed = (input || '').trim().toLowerCase();
if (!trimmed) return null;
const directMatch = projectsCache.find(p => projectDisplayKey(p).toLowerCase() === trimmed);
if (directMatch) return directMatch;
const codeMatches = projectsCache.filter(p => (p.code || '').toLowerCase() === trimmed);
if (codeMatches.length === 1) {
return codeMatches[0];
}
if (codeMatches.length > 1) {
alert('У проекта несколько вариантов. Укажите вариант в формате "CODE (variant)".');
}
return null;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
@@ -444,21 +480,21 @@ async function createConfig() {
return;
}
const projectName = document.getElementById('create-project-input').value.trim();
const projectCode = document.getElementById('create-project-input').value.trim();
let projectUUID = '';
if (projectName) {
const matchedProject = projectsCache.find(p => p.name.toLowerCase() === projectName.toLowerCase());
if (projectCode) {
const matchedProject = findProjectByInput(projectCode);
if (matchedProject) {
if (!matchedProject.is_active) {
alert('Проект с таким названием находится в архиве. Восстановите его или выберите другой.');
alert('Проект с таким кодом находится в архиве. Восстановите его или выберите другой.');
return;
}
projectUUID = matchedProject.uuid;
} else {
pendingCreateConfigName = name;
pendingCreateProjectName = projectName;
openCreateProjectOnCreateModal(projectName);
pendingCreateProjectCode = projectCode;
openCreateProjectOnCreateModal(projectCode);
return;
}
}
@@ -506,12 +542,14 @@ function openMoveProjectModal(uuid, configName, currentProjectUUID) {
projectsCache.forEach(project => {
if (!project.is_active) return;
const option = document.createElement('option');
option.value = project.name;
option.value = projectDisplayKey(project);
option.label = project.name || '';
options.appendChild(option);
});
if (currentProjectUUID && projectNameByUUID[currentProjectUUID]) {
input.value = projectNameByUUID[currentProjectUUID];
if (currentProjectUUID && projectCodeByUUID[currentProjectUUID]) {
const variant = projectVariantByUUID[currentProjectUUID] || '';
input.value = variant ? (projectCodeByUUID[currentProjectUUID] + ' (' + variant + ')') : projectCodeByUUID[currentProjectUUID];
} else {
input.value = '';
}
@@ -527,23 +565,23 @@ function closeMoveProjectModal() {
async function confirmMoveProject() {
const uuid = document.getElementById('move-project-uuid').value;
const projectName = document.getElementById('move-project-input').value.trim();
const projectCode = document.getElementById('move-project-input').value.trim();
if (!uuid) return;
let projectUUID = '';
if (projectName) {
const matchedProject = projectsCache.find(p => p.name.toLowerCase() === projectName.toLowerCase());
if (projectCode) {
const matchedProject = findProjectByInput(projectCode);
if (matchedProject) {
if (!matchedProject.is_active) {
alert('Проект с таким названием находится в архиве. Восстановите его или выберите другой.');
alert('Проект с таким кодом находится в архиве. Восстановите его или выберите другой.');
return;
}
projectUUID = matchedProject.uuid;
} else {
pendingMoveConfigUUID = uuid;
pendingMoveProjectName = projectName;
openCreateProjectOnMoveModal(projectName);
pendingMoveProjectCode = projectCode;
openCreateProjectOnMoveModal(projectCode);
return;
}
}
@@ -560,7 +598,9 @@ function clearCreateProjectInput() {
}
function openCreateProjectOnMoveModal(projectName) {
document.getElementById('create-project-on-move-name').textContent = projectName;
document.getElementById('create-project-on-move-code').textContent = projectName;
document.getElementById('create-project-on-move-name').value = projectName;
document.getElementById('create-project-on-move-variant').value = '';
document.getElementById('create-project-on-move-description').textContent = 'Создать и привязать квоту?';
document.getElementById('create-project-on-move-confirm-btn').textContent = 'Создать и привязать';
document.getElementById('create-project-on-move-modal').classList.remove('hidden');
@@ -568,7 +608,9 @@ function openCreateProjectOnMoveModal(projectName) {
}
function openCreateProjectOnCreateModal(projectName) {
document.getElementById('create-project-on-move-name').textContent = projectName;
document.getElementById('create-project-on-move-code').textContent = projectName;
document.getElementById('create-project-on-move-name').value = projectName;
document.getElementById('create-project-on-move-variant').value = '';
document.getElementById('create-project-on-move-description').textContent = 'Создать и использовать для новой конфигурации?';
document.getElementById('create-project-on-move-confirm-btn').textContent = 'Создать и использовать';
document.getElementById('create-project-on-move-modal').classList.remove('hidden');
@@ -579,24 +621,30 @@ function closeCreateProjectOnMoveModal() {
document.getElementById('create-project-on-move-modal').classList.add('hidden');
document.getElementById('create-project-on-move-modal').classList.remove('flex');
pendingMoveConfigUUID = '';
pendingMoveProjectName = '';
pendingMoveProjectCode = '';
pendingCreateConfigName = '';
pendingCreateProjectName = '';
pendingCreateProjectCode = '';
document.getElementById('create-project-on-move-name').value = '';
document.getElementById('create-project-on-move-variant').value = '';
}
async function confirmCreateProjectOnMove() {
if (pendingCreateConfigName && pendingCreateProjectName) {
const projectNameInput = document.getElementById('create-project-on-move-name');
const projectVariantInput = document.getElementById('create-project-on-move-variant');
const projectName = (projectNameInput.value || '').trim();
const projectVariant = (projectVariantInput.value || '').trim();
if (pendingCreateConfigName && pendingCreateProjectCode) {
const configName = pendingCreateConfigName;
const projectName = pendingCreateProjectName;
const projectCode = pendingCreateProjectCode;
try {
const createResp = await fetch('/api/projects', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ name: projectName })
body: JSON.stringify({ name: projectName, code: projectCode, variant: projectVariant })
});
if (!createResp.ok) {
if (createResp.status === 409) {
alert('Проект с таким названием уже существует');
alert('Проект с таким кодом и вариантом уже существует');
return;
}
const err = await createResp.json();
@@ -606,14 +654,14 @@ async function confirmCreateProjectOnMove() {
const newProject = await createResp.json();
pendingCreateConfigName = '';
pendingCreateProjectName = '';
pendingCreateProjectCode = '';
await loadProjectsForConfigUI();
const created = await createConfigWithProject(configName, newProject.uuid);
if (created) {
closeCreateProjectOnMoveModal();
} else {
closeCreateProjectOnMoveModal();
document.getElementById('create-project-input').value = projectName;
document.getElementById('create-project-input').value = projectCode;
}
} catch (e) {
alert('Ошибка создания проекта');
@@ -622,8 +670,8 @@ async function confirmCreateProjectOnMove() {
}
const configUUID = pendingMoveConfigUUID;
const projectName = pendingMoveProjectName;
if (!configUUID || !projectName) {
const projectCode = pendingMoveProjectCode;
if (!configUUID || !projectCode) {
closeCreateProjectOnMoveModal();
return;
}
@@ -632,11 +680,11 @@ async function confirmCreateProjectOnMove() {
const createResp = await fetch('/api/projects', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ name: projectName })
body: JSON.stringify({ name: projectName, code: projectCode, variant: projectVariant })
});
if (!createResp.ok) {
if (createResp.status === 409) {
alert('Проект с таким названием уже существует');
alert('Проект с таким кодом и вариантом уже существует');
return;
}
const err = await createResp.json();
@@ -646,9 +694,9 @@ async function confirmCreateProjectOnMove() {
const newProject = await createResp.json();
pendingMoveConfigUUID = '';
pendingMoveProjectName = '';
pendingMoveProjectCode = '';
await loadProjectsForConfigUI();
document.getElementById('move-project-input').value = projectName;
document.getElementById('move-project-input').value = projectCode;
const moved = await moveConfigToProject(configUUID, newProject.uuid);
if (moved) {
closeCreateProjectOnMoveModal();
@@ -835,6 +883,8 @@ document.getElementById('configs-search').addEventListener('input', function(e)
async function loadProjectsForConfigUI() {
projectsCache = [];
projectNameByUUID = {};
projectCodeByUUID = {};
projectVariantByUUID = {};
try {
// Use /api/projects/all to get all projects without pagination
const resp = await fetch('/api/projects/all');
@@ -847,7 +897,11 @@ async function loadProjectsForConfigUI() {
projectsCache = allProjects;
allProjects.forEach(project => {
projectNameByUUID[project.uuid] = project.name;
const variant = (project.variant || '').trim();
const baseName = project.name || '';
projectNameByUUID[project.uuid] = variant ? (baseName + ' (' + variant + ')') : baseName;
projectCodeByUUID[project.uuid] = project.code || '';
projectVariantByUUID[project.uuid] = project.variant || '';
});
const createOptions = document.getElementById('create-project-options');
@@ -856,7 +910,8 @@ async function loadProjectsForConfigUI() {
projectsCache.forEach(project => {
if (!project.is_active) return;
const option = document.createElement('option');
option.value = project.name;
option.value = projectDisplayKey(project);
option.label = project.name || '';
createOptions.appendChild(option);
});
}