v1.1
This commit is contained in:
+481
@@ -0,0 +1,481 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||
<title>Комитеты бизнес-объединений ДНР</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Inter', sans-serif; background: #f8fafc; color: #0f172a; line-height: 1.5; padding: 2rem 1rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
|
||||
.header { text-align: center; margin-bottom: 2rem; }
|
||||
.header h1 { font-size: 2rem; font-weight: 700; background: linear-gradient(135deg, #1e3a5f, #0f2b40); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
||||
.header p { color: #475569; margin-top: 0.5rem; }
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #1e3a5f;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
|
||||
/* Фильтры */
|
||||
.filters {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
border: 1px solid #eef2f6;
|
||||
}
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
.filter-group label {
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
}
|
||||
.org-filter {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
background: white;
|
||||
}
|
||||
.status-filter {
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
background: white;
|
||||
}
|
||||
.search-input {
|
||||
flex: 2;
|
||||
min-width: 250px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.stats {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Сетка комитетов */
|
||||
.committees-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.committee-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #eef2f6;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.03);
|
||||
}
|
||||
.committee-card:hover {
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.05);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.card-header {
|
||||
padding: 1.2rem;
|
||||
background: linear-gradient(135deg, #1e3a5f, #0f2b40);
|
||||
color: white;
|
||||
}
|
||||
.org-badge {
|
||||
display: inline-block;
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.committee-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.committee-status {
|
||||
display: inline-block;
|
||||
font-size: 0.7rem;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
.card-body {
|
||||
padding: 1.2rem;
|
||||
}
|
||||
.leader-section {
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #eef2f6;
|
||||
}
|
||||
.leader-title {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #6c86a3;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.leader-name {
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.leader-contacts {
|
||||
font-size: 0.75rem;
|
||||
color: #475569;
|
||||
}
|
||||
.leader-contacts a {
|
||||
color: #1e3a5f;
|
||||
text-decoration: none;
|
||||
}
|
||||
.leader-contacts a:hover { text-decoration: underline; }
|
||||
.leader-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.tag {
|
||||
background: #e2e8f0;
|
||||
padding: 2px 8px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.7rem;
|
||||
color: #334155;
|
||||
}
|
||||
.committee-description {
|
||||
font-size: 0.85rem;
|
||||
color: #475569;
|
||||
line-height: 1.5;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid #eef2f6;
|
||||
}
|
||||
.expand-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #1e3a5f;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.full-description {
|
||||
display: none;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
font-size: 0.85rem;
|
||||
color: #334155;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.full-description.open { display: block; }
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #64748b;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: #6c86a3;
|
||||
border-top: 1px solid #e2edf7;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.committees-grid { grid-template-columns: 1fr; }
|
||||
.filter-group { flex-direction: column; align-items: stretch; }
|
||||
.org-filter, .status-filter, .search-input { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a href="/" class="back-link"><i class="fas fa-arrow-left"></i> Назад к списку организаций</a>
|
||||
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-users"></i> Комитеты бизнес-объединений</h1>
|
||||
<p>Детальная информация о комитетах, их руководителях и деятельности</p>
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<div class="filter-group">
|
||||
<label><i class="fas fa-building"></i> Организация:</label>
|
||||
<select id="orgFilter" class="org-filter">
|
||||
<option value="all">Все организации</option>
|
||||
</select>
|
||||
|
||||
<label><i class="fas fa-flag-checkered"></i> Статус:</label>
|
||||
<select id="statusFilter" class="status-filter">
|
||||
<option value="all">Все статусы</option>
|
||||
<option value="active">Активные</option>
|
||||
<option value="medium">Средняя активность</option>
|
||||
<option value="inactive">Неактивные</option>
|
||||
<option value="candidate">С кандидатом</option>
|
||||
</select>
|
||||
|
||||
<label><i class="fas fa-search"></i> Поиск:</label>
|
||||
<input type="text" id="searchInput" class="search-input" placeholder="Название комитета, руководитель...">
|
||||
</div>
|
||||
<div class="stats" id="stats">
|
||||
Загрузка...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="committeesContainer" class="committees-grid">
|
||||
<div class="loading"><i class="fas fa-spinner fa-spin"></i> Загрузка комитетов...</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-note">
|
||||
<i class="fas fa-info-circle"></i> Данные о комитетах загружаются из папки committees/.
|
||||
Статус активности определяется на основе информации о руководителе.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let allCommittees = [];
|
||||
let organizationsList = [];
|
||||
|
||||
async function loadOrganizations() {
|
||||
try {
|
||||
const response = await fetch('/data.json');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
organizationsList = data.organizations || [];
|
||||
// Добавляем также дополнительные организации, если есть комитеты
|
||||
const additional = data.additionalOrganizations || [];
|
||||
organizationsList = [...organizationsList, ...additional];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки организаций:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCommittees() {
|
||||
const committeeFiles = [
|
||||
{ file: '/committees/opora_committees.json', orgName: 'ОПОРА РОССИИ' },
|
||||
// Добавьте другие файлы комитетов по мере появления:
|
||||
// { file: '/committees/deloros_committees.json', orgName: 'Деловая Россия' },
|
||||
// { file: '/committees/spp_committees.json', orgName: 'Союз промышленников и предпринимателей ДНР' },
|
||||
];
|
||||
|
||||
allCommittees = [];
|
||||
|
||||
for (const source of committeeFiles) {
|
||||
try {
|
||||
const response = await fetch(source.file);
|
||||
if (response.ok) {
|
||||
const committees = await response.json();
|
||||
committees.forEach(committee => {
|
||||
allCommittees.push({
|
||||
...committee,
|
||||
organization: source.orgName,
|
||||
organizationId: source.orgName.toLowerCase().replace(/[^a-z0-9]/g, '_')
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Файл ${source.file} не найден`);
|
||||
}
|
||||
}
|
||||
|
||||
renderFilters();
|
||||
renderCommittees();
|
||||
}
|
||||
|
||||
function renderFilters() {
|
||||
const orgSelect = document.getElementById('orgFilter');
|
||||
const uniqueOrgs = [...new Set(allCommittees.map(c => c.organization))];
|
||||
|
||||
orgSelect.innerHTML = '<option value="all">Все организации</option>';
|
||||
uniqueOrgs.forEach(org => {
|
||||
const option = document.createElement('option');
|
||||
option.value = org;
|
||||
option.textContent = org;
|
||||
orgSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function getLeaderStatus(leader) {
|
||||
if (!leader || !leader.status) return 'unknown';
|
||||
return leader.status;
|
||||
}
|
||||
|
||||
function filterCommittees() {
|
||||
const orgFilter = document.getElementById('orgFilter').value;
|
||||
const statusFilter = document.getElementById('statusFilter').value;
|
||||
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||||
|
||||
return allCommittees.filter(committee => {
|
||||
// Фильтр по организации
|
||||
if (orgFilter !== 'all' && committee.organization !== orgFilter) return false;
|
||||
|
||||
// Фильтр по статусу
|
||||
if (statusFilter !== 'all') {
|
||||
const leaderStatus = getLeaderStatus(committee.current_leader);
|
||||
if (statusFilter === 'candidate') {
|
||||
if (!committee.candidate_for_leader?.name) return false;
|
||||
} else if (leaderStatus !== statusFilter) return false;
|
||||
}
|
||||
|
||||
// Поиск
|
||||
if (searchTerm) {
|
||||
const searchFields = [
|
||||
committee.name,
|
||||
committee.current_leader?.name,
|
||||
committee.current_leader?.title,
|
||||
committee.candidate_for_leader?.name,
|
||||
committee.description
|
||||
].filter(Boolean).join(' ').toLowerCase();
|
||||
if (!searchFields.includes(searchTerm)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function renderCommittees() {
|
||||
const container = document.getElementById('committeesContainer');
|
||||
const filtered = filterCommittees();
|
||||
const stats = document.getElementById('stats');
|
||||
|
||||
stats.innerHTML = `Найдено: ${filtered.length} комитетов из ${allCommittees.length}`;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = '<div class="no-results"><i class="fas fa-inbox"></i><br>Комитеты не найдены. Попробуйте изменить параметры фильтрации.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
filtered.forEach((committee, idx) => {
|
||||
const leader = committee.current_leader;
|
||||
const candidate = committee.candidate_for_leader;
|
||||
const statusClass = leader?.status === 'active' ? 'Активен' : (leader?.status === 'medium' ? 'Средняя активность' : 'Статус не определён');
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'committee-card';
|
||||
card.innerHTML = `
|
||||
<div class="card-header">
|
||||
<div class="org-badge"><i class="fas fa-building"></i> ${escapeHtml(committee.organization)}</div>
|
||||
<div class="committee-name">${escapeHtml(committee.name)}</div>
|
||||
<div class="committee-status">${statusClass}</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="leader-section">
|
||||
<div class="leader-title"><i class="fas fa-user-tie"></i> Текущий руководитель</div>
|
||||
<div class="leader-name">${leader?.name ? escapeHtml(leader.name) : '—'}</div>
|
||||
<div class="leader-contacts">
|
||||
${leader?.contacts?.email ? `<i class="fas fa-envelope"></i> <a href="mailto:${escapeHtml(leader.contacts.email)}">${escapeHtml(leader.contacts.email)}</a><br>` : ''}
|
||||
${leader?.contacts?.phone ? `<i class="fas fa-phone"></i> <a href="tel:${escapeHtml(leader.contacts.phone)}">${escapeHtml(leader.contacts.phone)}</a><br>` : ''}
|
||||
${leader?.contacts?.telegram ? `<i class="fab fa-telegram"></i> <a href="https://t.me/${escapeHtml(leader.contacts.telegram.replace('@', ''))}" target="_blank">${escapeHtml(leader.contacts.telegram)}</a><br>` : ''}
|
||||
</div>
|
||||
<div class="leader-tags">
|
||||
${leader?.title ? `<span class="tag">${escapeHtml(leader.title)}</span>` : ''}
|
||||
${leader?.additional_info ? `<span class="tag">${escapeHtml(leader.additional_info.substring(0, 50))}${leader.additional_info.length > 50 ? '...' : ''}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${candidate?.name ? `
|
||||
<div class="leader-section">
|
||||
<div class="leader-title"><i class="fas fa-user-plus"></i> Кандидат на должность</div>
|
||||
<div class="leader-name">${escapeHtml(candidate.name)}</div>
|
||||
<div class="leader-contacts">
|
||||
${candidate.contacts?.email ? `<i class="fas fa-envelope"></i> <a href="mailto:${escapeHtml(candidate.contacts.email)}">${escapeHtml(candidate.contacts.email)}</a><br>` : ''}
|
||||
${candidate.contacts?.phone ? `<i class="fas fa-phone"></i> <a href="tel:${escapeHtml(candidate.contacts.phone)}">${escapeHtml(candidate.contacts.phone)}</a><br>` : ''}
|
||||
${candidate.contacts?.telegram ? `<i class="fab fa-telegram"></i> <a href="https://t.me/${escapeHtml(candidate.contacts.telegram.replace('@', ''))}" target="_blank">${escapeHtml(candidate.contacts.telegram)}</a><br>` : ''}
|
||||
</div>
|
||||
<div class="leader-tags">
|
||||
${candidate.title ? `<span class="tag">${escapeHtml(candidate.title)}</span>` : ''}
|
||||
${candidate.additional_info ? `<span class="tag">${escapeHtml(candidate.additional_info.substring(0, 50))}${candidate.additional_info.length > 50 ? '...' : ''}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="committee-description">
|
||||
<strong>О комитете:</strong><br>
|
||||
${escapeHtml(committee.description.substring(0, 200))}${committee.description.length > 200 ? '...' : ''}
|
||||
</div>
|
||||
|
||||
${committee.description.length > 200 ? `
|
||||
<button class="expand-btn" onclick="toggleDescription(this)">
|
||||
<i class="fas fa-chevron-down"></i> Читать полностью
|
||||
</button>
|
||||
<div class="full-description">
|
||||
${escapeHtml(committee.description)}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleDescription(btn) {
|
||||
const fullDesc = btn.nextElementSibling;
|
||||
const icon = btn.querySelector('i');
|
||||
if (fullDesc.classList.contains('open')) {
|
||||
fullDesc.classList.remove('open');
|
||||
icon.className = 'fas fa-chevron-down';
|
||||
btn.innerHTML = '<i class="fas fa-chevron-down"></i> Читать полностью';
|
||||
} else {
|
||||
fullDesc.classList.add('open');
|
||||
icon.className = 'fas fa-chevron-up';
|
||||
btn.innerHTML = '<i class="fas fa-chevron-up"></i> Свернуть';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>]/g, m => ({ '&': '&', '<': '<', '>': '>' }[m]));
|
||||
}
|
||||
|
||||
// Добавляем обработчики фильтров
|
||||
document.getElementById('orgFilter').addEventListener('change', () => renderCommittees());
|
||||
document.getElementById('statusFilter').addEventListener('change', () => renderCommittees());
|
||||
document.getElementById('searchInput').addEventListener('input', () => renderCommittees());
|
||||
|
||||
// Загрузка данных
|
||||
async function init() {
|
||||
await loadOrganizations();
|
||||
await loadCommittees();
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,404 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||
<title>Редактор комитетов</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Inter', sans-serif; background: #f8fafc; color: #0f172a; line-height: 1.5; padding: 2rem 1rem; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
|
||||
.header { text-align: center; margin-bottom: 2rem; }
|
||||
.header h1 { font-size: 2rem; font-weight: 700; background: linear-gradient(135deg, #1e3a5f, #0f2b40); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
||||
.header p { color: #475569; margin-top: 0.5rem; }
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #1e3a5f;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
|
||||
.committee-selector {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid #eef2f6;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.committee-selector select {
|
||||
flex: 2;
|
||||
min-width: 250px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.committee-selector button {
|
||||
background: #1e3a5f;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.editor-form {
|
||||
background: white;
|
||||
border-radius: 24px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #eef2f6;
|
||||
}
|
||||
.form-section {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #eef2f6;
|
||||
}
|
||||
.section-title {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
color: #1e3a5f;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: #475569;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.form-group input, .form-group textarea, .form-group select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #cbd5e1;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.form-group textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
.form-3col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
.status-select {
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
}
|
||||
.save-btn {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-top: 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.save-btn:hover { background: #059669; }
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
background: #15803d;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 50px;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
.toast.error { background: #dc2626; }
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.form-row, .form-3col { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a href="/committees.html" class="back-link"><i class="fas fa-arrow-left"></i> Назад к списку комитетов</a>
|
||||
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-edit"></i> Редактор комитетов</h1>
|
||||
<p>Редактирование информации о руководителях и кандидатах комитетов</p>
|
||||
</div>
|
||||
|
||||
<div class="committee-selector">
|
||||
<select id="committeeSelect">
|
||||
<option value="">-- Выберите комитет --</option>
|
||||
</select>
|
||||
<button id="loadCommitteeBtn"><i class="fas fa-folder-open"></i> Загрузить</button>
|
||||
</div>
|
||||
|
||||
<div id="editorContainer" style="display: none;">
|
||||
<form id="editorForm" class="editor-form">
|
||||
<div class="form-section">
|
||||
<div class="section-title"><i class="fas fa-info-circle"></i> Информация о комитете</div>
|
||||
<div class="form-group">
|
||||
<label>Название комитета</label>
|
||||
<input type="text" id="committeeName" readonly style="background:#f1f5f9;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Описание деятельности</label>
|
||||
<textarea id="committeeDescription" readonly style="background:#f1f5f9;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title"><i class="fas fa-user-tie"></i> Текущий руководитель</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>ФИО руководителя</label>
|
||||
<input type="text" id="leaderName" placeholder="Иванов Иван Иванович">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Должность / Регалии</label>
|
||||
<input type="text" id="leaderTitle" placeholder="Председатель комитета">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-3col">
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" id="leaderEmail" placeholder="email@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Телефон</label>
|
||||
<input type="text" id="leaderPhone" placeholder="+7 (xxx) xxx-xx-xx">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Telegram</label>
|
||||
<input type="text" id="leaderTelegram" placeholder="@username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Статус активности</label>
|
||||
<select id="leaderStatus" class="status-select">
|
||||
<option value="active">Активен</option>
|
||||
<option value="medium">Средняя активность</option>
|
||||
<option value="inactive">Не активен</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Дополнительная информация</label>
|
||||
<input type="text" id="leaderAdditional" placeholder="Член правления, эксперт и т.д.">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title"><i class="fas fa-user-plus"></i> Кандидат на должность руководителя</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>ФИО кандидата</label>
|
||||
<input type="text" id="candidateName" placeholder="Петров Петр Петрович">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Должность / Регалии</label>
|
||||
<input type="text" id="candidateTitle" placeholder="Заместитель председателя">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-3col">
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" id="candidateEmail" placeholder="email@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Телефон</label>
|
||||
<input type="text" id="candidatePhone" placeholder="+7 (xxx) xxx-xx-xx">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Telegram</label>
|
||||
<input type="text" id="candidateTelegram" placeholder="@username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Статус кандидата</label>
|
||||
<select id="candidateStatus" class="status-select">
|
||||
<option value="candidate">Кандидат</option>
|
||||
<option value="active">Активен</option>
|
||||
<option value="medium">Средняя активность</option>
|
||||
<option value="inactive">Не активен</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Дополнительная информация</label>
|
||||
<input type="text" id="candidateAdditional" placeholder="Опыт работы, достижения">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="save-btn"><i class="fas fa-save"></i> Сохранить изменения</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast">✅ Данные сохранены</div>
|
||||
|
||||
<script>
|
||||
let committeesData = [];
|
||||
let currentCommitteeIndex = -1;
|
||||
let currentFile = 'opora_committees.json';
|
||||
|
||||
function showToast(msg, isError = false) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = msg;
|
||||
toast.className = isError ? 'toast error' : 'toast';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(() => { toast.style.display = 'none'; }, 2500);
|
||||
}
|
||||
|
||||
async function loadCommittees() {
|
||||
try {
|
||||
const response = await fetch(`/committees/${currentFile}`);
|
||||
if (response.ok) {
|
||||
committeesData = await response.json();
|
||||
populateSelect();
|
||||
showToast('Комитеты загружены');
|
||||
} else {
|
||||
showToast('Ошибка загрузки файла комитетов', true);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Ошибка загрузки: ' + error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function populateSelect() {
|
||||
const select = document.getElementById('committeeSelect');
|
||||
select.innerHTML = '<option value="">-- Выберите комитет --</option>';
|
||||
committeesData.forEach((committee, idx) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = idx;
|
||||
option.textContent = committee.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function loadCommitteeToForm(index) {
|
||||
const committee = committeesData[index];
|
||||
if (!committee) return;
|
||||
|
||||
currentCommitteeIndex = index;
|
||||
|
||||
document.getElementById('committeeName').value = committee.name;
|
||||
document.getElementById('committeeDescription').value = committee.description;
|
||||
|
||||
// Текущий руководитель
|
||||
document.getElementById('leaderName').value = committee.current_leader?.name || '';
|
||||
document.getElementById('leaderTitle').value = committee.current_leader?.title || '';
|
||||
document.getElementById('leaderEmail').value = committee.current_leader?.contacts?.email || '';
|
||||
document.getElementById('leaderPhone').value = committee.current_leader?.contacts?.phone || '';
|
||||
document.getElementById('leaderTelegram').value = committee.current_leader?.contacts?.telegram || '';
|
||||
document.getElementById('leaderStatus').value = committee.current_leader?.status || 'active';
|
||||
document.getElementById('leaderAdditional').value = committee.current_leader?.additional_info || '';
|
||||
|
||||
// Кандидат
|
||||
document.getElementById('candidateName').value = committee.candidate_for_leader?.name || '';
|
||||
document.getElementById('candidateTitle').value = committee.candidate_for_leader?.title || '';
|
||||
document.getElementById('candidateEmail').value = committee.candidate_for_leader?.contacts?.email || '';
|
||||
document.getElementById('candidatePhone').value = committee.candidate_for_leader?.contacts?.phone || '';
|
||||
document.getElementById('candidateTelegram').value = committee.candidate_for_leader?.contacts?.telegram || '';
|
||||
document.getElementById('candidateStatus').value = committee.candidate_for_leader?.status || 'candidate';
|
||||
document.getElementById('candidateAdditional').value = committee.candidate_for_leader?.additional_info || '';
|
||||
|
||||
document.getElementById('editorContainer').style.display = 'block';
|
||||
}
|
||||
|
||||
function saveCommittee() {
|
||||
const committee = committeesData[currentCommitteeIndex];
|
||||
if (!committee) return;
|
||||
|
||||
// Сохраняем данные текущего руководителя
|
||||
committee.current_leader = {
|
||||
name: document.getElementById('leaderName').value,
|
||||
contacts: {
|
||||
email: document.getElementById('leaderEmail').value,
|
||||
phone: document.getElementById('leaderPhone').value,
|
||||
telegram: document.getElementById('leaderTelegram').value
|
||||
},
|
||||
title: document.getElementById('leaderTitle').value,
|
||||
status: document.getElementById('leaderStatus').value,
|
||||
additional_info: document.getElementById('leaderAdditional').value
|
||||
};
|
||||
|
||||
// Сохраняем данные кандидата
|
||||
committee.candidate_for_leader = {
|
||||
name: document.getElementById('candidateName').value,
|
||||
contacts: {
|
||||
email: document.getElementById('candidateEmail').value,
|
||||
phone: document.getElementById('candidatePhone').value,
|
||||
telegram: document.getElementById('candidateTelegram').value
|
||||
},
|
||||
title: document.getElementById('candidateTitle').value,
|
||||
status: document.getElementById('candidateStatus').value,
|
||||
additional_info: document.getElementById('candidateAdditional').value
|
||||
};
|
||||
|
||||
saveToServer();
|
||||
}
|
||||
|
||||
async function saveToServer() {
|
||||
try {
|
||||
const response = await fetch(`/save_committees/${currentFile}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(committeesData)
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showToast('✅ Данные сохранены');
|
||||
} else {
|
||||
showToast('❌ Ошибка сохранения: ' + result.message, true);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('❌ Ошибка соединения: ' + error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('loadCommitteeBtn').addEventListener('click', () => {
|
||||
const idx = document.getElementById('committeeSelect').value;
|
||||
if (idx !== '') {
|
||||
loadCommitteeToForm(parseInt(idx));
|
||||
} else {
|
||||
showToast('Выберите комитет', true);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('editorForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
saveCommittee();
|
||||
});
|
||||
|
||||
// Загрузка данных
|
||||
loadCommittees();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+130
-648
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||
<title>Бизнес-объединения ДНР | Аналитический обзор + Админ-панель</title>
|
||||
<title>Бизнес-объединения ДНР | Аналитический обзор</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<style>
|
||||
@@ -44,116 +44,56 @@
|
||||
.org-details { padding: 1.5rem; background: #fefefe; border-top: 1px solid #f1f5f9; display: none; }
|
||||
.org-details.open { display: block; }
|
||||
|
||||
/* Стили для комитетов */
|
||||
.committees-section { margin-top: 1.5rem; }
|
||||
.committees-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1rem; margin-top: 1rem; }
|
||||
.committee-card { background: #f8fafc; border-radius: 16px; border: 1px solid #e2e8f0; overflow: hidden; transition: all 0.2s; }
|
||||
.committee-card:hover { border-color: #cbd5e1; background: #fefefe; }
|
||||
.committee-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem; background: #f1f5f9; cursor: pointer; }
|
||||
.committee-name { font-weight: 600; color: #1e3a5f; }
|
||||
.committee-status { font-size: 0.7rem; padding: 2px 8px; border-radius: 20px; }
|
||||
.status-active { background: #dcfce7; color: #15803d; }
|
||||
.status-medium { background: #fef3c7; color: #b45309; }
|
||||
.status-inactive { background: #fee2e2; color: #dc2626; }
|
||||
.committee-details { padding: 1rem; display: none; border-top: 1px solid #e2e8f0; }
|
||||
.committee-details.open { display: block; }
|
||||
.leader-info { background: white; border-radius: 12px; padding: 0.75rem; margin-bottom: 0.75rem; border-left: 3px solid #1e3a5f; }
|
||||
.leader-label { font-size: 0.7rem; text-transform: uppercase; color: #6c86a3; font-weight: 600; }
|
||||
.leader-name { font-weight: 700; color: #0f172a; margin: 4px 0; }
|
||||
.leader-contacts { font-size: 0.8rem; color: #475569; }
|
||||
.leader-contacts a { color: #1e3a5f; text-decoration: none; }
|
||||
.leader-contacts a:hover { text-decoration: underline; }
|
||||
.committee-description { font-size: 0.85rem; color: #334155; line-height: 1.5; margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid #e2e8f0; }
|
||||
|
||||
.details-section { margin-bottom: 1.5rem; }
|
||||
.section-title { font-weight: 700; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.5px; color: #5b6e8c; margin-bottom: 0.8rem; border-left: 3px solid #1e3a5f; padding-left: 10px; }
|
||||
.section-title i { margin-right: 8px; }
|
||||
|
||||
.info-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; }
|
||||
.info-card { background: #f8fafc; border-radius: 16px; padding: 0.8rem 1rem; transition: all 0.2s; }
|
||||
.info-card .label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.3px; font-weight: 600; color: #6c86a3; margin-bottom: 0.3rem; }
|
||||
.info-card .value { font-size: 0.9rem; font-weight: 500; color: #1e2a44; line-height: 1.4; word-break: break-word; }
|
||||
.info-card .value a { color: #1e3a5f; text-decoration: none; }
|
||||
.info-card .value a:hover { text-decoration: underline; }
|
||||
|
||||
.full-width { grid-column: 1 / -1; }
|
||||
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 5px; }
|
||||
.tag { background: #e2e8f0; padding: 3px 10px; border-radius: 20px; font-size: 0.75rem; color: #334155; }
|
||||
.links-list { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.link-item { background: #e2e8f0; padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; }
|
||||
.link-item a { color: #1e3a5f; text-decoration: none; }
|
||||
.link-item a:hover { text-decoration: underline; }
|
||||
|
||||
.links-cell { max-width: 250px; }
|
||||
.resource-link {
|
||||
display: inline-block;
|
||||
background: #eef2ff;
|
||||
color: #1e3a5f;
|
||||
padding: 3px 8px;
|
||||
border-radius: 16px;
|
||||
font-size: 0.7rem;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 120px;
|
||||
}
|
||||
.links-list { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.resource-link { display: inline-block; background: #eef2ff; color: #1e3a5f; padding: 4px 12px; border-radius: 20px; font-size: 0.75rem; text-decoration: none; }
|
||||
.resource-link:hover { background: #1e3a5f; color: white; }
|
||||
|
||||
hr { margin: 1rem 0; border-color: #ecf3f9; }
|
||||
|
||||
.admin-panel {
|
||||
position: fixed; top: 0; right: -650px; width: 650px; height: 100vh;
|
||||
background: white; box-shadow: -5px 0 30px rgba(0,0,0,0.2);
|
||||
z-index: 1001; transition: right 0.3s ease; overflow-y: auto; padding: 1.5rem;
|
||||
}
|
||||
.admin-panel.open { right: 0; }
|
||||
.admin-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 2px solid #e2e8f0; }
|
||||
.admin-header h3 { font-size: 1.3rem; color: #1e3a5f; }
|
||||
.close-admin { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #64748b; }
|
||||
.org-selector { margin-bottom: 1.5rem; }
|
||||
.org-selector select { width: 100%; padding: 10px; border-radius: 12px; border: 1px solid #cbd5e1; font-size: 0.9rem; }
|
||||
.org-type-selector { margin-bottom: 1rem; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
|
||||
.org-type-selector label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
|
||||
.edit-form { display: flex; flex-direction: column; gap: 1rem; }
|
||||
.form-group { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
.form-group label { font-weight: 600; font-size: 0.8rem; color: #475569; }
|
||||
.form-group label .hint { font-weight: normal; color: #6c86a3; font-size: 0.7rem; }
|
||||
.form-group input, .form-group textarea, .form-group select { padding: 8px 12px; border-radius: 10px; border: 1px solid #cbd5e1; font-family: 'Inter', sans-serif; font-size: 0.85rem; }
|
||||
.form-group textarea { min-height: 60px; resize: vertical; }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||
|
||||
.links-container { border: 1px solid #e2e8f0; border-radius: 12px; padding: 0.75rem; background: #fafcff; }
|
||||
.link-input-group { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; }
|
||||
.link-input-group input { flex: 1; }
|
||||
.remove-link-btn { background: #fee2e2; color: #dc2626; border: none; width: 32px; height: 32px; border-radius: 8px; cursor: pointer; }
|
||||
.remove-link-btn:hover { background: #fecaca; }
|
||||
.add-link-btn { background: #e2e8f0; border: none; padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 0.8rem; margin-top: 8px; }
|
||||
.add-link-btn:hover { background: #cbd5e1; }
|
||||
|
||||
.btn-group { display: flex; flex-direction: column; gap: 0.75rem; margin-top: 1rem; }
|
||||
.save-btn { background: #1e3a5f; color: white; border: none; padding: 12px; border-radius: 12px; font-weight: 600; cursor: pointer; }
|
||||
.save-btn:hover { background: #0f2b40; }
|
||||
.delete-btn { background: #dc2626; color: white; border: none; padding: 12px; border-radius: 12px; font-weight: 600; cursor: pointer; }
|
||||
.delete-btn:hover { background: #b91c1c; }
|
||||
.add-btn { background: #10b981; color: white; border: none; padding: 12px; border-radius: 12px; font-weight: 600; cursor: pointer; }
|
||||
.add-btn:hover { background: #059669; }
|
||||
.move-btn { background: #f59e0b; color: white; border: none; padding: 12px; border-radius: 12px; font-weight: 600; cursor: pointer; }
|
||||
.move-btn:hover { background: #d97706; }
|
||||
.reset-btn { background: #e2e8f0; color: #1e293b; border: none; padding: 12px; border-radius: 12px; font-weight: 600; cursor: pointer; }
|
||||
.reset-btn:hover { background: #cbd5e1; }
|
||||
|
||||
.toast { position: fixed; bottom: 80px; right: 30px; background: #15803d; color: white; padding: 12px 20px; border-radius: 50px; z-index: 1002; display: none; }
|
||||
.toast.error { background: #dc2626; }
|
||||
.overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; display: none; }
|
||||
.overlay.show { display: block; }
|
||||
|
||||
.summary-wrapper { overflow-x: auto; background: white; border-radius: 24px; }
|
||||
.summary-table { width: 100%; border-collapse: collapse; }
|
||||
.summary-table th { background: #f1f5f9; padding: 14px; text-align: left; font-weight: 600; font-size: 0.85rem; }
|
||||
.summary-table td { padding: 14px; border-bottom: 1px solid #eef2f8; font-size: 0.85rem; vertical-align: middle; }
|
||||
.summary-table tr:hover { background: #fafcff; }
|
||||
.action-btns { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.edit-row-btn, .move-to-main-btn, .move-to-additional-btn, .delete-row-btn {
|
||||
background: none; border: none; cursor: pointer; font-size: 1rem; padding: 4px 8px;
|
||||
border-radius: 8px; transition: all 0.2s;
|
||||
}
|
||||
.edit-row-btn { color: #1e3a5f; }
|
||||
.edit-row-btn:hover { background: #eef2ff; }
|
||||
.move-to-main-btn { color: #10b981; }
|
||||
.move-to-main-btn:hover { background: #dcfce7; }
|
||||
.move-to-additional-btn { color: #f59e0b; }
|
||||
.move-to-additional-btn:hover { background: #fef3c7; }
|
||||
.delete-row-btn { color: #dc2626; }
|
||||
.delete-row-btn:hover { background: #fee2e2; }
|
||||
|
||||
.footer-note { margin-top: 2rem; text-align: center; font-size: 0.75rem; color: #6c86a3; }
|
||||
.loading { text-align: center; padding: 3rem; color: #64748b; }
|
||||
|
||||
@media (max-width: 750px) {
|
||||
.admin-panel { width: 100%; right: -100%; }
|
||||
.form-row { grid-template-columns: 1fr; }
|
||||
.committees-grid { grid-template-columns: 1fr; }
|
||||
.info-grid { grid-template-columns: 1fr; }
|
||||
.action-btns { flex-direction: column; gap: 4px; }
|
||||
.links-cell { max-width: 180px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -161,14 +101,20 @@
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-chart-line"></i> Бизнес-объединения ДНР</h1>
|
||||
<p>Аналитический обзор с расширенной информацией по основным организациям</p>
|
||||
<p>Аналитический обзор с детальной информацией по комитетам организаций</p>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="detailed"><i class="fas fa-chart-simple"></i> Детальный анализ (расширенный)</button>
|
||||
<button class="tab-btn active" data-tab="detailed"><i class="fas fa-chart-simple"></i> Детальный анализ</button>
|
||||
<button class="tab-btn" data-tab="summary"><i class="fas fa-table-list"></i> Сводная таблица</button>
|
||||
<div>
|
||||
<a href="/committees.html" style="display: inline-flex; align-items: center; gap: 8px; background: #1e3a5f; color: white; padding: 8px 20px; border-radius: 40px; text-decoration: none; font-size: 0.9rem;">
|
||||
<i class="fas fa-users"></i> Комитеты организаций
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="detailedPanel" class="tab-panel active-panel">
|
||||
<div id="detailedContainer" class="org-grid"><div class="loading"><i class="fas fa-spinner fa-spin"></i> Загрузка...</div></div>
|
||||
</div>
|
||||
@@ -177,123 +123,20 @@
|
||||
<div class="summary-wrapper">
|
||||
<table class="summary-table">
|
||||
<thead>
|
||||
<tr><th>Название</th><th>Тип</th><th>Статус</th><th>Членство</th><th>Приоритет</th><th>Ресурсы</th><th style="width: 180px;">Действия</th></tr>
|
||||
</thead>
|
||||
<tbody id="summaryTableBody"><tr><td colspan="7" class="loading">Загрузка...</td></tr></tbody>
|
||||
<tr><th>Название</th><th>Тип</th><th>Статус</th><th>Членство</th><th>Приоритет</th><th>Ресурсы</th></thead>
|
||||
<tbody id="summaryTableBody"><td><td colspan="6" class="loading">Загрузка...</tr></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-note">
|
||||
<i class="fas fa-info-circle"></i> Данные хранятся в файле data.json. Все изменения сохраняются на сервере.
|
||||
<i class="fas fa-info-circle"></i> Данные хранятся в файле data.json. Комитеты организаций загружаются из папки committees/.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="admin-toggle" id="adminToggleBtn"><i class="fas fa-user-shield"></i> Админ-панель</button>
|
||||
<div class="overlay" id="overlay"></div>
|
||||
<div class="admin-panel" id="adminPanel">
|
||||
<div class="admin-header">
|
||||
<h3><i class="fas fa-edit"></i> Редактор организаций</h3>
|
||||
<button class="close-admin" id="closeAdminBtn">×</button>
|
||||
</div>
|
||||
<div class="org-selector">
|
||||
<label>Выберите организацию:</label>
|
||||
<select id="orgSelect"></select>
|
||||
</div>
|
||||
<div class="org-type-selector">
|
||||
<label><input type="radio" name="orgTypeRadio" value="main"> Основная (расширенная информация)</label>
|
||||
<label><input type="radio" name="orgTypeRadio" value="additional"> Дополнительная</label>
|
||||
</div>
|
||||
<form id="editForm" class="edit-form">
|
||||
<div class="form-group">
|
||||
<label>Название организации</label>
|
||||
<input type="text" id="orgName" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Приоритет</label>
|
||||
<select id="orgPriority">
|
||||
<option value="high">Высокий</option>
|
||||
<option value="medium">Средний</option>
|
||||
<option value="low">Низкий</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Статус</label>
|
||||
<select id="orgStatus">
|
||||
<option value="active">Действует</option>
|
||||
<option value="requires_update">Требует уточнения</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Тип организации</label>
|
||||
<input type="text" id="orgType" placeholder="Например: Общероссийская ассоциация">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Количество членов <span class="hint">(например: 14 человек, 63 подписчика)</span></label>
|
||||
<input type="text" id="orgMembersCount" placeholder="Количество членов, состав">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Ресурсы организации <span class="hint">(сайты, соцсети, мессенджеры)</span></label>
|
||||
<div class="links-container" id="linksContainer">
|
||||
<div id="linksList"></div>
|
||||
<button type="button" class="add-link-btn" id="addLinkBtn"><i class="fas fa-plus"></i> Добавить ссылку</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mainOrgFields" style="display: none;">
|
||||
<div class="form-group"><label>Полное наименование</label><input type="text" id="orgFullName" placeholder="Полное официальное наименование"></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Дата создания</label><input type="text" id="orgCreated" placeholder="Год или дата создания"></div>
|
||||
<div class="form-group"><label>Юридическая форма</label><input type="text" id="orgLegalForm" placeholder="ОПФ"></div>
|
||||
</div>
|
||||
<div class="form-group"><label>Адрес</label><input type="text" id="orgAddress" placeholder="Юридический/фактический адрес"></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Ключевые лица</label><input type="text" id="orgKeyPeople" placeholder="Заместители, члены правления"></div>
|
||||
<div class="form-group"><label>Отраслевой состав</label><input type="text" id="orgSectors" placeholder="Отрасли участников (через запятую)"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Критерии вступления</label><input type="text" id="orgCriteria" placeholder="Требования к кандидатам"></div>
|
||||
<div class="form-group"><label>Членские взносы</label><input type="text" id="orgFees" placeholder="Стоимость"></div>
|
||||
</div>
|
||||
<div class="form-group"><label>Контактная информация</label><input type="text" id="orgContacts" placeholder="Email, телефон для связи"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group"><label>Руководитель</label><input type="text" id="orgHead" placeholder="ФИО руководителя"></div>
|
||||
<div class="form-group"><label>Цели и задачи</label><textarea id="orgGoals" rows="2"></textarea></div>
|
||||
<div class="form-group"><label>Основная деятельность</label><textarea id="orgActivity" rows="2"></textarea></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Сильные стороны</label><textarea id="orgStrengths" rows="2"></textarea></div>
|
||||
<div class="form-group"><label>Слабые стороны</label><textarea id="orgWeaknesses" rows="2"></textarea></div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="save-btn"><i class="fas fa-save"></i> Сохранить изменения</button>
|
||||
<button type="button" class="add-btn" id="addNewBtn"><i class="fas fa-plus"></i> Добавить новую организацию</button>
|
||||
<button type="button" class="delete-btn" id="deleteOrgBtn"><i class="fas fa-trash"></i> Удалить организацию</button>
|
||||
<button type="button" class="move-btn" id="moveOrgBtn"><i class="fas fa-exchange-alt"></i> Переместить в другую категорию</button>
|
||||
<button type="button" class="reset-btn" id="resetDataBtn"><i class="fas fa-undo"></i> Сбросить все данные</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="toast" class="toast">✅ Данные сохранены</div>
|
||||
|
||||
<script>
|
||||
let currentData = null;
|
||||
let currentEditId = null;
|
||||
let currentEditType = 'main';
|
||||
let currentLinks = [];
|
||||
|
||||
function showToast(msg, isError = false) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = msg;
|
||||
toast.className = isError ? 'toast error' : 'toast';
|
||||
toast.style.display = 'block';
|
||||
setTimeout(() => { toast.style.display = 'none'; }, 2500);
|
||||
}
|
||||
let committeesData = {};
|
||||
|
||||
async function loadDataFromServer() {
|
||||
try {
|
||||
@@ -302,53 +145,99 @@
|
||||
currentData = await response.json();
|
||||
if (!currentData.organizations) currentData.organizations = [];
|
||||
if (!currentData.additionalOrganizations) currentData.additionalOrganizations = [];
|
||||
|
||||
// Загружаем комитеты для организаций
|
||||
await loadCommittees();
|
||||
|
||||
renderDetailed();
|
||||
renderSummaryTable();
|
||||
initAdminSelect();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
document.getElementById('detailedContainer').innerHTML = '<div class="loading" style="color:red;">❌ Ошибка загрузки данных. Убедитесь, что файл data.json существует.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDataToServer() {
|
||||
async function loadCommittees() {
|
||||
// Загружаем комитеты для ОПОРЫ РОССИИ
|
||||
try {
|
||||
const response = await fetch('/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(currentData)
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showToast('✅ Данные сохранены в data.json');
|
||||
return true;
|
||||
} else {
|
||||
showToast('❌ ' + result.message, true);
|
||||
return false;
|
||||
const response = await fetch('/committees/opora_committees.json');
|
||||
if (response.ok) {
|
||||
committeesData['opora'] = await response.json();
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('❌ Ошибка соединения с сервером', true);
|
||||
return false;
|
||||
console.log('Файл комитетов ОПОРЫ РОССИИ не найден');
|
||||
committeesData['opora'] = [];
|
||||
}
|
||||
|
||||
// Здесь можно добавить загрузку комитетов для других организаций
|
||||
// Например, для Деловой России, СПП ДНР и т.д.
|
||||
}
|
||||
|
||||
async function saveAndRefresh() {
|
||||
const success = await saveDataToServer();
|
||||
if (success) {
|
||||
await loadDataFromServer();
|
||||
function getCommitteesForOrg(orgName) {
|
||||
if (orgName.includes('ОПОРА РОССИИ')) {
|
||||
return committeesData['opora'] || [];
|
||||
}
|
||||
return success;
|
||||
return [];
|
||||
}
|
||||
|
||||
function renderLinks(links) {
|
||||
if (!links || links.length === 0) return '<span class="tag">—</span>';
|
||||
return `<div class="links-list">${links.map(link => `<div class="link-item"><a href="${escapeHtml(link)}" target="_blank">${escapeHtml(link)}</a></div>`).join('')}</div>`;
|
||||
function renderLeaderInfo(leader, title) {
|
||||
if (!leader || !leader.name) return '<div class="leader-info"><div class="leader-label">Информация отсутствует</div></div>';
|
||||
|
||||
const statusMap = {
|
||||
'active': 'Активен',
|
||||
'medium': 'Средняя активность',
|
||||
'inactive': 'Не активен',
|
||||
'candidate': 'Кандидат'
|
||||
};
|
||||
const statusText = statusMap[leader.status] || leader.status || '—';
|
||||
|
||||
return `
|
||||
<div class="leader-info">
|
||||
<div class="leader-label">${title}</div>
|
||||
<div class="leader-name">${escapeHtml(leader.name)}</div>
|
||||
<div class="leader-contacts">
|
||||
${leader.contacts?.email ? `<i class="fas fa-envelope"></i> <a href="mailto:${escapeHtml(leader.contacts.email)}">${escapeHtml(leader.contacts.email)}</a><br>` : ''}
|
||||
${leader.contacts?.phone ? `<i class="fas fa-phone"></i> <a href="tel:${escapeHtml(leader.contacts.phone)}">${escapeHtml(leader.contacts.phone)}</a><br>` : ''}
|
||||
${leader.contacts?.telegram ? `<i class="fab fa-telegram"></i> <a href="https://t.me/${escapeHtml(leader.contacts.telegram.replace('@', ''))}" target="_blank">${escapeHtml(leader.contacts.telegram)}</a><br>` : ''}
|
||||
</div>
|
||||
<div style="font-size:0.75rem; color:#475569; margin-top:4px;">
|
||||
<span class="tag">${escapeHtml(leader.title || '—')}</span>
|
||||
<span class="tag">Статус: ${statusText}</span>
|
||||
</div>
|
||||
${leader.additional_info ? `<div style="font-size:0.75rem; color:#6c86a3; margin-top:6px;">${escapeHtml(leader.additional_info)}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function truncateUrl(url) {
|
||||
if (!url) return '';
|
||||
if (url.length <= 40) return url;
|
||||
return url.substring(0, 37) + '...';
|
||||
function renderCommittees(orgName) {
|
||||
const committees = getCommitteesForOrg(orgName);
|
||||
if (!committees || committees.length === 0) return '';
|
||||
|
||||
return `
|
||||
<div class="committees-section">
|
||||
<div class="section-title"><i class="fas fa-users"></i> Комитеты организации</div>
|
||||
<div class="committees-grid" id="committees-${orgName.replace(/[^a-zA-Z0-9]/g, '_')}">
|
||||
${committees.map((committee, idx) => `
|
||||
<div class="committee-card">
|
||||
<div class="committee-header" onclick="toggleCommitteeDetails('${orgName.replace(/[^a-zA-Z0-9]/g, '_')}_${idx}')">
|
||||
<span class="committee-name"><i class="fas fa-folder"></i> ${escapeHtml(committee.name)}</span>
|
||||
<span class="committee-status ${committee.current_leader?.status === 'active' ? 'status-active' : (committee.current_leader?.status === 'medium' ? 'status-medium' : 'status-inactive')}">
|
||||
${committee.current_leader?.status === 'active' ? 'Активен' : (committee.current_leader?.status === 'medium' ? 'Средняя активность' : 'Статус не определён')}
|
||||
</span>
|
||||
</div>
|
||||
<div class="committee-details" id="committee-details-${orgName.replace(/[^a-zA-Z0-9]/g, '_')}_${idx}">
|
||||
${renderLeaderInfo(committee.current_leader, '👤 Текущий руководитель')}
|
||||
${renderLeaderInfo(committee.candidate_for_leader, '👥 Кандидат на должность руководителя')}
|
||||
<div class="committee-description">
|
||||
<strong>О комитете:</strong><br>
|
||||
${escapeHtml(committee.description.substring(0, 300))}${committee.description.length > 300 ? '...' : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDetailed() {
|
||||
@@ -367,7 +256,7 @@
|
||||
if (org.link && org.link !== '—' && !allLinks.includes(org.link)) allLinks.push(org.link);
|
||||
allLinks = allLinks.filter(l => l && l.trim() !== '');
|
||||
|
||||
const membersDisplay = org.membersCount || org.summaryMembership || org.detailed?.membership?.count || '—';
|
||||
const membersDisplay = org.membersCount || org.summaryMembership || d.membership?.count || '—';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'org-card';
|
||||
@@ -389,7 +278,7 @@
|
||||
<div class="info-card"><div class="label">🏛️ Юридическая форма</div><div class="value">${escapeHtml(d.general?.legalForm || 'Не указана')}</div></div>
|
||||
<div class="info-card"><div class="label">📍 Адрес</div><div class="value">${escapeHtml(d.general?.address || 'Не указан')}</div></div>
|
||||
<div class="info-card"><div class="label">👥 Количество членов</div><div class="value">${escapeHtml(membersDisplay)}</div></div>
|
||||
<div class="info-card"><div class="label">🔗 Ресурсы</div><div class="value">${renderLinks(allLinks)}</div></div>
|
||||
<div class="info-card"><div class="label">🔗 Ресурсы</div><div class="value"><div class="links-list">${allLinks.map(link => `<a href="${escapeHtml(link)}" target="_blank" class="resource-link">${escapeHtml(link.length > 40 ? link.substring(0, 40) + '...' : link)}</a>`).join('') || '—'}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="details-section">
|
||||
@@ -424,6 +313,7 @@
|
||||
<div class="info-card full-width"><div class="label">🎯 Рекомендации</div><div class="value">${escapeHtml(d.assessment?.priority || '—')}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
${renderCommittees(org.name)}
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
@@ -449,35 +339,20 @@
|
||||
const statusText = org.status === 'active' ? 'Действует' : 'Требует уточнения';
|
||||
const priorityText = org.priority === 'high' ? 'Высокий' : (org.priority === 'medium' ? 'Средний' : 'Низкий');
|
||||
const displayType = type === 'main' ? (org.shortType || 'Основная') : (org.type || 'Дополнительная');
|
||||
|
||||
// Получаем количество членов из всех возможных полей
|
||||
let members = '—';
|
||||
if (org.membersCount && org.membersCount !== 'Данные отсутствуют') {
|
||||
members = org.membersCount;
|
||||
} else if (type === 'main' && org.summaryMembership && org.summaryMembership !== 'Данные отсутствуют') {
|
||||
members = org.summaryMembership;
|
||||
} else if (type === 'additional' && org.members && org.members !== 'Данные отсутствуют') {
|
||||
members = org.members;
|
||||
} else if (org.detailed?.membership?.count && org.detailed.membership.count !== 'Нет公开 данных') {
|
||||
members = org.detailed.membership.count;
|
||||
}
|
||||
const members = org.membersCount || (type === 'main' ? (org.summaryMembership || '—') : (org.members || '—'));
|
||||
|
||||
let allLinks = [];
|
||||
if (org.resources?.links && Array.isArray(org.resources.links)) allLinks = [...org.resources.links];
|
||||
if (org.links && Array.isArray(org.links)) allLinks = [...allLinks, ...org.links];
|
||||
if (org.link && org.link !== '—' && !allLinks.includes(org.link)) allLinks.push(org.link);
|
||||
allLinks = allLinks.filter(l => l && l.trim() !== '' && l !== '—');
|
||||
if (org.resources?.links) allLinks = [...org.resources.links];
|
||||
if (org.links) allLinks = [...allLinks, ...org.links];
|
||||
if (org.link && org.link !== '—') allLinks.push(org.link);
|
||||
allLinks = allLinks.filter(l => l && l.trim() !== '');
|
||||
|
||||
let linksHtml = '—';
|
||||
if (allLinks.length > 0) {
|
||||
linksHtml = `<div class="links-list">${allLinks.map(link =>
|
||||
`<a href="${escapeHtml(link)}" target="_blank" class="resource-link" title="${escapeHtml(link)}">${escapeHtml(truncateUrl(link))}</a>`
|
||||
).join('')}</div>`;
|
||||
linksHtml = `<div class="links-list">${allLinks.map(link => `<a href="${escapeHtml(link)}" target="_blank" class="resource-link">${escapeHtml(link.length > 40 ? link.substring(0, 40) + '...' : link)}</a>`).join('')}</div>`;
|
||||
}
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.setAttribute('data-id', org.id);
|
||||
row.setAttribute('data-type', type);
|
||||
row.innerHTML = `
|
||||
<td style="font-weight:500;">${escapeHtml(org.name)}</td>
|
||||
<td>${escapeHtml(displayType)}</td>
|
||||
@@ -485,373 +360,10 @@
|
||||
<td>${escapeHtml(members)}</td>
|
||||
<td>${priorityText}</td>
|
||||
<td class="links-cell">${linksHtml}</td>
|
||||
<td class="action-btns">
|
||||
<button class="edit-row-btn" onclick="editOrganization('${type}', ${org.id})" title="Редактировать"><i class="fas fa-edit"></i></button>
|
||||
${type === 'additional' ?
|
||||
`<button class="move-to-main-btn" onclick="moveToMain(${org.id})" title="Переместить в основные"><i class="fas fa-arrow-up"></i></button>` :
|
||||
`<button class="move-to-additional-btn" onclick="moveToAdditional(${org.id})" title="Переместить в дополнительные"><i class="fas fa-arrow-down"></i></button>`
|
||||
}
|
||||
<button class="delete-row-btn" onclick="deleteOrganization('${type}', ${org.id})" title="Удалить"><i class="fas fa-trash"></i></button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
|
||||
function renderLinkInputs() {
|
||||
const container = document.getElementById('linksList');
|
||||
container.innerHTML = '';
|
||||
currentLinks.forEach((link, index) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'link-input-group';
|
||||
div.innerHTML = `
|
||||
<input type="url" class="link-input" value="${escapeHtml(link)}" placeholder="https://...">
|
||||
<button type="button" class="remove-link-btn" data-index="${index}"><i class="fas fa-trash"></i></button>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.remove-link-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const idx = parseInt(btn.dataset.index);
|
||||
currentLinks.splice(idx, 1);
|
||||
renderLinkInputs();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.link-input').forEach((input, idx) => {
|
||||
input.addEventListener('change', (e) => {
|
||||
currentLinks[idx] = e.target.value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function collectLinks() {
|
||||
const inputs = document.querySelectorAll('.link-input');
|
||||
const links = [];
|
||||
inputs.forEach(input => {
|
||||
if (input.value.trim()) {
|
||||
links.push(input.value.trim());
|
||||
}
|
||||
});
|
||||
return links;
|
||||
}
|
||||
|
||||
window.editOrganization = function(type, id) {
|
||||
currentEditType = type;
|
||||
currentEditId = id;
|
||||
document.getElementById('adminPanel').classList.add('open');
|
||||
document.getElementById('overlay').classList.add('show');
|
||||
document.querySelector(`input[name="orgTypeRadio"][value="${type}"]`).checked = true;
|
||||
toggleMainFields(type === 'main');
|
||||
initAdminSelect();
|
||||
const select = document.getElementById('orgSelect');
|
||||
select.value = `${type}|${id}`;
|
||||
populateAdminForm();
|
||||
};
|
||||
|
||||
function toggleMainFields(show) {
|
||||
const fields = document.getElementById('mainOrgFields');
|
||||
fields.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function populateAdminForm() {
|
||||
let org = null;
|
||||
if (currentEditType === 'main') {
|
||||
org = currentData.organizations?.find(o => o.id === currentEditId);
|
||||
} else {
|
||||
org = currentData.additionalOrganizations?.find(a => a.id === currentEditId);
|
||||
}
|
||||
if (!org) return;
|
||||
|
||||
document.getElementById('orgName').value = org.name || '';
|
||||
document.getElementById('orgPriority').value = org.priority || 'medium';
|
||||
document.getElementById('orgStatus').value = org.status || 'active';
|
||||
document.getElementById('orgType').value = currentEditType === 'main' ? (org.shortType || '') : (org.type || '');
|
||||
|
||||
// Загружаем количество членов из правильного поля
|
||||
let membersValue = '';
|
||||
if (org.membersCount && org.membersCount !== 'Данные отсутствуют') {
|
||||
membersValue = org.membersCount;
|
||||
} else if (currentEditType === 'main' && org.summaryMembership && org.summaryMembership !== 'Данные отсутствуют') {
|
||||
membersValue = org.summaryMembership;
|
||||
} else if (currentEditType === 'additional' && org.members && org.members !== 'Данные отсутствуют') {
|
||||
membersValue = org.members;
|
||||
} else if (org.detailed?.membership?.count) {
|
||||
membersValue = org.detailed.membership.count;
|
||||
}
|
||||
document.getElementById('orgMembersCount').value = membersValue;
|
||||
|
||||
// Загружаем ссылки
|
||||
if (org.resources?.links) currentLinks = [...org.resources.links];
|
||||
else if (org.links) currentLinks = [...org.links];
|
||||
else if (org.link && org.link !== '—') currentLinks = [org.link];
|
||||
else currentLinks = [];
|
||||
renderLinkInputs();
|
||||
|
||||
if (currentEditType === 'main') {
|
||||
const d = org.detailed || {};
|
||||
document.getElementById('orgFullName').value = d.general?.fullName || '';
|
||||
document.getElementById('orgCreated').value = d.general?.created || '';
|
||||
document.getElementById('orgLegalForm').value = d.general?.legalForm || '';
|
||||
document.getElementById('orgAddress').value = d.general?.address || '';
|
||||
document.getElementById('orgKeyPeople').value = d.leadership?.keyPeople || '';
|
||||
document.getElementById('orgSectors').value = d.membership?.sectors || '';
|
||||
document.getElementById('orgCriteria').value = d.membership?.criteria || '';
|
||||
document.getElementById('orgFees').value = d.membership?.fees || '';
|
||||
document.getElementById('orgContacts').value = d.resources?.contacts || '';
|
||||
}
|
||||
|
||||
document.getElementById('orgHead').value = org.detailed?.leadership?.head || '';
|
||||
document.getElementById('orgGoals').value = org.detailed?.goals || '';
|
||||
document.getElementById('orgActivity').value = org.detailed?.activity || '';
|
||||
document.getElementById('orgStrengths').value = org.detailed?.assessment?.strengths || '';
|
||||
document.getElementById('orgWeaknesses').value = org.detailed?.assessment?.weaknesses || '';
|
||||
}
|
||||
|
||||
function updateOrgFromForm() {
|
||||
let org = null;
|
||||
const collectedLinks = collectLinks();
|
||||
const membersValue = document.getElementById('orgMembersCount').value;
|
||||
|
||||
if (currentEditType === 'main') {
|
||||
org = currentData.organizations?.find(o => o.id === currentEditId);
|
||||
if (!org) return;
|
||||
|
||||
org.name = document.getElementById('orgName').value;
|
||||
org.priority = document.getElementById('orgPriority').value;
|
||||
org.status = document.getElementById('orgStatus').value;
|
||||
org.shortType = document.getElementById('orgType').value;
|
||||
org.membersCount = membersValue;
|
||||
org.summaryMembership = membersValue;
|
||||
|
||||
org.resources = org.resources || {};
|
||||
org.resources.links = collectedLinks;
|
||||
org.link = collectedLinks[0] || '';
|
||||
|
||||
if (!org.detailed) org.detailed = {};
|
||||
if (!org.detailed.general) org.detailed.general = {};
|
||||
if (!org.detailed.leadership) org.detailed.leadership = {};
|
||||
if (!org.detailed.membership) org.detailed.membership = {};
|
||||
if (!org.detailed.resources) org.detailed.resources = {};
|
||||
if (!org.detailed.assessment) org.detailed.assessment = {};
|
||||
|
||||
org.detailed.membership.count = membersValue;
|
||||
org.detailed.general.fullName = document.getElementById('orgFullName').value;
|
||||
org.detailed.general.created = document.getElementById('orgCreated').value;
|
||||
org.detailed.general.legalForm = document.getElementById('orgLegalForm').value;
|
||||
org.detailed.general.address = document.getElementById('orgAddress').value;
|
||||
org.detailed.leadership.keyPeople = document.getElementById('orgKeyPeople').value;
|
||||
org.detailed.membership.sectors = document.getElementById('orgSectors').value;
|
||||
org.detailed.membership.criteria = document.getElementById('orgCriteria').value;
|
||||
org.detailed.membership.fees = document.getElementById('orgFees').value;
|
||||
org.detailed.resources.contacts = document.getElementById('orgContacts').value;
|
||||
org.detailed.leadership.head = document.getElementById('orgHead').value;
|
||||
org.detailed.goals = document.getElementById('orgGoals').value;
|
||||
org.detailed.activity = document.getElementById('orgActivity').value;
|
||||
org.detailed.assessment.strengths = document.getElementById('orgStrengths').value;
|
||||
org.detailed.assessment.weaknesses = document.getElementById('orgWeaknesses').value;
|
||||
} else {
|
||||
org = currentData.additionalOrganizations?.find(a => a.id === currentEditId);
|
||||
if (!org) return;
|
||||
|
||||
org.name = document.getElementById('orgName').value;
|
||||
org.priority = document.getElementById('orgPriority').value;
|
||||
org.status = document.getElementById('orgStatus').value;
|
||||
org.type = document.getElementById('orgType').value;
|
||||
org.members = membersValue;
|
||||
org.membersCount = membersValue;
|
||||
org.links = collectedLinks;
|
||||
org.link = collectedLinks[0] || '';
|
||||
|
||||
if (!org.detailed) org.detailed = { leadership: {}, assessment: {} };
|
||||
org.detailed.leadership.head = document.getElementById('orgHead').value;
|
||||
org.detailed.goals = document.getElementById('orgGoals').value;
|
||||
org.detailed.activity = document.getElementById('orgActivity').value;
|
||||
if (!org.detailed.assessment) org.detailed.assessment = {};
|
||||
org.detailed.assessment.strengths = document.getElementById('orgStrengths').value;
|
||||
org.detailed.assessment.weaknesses = document.getElementById('orgWeaknesses').value;
|
||||
}
|
||||
|
||||
saveAndRefresh().then(() => { initAdminSelect(); });
|
||||
}
|
||||
|
||||
window.moveToMain = async function(id) {
|
||||
const org = currentData.additionalOrganizations?.find(a => a.id === id);
|
||||
if (!org) return;
|
||||
|
||||
let allLinks = [];
|
||||
if (org.links) allLinks = [...org.links];
|
||||
else if (org.link && org.link !== '—') allLinks = [org.link];
|
||||
allLinks = [...new Set(allLinks.filter(l => l && l.trim() !== ''))];
|
||||
|
||||
const membersVal = org.membersCount || org.members || 'Данные отсутствуют';
|
||||
|
||||
const newMainOrg = {
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
priority: org.priority,
|
||||
status: org.status,
|
||||
shortType: org.type || 'Организация',
|
||||
membersCount: membersVal,
|
||||
summaryMembership: membersVal,
|
||||
link: allLinks[0] || '',
|
||||
resources: { links: allLinks },
|
||||
detailed: {
|
||||
general: { fullName: org.name, created: '', legalForm: '', address: '', statusText: 'Действует' },
|
||||
leadership: { head: org.detailed?.leadership?.head || '', keyPeople: '', branches: '' },
|
||||
membership: { count: membersVal, criteria: '', fees: '', sectors: '' },
|
||||
goals: org.detailed?.goals || '',
|
||||
activity: org.detailed?.activity || '',
|
||||
resources: { contacts: '' },
|
||||
assessment: { strengths: '', weaknesses: '', priority: '' }
|
||||
}
|
||||
};
|
||||
|
||||
currentData.additionalOrganizations = currentData.additionalOrganizations.filter(a => a.id !== id);
|
||||
currentData.organizations.push(newMainOrg);
|
||||
await saveAndRefresh();
|
||||
showToast(`✅ "${org.name}" перемещена в основные`);
|
||||
};
|
||||
|
||||
window.moveToAdditional = async function(id) {
|
||||
const org = currentData.organizations?.find(o => o.id === id);
|
||||
if (!org) return;
|
||||
|
||||
let allLinks = [];
|
||||
if (org.resources?.links) allLinks = [...org.resources.links];
|
||||
else if (org.link && org.link !== '—') allLinks = [org.link];
|
||||
allLinks = [...new Set(allLinks.filter(l => l && l.trim() !== ''))];
|
||||
|
||||
const membersVal = org.membersCount || org.summaryMembership || 'Данные отсутствуют';
|
||||
|
||||
const newAdditionalOrg = {
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
priority: org.priority,
|
||||
status: org.status,
|
||||
type: org.shortType || 'Основная',
|
||||
members: membersVal,
|
||||
membersCount: membersVal,
|
||||
link: allLinks[0] || '',
|
||||
links: allLinks,
|
||||
detailed: {
|
||||
leadership: { head: org.detailed?.leadership?.head || '' },
|
||||
goals: org.detailed?.goals || '',
|
||||
activity: org.detailed?.activity || '',
|
||||
assessment: { strengths: org.detailed?.assessment?.strengths || '', weaknesses: org.detailed?.assessment?.weaknesses || '' }
|
||||
}
|
||||
};
|
||||
|
||||
currentData.organizations = currentData.organizations.filter(o => o.id !== id);
|
||||
currentData.additionalOrganizations.push(newAdditionalOrg);
|
||||
await saveAndRefresh();
|
||||
showToast(`✅ "${org.name}" перемещена в дополнительные`);
|
||||
};
|
||||
|
||||
window.deleteOrganization = async function(type, id) {
|
||||
const name = type === 'main'
|
||||
? currentData.organizations?.find(o => o.id === id)?.name
|
||||
: currentData.additionalOrganizations?.find(a => a.id === id)?.name;
|
||||
if (!confirm(`Удалить "${name}"?`)) return;
|
||||
|
||||
if (type === 'main') {
|
||||
currentData.organizations = currentData.organizations.filter(o => o.id !== id);
|
||||
} else {
|
||||
currentData.additionalOrganizations = currentData.additionalOrganizations.filter(a => a.id !== id);
|
||||
}
|
||||
await saveAndRefresh();
|
||||
showToast(`🗑️ "${name}" удалена`);
|
||||
};
|
||||
|
||||
function initAdminSelect() {
|
||||
const select = document.getElementById('orgSelect');
|
||||
select.innerHTML = '';
|
||||
const mainOrgs = (currentData.organizations || []).map(o => ({ id: o.id, name: o.name, type: 'main' }));
|
||||
const additionalOrgs = (currentData.additionalOrganizations || []).map(a => ({ id: a.id, name: a.name, type: 'additional' }));
|
||||
const allItems = [...mainOrgs, ...additionalOrgs];
|
||||
|
||||
allItems.forEach(item => {
|
||||
const option = document.createElement('option');
|
||||
option.value = `${item.type}|${item.id}`;
|
||||
option.textContent = `${item.name} ${item.type === 'main' ? '(основная)' : '(дополнительная)'}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
select.addEventListener('change', (e) => {
|
||||
const [type, id] = e.target.value.split('|');
|
||||
currentEditType = type;
|
||||
currentEditId = parseInt(id);
|
||||
toggleMainFields(type === 'main');
|
||||
populateAdminForm();
|
||||
document.querySelector(`input[name="orgTypeRadio"][value="${type}"]`).checked = true;
|
||||
});
|
||||
}
|
||||
|
||||
async function addNewOrganization() {
|
||||
const newId = Math.max(
|
||||
...(currentData.organizations?.map(o => o.id) || [0]),
|
||||
...(currentData.additionalOrganizations?.map(a => a.id) || [0])
|
||||
) + 1;
|
||||
|
||||
const newOrg = {
|
||||
id: newId,
|
||||
name: "Новая организация",
|
||||
priority: "medium",
|
||||
status: "active",
|
||||
type: "Новый тип",
|
||||
members: "Данные отсутствуют",
|
||||
membersCount: "Данные отсутствуют",
|
||||
link: "",
|
||||
links: [],
|
||||
detailed: {
|
||||
leadership: { head: "" },
|
||||
goals: "",
|
||||
activity: "",
|
||||
assessment: { strengths: "", weaknesses: "" }
|
||||
}
|
||||
};
|
||||
|
||||
if (!currentData.additionalOrganizations) currentData.additionalOrganizations = [];
|
||||
currentData.additionalOrganizations.push(newOrg);
|
||||
await saveAndRefresh();
|
||||
showToast('➕ Новая организация добавлена в дополнительные');
|
||||
}
|
||||
|
||||
async function deleteCurrentOrganization() {
|
||||
if (!confirm(`Удалить "${document.getElementById('orgName').value}"?`)) return;
|
||||
if (currentEditType === 'main') {
|
||||
currentData.organizations = currentData.organizations.filter(o => o.id !== currentEditId);
|
||||
} else {
|
||||
currentData.additionalOrganizations = currentData.additionalOrganizations.filter(a => a.id !== currentEditId);
|
||||
}
|
||||
await saveAndRefresh();
|
||||
showToast('🗑️ Организация удалена');
|
||||
}
|
||||
|
||||
async function moveCurrentOrganization() {
|
||||
if (currentEditType === 'main') {
|
||||
await moveToAdditional(currentEditId);
|
||||
} else {
|
||||
await moveToMain(currentEditId);
|
||||
}
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
if (confirm('Сбросить все данные к исходным? Все изменения будут потеряны.')) {
|
||||
try {
|
||||
const response = await fetch('/reset', { method: 'POST' });
|
||||
if (response.ok) {
|
||||
await loadDataFromServer();
|
||||
showToast('🔄 Данные сброшены к исходным');
|
||||
} else {
|
||||
showToast('❌ Ошибка сброса. Файл data.default.json не найден.', true);
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('❌ Ошибка соединения', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.toggleDetails = function(id) {
|
||||
const el = document.getElementById(`details-${id}`);
|
||||
const icon = document.getElementById(`icon-${id}`);
|
||||
@@ -864,47 +376,17 @@
|
||||
}
|
||||
};
|
||||
|
||||
window.toggleCommitteeDetails = function(id) {
|
||||
const el = document.getElementById(`committee-details-${id}`);
|
||||
if (el.classList.contains('open')) {
|
||||
el.classList.remove('open');
|
||||
} else {
|
||||
el.classList.add('open');
|
||||
}
|
||||
};
|
||||
|
||||
function escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>]/g, m => ({ '&': '&', '<': '<', '>': '>' }[m])); }
|
||||
|
||||
const adminPanel = document.getElementById('adminPanel');
|
||||
const overlay = document.getElementById('overlay');
|
||||
document.getElementById('adminToggleBtn').addEventListener('click', () => {
|
||||
adminPanel.classList.add('open');
|
||||
overlay.classList.add('show');
|
||||
initAdminSelect();
|
||||
toggleMainFields(currentEditType === 'main');
|
||||
});
|
||||
function closeAdmin() { adminPanel.classList.remove('open'); overlay.classList.remove('show'); }
|
||||
document.getElementById('closeAdminBtn').addEventListener('click', closeAdmin);
|
||||
overlay.addEventListener('click', closeAdmin);
|
||||
document.getElementById('editForm').addEventListener('submit', (e) => { e.preventDefault(); updateOrgFromForm(); });
|
||||
document.getElementById('addNewBtn').addEventListener('click', addNewOrganization);
|
||||
document.getElementById('deleteOrgBtn').addEventListener('click', deleteCurrentOrganization);
|
||||
document.getElementById('moveOrgBtn').addEventListener('click', moveCurrentOrganization);
|
||||
document.getElementById('resetDataBtn').addEventListener('click', resetToDefault);
|
||||
document.getElementById('addLinkBtn').addEventListener('click', () => {
|
||||
currentLinks.push('');
|
||||
renderLinkInputs();
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[name="orgTypeRadio"]').forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
if (e.target.checked) {
|
||||
toggleMainFields(e.target.value === 'main');
|
||||
const select = document.getElementById('orgSelect');
|
||||
const options = Array.from(select.options);
|
||||
const matchingOption = options.find(opt => opt.value.startsWith(e.target.value + '|'));
|
||||
if (matchingOption) {
|
||||
select.value = matchingOption.value;
|
||||
const [type, id] = matchingOption.value.split('|');
|
||||
currentEditType = type;
|
||||
currentEditId = parseInt(id);
|
||||
populateAdminForm();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const btns = document.querySelectorAll('.tab-btn');
|
||||
const panels = { detailed: document.getElementById('detailedPanel'), summary: document.getElementById('summaryPanel') };
|
||||
btns.forEach(btn => {
|
||||
|
||||
@@ -63,6 +63,22 @@ class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
if parsed_path.path == '/' or parsed_path.path == '/index.html':
|
||||
self.path = '/index.html'
|
||||
|
||||
if parsed_path.path == '/committees.html':
|
||||
self.path = '/committees.html'
|
||||
|
||||
if parsed_path.path.startswith('/committees/'):
|
||||
file_path = self.path[1:] # убираем первый слеш
|
||||
if os.path.exists(file_path):
|
||||
self.send_response(200)
|
||||
if file_path.endswith('.json'):
|
||||
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
||||
self.end_headers()
|
||||
with open(file_path, 'rb') as f:
|
||||
self.wfile.write(f.read())
|
||||
else:
|
||||
self.send_error(404, 'File not found')
|
||||
return
|
||||
|
||||
if self.path.endswith('.html'):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
||||
@@ -132,6 +148,31 @@ class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
self.wfile.write(response.encode('utf-8'))
|
||||
return
|
||||
|
||||
# Сохранение комитетов
|
||||
if parsed_path.path.startswith('/save_committees/'):
|
||||
filename = parsed_path.path.replace('/save_committees/', '')
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
post_data = self.rfile.read(content_length)
|
||||
|
||||
try:
|
||||
data = json.loads(post_data.decode('utf-8'))
|
||||
file_path = f'committees/{filename}'
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
response = json.dumps({'success': True, 'message': 'Данные комитетов сохранены'})
|
||||
self.wfile.write(response.encode('utf-8'))
|
||||
except Exception as e:
|
||||
self.send_response(500)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
response = json.dumps({'success': False, 'message': str(e)})
|
||||
self.wfile.write(response.encode('utf-8'))
|
||||
return
|
||||
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user