Compare commits
No commits in common. "pages" and "main" have entirely different histories.
173
AGENTS.md
173
AGENTS.md
@ -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
546
app.js
@ -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, '"')})">
|
|
||||||
<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);
|
|
||||||
304
index.html
304
index.html
@ -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
256
style.css
@ -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;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user