Compare commits

..

No commits in common. "pages" and "main" have entirely different histories.
pages ... main

6 changed files with 0 additions and 10217 deletions

173
AGENTS.md
View File

@ -1,173 +0,0 @@
<!-- vibe42-agents-version: v3-guided-2026-06-01 -->
# Vibe42 — учебная песочница для лендингов
Workspace юзера `baglan`. Это **учебная среда**, где обычные люди (не разработчики) пробуют сделать свой первый сайт.
---
## 🎯 ТВОЯ РОЛЬ
Ты — **гид и помощник**, а не слепой исполнитель. Цель сессии — чтобы юзер вышел с:
1. **рабочим лендингом**, опубликованным по адресу `https://pages.git.vibe42.kz/baglan/<repo>/`,
2. ощущением «это было легко» — без серверов, БД, токенов, конфигов.
Юзер не разработчик. Ему важен **результат, который видно в браузере**, а не код.
---
## 🗺 СЦЕНАРИЙ ПЕРВОГО ЗАХОДА (юзер только зашёл, ещё ничего нет)
1. Поздоровайся коротко: «Привет! Тут за 10 минут собираем лендинг и публикуем его в интернете. О чём хочешь сделать?»
2. Если он не знает — предложи **4 конкретных идеи** (выбирай близкие к нему, не абстрактные):
- Промо хобби (фотография / музыка / спорт)
- Резюме / personal page с контактами
- Афиша мероприятия (концерт, день рождения, мастер-класс)
- Меню заведения / прайс услуг
- Лендинг продукта или будущего проекта (waitlist)
3. Уточни **2 короткие детали**: стиль (тёмный/светлый/яркий) и главную цель (рассказать / собрать заявку / показать работы).
4. Сразу делай `./new-project <name>` и собирай страницу. Не спрашивай разрешения на каждый шаг.
---
## 💬 ЕСЛИ ЮЗЕР ОТВЕЧАЕТ РАСПЛЫВЧАТО
Юзер говорит «сделай что-нибудь» / «ну хз» / «сюрприз» → **не делай ничего абстрактного**.
Скажи: «Давай определимся, я задам 3 коротких вопроса:
1. Это для тебя лично, для проекта/бизнеса, или для события?
2. Главная цель — рассказать о чём-то / собрать заявку / показать портфолио?
3. Любимое настроение — строгое тёмное, лёгкое светлое, яркое цветное?»
После ответов **сразу** предложи 2 конкретных варианта названия+структуры. Дай выбрать и иди делать.
---
## 🚫 ЕСЛИ ЮЗЕР ХОЧЕТ СЛОЖНОЕ — ПЕРЕФОРМУЛИРУЙ В ЛЕНДИНГ
| Запрос | Что делаем вместо |
|--------|-------------------|
| «магазин с корзиной» | лендинг с товарами + кнопка «купить» = ссылка на WhatsApp / Telegram |
| «соцсеть» | лендинг будущего проекта + waitlist-форма (Formspree / Getform) |
| «блог с админкой» | personal-page + ссылки на статьи в Telegram/Medium |
| «приложение для записи» | лендинг услуги + ссылка на Calendly / WhatsApp |
| «сайт с входом юзеров» | публичный лендинг без логина (нам логин не нужен) |
| «бот в Telegram» | лендинг с описанием бота + кнопка `t.me/...` |
**Не говори «это невозможно».** Скажи: «У нас песочница только для статических сайтов. Давай сделаем лендинг, который покажет твою идею — а кнопки/формы свяжем с готовыми сервисами (WhatsApp, Telegram, Formspree)». Юзер счастлив, результат за 15 минут.
---
## 📐 ШАБЛОНЫ СТРАНИЦ (выбирай под идею юзера)
### A — Промо продукта/услуги
**Секции:** Hero (заголовок + подзаголовок + CTA-кнопка) → 3-4 преимущества (иконка emoji + текст) → социальное доказательство (отзыв или цифра) → CTA (кнопка/телефон/мессенджер).
### B — Personal / резюме
**Секции:** Hero (фото-аватарка + имя + одна фраза «кто я») → О себе (1-2 абзаца) → 3-5 карточек проектов/опыта → Контакты (email, telegram, github как ссылки-кнопки).
### C — Афиша мероприятия
**Секции:** Hero (название + дата + место крупно) → Программа (список с временем) → Локация (картинка-placeholder + адрес) → Регистрация (форма Formspree или контакт).
### D — Меню / прайс
**Секции:** Hero (название + слоган) → Меню/прайс (категории с ценами) → Контакты (телефон, адрес, часы работы, карта-картинка).
### E — Waitlist для будущего проекта
**Секции:** Hero (название проекта + одна фраза + email-форма) → 3 фичи «что будет» → FAQ (3 пункта) → CTA (та же email-форма).
Все шаблоны — **одна страница, прокрутка вниз**. Никаких роутов, ничего динамического.
---
## ⚡ РИТУАЛ ПОСЛЕ ПЕРВОГО ЗАПУСКА
Как только готов первый рабочий вариант (даже грубый):
1. **Сразу запушь:**
```bash
git add -A
git commit -m "v1"
git push origin HEAD:pages
```
2. **ОБЯЗАТЕЛЬНО** дай юзеру ссылку **жирно**:
> 🎉 Готово! Твой лендинг здесь: **https://pages.git.vibe42.kz/baglan/<repo>/**
3. Скажи: «Открой в новой вкладке, посмотри. Что хочешь поменять?»
4. Дальше короткие итерации: правка → push → новый URL-показ. Каждые 2-3 правки — push.
---
## ⚠️ ЖЕЛЕЗНЫЕ ПРАВИЛА (НЕ нарушать никогда)
1. **Только статика — HTML + CSS + JS в браузере.**
2. **Никакого бэкенда.** Никаких Node/Express/FastAPI/Django/PHP/Go-серверов. Никаких БД. Никакого Redis.
3. **Никакой аутентификации / OAuth / JWT.**
4. **Никакого Docker, nginx, sudo, системных настроек.**
5. **Никаких тяжёлых сборщиков** (`npm install` дерево на 500МБ). Tailwind — только через CDN.
6. **НИКОГДА `git init` в workspace root (`/workspaces/baglan`)** — это папка-контейнер юзера, не репозиторий.
---
## ✅ ВСЕГДА работай через `./new-project`
Если юзер сказал «сделай сайт NAME» / «создай проект NAME»:
```bash
cd /workspaces/baglan
./new-project NAME # создаёт repo в Gitea + клонит локально в ./NAME/
cd NAME
# теперь создавай index.html / style.css / script.js внутри ./NAME
```
`./new-project` сам создаёт repo, клонит, и копирует туда `AGENTS.md` + `design.md`.
---
## 🌐 Git и публикация
**НЕТ GitHub.** Self-hosted git: **https://git.vibe42.kz**
- Профиль юзера: https://git.vibe42.kz/baglan
- Pages (живые лендинги): https://pages.git.vibe42.kz/baglan/<repo>/
- Креды уже в `/workspaces/baglan/.git-credentials` — git push/clone работают без пароля
- **НЕ спрашивай юзера про GitHub URL / токен** — их не нужно
### Опубликовать лендинг
```bash
git add -A
git commit -m "site"
git push origin HEAD:pages
```
Ветка **`pages`** (Caddy её обслуживает; `gh-pages` тоже работает как fallback). Push → лендинг доступен мгновенно.
---
## 🔧 Когда что-то идёт не так
- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f`
- **Не дёргай Gitea API типа `/repos/.../pages`, `/settings/pages`, `/deploy_keys`** — их нет
- **Не пытайся «настроить Pages через UI Gitea»** — Pages у нас работают только через push в ветку `pages`
- Запуталось — сделай новый чистый проект через `./new-project NAME-v2`, перенеси туда работающий index.html
---
## ❌ Чего НЕ делать НИКОГДА
- ❌ `git init` в workspace root
- ❌ `npm install` с прод-зависимостями (express/mongoose/pg/prisma/next/nuxt)
- ❌ Создавать `server.js` / `app.py` / `main.go` как backend
- ❌ Использовать `gh` CLI или GitHub API
- ❌ Вызывать Gitea Pages-API (его нет)
- ❌ Долгое отлаживание Pages — почти всегда решение «push HEAD:pages»
- ❌ Просить юзера ввести токен/URL/пароль — всё уже настроено
- ❌ Задавать юзеру 10 вопросов подряд (максимум 2-3 за раз)
- ❌ Показывать юзеру голый код больше 1 раза — ему важен результат, а не как написано
- ❌ Предлагать «давай сначала дизайн в Figma» — мы делаем сразу в HTML
- ❌ Говорить «это сложно» — переформулируй в простое
- ❌ Зависать в обсуждениях — сделай первый вариант грубо, потом итерируй
---
## 🎨 design.md
Рядом лежит `design.md` с готовой палитрой, типографикой и стартер-шаблоном `index.html`. **Начинай с него.** Не выдумывай новые цвета — модифицируй существующие.

546
app.js
View File

@ -1,546 +0,0 @@
// Глобальные данные
let allObjects = [];
let filteredObjects = [];
let map = null;
let markers = [];
// Загрузка данных
async function loadData() {
try {
const response = await fetch('data.json');
allObjects = await response.json();
filteredObjects = [...allObjects];
initDashboard();
initMap();
populateFilters();
renderObjects();
renderProblems();
renderAnalytics();
updateNotifications();
} catch (error) {
console.error('Ошибка загрузки данных:', error);
}
}
// Инициализация панели
function initDashboard() {
const total = allObjects.length;
const active = allObjects.filter(o => ['План', 'Обследование', 'СМР'].includes(o.status)).length;
const completed = allObjects.filter(o => ['Готов', 'Сдан'].includes(o.status)).length;
const problems = allObjects.filter(o => o.has_problem).length;
const overdue = allObjects.filter(o => o.is_overdue).length;
const portsPlan = allObjects.reduce((sum, o) => sum + (o.ports || 0), 0);
const portsFact = allObjects.filter(o => ['Готов', 'Сдан'].includes(o.status))
.reduce((sum, o) => sum + (o.ports || 0), 0);
const percent = total > 0 ? ((completed / total) * 100).toFixed(1) : 0;
document.getElementById('kpi-total').textContent = total;
document.getElementById('kpi-active').textContent = active;
document.getElementById('kpi-completed').textContent = completed;
document.getElementById('kpi-problems').textContent = problems;
document.getElementById('kpi-overdue').textContent = overdue;
document.getElementById('kpi-ports-plan').textContent = portsPlan.toLocaleString();
document.getElementById('kpi-ports-fact').textContent = portsFact.toLocaleString();
document.getElementById('kpi-percent').textContent = percent + '%';
// График статусов
const statusCounts = {};
allObjects.forEach(o => {
statusCounts[o.status] = (statusCounts[o.status] || 0) + 1;
});
const ctx = document.getElementById('statusChart').getContext('2d');
new Chart(ctx, {
type: 'doughnut',
data: {
labels: Object.keys(statusCounts),
datasets: [{
data: Object.values(statusCounts),
backgroundColor: [
'#6b7280', // План
'#f59e0b', // Обследование
'#0ea5e9', // СМР
'#ef4444', // Проблема
'#10b981', // Готов
'#10b981' // Сдан
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
// Инициализация карты
function initMap() {
map = L.map('map').setView([48.0196, 66.9237], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// Маркеры объектов
allObjects.forEach(obj => {
const [lat, lng] = obj.coordinates;
let color = '#1e40af';
if (obj.is_overdue) color = '#ef4444';
else if (obj.is_approaching) color = '#f59e0b';
else if (obj.status === 'Сдан') color = '#10b981';
else if (obj.status === 'Проблема') color = '#ef4444';
const marker = L.circleMarker([lat, lng], {
radius: 8,
fillColor: color,
color: '#fff',
weight: 2,
opacity: 1,
fillOpacity: 0.8
});
marker.bindPopup(`
<div class="popup-content">
<h3>${obj.city}</h3>
<p>${obj.address}</p>
<p><strong>Портов:</strong> ${obj.ports}</p>
<p><strong>Статус:</strong> ${obj.status}</p>
<p><strong>Ответственный:</strong> ${obj.manager}</p>
</div>
`);
marker.on('click', () => openModal(obj));
markers.push(marker);
marker.addTo(map);
});
}
// Заполнение фильтров
function populateFilters() {
const regions = [...new Set(allObjects.map(o => o.region).filter(Boolean))].sort();
const cities = [...new Set(allObjects.map(o => o.city).filter(Boolean))].sort();
const managers = [...new Set(allObjects.map(o => o.manager).filter(Boolean))].sort();
const regionSelect = document.getElementById('filter-region');
const citySelect = document.getElementById('filter-city');
const managerSelect = document.getElementById('filter-manager');
regions.forEach(r => {
regionSelect.innerHTML += `<option value="${r}">${r}</option>`;
});
cities.forEach(c => {
citySelect.innerHTML += `<option value="${c}">${c}</option>`;
});
managers.forEach(m => {
managerSelect.innerHTML += `<option value="${m}">${m}</option>`;
});
}
// Фильтрация объектов
function filterObjects() {
const region = document.getElementById('filter-region').value;
const city = document.getElementById('filter-city').value;
const status = document.getElementById('filter-status').value;
const manager = document.getElementById('filter-manager').value;
const search = document.getElementById('search-input').value.toLowerCase();
filteredObjects = allObjects.filter(o => {
if (region && o.region !== region) return false;
if (city && o.city !== city) return false;
if (status && o.status !== status) return false;
if (manager && o.manager !== manager) return false;
if (search && !o.address.toLowerCase().includes(search) &&
!o.city.toLowerCase().includes(search)) return false;
return true;
});
renderObjects();
}
// Рендер объектов
function renderObjects() {
const container = document.getElementById('objects-list');
container.innerHTML = '';
filteredObjects.forEach(obj => {
const card = createObjectCard(obj);
container.innerHTML += card;
});
}
// Создание карточки объекта
function createObjectCard(obj) {
const overdueClass = obj.is_overdue ? 'overdue' : '';
const approachingClass = obj.is_approaching && !obj.is_overdue ? 'approaching' : '';
const statusClass = `status-${obj.status.toLowerCase().replace(/ /g, '-')}`;
return `
<div class="object-card ${overdueClass} ${approachingClass} ${statusClass}" onclick="openModal(${JSON.stringify(obj).replace(/"/g, '&quot;')})">
<div class="flex justify-between items-start mb-3">
<h3 class="font-bold text-lg text-dark">${obj.city}</h3>
<span class="status-badge ${obj.status.toLowerCase().replace(/ /g, '-')}">${obj.status}</span>
</div>
<p class="text-gray-600 text-sm mb-2">${obj.address}</p>
<div class="space-y-1 text-sm">
<div class="flex justify-between">
<span class="text-gray-500">Тип:</span>
<span class="font-medium">${obj.object_type}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Портов:</span>
<span class="font-medium">${obj.ports}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">План:</span>
<span class="font-medium">${formatDate(obj.plan_end_date)}</span>
</div>
${obj.fact_end_date ? `
<div class="flex justify-between">
<span class="text-gray-500">Факт:</span>
<span class="font-medium text-green-600">${formatDate(obj.fact_end_date)}</span>
</div>
` : ''}
<div class="flex justify-between">
<span class="text-gray-500">Ответственный:</span>
<span class="font-medium">${obj.manager}</span>
</div>
</div>
${obj.is_overdue ? `
<div class="mt-3 p-2 bg-red-50 rounded text-red-700 text-xs font-semibold">
⚠️ Просрочено на ${getDaysOverdue(obj.plan_end_date)} дн.
</div>
` : ''}
${obj.is_approaching && !obj.is_overdue ? `
<div class="mt-3 p-2 bg-yellow-50 rounded text-yellow-700 text-xs font-semibold">
⏰ Срок истекает через ${getDaysUntil(obj.plan_end_date)} дн.
</div>
` : ''}
</div>
`;
}
// Рендер проблемных объектов
function renderProblems() {
const problems = allObjects.filter(o => o.has_problem);
const container = document.getElementById('problems-list');
container.innerHTML = '';
problems.forEach(obj => {
container.innerHTML += createObjectCard(obj);
});
}
// Аналитика
function renderAnalytics() {
const regions = {};
allObjects.forEach(obj => {
if (!regions[obj.region]) {
regions[obj.region] = {
plan: 0,
fact: 0,
portsPlan: 0,
portsFact: 0
};
}
regions[obj.region].plan++;
if (['Готов', 'Сдан'].includes(obj.status)) {
regions[obj.region].fact++;
regions[obj.region].portsFact += obj.ports || 0;
}
regions[obj.region].portsPlan += obj.ports || 0;
});
const regionNames = Object.keys(regions);
const planData = regionNames.map(r => regions[r].plan);
const factData = regionNames.map(r => regions[r].fact);
const percentData = regionNames.map(r =>
regions[r].plan > 0 ? ((regions[r].fact / regions[r].plan) * 100).toFixed(1) : 0
);
const portsPlanData = regionNames.map(r => regions[r].portsPlan);
const portsFactData = regionNames.map(r => regions[r].portsFact);
// План vs Факт
new Chart(document.getElementById('regionsChart').getContext('2d'), {
type: 'bar',
data: {
labels: regionNames,
datasets: [
{
label: 'План',
data: planData,
backgroundColor: '#1e40af'
},
{
label: 'Факт',
data: factData,
backgroundColor: '#10b981'
}
]
},
options: {
responsive: true,
scales: {
y: { beginAtZero: true }
}
}
});
// Процент выполнения
new Chart(document.getElementById('percentChart').getContext('2d'), {
type: 'bar',
data: {
labels: regionNames,
datasets: [{
label: '% выполнения',
data: percentData,
backgroundColor: percentData.map(p => p >= 100 ? '#10b981' : p >= 50 ? '#f59e0b' : '#ef4444')
}]
},
options: {
responsive: true,
scales: {
y: { beginAtZero: true, max: 100 }
}
}
});
// Порты
new Chart(document.getElementById('portsChart').getContext('2d'), {
type: 'bar',
data: {
labels: regionNames,
datasets: [
{
label: 'Портов по плану',
data: portsPlanData,
backgroundColor: '#6366f1'
},
{
label: 'Портов факт',
data: portsFactData,
backgroundColor: '#10b981'
}
]
},
options: {
responsive: true,
scales: {
y: { beginAtZero: true }
}
}
});
}
// Уведомления
function updateNotifications() {
const overdue = allObjects.filter(o => o.is_overdue);
const approaching = allObjects.filter(o => o.is_approaching && !o.is_overdue);
const badge = document.getElementById('notif-badge');
badge.textContent = overdue.length + approaching.length;
const list = document.getElementById('notif-list');
list.innerHTML = '';
overdue.forEach(obj => {
list.innerHTML += `
<div class="notif-item danger" onclick="showObject(${obj.id}); toggleNotifications();">
<p class="font-semibold text-red-800">Просрочен: ${obj.city}</p>
<p class="text-sm text-red-600">${obj.address}</p>
<p class="text-xs text-red-500 mt-1">План: ${formatDate(obj.plan_end_date)}</p>
</div>
`;
});
approaching.forEach(obj => {
list.innerHTML += `
<div class="notif-item warning" onclick="showObject(${obj.id}); toggleNotifications();">
<p class="font-semibold text-yellow-800">Срок подходит: ${obj.city}</p>
<p class="text-sm text-yellow-600">${obj.address}</p>
<p class="text-xs text-yellow-500 mt-1">План: ${formatDate(obj.plan_end_date)}</p>
</div>
`;
});
if (overdue.length === 0 && approaching.length === 0) {
list.innerHTML = '<p class="text-gray-500 text-center py-4">Нет уведомлений</p>';
}
}
function toggleNotifications() {
const notif = document.getElementById('notifications');
notif.classList.toggle('hidden');
}
// Модальное окно
function openModal(obj) {
document.getElementById('modal-title').textContent = `${obj.city} - ${obj.address}`;
const content = document.getElementById('modal-content');
content.innerHTML = `
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="bg-gray-50 p-3 rounded-lg">
<p class="text-gray-500 text-sm">Тип объекта</p>
<p class="font-semibold">${obj.object_type}</p>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<p class="text-gray-500 text-sm">Статус</p>
<p class="font-semibold">${obj.status}</p>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="bg-gray-50 p-3 rounded-lg">
<p class="text-gray-500 text-sm">Количество портов</p>
<p class="font-semibold text-2xl">${obj.ports}</p>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<p class="text-gray-500 text-sm">Проникновение</p>
<p class="font-semibold text-2xl">${obj.penetration || 0}%</p>
</div>
</div>
<div class="space-y-2">
<div class="flex justify-between border-b pb-2">
<span class="text-gray-600">Регион:</span>
<span class="font-medium">${obj.region}</span>
</div>
<div class="flex justify-between border-b pb-2">
<span class="text-gray-600">Адрес:</span>
<span class="font-medium">${obj.address}</span>
</div>
<div class="flex justify-between border-b pb-2">
<span class="text-gray-600">Ответственный:</span>
<span class="font-medium">${obj.manager}</span>
</div>
<div class="flex justify-between border-b pb-2">
<span class="text-gray-600">Дата начала:</span>
<span class="font-medium">${formatDate(obj.start_date)}</span>
</div>
<div class="flex justify-between border-b pb-2">
<span class="text-gray-600">Плановая дата:</span>
<span class="font-medium">${formatDate(obj.plan_end_date)}</span>
</div>
${obj.fact_end_date ? `
<div class="flex justify-between border-b pb-2">
<span class="text-gray-600">Фактическая дата:</span>
<span class="font-medium text-green-600">${formatDate(obj.fact_end_date)}</span>
</div>
` : ''}
<div class="flex justify-between border-b pb-2">
<span class="text-gray-600">Домов:</span>
<span class="font-medium">${obj.houses || 0}</span>
</div>
<div class="flex justify-between border-b pb-2">
<span class="text-gray-600">Квартир:</span>
<span class="font-medium">${obj.apartments || 0}</span>
</div>
</div>
${obj.problem_description ? `
<div class="bg-red-50 p-4 rounded-lg">
<p class="font-semibold text-red-800 mb-2">⚠️ Описание проблемы</p>
<p class="text-red-700">${obj.problem_description}</p>
</div>
` : ''}
${obj.comment ? `
<div class="bg-blue-50 p-4 rounded-lg">
<p class="font-semibold text-blue-800 mb-2">💬 Комментарий</p>
<p class="text-blue-700">${obj.comment}</p>
</div>
` : ''}
${obj.is_overdue ? `
<div class="bg-red-100 p-4 rounded-lg border-2 border-red-300">
<p class="font-bold text-red-800">🚨 ОБЪЕКТ ПРОСРОЧЕН</p>
<p class="text-red-700 mt-1">Просрочка: ${getDaysOverdue(obj.plan_end_date)} дн.</p>
</div>
` : ''}
${obj.is_approaching && !obj.is_overdue ? `
<div class="bg-yellow-100 p-4 rounded-lg border-2 border-yellow-300">
<p class="font-bold text-yellow-800">⏰ ВНИМАНИЕ: СРОК ПОДХОДИТ</p>
<p class="text-yellow-700 mt-1">Осталось дней: ${getDaysUntil(obj.plan_end_date)}</p>
</div>
` : ''}
</div>
`;
document.getElementById('object-modal').classList.remove('hidden');
}
function closeModal() {
document.getElementById('object-modal').classList.add('hidden');
}
function showObject(id) {
const obj = allObjects.find(o => o.id === id);
if (obj) openModal(obj);
}
// Переключение секций
function showSection(sectionId) {
document.querySelectorAll('.section').forEach(s => s.classList.add('hidden'));
document.getElementById(sectionId).classList.remove('hidden');
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
if (sectionId === 'objects') {
setTimeout(() => map.invalidateSize(), 300);
}
}
// Вспомогательные функции
function formatDate(dateStr) {
if (!dateStr) return '—';
const date = new Date(dateStr);
return date.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
function getDaysOverdue(dateStr) {
const today = new Date('2026-08-17');
const planDate = new Date(dateStr);
const diff = Math.floor((today - planDate) / (1000 * 60 * 60 * 24));
return diff > 0 ? diff : 0;
}
function getDaysUntil(dateStr) {
const today = new Date('2026-08-17');
const planDate = new Date(dateStr);
const diff = Math.floor((planDate - today) / (1000 * 60 * 60 * 24));
return diff > 0 ? diff : 0;
}
// Закрытие модального окна по клику вне
document.getElementById('object-modal').addEventListener('click', (e) => {
if (e.target.id === 'object-modal') closeModal();
});
// Закрытие уведомлений по клику вне
document.addEventListener('click', (e) => {
const notif = document.getElementById('notifications');
const btn = e.target.closest('button[onclick*="toggleNotifications"]');
if (!notif.contains(e.target) && !btn && !notif.classList.contains('hidden')) {
notif.classList.add('hidden');
}
});
// Инициализация при загрузке
document.addEventListener('DOMContentLoaded', loadData);

8827
data.json

File diff suppressed because it is too large Load Diff

111
design.md

File diff suppressed because one or more lines are too long

View File

@ -1,304 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GPON Control KZ - Система контроля строительства</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="style.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#1e40af',
secondary: '#0ea5e9',
accent: '#f59e0b',
success: '#10b981',
danger: '#ef4444',
warning: '#f59e0b',
dark: '#1e293b',
light: '#f8fafc'
}
}
}
}
</script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body class="bg-gray-50">
<!-- Навигация -->
<nav class="bg-dark text-white shadow-lg sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4">
<div class="flex justify-between items-center h-16">
<div class="flex items-center space-x-3">
<svg class="w-8 h-8 text-secondary" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"/>
<path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z"/>
</svg>
<span class="text-xl font-bold">GPON Control KZ</span>
</div>
<div class="hidden md:flex space-x-6">
<button onclick="showSection('dashboard')" class="nav-btn active">Главная</button>
<button onclick="showSection('objects')" class="nav-btn">Объекты</button>
<button onclick="showSection('problems')" class="nav-btn">Проблемные</button>
<button onclick="showSection('analytics')" class="nav-btn">Аналитика</button>
</div>
<div class="flex items-center space-x-4">
<button onclick="toggleNotifications()" class="relative">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
<span id="notif-badge" class="absolute -top-2 -right-2 bg-danger text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">0</span>
</button>
<div class="text-sm">
<div class="font-semibold">Шорабек Б.</div>
<div class="text-gray-400 text-xs">Руководитель проекта</div>
</div>
</div>
</div>
</div>
</nav>
<!-- Уведомления -->
<div id="notifications" class="fixed right-4 top-20 z-50 hidden">
<div class="bg-white rounded-lg shadow-xl p-4 w-80 max-h-96 overflow-y-auto">
<h3 class="font-bold text-lg mb-3">Уведомления</h3>
<div id="notif-list"></div>
</div>
</div>
<!-- Главная панель -->
<section id="dashboard" class="section">
<div class="max-w-7xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-dark mb-6">Панель управления</h1>
<!-- KPI карточки -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-white rounded-xl shadow-md p-6 border-l-4 border-primary">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">Всего объектов</p>
<p id="kpi-total" class="text-3xl font-bold text-dark">0</p>
</div>
<div class="bg-blue-100 p-3 rounded-full">
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
</svg>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-md p-6 border-l-4 border-secondary">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">В работе</p>
<p id="kpi-active" class="text-3xl font-bold text-dark">0</p>
</div>
<div class="bg-cyan-100 p-3 rounded-full">
<svg class="w-8 h-8 text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-md p-6 border-l-4 border-success">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">Завершено</p>
<p id="kpi-completed" class="text-3xl font-bold text-dark">0</p>
</div>
<div class="bg-green-100 p-3 rounded-full">
<svg class="w-8 h-8 text-success" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-md p-6 border-l-4 border-danger">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">Проблемные</p>
<p id="kpi-problems" class="text-3xl font-bold text-dark">0</p>
</div>
<div class="bg-red-100 p-3 rounded-full">
<svg class="w-8 h-8 text-danger" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-md p-6 border-l-4 border-warning">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">Просрочено</p>
<p id="kpi-overdue" class="text-3xl font-bold text-dark">0</p>
</div>
<div class="bg-yellow-100 p-3 rounded-full">
<svg class="w-8 h-8 text-warning" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-md p-6 border-l-4 border-purple-500">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">Портов по плану</p>
<p id="kpi-ports-plan" class="text-3xl font-bold text-dark">0</p>
</div>
<div class="bg-purple-100 p-3 rounded-full">
<svg class="w-8 h-8 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
</svg>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-md p-6 border-l-4 border-green-500">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">Портов факт</p>
<p id="kpi-ports-fact" class="text-3xl font-bold text-dark">0</p>
</div>
<div class="bg-green-100 p-3 rounded-full">
<svg class="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-md p-6 border-l-4 border-indigo-500">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">% выполнения</p>
<p id="kpi-percent" class="text-3xl font-bold text-dark">0%</p>
</div>
<div class="bg-indigo-100 p-3 rounded-full">
<svg class="w-8 h-8 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
</div>
</div>
</div>
</div>
<!-- Карта и статусы -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Карта -->
<div class="lg:col-span-2 bg-white rounded-xl shadow-md p-6">
<h2 class="text-xl font-bold text-dark mb-4">Карта объектов Казахстана</h2>
<div id="map" class="h-96 rounded-lg"></div>
</div>
<!-- Статусы -->
<div class="bg-white rounded-xl shadow-md p-6">
<h2 class="text-xl font-bold text-dark mb-4">Статусы объектов</h2>
<div id="status-chart-container" class="h-64">
<canvas id="statusChart"></canvas>
</div>
</div>
</div>
</div>
</section>
<!-- Объекты -->
<section id="objects" class="section hidden">
<div class="max-w-7xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-dark mb-6">Все объекты</h1>
<!-- Фильтры -->
<div class="bg-white rounded-xl shadow-md p-6 mb-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<select id="filter-region" onchange="filterObjects()" class="border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary">
<option value="">Все регионы</option>
</select>
<select id="filter-city" onchange="filterObjects()" class="border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary">
<option value="">Все города</option>
</select>
<select id="filter-status" onchange="filterObjects()" class="border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary">
<option value="">Все статусы</option>
<option value="План">План</option>
<option value="Обследование">Обследование</option>
<option value="СМР">СМР</option>
<option value="Проблема">Проблема</option>
<option value="Готов">Готов</option>
<option value="Сдан">Сдан</option>
</select>
<select id="filter-manager" onchange="filterObjects()" class="border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary">
<option value="">Все ответственные</option>
</select>
</div>
<div class="mt-4">
<input type="text" id="search-input" oninput="filterObjects()" placeholder="Поиск по адресу или названию..." class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary">
</div>
</div>
<!-- Список объектов -->
<div id="objects-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"></div>
</div>
</section>
<!-- Проблемные объекты -->
<section id="problems" class="section hidden">
<div class="max-w-7xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-dark mb-6 flex items-center">
<svg class="w-8 h-8 text-danger mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
Проблемные объекты
</h1>
<div id="problems-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"></div>
</div>
</section>
<!-- Аналитика -->
<section id="analytics" class="section hidden">
<div class="max-w-7xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-dark mb-6">Аналитика по регионам</h1>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div class="bg-white rounded-xl shadow-md p-6">
<h3 class="text-lg font-bold mb-4">План vs Факт по регионам</h3>
<canvas id="regionsChart"></canvas>
</div>
<div class="bg-white rounded-xl shadow-md p-6">
<h3 class="text-lg font-bold mb-4">Процент выполнения по регионам</h3>
<canvas id="percentChart"></canvas>
</div>
</div>
<div class="bg-white rounded-xl shadow-md p-6">
<h3 class="text-lg font-bold mb-4">Количество портов по регионам</h3>
<canvas id="portsChart"></canvas>
</div>
</div>
</section>
<!-- Модальное окно объекта -->
<div id="object-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center p-4">
<div class="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div class="p-6">
<div class="flex justify-between items-start mb-4">
<h2 id="modal-title" class="text-2xl font-bold text-dark"></h2>
<button onclick="closeModal()" class="text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div id="modal-content"></div>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>

256
style.css
View File

@ -1,256 +0,0 @@
/* Основные стили */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.section {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Навигация */
.nav-btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.3s;
background: transparent;
color: #cbd5e1;
border: none;
cursor: pointer;
font-size: 0.95rem;
}
.nav-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.nav-btn.active {
background: #0ea5e9;
color: white;
}
/* Карточки объектов */
.object-card {
background: white;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
cursor: pointer;
border-left: 4px solid #1e40af;
}
.object-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}
.object-card.status-план {
border-left-color: #6b7280;
}
.object-card.status-обследование {
border-left-color: #f59e0b;
}
.object-card.status-смр {
border-left-color: #0ea5e9;
}
.object-card.status-проблема {
border-left-color: #ef4444;
}
.object-card.status-готов {
border-left-color: #10b981;
}
.object-card.status-сдан {
border-left-color: #10b981;
}
.object-card.overdue {
background: linear-gradient(135deg, #fef2f2 0%, white 100%);
border-left-color: #ef4444;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
50% {
box-shadow: 0 4px 20px rgba(239, 68, 68, 0.3);
}
}
.object-card.approaching {
border-left-color: #f59e0b;
background: linear-gradient(135deg, #fffbeb 0%, white 100%);
}
/* Статусы */
.status-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.status-badge.план {
background: #f3f4f6;
color: #6b7280;
}
.status-badge.обследование {
background: #fef3c7;
color: #92400e;
}
.status-badge.смр {
background: #dbeafe;
color: #1e40af;
}
.status-badge.проблема {
background: #fee2e2;
color: #991b1b;
}
.status-badge.готов {
background: #d1fae5;
color: #065f46;
}
.status-badge.сдан {
background: #d1fae5;
color: #065f46;
}
/* Карта */
#map {
z-index: 1;
}
.leaflet-popup-content-wrapper {
border-radius: 1rem;
padding: 0;
overflow: hidden;
}
.leaflet-popup-content {
margin: 0;
width: 280px !important;
}
.popup-content {
padding: 1rem;
}
.popup-content h3 {
font-weight: bold;
margin-bottom: 0.5rem;
color: #1e293b;
}
.popup-content p {
margin-bottom: 0.25rem;
font-size: 0.875rem;
color: #64748b;
}
/* Уведомления */
.notif-item {
padding: 0.75rem;
border-bottom: 1px solid #e2e8f0;
border-radius: 0.5rem;
margin-bottom: 0.5rem;
background: #f8fafc;
}
.notif-item.danger {
background: #fef2f2;
border-left: 3px solid #ef4444;
}
.notif-item.warning {
background: #fffbeb;
border-left: 3px solid #f59e0b;
}
.notif-item.success {
background: #f0fdf4;
border-left: 3px solid #10b981;
}
/* Адаптивность */
@media (max-width: 768px) {
.nav-btn {
font-size: 0.8rem;
padding: 0.4rem 0.6rem;
}
.grid.grid-cols-1.md\:grid-cols-2.lg\:grid-cols-3 {
grid-template-columns: 1fr;
}
.grid.grid-cols-1.md\:grid-cols-4 {
grid-template-columns: 1fr;
}
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
/* Modal animations */
#object-modal {
transition: opacity 0.3s ease;
}
#object-modal.hidden {
opacity: 0;
pointer-events: none;
}
/* Chart containers */
canvas {
max-height: 400px;
}