Initial commit: Merakomis portal, Docker stack and user-reader API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keboss-m 2026-06-24 11:04:05 +03:00
commit 5c21d25d45
22274 changed files with 1964384 additions and 0 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
load_prefix /opt/alt/php74/usr

19
.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# Локальные данные Docker (не коммитить)
data/db/
data/backup/
data/dumps/
data/mysql-init/*.sql
# Полные дампы и крупные архивы (восстановить локально / из scripts)
/date-db-mysql_*/
date-db-mysql_*.sql.zip
www.zip
meraproject-test.zip
# Python
__pycache__/
*.py[cod]
.pytest_cache/
# Локальные IDE (не обязательны в репозитории)
.cursor/

226
.htaccess Normal file
View File

@ -0,0 +1,226 @@
<IfModule mod_headers.c>
Header append Vary Accept env=REDIRECT_accept
</IfModule>
<IfModule mod_mime.c>
AddType image/webp .webp
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_ACCEPT} image/webp
RewriteCond %{REQUEST_URI} (.*)(\.(jpe?g|png))$
RewriteCond %{DOCUMENT_ROOT}/%1\_webp.webp -f
RewriteRule .* %1_webp\.webp [L,T=image/webp]
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} (.*)(\.(jpg))$
RewriteCond %{DOCUMENT_ROOT}/%1_jpg\.jpg -f
RewriteRule .* %1_jpg\.jpg [L,T=image/jpg]
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} (.*)(\.(png))$
RewriteCond %{DOCUMENT_ROOT}/%1_png\.png -f
RewriteRule .* %1_png\.png [L,T=image/png]
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine on
Options +FollowSymlinks
RewriteRule ^(.*).md index.php [L]
RewriteRule ^config.php$ index.php [L]
RewriteRule ^robots.txt$ index.php [L]
RewriteRule ^sitemap.xml$ index.php [L]
RewriteRule ^yml.xml$ index.php [L]
RewriteCond %{HTTP_HOST} ^www\.(.*)$
RewriteRule ^(.*)$ https://%1/$1 [L,R=301]
#RewriteCond %{REQUEST_FILENAME} !-f
#RewriteCond %{REQUEST_URI} !(.*)/$
#RewriteCond %{REQUEST_FILENAME} !-f
#RewriteRule ^(.+[^/])$ /$1/ [R=301,L]
#RewriteRule ^api/?([^/]+)?/?([^/]+)?/$ module/api.php?module=$1&action=$2&%{QUERY_STRING} [L]
RewriteRule ^api/([^/]+)?/?([^/]+)?/$ index.php?api=1&module=$1&action=$2&%{QUERY_STRING} [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^.*$ index.php?%{QUERY_STRING} [L]
</ifModule>
AddType application/x-javascript .js
#AddType application/octet-stream .docx .mov .pdf .xlsx
AddType application/vnd.ms-word.document.macroEnabled.12 docm
AddType application/vnd.openxmlformats-officedocument.wordprocessingml.document docx
AddType application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx
AddType application/vnd.ms-powerpoint.template.macroEnabled.12 potm
AddType application/vnd.openxmlformats-officedocument.presentationml.template potx
AddType application/vnd.ms-powerpoint.addin.macroEnabled.12 ppam
AddType application/vnd.ms-powerpoint.slideshow.macroEnabled.12 ppsm
AddType application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx
AddType application/vnd.ms-powerpoint.presentation.macroEnabled.12 pptm
AddType application/vnd.openxmlformats-officedocument.presentationml.presentation pptx
AddType application/vnd.ms-excel.addin.macroEnabled.12 xlam
AddType application/vnd.ms-excel.sheet.binary.macroEnabled.12 xlsb
AddType application/vnd.ms-excel.sheet.macroEnabled.12 xlsm
AddType application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx
AddType application/vnd.ms-excel.template.macroEnabled.12 xltm
AddType application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx
#--------------------
<IfModule mod_deflate.c>
# force deflate for mangled headers
# developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping/
<IfModule mod_setenvif.c>
<IfModule mod_headers.c>
SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
</IfModule>
</IfModule>
<IfModule !mod_filter.c>
# Legacy versions of Apache
AddOutputFilterByType DEFLATE text/html text/plain text/css application/json
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE text/xml application/xml text/x-component
AddOutputFilterByType DEFLATE application/xhtml+xml application/rss+xml application/atom+xml
AddOutputFilterByType DEFLATE image/svg+xml application/vnd.ms-fontobject application/x-font-ttf font/opentype
</IfModule>
</IfModule>
# FileETag None is not enough for every server.
<IfModule mod_headers.c>
Header unset ETag
</IfModule>
# Since we`re sending far-future expires, we don't need ETags for static content.
# developer.yahoo.com/performance/rules.html#etags
FileETag None
# ----------------------------------------------------------------------
# Кроссдоменные изображения (CORS-enabled images) (@crossorigin)
# ----------------------------------------------------------------------
# Отсылать CORS заголовки, если браузер требует их, для изображений по умолчанию включено.
# http://developer.mozilla.org/en/CORS_Enabled_Image
# http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html
# http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/
# wiki.mozilla.org/Security/Reviews/crossoriginAttribute
<IfModule mod_setenvif.c>
<IfModule mod_headers.c>
# mod_headers, ну почему ты не определяешь Content-Type?!
<FilesMatch "\.(gif|png|jpe?g|svg|svgz|ico|webp)$">
SetEnvIf Origin ":" IS_CORS
Header set Access-Control-Allow-Origin "*" env=IS_CORS
</FilesMatch>
</IfModule>
</IfModule>
# ----------------------------------------------------------------------
# Задаем Expires заголовки (срок актуальности файла) (для лучшего кэширования)
# ----------------------------------------------------------------------
# Указываются заголовки с большим сроком "годности",
# предполагается, что вы используете кэш на основе названий файлов (all.css?v001).
# В дополнение, учтите, что старые прокси могут кэшировать не верно
# http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/
# Если вы не используете названия файлов для контроля версий, измените кэш для CSS и JS, например на
# "access plus 1 week".
<IfModule mod_expires.c>
ExpiresActive on
# Задаем значение по умолчанию (для всех файлов)
ExpiresDefault "access plus 1 month"
# cache.appcache нельзя кэшировать в FF 3.6 (спасибо Remy ~Introducing HTML5)
ExpiresByType text/cache-manifest "access plus 0 seconds"
# Ваш html документ
<FilesMatch \.(html|xhtml|xml|shtml|phtml|php|txt)$>
ExpiresDefault "access plus 0 seconds"
</FilesMatch>
ExpiresByType text/html "access plus 0 seconds"
# Данные
ExpiresByType text/xml "access plus 0 seconds"
ExpiresByType application/xml "access plus 0 seconds"
ExpiresByType application/json "access plus 0 seconds"
# Рассылка
ExpiresByType application/rss+xml "access plus 1 hour"
ExpiresByType application/atom+xml "access plus 1 hour"
# Favicon (не может быть переименован)
<FilesMatch \.(ico)$>
ExpiresDefault "access plus 1 week"
</FilesMatch>
ExpiresByType image/x-icon "access plus 1 week"
# Медиа: изображения, видео, аудио
<FilesMatch \.(gif|png|jpg|jpeg|ogg|mp4|mkv|flv|swf|wmv|asf|asx|wma|wax|wmx|wm)$>
ExpiresDefault "access plus 1 year"
</FilesMatch>
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType video/ogg "access plus 1 month"
ExpiresByType audio/ogg "access plus 1 month"
ExpiresByType video/mp4 "access plus 1 month"
ExpiresByType video/webm "access plus 1 month"
# HTC файлы (css3pie)
ExpiresByType text/x-component "access plus 1 month"
# Веб-шрифты
<FilesMatch \.(eot|ttf|otf|svg|woff)$>
ExpiresDefault "access plus 1 year"
</FilesMatch>
ExpiresByType application/x-font-ttf "access plus 1 month"
ExpiresByType font/opentype "access plus 1 month"
ExpiresByType application/x-font-woff "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
ExpiresByType application/vnd.ms-fontobject "access plus 1 month"
# CSS и JavaScript
<FilesMatch \.(css|js)$>
ExpiresDefault "access plus 1 year"
</FilesMatch>
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
# Статичные ресурсы
<FilesMatch \.(swf|pdf|doc|rtf|xls|ppt)$>
ExpiresDefault "access plus 1 year"
</FilesMatch>
ExpiresByType application/x-shockwave-flash "access plus 1 year"
ExpiresByType application/pdf "access plus 1 year"
ExpiresByType application/msword "access plus 1 year"
ExpiresByType application/rtf "access plus 1 year"
ExpiresByType application/vnd.ms-excel "access plus 1 year"
ExpiresByType application/vnd.ms-powerpoint "access plus 1 year"
</IfModule>

816
DEVELOPERS.md Normal file
View File

@ -0,0 +1,816 @@
# User Reader — документация для разработчиков
HTTP-сервис на **FastAPI**, читает данные Merakomis из **MySQL** и отдаёт JSON. Предназначен для потребления **другими микросервисами и внешними приложениями**: синхронизация каталога сотрудников, справочники проектов, сводки трудозатрат, запись табеля.
Интерактивная схема: **`/docs`** (Swagger UI), **`/redoc`**.
**Справочник только по API, доступу и работе с эндпоинтами:** [docs/user-reader-api.md](docs/user-reader-api.md).
---
## Содержание
1. [Быстрый старт для интеграции](#быстрый-старт-для-интеграции)
2. [Сеть и развёртывание](#сеть-и-развёртывание)
3. [Аутентификация](#аутентификация)
4. [Общие соглашения API](#общие-соглашения-api)
5. [Модель данных Merakomis](#модель-данных-merakomis)
6. [Справочник эндпоинтов](#справочник-эндпоинтов)
7. [Сотрудники](#сотрудники)
8. [Трудозатраты (read)](#трудозатраты-read)
9. [Табель (read / write)](#табель-read--write)
10. [Сценарии интеграции](#сценарии-интеграции)
11. [Коды ошибок](#коды-ошибок)
12. [Примеры запросов](#примеры-запросов)
13. [Запуск и сборка](#запуск-и-сборка)
---
## Быстрый старт для интеграции
Минимальный цикл подключения другого сервиса:
1. **Проверить доступность**`GET /api/health` (без ключа).
2. **Узнать схему сотрудников**`GET /api/meta` с API-ключом.
3. **Синхронизировать сотрудников**`GET /api/employees` или инкремент `GET /api/employees/delta`.
4. **Получить сводку часов за период**`GET /api/work-report?date_from=…&date_to=…`.
5. **(Опционально) Записать часы** — `PUT /api/time-entries` с заголовками `X-Api-Key` и `X-Acting-Emp-Id`.
```text
Ваш сервис user-reader (:8090) MySQL
| X-Api-Key: … | |
| GET /api/work-report -------->| SELECT tMerakomisTime … |
|<-------- JSON items -----------| |
```
**Рекомендуемые эндпоинты для интеграции:**
| Задача | Эндпоинт |
|--------|----------|
| Каталог сотрудников | `/api/employees`, `/api/employees/delta` |
| Сводка часов (основной контракт) | `/api/work-report` |
| Сводка по проектам | `/api/project-report` |
| Участники проектов | `/api/project-members` |
| Запись в состав проекта | `PUT /api/project-members` |
| Разделы проекта (для формы) | `/api/project-sections` |
| Запись часов | `PUT /api/time-entries` |
---
## Сеть и развёртывание
### Docker Compose (этот репозиторий)
| Сервис | Контейнер | Порт на хосте | Внутри сети compose |
|--------|-----------|---------------|---------------------|
| user-reader | `meraproject-user-reader` | **8090** | `http://user-reader:8090` |
| MySQL | `meraproject-mysqldb` | 3307 | `db:3306` |
Переменные окружения user-reader заданы в `docker-compose.yml`:
```yaml
MYSQL_HOST: db
MYSQL_DATABASE: j7508239_tracker
USER_READER_API_KEY: "local-dev-key-change-in-prod" # сменить на проде
```
**С другого контейнера в той же compose-сети** обращайтесь к `http://user-reader:8090`.
**С хоста или другого сервера** — `http://<IP_хоста>:8090`.
**CORS:** включён для всех origin (`*`). Браузерные клиенты с другого домена могут вызывать API при наличии ключа.
### uvicorn локально
```bash
cd services/user-reader
pip install -r requirements.txt
# задать MYSQL_* и USER_READER_API_KEY
uvicorn app.main:app --host 0.0.0.0 --port 8090
```
### Статические страницы-превью (без отдельного фронтенда)
| URL | Назначение |
|-----|------------|
| `GET /` | Сотрудники |
| `GET /labor` | Проекты, разделы, сырые записи |
| `GET /summary` | UI над `/api/work-report` |
| `GET /project-report` | UI над `/api/project-report` |
| `GET /time-entry` | Форма записи часов (`PUT /api/time-entries`) |
| `GET /project-member` | Форма записи состава (`PUT /api/project-members`) |
---
## Аутентификация
Два независимых уровня.
### 1. Service-to-service: API-ключ
Если задан непустой **`USER_READER_API_KEY`**, все эндпоинты с пометкой «защищённый» требуют:
- заголовок **`X-Api-Key: <ключ>`**, или
- заголовок **`Authorization: Bearer <ключ>`**
Если ключ **не задан** (пустая строка) — проверки нет (только для локальной отладки).
Ответ при ошибке: **`401`**, тело FastAPI `{"detail": "…"}`.
### 2. Табель: идентичность сотрудника
Эндпоинты записи и чтения табеля (кроме справочников) дополнительно требуют:
| Заголовок / поле | Назначение |
|------------------|------------|
| **`X-Acting-Emp-Id`** | Кто действует (аналог PHP-сессии `fg_emp_id`) |
| **`emp_id`** в query/body | За кого операция; если опущен → равен `acting` |
Права проверяются по правилам Merakomis (админ, РП, делегат и т.д.) — см. `GET /api/labor/permissions`.
---
## Общие соглашения API
### Формат ответов
- Кодировка: **UTF-8**, тело — **JSON**.
- Даты в query: **`YYYY-MM-DD`**.
- Пагинация: `limit` + `offset`; в ответе обычно есть `total`, `count`.
- Числа с плавающей точкой (часы): до 2 знаков в агрегатах.
### Сериализация значений из MySQL
| Тип MySQL | JSON |
|-----------|------|
| `datetime` | ISO-строка с пробелом и секундами |
| `date` | `YYYY-MM-DD` |
| `Decimal` | `float` |
| `bytes` | UTF-8 с заменой ошибок |
### Секретные поля
В выдаче сотрудников **никогда** не попадают колонки с паролями: суффикс `_pass`, подстроки `password`, `secret`.
### Имена полей
- В таблицах MySQL колонки имеют префикс `tMerakomisProject_code` и т.п.
- В JSON API — **короткие алиасы**: `code`, `project_name`, `step_name`.
---
## Модель данных Merakomis
Ключевые таблицы (физические имена в MySQL обычно в нижнем регистре).
| Логическое имя | Таблица | Назначение |
|----------------|---------|------------|
| Сотрудник | `tMerakomisEmp` | Каталог сотрудников |
| Проект | `tMerakomisProject` | Проекты компании |
| Стадия проекта | `tMerakomisDStep` | Справочник стадий (ПД, РД, К…) |
| Раздел | `tMerakomisDSection` | Разделы внутри проекта (ОВ1, ТХ…) |
| Участник команды | `tMerakomisTeamMember` | Сотрудник ↔ команда ↔ раздел |
| Запись времени | `tMerakomisTime` | Часы по дням |
| Отсутствие | `tMerakomisTimeAbsence` | Отпуск, больничный… |
| Тип отсутствия | `tMerakomisDAbsence` | Справочник типов |
| Должность / отдел | `tMerakomisPost``tMerakomisDDepartment` | Орготдел сотрудника (АР, ОВ…) |
### Стадия проекта (`step` / `step_name`)
- В проекте: колонка **`tMerakomisProject_step`** → в API поле **`step`** (целое id).
- Название стадии: справочник **`tMerakomisDStep`** (`id`, `name`, `text`).
- `name` — краткое обозначение: **ПД**, **РД**, **К**, **АН**
- `text` — полное описание («Проектная документация»).
- В эндпоинтах, где есть **`project_name`**, дополнительно отдаётся **`step_name`** (краткое название из справочника; пустая строка, если стадия не задана).
### Статус проекта (`status` / `status_name` / `archive`)
- В проекте: **`tMerakomisProject_status`** → в API **`status`** (целое 0…3), **`status_name`** — подпись из портала Merakomis (`eStatus.php`).
- Архив: **`tMerakomisProject_archive`** (`0`/`1`), **`tMerakomisProject_archive_date`** → **`archive_date`** (`YYYY-MM-DD` или `null`).
- В эндпоинтах с **`project_name`** (сводки, состав команды) поля **`status`**, **`status_name`**, **`archive`**, **`archive_date`** идут рядом со стадией.
| `status` | `status_name` |
|----------|---------------|
| `0` | Не определён |
| `1` | В работе |
| `2` | Завершён |
| `3` | Пауза |
> **Не путать:** поле `step` в **`tMerakomisDSection`** — фильтр разделов по стадии, это другая сущность.
### Переработки (`is_over`)
| `is_over` | Смысл |
|-----------|--------|
| `0` | Рабочие часы |
| `1` | Переработка |
В агрегатах:
- **`over1`** — переработка в рабочий день;
- **`over2`** — переработка в выходной/праздник или день отсутствия;
- **`over`** = `over1` + `over2`;
- **`total`** = `hours` + `over`.
Раздел проекта в сводках берётся из **членства в команде** (`TeamMember.section`), а не из строки времени.
---
## Справочник эндпоинтов
| Метод | Путь | Ключ | Acting | Назначение |
|-------|------|------|--------|------------|
| GET | `/api/health` | — | — | Живость и БД |
| GET | `/api/meta` | ✓ | — | Мета сотрудников |
| GET | `/api/employees` | ✓ | — | Список сотрудников |
| GET | `/api/employees/delta` | ✓ | — | Инкремент сотрудников |
| GET | `/api/labor/meta` | ✓ | — | Имена таблиц трудозатрат |
| GET | `/api/projects` | ✓ | — | Справочник проектов |
| GET | `/api/sections` | ✓ | — | Справочник разделов |
| GET | `/api/project-members` | ✓ | — | Участники проектов |
| GET | `/api/project-sections` | ✓ | — | Разделы, допустимые на проекте |
| GET | `/api/member-roles` | ✓ | — | Справочник ролей в команде |
| GET | `/api/time-entries` | ✓ | — | Сырые записи времени |
| GET | `/api/labor-summary` | ✓ | — | Агрегат (внутренние поля) |
| GET | `/api/work-report` | ✓ | — | **Сводка для интеграции** |
| GET | `/api/project-report` | ✓ | — | Сводка по проектам |
| GET | `/api/calendar-days` | ✓ | — | Производственный календарь |
| GET | `/api/absence-types` | ✓ | — | Типы отсутствий |
| GET | `/api/labor/permissions` | ✓ | ✓ | Проверка прав |
| GET | `/api/time-summary` | ✓ | ✓ | Сводка табеля (месяц/год) |
| GET | `/api/time-calendar` | ✓ | ✓ | Данные календаря табеля |
| PUT | `/api/time-entries` | ✓ | ✓ | Запись часов |
| PUT | `/api/project-members` | ✓ | ✓ | Запись состава проекта |
| POST | `/api/batch` | ✓ | — | Пакет до 25 GET-запросов |
| PUT | `/api/absences` | ✓ | ✓ | Отсутствия по датам |
| PUT | `/api/absences/range` | ✓ | ✓ | Отсутствия за диапазон |
✓ в колонке «Ключ» = нужен `USER_READER_API_KEY`, если он задан в окружении.
✓ в колонке «Acting» = обязателен заголовок `X-Acting-Emp-Id`.
---
## Переменные окружения
| Переменная | Назначение |
|------------|------------|
| `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE` | Подключение к MySQL |
| `USER_READER_API_KEY` | Service-to-service ключ; пусто = без проверки |
| `EMP_TABLE` | Явное имя таблицы сотрудников |
| `USER_TABLE` / `USER_READER_FALLBACK_TABLE` | Резервная таблица, если нет `tMerakomisEmp` |
| `DEBUG` | `1` — в ответе 500 может быть traceback |
---
## Сотрудники
### Режимы схемы таблицы
Сервис определяет таблицу через `resolve_emp_table()`:
1. **`merakomis_emp`** — колонки `tMerakomisEmp_*`; в JSON поля без префикса. Фильтр `removed = 0`, если колонка есть.
2. **`generic_user_table`** — произвольная таблица; все несекретные колонки.
PHP-ориентир: **`php_model`**: `"themes/merakomis/emp/model.php"`.
### `GET /api/health`
Без API-ключа.
```json
{ "ok": true, "db": true, "database": "j7508239_tracker" }
```
### `GET /api/meta` — защищённый
Метаданные: физическая таблица, режим схемы, поля выдачи, колонка для delta.
| Поле | Описание |
|------|----------|
| `schema_mode` | `merakomis_emp` или `generic_user_table` |
| `delta_field` | Поле для инкрементальной синхронизации (`updated` или эвристика) |
| `selected_fields` | Имена полей в `/api/employees` |
| `where_removed` | Колонка «удалён» в БД |
### `GET /api/employees` — защищённый
| Параметр | По умолчанию | Ограничения |
|----------|--------------|-------------|
| `limit` | `100` | 1…500 |
| `offset` | `0` | ≥ 0 |
**Ответ:** `total`, `limit`, `offset`, `items[]`.
Дополнительные поля (Merakomis):
| Поле | Описание |
|------|----------|
| `department` | Коды отдела через запятую: **АР**, **ГП**, **ОВ**… |
| `department_codes` | Массив кодов |
| `departments` | `department_id`, `name`, `short`, `code` |
| `staffing_ids`, `staffing_names`, `staffing_title`, `staffings` | Должность (Архитектор, ГИП, ГАП…) из `tMerakomisDStaffing` |
| `staffing` | Сырой JSON-массив id должностей в БД |
Источник отделов: `tMerakomisPost``tMerakomisDDepartment`. Поле `roles` — не должность, а служебный фильтр подразделения в портале.
### `GET /api/employees/delta` — защищённый
Инкремент: строки, у которых время изменения **строго больше** `since_updated` (Unix, секунды).
| Параметр | По умолчанию |
|----------|--------------|
| `since_updated` | `0` |
| `limit` | `500` |
**Ответ:** `since_updated`, `max_updated`, `delta_column`, `count`, `items`.
**Цикл синхронизации:**
```text
since = 0
loop:
GET /api/employees/delta?since_updated={since}&limit=500
обработать items
since = max_updated
пока count == limit
```
---
## Трудозатраты (read)
Мета-информация: **`GET /api/labor/meta`** — физические имена таблиц.
### `GET /api/projects` — защищённый
Справочник `tMerakomisProject`.
| Параметр | По умолчанию | Описание |
|----------|--------------|----------|
| `limit` | `100` | 1…500 |
| `offset` | `0` | |
| `include_removed` | `false` | Включать `removed = 1` |
| `include_archive` | `true` | Не фильтровать архивные |
**Поля `items`:** `id`, `code`, `name`, `director`, `step` (id стадии), `status`, `status_name`, `team`, `archive`, `archive_date`, `removed`, `date`, `date_end`.
Для человекочитаемой стадии и статуса используйте join через `/api/project-members` или агрегаты ниже (`step_name`, `status_name`).
### `GET /api/sections` — защищённый
Справочник `tMerakomisDSection`: `id`, `name`, `parent`, `step`.
| Параметр | Описание |
|----------|----------|
| `step` | Фильтр разделов по стадии (id из `tMerakomisDSection.step`) |
### `GET /api/project-members` — защищённый
Связь сотрудник ↔ проект ↔ раздел команды.
| Параметр | По умолчанию | Описание |
|----------|--------------|----------|
| `project_id`, `emp_id` | — | Фильтры |
| `active_only` | `true` | Только активные в команде |
| `limit` | `100` | 1…500 |
| `offset` | `0` | |
**Поля `items`:**
| Поле | Описание |
|------|----------|
| `member_id` | id записи TeamMember |
| `emp_id`, `emp_name` | Сотрудник |
| `project_id`, `project_code`, `project_name` | Проект |
| **`step_name`** | Краткое название стадии (ПД, РД…) |
| **`status`**, **`status_name`** | Статус проекта |
| **`archive`**, **`archive_date`** | Архив и дата архивации |
| `section_id`, `section_name` | Раздел в команде |
| `role` | Роль в команде |
| `active` | Активен в команде |
### `GET /api/project-sections` — защищённый
Разделы, допустимые для проекта (`tMerakomisProjectSection` → `tMerakomisDSection`).
| Параметр | Обязательно |
|----------|:-----------:|
| `project_id` | да |
**Ответ:** `project_id`, `total`, `items[]` с `section_id`, `section_name`.
### `GET /api/member-roles` — защищённый
Справочник ролей в команде (порт `eMemberRole`).
| Параметр | Описание |
|----------|----------|
| `role` | Включить архивную роль при редактировании |
**Ответ:** `total`, `items[]` с `id`, `title`.
### `PUT /api/project-members` — защищённый + Acting
Upsert участника команды: если `(team, emp)` уже есть — обновление, иначе вставка.
**Тело:**
```json
{
"project_id": 86,
"emp_id": 46,
"section_id": 12,
"role": 22,
"active": true,
"text": ""
}
```
| Поле | Обязательно | Описание |
|------|:-----------:|----------|
| `project_id` | да | id проекта |
| `emp_id` | да | id сотрудника |
| `section_id` | да | id из `GET /api/project-sections` |
| `role` | да | id из `GET /api/member-roles` |
| `active` | | По умолчанию `true` |
| `text` | | Комментарий |
Права: порт `Rules::isRwMember` (админ, РП, ГИП, директор отдела).
**Ответ `200`:** `ok: true` и поля как у элемента `GET /api/project-members`.
### `GET /api/time-entries` — защищённый
Сырые строки `tMerakomisTime`.
| Параметр | Описание |
|----------|----------|
| `date_from`, `date_to` | `YYYY-MM-DD` |
| `emp_id`, `project_id` | Фильтры |
| `is_over` | `0` — рабочие, `1` — переработка |
| `limit` | 1…2000 (по умолчанию 500) |
**Поля:** `id`, `emp`, `project`, `date`, `duration`, `is_over`.
### `GET /api/work-report` — защищённый ⭐ основной контракт
Агрегат за период: **сотрудник × проект** с орготделом, разделом, стадией и часами.
**Обязательно:** `date_from`, `date_to` (`YYYY-MM-DD`, включительно).
| Параметр | По умолчанию | Описание |
|----------|--------------|----------|
| `emp_id` | — | Фильтр по сотруднику |
| `project_id` | — | Фильтр по проекту |
| `limit` | `500` | 1…2000 |
| `offset` | `0` | Пагинация |
**Поля элемента `items`:**
| Поле | Тип | Описание |
|------|-----|----------|
| `id` | int | ID сотрудника |
| `employee` | string | ФИО |
| `department` | string \| null | Орготдел (коды через запятую) |
| `staffing_title` | string \| null | Должность (Архитектор, ГИП…) |
| `section` | string \| null | Раздел в проекте |
| `project_code` | string | Код проекта |
| **`step_name`** | string | Стадия (ПД, РД…) |
| **`status`** | int \| null | Код статуса проекта |
| **`status_name`** | string \| null | Подпись статуса |
| **`archive`** | int | `0` / `1` |
| **`archive_date`** | string \| null | Дата архивации |
| `project_name` | string | Название проекта |
| `hours`, `over`, `over1`, `over2`, `total` | number | Часы (см. модель данных) |
**Пример строки:**
```json
{
"id": 46,
"employee": "Агапова Анастасия Михайловна",
"department": "ОВ",
"staffing_title": "Инженер",
"section": "ОВ1",
"project_code": "2025-0016",
"step_name": "РД",
"status": 1,
"status_name": "В работе",
"archive": 0,
"archive_date": null,
"project_name": "ЖК Машкова",
"hours": 8.0,
"over": 0.0,
"over1": 0.0,
"over2": 0.0,
"total": 8.0
}
```
**Пагинация длинного периода:** увеличивайте `offset` шагами до `limit` пока `offset + count < total`. Delta-режима нет — каждый запрос пересчитывает период.
### `GET /api/project-report` — защищённый
Суммарные часы по **проекту** с разбивкой по орготделу и разделу (все сотрудники).
**Обязательно:** `date_from`, `date_to`.
| Параметр | Описание |
|----------|----------|
| `project_id` | Фильтр по проекту |
| `limit`, `offset` | Пагинация (1…2000) |
**Поля `items`:** `id` (project_id), `project_code`, **`step_name`**, **`status`**, **`status_name`**, **`archive`**, **`archive_date`**, `project_name`, `department`, `section`, `hours`, `over`, `over1`, `over2`, `total`.
Одна строка = уникальная тройка **проект + department + section**.
### `GET /api/labor-summary` — защищённый
Низкоуровневый агрегат (те же расчёты, что у `/api/work-report`), с внутренними id Merakomis. Для интеграции предпочтительнее **`/api/work-report`**.
**Поля строки:** `emp_id`, `emp_name`, `department`, `staffing_title`, `project_id`, `project_code`, **`step_name`**, **`status`**, **`status_name`**, **`archive`**, **`archive_date`**, `project_name`, `section_id`, `section_name`, `role`, `hours`, `over`, `over1`, `over2`, `total`.
---
## Табель (read / write)
Полная спецификация write-логики и портирования PHP: `docs/change-proposal-labor-api-write.md`.
### `GET /api/calendar-days` — защищённый
Производственный календарь за период.
| Параметр | Обязательно |
|----------|-------------|
| `date_from`, `date_to` | да |
### `GET /api/absence-types` — защищённый
Справочник типов отсутствий. В `items` всегда есть элемент `id: 0` («снять отсутствие»).
### `GET /api/labor/permissions` — защищённый + Acting
| Параметр | Описание |
|----------|----------|
| `target_emp_id` | За кого проверяем (обязательно) |
| `project_id` | Для проверки записи часов в проект |
**Ответ:** `can_read`, `can_write_time`, `can_write_absence`, **`can_write_member`**, `is_admin`, `is_delegate_writer`.
| Параметр | Описание |
|----------|----------|
| `target_emp_id` | Обязательно |
| `project_id` | Для `can_write_time` и `can_write_member` |
| `member_id` | Опционально, для проверки прав на редактирование записи |
### `GET /api/time-calendar` — защищённый + Acting
Данные календаря табеля сотрудника (формат, близкий к PHP `getTimeTable`).
| Параметр | По умолчанию | Описание |
|----------|--------------|----------|
| `emp_id` | acting | Чей табель |
| `project_id` | `0` | `0` — сводный табель; иначе — по проекту |
### `GET /api/time-summary` — защищённый + Acting
Сводка за текущий месяц и год (блоки `month`, `year` с разбивкой по проектам).
### `PUT /api/time-entries` — защищённый + Acting
Запись или обновление часов за день (аналог PHP `addFromCalendar`).
**Тело (JSON):**
```json
{
"emp_id": 46,
"project_id": 86,
"date": "2026-06-10",
"time": 8,
"over": 0
}
```
| Поле | Описание |
|------|----------|
| `emp_id` | Опционально; по умолчанию = acting |
| `project_id` | Обязательно |
| `date` | `YYYY-MM-DD` |
| `time` | Часы, 0…24 |
| `over` | `0` — рабочие, `1` — переработка |
`time: 0` удаляет запись за этот день/тип.
**Ответ `200`:** `ok`, `id`, `duration`, `info`, `limits` (применённые лимиты дня).
### `PUT /api/absences` — защищённый + Acting
```json
{
"emp_id": 46,
"type": 3,
"dates": ["2026-06-10", "2026-06-11"]
}
```
`type: 0` — снять отсутствие. При установке отсутствия рабочие часы за день удаляются.
### `PUT /api/absences/range` — защищённый + Acting
```json
{
"emp_id": 46,
"absence_id": 3,
"begin": "2026-06-10",
"end": "2026-06-20"
}
```
`absence_id: 0` — очистить диапазон (удалить отсутствия и рабочие часы).
---
## Сценарии интеграции
### A. Периодическая синхронизация сотрудников
1. Храните у себя `last_since_updated` (Unix).
2. Вызывайте `/api/employees/delta?since_updated={last}&limit=500`.
3. Upsert по `id`; обновляйте `last_since_updated = max_updated`.
4. Раз в сутки — полная сверка через `/api/employees` с пагинацией.
### B. Отчёт по трудозатратам за месяц
1. `GET /api/work-report?date_from=2026-05-01&date_to=2026-05-31&fetch_all=true` — один запрос на весь период.
2. Или `POST /api/batch` с несколькими периодами в `requests[]`.
3. Группируйте у себя по `department`, `step_name`, `project_code` при необходимости.
### C. Список проектов сотрудника для UI
`GET /api/project-members?emp_id={id}&active_only=true` — в ответе `project_code`, `step_name`, `status_name`, `archive`, `project_name`.
### D. Запись часов из мобильного/бота
1. Проверить права: `GET /api/labor/permissions?target_emp_id={id}&project_id={pid}`.
2. Записать: `PUT /api/time-entries` с `X-Acting-Emp-Id` и телом.
3. Обработать `403` (нет прав), `400` (невалидная дата).
### E. Сводка по проектам для руководства
`GET /api/project-report?date_from=…&date_to=…` — агрегат по проект + отдел + раздел.
### F. Управление составом проекта из внешнего приложения
1. `GET /api/project-sections?project_id={pid}` — допустимые разделы.
2. `GET /api/member-roles` — роли.
3. `GET /api/labor/permissions?target_emp_id={acting}&project_id={pid}``can_write_member`.
4. `PUT /api/project-members` с `X-Acting-Emp-Id` и телом.
5. Проверка: `GET /api/project-members?project_id={pid}`.
---
## Коды ошибок
| Код | Когда |
|-----|--------|
| `400` | Невалидные параметры, `date_from > date_to`, нет колонки для delta, нет `X-Acting-Emp-Id` |
| `401` | Неверный или отсутствующий API-ключ |
| `403` | Нет прав на табель / запись |
| `404` | Сотрудник или проект не найден |
| `500` | Ошибка MySQL, не найдена таблица Merakomis |
Write-эндпоинты часто возвращают структурированный `detail`:
```json
{ "code": "forbidden", "message": "Нет прав на запись часов" }
```
---
## Примеры запросов
Локальный ключ из `docker-compose.yml`: **`local-dev-key-change-in-prod`**.
Замените `HOST` на IP/имя хоста.
```bash
# Health (без ключа)
curl -s http://HOST:8090/api/health
# Мета сотрудников
curl -s -H "X-Api-Key: local-dev-key-change-in-prod" \
http://HOST:8090/api/meta
# Сводка часов (основной контракт интеграции)
curl -s -H "X-Api-Key: local-dev-key-change-in-prod" \
"http://HOST:8090/api/work-report?date_from=2026-05-01&date_to=2026-05-31&limit=2000"
# Участники проекта со стадией
curl -s -H "X-Api-Key: local-dev-key-change-in-prod" \
"http://HOST:8090/api/project-members?emp_id=46&active_only=true"
# Состав проекта: запись
curl -s -X PUT "http://HOST:8090/api/project-members" \
-H "X-Api-Key: local-dev-key-change-in-prod" \
-H "X-Acting-Emp-Id: 1" \
-H "Content-Type: application/json" \
-d '{"project_id":86,"emp_id":46,"section_id":12,"role":22,"active":true}'
# Инкремент сотрудников
curl -s -H "X-Api-Key: local-dev-key-change-in-prod" \
"http://HOST:8090/api/employees/delta?since_updated=0&limit=500"
# Запись часов
curl -s -X PUT "http://HOST:8090/api/time-entries" \
-H "X-Api-Key: local-dev-key-change-in-prod" \
-H "X-Acting-Emp-Id: 46" \
-H "Content-Type: application/json" \
-d '{"project_id":86,"date":"2026-06-10","time":8,"over":0}'
```
### Пример на Python (httpx)
```python
import httpx
BASE = "http://user-reader:8090"
HEADERS = {"X-Api-Key": "local-dev-key-change-in-prod"}
def fetch_work_report(date_from: str, date_to: str) -> list[dict]:
items: list[dict] = []
offset = 0
limit = 2000
while True:
r = httpx.get(
f"{BASE}/api/work-report",
headers=HEADERS,
params={
"date_from": date_from,
"date_to": date_to,
"limit": limit,
"offset": offset,
},
timeout=60.0,
)
r.raise_for_status()
data = r.json()
items.extend(data["items"])
offset += data["count"]
if offset >= data["total"]:
break
return items
```
---
## Запуск и сборка
Зависимости: `services/user-reader/requirements.txt`.
Точка входа: **`app.main:app`** (uvicorn).
Исходники схем:
| Модуль | Назначение |
|--------|------------|
| `app/emp_schema.py` | Сотрудники |
| `app/merakomis_schema.py` | Проекты, время, стадии |
| `app/labor.py` | Read API трудозатрат |
| `app/labor_write.py` | Write API табеля |
| `app/project_members_read.py` | Read: project-sections, member-roles |
| `app/project_members_write.py` | Write: PUT project-members |
| `app/labor_calendar.py` | Read API табеля |
| `app/labor_permissions.py` | Права |
### Сборка образа и ошибка `failed to solve`
При `docker compose build user-reader` сообщение **`failed to solve: python:...`** — Docker не смог скачать базовый образ с Hub.
Базовый образ: **`python:3.12-slim-bookworm`**.
**Вариант A** — есть интернет:
```bash
docker pull python:3.12-slim-bookworm
docker compose build user-reader
docker compose up -d --no-deps user-reader
```
**Вариант B** — сервер без Hub:
```powershell
.\services\user-reader\scripts\export-image.ps1
# скопировать user-reader-image.tar на сервер
.\services\user-reader\scripts\import-image.ps1
```
**Вариант C** — зеркало registry в `daemon.json`.
---
## Связанные документы
| Документ | Содержание |
|----------|------------|
| [docs/user-reader-api.md](docs/user-reader-api.md) | **API, доступ, эндпоинты, сценарии вызова** |
| `docs/change-proposal-labor-api-write.md` | Детальная спецификация write API и портирования PHP |
| `docs/integration-external-app-employees.md` | Legacy PHP API (cookie); для новых интеграций используйте user-reader |

18
Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM php:7.3-apache
# Основные модули PHP.
RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
RUN sed -i 's/;date.timezone =/date.timezone = Europe\/Moscow/' /usr/local/etc/php/php.ini
RUN sed -i 's/short_open_tag = Off/short_open_tag = On/' /usr/local/etc/php/php.ini
RUN sed -i 's/upload_max_filesize = 2M/upload_max_filesize = 4000M/' /usr/local/etc/php/php.ini
RUN sed -i 's/post_max_size = 8M/post_max_size = 4000M/' /usr/local/etc/php/php.ini
RUN apt update && apt install -y libicu-dev zlib1g-dev libpng-dev libzip-dev curl libcurl4-openssl-dev libjpeg-dev libfreetype6-dev
RUN docker-php-ext-configure gd --with-jpeg-dir=/usr --with-freetype-dir=/usr && docker-php-ext-install gd
RUN docker-php-ext-install curl
RUN docker-php-ext-install mysqli
# Microsoft SQL Server (sqlsrv / pdo_sqlsrv) убран из образа: на базе php:7.3 + текущий Debian
# PECL часто собирает только pdo_sqlsrv, без sqlsrv — ломает docker-php-ext-enable.
# Основной проект использует MySQL (mysqli). Тесты sqlsrv — themes/merakomis/test/controller.php.
RUN a2enmod rewrite

View File

@ -0,0 +1,252 @@
import os
import re
import traceback
from datetime import date, datetime
from decimal import Decimal
from typing import Any
import pymysql
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pymysql.cursors import DictCursor
MYSQL = {
"host": os.environ.get("MYSQL_HOST", "db"),
"port": int(os.environ.get("MYSQL_PORT", "3306")),
"user": os.environ.get("MYSQL_USER", "root"),
"password": os.environ.get("MYSQL_PASSWORD", ""),
"database": os.environ.get("MYSQL_DATABASE", "j7508239_tracker"),
"charset": "utf8mb4",
"cursorclass": DictCursor,
}
app = FastAPI(title="Mera user reader", version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
def get_conn():
return pymysql.connect(**MYSQL)
def _json_cell(v: Any) -> Any:
if v is None:
return None
if isinstance(v, datetime):
return v.isoformat(sep=" ", timespec="seconds")
if isinstance(v, date):
return v.isoformat()
if isinstance(v, Decimal):
return float(v)
if isinstance(v, bytes):
return v.decode("utf-8", errors="replace")
return v
def _is_secret_col(name: str) -> bool:
n = name.lower()
return n.endswith("_pass") or "password" in n or "secret" in n
def _strip_sensitive(row: dict) -> dict:
return {k: v for k, v in row.items() if not _is_secret_col(k)}
def json_rows(rows: list[dict]) -> list[dict]:
return [
{k: _json_cell(v) for k, v in _strip_sensitive(row).items()} for row in rows
]
def _db_name(cur) -> str:
cur.execute("SELECT DATABASE() AS d")
return cur.fetchone()["d"]
def _list_tables(cur, db: str) -> list[str]:
cur.execute(
"""
SELECT TABLE_NAME AS n FROM information_schema.TABLES
WHERE TABLE_SCHEMA = %s ORDER BY TABLE_NAME
""",
(db,),
)
return [r["n"] for r in cur.fetchall()]
def _quote_ident(name: str) -> str:
return "`" + name.replace("`", "``") + "`"
def resolve_emp_table(cur, db: str) -> str:
"""Имя таблицы сотрудников Merakomis (или из EMP_TABLE)."""
env = os.environ.get("EMP_TABLE", "").strip().strip("`")
if env:
cur.execute(
"""
SELECT COUNT(*) AS c FROM information_schema.TABLES
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
""",
(db, env),
)
if cur.fetchone()["c"]:
return env
raise RuntimeError(
f"EMP_TABLE={env!r} не найдена в БД {db!r}. "
f"Есть таблицы: {', '.join(_list_tables(cur, db)[:50])}"
)
cur.execute(
"""
SELECT TABLE_NAME AS n
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = %s
AND (
LOWER(TABLE_NAME) = 'tmerakomisemp'
OR (
LOWER(TABLE_NAME) REGEXP 'merakomis.*emp'
AND LOWER(TABLE_NAME) NOT LIKE '%%children%%'
AND LOWER(TABLE_NAME) NOT LIKE '%%group%%'
)
)
ORDER BY
CASE WHEN LOWER(TABLE_NAME) = 'tmerakomisemp' THEN 0 ELSE 1 END,
CHAR_LENGTH(TABLE_NAME)
LIMIT 1
""",
(db,),
)
row = cur.fetchone()
if row:
return row["n"]
all_t = _list_tables(cur, db)
hint = ", ".join([t for t in all_t if re.search(r"emp|user|staff|profile", t, re.I)][:30])
raise RuntimeError(
"Таблица сотрудников Merakomis не найдена (ожидали что-то вроде tmerakomisemp). "
"Импортируйте дамп с этой таблицей или задайте переменную окружения EMP_TABLE. "
f"База: {db!r}, таблиц: {len(all_t)}. Похожие по имени: {hint or ''}"
)
def _table_columns(cur, db: str, table: str) -> list[str]:
cur.execute(
"""
SELECT COLUMN_NAME AS c
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
ORDER BY ORDINAL_POSITION
""",
(db, table),
)
return [r["c"] for r in cur.fetchall()]
def _pick_order_column(cols: list[str]) -> str:
for c in cols:
if "name" in c.lower() and "emp" in c.lower():
return c
for c in cols:
if c.lower().endswith("_name"):
return c
return cols[0]
def _pick_removed_column(cols: list[str]) -> str | None:
for c in cols:
if c.lower().endswith("_removed"):
return c
return None
@app.get("/")
def index():
return FileResponse(
os.path.join(os.path.dirname(__file__), "..", "static", "index.html")
)
@app.get("/api/health")
def health() -> dict[str, Any]:
try:
with get_conn() as c:
with c.cursor() as cur:
cur.execute("SELECT 1 AS ok")
row = cur.fetchone()
db = _db_name(cur)
return {"ok": True, "db": bool(row and row.get("ok") == 1), "database": db}
except pymysql.Error as e:
return {"ok": False, "db": False, "error": str(e)}
@app.get("/api/meta")
def meta() -> dict[str, Any]:
"""Какую таблицу нашли и какие колонки (без запуска полного списка людей)."""
try:
with get_conn() as c:
with c.cursor() as cur:
db = _db_name(cur)
table = resolve_emp_table(cur, db)
cols = _table_columns(cur, db, table)
return {
"database": db,
"table": table,
"columns": cols,
"removed_column": _pick_removed_column(cols),
"order_column": _pick_order_column(cols),
}
except pymysql.Error as e:
raise HTTPException(status_code=500, detail=str(e)) from e_
@app.get("/api/employees")
def employees(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
) -> dict[str, Any]:
try:
with get_conn() as c:
with c.cursor() as cur:
db = _db_name(cur)
table = resolve_emp_table(cur, db)
cols = _table_columns(cur, db, table)
if not cols:
raise RuntimeError(f"Таблица {_quote_ident(table)} без колонок")
removed = _pick_removed_column(cols)
order_col = _pick_order_column(cols)
tq = _quote_ident(table)
oq = _quote_ident(order_col)
where_sql = ""
if removed:
where_sql = f" WHERE {_quote_ident(removed)} = 0 "
sql = f"SELECT * FROM {tq} {where_sql} ORDER BY {oq} LIMIT %s OFFSET %s"
cur.execute(sql, (limit, offset))
rows = cur.fetchall()
cnt_sql = f"SELECT COUNT(*) AS n FROM {tq} {where_sql}"
cur.execute(cnt_sql)
total = cur.fetchone()["n"]
return {
"table": table,
"total": int(total),
"limit": limit,
"offset": offset,
"items": json_rows(rows),
}
except pymysql.Error as e:
raise HTTPException(status_code=500, detail=str(e)) from e
except Exception as e:
dbg = os.environ.get("DEBUG", "")
msg = str(e)
if dbg == "1":
msg = f"{msg}\n{traceback.format_exc()}"
raise HTTPException(status_code=500, detail=msg) from e_

View File

@ -0,0 +1,851 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Портфель проектов v22</title>
<style>
:root {
--bg: #0c1117;
--surface: #131a22;
--surface-2: #18212b;
--surface-3: #1e2935;
--border: rgba(120,160,200,0.16);
--text: #e8eef5;
--muted: #93a3b5;
--faint: #647386;
--accent: #5aa6e0;
--accent-2: #6fd1c0;
--ok: #5cc0a0;
--warn: #e0b15a;
--bad: #e07a6f;
--bar-track: rgba(255,255,255,0.07);
--shadow: 0 16px 44px rgba(0,0,0,0.40);
--radius: 14px;
/* Единый нейтральный серый для всех шкал */
--bar-neutral: rgba(100,130,160,0.55);
--bar-neutral-s: rgba(100,130,160,0.65); /* сводная */
--c1: 260px; --c2: 280px;
--grid-cols: var(--c1) 100px var(--c2) 1.1fr 2.2fr 2.2fr;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: Inter, Arial, sans-serif;
background:
radial-gradient(circle at top left, rgba(90,166,224,0.08), transparent 32%),
radial-gradient(circle at top right, rgba(111,209,192,0.07), transparent 30%),
linear-gradient(180deg, #0e141b 0%, #0a0f14 100%);
color: var(--text);
}
html { height: 100%; scroll-padding-top: 70px; }
body { min-height: 100%; }
.page { max-width: 1800px; margin: 0 auto; padding: 24px 22px 40px; }
/* sticky-header удалён */
/* ─── КОМАНДА ─── */
.team-cell { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:6px 6px; }
.team-roles { flex:0 0 auto; display:flex; flex-direction:column; gap:1px; align-self:center; }
.team-role-row { display:flex; align-items:center; gap:4px; }
.team-role-name { color:var(--faint); font-size:10px; min-width:34px; }
.team-role-count { font-weight:700; color:var(--text); font-size:11px; min-width:16px; text-align:right; }
.team-donut-wrap { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px; }
.team-total { font-size:11px; font-weight:700; color:var(--muted); white-space:nowrap; }
/* ─── ТОПБАР ─── */
.topbar { margin-bottom: 7px; }
.eyebrow { color: var(--accent); text-transform: uppercase; letter-spacing: .14em; font-size: 12px; margin-bottom: 6px; }
h1 { font-size: 30px; font-weight: 700; }
.date-tag {
display: inline-flex; align-items: center; gap: 6px;
background: rgba(90,166,224,0.12); border: 1px solid rgba(90,166,224,0.28);
border-radius: 8px; padding: 4px 12px;
font-size: 12px; color: var(--accent); font-weight: 600;
margin-top: 8px;
}
.date-tag .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 5px var(--ok); }
/* ─── СВОДНАЯ СТРОКА (7 колонок, над доской) ─── */
.portfolio-summary {
display: grid;
grid-template-columns: var(--grid-cols);
gap: 10px;
margin-bottom: 12px;
}
.ps-card {
background: rgba(90,166,224,0.06);
border: 1px solid rgba(90,166,224,0.15);
border-radius: 10px;
padding: 10px 14px;
display: flex; flex-direction: column; justify-content: center;
}
.ps-card .ps-title { font-size: 9px; text-transform: uppercase; letter-spacing: .1em; color: var(--faint); margin-bottom: 3px; }
.ps-card .ps-val { font-size: 15px; font-weight: 700; color: var(--accent); }
.ps-card .ps-sub { font-size: 11px; color: var(--muted); margin-top: 2px; }
.ps-metric {
background: rgba(255,255,255,0.022);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 14px;
display: flex; flex-direction: column; justify-content: center; gap: 5px;
}
.ps-metric .ps-title { font-size: 9px; text-transform: uppercase; letter-spacing: .08em; color: var(--faint); }
.ps-avg { display: flex; align-items: center; gap: 8px; }
.ps-avg .avg-n { font-size: 18px; font-weight: 700; min-width: 44px; color: var(--text); }
.ps-avg .avg-bar { flex: 1; height: 5px; border-radius: 3px; background: var(--bar-track); overflow: hidden; }
.ps-avg .avg-fill { height: 100%; border-radius: inherit; }
/* Итоговые заголовки: серый или красный если >100% */
.avg-neutral { background: var(--bar-neutral); }
.avg-overflow { background: rgba(224,122,111,0.85); }
/* ─── ДОСКА ─── */
.board { display: flex; flex-direction: column; gap: 0; overflow: visible; }
.board-head {
display: grid; grid-template-columns: var(--grid-cols); gap: 10px;
margin-bottom: 0;
position: sticky;
top: 0;
z-index: 100;
background: #0d131a;
padding: 10px 0 8px;
box-shadow: 0 6px 24px rgba(0,0,0,0.6);
}
.col-head {
background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 13px 12px;
display: flex; align-items: center; justify-content: center; gap: 6px;
font-weight: 700; font-size: 14px;
box-shadow: var(--shadow);
}
.col-sort {
width: 18px; height: 18px; border-radius: 5px;
background: rgba(255,255,255,0.05); border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
font-size: 9px; color: var(--faint); cursor: pointer; flex-shrink: 0;
}
.col-sort:hover { border-color: rgba(90,166,224,0.4); color: var(--accent); }
.board-body { display: flex; flex-direction: column; gap: 4px; }
/* ─── СТРОКА ─── */
.board-row {
display: grid; grid-template-columns: var(--grid-cols); gap: 6px;
}
.board-row .proj-cell { border-left: 3px solid transparent; }
/* Левая полоска — по стадии проекта */
.board-row.stage-kk .proj-cell { border-left-color: #E0B15A; }
.board-row.stage-pd .proj-cell { border-left-color: #5CC0A0; }
.board-row.stage-rd .proj-cell { border-left-color: #5AA6E0; }
/* health-dot остаётся отдельной логикой */
/* ─── ЯЧЕЙКИ ─── */
.cell {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 8px 10px; min-height: 0;
display: flex; flex-direction: column; justify-content: center;
}
.cell.overflow-cell {
background: rgba(224,122,111,0.07);
border-color: rgba(224,122,111,0.30);
}
/* ─── КАРТОЧКА ПРОЕКТА ─── */
.proj-cell { align-items: stretch; justify-content: flex-start; }
.proj-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
.proj-code { font-size: 10px; color: var(--muted); margin-bottom: 1px; }
.proj-code b { color: var(--text); }
.proj-name { font-size: 13px; font-weight: 700; line-height: 1.2; }
.proj-dev { font-size: 10px; color: var(--faint); margin-top: 1px; }
.proj-gip { font-size: 10px; color: var(--muted); margin-top: 2px; }
.badge { flex: none; font-size: 11px; font-weight: 700; border-radius: 8px; padding: 4px 9px; }
/* Стадии: КК=золотой, ПД=зелёный, РД=синий */
.badge-kk { background: rgba(224,177,90,0.15); border: 2px solid rgba(224,177,90,0.55); color: #E0B15A; font-weight: 800; }
.badge-pd { background: rgba(92,192,160,0.15); border: 2px solid rgba(92,192,160,0.55); color: #5CC0A0; font-weight: 800; }
.badge-rd { background: rgba(90,166,224,0.15); border: 2px solid rgba(90,166,224,0.55); color: #5AA6E0; font-weight: 800; }
.proj-health { display: flex; align-items: center; gap: 6px; margin-top: 4px; }
.health-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.health-dot.ok { background: var(--ok); box-shadow: 0 0 6px var(--ok); }
.health-dot.warn { background: var(--warn); box-shadow: 0 0 6px var(--warn); }
.health-dot.bad { background: var(--bad); box-shadow: 0 0 6px var(--bad); }
.health-label { font-size: 11px; color: var(--muted); }
.proj-link { font-size: 10px; color: var(--accent); margin-top: 3px; cursor: pointer; opacity: 0.75; text-decoration: none; display: inline-flex; align-items: center; gap: 3px; }
.proj-link:hover { opacity: 1; }
/* ─── СВОДНАЯ КОЛОНКА (mini-bars) ─── */
.summary-list { display: flex; flex-direction: column; gap: 3px; }
.srow { display: flex; align-items: center; gap: 6px; }
.srow .slabel { width: 58px; flex-shrink: 0; color: var(--faint); font-size: 10px; text-transform: lowercase; }
.srow .sbar-wrap {
flex: 1; height: 9px; border-radius: 4px;
background: var(--bar-track); border: 1px solid var(--border);
overflow: hidden; position: relative;
}
.srow .sbar-fill { height: 100%; border-radius: inherit; }
/* Единый нейтральный серый для всех мини-баров */
.sbar-neutral { background: var(--bar-neutral-s); }
/* Перерасход в сводной */
.sbar-over { background: rgba(224,122,111,0.85); width: 100% !important; }
.sbar-wrap.over-wrap { border-color: rgba(224,122,111,0.3); background: rgba(224,122,111,0.08); }
.srow .spct { width: 36px; text-align: right; font-size: 11px; font-weight: 700; flex-shrink: 0; color: var(--muted); }
.spct-over { color: var(--bad) !important; }
.ok { color: var(--ok); }
.warn { color: var(--warn); }
.bad { color: var(--bad); }
/* ─── МЕТРИКА ─── */
.metric-top {
display: flex; justify-content: space-between; align-items: stretch;
flex: 1; margin-bottom: 4px;
}
.metric-pair { font-size: 11px; color: var(--muted); line-height: 1.7; display: flex; flex-direction: column; justify-content: space-evenly; flex: 1; }
.mp-label { font-size: 9px; text-transform: uppercase; letter-spacing: .07em; color: var(--faint); margin-bottom: 1px; }
.mp-plan { color: var(--faint); margin-bottom: 0px; }
.mp-fact { color: var(--faint); font-weight: 400; }
.metric-kpi {
text-align: right;
border-left: 1px solid var(--border);
padding-left: 12px;
display: flex; flex-direction: column; align-items: flex-end; justify-content: space-evenly;
}
.kpi-label { font-size: 9px; text-transform: uppercase; letter-spacing: .07em; color: var(--faint); margin-bottom: 0px; }
.kpi-n { font-size: 14px; font-weight: 800; line-height: 1; color: var(--faint); }
.kpi-n.bad { color: var(--bad); }
.kpi-u { font-size: 10px; color: var(--faint); margin-top: 0px; }
.kpi-delta { font-size: 10px; font-weight: 700; color: var(--bad); margin-top: 0px; }
/* ─── БАР — единый серый, читаемый % ─── */
.bar-row {
display: flex; align-items: center; gap: 6px;
}
.bar {
height: 9px;
border-radius: 4px;
background: var(--bar-track);
border: 1px solid var(--border);
overflow: hidden;
position: relative;
flex: 1;
}
.bar .fill {
height: 100%;
border-radius: inherit;
background: var(--bar-neutral);
}
.bar .pct { display: none; }
.bar-pct-over { color: var(--bad) !important; font-weight: 800; }
.bar-pct-skew { color: #FFA020 !important; font-weight: 800; }
.bar-pct {
font-size: 11px; font-weight: 700;
color: var(--muted);
white-space: nowrap;
width: 36px;
text-align: right;
flex-shrink: 0;
}
/* Бар при перерасходе >100% — красный + пульсация */
.bar.bar-overflow {
border-color: rgba(224,122,111,0.55);
background: rgba(224,122,111,0.85);
}
.bar.bar-overflow .fill {
width: 100% !important;
background: rgba(224,122,111,0.85);
border-radius: inherit;
position: relative;
}
.bar.bar-overflow .fill::after {
content: '';
position: absolute; right: 0; top: 0; bottom: 0; width: 4px;
background: rgba(255,80,60,0.9);
animation: pulse-edge 1.4s ease-in-out infinite;
}
@keyframes pulse-edge {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.bar.bar-overflow .pct {
color: #fff;
font-weight: 800;
text-shadow: 0 1px 4px rgba(0,0,0,0.9);
}
/* ─── ПЕРЕКОС >20% — оранжевый нижний бордер ─── */
.cell.skew-cell {
background: rgba(255,140,0,0.07);
border-color: rgba(255,140,0,0.30);
}
/* СРЫВ перекрывает — overflow-cell не получает skew стиль */
.cell.overflow-cell {
border-bottom-color: rgba(224,122,111,0.30); /* красный, не оранжевый */
}
/* Оранжевый мини-бар при расхождении >20% */
.sbar-skew { background: rgba(255,150,30,0.85) !important; }
.spct-skew { color: #FFA020 !important; font-weight: 800; }
/* overflow-cell.skew-cell — красный фон приоритетнее */
.cell.overflow-cell.skew-cell {
background: rgba(224,122,111,0.07);
border-color: rgba(224,122,111,0.30);
}
/* Ячейки метрик (Готовность, Трудозатраты, Сроки, Поступления, Затраты) — без скруглений */
.board-row .cell:nth-child(n+4) {
border-radius: 4px;
}
/* Бары внутри этих ячеек тоже без скруглений */
.board-row .cell:nth-child(n+4) .bar {
border-radius: 4px;
}
.board-row .cell:nth-child(n+4) .bar .fill {
border-radius: 2px;
}
/* ─── ПАРНАЯ ЯЧЕЙКА (Готовн.+Трудозат., Поступл.+Затраты) ─── */
.pair-cell { display: flex; flex-direction: row; flex-wrap: nowrap; gap: 10px; padding: 8px 10px; }
.pair-half { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; justify-content: space-between; }
.pair-half + .pair-half { border-left: 1px solid var(--border); padding-left: 10px; }
.pair-half .metric-top { margin-bottom: 4px; }
/* override v21 rule that gave nth-child >=4 ячейкам square corners is fine, no override needed */
/* ─── ЯЧЕЙКА СРОКОВ (начало/окончание + осталось) ─── */
/* выравнивание по вертикали повторяет структуру .metric-top в соседних ячейках:
верхние .mp-label / .kpi-label стоят на одной линии; первая .dr-line = первая строка .metric-pair */
/* Повторяем поведение .metric-top + .metric-pair (space-evenly, line-height: 1.7),
чтобы верхние метки и строки внутри стояли на тех же высотах, что в соседних ячейках */
/* dates-cell: повторяет геометрию .pair-half — metric-top (gap-row-1) + bar-row снизу.
Никакого space-evenly на pair-ах, чтобы строки лежали ровно как у соседей. */
.dates-cell { display: flex; flex-direction: column; padding: 8px 10px; }
.dates-top { display: flex; gap: 10px; align-items: stretch; margin-bottom: 4px; flex: 1; }
.dates-range { display: flex; flex-direction: column; flex: 1.6; }
.dates-kpi { display: flex; flex-direction: column; flex: 1; border-left: 1px solid var(--border); padding-left: 12px; text-align: right; align-items: flex-end; justify-content: space-evenly; }
.dates-kpi .kpi-label { font-size: 9px; text-transform: uppercase; letter-spacing: .07em; color: var(--faint); margin-bottom: 0; }
.dates-range .mp-label { font-size: 9px; visibility: hidden; margin-bottom: 0; }
.dates-range .dates-pair { font-size: 11px; color: var(--muted); line-height: 1.7; display: flex; flex-direction: column; justify-content: space-evenly; flex: 1; }
.dates-range .dr-line { color: var(--faint); white-space: nowrap; }
.dates-range .dr-line.bad { color: var(--bad); font-weight: 600; }
.dates-kpi .kpi-n { font-size: 14px; font-weight: 800; line-height: 1; color: var(--faint); }
.dates-kpi .kpi-n.bad { color: var(--bad); }
.dates-kpi .kpi-u { font-size: 10px; color: var(--faint); line-height: 1.2; }
.dates-kpi .kpi-delta { font-size: 10px; line-height: 1.2; }
/* ─── ВЫРАВНИВАНИЕ ГРИДОВ ─── */
.board-head { gap: 6px !important; }
/* ─── РАЗМЕР ШРИФТА ШАПКИ: +30% (14px → 18px) ─── */
.col-head { font-size: 18px !important; }
/* ─── ПАРНАЯ ШАПКА ─── */
/* ПАРНАЯ ШАПКА: «Проектирование»/«Деньги» без линий, линии-брекеты под заголовком */
.col-head.col-head-pair { flex-direction: column; justify-content: center; align-items: stretch; gap: 0; padding: 4px 14px 6px; }
.col-head-roof { display: flex; align-items: center; justify-content: center; width: 100%; line-height: 1.1; }
.col-head-roof-text { white-space: nowrap; font-size: 16px; font-weight: 700; color: #fff; padding: 0 4px; }
/* под заголовком — просто прямая жёлтая линия */
.col-head-bracket { height: 1px; margin: 4px 0 4px 25%; width: 50%; background: rgba(255,255,255,0.25); }
.col-head.col-head-pair .col-head-pair-row { display: flex; width: 100%; gap: 0; align-items: center; justify-content: space-around; }
.col-head.col-head-pair .col-head-half { flex: 1; text-align: center; font-size: 16px !important; font-weight: 700; opacity: 0.85; text-transform: none; letter-spacing: 0; line-height: 1.1; }
.col-head.col-head-pair .col-head-half + .col-head-half { border-left: none; }
/* ─── TOPBAR: h1 и date-tag в одной строке ─── */
.topbar-head { display: flex; align-items: center; gap: 16px; }
.topbar-head h1 { margin: 0; }
.topbar-head .date-tag { margin: 0; }
/* ─── КНОПКИ-РАЗДЕЛЫ (над шапкой) ─── */
.sections-tabs { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.sec-tab { padding: 5px 12px; border: 1px solid var(--border); border-radius: 4px;
font-size: 11px; font-weight: 600; letter-spacing: .04em; color: var(--muted);
background: var(--surface, #11161d); cursor: default; user-select: none; }
.sec-tab.active { background: rgba(90, 166, 224, 0.18); border-color: rgba(90,166,224,.55); color: #cfe5fa; }
/* ─── СТРОКА ФИЛЬТРОВ ─── */
.board-filters { display: grid; grid-template-columns: var(--grid-cols); gap: var(--gap, 6px); margin-top: 4px; }
.filter-cell { background: var(--surface, #0e131a); border: 1px solid var(--border); border-radius: 8px;
display: flex; align-items: center; justify-content: center; padding: 3px 6px; min-height: 21px; }
.filter-cell.filter-pair { justify-content: space-around; }
.filter-cell.filter-pair .filter-icon + .filter-icon { border-left: 1px solid var(--border); padding-left: 0; margin-left: 0; }
.filter-cell.filter-pair .filter-icon { flex: 1; display: flex; align-items: center; justify-content: center; }
.filter-icon { color: var(--faint); display: flex; align-items: center; justify-content: center; }
.filter-icon:hover { color: var(--muted); }
/* ─── STICKY ШАПКА + ФИЛЬТРЫ, SCROLL ТОЛЬКО СПИСКА ─── */
.board { display: flex; flex-direction: column; min-height: 0; }
.board-head { position: sticky; top: 0; z-index: 3; background: var(--bg, #0a0d12); }
.board-filters { position: sticky; top: 56px; z-index: 2; background: var(--bg, #0a0d12); padding-bottom: 6px; }
.board-body { overflow-y: auto; max-height: calc(100vh - 240px); scrollbar-gutter: stable; }
.board-body::-webkit-scrollbar { width: 8px; }
.board-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,.08); border-radius: 3px; }
.board-body::-webkit-scrollbar-track { background: transparent; }
/* head/filters резервируют такой же отступ справа, как в body */
.board-head, .board-filters { padding-right: 8px; box-sizing: border-box; }
/* ─── СТАТУСЫ ПРОЕКТОВ (СТОП/ТЕК/НОВ) ─── */
/* все бейджи (стадия + статус) в карточке — фиксированной ширины по самому широкому (СТОП) */
.badge-stack { display: inline-flex; flex-direction: column; align-items: stretch; gap: 4px; width: 44px; opacity: 0.55; }
.badge-stack .badge { width: 100%; box-sizing: border-box; text-align: center; font-size: 8px; padding: 3px 6px; border-radius: 6px; border-width: 1.5px; line-height: 1; }
.badge-status { display: block; width: 100%; box-sizing: border-box; font-size: 8px; font-weight: 700; padding: 3px 6px; border-radius: 6px; text-align: center; line-height: 1; }
.status-stop { background: rgba(232,90,90,0.15); border: 1.5px solid rgba(232,90,90,0.55); color: #e85a5a; }
.status-new { background: rgba(92,192,160,0.15); border: 1.5px solid rgba(92,192,160,0.55); color: #5CC0A0; }
.status-cur { background: rgba(160,180,200,0.12); border: 1.5px solid rgba(160,180,200,0.45); color: #a0b4c8; }
</style>
</head>
<body>
<main class="page">
<!-- ТОПБАР -->
<div class="topbar">
<div class="topbar-head"><h1>Портфель проектов</h1><div class="date-tag"><span class="dot"></span> Данные на: 08.06.2026</div><div class="sections-tabs"><div class="sec-tab active">ВСЕ РАЗДЕЛЫ</div><div class="sec-tab">ГП</div><div class="sec-tab">АР</div><div class="sec-tab">КР</div><div class="sec-tab">ОВиК</div><div class="sec-tab">ВК</div><div class="sec-tab">ЭОМ</div><div class="sec-tab">ССиАК</div></div></div>
</div>
<div class="board">
<div class="board-head"><div class="col-head">Проекты</div><div class="col-head">Команда</div><div class="col-head">Сводная</div><div class="col-head">Сроки</div><div class="col-head col-head-pair"><div class="col-head-roof"><span class="col-head-roof-text">Проектирование</span></div><div class="col-head-bracket"></div><div class="col-head-pair-row"><div class="col-head-half">готовность</div><div class="col-head-half">трудозатраты</div></div></div><div class="col-head col-head-pair"><div class="col-head-roof"><span class="col-head-roof-text">Деньги</span></div><div class="col-head-bracket"></div><div class="col-head-pair-row"><div class="col-head-half">поступления</div><div class="col-head-half">затраты</div></div></div></div><div class="board-filters"><div class="filter-cell"><div class="filter-icon"><svg fill="none" height="13" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.6" viewbox="0 0 16 16" width="13"><path d="M2 3h12l-4.5 5.5V13l-3 1.5V8.5L2 3z"></path></svg></div></div><div class="filter-cell"><div class="filter-icon"><svg fill="none" height="13" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.6" viewbox="0 0 16 16" width="13"><path d="M2 3h12l-4.5 5.5V13l-3 1.5V8.5L2 3z"></path></svg></div></div><div class="filter-cell"><div class="filter-icon"><svg fill="none" height="13" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.6" viewbox="0 0 16 16" width="13"><path d="M2 3h12l-4.5 5.5V13l-3 1.5V8.5L2 3z"></path></svg></div></div><div class="filter-cell"><div class="filter-icon"><svg fill="none" height="13" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.6" viewbox="0 0 16 16" width="13"><path d="M2 3h12l-4.5 5.5V13l-3 1.5V8.5L2 3z"></path></svg></div></div><div class="filter-cell filter-pair"><div class="filter-icon"><svg fill="none" height="13" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.6" viewbox="0 0 16 16" width="13"><path d="M2 3h12l-4.5 5.5V13l-3 1.5V8.5L2 3z"></path></svg></div><div class="filter-icon"><svg fill="none" height="13" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.6" viewbox="0 0 16 16" width="13"><path d="M2 3h12l-4.5 5.5V13l-3 1.5V8.5L2 3z"></path></svg></div></div><div class="filter-cell filter-pair"><div class="filter-icon"><svg fill="none" height="13" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.6" viewbox="0 0 16 16" width="13"><path d="M2 3h12l-4.5 5.5V13l-3 1.5V8.5L2 3z"></path></svg></div><div class="filter-icon"><svg fill="none" height="13" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.6" viewbox="0 0 16 16" width="13"><path d="M2 3h12l-4.5 5.5V13l-3 1.5V8.5L2 3z"></path></svg></div></div></div>
<div class="board-body">
<!-- СТРОКА 1: ЖК Северный Парк — СРЫВ Трудозатраты + Сроки -->
<div class="board-row health-bad stage-pd">
<div class="cell proj-cell">
<div class="proj-head">
<div>
<div class="proj-code">ШИФР <b>2025-0925</b></div>
<div class="proj-name">ЖК Северный Парк</div>
<div class="proj-dev">СЗ Альфа Девелопмент</div>
<div class="proj-gip">ГИП: А. Иванов · 74,0 млн ₽</div>
</div>
<div class="badge-stack"><span class="badge badge-pd">ПД</span><span class="badge-status status-stop">СТОП</span></div>
</div>
<div class="proj-health">
<span class="health-dot bad"></span>
<span class="health-label">В работе · Перерасход</span>
</div>
</div><div class="cell team-cell"><div class="team-donut-wrap"><svg height="66" viewbox="0 0 66 66" width="66"><circle cx="33" cy="33" fill="none" r="26" stroke="rgba(120,160,200,0.12)" stroke-width="5"></circle><circle cx="33" cy="33" fill="none" r="26" stroke="#5aa6e0" stroke-dasharray="24.5 138.9" stroke-dashoffset="40.8" stroke-linecap="round" stroke-width="5"></circle><text fill="#5aa6e0" font-family="Inter,Arial,sans-serif" font-size="15" font-weight="700" text-anchor="middle" x="33" y="38">15%</text></svg><div class="team-total">34 чел.</div></div></div><div class="cell" style="padding:8px 8px;">
<div class="summary-list">
<div class="srow"><span class="slabel">сроки</span><div class="sbar-wrap over-wrap"><div class="sbar-fill sbar-over"></div></div><span class="spct spct-over">160%</span></div><div class="srow"><span class="slabel">готовность</span><div class="sbar-wrap"><div class="sbar-fill sbar-skew" style="width:60%"></div></div><span class="spct spct-skew">60%</span></div><div class="srow"><span class="slabel">трудозат</span><div class="sbar-wrap over-wrap"><div class="sbar-fill sbar-over"></div></div><span class="spct spct-over">125%</span></div><div class="srow"><span class="slabel">поступл.</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:60%"></div></div><span class="spct">60%</span></div><div class="srow"><span class="slabel">затраты</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:68%"></div></div><span class="spct">68%</span></div></div>
</div><div class="cell dates-cell overflow-cell"><div class="dates-top"><div class="dates-range"><div class="mp-label"> </div><div class="dates-pair"><div class="dr-line">начало: 2026.05.01</div><div class="dr-line bad">оконч.: 2026.07.01</div></div></div><div class="dates-kpi"><div class="kpi-label" style="color:var(--bad);font-weight:700;font-size:10px;letter-spacing:.08em;">СРЫВ</div><div class="kpi-n bad">150</div><div class="kpi-u">дней</div><div class="kpi-delta">+60 дн</div></div></div><div class="bar-row"><div class="bar bar-overflow"><div class="fill"></div></div><span class="bar-pct bar-pct-over">160%</span></div></div><div class="cell pair-cell overflow-cell skew-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair">
<div class="mp-label">план / факт</div>
<div class="mp-plan">100%</div>
<div class="mp-fact">60%</div>
</div>
<div class="metric-kpi">
<div class="kpi-label">остаток</div>
<div class="kpi-n">40</div>
<div class="kpi-u">%</div>
</div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:60%;background:rgba(255,160,50,0.75);"></div></div><span class="bar-pct bar-pct-skew">60%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair">
<div class="mp-label">план / факт</div>
<div class="mp-plan">1 200 ч/ч</div>
<div class="mp-fact bad">1 500 ч/ч</div>
</div>
<div class="metric-kpi">
<div class="kpi-label" style="color:var(--bad);font-weight:700;font-size:10px;letter-spacing:.08em;">СРЫВ</div>
<div class="kpi-n bad">1875</div>
<div class="kpi-u">ч/ч</div>
<div class="kpi-delta">+300 ч/ч</div>
</div>
</div><div class="bar-row"><div class="bar bar-overflow"><div class="fill"></div></div><span class="bar-pct bar-pct-over">125%</span></div></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair">
<div class="mp-label">план / факт</div>
<div class="mp-plan">1,2 млн ₽</div>
<div class="mp-fact">0,7 млн ₽</div>
</div>
<div class="metric-kpi">
<div class="kpi-label">остаток</div>
<div class="kpi-n">0,5</div>
<div class="kpi-u">млн</div>
</div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:60%"></div></div><span class="bar-pct">60%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair">
<div class="mp-label">план / факт</div>
<div class="mp-plan">1,0 млн ₽</div>
<div class="mp-fact">0,68 млн ₽</div>
</div>
<div class="metric-kpi">
<div class="kpi-label">остаток</div>
<div class="kpi-n">0,32</div>
<div class="kpi-u">млн</div>
</div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:68%"></div></div><span class="bar-pct">68%</span></div></div></div></div>
<!-- СТРОКА 2: ЖК Север — СРЫВ Затраты 138% -->
<div class="board-row health-bad stage-pd">
<div class="cell proj-cell">
<div class="proj-head">
<div>
<div class="proj-code">ШИФР <b>2023-0011</b></div>
<div class="proj-name">ЖК Север</div>
<div class="proj-dev">Град Инвест</div>
<div class="proj-gip">ГИП: И. Смирнов · 58,0 млн ₽</div>
</div>
<div class="badge-stack"><span class="badge badge-pd">ПД</span><span class="badge-status status-cur">ТЕК</span></div>
</div>
<div class="proj-health">
<span class="health-dot bad"></span>
<span class="health-label">В работе · Перерасход затрат</span>
</div>
</div><div class="cell team-cell"><div class="team-donut-wrap"><svg height="66" viewbox="0 0 66 66" width="66"><circle cx="33" cy="33" fill="none" r="26" stroke="rgba(120,160,200,0.12)" stroke-width="5"></circle><circle cx="33" cy="33" fill="none" r="26" stroke="#5aa6e0" stroke-dasharray="16.3 147.0" stroke-dashoffset="40.8" stroke-linecap="round" stroke-width="5"></circle><text fill="#5aa6e0" font-family="Inter,Arial,sans-serif" font-size="15" font-weight="700" text-anchor="middle" x="33" y="38">10%</text></svg><div class="team-total">24 чел.</div></div></div><div class="cell" style="padding:8px 8px;">
<div class="summary-list">
<div class="srow"><span class="slabel">сроки</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:68%"></div></div><span class="spct">68%</span></div><div class="srow"><span class="slabel">готовность</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:66%"></div></div><span class="spct">66%</span></div><div class="srow"><span class="slabel">трудозат</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:63%"></div></div><span class="spct">63%</span></div><div class="srow"><span class="slabel">поступл.</span><div class="sbar-wrap"><div class="sbar-fill sbar-skew" style="width:57%"></div></div><span class="spct spct-skew">57%</span></div><div class="srow"><span class="slabel">затраты</span><div class="sbar-wrap over-wrap"><div class="sbar-fill sbar-over"></div></div><span class="spct spct-over">138%</span></div></div>
</div><div class="cell dates-cell"><div class="dates-top"><div class="dates-range"><div class="mp-label"> </div><div class="dates-pair"><div class="dr-line">начало: 2026.01.10</div><div class="dr-line">оконч.: 2026.04.25</div></div></div><div class="dates-kpi"><div class="kpi-label">осталось</div><div class="kpi-n">41</div><div class="kpi-u">дня</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:68%"></div></div><span class="bar-pct">68%</span></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">100%</div><div class="mp-fact">66%</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">34</div><div class="kpi-u">%</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:66%"></div></div><span class="bar-pct">66%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">1 750 ч/ч</div><div class="mp-fact">1 100 ч/ч</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">640</div><div class="kpi-u">ч/ч</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:63%"></div></div><span class="bar-pct">63%</span></div></div></div><div class="cell pair-cell overflow-cell skew-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">2,1 млн ₽</div><div class="mp-fact">1,2 млн ₽</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">0,9</div><div class="kpi-u">млн</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:57%;background:rgba(255,160,50,0.75);"></div></div><span class="bar-pct bar-pct-skew">57%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair">
<div class="mp-label">план / факт</div>
<div class="mp-plan">1,8 млн ₽</div>
<div class="mp-fact bad">2,5 млн ₽</div>
</div>
<div class="metric-kpi">
<div class="kpi-label" style="color:var(--bad);font-weight:700;font-size:10px;letter-spacing:.08em;">СРЫВ</div>
<div class="kpi-n bad">3,4</div>
<div class="kpi-u">млн ₽</div>
<div class="kpi-delta">+0,7 млн</div>
</div>
</div><div class="bar-row"><div class="bar bar-overflow"><div class="fill"></div></div><span class="bar-pct bar-pct-over">138%</span></div></div></div></div>
<!-- СТРОКА 3: ЖК Юг — норма -->
<div class="board-row health-ok stage-rd">
<div class="cell proj-cell">
<div class="proj-head">
<div>
<div class="proj-code">ШИФР <b>2023-0014</b></div>
<div class="proj-name">ЖК Юг</div>
<div class="proj-dev">Сити Девелопмент</div>
<div class="proj-gip">ГИП: М. Волков · 82,0 млн ₽</div>
</div>
<div class="badge-stack"><span class="badge badge-rd">РД</span><span class="badge-status status-new">НОВ</span></div>
</div>
<div class="proj-health">
<span class="health-dot ok"></span>
<span class="health-label">В работе · 72%</span>
</div>
</div><div class="cell team-cell"><div class="team-donut-wrap"><svg height="66" viewbox="0 0 66 66" width="66"><circle cx="33" cy="33" fill="none" r="26" stroke="rgba(120,160,200,0.12)" stroke-width="5"></circle><circle cx="33" cy="33" fill="none" r="26" stroke="#5aa6e0" stroke-dasharray="32.7 130.7" stroke-dashoffset="40.8" stroke-linecap="round" stroke-width="5"></circle><text fill="#5aa6e0" font-family="Inter,Arial,sans-serif" font-size="15" font-weight="700" text-anchor="middle" x="33" y="38">20%</text></svg><div class="team-total">40 чел.</div></div></div><div class="cell" style="padding:8px 8px;">
<div class="summary-list">
<div class="srow"><span class="slabel">сроки</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:72%"></div></div><span class="spct">72%</span></div><div class="srow"><span class="slabel">готовность</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:72%"></div></div><span class="spct">72%</span></div><div class="srow"><span class="slabel">трудозат</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:72%"></div></div><span class="spct">72%</span></div><div class="srow"><span class="slabel">поступл.</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:72%"></div></div><span class="spct">72%</span></div><div class="srow"><span class="slabel">затраты</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:68%"></div></div><span class="spct">68%</span></div></div>
</div><div class="cell dates-cell"><div class="dates-top"><div class="dates-range"><div class="mp-label"> </div><div class="dates-pair"><div class="dr-line">начало: 2026.03.15</div><div class="dr-line">оконч.: 2026.07.30</div></div></div><div class="dates-kpi"><div class="kpi-label">осталось</div><div class="kpi-n">48</div><div class="kpi-u">дня</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:72%"></div></div><span class="bar-pct">72%</span></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">100%</div><div class="mp-fact">72%</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">28</div><div class="kpi-u">%</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:72%"></div></div><span class="bar-pct">72%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">2 100 ч/ч</div><div class="mp-fact">1 520 ч/ч</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">580</div><div class="kpi-u">ч/ч</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:72%"></div></div><span class="bar-pct">72%</span></div></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">2,8 млн ₽</div><div class="mp-fact">2,0 млн ₽</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">0,8</div><div class="kpi-u">млн</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:72%"></div></div><span class="bar-pct">72%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">2,2 млн ₽</div><div class="mp-fact">1,5 млн ₽</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">0,7</div><div class="kpi-u">млн</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:68%"></div></div><span class="bar-pct">68%</span></div></div></div></div>
<!-- СТРОКА 4: БЦ Zorge — ПЕРЕКОС: трудозатраты опережают готовность на 25% -->
<div class="board-row health-warn stage-kk">
<div class="cell proj-cell">
<div class="proj-head">
<div>
<div class="proj-code">ШИФР <b>2025-0013-ПД</b></div>
<div class="proj-name">БЦ Zorge</div>
<div class="proj-dev">Форсайт</div>
<div class="proj-gip">ГИП: Е. Петрова · 31,0 млн ₽</div>
</div>
<div class="badge-stack"><span class="badge badge-kk">КК</span><span class="badge-status status-cur">ТЕК</span></div>
</div>
<div class="proj-health">
<span class="health-dot warn"></span>
<span class="health-label">В работе · 35%</span>
</div>
</div><div class="cell team-cell"><div class="team-donut-wrap"><svg height="66" viewbox="0 0 66 66" width="66"><circle cx="33" cy="33" fill="none" r="26" stroke="rgba(120,160,200,0.12)" stroke-width="5"></circle><circle cx="33" cy="33" fill="none" r="26" stroke="#5aa6e0" stroke-dasharray="19.6 143.8" stroke-dashoffset="40.8" stroke-linecap="round" stroke-width="5"></circle><text fill="#5aa6e0" font-family="Inter,Arial,sans-serif" font-size="15" font-weight="700" text-anchor="middle" x="33" y="38">12%</text></svg><div class="team-total">17 чел.</div></div></div><div class="cell" style="padding:8px 8px;">
<div class="summary-list">
<div class="srow"><span class="slabel">сроки</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:55%"></div></div><span class="spct">55%</span></div><div class="srow"><span class="slabel">готовность</span><div class="sbar-wrap"><div class="sbar-fill sbar-skew" style="width:35%"></div></div><span class="spct spct-skew">35%</span></div><div class="srow"><span class="slabel">трудозат</span><div class="sbar-wrap"><div class="sbar-fill sbar-skew" style="width:60%"></div></div><span class="spct spct-skew">60%</span></div><div class="srow"><span class="slabel">поступл.</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:25%"></div></div><span class="spct">25%</span></div><div class="srow"><span class="slabel">затраты</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:38%"></div></div><span class="spct">38%</span></div></div>
</div><div class="cell dates-cell"><div class="dates-top"><div class="dates-range"><div class="mp-label"> </div><div class="dates-pair"><div class="dr-line">начало: 2026.04.01</div><div class="dr-line">оконч.: 2026.09.30</div></div></div><div class="dates-kpi"><div class="kpi-label">осталось</div><div class="kpi-n">114</div><div class="kpi-u">дней</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:55%"></div></div><span class="bar-pct">55%</span></div></div><div class="cell pair-cell skew-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">100%</div><div class="mp-fact">35%</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">65</div><div class="kpi-u">%</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:35%;background:rgba(255,160,50,0.75);"></div></div><span class="bar-pct bar-pct-skew">35%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">480 ч/ч</div><div class="mp-fact">290 ч/ч</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">190</div><div class="kpi-u">ч/ч</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:60%;background:rgba(255,160,50,0.75);"></div></div><span class="bar-pct bar-pct-skew">60%</span></div></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">0,6 млн ₽</div><div class="mp-fact">0,15 млн ₽</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">0,45</div><div class="kpi-u">млн</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:25%"></div></div><span class="bar-pct">25%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">0,55 млн ₽</div><div class="mp-fact">0,21 млн ₽</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">0,34</div><div class="kpi-u">млн</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:38%"></div></div><span class="bar-pct">38%</span></div></div></div></div>
<!-- СТРОКА 5: ЖК Большой Палашевский ЭТАП 1 -->
<div class="board-row health-warn stage-rd">
<div class="cell proj-cell">
<div class="proj-head">
<div>
<div class="proj-code">ШИФР <b>2024-0001</b></div>
<div class="proj-name">ЖК Большой Палашевский ЭТАП 1</div>
<div class="proj-dev">Sminex</div>
<div class="proj-gip">ГИП: С. Орлов · 83,8 млн ₽</div>
</div>
<div class="badge-stack"><span class="badge badge-rd">РД</span><span class="badge-status status-stop">СТОП</span></div>
</div>
<div class="proj-health">
<span class="health-dot warn"></span>
<span class="health-label">В работе · 52%</span>
</div>
</div><div class="cell team-cell"><div class="team-donut-wrap"><svg height="66" viewbox="0 0 66 66" width="66"><circle cx="33" cy="33" fill="none" r="26" stroke="rgba(120,160,200,0.12)" stroke-width="5"></circle><circle cx="33" cy="33" fill="none" r="26" stroke="#5aa6e0" stroke-dasharray="29.4 134.0" stroke-dashoffset="40.8" stroke-linecap="round" stroke-width="5"></circle><text fill="#5aa6e0" font-family="Inter,Arial,sans-serif" font-size="15" font-weight="700" text-anchor="middle" x="33" y="38">18%</text></svg><div class="team-total">25 чел.</div></div></div><div class="cell" style="padding:8px 8px;">
<div class="summary-list">
<div class="srow"><span class="slabel">сроки</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:60%"></div></div><span class="spct">60%</span></div><div class="srow"><span class="slabel">готовность</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:52%"></div></div><span class="spct">52%</span></div><div class="srow"><span class="slabel">трудозат</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:58%"></div></div><span class="spct">58%</span></div><div class="srow"><span class="slabel">поступл.</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:50%"></div></div><span class="spct">50%</span></div><div class="srow"><span class="slabel">затраты</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:54%"></div></div><span class="spct">54%</span></div></div>
</div><div class="cell dates-cell"><div class="dates-top"><div class="dates-range"><div class="mp-label"> </div><div class="dates-pair"><div class="dr-line">начало: 2025.11.01</div><div class="dr-line">оконч.: 2026.08.15</div></div></div><div class="dates-kpi"><div class="kpi-label">осталось</div><div class="kpi-n">68</div><div class="kpi-u">дней</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:60%"></div></div><span class="bar-pct">60%</span></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">100%</div><div class="mp-fact">52%</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">48</div><div class="kpi-u">%</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:52%"></div></div><span class="bar-pct">52%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">1 640 ч/ч</div><div class="mp-fact">950 ч/ч</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">690</div><div class="kpi-u">ч/ч</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:58%"></div></div><span class="bar-pct">58%</span></div></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">3,2 млн ₽</div><div class="mp-fact">1,6 млн ₽</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">1,6</div><div class="kpi-u">млн</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:50%"></div></div><span class="bar-pct">50%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">2,9 млн ₽</div><div class="mp-fact">1,57 млн ₽</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">1,33</div><div class="kpi-u">млн</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:54%"></div></div><span class="bar-pct">54%</span></div></div></div></div>
<!-- СТРОКА 6: ТПУ Восток -->
<div class="board-row health-warn stage-rd">
<div class="cell proj-cell">
<div class="proj-head">
<div>
<div class="proj-code">ШИФР <b>2022-0020</b></div>
<div class="proj-name">ТПУ Восток</div>
<div class="proj-dev">Урбан Система</div>
<div class="proj-gip">ГИП: В. Лапин · 44,0 млн ₽</div>
</div>
<div class="badge-stack"><span class="badge badge-rd">РД</span><span class="badge-status status-new">НОВ</span></div>
</div>
<div class="proj-health">
<span class="health-dot warn"></span>
<span class="health-label">В работе · 61%</span>
</div>
</div><div class="cell team-cell"><div class="team-donut-wrap"><svg height="66" viewbox="0 0 66 66" width="66"><circle cx="33" cy="33" fill="none" r="26" stroke="rgba(120,160,200,0.12)" stroke-width="5"></circle><circle cx="33" cy="33" fill="none" r="26" stroke="#5aa6e0" stroke-dasharray="35.9 127.4" stroke-dashoffset="40.8" stroke-linecap="round" stroke-width="5"></circle><text fill="#5aa6e0" font-family="Inter,Arial,sans-serif" font-size="15" font-weight="700" text-anchor="middle" x="33" y="38">22%</text></svg><div class="team-total">34 чел.</div></div></div><div class="cell" style="padding:8px 8px;">
<div class="summary-list">
<div class="srow"><span class="slabel">сроки</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:70%"></div></div><span class="spct">70%</span></div><div class="srow"><span class="slabel">готовность</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:61%"></div></div><span class="spct">61%</span></div><div class="srow"><span class="slabel">трудозат</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:65%"></div></div><span class="spct">65%</span></div><div class="srow"><span class="slabel">поступл.</span><div class="sbar-wrap"><div class="sbar-fill sbar-skew" style="width:35%"></div></div><span class="spct spct-skew">35%</span></div><div class="srow"><span class="slabel">затраты</span><div class="sbar-wrap"><div class="sbar-fill sbar-skew" style="width:62%"></div></div><span class="spct spct-skew">62%</span></div></div>
</div><div class="cell dates-cell"><div class="dates-top"><div class="dates-range"><div class="mp-label"> </div><div class="dates-pair"><div class="dr-line">начало: 2025.09.01</div><div class="dr-line">оконч.: 2026.06.30</div></div></div><div class="dates-kpi"><div class="kpi-label">осталось</div><div class="kpi-n">22</div><div class="kpi-u">дня</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:70%"></div></div><span class="bar-pct">70%</span></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">100%</div><div class="mp-fact">61%</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">39</div><div class="kpi-u">%</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:61%"></div></div><span class="bar-pct">61%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">900 ч/ч</div><div class="mp-fact">585 ч/ч</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">315</div><div class="kpi-u">ч/ч</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:65%"></div></div><span class="bar-pct">65%</span></div></div></div><div class="cell pair-cell skew-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">1,5 млн ₽</div><div class="mp-fact">0,53 млн ₽</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">0,97</div><div class="kpi-u">млн</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:35%;background:rgba(255,160,50,0.75);"></div></div><span class="bar-pct bar-pct-skew">35%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">1,3 млн ₽</div><div class="mp-fact">0,81 млн ₽</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">0,49</div><div class="kpi-u">млн</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:62%;background:rgba(255,160,50,0.75);"></div></div><span class="bar-pct bar-pct-skew">62%</span></div></div></div></div>
<!-- СТРОКА 7: БЦ Meridian — СРЫВ Затраты 118% -->
<div class="board-row health-bad stage-rd">
<div class="cell proj-cell">
<div class="proj-head">
<div>
<div class="proj-code">ШИФР <b>2024-0010</b></div>
<div class="proj-name">БЦ Meridian</div>
<div class="proj-dev">North Capital</div>
<div class="proj-gip">ГИП: Н. Соколов · 65,0 млн ₽</div>
</div>
<div class="badge-stack"><span class="badge badge-rd">РД</span><span class="badge-status status-cur">ТЕК</span></div>
</div>
<div class="proj-health">
<span class="health-dot bad"></span>
<span class="health-label">В работе · Перерасход затрат</span>
</div>
</div><div class="cell team-cell"><div class="team-donut-wrap"><svg height="66" viewbox="0 0 66 66" width="66"><circle cx="33" cy="33" fill="none" r="26" stroke="rgba(120,160,200,0.12)" stroke-width="5"></circle><circle cx="33" cy="33" fill="none" r="26" stroke="#5aa6e0" stroke-dasharray="26.1 137.2" stroke-dashoffset="40.8" stroke-linecap="round" stroke-width="5"></circle><text fill="#5aa6e0" font-family="Inter,Arial,sans-serif" font-size="15" font-weight="700" text-anchor="middle" x="33" y="38">16%</text></svg><div class="team-total">23 чел.</div></div></div><div class="cell" style="padding:8px 8px;">
<div class="summary-list">
<div class="srow"><span class="slabel">сроки</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:62%"></div></div><span class="spct">62%</span></div><div class="srow"><span class="slabel">готовность</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:58%"></div></div><span class="spct">58%</span></div><div class="srow"><span class="slabel">трудозат</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:65%"></div></div><span class="spct">65%</span></div><div class="srow"><span class="slabel">поступл.</span><div class="sbar-wrap"><div class="sbar-fill sbar-skew" style="width:52%"></div></div><span class="spct spct-skew">52%</span></div><div class="srow"><span class="slabel">затраты</span><div class="sbar-wrap over-wrap"><div class="sbar-fill sbar-over"></div></div><span class="spct spct-over">118%</span></div></div>
</div><div class="cell dates-cell"><div class="dates-top"><div class="dates-range"><div class="mp-label"> </div><div class="dates-pair"><div class="dr-line">начало: 2025.12.01</div><div class="dr-line">оконч.: 2026.09.01</div></div></div><div class="dates-kpi"><div class="kpi-label">осталось</div><div class="kpi-n">85</div><div class="kpi-u">дней</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:62%"></div></div><span class="bar-pct">62%</span></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">100%</div><div class="mp-fact">58%</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">42</div><div class="kpi-u">%</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:58%"></div></div><span class="bar-pct">58%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">1 200 ч/ч</div><div class="mp-fact">780 ч/ч</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">420</div><div class="kpi-u">ч/ч</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:65%"></div></div><span class="bar-pct">65%</span></div></div></div><div class="cell pair-cell overflow-cell skew-cell"><div class="pair-half"><div class="metric-top">
<div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">2,4 млн ₽</div><div class="mp-fact">1,25 млн ₽</div></div>
<div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">1,15</div><div class="kpi-u">млн</div></div>
</div><div class="bar-row"><div class="bar"><div class="fill" style="width:52%;background:rgba(255,160,50,0.75);"></div></div><span class="bar-pct bar-pct-skew">52%</span></div></div><div class="pair-half"><div class="metric-top">
<div class="metric-pair">
<div class="mp-label">план / факт</div>
<div class="mp-plan">2,0 млн ₽</div>
<div class="mp-fact bad">2,36 млн ₽</div>
</div>
<div class="metric-kpi">
<div class="kpi-label" style="color:var(--bad);font-weight:700;font-size:10px;letter-spacing:.08em;">СРЫВ</div>
<div class="kpi-n bad">2,36</div>
<div class="kpi-u">млн ₽</div>
<div class="kpi-delta">+0,36 млн</div>
</div>
</div><div class="bar-row"><div class="bar bar-overflow"><div class="fill"></div></div><span class="bar-pct bar-pct-over">118%</span></div></div></div></div>
<!-- СТРОКА 8: ЖК Речной — норма -->
<div class="board-row health-ok stage-rd">
<div class="cell proj-cell">
<div class="proj-head"><div><div class="proj-code">ШИФР <b>2024-0015</b></div><div class="proj-name">ЖК Речной</div><div class="proj-dev">Речные Берега</div><div class="proj-gip">ГИП: А. Фёдоров · 91,0 млн ₽</div></div><div class="badge-stack"><span class="badge badge-rd">РД</span><span class="badge-status status-cur">ТЕК</span></div></div>
<div class="proj-health"><span class="health-dot ok"></span><span class="health-label">В работе · 78%</span></div>
</div><div class="cell team-cell"><div class="team-donut-wrap"><svg height="66" viewbox="0 0 66 66" width="66"><circle cx="33" cy="33" fill="none" r="26" stroke="rgba(120,160,200,0.12)" stroke-width="5"></circle><circle cx="33" cy="33" fill="none" r="26" stroke="#5aa6e0" stroke-dasharray="29.4 134.0" stroke-dashoffset="40.8" stroke-linecap="round" stroke-width="5"></circle><text fill="#5aa6e0" font-family="Inter,Arial,sans-serif" font-size="15" font-weight="700" text-anchor="middle" x="33" y="38">18%</text></svg><div class="team-total">29 чел.</div></div></div><div class="cell" style="padding:8px 8px;"><div class="summary-list"><div class="srow"><span class="slabel">сроки</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:80%"></div></div><span class="spct">80%</span></div><div class="srow"><span class="slabel">готовность</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:78%"></div></div><span class="spct">78%</span></div><div class="srow"><span class="slabel">трудозат</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:75%"></div></div><span class="spct">75%</span></div><div class="srow"><span class="slabel">поступл.</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:76%"></div></div><span class="spct">76%</span></div><div class="srow"><span class="slabel">затраты</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:74%"></div></div><span class="spct">74%</span></div></div></div><div class="cell dates-cell"><div class="dates-top"><div class="dates-range"><div class="mp-label"> </div><div class="dates-pair"><div class="dr-line">начало: 2025.06.01</div><div class="dr-line">оконч.: 2026.10.15</div></div></div><div class="dates-kpi"><div class="kpi-label">осталось</div><div class="kpi-n">127</div><div class="kpi-u">дней</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:80%"></div></div><span class="bar-pct">80%</span></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">100%</div><div class="mp-fact">78%</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">22</div><div class="kpi-u">%</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:78%"></div></div><span class="bar-pct">78%</span></div></div><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">2 400 ч/ч</div><div class="mp-fact">1 800 ч/ч</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">600</div><div class="kpi-u">ч/ч</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:75%"></div></div><span class="bar-pct">75%</span></div></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">4,5 млн ₽</div><div class="mp-fact">3,4 млн ₽</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">1,1</div><div class="kpi-u">млн</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:76%"></div></div><span class="bar-pct">76%</span></div></div><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">3,8 млн ₽</div><div class="mp-fact">2,8 млн ₽</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">1,0</div><div class="kpi-u">млн</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:74%"></div></div><span class="bar-pct">74%</span></div></div></div></div>
<!-- СТРОКА 9: ЖК Западный — перекос поступл/затраты -->
<div class="board-row health-warn stage-pd">
<div class="cell proj-cell">
<div class="proj-head"><div><div class="proj-code">ШИФР <b>2023-0031</b></div><div class="proj-name">ЖК Западный</div><div class="proj-dev">Запад Девелопмент</div><div class="proj-gip">ГИП: О. Белова · 54,0 млн ₽</div></div><div class="badge-stack"><span class="badge badge-pd">ПД</span><span class="badge-status status-stop">СТОП</span></div></div>
<div class="proj-health"><span class="health-dot warn"></span><span class="health-label">В работе · Перерасход затрат</span></div>
</div><div class="cell team-cell"><div class="team-donut-wrap"><svg height="66" viewbox="0 0 66 66" width="66"><circle cx="33" cy="33" fill="none" r="26" stroke="rgba(120,160,200,0.12)" stroke-width="5"></circle><circle cx="33" cy="33" fill="none" r="26" stroke="#5aa6e0" stroke-dasharray="22.6 140.7" stroke-dashoffset="40.8" stroke-linecap="round" stroke-width="5"></circle><text fill="#5aa6e0" font-family="Inter,Arial,sans-serif" font-size="15" font-weight="700" text-anchor="middle" x="33" y="38">14%</text></svg><div class="team-total">23 чел.</div></div></div><div class="cell" style="padding:8px 8px;"><div class="summary-list"><div class="srow"><span class="slabel">сроки</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:52%"></div></div><span class="spct">52%</span></div><div class="srow"><span class="slabel">готовность</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:55%"></div></div><span class="spct">55%</span></div><div class="srow"><span class="slabel">трудозат</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:58%"></div></div><span class="spct">58%</span></div><div class="srow"><span class="slabel">поступл.</span><div class="sbar-wrap"><div class="sbar-fill sbar-skew" style="width:42%"></div></div><span class="spct spct-skew">42%</span></div><div class="srow"><span class="slabel">затраты</span><div class="sbar-wrap over-wrap"><div class="sbar-fill sbar-over"></div></div><span class="spct spct-over">112%</span></div></div></div><div class="cell dates-cell"><div class="dates-top"><div class="dates-range"><div class="mp-label"> </div><div class="dates-pair"><div class="dr-line">начало: 2025.03.01</div><div class="dr-line">оконч.: 2026.12.01</div></div></div><div class="dates-kpi"><div class="kpi-label">осталось</div><div class="kpi-n">174</div><div class="kpi-u">дней</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:52%"></div></div><span class="bar-pct">52%</span></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">100%</div><div class="mp-fact">55%</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">45</div><div class="kpi-u">%</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:55%"></div></div><span class="bar-pct">55%</span></div></div><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">1 900 ч/ч</div><div class="mp-fact">1 100 ч/ч</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">800</div><div class="kpi-u">ч/ч</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:58%"></div></div><span class="bar-pct">58%</span></div></div></div><div class="cell pair-cell overflow-cell skew-cell"><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">3,1 млн ₽</div><div class="mp-fact">1,3 млн ₽</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">1,8</div><div class="kpi-u">млн</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:42%;background:rgba(255,160,50,0.75);"></div></div><span class="bar-pct bar-pct-skew">42%</span></div></div><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">2,5 млн ₽</div><div class="mp-fact bad">2,8 млн ₽</div></div><div class="metric-kpi"><div class="kpi-label" style="color:var(--bad);font-weight:700;font-size:10px;">СРЫВ</div><div class="kpi-n bad">2,8</div><div class="kpi-u">млн ₽</div><div class="kpi-delta">+0,3 млн</div></div></div><div class="bar-row"><div class="bar bar-overflow"><div class="fill"></div></div><span class="bar-pct bar-pct-over">112%</span></div></div></div></div>
<!-- СТРОКА 10: МФЦ Центральный — КК ранняя стадия -->
<div class="board-row health-ok stage-kk">
<div class="cell proj-cell">
<div class="proj-head"><div><div class="proj-code">ШИФР <b>2025-0044</b></div><div class="proj-name">МФЦ Центральный</div><div class="proj-dev">ЦентрСтрой</div><div class="proj-gip">ГИП: В. Кузнецов · 38,0 млн ₽</div></div><div class="badge-stack"><span class="badge badge-kk">КК</span><span class="badge-status status-new">НОВ</span></div></div>
<div class="proj-health"><span class="health-dot ok"></span><span class="health-label">В работе · 30%</span></div>
</div><div class="cell team-cell"><div class="team-donut-wrap"><svg height="66" viewbox="0 0 66 66" width="66"><circle cx="33" cy="33" fill="none" r="26" stroke="rgba(120,160,200,0.12)" stroke-width="5"></circle><circle cx="33" cy="33" fill="none" r="26" stroke="#5aa6e0" stroke-dasharray="14.5 148.8" stroke-dashoffset="40.8" stroke-linecap="round" stroke-width="5"></circle><text fill="#5aa6e0" font-family="Inter,Arial,sans-serif" font-size="15" font-weight="700" text-anchor="middle" x="33" y="38">9%</text></svg><div class="team-total">15 чел.</div></div></div><div class="cell" style="padding:8px 8px;"><div class="summary-list"><div class="srow"><span class="slabel">сроки</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:28%"></div></div><span class="spct">28%</span></div><div class="srow"><span class="slabel">готовность</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:30%"></div></div><span class="spct">30%</span></div><div class="srow"><span class="slabel">трудозат</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:32%"></div></div><span class="spct">32%</span></div><div class="srow"><span class="slabel">поступл.</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:30%"></div></div><span class="spct">30%</span></div><div class="srow"><span class="slabel">затраты</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:29%"></div></div><span class="spct">29%</span></div></div></div><div class="cell dates-cell"><div class="dates-top"><div class="dates-range"><div class="mp-label"> </div><div class="dates-pair"><div class="dr-line">начало: 2026.01.15</div><div class="dr-line">оконч.: 2027.03.01</div></div></div><div class="dates-kpi"><div class="kpi-label">осталось</div><div class="kpi-n">293</div><div class="kpi-u">дней</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:28%"></div></div><span class="bar-pct">28%</span></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">100%</div><div class="mp-fact">30%</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">70</div><div class="kpi-u">%</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:30%"></div></div><span class="bar-pct">30%</span></div></div><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">980 ч/ч</div><div class="mp-fact">314 ч/ч</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">666</div><div class="kpi-u">ч/ч</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:32%"></div></div><span class="bar-pct">32%</span></div></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">1,8 млн ₽</div><div class="mp-fact">0,54 млн ₽</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">1,26</div><div class="kpi-u">млн</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:30%"></div></div><span class="bar-pct">30%</span></div></div><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">1,5 млн ₽</div><div class="mp-fact">0,44 млн ₽</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">1,06</div><div class="kpi-u">млн</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:29%"></div></div><span class="bar-pct">29%</span></div></div></div></div>
<!-- СТРОКА 11: ЖК Парковый — СРЫВ трудозатраты -->
<div class="board-row health-bad stage-pd">
<div class="cell proj-cell">
<div class="proj-head"><div><div class="proj-code">ШИФР <b>2023-0019</b></div><div class="proj-name">ЖК Парковый</div><div class="proj-dev">ПаркСити</div><div class="proj-gip">ГИП: Д. Миронов · 67,5 млн ₽</div></div><div class="badge-stack"><span class="badge badge-pd">ПД</span><span class="badge-status status-cur">ТЕК</span></div></div>
<div class="proj-health"><span class="health-dot bad"></span><span class="health-label">В работе · СРЫВ трудозатрат</span></div>
</div><div class="cell team-cell"><div class="team-donut-wrap"><svg height="66" viewbox="0 0 66 66" width="66"><circle cx="33" cy="33" fill="none" r="26" stroke="rgba(120,160,200,0.12)" stroke-width="5"></circle><circle cx="33" cy="33" fill="none" r="26" stroke="#5aa6e0" stroke-dasharray="34.3 129.0" stroke-dashoffset="40.8" stroke-linecap="round" stroke-width="5"></circle><text fill="#5aa6e0" font-family="Inter,Arial,sans-serif" font-size="15" font-weight="700" text-anchor="middle" x="33" y="38">21%</text></svg><div class="team-total">36 чел.</div></div></div><div class="cell" style="padding:8px 8px;"><div class="summary-list"><div class="srow"><span class="slabel">сроки</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:71%"></div></div><span class="spct">71%</span></div><div class="srow"><span class="slabel">готовность</span><div class="sbar-wrap"><div class="sbar-fill sbar-skew" style="width:48%"></div></div><span class="spct spct-skew">48%</span></div><div class="srow"><span class="slabel">трудозат</span><div class="sbar-wrap over-wrap"><div class="sbar-fill sbar-over"></div></div><span class="spct spct-over">131%</span></div><div class="srow"><span class="slabel">поступл.</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:50%"></div></div><span class="spct">50%</span></div><div class="srow"><span class="slabel">затраты</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:55%"></div></div><span class="spct">55%</span></div></div></div><div class="cell dates-cell"><div class="dates-top"><div class="dates-range"><div class="mp-label"> </div><div class="dates-pair"><div class="dr-line">начало: 2024.09.01</div><div class="dr-line">оконч.: 2026.11.30</div></div></div><div class="dates-kpi"><div class="kpi-label">осталось</div><div class="kpi-n">172</div><div class="kpi-u">дней</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:71%"></div></div><span class="bar-pct">71%</span></div></div><div class="cell pair-cell overflow-cell skew-cell"><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">100%</div><div class="mp-fact">48%</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">52</div><div class="kpi-u">%</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:48%;background:rgba(255,160,50,0.75);"></div></div><span class="bar-pct bar-pct-skew">48%</span></div></div><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">1 600 ч/ч</div><div class="mp-fact bad">2 096 ч/ч</div></div><div class="metric-kpi"><div class="kpi-label" style="color:var(--bad);font-weight:700;font-size:10px;">СРЫВ</div><div class="kpi-n bad">2 096</div><div class="kpi-u">ч/ч</div><div class="kpi-delta">+496 ч/ч</div></div></div><div class="bar-row"><div class="bar bar-overflow"><div class="fill"></div></div><span class="bar-pct bar-pct-over">131%</span></div></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">3,2 млн ₽</div><div class="mp-fact">1,6 млн ₽</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">1,6</div><div class="kpi-u">млн</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:50%"></div></div><span class="bar-pct">50%</span></div></div><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">2,8 млн ₽</div><div class="mp-fact">1,54 млн ₽</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">1,26</div><div class="kpi-u">млн</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:55%"></div></div><span class="bar-pct">55%</span></div></div></div></div>
<!-- СТРОКА 12: ТЦ Галактика — норма -->
<div class="board-row health-ok stage-kk">
<div class="cell proj-cell">
<div class="proj-head"><div><div class="proj-code">ШИФР <b>2025-0051</b></div><div class="proj-name">ТЦ Галактика</div><div class="proj-dev">GalaxyRetail</div><div class="proj-gip">ГИП: Е. Романова · 120,0 млн ₽</div></div><div class="badge-stack"><span class="badge badge-kk">КК</span><span class="badge-status status-cur">ТЕК</span></div></div>
<div class="proj-health"><span class="health-dot ok"></span><span class="health-label">В работе · 22%</span></div>
</div><div class="cell team-cell"><div class="team-donut-wrap"><svg height="66" viewbox="0 0 66 66" width="66"><circle cx="33" cy="33" fill="none" r="26" stroke="rgba(120,160,200,0.12)" stroke-width="5"></circle><circle cx="33" cy="33" fill="none" r="26" stroke="#5aa6e0" stroke-dasharray="36.1 127.2" stroke-dashoffset="40.8" stroke-linecap="round" stroke-width="5"></circle><text fill="#5aa6e0" font-family="Inter,Arial,sans-serif" font-size="15" font-weight="700" text-anchor="middle" x="33" y="38">23%</text></svg><div class="team-total">46 чел.</div></div></div><div class="cell" style="padding:8px 8px;"><div class="summary-list"><div class="srow"><span class="slabel">сроки</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:20%"></div></div><span class="spct">20%</span></div><div class="srow"><span class="slabel">готовность</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:22%"></div></div><span class="spct">22%</span></div><div class="srow"><span class="slabel">трудозат</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:24%"></div></div><span class="spct">24%</span></div><div class="srow"><span class="slabel">поступл.</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:22%"></div></div><span class="spct">22%</span></div><div class="srow"><span class="slabel">затраты</span><div class="sbar-wrap"><div class="sbar-fill sbar-neutral" style="width:21%"></div></div><span class="spct">21%</span></div></div></div><div class="cell dates-cell"><div class="dates-top"><div class="dates-range"><div class="mp-label"> </div><div class="dates-pair"><div class="dr-line">начало: 2026.02.01</div><div class="dr-line">оконч.: 2027.08.01</div></div></div><div class="dates-kpi"><div class="kpi-label">осталось</div><div class="kpi-n">416</div><div class="kpi-u">дней</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:20%"></div></div><span class="bar-pct">20%</span></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">100%</div><div class="mp-fact">22%</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">78</div><div class="kpi-u">%</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:22%"></div></div><span class="bar-pct">22%</span></div></div><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">5 200 ч/ч</div><div class="mp-fact">1 248 ч/ч</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">3 952</div><div class="kpi-u">ч/ч</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:24%"></div></div><span class="bar-pct">24%</span></div></div></div><div class="cell pair-cell"><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">6,0 млн ₽</div><div class="mp-fact">1,32 млн ₽</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">4,68</div><div class="kpi-u">млн</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:22%"></div></div><span class="bar-pct">22%</span></div></div><div class="pair-half"><div class="metric-top"><div class="metric-pair"><div class="mp-label">план / факт</div><div class="mp-plan">5,2 млн ₽</div><div class="mp-fact">1,09 млн ₽</div></div><div class="metric-kpi"><div class="kpi-label">остаток</div><div class="kpi-n">4,11</div><div class="kpi-u">млн</div></div></div><div class="bar-row"><div class="bar"><div class="fill" style="width:21%"></div></div><span class="bar-pct">21%</span></div></div></div></div>
</div><!-- /.board-body -->
</div><!-- /.board -->
</main>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13
config.php Normal file
View File

@ -0,0 +1,13 @@
<?
define("MYSQL_HOST", "db");
define("MYSQL_BASE", "j7508239_tracker");
define("MYSQL_ADMIN", "root");
define("MYSQL_PASSWORD","Q#Y34KsmCfy*");
define("MYSQL_SSL_CERTIFICATE","");
define("LICENSE_KEY", "victor");
define("LANG", "ru");
define("SALT", "I&^#(*@Y(*#YLKJLASDASD");
define("ADMIN_PAGE", "admin");

View File

@ -0,0 +1,15 @@
<?
use ms\ms\structure\ePages as Pages;
if(IF_AUTH){
include_once(dirname(__FILE__).'/../jade/index.php');
} else {
switch (Site::$URL[0]){
case Pages::REG : include_once(dirname(__FILE__).'/../jade/profile/reg.php'); break;
case Pages::REMIND : include_once(dirname(__FILE__).'/../jade/profile/remind.php'); break;
case Pages::AUTH :
default : include_once(dirname(__FILE__).'/../jade/profile/auth.php'); break;
}
}

View File

View File

File diff suppressed because one or more lines are too long

114
content/admin/index.php Normal file
View File

@ -0,0 +1,114 @@
<?
use core\site\structure\HTMLPage as Page;
use ms\ms;
use shop\category\Relation;
if($_GET['test']) {
/*
\shop\category\Relation::delete();
\shop\category\Relation::setParent(1);
\shop\category\Relation::setParent(2,1);
\shop\category\Relation::setParent(3,2);
\shop\category\Relation::setParent(4,2);
\shop\category\Relation::setParent(5,4);
\shop\category\Relation::setParent(6);
\shop\category\Relation::setParent(2,6);
/**/
exit;
}
if($_GET['category_relation']) {
set_time_limit(0);
$a = \shop\Category::getAll(true);
foreach ($a as $v){
echo '<pre>';
echo var_dump($v);
echo '</pre>';
Relation::setParent($v[\shop\Category::$ID],$v[\shop\Category::$PARENT]);
}
exit;
}
if($_GET['install']) {
\Core::install();
exit;
}
if($_GET['lang']) {
Ms::reinstallLang();
exit;
}
if($_GET['unique']) {
Ms::installUniques();
exit;
}
if($_GET['migrate']) {
//*
// Показывать ли все ошибки
error_reporting(E_ERROR);
ini_set('display_errors',true);
/*/
ini_set('display_errors','Off');
/**/
set_time_limit(0);
Ms::createTables();
Ms::checkColumns(true);
Ms::reinstallLang();
\core\core\structure\Install::installCoreLang();
//*
echo '<pre>';
echo var_dump(\DB::$logs);
echo '</pre>';
/**/
exit;
}
MS::init();
$PAGE = new \core\site\structure\HTMLPage;
if(Site::$justContent) {
ob_start();
include_once('blocks/content.php');
Page::addBodyText(ob_get_clean());
$res = new stdClass();
$res->content = Page::getBodyContent();
$res->title = Page::getTitleText();
$res->data = Page::$data;
$res->time_compile = microtime(true)-$GLOBALS['start_engine'];
$res->time_bd = \DB::$total_time;
echo je($res);
} else {
PAGE::addStyle (Site::$themeAbsPath."css/style.css");
PAGE::addScript("/lib/js/fastdom.js");
PAGE::addScript("/lib/js/jquery-3.3.1.min.js");
Page::addScript('/lib/js/bws/ui.js');
Page::addStyle('/lib/js/bws/ui.css');
Page::addScript('/lib/js/bws/form.js');
Page::addStyle('/lib/js/perfect-scrollbar/css/perfect-scrollbar.css');
Page::addScript('/lib/js/perfect-scrollbar/js/perfect-scrollbar.jquery.js');
Page::addScript('/lib/js/perfect-scrollbar/js/perfect-scrollbar.js');
Page::addStyle('/lib/js/date/jquery.datetimepicker.css');
Page::addScript('/lib/js/date/jquery.datetimepicker.js');
Page::addScript('/lib/js/tinymce/js/tinymce/tinymce.min.js');
Page::addScript(Site::$themeAbsPath."js/all.js");
Page::addMetaCode('<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">');
Page::$favicon = Site::$themeAbsPath."favicon.ico";
if(IF_AUTH){
ob_start(); include_once('jade/sample/bar/left.php'); $LEFT = ob_get_clean();
ob_start(); include_once('blocks/content.php'); $CONTENT = ob_get_clean();
ob_start(); include_once('jade/sample/bar/right.php'); $RIGHT = ob_get_clean();
ob_start(); include_once('jade/sample/bar/footer.php'); $FOOTER = ob_get_clean();
Page::addBodyText('<div id="wrap_content"><div id="content">'.$CONTENT.'</div></div>'.$RIGHT.$FOOTER);
} else {
ob_start(); include_once('blocks/content.php'); $CONTENT = ob_get_clean();
Page::addBodyText('<div id="wrap_content"><div id="content">'.$CONTENT.'</div></div>');
}
echo Page::getPage();
}

View File

View File

View File

@ -0,0 +1,2 @@
<?php use ms\dashboard; ?><div id="dashlist_wrap"><div id="dash_list"><!-- .tbclear.dash_li.arrow_dash(onclick="$.fn.DASHBOARD('prev')") «
.dash_li.arrow_dash(onclick="$.fn.DASHBOARD('next')") »--><div id="dash_elements"><?php echo Dashboard::getMyList(); ?></div><div class="dash_li add_dashboard">+ Добавить рабочий стол</div></div></div><div id="dashboards"></div>

View File

@ -0,0 +1,6 @@
<?php use ms\ms\structure\ePages as Pages; ?><?php $reg = Pages::url(Pages::REG); ?><div id="auth_form"><div id="auth_content"><div id="title">Авторизоваться</div><form id="form" onsubmit="return $.fn.PROFILE('auth')"><div data-input="email" data-name="login"><input placeholder="email@email.ru" class="i"/></div><div data-input="text" data-name="pass"><input placeholder="Пароль" class="i"/></div><div class="tac"><button class="btn">Авторизоваться</button></div><!-- a(href="#{$reg}") Зарегистрироваться--><div class="res"></div></form></div></div><script>$("#form").FORM({
inputs: {
login :{validation:{notEmpty:true}, },
pass :{validation:{notEmpty:true}, },
}
})</script>

View File

@ -0,0 +1,6 @@
<?php use ms\ms\structure\ePages as Pages; ?><?php $reg = Pages::url(Pages::AUTH); ?><form id="form" onsubmit="return $.fn.PROFILE('reg')"><div data-input="email" data-name="login"><input placeholder="login"/></div><div data-input="text" data-name="pass"><input placeholder="pass1"/></div><button>Зарегистрироваться</button><a href="<?= htmlspecialchars($reg, ENT_QUOTES, 'UTF-8'); ?>">Авторизоваться</a><div class="res"></div></form><script>$("#form").FORM({
inputs: {
login :{validation:{notEmpty:true}, },
pass :{validation:{notEmpty:true}, },
}
})</script>

View File

@ -0,0 +1 @@
<form onsubmit="return $.fn.PROFILE('auth')"><input name="login"/><input name="password"/></form>

View File

@ -0,0 +1,4 @@
<div id="footer"><div id="footer_left"><div id="start" class="icon"><div class="img black start"></div></div><!-- #icons.icon
.img.white.contact
.icon
.img.red.news--></div><!-- #footer_right--></div>

View File

@ -0,0 +1 @@
<!-- #leftbarp левая панель-->

View File

@ -0,0 +1 @@
<div id="rightbar"><div id="account"><span onclick="$.fn.PROFILE('exit')">Выйти</span></div></div>

View File

@ -0,0 +1 @@
<div style="background:black;color:white;display:block;width:200px;height:200px" class="drag"><div class="header">Заголовок</div><div class="wwww"></div></div><div style="background:black;color:white;display:block;width:200px;height:200px" class="drag"><div class="header">Заголовок</div><div class="wwww"></div></div>

View File

@ -0,0 +1 @@
<div id="leftbar"><p>левая панель</p></div>

View File

@ -0,0 +1 @@
<div class="dashboard"><div class="grid_dash"><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Новости</div></div></div><div class="td"><div class="icon_td white"><div class="img contact2"></div><div class="title">Контакты</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Безопасность превыше</div></div></div><div class="td"><div class="icon_td white"><div class="img youtube"></div><div class="title">Youtube</div></div></div><div class="td"><div class="icon_td white"><div class="img support"></div><div class="title">Поддержка</div></div></div><div class="td"><div class="icon_td white"><div class="img search"></div><div class="title">Поиск</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Новости</div></div></div><div class="td"><div class="icon_td white"><div class="img contact2"></div><div class="title">Контакты</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Безопасность превыше</div></div></div><div class="td"><div class="icon_td white"><div class="img youtube"></div><div class="title">Youtube</div></div></div><div class="td"><div class="icon_td white"><div class="img support"></div><div class="title">Поддержка</div></div></div><div class="td"><div class="icon_td white"><div class="img search"></div><div class="title">Поиск</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Новости</div></div></div><div class="td"><div class="icon_td white"><div class="img contact2"></div><div class="title">Контакты</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Безопасность превыше</div></div></div><div class="td"><div class="icon_td white"><div class="img youtube"></div><div class="title">Youtube</div></div></div><div class="td"><div class="icon_td white"><div class="img support"></div><div class="title">Поддержка</div></div></div><div class="td"><div class="icon_td white"><div class="img search"></div><div class="title">Поиск</div></div></div><!--.td#help Тут будет помощь--><div id="table" class="td test_table_wrap"><div class="test_table"><?php for($i=1;$i<=10;$i++){; ?><div class="tth"><?php echo htmlspecialchars("Заголовок ".$i, ENT_QUOTES, 'UTF-8'); ?></div><?php }; ?><?php for($i=1;$i<=300;$i++){; ?><div class="ttd"><?php echo htmlspecialchars("ячейка ".$i, ENT_QUOTES, 'UTF-8'); ?></div><?php }; ?></div></div><!-- .td.test_table_wrap#table1include ../test/table--></div></div>

View File

@ -0,0 +1 @@
<div class="dashboard"><div class="grid_dash"><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Новости</div></div></div><div class="td"><div class="icon_td white"><div class="img contact2"></div><div class="title">Контакты</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Безопасность превыше</div></div></div><div class="td"><div class="icon_td white"><div class="img youtube"></div><div class="title">Youtube</div></div></div><div class="td"><div class="icon_td white"><div class="img search"></div><div class="title">Поддержка</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Новости</div></div></div><div class="td"><div class="icon_td white"><div class="img contact2"></div><div class="title">Контакты</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Безопасность превыше</div></div></div><div class="td"><div class="icon_td white"><div class="img youtube"></div><div class="title">Youtube</div></div></div><div class="td"><div class="icon_td white"><div class="img search"></div><div class="title">Поддержка</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Новости</div></div></div><div class="td"><div class="icon_td white"><div class="img contact2"></div><div class="title">Контакты</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Безопасность превыше</div></div></div><div class="td"><div class="icon_td white"><div class="img youtube"></div><div class="title">Youtube</div></div></div><div class="td"><div class="icon_td white"><div class="img search"></div><div class="title">Поддержка</div></div></div></div></div>

View File

@ -0,0 +1 @@
<?php use ms\dashboard; ?><?php $a = Dashboard::getMy(true); ?><div id="dashlist_wrap"><div id="dash_list"><div class="tbclear"><div onclick="$.fn.DASHBOARD('prev')" class="dash_li arrow_dash">«</div><div onclick="$.fn.DASHBOARD('next')" class="dash_li arrow_dash">»</div></div><div id="dash_elements"><?php foreach($a as $k=>$v){; ?><?php $id = $v[Dashboard::$ID]; ?><div onclick="$.fn.DASHBOARD('setDashboard',<?= htmlspecialchars($id, ENT_QUOTES, 'UTF-8'); ?>)" class="dash_li"><?php echo Dashboard::getName($v); ?></div><?php }; ?></div><div onclick="" class="dash_li">+ Добавить рабочий стол</div></div></div><div id="dashboards"><div id="dashboards_slider"><?php foreach($a as $v){; ?><div class="dashboard"><div class="grid_dash"><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Новости</div></div></div><div class="td"><div class="icon_td white"><div class="img contact2"></div><div class="title">Контакты</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Безопасность превыше</div></div></div><div class="td"><div class="icon_td white"><div class="img youtube"></div><div class="title">Youtube</div></div></div><div class="td"><div class="icon_td white"><div class="img support"></div><div class="title">Поддержка</div></div></div><div class="td"><div class="icon_td white"><div class="img search"></div><div class="title">Поиск</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Новости</div></div></div><div class="td"><div class="icon_td white"><div class="img contact2"></div><div class="title">Контакты</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Безопасность превыше</div></div></div><div class="td"><div class="icon_td white"><div class="img youtube"></div><div class="title">Youtube</div></div></div><div class="td"><div class="icon_td white"><div class="img support"></div><div class="title">Поддержка</div></div></div><div class="td"><div class="icon_td white"><div class="img search"></div><div class="title">Поиск</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Новости</div></div></div><div class="td"><div class="icon_td white"><div class="img contact2"></div><div class="title">Контакты</div></div></div><div class="td"><div class="icon_td white"><div class="img news2"></div><div class="title">Безопасность превыше</div></div></div><div class="td"><div class="icon_td white"><div class="img youtube"></div><div class="title">Youtube</div></div></div><div class="td"><div class="icon_td white"><div class="img support"></div><div class="title">Поддержка</div></div></div><div class="td"><div class="icon_td white"><div class="img search"></div><div class="title">Поиск</div></div></div><!--.td#help Тут будет помощь--><div id="table" class="td test_table_wrap"><div class="test_table"><?php for($i=1;$i<=10;$i++){; ?><div class="tth"><?php echo htmlspecialchars("Заголовок ".$i, ENT_QUOTES, 'UTF-8'); ?></div><?php }; ?><?php for($i=1;$i<=300;$i++){; ?><div class="ttd"><?php echo htmlspecialchars("ячейка ".$i, ENT_QUOTES, 'UTF-8'); ?></div><?php }; ?></div></div><!-- .td.test_table_wrap#table1include ../test/table--></div></div><?php }; ?></div></div><!--include blocks/typography-->

View File

@ -0,0 +1 @@
<div class="row"><div style="background:yellow;height:50px" class="c c-mob-100 c-pad-25 c-desk-50"></div><div style="background:blue;height:50px" class="c c-50 c-mob-100 c-pad-25 c-desk-50"></div><div style="background:purple;height:50px" class="c c-33 c-mob-100 c-pad-25 c-desk-50"></div><div style="background:navy;height:50px" class="c c-17 c-mob-100 c-pad-25 c-desk-50"></div></div><div class="row"><div style="background:yellow;height:50px" class="c c-mob-1-3 c-pad-4-9"></div><div style="background:blue;height:50px" class="c c-mob-1-3 c-pad-4-9"></div><div style="background:purple;height:50px" class="c c-mob-1-3 c-pad-4-9"></div><div style="background:navy;height:50px" class="c c-mob-1-3 c-pad-4-9"></div></div><div class="row"><div style="background:yellow;height:50px" class="c c-25"></div><div style="background:blue;height:50px" class="c c-25"></div><div style="background:purple;height:50px" class="c c-25"></div><div style="background:navy;height:50px" class="c c-25"></div></div><div class="row"><div class="c c-1-3">Блок ровно в 1/3</div><div class="c c-1-3">Блок ровно в 1/3</div><div class="c c-1-3">Блок ровно в 1/3</div></div><div class="row"><div class="c c-2-7">Блок ровно в 2/7</div><div class="c c-5-7">Блок ровно в 5/7</div></div>

View File

View File

@ -0,0 +1 @@
<div class="test_table"><?php for($i=1;$i<=10;$i++){; ?><div class="tth"><?php echo htmlspecialchars("Заголовок ".$i, ENT_QUOTES, 'UTF-8'); ?></div><?php }; ?><?php for($i=1;$i<=300;$i++){; ?><div class="ttd"><?php echo htmlspecialchars("ячейка ".$i, ENT_QUOTES, 'UTF-8'); ?></div><?php }; ?></div>

View File

1872
content/admin/js/all.js Normal file

File diff suppressed because it is too large Load Diff

1
content/admin/js/all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 MiB

View File

@ -0,0 +1 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 24 24" height="24px" id="Layer_1" version="1.1" viewBox="0 0 24 24" width="24px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g><g><g><g><path d="M14.466,19.182H7.938c-0.62,0-1.125-0.492-1.125-1.094v-1.145c0-2.002,1.763-4.145,4.388-4.145 c2.626,0,4.39,2.143,4.39,4.145v1.145C15.591,18.689,15.086,19.182,14.466,19.182z M11.201,13.596 c-2.147,0-3.59,1.732-3.59,3.348v1.145c0,0.162,0.146,0.295,0.327,0.295h6.528c0.181,0,0.327-0.133,0.327-0.295v-1.145 C14.793,15.328,13.35,13.596,11.201,13.596z"/></g></g></g></g><g><g><g><g><path d="M19.263,11.862c-1.961,0-3.556-1.58-3.556-3.521s1.595-3.521,3.556-3.521c1.96,0,3.555,1.58,3.555,3.521 S21.223,11.862,19.263,11.862z M19.263,5.617c-1.521,0-2.758,1.222-2.758,2.724c0,1.501,1.237,2.724,2.758,2.724 S22.02,9.842,22.02,8.34C22.02,6.838,20.783,5.617,19.263,5.617z"/></g></g></g></g><g><g><g><g><path d="M11.254,12.513c-1.764,0-3.199-1.421-3.199-3.168c0-1.748,1.435-3.17,3.199-3.17c1.765,0,3.202,1.422,3.202,3.17 C14.456,11.092,13.02,12.513,11.254,12.513z M11.254,6.973c-1.324,0-2.401,1.064-2.401,2.372c0,1.307,1.077,2.37,2.401,2.37 c1.325,0,2.404-1.063,2.404-2.37C13.658,8.038,12.58,6.973,11.254,6.973z"/></g></g></g></g><g><g><g><g><path d="M3.994,13.1c-1.588,0-2.88-1.279-2.88-2.851c0-1.572,1.292-2.851,2.88-2.851s2.88,1.279,2.88,2.851 C6.874,11.821,5.582,13.1,3.994,13.1z M3.994,8.196c-1.148,0-2.082,0.921-2.082,2.053s0.934,2.053,2.082,2.053 c1.148,0,2.082-0.921,2.082-2.053S5.142,8.196,3.994,8.196z"/></g></g></g></g><g><g><g><g><path d="M6.869,19.182H1.116c-0.598,0-1.084-0.484-1.084-1.082V16.99c0-1.949,1.592-4.037,3.961-4.037 c2.371,0,3.963,2.087,3.963,4.037V18.1C7.957,18.697,7.469,19.182,6.869,19.182z M3.994,13.752 c-1.892,0-3.164,1.674-3.164,3.238V18.1c0,0.156,0.128,0.283,0.286,0.283h5.753c0.16,0,0.29-0.127,0.29-0.283V16.99 C7.159,15.426,5.887,13.752,3.994,13.752z"/></g></g></g></g><g><g><g><g><path d="M22.813,19.113H15.71c-0.636,0-1.152-0.512-1.152-1.141v-1.334c0-2.273,1.89-4.706,4.704-4.706 s4.706,2.432,4.706,4.706v1.334C23.968,18.602,23.449,19.113,22.813,19.113z M19.262,12.731c-2.337,0-3.906,2.019-3.906,3.908 v1.334c0,0.189,0.159,0.344,0.354,0.344h7.103c0.197,0,0.357-0.154,0.357-0.344v-1.334 C23.17,14.75,21.599,12.731,19.262,12.731z"/></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="news">
<path class="st0" d="M29,0H7C5.3,0,4,1.3,4,3v2H3C1.3,5,0,6.3,0,8v20c0,2.2,1.8,4,4,4h24c2.2,0,4-1.8,4-4V3C32,1.3,30.7,0,29,0z
M30,28c0,1.1-0.9,2-2,2H4c-1.1,0-2-0.9-2-2V8c0-0.6,0.4-1,1-1h1v20c0,0.6,0.4,1,1,1s1-0.4,1-1V3c0-0.6,0.4-1,1-1h22
c0.6,0,1,0.4,1,1V28z"/>
<path class="st0" d="M19.5,13h8c0.3,0,0.5-0.2,0.5-0.5S27.8,12,27.5,12h-8c-0.3,0-0.5,0.2-0.5,0.5S19.2,13,19.5,13z"/>
<path class="st0" d="M19.5,10h8c0.3,0,0.5-0.2,0.5-0.5S27.8,9,27.5,9h-8C19.2,9,19,9.2,19,9.5S19.2,10,19.5,10z"/>
<path class="st0" d="M19.5,7h8C27.8,7,28,6.8,28,6.5S27.8,6,27.5,6h-8C19.2,6,19,6.2,19,6.5S19.2,7,19.5,7z"/>
<path class="st0" d="M16.5,27h-8C8.2,27,8,27.2,8,27.5C8,27.8,8.2,28,8.5,28h8c0.3,0,0.5-0.2,0.5-0.5C17,27.2,16.8,27,16.5,27z"/>
<path class="st0" d="M16.5,24h-8C8.2,24,8,24.2,8,24.5C8,24.8,8.2,25,8.5,25h8c0.3,0,0.5-0.2,0.5-0.5C17,24.2,16.8,24,16.5,24z"/>
<path class="st0" d="M16.5,21h-8C8.2,21,8,21.2,8,21.5C8,21.8,8.2,22,8.5,22h8c0.3,0,0.5-0.2,0.5-0.5C17,21.2,16.8,21,16.5,21z"/>
<path class="st0" d="M27.5,27h-8c-0.3,0-0.5,0.2-0.5,0.5c0,0.3,0.2,0.5,0.5,0.5h8c0.3,0,0.5-0.2,0.5-0.5C28,27.2,27.8,27,27.5,27z"
/>
<path class="st0" d="M27.5,24h-8c-0.3,0-0.5,0.2-0.5,0.5c0,0.3,0.2,0.5,0.5,0.5h8c0.3,0,0.5-0.2,0.5-0.5C28,24.2,27.8,24,27.5,24z"
/>
<path class="st0" d="M27.5,21h-8c-0.3,0-0.5,0.2-0.5,0.5c0,0.3,0.2,0.5,0.5,0.5h8c0.3,0,0.5-0.2,0.5-0.5C28,21.2,27.8,21,27.5,21z"
/>
<path class="st0" d="M27.5,15h-19C8.2,15,8,15.2,8,15.5S8.2,16,8.5,16h19c0.3,0,0.5-0.2,0.5-0.5S27.8,15,27.5,15z"/>
<path class="st0" d="M27.5,18h-19C8.2,18,8,18.2,8,18.5C8,18.8,8.2,19,8.5,19h19c0.3,0,0.5-0.2,0.5-0.5C28,18.2,27.8,18,27.5,18z"
/>
<path class="st0" d="M9,13h7c0.6,0,1-0.4,1-1V5c0-0.6-0.4-1-1-1H9C8.4,4,8,4.5,8,5v7C8,12.6,8.4,13,9,13z M10,6h5v5h-5V6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
<style type="text/css">
.st0{fill:#000222;}
</style>
<g id="news">
<path class="st0" d="M29,0H7C5.3,0,4,1.3,4,3v2H3C1.3,5,0,6.3,0,8v20c0,2.2,1.8,4,4,4h24c2.2,0,4-1.8,4-4V3C32,1.3,30.7,0,29,0z
M30,28c0,1.1-0.9,2-2,2H4c-1.1,0-2-0.9-2-2V8c0-0.6,0.4-1,1-1h1v20c0,0.6,0.4,1,1,1s1-0.4,1-1V3c0-0.6,0.4-1,1-1h22
c0.6,0,1,0.4,1,1V28z"/>
<path class="st0" d="M19.5,13h8c0.3,0,0.5-0.2,0.5-0.5S27.8,12,27.5,12h-8c-0.3,0-0.5,0.2-0.5,0.5S19.2,13,19.5,13z"/>
<path class="st0" d="M19.5,10h8c0.3,0,0.5-0.2,0.5-0.5S27.8,9,27.5,9h-8C19.2,9,19,9.2,19,9.5S19.2,10,19.5,10z"/>
<path class="st0" d="M19.5,7h8C27.8,7,28,6.8,28,6.5S27.8,6,27.5,6h-8C19.2,6,19,6.2,19,6.5S19.2,7,19.5,7z"/>
<path class="st0" d="M16.5,27h-8C8.2,27,8,27.2,8,27.5S8.2,28,8.5,28h8c0.3,0,0.5-0.2,0.5-0.5S16.8,27,16.5,27z"/>
<path class="st0" d="M16.5,24h-8C8.2,24,8,24.2,8,24.5S8.2,25,8.5,25h8c0.3,0,0.5-0.2,0.5-0.5S16.8,24,16.5,24z"/>
<path class="st0" d="M16.5,21h-8C8.2,21,8,21.2,8,21.5S8.2,22,8.5,22h8c0.3,0,0.5-0.2,0.5-0.5S16.8,21,16.5,21z"/>
<path class="st0" d="M27.5,27h-8c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5h8c0.3,0,0.5-0.2,0.5-0.5S27.8,27,27.5,27z"/>
<path class="st0" d="M27.5,24h-8c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5h8c0.3,0,0.5-0.2,0.5-0.5S27.8,24,27.5,24z"/>
<path class="st0" d="M27.5,21h-8c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5h8c0.3,0,0.5-0.2,0.5-0.5S27.8,21,27.5,21z"/>
<path class="st0" d="M27.5,15h-19C8.2,15,8,15.2,8,15.5S8.2,16,8.5,16h19c0.3,0,0.5-0.2,0.5-0.5S27.8,15,27.5,15z"/>
<path class="st0" d="M27.5,18h-19C8.2,18,8,18.2,8,18.5S8.2,19,8.5,19h19c0.3,0,0.5-0.2,0.5-0.5S27.8,18,27.5,18z"/>
<path class="st0" d="M9,13h7c0.6,0,1-0.4,1-1V5c0-0.6-0.4-1-1-1H9C8.4,4,8,4.5,8,5v7C8,12.6,8.4,13,9,13z M10,6h5v5h-5V6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<g>
<g>
<g>
<g>
<path class="st0" d="M8.6,20.3L8.1,20c-1-0.6-2.2-1.2-3.9-1.2c-1,0-2.1,0.2-3.4,0.7L0,19.6l2.2-7.7l0.2-0.1
c0.2-0.1,0.4-0.1,0.5-0.2c0.8-0.3,1.9-0.6,3.2-0.7c1.2-0.1,2.4,0.2,3.7,0.9c0.3,0.1,0.5,0.3,0.7,0.4l0.3,0.2L8.6,20.3z
M4.2,17.8c1.7,0,2.9,0.5,3.9,1.1l1.8-6.2c-0.1-0.1-0.3-0.2-0.4-0.2c-1.1-0.6-2.2-0.8-3.2-0.8c-1.2,0.1-2.3,0.4-3,0.7
c-0.1,0-0.2,0.1-0.3,0.1l-1.6,5.8C2.3,18,3.3,17.8,4.2,17.8z"/>
</g>
</g>
</g>
</g>
<g>
<g>
<g>
<g>
<path class="st0" d="M14.5,22.7c-1.5,0-3.5-1-4.2-1.4L10,21.1l2.2-7.7l0.5,0.3c2,1.3,4.7,1.6,7.7,0.7l0.8-0.2l-2.1,7.5
l-0.2,0.1C17.2,22.4,15.8,22.7,14.5,22.7z M11,20.7c0.8,0.4,2.3,1.2,3.5,1.2c1.1,0,2.4-0.3,3.8-0.8l1.6-5.6
c-2.7,0.7-5.2,0.4-7.2-0.7L11,20.7z"/>
</g>
</g>
</g>
</g>
<g>
<g>
<g>
<g>
<path class="st0" d="M11.3,10.5l-0.5-0.3C9.9,9.6,8.7,9,7,9C6,9,4.8,9.2,3.5,9.7L2.7,10l2.2-7.7l0.2-0.1
c1.5-0.6,2.8-0.9,3.9-0.9c0,0,0,0,0,0c0.6,0,1.2,0.1,1.8,0.2c0.8,0.2,1.9,0.8,2.4,1.1l0.3,0.2L11.3,10.5z M7,8.2
c1.6,0,2.9,0.5,3.8,1.1l1.8-6.1c-0.5-0.3-1.3-0.7-1.9-0.8c-0.5-0.1-1-0.2-1.6-0.2c-1,0-2.2,0.3-3.4,0.8L4,8.6
C5.1,8.3,6.1,8.2,7,8.2z"/>
</g>
</g>
</g>
</g>
<g>
<g>
<g>
<g>
<path class="st0" d="M17.5,13.1L17.5,13.1c-1.6,0-3.1-0.5-4.5-1.3l-0.3-0.2L15,3.8l0.5,0.3c0.9,0.6,2.3,1.3,4.3,1.3
c1.1,0,2.3-0.2,3.5-0.7L24,4.4l-2.3,7.7l-0.2,0.1C20.2,12.8,18.8,13.1,17.5,13.1z M13.7,11.2c1.2,0.7,2.5,1,3.8,1l0,0
c1.1,0,2.3-0.3,3.5-0.8l1.7-5.7c-1,0.3-2,0.5-3,0.5c-1.8,0-3.3-0.5-4.2-1.1L13.7,11.2z"/>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<style type="text/css">
.st0{display:none;}
.st1{display:inline;fill:none;stroke:#000000;stroke-width:16;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;}
</style>
<g id="layer" class="st0">
<rect x="51" y="51" class="st1" width="188" height="188"/>
<rect x="273" y="51" class="st1" width="188" height="188"/>
<rect x="51" y="273" class="st1" width="188" height="188"/>
<rect x="273" y="273" class="st1" width="188" height="188"/>
</g>
<g id="layer_copy">
<g>
<path class="st2" d="M239,247H51c-4.4,0-8-3.6-8-8V51c0-4.4,3.6-8,8-8h188c4.4,0,8,3.6,8,8v188C247,243.4,243.4,247,239,247z
M59,231h172V59H59V231z"/>
</g>
<g>
<path class="st2" d="M461,247H273c-4.4,0-8-3.6-8-8V51c0-4.4,3.6-8,8-8h188c4.4,0,8,3.6,8,8v188C469,243.4,465.4,247,461,247z
M281,231h172V59H281V231z"/>
</g>
<g>
<path class="st2" d="M239,469H51c-4.4,0-8-3.6-8-8V273c0-4.4,3.6-8,8-8h188c4.4,0,8,3.6,8,8v188C247,465.4,243.4,469,239,469z
M59,453h172V281H59V453z"/>
</g>
<g>
<path class="st2" d="M461,469H273c-4.4,0-8-3.6-8-8V273c0-4.4,3.6-8,8-8h188c4.4,0,8,3.6,8,8v188C469,465.4,465.4,469,461,469z
M281,453h172V281H281V453z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 238 226" style="enable-background:new 0 0 238 226;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#FFFFFF;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
</style>
<g>
<rect x="7" y="8.3" class="st0" width="45.7" height="45.7"/>
<rect x="94.5" y="8.3" class="st0" width="45.7" height="45.7"/>
<rect x="182" y="8.3" class="st0" width="45.7" height="45.7"/>
<rect x="7" y="90.3" class="st0" width="45.7" height="45.7"/>
<rect x="94.5" y="90.3" class="st0" width="45.7" height="45.7"/>
<rect x="182" y="90.3" class="st0" width="45.7" height="45.7"/>
<rect x="7" y="172.3" class="st0" width="45.7" height="45.7"/>
<rect x="94.5" y="172.3" class="st0" width="45.7" height="45.7"/>
<rect x="182" y="172.3" class="st0" width="45.7" height="45.7"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg8" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 700 700"
style="enable-background:new 0 0 700 700;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:9.2604;stroke-linecap:round;stroke-linejoin:round;}
</style>
<g id="layer1" transform="translate(244.17261,202.68451)">
<g id="g2246" transform="translate(1239.703,-165.92092)">
<path id="path1419" class="st0" d="M-1466.2,645.6l664.6-664.6"/>
<path id="path1421" class="st0" d="M-1466.2-19.1l664.6,664.6"/>
</g>
<g id="g2400" transform="translate(-4.6466675)">
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg8" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 700 700"
style="enable-background:new 0 0 700 700;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#FFFFFF;stroke-width:9.2604;stroke-linecap:round;stroke-linejoin:round;}
</style>
<g id="layer1" transform="translate(244.17261,202.68451)">
<g id="g2246" transform="translate(1239.703,-165.92092)">
<path id="path1419" class="st0" d="M-1466.2,645.6l664.6-664.6"/>
<path id="path1421" class="st0" d="M-1466.2-19.1l664.6,664.6"/>
</g>
<g id="g2400" transform="translate(-4.6466675)">
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg8" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 700 700"
style="enable-background:new 0 0 700 700;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:9.2604;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:6.2;}
</style>
<g id="layer1" transform="translate(244.17261,202.68451)">
<g id="g2189" transform="translate(-468.25618,320.72341)">
<rect id="rect931" x="241.6" y="-422.8" class="st0" width="581.8" height="581.8"/>
<path id="path1745" class="st0" d="M324.7-422.8v-83.2l0,0h581.8V75.9h-83.2"/>
</g>
<g id="g2400" transform="translate(-4.6466675)">
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg8" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 700 700"
style="enable-background:new 0 0 700 700;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#FFFFFF;stroke-width:9.2604;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:6.2;}
</style>
<g id="layer1" transform="translate(244.17261,202.68451)">
<g id="g2189" transform="translate(-468.25618,320.72341)">
<rect id="rect931" x="241.6" y="-422.8" class="st0" width="581.8" height="581.8"/>
<path id="path1745" class="st0" d="M324.7-422.8v-83.2l0,0h581.8V75.9h-83.2"/>
</g>
<g id="g2400" transform="translate(-4.6466675)">
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg8" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 700 700"
style="enable-background:new 0 0 700 700;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:9.2604;stroke-linecap:round;stroke-linejoin:round;}
</style>
<g id="layer1" transform="translate(244.17261,202.68451)">
<path id="path1245-9" class="st0" d="M-226.4,147.3H438"/>
<g id="g2400" transform="translate(-4.6466675)">
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg8" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 700 700"
style="enable-background:new 0 0 700 700;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#FFFFFF;stroke-width:9.2604;stroke-linecap:round;stroke-linejoin:round;}
</style>
<g id="layer1" transform="translate(244.17261,202.68451)">
<path id="path1245-9" class="st0" d="M-226.4,147.3H438"/>
<g id="g2400" transform="translate(-4.6466675)">
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" ?><svg style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_1"><path d="M45.633,17.348C46.54,19.792,47,22.367,47,25c0,12.131-9.869,22-22,22c-6.595,0-12.795-2.963-16.958-8H15v-2H5v10h2v-6.126 C11.53,46.007,18.068,49,25,49c13.234,0,24-10.767,24-24c0-2.872-0.502-5.68-1.492-8.348L45.633,17.348z"/><path d="M35,13h10V3h-2v6.125C38.47,3.992,31.934,1,25,1C11.767,1,1,11.767,1,25c0,2.872,0.502,5.68,1.492,8.348l1.875-0.696 C3.46,30.208,3,27.633,3,25C3,12.869,12.869,3,25,3c6.598,0,12.795,2.962,16.958,8H35V13z"/></g><g/></svg>

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Some files were not shown because too many files have changed in this diff Show More