fix: polish analytics dashboard tables
This commit is contained in:
@@ -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`.
|
||||
166
src/style.css
166
src/style.css
@@ -989,7 +989,8 @@ select {
|
||||
.analytics-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
color: var(--Infographic-Main-Dark-Blue, #022456);
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
@@ -997,11 +998,18 @@ select {
|
||||
|
||||
.analytics-table th,
|
||||
.analytics-table td {
|
||||
padding: 6px 7px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.analytics-table th {
|
||||
padding: 6px 7px;
|
||||
}
|
||||
|
||||
.analytics-table td {
|
||||
padding: 8px 7px;
|
||||
}
|
||||
|
||||
.analytics-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -1009,26 +1017,38 @@ select {
|
||||
background: var(--Infographic-Base-White, #ffffff);
|
||||
color: rgb(2 36 86 / 0.66);
|
||||
font-weight: 800;
|
||||
transition: background-color 160ms ease;
|
||||
}
|
||||
|
||||
.analytics-table th:hover {
|
||||
background: rgb(232 236 242 / 0.9);
|
||||
background: var(--Infographic-Base-White, #ffffff);
|
||||
}
|
||||
|
||||
.analytics-table tbody tr {
|
||||
.analytics-table tbody td {
|
||||
transition: background-color 160ms ease;
|
||||
}
|
||||
|
||||
.analytics-table tbody tr:hover {
|
||||
.analytics-table tbody tr:hover td {
|
||||
background: rgb(243 243 243 / 0.7);
|
||||
}
|
||||
|
||||
.analytics-table tbody tr:hover td:first-child {
|
||||
border-radius: 10px 0 0 10px;
|
||||
}
|
||||
|
||||
.analytics-table tbody tr:hover td:last-child {
|
||||
border-radius: 0 10px 10px 0;
|
||||
}
|
||||
|
||||
.analytics-table tbody tr.analytics-table-empty-row:hover,
|
||||
.ranking-table tbody tr.ranking-table-empty-row:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.analytics-table tbody tr.analytics-table-empty-row:hover td,
|
||||
.ranking-table tbody tr.ranking-table-empty-row:hover td {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.analytics-table th:first-child,
|
||||
.analytics-table td:first-child {
|
||||
width: 44px;
|
||||
@@ -1063,7 +1083,30 @@ select {
|
||||
|
||||
.sez-priority-table th:nth-child(3),
|
||||
.sez-priority-table td:nth-child(3) {
|
||||
width: 140px;
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.analytics-table-header-label {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 32px;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
padding: 6px 10px;
|
||||
transition: background-color 160ms ease;
|
||||
}
|
||||
|
||||
.analytics-table th:first-child .analytics-table-header-label {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.analytics-table th:last-child .analytics-table-header-label {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.analytics-table th:hover .analytics-table-header-label {
|
||||
background: rgb(232 236 242 / 0.9);
|
||||
}
|
||||
|
||||
.analytics-table-sort-button {
|
||||
@@ -1072,20 +1115,33 @@ select {
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border-radius: 10px;
|
||||
min-height: 32px;
|
||||
padding: 6px 10px;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-weight: inherit;
|
||||
text-align: right;
|
||||
transition: background-color 160ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.analytics-table th:hover .analytics-table-sort-button,
|
||||
.analytics-table-sort-button:hover,
|
||||
.analytics-table-sort-button:focus-visible {
|
||||
background: rgb(232 236 242 / 0.9);
|
||||
}
|
||||
|
||||
.analytics-table-sort-button span {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
overflow-wrap: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.analytics-table-sort-button svg {
|
||||
@@ -1093,7 +1149,7 @@ select {
|
||||
min-width: 15px;
|
||||
height: 15px;
|
||||
flex: 0 0 15px;
|
||||
color: rgb(2 36 86 / 0.28);
|
||||
color: rgb(2 36 86 / 0.42);
|
||||
stroke-width: 2.25;
|
||||
transform-origin: center;
|
||||
transition:
|
||||
@@ -1103,7 +1159,7 @@ select {
|
||||
|
||||
.analytics-table-sort-button:hover svg,
|
||||
.analytics-table-sort-button.is-active svg {
|
||||
color: rgb(2 36 86 / 0.62);
|
||||
color: rgb(2 36 86 / 0.68);
|
||||
}
|
||||
|
||||
.analytics-table-sort-button.is-desc svg {
|
||||
@@ -1173,9 +1229,10 @@ select {
|
||||
|
||||
.ranking-table {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-width: 560px;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
color: var(--Infographic-Main-Dark-Blue, #022456);
|
||||
font-size: 12.5px;
|
||||
line-height: 17px;
|
||||
@@ -1186,11 +1243,11 @@ select {
|
||||
}
|
||||
|
||||
.ranking-table__region-col {
|
||||
width: 45%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.ranking-table__metric-col {
|
||||
width: 16%;
|
||||
width: 136px;
|
||||
}
|
||||
|
||||
.analytics-dashboard-grid.is-clusters .ranking-table__region-col,
|
||||
@@ -1202,16 +1259,23 @@ select {
|
||||
.analytics-dashboard-grid.is-clusters .ranking-table__metric-col,
|
||||
.analytics-dashboard-grid.is-sez .ranking-table__metric-col,
|
||||
.analytics-dashboard-grid.is-technoparks .ranking-table__metric-col {
|
||||
width: 112px;
|
||||
width: 136px;
|
||||
}
|
||||
|
||||
.ranking-table th,
|
||||
.ranking-table td {
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ranking-table th {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.ranking-table td {
|
||||
padding: 8px 8px;
|
||||
}
|
||||
|
||||
.ranking-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -1219,11 +1283,10 @@ select {
|
||||
background: var(--Infographic-Base-White, #ffffff);
|
||||
color: var(--Infographic-Main-Dark-Blue, #022456);
|
||||
font-weight: 800;
|
||||
transition: background-color 160ms ease;
|
||||
}
|
||||
|
||||
.ranking-table th:hover {
|
||||
background: rgb(232 236 242 / 0.96);
|
||||
background: var(--Infographic-Base-White, #ffffff);
|
||||
}
|
||||
|
||||
.ranking-table th:first-child,
|
||||
@@ -1242,29 +1305,60 @@ select {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ranking-table-header-label {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 32px;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
padding: 6px 10px;
|
||||
transition: background-color 160ms ease;
|
||||
}
|
||||
|
||||
.ranking-table th:first-child .ranking-table-header-label {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ranking-table th:nth-child(n + 3) .ranking-table-header-label {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ranking-table th:hover .ranking-table-header-label {
|
||||
background: rgb(232 236 242 / 0.96);
|
||||
}
|
||||
|
||||
.ranking-table-sort-button {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border-radius: 10px;
|
||||
min-height: 32px;
|
||||
padding: 6px 10px;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-weight: inherit;
|
||||
text-align: right;
|
||||
transition: background-color 160ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ranking-table-sort-button span {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
.ranking-table th:hover .ranking-table-sort-button,
|
||||
.ranking-table-sort-button:hover,
|
||||
.ranking-table-sort-button:focus-visible {
|
||||
background: rgb(232 236 242 / 0.96);
|
||||
}
|
||||
|
||||
.ranking-table th:nth-child(n + 3) .ranking-table-sort-button span {
|
||||
.ranking-table-sort-button span {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
overflow-wrap: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1274,7 +1368,7 @@ select {
|
||||
min-width: 15px;
|
||||
height: 15px;
|
||||
flex: 0 0 15px;
|
||||
color: rgb(2 36 86 / 0.28);
|
||||
color: rgb(2 36 86 / 0.42);
|
||||
stroke-width: 2.25;
|
||||
transform-origin: center;
|
||||
transition:
|
||||
@@ -1284,17 +1378,29 @@ select {
|
||||
|
||||
.ranking-table-sort-button:hover svg,
|
||||
.ranking-table-sort-button.is-active svg {
|
||||
color: rgb(2 36 86 / 0.62);
|
||||
color: rgb(2 36 86 / 0.68);
|
||||
}
|
||||
|
||||
.ranking-table-sort-button.is-desc svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.ranking-table tbody tr:hover {
|
||||
.ranking-table tbody td {
|
||||
transition: background-color 160ms ease;
|
||||
}
|
||||
|
||||
.ranking-table tbody tr:hover td {
|
||||
background: rgb(243 243 243 / 0.7);
|
||||
}
|
||||
|
||||
.ranking-table tbody tr:hover td:first-child {
|
||||
border-radius: 10px 0 0 10px;
|
||||
}
|
||||
|
||||
.ranking-table tbody tr:hover td:last-child {
|
||||
border-radius: 0 10px 10px 0;
|
||||
}
|
||||
|
||||
.analytics-chart-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1446,7 +1552,7 @@ select {
|
||||
|
||||
.cluster-specializations-table th:nth-child(3),
|
||||
.cluster-specializations-table td:nth-child(3) {
|
||||
width: 170px;
|
||||
width: 220px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
@@ -206,13 +206,6 @@ const dashboardTabs = computed<DashboardTabOption[]>(() => [
|
||||
...option,
|
||||
kind: 'association' as const,
|
||||
})),
|
||||
{
|
||||
associations_count: null,
|
||||
disabled: false,
|
||||
id: REGION_RATING_TAB_ID,
|
||||
kind: 'region-rating',
|
||||
label: 'Рейтинг субъектов РФ',
|
||||
},
|
||||
])
|
||||
const isRegionRatingTabActive = computed(() => activeDashboardTab.value === REGION_RATING_TAB_ID)
|
||||
const isDashboardDataLoading = computed(() => dashboard.isAnalyticsFetching)
|
||||
@@ -979,8 +972,14 @@ const formatAreaUnit = (unit: string): string => (unit === 'ha' ? 'га' : unit)
|
||||
<table class="analytics-table sez-priority-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" aria-label="Номер">№</th>
|
||||
<th>Наименование направления деятельности</th>
|
||||
<th scope="col" aria-label="Номер">
|
||||
<span class="analytics-table-header-label">№</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="analytics-table-header-label">
|
||||
Наименование направления деятельности
|
||||
</span>
|
||||
</th>
|
||||
<th :aria-sort="getSortAria(sezPrioritySort, 'sez_count')">
|
||||
<button
|
||||
type="button"
|
||||
@@ -992,8 +991,8 @@ const formatAreaUnit = (unit: string): string => (unit === 'ha' ? 'га' : unit)
|
||||
aria-label="Сортировать приоритетные направления по количеству ОЭЗ"
|
||||
@click="updateMainTableSort('sez', 'sez_count')"
|
||||
>
|
||||
<ArrowUp aria-hidden="true" />
|
||||
<span>Количество ОЭЗ</span>
|
||||
<ArrowUp aria-hidden="true" />
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
@@ -1066,8 +1065,12 @@ const formatAreaUnit = (unit: string): string => (unit === 'ha' ? 'га' : unit)
|
||||
<table class="analytics-table technopark-area-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" aria-label="Номер">№</th>
|
||||
<th>Название технопарка</th>
|
||||
<th scope="col" aria-label="Номер">
|
||||
<span class="analytics-table-header-label">№</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="analytics-table-header-label">Название технопарка</span>
|
||||
</th>
|
||||
<th :aria-sort="getSortAria(technoparkAreaSort, 'area_value')">
|
||||
<button
|
||||
type="button"
|
||||
@@ -1079,8 +1082,8 @@ const formatAreaUnit = (unit: string): string => (unit === 'ha' ? 'га' : unit)
|
||||
aria-label="Сортировать технопарки по площади"
|
||||
@click="updateMainTableSort('technoparks', 'area_value')"
|
||||
>
|
||||
<ArrowUp aria-hidden="true" />
|
||||
<span>Площадь</span>
|
||||
<ArrowUp aria-hidden="true" />
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
@@ -1117,8 +1120,14 @@ const formatAreaUnit = (unit: string): string => (unit === 'ha' ? 'га' : unit)
|
||||
<table class="analytics-table cluster-specializations-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" aria-label="Номер">№</th>
|
||||
<th>Наименование специализации</th>
|
||||
<th scope="col" aria-label="Номер">
|
||||
<span class="analytics-table-header-label">№</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="analytics-table-header-label">
|
||||
Наименование специализации
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
:aria-sort="getSortAria(clusterSpecializationsSort, 'clusters_count')"
|
||||
>
|
||||
@@ -1138,8 +1147,8 @@ const formatAreaUnit = (unit: string): string => (unit === 'ha' ? 'га' : unit)
|
||||
aria-label="Сортировать специализации по количеству кластеров"
|
||||
@click="updateMainTableSort('clusters', 'clusters_count')"
|
||||
>
|
||||
<ArrowUp aria-hidden="true" />
|
||||
<span>Количество кластеров</span>
|
||||
<ArrowUp aria-hidden="true" />
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
@@ -1297,8 +1306,12 @@ const formatAreaUnit = (unit: string): string => (unit === 'ha' ? 'га' : unit)
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" aria-label="Номер">№</th>
|
||||
<th>Субъект РФ</th>
|
||||
<th scope="col" aria-label="Номер">
|
||||
<span class="ranking-table-header-label">№</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="ranking-table-header-label">Субъект РФ</span>
|
||||
</th>
|
||||
<th
|
||||
v-for="column in regionRankingColumns"
|
||||
:key="column.key"
|
||||
@@ -1314,8 +1327,8 @@ const formatAreaUnit = (unit: string): string => (unit === 'ha' ? 'га' : unit)
|
||||
:aria-label="`Сортировать рейтинг субъектов по колонке ${column.label}`"
|
||||
@click="updateRegionRankingSort(column.key)"
|
||||
>
|
||||
<ArrowUp aria-hidden="true" />
|
||||
<span>{{ column.label }}</span>
|
||||
<ArrowUp aria-hidden="true" />
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user