fix: polish analytics dashboard tables
Some checks failed
CI/CD Pipeline / Quality Gate (push) Failing after 2m21s
CI/CD Pipeline / Build and Deploy Dev via Compose (push) Has been skipped

This commit is contained in:
Gleb Korotkiy
2026-06-21 12:27:22 +03:00
parent f43f2055e3
commit da273308f9
3 changed files with 623 additions and 49 deletions

View File

@@ -0,0 +1,455 @@
# Backend Issues Competence Map Analytics Current Data
Назначение: список проблем текущей реализации backend-данных для страницы
`Все сервисы / Карта компетенций / Аналитическая панель`.
Документ нужен как задача для backend-разработки.
Цель: привести фактический response тестового backend-а к контракту из
`docs/info/backend-endpoints-competence-map-analytics-from-layouts.md` и `openapi.json`.
Тестовый endpoint:
```text
GET https://anal-back.dev.nii-ecos.ru/api/v1/competence-map/analytics/
```
Проблемы ниже относятся к page-level endpoint-у:
```text
GET /api/v1/competence-map/analytics/?association_type=all
GET /api/v1/competence-map/analytics/?association_type=sez
GET /api/v1/competence-map/analytics/?association_type=technoparks
GET /api/v1/competence-map/analytics/?association_type=clusters
```
Новые endpoint-ы для этих исправлений не нужны. Исправлять нужно наполнение существующего response.
---
## Общие требования к исправлению
- backend должен возвращать реальные бизнес-названия сущностей, а не технические placeholder-строки;
- frontend не должен подменять `Субъект РФ 11`, `Отрасль 24` и подобные значения локальными маппингами;
- если сущность имеет `id`, backend должен резолвить человекочитаемое имя на сервере;
- если данные по применимому блоку есть в источниках, backend должен вернуть строки блока, а не пустой массив;
- если показатель применим для выбранного `association_type`, backend должен вернуть его в контрактном поле;
- `0` является валидным числом и должен возвращаться как `0`, а не как `null`, пустая строка или отсутствие поля;
- пустой массив допустим только когда данных действительно нет после применения фильтров;
- `null` допустим только когда блок или показатель неприменим к выбранному состоянию страницы.
---
## Проблема 1. Placeholder-названия субъектов РФ и отраслей
### Где видно
На странице в блоках рейтинга и топа отраслей отображаются значения вида:
```text
Субъект РФ 11
Субъект РФ 59
Субъект РФ 65
Отрасль 24
Отрасль 127
Отрасль 31
```
Это не реальные названия субъектов РФ и отраслей. Такие значения нельзя отдавать в production response.
### Затронутые поля
`region_ranking.rows[]`:
```json
{
"rank": 1,
"region_id": "11",
"region_name": "Субъект РФ 11"
}
```
Должно быть:
```json
{
"rank": 1,
"region_id": "11",
"region_name": "<реальное название субъекта РФ из справочника>"
}
```
`top_industries.rows[]`:
```json
{
"industry_id": "24",
"industry_name": "Отрасль 24"
}
```
Должно быть:
```json
{
"industry_id": "24",
"industry_name": "<реальное название отрасли из справочника>"
}
```
### Требования
Для `region_ranking.rows[]`:
- `region_id` должен оставаться стабильным backend-id субъекта РФ;
- `region_name` должен быть реальным названием субъекта РФ;
- `region_name` не должен генерироваться как `Субъект РФ ${id}`;
- если субъект не найден в справочнике, это ошибка данных, которую нужно логировать на backend-е;
- frontend должен получать уже готовое отображаемое имя.
Для `top_industries.rows[]`:
- `industry_id` должен оставаться стабильным backend-id отрасли;
- `industry_name` должен быть реальным названием отрасли;
- `industry_name` не должен генерироваться как `Отрасль ${id}`;
- если отрасль не найдена в справочнике, это ошибка данных, которую нужно логировать на backend-е;
- frontend должен получать уже готовое отображаемое имя.
### Где исправлять
Исправление должно затронуть все состояния страницы:
```text
association_type=all
association_type=sez
association_type=technoparks
association_type=clusters
```
Поля:
- `region_ranking.rows[].region_name`;
- `top_industries.rows[].industry_name`;
- при необходимости тот же источник справочников для `/analytics/filters/`, если там используются те же
placeholder-значения в `regions[].label` или `industries[].label`.
### Acceptance criteria
Response не должен содержать placeholder-паттерны:
```text
region_ranking.rows[].region_name != "Субъект РФ <id>"
top_industries.rows[].industry_name != "Отрасль <id>"
```
Для каждого `association_type` нужно проверить:
```text
GET /api/v1/competence-map/analytics/?association_type=all
GET /api/v1/competence-map/analytics/?association_type=sez
GET /api/v1/competence-map/analytics/?association_type=technoparks
GET /api/v1/competence-map/analytics/?association_type=clusters
```
И убедиться, что:
- все строки `region_ranking.rows[]` имеют заполненный `region_name`;
- все строки `top_industries.rows[]` имеют заполненный `industry_name`;
- значения являются реальными названиями, а не техническими fallback-строками.
---
## Проблема 2. Не приходят строки таблицы приоритетных направлений ОЭЗ
### Где видно
В состоянии `association_type=sez` блок
`Типы ОЭЗ и приоритетные направления деятельности` отображает donut с типами ОЭЗ, но таблица
`Приоритетные направления деятельности ОЭЗ` пустая и показывает `Нет данных`.
При этом по контракту этот блок должен состоять из двух частей:
- `main_block.sez_types` - типы ОЭЗ;
- `main_block.priority_directions` - таблица приоритетных направлений ОЭЗ.
### Затронутый endpoint
```text
GET /api/v1/competence-map/analytics/?association_type=sez
```
### Затронутые поля
```json
{
"association_type": "sez",
"main_block": {
"type": "sez_types_and_priority_directions",
"sez_types": {},
"priority_directions": {
"title": "Приоритетные направления деятельности ОЭЗ",
"sort": {
"field": "sez_count",
"direction": "desc"
},
"ranking_method": "ordinal",
"rows": []
}
}
}
```
Сейчас `rows` фактически пустой или не содержит нужных данных.
### Требуемый формат
`main_block.priority_directions.rows[]` должен возвращать строки в формате:
```json
{
"rank": 1,
"direction_id": "ict",
"direction_name": "Информационно-коммуникационные технологии",
"sez_count": 4
}
```
Полный блок должен сохранять структуру:
```json
{
"priority_directions": {
"title": "Приоритетные направления деятельности ОЭЗ",
"sort": {
"field": "sez_count",
"direction": "desc"
},
"ranking_method": "ordinal",
"rows": [
{
"rank": 1,
"direction_id": "ict",
"direction_name": "Информационно-коммуникационные технологии",
"sez_count": 4
},
{
"rank": 2,
"direction_id": "aircraft-maintenance",
"direction_name": "Обслуживание и ремонт воздушных судов",
"sez_count": 3
}
]
}
}
```
### Требования
- `main_block.type` для `association_type=sez` должен быть `sez_types_and_priority_directions`;
- `main_block.priority_directions` должен присутствовать вместе с `main_block.sez_types`;
- `priority_directions.rows` должен быть массивом;
- строки должны быть отсортированы на backend-е согласно `sort`;
- `rank` должен быть рассчитан backend-ом и начинаться с `1`;
- `direction_id` должен быть стабильным id направления;
- `direction_name` должен быть реальным названием направления, а не placeholder;
- `sez_count` должен быть числом `>= 0`;
- пустой `rows: []` допустим только если после применения фильтров действительно нет ни одного направления.
### Acceptance criteria
Для запроса без дополнительных фильтров:
```text
GET /api/v1/competence-map/analytics/?association_type=sez
```
Ожидается:
- `main_block.priority_directions.rows.length > 0`;
- каждая строка имеет `rank`, `direction_id`, `direction_name`, `sez_count`;
- `direction_name` заполнен реальным названием;
- `sez_count` является числом;
- таблица на frontend-е перестает показывать `Нет данных`, если данные в источнике есть.
---
## Проблема 3. Не приходят KPI-карточки численности сотрудников для ОЭЗ и технопарков
### Где видно
На вкладках `ОЭЗ` и `Технопарки` есть блок `Численность сотрудников`.
Сейчас карточки отображаются с прочерками, потому что backend не возвращает данные в `employee_stats`.
Этот блок является серверным состоянием страницы: frontend не может рассчитать эти значения локально.
### Затронутые endpoint-ы
```text
GET /api/v1/competence-map/analytics/?association_type=sez
GET /api/v1/competence-map/analytics/?association_type=technoparks
```
### ОЭЗ. Требуемые поля
Для `association_type=sez` backend должен возвращать:
```json
{
"association_type": "sez",
"employee_stats": {
"items": [
{
"code": "opk_resident_employees_count",
"label": "Численность сотрудников организаций ОПК-резидентов ОЭЗ",
"value": 1678,
"value_type": "integer",
"numerator": 1678,
"denominator": null
},
{
"code": "sez_employees_count",
"label": "Численность сотрудников ОЭЗ",
"value": 5235,
"value_type": "integer",
"numerator": 5235,
"denominator": null
}
]
}
}
```
Обязательные метрики для ОЭЗ:
| `code` | Назначение | `value_type` |
|---|---|---|
| `opk_resident_employees_count` | численность сотрудников организаций ОПК-резидентов ОЭЗ | `integer` |
| `sez_employees_count` | общая численность сотрудников ОЭЗ | `integer` |
### Технопарки. Требуемые поля
Для `association_type=technoparks` backend должен возвращать:
```json
{
"association_type": "technoparks",
"employee_stats": {
"items": [
{
"code": "opk_resident_employees_count",
"label": "Численность сотрудников организаций ОПК-резидентов технопарков",
"value": 1678,
"value_type": "integer",
"numerator": 1678,
"denominator": null
},
{
"code": "opk_resident_employees_share_percent",
"label": "Доля сотрудников организаций ОПК-резидентов технопарков",
"value": 35.0,
"value_type": "percent",
"numerator": 1678,
"denominator": 4794
}
]
}
}
```
Обязательные метрики для технопарков:
| `code` | Назначение | `value_type` |
|---|---|---|
| `opk_resident_employees_count` | численность сотрудников организаций ОПК-резидентов технопарков | `integer` |
| `opk_resident_employees_share_percent` | доля сотрудников организаций ОПК-резидентов технопарков | `percent` |
### Требования
- `employee_stats` для `association_type=sez` должен быть объектом, а не `null`;
- `employee_stats` для `association_type=technoparks` должен быть объектом, а не `null`;
- `employee_stats.items` должен содержать все обязательные метрики для выбранного типа;
- `items[].code` должен быть стабильным;
- `items[].label` должен быть готовым отображаемым названием;
- `items[].value` должен быть числом;
- для процентных метрик `numerator` и `denominator` обязательны, если знаменатель известен;
- `denominator` у процентной метрики не должен быть `0`;
- `0` нужно возвращать как `0`, если показатель реально равен нулю;
- не возвращать `employee_stats: null` для применимого блока только потому, что расчет ещё не реализован.
### Когда `employee_stats` может быть `null`
`employee_stats: null` допустим для состояний, где блок сотрудников не применяется:
```text
association_type=all
association_type=clusters
```
Для `sez` и `technoparks` блок применим и должен быть заполнен.
### Acceptance criteria
Для запроса:
```text
GET /api/v1/competence-map/analytics/?association_type=sez
```
Ожидается:
- `employee_stats !== null`;
- `employee_stats.items.length >= 2`;
- есть item с `code = "opk_resident_employees_count"`;
- есть item с `code = "sez_employees_count"`;
- оба `value` являются числами.
Для запроса:
```text
GET /api/v1/competence-map/analytics/?association_type=technoparks
```
Ожидается:
- `employee_stats !== null`;
- `employee_stats.items.length >= 2`;
- есть item с `code = "opk_resident_employees_count"`;
- есть item с `code = "opk_resident_employees_share_percent"`;
- оба `value` являются числами;
- для `opk_resident_employees_share_percent` заполнены `numerator` и `denominator`.
---
## Сводка исправлений по backend response
| Проблема | Endpoint | Поля | Что нужно сделать |
|---|---|---|---|
| Placeholder субъектов РФ | `/analytics/` для всех `association_type` | `region_ranking.rows[].region_name` | возвращать реальные названия субъектов РФ |
| Placeholder отраслей | `/analytics/` для всех `association_type` | `top_industries.rows[].industry_name` | возвращать реальные названия отраслей |
| Пустая таблица направлений ОЭЗ | `/analytics/?association_type=sez` | `main_block.priority_directions.rows[]` | вернуть строки направлений ОЭЗ |
| Нет карточек сотрудников ОЭЗ | `/analytics/?association_type=sez` | `employee_stats.items[]` | вернуть две KPI-метрики сотрудников |
| Нет карточек сотрудников технопарков | `/analytics/?association_type=technoparks` | `employee_stats.items[]` | вернуть две KPI-метрики сотрудников |
---
## Минимальный backend checklist
Перед отдачей исправления нужно проверить:
```text
GET /api/v1/competence-map/analytics/?association_type=all
GET /api/v1/competence-map/analytics/?association_type=sez
GET /api/v1/competence-map/analytics/?association_type=technoparks
GET /api/v1/competence-map/analytics/?association_type=clusters
```
Проверки:
- `region_ranking.rows[].region_name` не содержит `Субъект РФ`;
- `top_industries.rows[].industry_name` не содержит `Отрасль `;
- для `association_type=sez` заполнен `main_block.priority_directions.rows`;
- для `association_type=sez` заполнен `employee_stats.items`;
- для `association_type=technoparks` заполнен `employee_stats.items`;
- все числовые поля возвращаются числами, а не строками;
- пустые массивы используются только когда данных действительно нет по выбранным фильтрам;
- response соответствует `openapi.json` и контракту
`docs/info/backend-endpoints-competence-map-analytics-from-layouts.md`.