232 lines
11 KiB
HTML
232 lines
11 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="createProject()" 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>
|
||
|
||
<script>
|
||
let status = 'active';
|
||
let projectsSearch = '';
|
||
|
||
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 setStatus(value) {
|
||
status = value;
|
||
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 = [];
|
||
try {
|
||
const resp = await fetch('/api/projects?status=' + status + '&search=' + encodeURIComponent(projectsSearch));
|
||
if (!resp.ok) {
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
const data = await resp.json();
|
||
rows = data.projects || [];
|
||
} catch (e) {
|
||
root.innerHTML = '<div class="text-red-600">Ошибка загрузки проектов: ' + escapeHtml(String(e.message || e)) + '</div>';
|
||
return;
|
||
}
|
||
|
||
if (!rows.length) {
|
||
root.innerHTML = '<div class="text-gray-500">Проектов нет</div>';
|
||
return;
|
||
}
|
||
|
||
let html = '<div class="overflow-x-auto"><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-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 += '<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Действия</th>';
|
||
html += '</tr></thead><tbody class="divide-y">';
|
||
|
||
rows.forEach(p => {
|
||
html += '<tr class="hover:bg-gray-50">';
|
||
html += '<td class="px-4 py-3 text-sm font-medium"><a class="text-blue-600 hover:underline" href="/projects/' + p.uuid + '">' + escapeHtml(p.name) + '</a></td>';
|
||
html += '<td class="px-4 py-3 text-sm text-gray-600">' + escapeHtml(p.owner_username || '—') + '</td>';
|
||
html += '<td class="px-4 py-3 text-sm text-right text-gray-700">' + (p.config_count || 0) + '</td>';
|
||
html += '<td class="px-4 py-3 text-sm text-right text-gray-700">' + formatMoney(p.total) + '</td>';
|
||
html += '<td class="px-4 py-3 text-sm text-right"><div class="inline-flex items-center gap-2">';
|
||
|
||
if (p.is_active) {
|
||
html += '<button onclick="copyProject(\'' + p.uuid + '\', \'' + escapeHtml(p.name).replace(/'/g, "\\'") + '\')" 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(\'' + p.uuid + '\', \'' + escapeHtml(p.name).replace(/'/g, "\\'") + '\')" 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>';
|
||
root.innerHTML = html;
|
||
}
|
||
|
||
async function createProject() {
|
||
const name = prompt('Название проекта');
|
||
if (!name || !name.trim()) return;
|
||
const resp = await fetch('/api/projects', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name: name.trim()})
|
||
});
|
||
if (!resp.ok) {
|
||
alert('Не удалось создать проект');
|
||
return;
|
||
}
|
||
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) {
|
||
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 createResp = await fetch('/api/projects', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name: newName.trim()})
|
||
});
|
||
if (!createResp.ok) {
|
||
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();
|
||
}
|
||
|
||
loadProjects();
|
||
|
||
document.getElementById('projects-search').addEventListener('input', function(e) {
|
||
projectsSearch = (e.target.value || '').trim();
|
||
loadProjects();
|
||
});
|
||
</script>
|
||
{{end}}
|
||
|
||
{{template "base" .}}
|