Add projects table controls and sync status tab with app version
This commit is contained in:
@@ -30,6 +30,11 @@
|
||||
<script>
|
||||
let status = 'active';
|
||||
let projectsSearch = '';
|
||||
let authorSearch = '';
|
||||
let currentPage = 1;
|
||||
let perPage = 10;
|
||||
let sortField = 'created_at';
|
||||
let sortDir = 'desc';
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
@@ -41,8 +46,33 @@ 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 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';
|
||||
@@ -57,36 +87,73 @@ async function loadProjects() {
|
||||
root.innerHTML = '<div class="text-gray-500">Загрузка...</div>';
|
||||
|
||||
let rows = [];
|
||||
let total = 0;
|
||||
let totalPages = 0;
|
||||
let page = currentPage;
|
||||
try {
|
||||
const resp = await fetch('/api/projects?status=' + status + '&search=' + encodeURIComponent(projectsSearch));
|
||||
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 || [];
|
||||
total = data.total || 0;
|
||||
totalPages = data.total_pages || 0;
|
||||
page = data.page || currentPage;
|
||||
currentPage = page;
|
||||
} 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 += '<thead class="bg-gray-50">';
|
||||
html += '<tr>';
|
||||
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="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(\'created_at\')" class="inline-flex items-center gap-1 hover:text-gray-700">Создан';
|
||||
if (sortField === 'created_at') {
|
||||
html += sortDir === 'asc' ? ' <span>↑</span>' : ' <span>↓</span>';
|
||||
}
|
||||
html += '</button></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">';
|
||||
html += '</tr>';
|
||||
html += '<tr>';
|
||||
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 += '<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">';
|
||||
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-gray-600">' + escapeHtml(formatDateTime(p.created_at)) + '</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">';
|
||||
@@ -117,7 +184,38 @@ async function loadProjects() {
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
async function createProject() {
|
||||
@@ -223,6 +321,7 @@ loadProjects();
|
||||
|
||||
document.getElementById('projects-search').addEventListener('input', function(e) {
|
||||
projectsSearch = (e.target.value || '').trim();
|
||||
currentPage = 1;
|
||||
loadProjects();
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user