First
This commit is contained in:
+80
@@ -0,0 +1,80 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
pythonenv*
|
||||
|
||||
# Серверные файлы
|
||||
data.json
|
||||
data.default.json
|
||||
|
||||
# Логи
|
||||
*.log
|
||||
*.pid
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# OS generated files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.backup
|
||||
*.tmp
|
||||
|
||||
# Package files
|
||||
*.egg
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
*.whl
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# pytest
|
||||
.pytest_cache/
|
||||
|
||||
# Coverage
|
||||
.coverage
|
||||
.coverage.*
|
||||
htmlcov/
|
||||
.coveragerc
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.venv
|
||||
|
||||
# Secrets
|
||||
*.key
|
||||
*.pem
|
||||
.env
|
||||
.secrets
|
||||
|
||||
# Проект специфичные
|
||||
data.default.json
|
||||
data.json
|
||||
*.db
|
||||
*.sqlite
|
||||
+503
@@ -0,0 +1,503 @@
|
||||
<!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; }
|
||||
|
||||
.admin-toggle {
|
||||
position: fixed; bottom: 20px; right: 20px; background: #1e3a5f; color: white;
|
||||
border: none; border-radius: 50px; padding: 12px 20px; cursor: pointer;
|
||||
font-weight: 600; z-index: 1000; box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.admin-toggle:hover { background: #0f2b40; transform: scale(1.02); }
|
||||
|
||||
.tabs { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.75rem; margin-bottom: 2rem; border-bottom: 2px solid #e2e8f0; padding-bottom: 0.75rem; }
|
||||
.tab-btn { background: transparent; border: none; font-size: 0.95rem; font-weight: 600; padding: 0.6rem 1.5rem; border-radius: 40px; cursor: pointer; color: #475569; }
|
||||
.tab-btn i { margin-right: 8px; }
|
||||
.tab-btn.active { background: #1e3a5f; color: white; }
|
||||
.tab-panel { display: none; animation: fadeIn 0.25s ease; }
|
||||
.tab-panel.active-panel { display: block; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px);} to { opacity: 1; transform: translateY(0);} }
|
||||
|
||||
.org-grid { display: flex; flex-direction: column; gap: 1rem; }
|
||||
.org-card { background: white; border-radius: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); border: 1px solid #eef2f6; overflow: hidden; }
|
||||
.org-header { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; padding: 1.2rem 1.5rem; background: #ffffff; border-bottom: 1px solid #f0f2f5; cursor: pointer; }
|
||||
.org-title { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||
.org-name { font-weight: 700; font-size: 1.1rem; color: #0f172a; }
|
||||
.priority-badge { background: #eef2ff; color: #1e40af; border-radius: 40px; padding: 0.2rem 0.7rem; font-size: 0.7rem; font-weight: 600; }
|
||||
.status-badge { border-radius: 40px; padding: 0.2rem 0.7rem; font-size: 0.7rem; font-weight: 600; }
|
||||
.status-active { background: #dcfce7; color: #15803d; }
|
||||
.status-warning { background: #fff3e3; color: #b45309; }
|
||||
.toggle-icon { font-size: 1.2rem; color: #64748b; }
|
||||
.org-details { padding: 0 1.5rem 1.2rem 1.5rem; background: #fefefe; border-top: 1px solid #f1f5f9; display: none; }
|
||||
.org-details.open { display: block; }
|
||||
.details-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; margin-top: 1.2rem; }
|
||||
.detail-item { background: #f9fafb; padding: 0.8rem 1rem; border-radius: 16px; }
|
||||
.detail-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; color: #5b6e8c; }
|
||||
.detail-value { font-size: 0.9rem; font-weight: 500; color: #1e2a44; margin-top: 4px; word-break: break-word; }
|
||||
hr { margin: 1.2rem 0; border-color: #ecf3f9; }
|
||||
|
||||
.admin-panel {
|
||||
position: fixed; top: 0; right: -550px; width: 550px; 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; }
|
||||
.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 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; }
|
||||
.save-btn { background: #1e3a5f; color: white; border: none; padding: 12px; border-radius: 12px; font-weight: 600; cursor: pointer; margin-top: 1rem; }
|
||||
.save-btn:hover { background: #0f2b40; }
|
||||
.delete-btn { background: #dc2626; color: white; border: none; padding: 12px; border-radius: 12px; font-weight: 600; cursor: pointer; margin-top: 0.5rem; }
|
||||
.delete-btn:hover { background: #b91c1c; }
|
||||
.add-btn { background: #10b981; color: white; border: none; padding: 12px; border-radius: 12px; font-weight: 600; cursor: pointer; margin-top: 0.5rem; }
|
||||
.add-btn:hover { background: #059669; }
|
||||
|
||||
.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; }
|
||||
.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: 600px) { .admin-panel { width: 100%; right: -100%; } .form-row { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1><i class="fas fa-chart-line"></i> Бизнес-объединения ДНР</h1>
|
||||
<p>Аналитический обзор + Админ-панель (данные хранятся в data.json)</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" data-tab="summary"><i class="fas fa-table-list"></i> Сводная таблица</button>
|
||||
</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>
|
||||
|
||||
<div id="summaryPanel" class="tab-panel">
|
||||
<div class="summary-wrapper">
|
||||
<table class="summary-table">
|
||||
<thead><tr><th>Название</th><th>Тип</th><th>Статус</th><th>Членство</th><th>Приоритет</th><th>Ресурс</th></thead>
|
||||
<tbody id="summaryTableBody"><tr><td colspan="6" class="loading">Загрузка...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-note">
|
||||
<i class="fas fa-info-circle"></i> Данные хранятся в файле data.json. Все изменения сохраняются на сервере.
|
||||
</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>Членство / состав</label>
|
||||
<input type="text" id="orgMembership" placeholder="Количество членов, состав">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Ссылка на ресурс</label>
|
||||
<input type="url" id="orgLink" placeholder="https://...">
|
||||
</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>
|
||||
<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="reset-btn" id="resetDataBtn" style="background:#e2e8f0; padding:12px; border-radius:12px; border:none; cursor:pointer;"><i class="fas fa-undo"></i> Сбросить все данные</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="toast" class="toast">✅ Данные сохранены</div>
|
||||
|
||||
<script>
|
||||
let currentData = null;
|
||||
let currentEditId = null;
|
||||
let currentEditType = 'main';
|
||||
|
||||
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 loadDataFromServer() {
|
||||
try {
|
||||
const response = await fetch('/data.json');
|
||||
if (!response.ok) throw new Error('Ошибка загрузки');
|
||||
currentData = await response.json();
|
||||
renderDetailed();
|
||||
renderSummaryTable();
|
||||
initAdminSelect();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
document.getElementById('detailedContainer').innerHTML = '<div class="loading" style="color:red;">❌ Ошибка загрузки данных. Убедитесь, что файл data.json существует.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDataToServer() {
|
||||
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;
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('❌ Ошибка соединения с сервером', true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderDetailed() {
|
||||
const container = document.getElementById('detailedContainer');
|
||||
if (!currentData) return;
|
||||
container.innerHTML = '';
|
||||
(currentData.organizations || []).forEach(org => {
|
||||
const d = org.detailed || {};
|
||||
const priorityText = org.priority === 'high' ? 'Высокий' : (org.priority === 'medium' ? 'Средний' : 'Низкий');
|
||||
const statusClass = org.status === 'active' ? 'status-active' : 'status-warning';
|
||||
const statusText = org.status === 'active' ? '✔ Действует' : 'Требует уточнения';
|
||||
const card = document.createElement('div');
|
||||
card.className = 'org-card';
|
||||
card.innerHTML = `
|
||||
<div class="org-header" onclick="toggleDetails(${org.id})">
|
||||
<div class="org-title">
|
||||
<span class="org-name"><i class="fas fa-building"></i> ${escapeHtml(org.name)}</span>
|
||||
<span class="priority-badge">Приоритет: ${priorityText}</span>
|
||||
<span class="status-badge ${statusClass}">${statusText}</span>
|
||||
</div>
|
||||
<div class="toggle-icon"><i class="fas fa-chevron-down" id="icon-${org.id}"></i></div>
|
||||
</div>
|
||||
<div class="org-details" id="details-${org.id}">
|
||||
<div class="details-grid">
|
||||
<div class="detail-item"><div class="detail-label">Руководитель</div><div class="detail-value">${escapeHtml(d.leadership?.head || '—')}</div></div>
|
||||
<div class="detail-item"><div class="detail-label">Цели</div><div class="detail-value">${escapeHtml(d.goals || '—')}</div></div>
|
||||
<div class="detail-item"><div class="detail-label">Активность</div><div class="detail-value">${escapeHtml(d.activity || '—')}</div></div>
|
||||
<div class="detail-item"><div class="detail-label">Членство</div><div class="detail-value">${escapeHtml(org.summaryMembership || '—')}</div></div>
|
||||
</div>
|
||||
<hr>
|
||||
<div><i class="fas fa-link"></i> <a href="${escapeHtml(org.link)}" target="_blank">${escapeHtml(org.link)}</a></div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function renderSummaryTable() {
|
||||
const tbody = document.getElementById('summaryTableBody');
|
||||
if (!currentData) return;
|
||||
tbody.innerHTML = '';
|
||||
const mainOrgs = (currentData.organizations || []).map(o => ({ ...o, displayType: o.shortType || 'Основная' }));
|
||||
const additionalOrgs = (currentData.additionalOrganizations || []).map(a => ({ ...a, displayType: a.type || 'Дополнительная' }));
|
||||
const allOrgs = [...mainOrgs, ...additionalOrgs];
|
||||
|
||||
allOrgs.forEach(org => {
|
||||
const statusClass = org.status === 'active' ? 'status-active' : 'status-warning';
|
||||
const statusText = org.status === 'active' ? 'Действует' : 'Требует уточнения';
|
||||
const priorityText = org.priority === 'high' ? 'Высокий' : (org.priority === 'medium' ? 'Средний' : 'Низкий');
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td style="font-weight:500;">${escapeHtml(org.name)}</td>
|
||||
<td>${escapeHtml(org.displayType || org.type || '—')}</td>
|
||||
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
||||
<td>${escapeHtml(org.members || org.summaryMembership || '—')}</td>
|
||||
<td>${priorityText}</td>
|
||||
<td>${org.link && org.link !== '—' ? `<a href="${escapeHtml(org.link)}" target="_blank">ресурс</a>` : '—'}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
populateAdminForm();
|
||||
document.querySelector(`input[name="orgTypeRadio"][value="${type}"]`).checked = true;
|
||||
});
|
||||
}
|
||||
|
||||
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 = org.shortType || org.type || '';
|
||||
document.getElementById('orgMembership').value = org.summaryMembership || org.members || '';
|
||||
document.getElementById('orgLink').value = org.link || '';
|
||||
document.getElementById('orgHead').value = org.detailed?.leadership?.head || '';
|
||||
document.getElementById('orgGoals').value = org.detailed?.goals || '';
|
||||
document.getElementById('orgActivity').value = org.detailed?.activity || '';
|
||||
}
|
||||
|
||||
function updateOrgFromForm() {
|
||||
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;
|
||||
|
||||
org.name = document.getElementById('orgName').value;
|
||||
org.priority = document.getElementById('orgPriority').value;
|
||||
org.status = document.getElementById('orgStatus').value;
|
||||
|
||||
if (currentEditType === 'main') {
|
||||
org.shortType = document.getElementById('orgType').value;
|
||||
org.summaryMembership = document.getElementById('orgMembership').value;
|
||||
} else {
|
||||
org.type = document.getElementById('orgType').value;
|
||||
org.members = document.getElementById('orgMembership').value;
|
||||
}
|
||||
|
||||
org.link = document.getElementById('orgLink').value;
|
||||
if (!org.detailed) org.detailed = { leadership: {} };
|
||||
if (!org.detailed.leadership) org.detailed.leadership = {};
|
||||
org.detailed.leadership.head = document.getElementById('orgHead').value;
|
||||
org.detailed.goals = document.getElementById('orgGoals').value;
|
||||
org.detailed.activity = document.getElementById('orgActivity').value;
|
||||
|
||||
saveDataToServer().then(success => {
|
||||
if (success) {
|
||||
renderDetailed();
|
||||
renderSummaryTable();
|
||||
initAdminSelect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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: "Данные отсутствуют",
|
||||
link: "",
|
||||
detailed: {
|
||||
leadership: { head: "" },
|
||||
goals: "",
|
||||
activity: ""
|
||||
}
|
||||
};
|
||||
|
||||
// По умолчанию добавляем в дополнительную категорию
|
||||
if (!currentData.additionalOrganizations) currentData.additionalOrganizations = [];
|
||||
currentData.additionalOrganizations.push(newOrg);
|
||||
|
||||
await saveDataToServer();
|
||||
await loadDataFromServer();
|
||||
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 saveDataToServer();
|
||||
await loadDataFromServer();
|
||||
showToast('🗑️ Организация удалена');
|
||||
}
|
||||
|
||||
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}`);
|
||||
if (el.classList.contains('open')) {
|
||||
el.classList.remove('open');
|
||||
icon.className = 'fas fa-chevron-down';
|
||||
} else {
|
||||
el.classList.add('open');
|
||||
icon.className = 'fas fa-chevron-up';
|
||||
}
|
||||
};
|
||||
|
||||
function escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>]/g, m => ({ '&': '&', '<': '<', '>': '>' }[m])); }
|
||||
|
||||
// Admin panel controls
|
||||
const adminPanel = document.getElementById('adminPanel');
|
||||
const overlay = document.getElementById('overlay');
|
||||
document.getElementById('adminToggleBtn').addEventListener('click', () => {
|
||||
adminPanel.classList.add('open');
|
||||
overlay.classList.add('show');
|
||||
initAdminSelect();
|
||||
});
|
||||
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('resetDataBtn').addEventListener('click', resetToDefault);
|
||||
|
||||
document.querySelectorAll('input[name="orgTypeRadio"]').forEach(radio => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
if (e.target.checked) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Tabs
|
||||
const btns = document.querySelectorAll('.tab-btn');
|
||||
const panels = { detailed: document.getElementById('detailedPanel'), summary: document.getElementById('summaryPanel') };
|
||||
btns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
btns.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const tabId = btn.getAttribute('data-tab');
|
||||
Object.values(panels).forEach(p => p.classList.remove('active-panel'));
|
||||
if (tabId === 'detailed') panels.detailed.classList.add('active-panel');
|
||||
else panels.summary.classList.add('active-panel');
|
||||
});
|
||||
});
|
||||
|
||||
loadDataFromServer();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
# This is a sample Python script.
|
||||
|
||||
# Press Shift+F10 to execute it or replace it with your code.
|
||||
# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
|
||||
|
||||
|
||||
def print_hi(name):
|
||||
# Use a breakpoint in the code line below to debug your script.
|
||||
print(f'Hi, {name}') # Press Ctrl+F8 to toggle the breakpoint.
|
||||
|
||||
|
||||
# Press the green button in the gutter to run the script.
|
||||
if __name__ == '__main__':
|
||||
print_hi('PyCharm')
|
||||
|
||||
# See PyCharm help at https://www.jetbrains.com/help/pycharm/
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Сервер для управления данными бизнес-объединений ДНР.
|
||||
Поддерживает:
|
||||
- GET /data.json - получение данных
|
||||
- POST /save - сохранение данных в файл
|
||||
- POST /reset - сброс к дефолтным данным (загружает из data.default.json)
|
||||
"""
|
||||
|
||||
import http.server
|
||||
import socketserver
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from urllib.parse import urlparse
|
||||
|
||||
PORT = 8092
|
||||
DATA_FILE = 'data.json'
|
||||
DEFAULT_DATA_FILE = 'data.default.json'
|
||||
|
||||
|
||||
# Дефолтные данные вынесены в отдельный файл data.default.json
|
||||
# Если нужно создать файл с дефолтом - раскомментировать функцию ниже
|
||||
|
||||
class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
|
||||
def end_headers(self):
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||
super().end_headers()
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
parsed_path = urlparse(self.path)
|
||||
|
||||
# Отдаём data.json
|
||||
if parsed_path.path == '/data.json':
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
||||
self.end_headers()
|
||||
try:
|
||||
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
||||
self.wfile.write(f.read().encode('utf-8'))
|
||||
except FileNotFoundError:
|
||||
self.send_error(404, 'data.json not found')
|
||||
return
|
||||
|
||||
# Отдаём index.html
|
||||
if parsed_path.path == '/' or parsed_path.path == '/index.html':
|
||||
self.path = '/index.html'
|
||||
|
||||
# Статические файлы
|
||||
if self.path.endswith('.html'):
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
||||
self.end_headers()
|
||||
try:
|
||||
with open(self.path[1:], 'rb') as f:
|
||||
self.wfile.write(f.read())
|
||||
except FileNotFoundError:
|
||||
self.send_error(404, 'File not found')
|
||||
return
|
||||
|
||||
super().do_GET()
|
||||
|
||||
def do_POST(self):
|
||||
parsed_path = urlparse(self.path)
|
||||
|
||||
# Сохранение данных
|
||||
if parsed_path.path == '/save':
|
||||
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'))
|
||||
|
||||
with open(DATA_FILE, '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 json.JSONDecodeError as e:
|
||||
self.send_response(400)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
response = json.dumps({'success': False, 'message': f'Ошибка JSON: {str(e)}'})
|
||||
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': f'Ошибка сервера: {str(e)}'})
|
||||
self.wfile.write(response.encode('utf-8'))
|
||||
return
|
||||
|
||||
# Сброс к дефолту (если есть файл data.default.json)
|
||||
if parsed_path.path == '/reset':
|
||||
try:
|
||||
if os.path.exists(DEFAULT_DATA_FILE):
|
||||
shutil.copy(DEFAULT_DATA_FILE, DATA_FILE)
|
||||
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'))
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
response = json.dumps({'success': False, '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()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
print(f"[{self.address_string()}] {format % args}")
|
||||
|
||||
|
||||
def ensure_data_file():
|
||||
"""Проверяет наличие data.json, если нет - создаёт из дефолта или пустой"""
|
||||
if not os.path.exists(DATA_FILE):
|
||||
if os.path.exists(DEFAULT_DATA_FILE):
|
||||
shutil.copy(DEFAULT_DATA_FILE, DATA_FILE)
|
||||
print(f"📄 Создан файл данных из {DEFAULT_DATA_FILE}")
|
||||
else:
|
||||
# Создаём минимальный пустой файл
|
||||
empty_data = {
|
||||
"organizations": [],
|
||||
"additionalOrganizations": [],
|
||||
"metadata": {"lastUpdated": "2026-01-15", "totalOrganizations": 0}
|
||||
}
|
||||
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(empty_data, f, ensure_ascii=False, indent=2)
|
||||
print(f"📄 Создан пустой файл данных: {DATA_FILE}")
|
||||
print(f"⚠️ Для работы с данными создайте файл {DEFAULT_DATA_FILE} с дефолтными данными")
|
||||
|
||||
|
||||
def run_server():
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
ensure_data_file()
|
||||
|
||||
with socketserver.TCPServer(("", PORT), CustomHTTPRequestHandler) as httpd:
|
||||
print("=" * 60)
|
||||
print(f"🚀 Сервер запущен!")
|
||||
print(f"📁 Директория: {os.getcwd()}")
|
||||
print(f"🌐 Локальный доступ: http://localhost:{PORT}")
|
||||
print(f"📄 Файл данных: {DATA_FILE}")
|
||||
print(f"📄 Дефолтный файл (если есть): {DEFAULT_DATA_FILE}")
|
||||
print(f"⏹️ Для остановки сервера нажмите Ctrl+C")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Сервер остановлен.")
|
||||
httpd.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_server()
|
||||
Reference in New Issue
Block a user