486 lines
22 KiB
HTML
486 lines
22 KiB
HTML
{{define "title"}}Проект - QuoteForge{{end}}
|
||
|
||
{{define "content"}}
|
||
<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="Назад к проектам">
|
||
<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>
|
||
</svg>
|
||
</a>
|
||
<h1 class="text-2xl font-bold" id="project-title">Проект</h1>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="action-buttons" class="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||
<button onclick="openCreateModal()" class="py-3 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>
|
||
</div>
|
||
<div class="mt-2">
|
||
<a id="tracker-link" href="https://tracker.yandex.ru/OPS-1933" target="_blank" rel="noopener noreferrer" class="text-sm text-blue-600 hover:text-blue-800 hover:underline">
|
||
открыть в трекере
|
||
</a>
|
||
</div>
|
||
|
||
<div class="mt-4 inline-flex rounded-lg border border-gray-200 overflow-hidden">
|
||
<button id="status-active-btn" onclick="setConfigStatusMode('active')" class="px-4 py-2 text-sm font-medium bg-blue-600 text-white">
|
||
Активные
|
||
</button>
|
||
<button id="status-archived-btn" onclick="setConfigStatusMode('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 id="configs-list">
|
||
<div class="text-center py-8 text-gray-500">Загрузка...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="create-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 class="block text-sm font-medium text-gray-700 mb-1">Название квоты</label>
|
||
<input type="text" id="create-name" placeholder="Например: OPP-2026-001"
|
||
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 space-x-3 mt-6">
|
||
<button onclick="closeCreateModal()" class="px-4 py-2 text-gray-600 hover:text-gray-800">Отмена</button>
|
||
<button onclick="createConfig()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-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>
|
||
<div class="space-y-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">Новое название</label>
|
||
<input type="text" id="rename-input"
|
||
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||
<input type="hidden" id="rename-uuid">
|
||
</div>
|
||
</div>
|
||
<div class="flex justify-end space-x-3 mt-6">
|
||
<button onclick="closeRenameModal()" class="px-4 py-2 text-gray-600 hover:text-gray-800">Отмена</button>
|
||
<button onclick="renameConfig()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="clone-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 class="block text-sm font-medium text-gray-700 mb-1">Название копии</label>
|
||
<input type="text" id="clone-input"
|
||
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||
<input type="hidden" id="clone-uuid">
|
||
</div>
|
||
</div>
|
||
<div class="flex justify-end space-x-3 mt-6">
|
||
<button onclick="closeCloneModal()" class="px-4 py-2 text-gray-600 hover:text-gray-800">Отмена</button>
|
||
<button onclick="cloneConfig()" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700">Копировать</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="import-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-3">
|
||
<label class="block text-sm font-medium text-gray-700">Квота</label>
|
||
<input id="import-config-input"
|
||
list="import-config-options"
|
||
placeholder="Начните вводить название квоты"
|
||
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||
<datalist id="import-config-options"></datalist>
|
||
<div class="text-xs text-gray-500">Квота будет перемещена в текущий проект.</div>
|
||
</div>
|
||
<div class="flex justify-end gap-2 mt-6">
|
||
<button onclick="closeImportModal()" class="px-4 py-2 text-gray-600 hover:text-gray-800">Отмена</button>
|
||
<button onclick="importConfigToProject()" class="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700">Импортировать</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const projectUUID = '{{.ProjectUUID}}';
|
||
let configStatusMode = 'active';
|
||
let project = null;
|
||
let allConfigs = [];
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text || '';
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function setConfigStatusMode(mode) {
|
||
if (mode !== 'active' && mode !== 'archived') return;
|
||
configStatusMode = mode;
|
||
applyStatusModeUI();
|
||
loadConfigs();
|
||
}
|
||
|
||
function applyStatusModeUI() {
|
||
const activeBtn = document.getElementById('status-active-btn');
|
||
const archivedBtn = document.getElementById('status-archived-btn');
|
||
const actionButtons = document.getElementById('action-buttons');
|
||
|
||
if (configStatusMode === 'archived') {
|
||
activeBtn.className = 'px-4 py-2 text-sm font-medium bg-white text-gray-700 hover:bg-gray-50';
|
||
archivedBtn.className = 'px-4 py-2 text-sm font-medium bg-blue-600 text-white border-l border-gray-200';
|
||
actionButtons.classList.add('hidden');
|
||
} else {
|
||
activeBtn.className = 'px-4 py-2 text-sm font-medium bg-blue-600 text-white';
|
||
archivedBtn.className = 'px-4 py-2 text-sm font-medium bg-white text-gray-700 hover:bg-gray-50 border-l border-gray-200';
|
||
actionButtons.classList.remove('hidden');
|
||
}
|
||
}
|
||
|
||
function renderConfigs(configs) {
|
||
const emptyText = configStatusMode === 'archived' ? 'Архив пуст' : 'Нет квот в проекте';
|
||
if (configs.length === 0) {
|
||
document.getElementById('configs-list').innerHTML =
|
||
'<div class="bg-white rounded-lg shadow p-8 text-center text-gray-500">' + emptyText + '</div>';
|
||
return;
|
||
}
|
||
|
||
let totalSum = 0;
|
||
let html = '<div class="bg-white rounded-lg shadow overflow-hidden"><table class="w-full">';
|
||
html += '<thead class="bg-gray-50"><tr>';
|
||
html += '<th class="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">Название</th>';
|
||
html += '<th class="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">Цена (за 1 шт)</th>';
|
||
html += '<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Кол-во</th>';
|
||
html += '<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Сумма</th>';
|
||
html += '<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Действия</th>';
|
||
html += '</tr></thead><tbody class="divide-y">';
|
||
|
||
configs.forEach(c => {
|
||
const date = new Date(c.created_at).toLocaleDateString('ru-RU');
|
||
const total = c.total_price || 0;
|
||
const serverCount = c.server_count || 1;
|
||
const author = c.owner_username || (c.user && c.user.username) || '—';
|
||
const unitPrice = serverCount > 0 ? (total / serverCount) : 0;
|
||
totalSum += total;
|
||
|
||
html += '<tr class="hover:bg-gray-50">';
|
||
html += '<td class="px-4 py-3 text-sm text-gray-500">' + date + '</td>';
|
||
if (configStatusMode === 'archived') {
|
||
html += '<td class="px-4 py-3 text-sm font-medium text-gray-700">' + escapeHtml(c.name) + '</td>';
|
||
} else {
|
||
html += '<td class="px-4 py-3 text-sm font-medium"><a href="/configurator?uuid=' + c.uuid + '" class="text-blue-600 hover:text-blue-800 hover:underline">' + escapeHtml(c.name) + '</a></td>';
|
||
}
|
||
html += '<td class="px-4 py-3 text-sm text-gray-500">' + escapeHtml(author) + '</td>';
|
||
html += '<td class="px-4 py-3 text-sm text-gray-500">$' + unitPrice.toLocaleString('en-US', {minimumFractionDigits: 2}) + '</td>';
|
||
html += '<td class="px-4 py-3 text-sm text-gray-500">' + serverCount + '</td>';
|
||
html += '<td class="px-4 py-3 text-sm text-right">$' + total.toLocaleString('en-US', {minimumFractionDigits: 2}) + '</td>';
|
||
html += '<td class="px-4 py-3 text-sm text-right space-x-2">';
|
||
if (configStatusMode === 'archived') {
|
||
html += '<button onclick="reactivateConfig(\'' + c.uuid + '\')" class="text-emerald-600 hover:text-emerald-800" 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></button>';
|
||
} else {
|
||
html += '<button onclick="openCloneModal(\'' + c.uuid + '\', \'' + escapeHtml(c.name).replace(/'/g, "\\'") + '\')" class="text-green-600 hover:text-green-800" 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></button>';
|
||
html += '<button onclick="openRenameModal(\'' + c.uuid + '\', \'' + escapeHtml(c.name).replace(/'/g, "\\'") + '\')" class="text-blue-600 hover:text-blue-800" 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></button>';
|
||
html += '<button onclick="deleteConfig(\'' + c.uuid + '\')" class="text-red-600 hover:text-red-800" 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></button>';
|
||
}
|
||
html += '</td></tr>';
|
||
});
|
||
|
||
html += '</tbody>';
|
||
html += '<tfoot class="bg-gray-50 border-t">';
|
||
html += '<tr>';
|
||
html += '<td class="px-4 py-3 text-sm font-medium text-gray-700" colspan="4">Итого по проекту</td>';
|
||
html += '<td class="px-4 py-3 text-sm text-gray-700">' + configs.length + '</td>';
|
||
html += '<td class="px-4 py-3 text-sm text-right font-semibold text-gray-900">$' + totalSum.toLocaleString('en-US', {minimumFractionDigits: 2}) + '</td>';
|
||
html += '<td class="px-4 py-3"></td>';
|
||
html += '</tr>';
|
||
html += '</tfoot>';
|
||
html += '</table></div>';
|
||
document.getElementById('configs-list').innerHTML = html;
|
||
}
|
||
|
||
async function loadProject() {
|
||
const resp = await fetch('/api/projects/' + projectUUID);
|
||
if (!resp.ok) {
|
||
document.getElementById('configs-list').innerHTML = '<div class="bg-white rounded-lg shadow p-8 text-center text-red-600">Проект не найден</div>';
|
||
return false;
|
||
}
|
||
project = await resp.json();
|
||
document.getElementById('project-title').textContent = project.name;
|
||
const trackerLink = document.getElementById('tracker-link');
|
||
if (trackerLink && project && project.name) {
|
||
trackerLink.href = 'https://tracker.yandex.ru/' + encodeURIComponent(project.name);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
async function loadConfigs() {
|
||
try {
|
||
const resp = await fetch('/api/projects/' + projectUUID + '/configs?status=' + configStatusMode);
|
||
if (!resp.ok) {
|
||
document.getElementById('configs-list').innerHTML =
|
||
'<div class="bg-white rounded-lg shadow p-8 text-center text-red-600">Ошибка загрузки</div>';
|
||
return;
|
||
}
|
||
|
||
const data = await resp.json();
|
||
allConfigs = (data.configurations || []);
|
||
renderConfigs(allConfigs);
|
||
} catch (e) {
|
||
document.getElementById('configs-list').innerHTML =
|
||
'<div class="bg-white rounded-lg shadow p-8 text-center text-red-600">Ошибка загрузки</div>';
|
||
}
|
||
}
|
||
|
||
function openCreateModal() {
|
||
document.getElementById('create-name').value = '';
|
||
document.getElementById('create-modal').classList.remove('hidden');
|
||
document.getElementById('create-modal').classList.add('flex');
|
||
document.getElementById('create-name').focus();
|
||
}
|
||
|
||
function closeCreateModal() {
|
||
document.getElementById('create-modal').classList.add('hidden');
|
||
document.getElementById('create-modal').classList.remove('flex');
|
||
}
|
||
|
||
async function createConfig() {
|
||
const name = document.getElementById('create-name').value.trim();
|
||
if (!name) {
|
||
alert('Введите название');
|
||
return;
|
||
}
|
||
const resp = await fetch('/api/projects/' + projectUUID + '/configs', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name: name, items: [], notes: '', server_count: 1})
|
||
});
|
||
if (!resp.ok) {
|
||
alert('Не удалось создать квоту');
|
||
return;
|
||
}
|
||
closeCreateModal();
|
||
loadConfigs();
|
||
}
|
||
|
||
async function deleteConfig(uuid) {
|
||
if (!confirm('Переместить квоту в архив?')) return;
|
||
await fetch('/api/configs/' + uuid, {method: 'DELETE'});
|
||
loadConfigs();
|
||
}
|
||
|
||
async function reactivateConfig(uuid) {
|
||
const resp = await fetch('/api/configs/' + uuid + '/reactivate', {method: 'POST'});
|
||
if (!resp.ok) {
|
||
alert('Не удалось восстановить квоту');
|
||
return;
|
||
}
|
||
const moved = await fetch('/api/configs/' + uuid + '/project', {
|
||
method: 'PATCH',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({project_uuid: projectUUID})
|
||
});
|
||
if (!moved.ok) {
|
||
alert('Квота восстановлена, но не удалось вернуть в проект');
|
||
}
|
||
loadConfigs();
|
||
}
|
||
|
||
function openRenameModal(uuid, currentName) {
|
||
document.getElementById('rename-uuid').value = uuid;
|
||
document.getElementById('rename-input').value = currentName;
|
||
document.getElementById('rename-modal').classList.remove('hidden');
|
||
document.getElementById('rename-modal').classList.add('flex');
|
||
}
|
||
|
||
function closeRenameModal() {
|
||
document.getElementById('rename-modal').classList.add('hidden');
|
||
document.getElementById('rename-modal').classList.remove('flex');
|
||
}
|
||
|
||
async function renameConfig() {
|
||
const uuid = document.getElementById('rename-uuid').value;
|
||
const name = document.getElementById('rename-input').value.trim();
|
||
if (!name) {
|
||
alert('Введите название');
|
||
return;
|
||
}
|
||
const resp = await fetch('/api/configs/' + uuid + '/rename', {
|
||
method: 'PATCH',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name: name})
|
||
});
|
||
if (!resp.ok) {
|
||
alert('Не удалось переименовать');
|
||
return;
|
||
}
|
||
closeRenameModal();
|
||
loadConfigs();
|
||
}
|
||
|
||
function openCloneModal(uuid, currentName) {
|
||
document.getElementById('clone-uuid').value = uuid;
|
||
document.getElementById('clone-input').value = currentName + ' (копия)';
|
||
document.getElementById('clone-modal').classList.remove('hidden');
|
||
document.getElementById('clone-modal').classList.add('flex');
|
||
}
|
||
|
||
function closeCloneModal() {
|
||
document.getElementById('clone-modal').classList.add('hidden');
|
||
document.getElementById('clone-modal').classList.remove('flex');
|
||
}
|
||
|
||
async function cloneConfig() {
|
||
const uuid = document.getElementById('clone-uuid').value;
|
||
const name = document.getElementById('clone-input').value.trim();
|
||
if (!name) {
|
||
alert('Введите название');
|
||
return;
|
||
}
|
||
const resp = await fetch('/api/projects/' + projectUUID + '/configs/' + uuid + '/clone', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name: name})
|
||
});
|
||
if (!resp.ok) {
|
||
alert('Не удалось скопировать');
|
||
return;
|
||
}
|
||
closeCloneModal();
|
||
loadConfigs();
|
||
}
|
||
|
||
function openImportModal() {
|
||
const activeOther = allConfigs.length ? null : null; // no-op placeholder
|
||
void activeOther;
|
||
document.getElementById('import-config-input').value = '';
|
||
document.getElementById('import-config-options').innerHTML = '';
|
||
loadImportOptions();
|
||
document.getElementById('import-modal').classList.remove('hidden');
|
||
document.getElementById('import-modal').classList.add('flex');
|
||
}
|
||
|
||
function closeImportModal() {
|
||
document.getElementById('import-modal').classList.add('hidden');
|
||
document.getElementById('import-modal').classList.remove('flex');
|
||
}
|
||
|
||
async function loadImportOptions() {
|
||
const resp = await fetch('/api/configs?page=1&per_page=500&status=active');
|
||
if (!resp.ok) return;
|
||
const data = await resp.json();
|
||
const options = document.getElementById('import-config-options');
|
||
options.innerHTML = '';
|
||
(data.configurations || [])
|
||
.filter(c => c.project_uuid !== projectUUID)
|
||
.forEach(c => {
|
||
const opt = document.createElement('option');
|
||
opt.value = c.name;
|
||
options.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
async function importConfigToProject() {
|
||
const query = document.getElementById('import-config-input').value.trim();
|
||
if (!query) {
|
||
alert('Выберите квоту');
|
||
return;
|
||
}
|
||
const resp = await fetch('/api/configs?page=1&per_page=500&status=active');
|
||
if (!resp.ok) {
|
||
alert('Не удалось загрузить список квот');
|
||
return;
|
||
}
|
||
const data = await resp.json();
|
||
const sourceConfigs = (data.configurations || []).filter(c => c.project_uuid !== projectUUID);
|
||
|
||
let targets = [];
|
||
if (query.includes('*')) {
|
||
targets = sourceConfigs.filter(c => wildcardMatch(c.name || '', query));
|
||
} else {
|
||
const found = sourceConfigs.find(c => (c.name || '').toLowerCase() === query.toLowerCase());
|
||
if (found) {
|
||
targets = [found];
|
||
}
|
||
}
|
||
|
||
if (!targets.length) {
|
||
alert('Подходящие квоты не найдены');
|
||
return;
|
||
}
|
||
|
||
let moved = 0;
|
||
let failed = 0;
|
||
for (const cfg of targets) {
|
||
const move = await fetch('/api/configs/' + cfg.uuid + '/project', {
|
||
method: 'PATCH',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({project_uuid: projectUUID})
|
||
});
|
||
if (move.ok) {
|
||
moved++;
|
||
} else {
|
||
failed++;
|
||
}
|
||
}
|
||
|
||
if (!moved) {
|
||
alert('Не удалось импортировать квоты');
|
||
return;
|
||
}
|
||
|
||
closeImportModal();
|
||
await loadConfigs();
|
||
if (targets.length > 1 || failed > 0) {
|
||
alert('Импорт завершен: перенесено ' + moved + ', ошибок ' + failed);
|
||
}
|
||
}
|
||
|
||
function wildcardMatch(value, pattern) {
|
||
const escaped = pattern
|
||
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||
.replace(/\*/g, '.*');
|
||
const regex = new RegExp('^' + escaped + '$', 'i');
|
||
return regex.test(value);
|
||
}
|
||
|
||
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('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.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') {
|
||
closeCreateModal();
|
||
closeRenameModal();
|
||
closeCloneModal();
|
||
closeImportModal();
|
||
}
|
||
});
|
||
|
||
document.addEventListener('DOMContentLoaded', async function() {
|
||
applyStatusModeUI();
|
||
const ok = await loadProject();
|
||
if (!ok) return;
|
||
await loadConfigs();
|
||
});
|
||
</script>
|
||
{{end}}
|
||
|
||
{{template "base" .}}
|