From eb110f677cfbaef8f0142f9b7e0be81c55801053 Mon Sep 17 00:00:00 2001 From: jze9 Date: Thu, 16 Jul 2026 14:11:06 +0500 Subject: [PATCH] =?UTF-8?q?feat(stats):=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=B0=D1=82=D1=8C=20=D0=B4=D0=B0=D1=88=D0=B1=D0=BE?= =?UTF-8?q?=D1=80=D0=B4=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B8=20=D1=81=20Flet=20=D0=BD=D0=B0=20React?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stats/ (Flet) генерировал графики через matplotlib в PNG и отдавал их через Flutter web с WebSocket — та же архитектура, что подводила основной фронтенд (см. известные ошибки в README). Плавающая версия flet>=0.82.2 недавно подтянула 0.86.0 с несовместимым протоколом фреймирования, и дашборд перестал открываться в браузере. stats-react/ — SPA по тому же паттерну, что и web-react (Vite+React+TS, свой Dockerfile+nginx, same-origin прокси /api вместо WebSocket). Функциональность 1:1: 4 summary-карточки, общие фильтры (тест/ организация/группа/год), два таба (прохождения / результаты тестов), 8 графиков, топ-10 таблица, фильтр по оси + сравнение по организациям/ группам. Круговая диаграмма заменена на 100%-stacked bar (dataviz: "part-to-whole rides on the stacked bar chart; donut stays deprioritized") с фолдингом длинного хвоста категорий в "Другое" — у оригинала было 32 категории на 8 цветов, из-за чего сегменты становились неразличимы. Категориальная палитра графиков провалидирована на CVD-безопасность (scripts/validate_palette.js, light+dark) — 8-цветная палитра из исходного designer.py не проходила проверку (hard fail по normal-vision floor). Бренд-оранжевый сохранён в палитре, остальные 7 слотов — из референсного набора skill'а. stats/ и Dockerfile.stats удалены, docker-compose.yml переключён на stats-react. Проверено сборкой всех трёх образов, полным compose up и визуально (headless Chrome, light+dark, обе вкладки, реальные данные с прод-сервера). Co-Authored-By: Claude Sonnet 5 --- .dockerignore | 2 +- Dockerfile.stats | 22 - README.md | 16 +- docker-compose.yml | 10 +- stats-react/.dockerignore | 6 + stats-react/.gitignore | 5 + stats-react/Dockerfile | 14 + stats-react/index.html | 13 + stats-react/nginx.conf | 36 + stats-react/package-lock.json | 1732 +++++++++++++++++ stats-react/package.json | 22 + .../assets => stats-react/public}/favicon.png | Bin stats-react/src/App.tsx | 342 ++++ stats-react/src/api/client.ts | 156 ++ stats-react/src/components/ChartCard.tsx | 24 + .../src/components/SearchableDropdown.tsx | 83 + stats-react/src/components/SummaryTile.tsx | 23 + stats-react/src/components/TopUsersTable.tsx | 33 + .../src/components/charts/BarChart.tsx | 115 ++ .../src/components/charts/ChartTooltip.tsx | 50 + .../src/components/charts/LineChart.tsx | 152 ++ .../src/components/charts/PartToWholeBar.tsx | 104 + stats-react/src/components/icons.tsx | 67 + stats-react/src/main.tsx | 10 + stats-react/src/styles/global.css | 478 +++++ stats-react/src/theme/palette.ts | 43 + stats-react/src/theme/useIsDark.ts | 18 + stats-react/tsconfig.json | 21 + stats-react/vite.config.ts | 18 + stats/api_client.py | 209 -- stats/assets/icons/IMG_4533.png | Bin 116920 -> 0 bytes stats/assets/icons/README | 3 - .../icons/clipboard-question_16542596.svg | 4 - .../icons/leaderboard-trophy_14227576.svg | 4 - stats/assets/icons/loading-animation.png | Bin 31068 -> 0 bytes stats/dashboard.py | 745 ------- stats/designer.py | 223 --- stats/main.py | 19 - 38 files changed, 3583 insertions(+), 1239 deletions(-) delete mode 100644 Dockerfile.stats create mode 100644 stats-react/.dockerignore create mode 100644 stats-react/.gitignore create mode 100644 stats-react/Dockerfile create mode 100644 stats-react/index.html create mode 100644 stats-react/nginx.conf create mode 100644 stats-react/package-lock.json create mode 100644 stats-react/package.json rename {stats/assets => stats-react/public}/favicon.png (100%) create mode 100644 stats-react/src/App.tsx create mode 100644 stats-react/src/api/client.ts create mode 100644 stats-react/src/components/ChartCard.tsx create mode 100644 stats-react/src/components/SearchableDropdown.tsx create mode 100644 stats-react/src/components/SummaryTile.tsx create mode 100644 stats-react/src/components/TopUsersTable.tsx create mode 100644 stats-react/src/components/charts/BarChart.tsx create mode 100644 stats-react/src/components/charts/ChartTooltip.tsx create mode 100644 stats-react/src/components/charts/LineChart.tsx create mode 100644 stats-react/src/components/charts/PartToWholeBar.tsx create mode 100644 stats-react/src/components/icons.tsx create mode 100644 stats-react/src/main.tsx create mode 100644 stats-react/src/styles/global.css create mode 100644 stats-react/src/theme/palette.ts create mode 100644 stats-react/src/theme/useIsDark.ts create mode 100644 stats-react/tsconfig.json create mode 100644 stats-react/vite.config.ts delete mode 100644 stats/api_client.py delete mode 100644 stats/assets/icons/IMG_4533.png delete mode 100644 stats/assets/icons/README delete mode 100644 stats/assets/icons/clipboard-question_16542596.svg delete mode 100644 stats/assets/icons/leaderboard-trophy_14227576.svg delete mode 100644 stats/assets/icons/loading-animation.png delete mode 100644 stats/dashboard.py delete mode 100644 stats/designer.py delete mode 100644 stats/main.py diff --git a/.dockerignore b/.dockerignore index dc8c194..b6317a9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,7 +2,7 @@ .venv data web-react -web +stats-react __pycache__ *.py[cod] .pytest_cache diff --git a/Dockerfile.stats b/Dockerfile.stats deleted file mode 100644 index 878c6d7..0000000 --- a/Dockerfile.stats +++ /dev/null @@ -1,22 +0,0 @@ -FROM python:3.14-slim - -WORKDIR /app - -# matplotlib нужен fontconfig для кириллицы -RUN apt-get update && apt-get install -y --no-install-recommends \ - fontconfig \ - fonts-dejavu-core \ - && rm -rf /var/lib/apt/lists/* - -RUN pip install --no-cache-dir \ - "flet==0.82.2" \ - "flet-web==0.82.2" \ - "uvicorn[standard]" \ - httpx \ - matplotlib - -COPY stats/ . - -EXPOSE 8081 - -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8081"] diff --git a/README.md b/README.md index 1575431..14f89bc 100644 --- a/README.md +++ b/README.md @@ -131,8 +131,7 @@ api-copp/ ├── .dockerignore # что не попадает в build context образов ├── .env.example # шаблон переменных окружения без секретов ├── Dockerfile.api # образ API -├── Dockerfile.stats # образ дашборда статистики -├── docker-compose.yml # api + web-react + stats (+ postgres/redis за профилем local-db) +├── docker-compose.yml # api + web-react + stats-react (+ postgres/redis за профилем local-db) │ ├── bd/ │ ├── __init__.py # Settings (pydantic), make_engine() @@ -175,7 +174,10 @@ api-copp/ │ ├── src/pages/ # страницы (Login, AnonymousEntry, Poll, MyTests, ...) │ └── nginx.conf # раздаёт статику SPA, проксирует /api → api:8000 │ -├── stats/ # отдельный дашборд статистики (Flet, порт 8081) +├── stats-react/ # дашборд статистики, React SPA (порт 8081, свой Dockerfile+nginx) +│ ├── src/api/client.ts # HTTP-клиент к /stats/*, /polls/, /organizations/, /groups/ +│ ├── src/components/charts/ # BarChart, LineChart, PartToWholeBar — свои SVG/HTML, без сторонних libs +│ └── src/App.tsx # фильтры + два таба (прохождения / результаты тестов) │ └── data/ # том Docker (./data:/app/data) ├── postgres/ # данные PostgreSQL @@ -620,3 +622,11 @@ ALTER TABLE radar_results ADD COLUMN IF NOT EXISTS image_path VARCHAR(512); **Было:** `docker-compose.yml` (api + web-react + stats) и `docker-compose_db.yml` (postgres + redis) — два отдельных файла и две команды на разворачивание. **Решение:** `postgres`/`redis` перенесены в тот же `docker-compose.yml`, за профилем `local-db` — не поднимаются командой `docker compose up -d` по умолчанию (приложение по умолчанию ходит на внешние `DB_HOST`/`REDIS_HOST` из `.env`), но доступны через `docker compose --profile local-db up -d`, когда нужна локальная БД. `docker-compose_db.yml` удалён. + +--- + +### 14. Дашборд статистики был на Flet — переписан на React ✅ + +**Проблема:** `stats/` (Flet, `Dockerfile.stats`) генерировал графики через matplotlib в PNG на сервере и отдавал их клиенту через Flutter web c долгоживущим WebSocket-соединением — та же архитектура, что подвела основной фронтенд (см. записи №1–4 выше). Плавающая версия `flet>=0.82.2` в какой-то момент подтянула мажорный `0.86.0` с несовместимым протоколом фреймирования, и дашборд перестал открываться в браузере. + +**Решение:** `stats/` и `Dockerfile.stats` удалены, дашборд переписан на React SPA (`stats-react/`, тот же паттерн, что и `web-react/`) — same-origin прокси `/api/*` вместо WebSocket, графики — свой SVG/HTML без сторонних библиотек (bar/line/part-to-whole с hover-тултипами), функциональность 1:1 с оригиналом (2 таба, 4+4 графика, топ-10 таблица, все фильтры). Категориальная палитра графиков провалидирована на CVD-безопасность (`node scripts/validate_palette.js`) — у исходной 8-цветной палитры из `designer.py` эта проверка проваливалась по нескольким пунктам. diff --git a/docker-compose.yml b/docker-compose.yml index 7aefe69..f7331bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,14 +37,12 @@ services: depends_on: - api restart: unless-stopped - stats: + stats-react: build: - context: . - dockerfile: Dockerfile.stats + context: ./stats-react + dockerfile: Dockerfile ports: - - "8081:8081" - environment: - - API_URL=http://api:8000 + - "8081:80" depends_on: - api restart: unless-stopped diff --git a/stats-react/.dockerignore b/stats-react/.dockerignore new file mode 100644 index 0000000..fe8741c --- /dev/null +++ b/stats-react/.dockerignore @@ -0,0 +1,6 @@ +node_modules +dist +.vite +*.local +Dockerfile +.dockerignore diff --git a/stats-react/.gitignore b/stats-react/.gitignore new file mode 100644 index 0000000..ec61b4b --- /dev/null +++ b/stats-react/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +*.local +.vite +*.tsbuildinfo diff --git a/stats-react/Dockerfile b/stats-react/Dockerfile new file mode 100644 index 0000000..ef580e5 --- /dev/null +++ b/stats-react/Dockerfile @@ -0,0 +1,14 @@ +# --- Стадия 1: сборка React-приложения --- +FROM node:20-alpine AS build +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install +COPY . . +RUN npm run build + +# --- Стадия 2: раздача статики через nginx + прокси /api --- +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/stats-react/index.html b/stats-react/index.html new file mode 100644 index 0000000..d117c13 --- /dev/null +++ b/stats-react/index.html @@ -0,0 +1,13 @@ + + + + + + + ЦОПП — Статистика + + +
+ + + diff --git a/stats-react/nginx.conf b/stats-react/nginx.conf new file mode 100644 index 0000000..54e9f4a --- /dev/null +++ b/stats-react/nginx.conf @@ -0,0 +1,36 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Статика SPA: всё неизвестное отдаём index.html (клиентский роутинг) + location / { + try_files $uri $uri/ /index.html; + } + + # Прокси на внутренний API-контейнер (браузер к api:8000 напрямую не ходит). + # Префикс /api срезается: /api/stats/summary -> http://api:8000/stats/summary + # + # Используем встроенный DNS Docker (127.0.0.11) и переменную в proxy_pass, + # чтобы nginx переразрешал имя `api` на каждый запрос. Иначе nginx кэширует + # IP при старте и ломается (502), если контейнер api перезапустился и сменил IP. + location /api/ { + resolver 127.0.0.11 valid=10s ipv6=off; + set $api_backend api; + rewrite ^/api/(.*)$ /$1 break; + proxy_pass http://$api_backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + } + + # Кэш статических ассетов с хэшем в имени + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/stats-react/package-lock.json b/stats-react/package-lock.json new file mode 100644 index 0000000..d38a09f --- /dev/null +++ b/stats-react/package-lock.json @@ -0,0 +1,1732 @@ +{ + "name": "copp-stats-react", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "copp-stats-react", + "version": "1.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.4", + "vite": "^5.4.6" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.392", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz", + "integrity": "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/stats-react/package.json b/stats-react/package.json new file mode 100644 index 0000000..2de875a --- /dev/null +++ b/stats-react/package.json @@ -0,0 +1,22 @@ +{ + "name": "copp-stats-react", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.4", + "vite": "^5.4.6" + } +} diff --git a/stats/assets/favicon.png b/stats-react/public/favicon.png similarity index 100% rename from stats/assets/favicon.png rename to stats-react/public/favicon.png diff --git a/stats-react/src/App.tsx b/stats-react/src/App.tsx new file mode 100644 index 0000000..cfd247a --- /dev/null +++ b/stats-react/src/App.tsx @@ -0,0 +1,342 @@ +import { useEffect, useState } from "react"; +import * as api from "./api/client"; +import type { AvgDatum, ChartDatum, Filters, TopUserRow } from "./api/client"; +import { SearchableDropdown, type Option } from "./components/SearchableDropdown"; +import { ChartCard } from "./components/ChartCard"; +import { SummaryTile } from "./components/SummaryTile"; +import { TopUsersTable } from "./components/TopUsersTable"; +import { BarChart } from "./components/charts/BarChart"; +import { LineChart } from "./components/charts/LineChart"; +import { PartToWholeBar } from "./components/charts/PartToWholeBar"; +import { useIsDark } from "./theme/useIsDark"; +import { categoricalColor } from "./theme/palette"; +import { + IconAssignment, + IconBarChart, + IconBusiness, + IconFilterOff, + IconInsights, + IconPlayCircle, + IconQuiz, + IconRefresh, +} from "./components/icons"; + +function toValueData(data: ChartDatum[]): { label: string; value: number }[] { + return data.map((d) => ({ label: d.label, value: d.count })); +} + +const COMPARE_OPTIONS: Option[] = [ + { key: "organization", text: "Организациям" }, + { key: "group", text: "Группам" }, +]; + +export default function App() { + const dark = useIsDark(); + + // ── справочники для фильтров ────────────────────────────────────────── + const [pollOptions, setPollOptions] = useState([]); + const [orgOptions, setOrgOptions] = useState([]); + const [groupOptions, setGroupOptions] = useState([]); + const [yearOptions, setYearOptions] = useState([]); + const [dimensionOptions, setDimensionOptions] = useState([]); + + // ── общие фильтры ──────────────────────────────────────────────────── + const [pollId, setPollId] = useState(null); + const [orgId, setOrgId] = useState(null); + const [groupId, setGroupId] = useState(null); + const [year, setYear] = useState(null); + + // ── фильтры таба 2 ─────────────────────────────────────────────────── + const [dimension, setDimension] = useState(null); + const [compareBy, setCompareBy] = useState("organization"); + + const [tab, setTab] = useState<0 | 1>(0); + + // ── summary ────────────────────────────────────────────────────────── + const [summary, setSummary] = useState({ + total_responses: 0, + total_polls: 0, + active_polls: 0, + orgs_active: 0, + }); + + // ── tab1 данные ────────────────────────────────────────────────────── + const [tab1Loading, setTab1Loading] = useState(true); + const [pollData, setPollData] = useState([]); + const [orgData, setOrgData] = useState([]); + const [groupData, setGroupData] = useState([]); + const [monthData, setMonthData] = useState([]); + + // ── tab2 данные ────────────────────────────────────────────────────── + const [tab2Loading, setTab2Loading] = useState(true); + const [avgDimData, setAvgDimData] = useState([]); + const [leadingData, setLeadingData] = useState([]); + const [compareData, setCompareData] = useState([]); + const [trendData, setTrendData] = useState([]); + const [topUsers, setTopUsers] = useState([]); + + const filters = (): Filters => ({ + poll_id: pollId, + org_id: orgId, + group_id: groupId, + year: year ? parseInt(year, 10) : null, + }); + + async function refreshTab1() { + setTab1Loading(true); + const f = filters(); + const [p, o, g, m] = await Promise.all([ + api.getResponsesGrouped("poll", f), + api.getResponsesGrouped("organization", f), + api.getResponsesGrouped("group", f), + api.getResponsesGrouped("month", f), + ]); + setPollData(p); + setOrgData(o); + setGroupData(g); + setMonthData(m); + setTab1Loading(false); + } + + async function refreshTab2() { + setTab2Loading(true); + const f = filters(); + const dim = dimension; + const [avg, lead] = await Promise.all([api.getAvgByDimension(f), api.getLeadingType(f)]); + let top: TopUserRow[] = []; + let compare: AvgDatum[] = []; + let trend: AvgDatum[] = []; + if (dim) { + [top, compare, trend] = await Promise.all([ + api.getTopUsers(dim, f), + api.getCompareGroups(dim, compareBy as api.CompareBy, f.poll_id, f.year), + api.getDimensionTrend(dim, f), + ]); + } + setAvgDimData(avg); + setLeadingData(lead); + setTopUsers(top); + setCompareData(compare); + setTrendData(trend); + setTab2Loading(false); + } + + function refreshCurrentTab() { + if (tab === 0) void refreshTab1(); + else void refreshTab2(); + } + + // ── первичная загрузка ─────────────────────────────────────────────── + useEffect(() => { + (async () => { + const [polls, orgs, groups, years, summaryData, dims] = await Promise.all([ + api.getPolls(), + api.getOrganizations(), + api.getGroups(), + api.getYears(), + api.getSummary(), + api.getDimensions(), + ]); + setPollOptions(polls.map((p) => ({ key: p.id, text: p.title }))); + setOrgOptions(orgs.map((o) => ({ key: o.id, text: o.name_organization }))); + setGroupOptions(groups.map((g) => ({ key: g.id, text: g.name_group }))); + setYearOptions(years.map((y) => ({ key: String(y), text: String(y) }))); + setDimensionOptions(dims.map((d) => ({ key: d, text: d }))); + setSummary(summaryData); + if (dims.length > 0) setDimension(dims[0]); + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Первый прогон обоих табов, как только известна выбранная ось (dimension). + const [initialized, setInitialized] = useState(false); + useEffect(() => { + if (initialized || dimensionOptions.length === 0) return; + setInitialized(true); + void refreshTab1(); + void refreshTab2(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dimensionOptions]); + + const onGeneralFilterChange = () => { + if (!initialized) return; + refreshCurrentTab(); + }; + + const onResultsFilterChange = () => { + if (!initialized) return; + void refreshTab2(); + }; + + const resetFilters = () => { + setPollId(null); + setOrgId(null); + setGroupId(null); + setYear(null); + setTimeout(refreshCurrentTab, 0); + }; + + const dimColor = (i: number) => categoricalColor(i, dark); + + return ( +
+
+ ЦОПП + + Статистика прохождений +
+ +
+
+ } color="var(--primary)" /> + } color="var(--info)" /> + } color="var(--success)" /> + } color="var(--accent)" /> +
+ +
+ Фильтры +
+ { + setPollId(v); + setTimeout(onGeneralFilterChange, 0); + }} + /> + { + setOrgId(v); + setTimeout(onGeneralFilterChange, 0); + }} + /> + { + setGroupId(v); + setTimeout(onGeneralFilterChange, 0); + }} + /> + { + setYear(v); + setTimeout(onGeneralFilterChange, 0); + }} + /> +
+
+ + +
+
+ +
+ + +
+ + {tab === 0 ? ( +
+ + "var(--primary)"} /> + + + "var(--info)"} /> + + + "var(--success)"} /> + + + String(v)} /> + +
+ ) : ( +
+
+ Фильтр по оси/типу +
+ { + setDimension(v); + setTimeout(onResultsFilterChange, 0); + }} + /> + { + setCompareBy(v ?? "organization"); + setTimeout(onResultsFilterChange, 0); + }} + /> +
+
+ + + ({ label: d.label, value: d.avg }))} + colorFor={dimColor} + formatValue={(v) => v.toFixed(1)} + /> + + + + ({ label: d.label, count: d.count }))} dark={dark} /> + + + + ({ label: d.label, value: d.avg }))} + colorFor={dimColor} + formatValue={(v) => v.toFixed(1)} + /> + + + + ({ label: d.label, value: d.avg }))} formatValue={(v) => v.toFixed(1)} /> + + +
+
Топ-10 участников по выбранной оси
+ {tab2Loading ? ( +
+
+
+ ) : ( + + )} +
+
+ )} +
+
+ ); +} diff --git a/stats-react/src/api/client.ts b/stats-react/src/api/client.ts new file mode 100644 index 0000000..bff8534 --- /dev/null +++ b/stats-react/src/api/client.ts @@ -0,0 +1,156 @@ +// Единое место для всех HTTP-запросов к бэкенду. +// Порт stats/api_client.py. Базовый путь /api проксируется nginx (прод) или Vite (dev) на api:8000. +// Все эндпоинты публичные — дашборд не требует авторизации (как и раньше). + +const BASE = "/api"; + +async function getJson(path: string): Promise { + try { + const resp = await fetch(BASE + path); + if (resp.status !== 200) return null; + return (await resp.json()) as T; + } catch { + return null; + } +} + +function qs(params: Record): string { + const usp = new URLSearchParams(); + for (const [k, v] of Object.entries(params)) { + if (v !== null && v !== undefined && v !== "") usp.set(k, String(v)); + } + const s = usp.toString(); + return s ? `?${s}` : ""; +} + +export interface Summary { + total_responses: number; + total_polls: number; + active_polls: number; + orgs_active: number; +} + +export interface Poll { + id: string; + title: string; +} + +export interface Organization { + id: string; + name_organization: string; +} + +export interface Group { + id: string; + name_group: string; +} + +export interface ChartDatum { + label: string; + count: number; +} + +export interface AvgDatum { + label: string; + avg: number; + count?: number; +} + +export interface TopUserRow { + name: string; + poll: string; + value: number; +} + +export type GroupBy = "poll" | "organization" | "group" | "month"; +export type CompareBy = "organization" | "group"; + +export interface Filters { + poll_id?: string | null; + org_id?: string | null; + group_id?: string | null; + year?: number | null; +} + +export async function getSummary(): Promise { + return ( + (await getJson("/stats/summary")) ?? { + total_responses: 0, + total_polls: 0, + active_polls: 0, + orgs_active: 0, + } + ); +} + +export async function getYears(): Promise { + return (await getJson("/stats/years")) ?? []; +} + +export async function getPolls(): Promise { + return (await getJson("/polls/")) ?? []; +} + +export async function getOrganizations(): Promise { + return (await getJson("/organizations/")) ?? []; +} + +export async function getGroups(): Promise { + return (await getJson("/groups/")) ?? []; +} + +export async function getResponsesGrouped(groupBy: GroupBy, f: Filters): Promise { + const query = qs({ group_by: groupBy, poll_id: f.poll_id, org_id: f.org_id, group_id: f.group_id, year: f.year }); + return (await getJson(`/stats/responses${query}`)) ?? []; +} + +export async function getDimensions(pollId?: string | null): Promise { + const query = qs({ poll_id: pollId }); + return (await getJson(`/stats/results/dimensions${query}`)) ?? []; +} + +export async function getAvgByDimension(f: Filters): Promise { + const query = qs({ poll_id: f.poll_id, org_id: f.org_id, group_id: f.group_id, year: f.year }); + return (await getJson(`/stats/results/avg-by-dimension${query}`)) ?? []; +} + +export async function getLeadingType(f: Filters): Promise { + const query = qs({ poll_id: f.poll_id, org_id: f.org_id, group_id: f.group_id, year: f.year }); + return (await getJson(`/stats/results/leading-type${query}`)) ?? []; +} + +export async function getCompareGroups( + dimensionName: string, + by: CompareBy, + pollId?: string | null, + year?: number | null, +): Promise { + const query = qs({ dimension_name: dimensionName, by, poll_id: pollId, year }); + return (await getJson(`/stats/results/compare-groups${query}`)) ?? []; +} + +export async function getTopUsers( + dimensionName: string, + f: Filters, + limit = 10, +): Promise { + const query = qs({ + dimension_name: dimensionName, + poll_id: f.poll_id, + org_id: f.org_id, + group_id: f.group_id, + year: f.year, + limit, + }); + return (await getJson(`/stats/results/top-users${query}`)) ?? []; +} + +export async function getDimensionTrend(dimensionName: string, f: Filters): Promise { + const query = qs({ + dimension_name: dimensionName, + poll_id: f.poll_id, + org_id: f.org_id, + group_id: f.group_id, + }); + return (await getJson(`/stats/results/dimension-trend${query}`)) ?? []; +} diff --git a/stats-react/src/components/ChartCard.tsx b/stats-react/src/components/ChartCard.tsx new file mode 100644 index 0000000..408074a --- /dev/null +++ b/stats-react/src/components/ChartCard.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from "react"; + +export function ChartCard({ + title, + loading, + children, +}: { + title: string; + loading?: boolean; + children: ReactNode; +}) { + return ( +
+
{title}
+ {loading ? ( +
+
+
+ ) : ( + children + )} +
+ ); +} diff --git a/stats-react/src/components/SearchableDropdown.tsx b/stats-react/src/components/SearchableDropdown.tsx new file mode 100644 index 0000000..8a01705 --- /dev/null +++ b/stats-react/src/components/SearchableDropdown.tsx @@ -0,0 +1,83 @@ +import { useMemo, useState } from "react"; +import { IconCaretDown } from "./icons"; + +export interface Option { + key: string; + text: string; +} + +// Выпадающий список с поиском — порт web-react/src/components/SearchableDropdown.tsx +// (изначально web/designer.py SearchableDropdown из Flet-версии). +export function SearchableDropdown({ + label, + hint, + options, + value, + onChange, +}: { + label?: string; + hint?: string; + options: Option[]; + value: string | null; + onChange: (key: string | null) => void; +}) { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + + const selected = useMemo(() => options.find((o) => o.key === value) ?? null, [options, value]); + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) return options; + return options.filter((o) => o.text.toLowerCase().includes(q)); + }, [options, query]); + + return ( +
+ {label && {label}} +
{ + setQuery(""); + setOpen(true); + }} + > + + {selected ? selected.text : hint ?? "Выберите..."} + + +
+ + {open && ( +
setOpen(false)}> +
e.stopPropagation()}> +
{label}
+ setQuery(e.target.value)} + /> +
+
+ {filtered.length === 0 &&
Ничего не найдено
} + {filtered.map((o) => ( +
{ + onChange(o.key); + setOpen(false); + }} + > + {o.text} +
+ ))} +
+
+
+ )} +
+ ); +} diff --git a/stats-react/src/components/SummaryTile.tsx b/stats-react/src/components/SummaryTile.tsx new file mode 100644 index 0000000..175215d --- /dev/null +++ b/stats-react/src/components/SummaryTile.tsx @@ -0,0 +1,23 @@ +import type { ReactNode } from "react"; + +export function SummaryTile({ + title, + value, + icon, + color, +}: { + title: string; + value: number | string; + icon: ReactNode; + color: string; +}) { + return ( +
+
+ {icon} + {title} +
+
{value}
+
+ ); +} diff --git a/stats-react/src/components/TopUsersTable.tsx b/stats-react/src/components/TopUsersTable.tsx new file mode 100644 index 0000000..5c4da28 --- /dev/null +++ b/stats-react/src/components/TopUsersTable.tsx @@ -0,0 +1,33 @@ +import type { TopUserRow } from "../api/client"; + +export function TopUsersTable({ rows }: { rows: TopUserRow[] }) { + if (rows.length === 0) { + return
Нет данных
; + } + return ( +
+ + + + + + + + + + {rows.map((r, i) => ( + + + + + + ))} + +
УчастникТестБалл
{r.name} + {r.poll} + + {r.value} +
+
+ ); +} diff --git a/stats-react/src/components/charts/BarChart.tsx b/stats-react/src/components/charts/BarChart.tsx new file mode 100644 index 0000000..c864401 --- /dev/null +++ b/stats-react/src/components/charts/BarChart.tsx @@ -0,0 +1,115 @@ +import { useState } from "react"; +import { ChartTooltip, type TooltipState } from "./ChartTooltip"; + +export interface BarDatum { + label: string; + value: number; +} + +// Горизонтальный bar chart: ≤24px толщина бара, 4px скруглённый конец, +// подпись значения у края, hover-тултип на каждом баре (сам бар — hit target). +export function BarChart({ + data, + colorFor, + formatValue = (v) => String(v), + xLabel, +}: { + data: BarDatum[]; + colorFor: (index: number) => string; + formatValue?: (v: number) => string; + xLabel?: string; +}) { + const [tooltip, setTooltip] = useState(null); + + if (data.length === 0) { + return
Нет данных
; + } + + const max = Math.max(...data.map((d) => d.value), 1); + + return ( +
+
+ {data.map((d, i) => { + const pct = Math.max((d.value / max) * 100, 2); + const color = colorFor(i); + return ( +
{ + const rect = e.currentTarget.getBoundingClientRect(); + const parent = e.currentTarget.parentElement!.parentElement!.getBoundingClientRect(); + setTooltip({ + x: rect.left - parent.left + rect.width * 0.3, + y: rect.top - parent.top, + label: d.label, + value: formatValue(d.value), + swatch: color, + }); + }} + onMouseLeave={() => setTooltip(null)} + onFocus={() => + setTooltip({ x: 0, y: 0, label: d.label, value: formatValue(d.value), swatch: color }) + } + onBlur={() => setTooltip(null)} + tabIndex={0} + > + + {d.label} + +
+
+
+ + {formatValue(d.value)} + +
+ ); + })} +
+ {xLabel && ( +
+ {xLabel} +
+ )} + +
+ ); +} diff --git a/stats-react/src/components/charts/ChartTooltip.tsx b/stats-react/src/components/charts/ChartTooltip.tsx new file mode 100644 index 0000000..09b84ec --- /dev/null +++ b/stats-react/src/components/charts/ChartTooltip.tsx @@ -0,0 +1,50 @@ +// Плавающий тултип, привязанный к позиции курсора внутри контейнера графика. +// Общий для всех типов графиков — см. dataviz: "значение — сильный элемент, подпись — вторичная". +export interface TooltipState { + x: number; + y: number; + label: string; + value: string; + swatch?: string; +} + +export function ChartTooltip({ tooltip }: { tooltip: TooltipState | null }) { + if (!tooltip) return null; + return ( +
+ {tooltip.swatch && ( + + )} + {tooltip.label} + {tooltip.value} +
+ ); +} diff --git a/stats-react/src/components/charts/LineChart.tsx b/stats-react/src/components/charts/LineChart.tsx new file mode 100644 index 0000000..25b4f4d --- /dev/null +++ b/stats-react/src/components/charts/LineChart.tsx @@ -0,0 +1,152 @@ +import { useRef, useState } from "react"; +import { ChartTooltip, type TooltipState } from "./ChartTooltip"; + +export interface LineDatum { + label: string; + value: number; +} + +const W = 1000; +const H = 300; +const PAD_L = 44; +const PAD_R = 16; +const PAD_T = 16; +const PAD_B = 48; + +// Линейный график для одного ряда: 2px линия, area fill ~10%, маркеры ≥8px +// с 2px кольцом в цвет поверхности, вертикальный crosshair с тултипом, +// прямая подпись значения на последней точке. +export function LineChart({ + data, + color = "var(--primary)", + formatValue = (v) => String(v), +}: { + data: LineDatum[]; + color?: string; + formatValue?: (v: number) => string; +}) { + const [hoverIdx, setHoverIdx] = useState(null); + const [tooltip, setTooltip] = useState(null); + const svgRef = useRef(null); + + if (data.length === 0) { + return
Нет данных
; + } + + const max = Math.max(...data.map((d) => d.value), 1); + const plotW = W - PAD_L - PAD_R; + const plotH = H - PAD_T - PAD_B; + const n = data.length; + + const xAt = (i: number) => PAD_L + (n === 1 ? plotW / 2 : (i / (n - 1)) * plotW); + const yAt = (v: number) => PAD_T + plotH - (v / max) * plotH; + + const points = data.map((d, i) => ({ x: xAt(i), y: yAt(d.value), ...d })); + const linePath = points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" "); + const areaPath = `${linePath} L ${points[points.length - 1].x} ${PAD_T + plotH} L ${points[0].x} ${PAD_T + plotH} Z`; + + const gridLines = [0, 0.25, 0.5, 0.75, 1]; + + const handleMove = (e: React.PointerEvent) => { + const svg = svgRef.current; + if (!svg) return; + const rect = svg.getBoundingClientRect(); + const relX = ((e.clientX - rect.left) / rect.width) * W; + let nearest = 0; + let best = Infinity; + points.forEach((p, i) => { + const d = Math.abs(p.x - relX); + if (d < best) { + best = d; + nearest = i; + } + }); + setHoverIdx(nearest); + const p = points[nearest]; + setTooltip({ + x: (p.x / W) * rect.width, + y: (p.y / H) * rect.height, + label: p.label, + value: formatValue(p.value), + swatch: color, + }); + }; + + const last = points[points.length - 1]; + + return ( +
+ { + setHoverIdx(null); + setTooltip(null); + }} + > + {/* gridlines */} + {gridLines.map((g) => { + const y = PAD_T + plotH * (1 - g); + return ( + + + + {Math.round(max * g)} + + + ); + })} + + {/* area fill */} + + + {/* line */} + + + {/* crosshair */} + {hoverIdx !== null && ( + + )} + + {/* markers */} + {points.map((p, i) => ( + + ))} + + {/* end label */} + + {formatValue(last.value)} + + + {/* x labels */} + {points.map((p, i) => { + const anchor = i === 0 ? "start" : i === points.length - 1 ? "end" : "middle"; + return ( + + {p.label} + + ); + })} + + +
+ ); +} diff --git a/stats-react/src/components/charts/PartToWholeBar.tsx b/stats-react/src/components/charts/PartToWholeBar.tsx new file mode 100644 index 0000000..a271d93 --- /dev/null +++ b/stats-react/src/components/charts/PartToWholeBar.tsx @@ -0,0 +1,104 @@ +import { useState } from "react"; +import { ChartTooltip, type TooltipState } from "./ChartTooltip"; +import { categoricalColor } from "../../theme/palette"; + +export interface ShareDatum { + label: string; + count: number; +} + +// Part-to-whole как один горизонтальный 100%-stacked bar вместо pie chart — +// dataviz: "part-to-whole rides on the stacked bar chart; donut stays deprioritized". +// Легенда обязательна (>=2 сегментов), т.к. на самих сегментах подписи не всегда влезают. +// Токен-потолок категориальной палитры — 8 слотов (см. dataviz choosing-a-form: +// "series 7-8: token ceiling; past it, fold the tail into Other"). Данные уже +// отсортированы бэкендом по убыванию — берём топ-7 и сворачиваем остаток. +const MAX_SLOTS = 7; +const OTHER_COLOR_LIGHT = "#898781"; +const OTHER_COLOR_DARK = "#a7b2c0"; + +function foldTail(data: ShareDatum[]): ShareDatum[] { + if (data.length <= MAX_SLOTS + 1) return data; + const head = data.slice(0, MAX_SLOTS); + const tail = data.slice(MAX_SLOTS); + const otherCount = tail.reduce((s, d) => s + d.count, 0); + return [...head, { label: `Другое (${tail.length})`, count: otherCount }]; +} + +export function PartToWholeBar({ data, dark }: { data: ShareDatum[]; dark: boolean }) { + const [tooltip, setTooltip] = useState(null); + + if (data.length === 0) { + return
Нет данных
; + } + + const folded = foldTail(data); + const wasFolded = folded.length < data.length; + const isOther = (i: number) => wasFolded && i === folded.length - 1; + const total = folded.reduce((s, d) => s + d.count, 0) || 1; + + return ( +
+
+ {folded.map((d, i) => { + const pct = (d.count / total) * 100; + const color = isOther(i) ? (dark ? OTHER_COLOR_DARK : OTHER_COLOR_LIGHT) : categoricalColor(i, dark); + return ( +
{ + const rect = e.currentTarget.getBoundingClientRect(); + const parent = e.currentTarget.parentElement!.parentElement!.getBoundingClientRect(); + setTooltip({ + x: rect.left - parent.left + rect.width / 2, + y: rect.top - parent.top, + label: d.label, + value: `${d.count} (${pct.toFixed(1)}%)`, + swatch: color, + }); + }} + onMouseLeave={() => setTooltip(null)} + onFocus={() => + setTooltip({ x: 0, y: 0, label: d.label, value: `${d.count} (${pct.toFixed(1)}%)`, swatch: color }) + } + onBlur={() => setTooltip(null)} + /> + ); + })} +
+ +
+ {folded.map((d, i) => { + const pct = (d.count / total) * 100; + const color = isOther(i) ? (dark ? OTHER_COLOR_DARK : OTHER_COLOR_LIGHT) : categoricalColor(i, dark); + return ( + + + {d.label} — {d.count} ({pct.toFixed(1)}%) + + ); + })} +
+ + +
+ ); +} diff --git a/stats-react/src/components/icons.tsx b/stats-react/src/components/icons.tsx new file mode 100644 index 0000000..8bd3813 --- /dev/null +++ b/stats-react/src/components/icons.tsx @@ -0,0 +1,67 @@ +// Небольшой набор inline-SVG иконок (замена Material Icons из Flet-версии). +import type { SVGProps } from "react"; + +type P = SVGProps; +const base = (p: P) => ({ + width: "1em", + height: "1em", + viewBox: "0 0 24 24", + fill: "currentColor", + ...p, +}); + +export const IconAssignment = (p: P) => ( + + + +); + +export const IconQuiz = (p: P) => ( + + + +); + +export const IconPlayCircle = (p: P) => ( + + + +); + +export const IconBusiness = (p: P) => ( + + + +); + +export const IconFilterOff = (p: P) => ( + + + +); + +export const IconRefresh = (p: P) => ( + + + + +); + +export const IconBarChart = (p: P) => ( + + + +); + +export const IconInsights = (p: P) => ( + + + + +); + +export const IconCaretDown = (p: P) => ( + + + +); diff --git a/stats-react/src/main.tsx b/stats-react/src/main.tsx new file mode 100644 index 0000000..1155add --- /dev/null +++ b/stats-react/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; +import "./styles/global.css"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/stats-react/src/styles/global.css b/stats-react/src/styles/global.css new file mode 100644 index 0000000..8438122 --- /dev/null +++ b/stats-react/src/styles/global.css @@ -0,0 +1,478 @@ +/* Сброс + переменные. Цвета — web-react/src/theme/themes.ts (designer.py), + категориальная палитра графиков — theme/palette.ts (провалидирована dataviz-скиллом). */ + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + font-family: system-ui, -apple-system, "Segoe UI", sans-serif; +} + +:root { + color-scheme: light; + --primary: #f85a40; + --primary-dark: #d94530; + --primary-light: #ffeab7; + --accent: #ffc845; + --info: #007fbd; + --success: #85c446; + + --background: #f4f6f8; + --surface: #ffffff; + --surface-alt: #f0f2f5; + --text-primary: #1e2a3a; + --text-secondary: #5a6779; + --text-on-primary: #ffffff; + --border: #dde3ea; + + --chart-grid: #dde3ea; + --chart-tooltip-bg: #1e2a3a; + --chart-tooltip-text: #ffffff; +} + +@media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --primary: #f85a40; + --primary-dark: #d94530; + --primary-light: #ffeab7; + --accent: #ffc845; + --info: #4db3e8; + --success: #a2d97a; + + --background: #141a22; + --surface: #1e2733; + --surface-alt: #263140; + --text-primary: #f2f4f7; + --text-secondary: #a7b2c0; + --text-on-primary: #ffffff; + --border: #33404f; + + --chart-grid: #33404f; + --chart-tooltip-bg: #f2f4f7; + --chart-tooltip-text: #1e2a3a; + } +} + +body { + background: var(--background); + color: var(--text-primary); +} + +.muted { + color: var(--text-secondary); +} + +/* ── Layout ──────────────────────────────────────────────────────────── */ + +.page { + min-height: 100vh; + padding-bottom: 2rem; +} + +.app-header { + display: flex; + align-items: center; + gap: 0.75rem; + background: var(--surface); + padding: 0.9rem 1.25rem; + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 20; +} + +.app-header .brand { + color: var(--primary); + font-size: 1.35rem; + font-weight: 700; +} + +.app-header .divider { + width: 1px; + height: 1.4rem; + background: var(--border); +} + +.app-header .subtitle { + color: var(--text-primary); + font-size: 1rem; +} + +.container { + max-width: 1300px; + margin: 0 auto; + padding: 0 1rem; +} + +/* ── Summary tiles ───────────────────────────────────────────────────── */ + +.summary-row { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + padding: 1rem 0; +} + +.summary-tile { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + padding: 0.9rem 1rem; + min-width: 190px; + flex: 1 1 190px; +} + +.summary-tile .tile-head { + display: flex; + align-items: center; + gap: 0.4rem; + color: var(--text-secondary); + font-size: 0.72rem; + margin-bottom: 0.35rem; +} + +.summary-tile .tile-value { + font-size: 1.7rem; + font-weight: 700; + color: var(--text-primary); +} + +/* ── Filters ─────────────────────────────────────────────────────────── */ + +.filters-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1rem; + margin-bottom: 1rem; +} + +.filters-label { + color: var(--text-secondary); + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.02em; + margin-bottom: 0.5rem; + display: block; +} + +.filters-row { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; + align-items: flex-end; +} + +.filters-actions { + display: flex; + gap: 0.5rem; + margin-top: 0.7rem; +} + +/* ── Buttons ─────────────────────────────────────────────────────────── */ + +.btn { + border: none; + border-radius: 8px; + padding: 0.5rem 0.9rem; + font-size: 0.85rem; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.4rem; +} + +.btn-primary { + background: var(--primary); + color: var(--text-on-primary); +} + +.btn-primary:hover { + background: var(--primary-dark); +} + +.btn-ghost { + background: transparent; + color: var(--primary); +} + +.btn-ghost:hover { + background: var(--primary-light); +} + +/* ── Dropdown ────────────────────────────────────────────────────────── */ + +.field { + display: flex; + flex-direction: column; + gap: 0.25rem; + min-width: 190px; +} + +.field-label { + color: var(--text-secondary); + font-size: 0.72rem; + font-weight: 500; +} + +.dropdown-control { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + padding: 0.55rem 0.75rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + cursor: pointer; + min-height: 2.3rem; +} + +.dropdown-value { + font-size: 0.85rem; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dropdown-value.placeholder { + color: var(--text-secondary); + font-style: italic; +} + +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + padding: 1rem; +} + +.modal { + background: var(--surface); + border-radius: 12px; + padding: 1rem; + width: 100%; + max-width: 380px; + max-height: 70vh; + display: flex; + flex-direction: column; +} + +.modal-title { + font-size: 0.95rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.6rem; +} + +.dropdown-search { + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.5rem 0.7rem; + font-size: 0.85rem; + background: var(--background); + color: var(--text-primary); +} + +.divider { + border: none; + border-top: 1px solid var(--border); + margin: 0.6rem 0; +} + +.sd-list { + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.dropdown-option { + padding: 0.55rem 0.5rem; + border-radius: 6px; + cursor: pointer; + font-size: 0.85rem; + color: var(--text-primary); +} + +.dropdown-option:hover { + background: var(--primary-light); +} + +.dropdown-option.active { + background: var(--primary); + color: var(--text-on-primary); +} + +.dropdown-empty { + color: var(--text-secondary); + font-size: 0.85rem; + padding: 0.5rem; + text-align: center; +} + +/* ── Tabs ────────────────────────────────────────────────────────────── */ + +.tabs-bar { + display: flex; + gap: 0.25rem; + border-bottom: 1px solid var(--border); + margin-bottom: 1rem; +} + +.tab-btn { + background: none; + border: none; + padding: 0.7rem 1rem; + font-size: 0.88rem; + color: var(--text-secondary); + cursor: pointer; + display: flex; + align-items: center; + gap: 0.4rem; + border-bottom: 2px solid transparent; + margin-bottom: -1px; +} + +.tab-btn.active { + color: var(--primary); + border-bottom-color: var(--primary); + font-weight: 600; +} + +/* ── Chart cards ─────────────────────────────────────────────────────── */ + +.chart-grid { + display: flex; + flex-direction: column; + gap: 1rem; + padding-bottom: 1.5rem; +} + +.chart-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1rem; +} + +.chart-card-title { + font-size: 0.92rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.7rem; +} + +.chart-empty { + color: var(--text-secondary); + font-style: italic; + font-size: 0.85rem; + padding: 2rem 0; + text-align: center; +} + +.chart-loading { + padding: 2rem 0; + text-align: center; + color: var(--text-secondary); + font-size: 0.85rem; +} + +/* ── Legend ──────────────────────────────────────────────────────────── */ + +.chart-legend { + display: flex; + flex-wrap: wrap; + gap: 0.6rem 1rem; + margin-top: 0.6rem; + font-size: 0.8rem; + color: var(--text-secondary); +} + +.legend-item { + display: flex; + align-items: center; + gap: 0.35rem; +} + +.legend-swatch { + width: 10px; + height: 10px; + border-radius: 3px; + flex-shrink: 0; +} + +/* ── Table ───────────────────────────────────────────────────────────── */ + +.stats-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} + +.stats-table th { + text-align: left; + color: var(--text-secondary); + font-weight: 600; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.02em; + padding: 0.5rem 0.7rem; + border-bottom: 1px solid var(--border); +} + +.stats-table td { + padding: 0.55rem 0.7rem; + border-bottom: 1px solid var(--border); +} + +.stats-table tr:last-child td { + border-bottom: none; +} + +.stats-table .value-cell { + font-weight: 700; + color: var(--primary); + font-variant-numeric: tabular-nums; +} + +.stats-table .poll-cell { + color: var(--text-secondary); + max-width: 260px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ── Spinner ─────────────────────────────────────────────────────────── */ + +.spinner { + width: 22px; + height: 22px; + border: 3px solid var(--border); + border-top-color: var(--primary); + border-radius: 50%; + animation: spin 0.7s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.spinner-center { + display: flex; + justify-content: center; + padding: 2rem; +} diff --git a/stats-react/src/theme/palette.ts b/stats-react/src/theme/palette.ts new file mode 100644 index 0000000..024a72c --- /dev/null +++ b/stats-react/src/theme/palette.ts @@ -0,0 +1,43 @@ +// Палитра дашборда. Бренд-цвета — из web-react/src/theme/themes.ts (designer.py). +// Категориальная 8-цветная палитра для графиков (pie/multi-bar) провалидирована +// dataviz-скиллом (scripts/validate_palette.js) на CVD-безопасность в light и +// dark режимах — порядок слотов и hex не менять без повторной валидации. +// Единственная замена относительно референсного набора: слот "orange" — бренд-оранжевый. + +export const BRAND = { + primary: "#F85A40", + primaryDark: "#D94530", + primaryLight: "#FFEAB7", + accent: "#FFC845", + info: "#007FBD", + success: "#85C446", +}; + +// [PASS] все проверки, light: surface #FFFFFF (WARN contrast на magenta/yellow/aqua — компенсируется прямыми подписями значений) +export const CATEGORICAL_LIGHT = [ + "#2a78d6", // blue + "#008300", // green + "#e87ba4", // magenta + "#eda100", // yellow + "#1baf7a", // aqua + "#F85A40", // orange (бренд) + "#4a3aa7", // violet + "#e34948", // red +]; + +// [PASS] все проверки, dark: surface #1a1a19 +export const CATEGORICAL_DARK = [ + "#3987e5", + "#008300", + "#d55181", + "#c98500", + "#199e70", + "#D94530", // orange (бренд, тёмный шаг) + "#9085e9", + "#e66767", +]; + +export function categoricalColor(index: number, dark: boolean): string { + const arr = dark ? CATEGORICAL_DARK : CATEGORICAL_LIGHT; + return arr[index % arr.length]; +} diff --git a/stats-react/src/theme/useIsDark.ts b/stats-react/src/theme/useIsDark.ts new file mode 100644 index 0000000..afc4698 --- /dev/null +++ b/stats-react/src/theme/useIsDark.ts @@ -0,0 +1,18 @@ +import { useEffect, useState } from "react"; + +// Категориальная палитра графиков зависит от режима (см. palette.ts) — +// нужно знать активный prefers-color-scheme, чтобы выбрать нужный набор. +export function useIsDark(): boolean { + const [dark, setDark] = useState( + () => typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches, + ); + + useEffect(() => { + const mq = window.matchMedia("(prefers-color-scheme: dark)"); + const onChange = () => setDark(mq.matches); + mq.addEventListener("change", onChange); + return () => mq.removeEventListener("change", onChange); + }, []); + + return dark; +} diff --git a/stats-react/tsconfig.json b/stats-react/tsconfig.json new file mode 100644 index 0000000..d109555 --- /dev/null +++ b/stats-react/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/stats-react/vite.config.ts b/stats-react/vite.config.ts new file mode 100644 index 0000000..1f4edc4 --- /dev/null +++ b/stats-react/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// В dev-режиме проксируем /api на запущенный локально API (docker compose up -d api). +// В проде эту роль выполняет nginx (stats-react/nginx.conf). +export default defineConfig({ + plugins: [react()], + server: { + port: 5174, + proxy: { + "/api": { + target: process.env.API_URL || "http://localhost:8000", + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ""), + }, + }, + }, +}); diff --git a/stats/api_client.py b/stats/api_client.py deleted file mode 100644 index 06bd07c..0000000 --- a/stats/api_client.py +++ /dev/null @@ -1,209 +0,0 @@ -""" -api_client.py — HTTP-клиент для stats-приложения. -Запросы идут к основному API без авторизации. -""" -import os -from typing import Optional -import httpx - -API_URL = os.getenv("API_URL", "http://api:8000") -_TIMEOUT = 15 - - -async def get_summary() -> dict: - try: - async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client: - resp = await client.get("/stats/summary") - if resp.status_code == 200: - return resp.json() - except Exception: - pass - return {"total_responses": 0, "total_polls": 0, "active_polls": 0, "orgs_active": 0} - - -async def get_years() -> list[int]: - try: - async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client: - resp = await client.get("/stats/years") - if resp.status_code == 200: - return resp.json() - except Exception: - pass - return [] - - -async def get_chart_data( - group_by: str, - poll_id: Optional[str] = None, - org_id: Optional[str] = None, - group_id: Optional[str] = None, - year: Optional[int] = None, -) -> list[dict]: - params: dict = {"group_by": group_by} - if poll_id: - params["poll_id"] = poll_id - if org_id: - params["org_id"] = org_id - if group_id: - params["group_id"] = group_id - if year: - params["year"] = year - try: - async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client: - resp = await client.get("/stats/responses", params=params) - if resp.status_code == 200: - return resp.json() - except Exception: - pass - return [] - - -async def get_polls() -> list[dict]: - try: - async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client: - resp = await client.get("/polls/") - if resp.status_code == 200: - return resp.json() - except Exception: - pass - return [] - - -async def get_organizations() -> list[dict]: - try: - async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client: - resp = await client.get("/organizations/") - if resp.status_code == 200: - return resp.json() - except Exception: - pass - return [] - - -async def get_groups() -> list[dict]: - try: - async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client: - resp = await client.get("/groups/") - if resp.status_code == 200: - return resp.json() - except Exception: - pass - return [] - - -async def get_dimensions(poll_id: Optional[str] = None) -> list[str]: - params = {} - if poll_id: - params["poll_id"] = poll_id - try: - async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client: - resp = await client.get("/stats/results/dimensions", params=params) - if resp.status_code == 200: - return resp.json() - except Exception: - pass - return [] - - -async def get_avg_by_dimension( - poll_id: Optional[str] = None, - org_id: Optional[str] = None, - group_id: Optional[str] = None, - year: Optional[int] = None, -) -> list[dict]: - params = {} - if poll_id: params["poll_id"] = poll_id - if org_id: params["org_id"] = org_id - if group_id: params["group_id"] = group_id - if year: params["year"] = year - try: - async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client: - resp = await client.get("/stats/results/avg-by-dimension", params=params) - if resp.status_code == 200: - return resp.json() - except Exception: - pass - return [] - - -async def get_leading_type( - poll_id: Optional[str] = None, - org_id: Optional[str] = None, - group_id: Optional[str] = None, - year: Optional[int] = None, -) -> list[dict]: - params = {} - if poll_id: params["poll_id"] = poll_id - if org_id: params["org_id"] = org_id - if group_id: params["group_id"] = group_id - if year: params["year"] = year - try: - async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client: - resp = await client.get("/stats/results/leading-type", params=params) - if resp.status_code == 200: - return resp.json() - except Exception: - pass - return [] - - -async def get_compare_groups( - dimension_name: str, - by: str = "organization", - poll_id: Optional[str] = None, - year: Optional[int] = None, -) -> list[dict]: - params = {"dimension_name": dimension_name, "by": by} - if poll_id: params["poll_id"] = poll_id - if year: params["year"] = year - try: - async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client: - resp = await client.get("/stats/results/compare-groups", params=params) - if resp.status_code == 200: - return resp.json() - except Exception: - pass - return [] - - -async def get_top_users( - dimension_name: str, - poll_id: Optional[str] = None, - org_id: Optional[str] = None, - group_id: Optional[str] = None, - year: Optional[int] = None, - limit: int = 10, -) -> list[dict]: - params = {"dimension_name": dimension_name, "limit": limit} - if poll_id: params["poll_id"] = poll_id - if org_id: params["org_id"] = org_id - if group_id: params["group_id"] = group_id - if year: params["year"] = year - try: - async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client: - resp = await client.get("/stats/results/top-users", params=params) - if resp.status_code == 200: - return resp.json() - except Exception: - pass - return [] - - -async def get_dimension_trend( - dimension_name: str, - poll_id: Optional[str] = None, - org_id: Optional[str] = None, - group_id: Optional[str] = None, -) -> list[dict]: - params = {"dimension_name": dimension_name} - if poll_id: params["poll_id"] = poll_id - if org_id: params["org_id"] = org_id - if group_id: params["group_id"] = group_id - try: - async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client: - resp = await client.get("/stats/results/dimension-trend", params=params) - if resp.status_code == 200: - return resp.json() - except Exception: - pass - return [] diff --git a/stats/assets/icons/IMG_4533.png b/stats/assets/icons/IMG_4533.png deleted file mode 100644 index 7c9703b381658dfb58504064c6fe08af9b4708f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116920 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 XH}1{PA?iq}-Y@%%_R@Z~{_Vd2-^~D8 diff --git a/stats/assets/icons/README b/stats/assets/icons/README deleted file mode 100644 index 709aa22..0000000 --- a/stats/assets/icons/README +++ /dev/null @@ -1,3 +0,0 @@ -# Положи сюда своё изображение с именем loading-animation.png -# Это заменит мигающий логотип Flet при загрузке страницы. -# Рекомендуемый размер: PNG, минимум 200x200 px. diff --git a/stats/assets/icons/clipboard-question_16542596.svg b/stats/assets/icons/clipboard-question_16542596.svg deleted file mode 100644 index 803761e..0000000 --- a/stats/assets/icons/clipboard-question_16542596.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/stats/assets/icons/leaderboard-trophy_14227576.svg b/stats/assets/icons/leaderboard-trophy_14227576.svg deleted file mode 100644 index 011f4e3..0000000 --- a/stats/assets/icons/leaderboard-trophy_14227576.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/stats/assets/icons/loading-animation.png b/stats/assets/icons/loading-animation.png deleted file mode 100644 index 91c0a0c2702b3f095e348f9d71685b5005bacfa8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31068 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 diff --git a/stats/dashboard.py b/stats/dashboard.py deleted file mode 100644 index ed6fd84..0000000 --- a/stats/dashboard.py +++ /dev/null @@ -1,745 +0,0 @@ -""" -dashboard.py — главный вид статистического дашборда. -Графики строятся через matplotlib и отображаются как изображения (ft.Image). -""" -import asyncio -import base64 -import io -import math - -import flet as ft -import matplotlib -import matplotlib.colors -import matplotlib.pyplot as plt -import matplotlib.font_manager as fm - -import designer as ds -import api_client as ac - -matplotlib.use("Agg") # без GUI, только в память - -# Цвета графиков -_PALETTE = [ - "#F85A40", "#007FBD", "#85C446", "#FFC845", - "#D94530", "#5A6779", "#006AA3", "#4A9E21", -] - - -def _wrap_label(text: str, max_len: int = 18) -> str: - """Перенос длинного текста через \n для подписей осей.""" - if len(text) <= max_len: - return text - words = text.split() - lines, line = [], "" - for w in words: - if len(line) + len(w) + 1 <= max_len: - line = f"{line} {w}".strip() - else: - if line: - lines.append(line) - line = w - if line: - lines.append(line) - return "\n".join(lines) - - -def _build_chart_image(data: list[dict], title: str, color: str) -> bytes | None: - """Строит горизонтальную bar chart через matplotlib, возвращает PNG bytes.""" - if not data: - return None - - labels = [_wrap_label(d["label"]) for d in data] - counts = [d["count"] for d in data] - n = len(labels) - - fig_h = max(4.5, n * 0.55 + 1.2) - fig, ax = plt.subplots(figsize=(10, fig_h)) - fig.patch.set_facecolor("#FFFFFF") - ax.set_facecolor("#F4F6F8") - - bars = ax.barh( - range(n), counts, - color=color, - height=0.6, - edgecolor="white", - linewidth=0.5, - ) - - # Подписи значений - for bar, v in zip(bars, counts): - ax.text( - v + max(counts) * 0.01, - bar.get_y() + bar.get_height() / 2, - str(v), - va="center", - ha="left", - fontsize=11, - color="#1E2A3A", - ) - - ax.set_yticks(range(n)) - ax.set_yticklabels(labels, fontsize=11, color="#5A6779") - ax.invert_yaxis() - ax.set_xlabel("Количество ответов", fontsize=11, color="#5A6779") - ax.xaxis.set_tick_params(labelsize=11, colors="#5A6779") - ax.spines["top"].set_visible(False) - ax.spines["right"].set_visible(False) - ax.spines["left"].set_color("#DDE3EA") - ax.spines["bottom"].set_color("#DDE3EA") - ax.set_xlim(0, max(counts) * 1.18 if max(counts) > 0 else 1) - ax.grid(axis="x", color="#DDE3EA", linewidth=0.5, linestyle="--") - - plt.tight_layout(pad=1.2) - - buf = io.BytesIO() - fig.savefig(buf, format="png", dpi=130, bbox_inches="tight") - plt.close(fig) - buf.seek(0) - return buf.read() - - -def _build_line_chart_image(data: list[dict]) -> bytes | None: - """Строит линейный график для данных по месяцам.""" - if not data: - return None - - labels = [d["label"] for d in data] - counts = [d["count"] for d in data] - n = len(labels) - - fig, ax = plt.subplots(figsize=(max(10, n * 1.2), 5.5)) - fig.patch.set_facecolor("#FFFFFF") - ax.set_facecolor("#F4F6F8") - - ax.plot(range(n), counts, color="#F85A40", linewidth=2.5, marker="o", - markersize=7, markerfacecolor="#F85A40", markeredgecolor="white", - markeredgewidth=1.5) - ax.fill_between(range(n), counts, alpha=0.12, color="#F85A40") - - for i, v in enumerate(counts): - ax.text(i, v + max(counts) * 0.03, str(v), ha="center", - fontsize=11, color="#1E2A3A") - - ax.set_xticks(range(n)) - ax.set_xticklabels(labels, rotation=35, ha="right", fontsize=11, color="#5A6779") - ax.yaxis.set_tick_params(labelsize=11, colors="#5A6779") - ax.set_ylabel("Ответов", fontsize=11, color="#5A6779") - ax.spines["top"].set_visible(False) - ax.spines["right"].set_visible(False) - ax.spines["left"].set_color("#DDE3EA") - ax.spines["bottom"].set_color("#DDE3EA") - ax.set_ylim(0, max(counts) * 1.25) - ax.grid(axis="y", color="#DDE3EA", linewidth=0.5, linestyle="--") - - plt.tight_layout(pad=1.2) - buf = io.BytesIO() - fig.savefig(buf, format="png", dpi=130, bbox_inches="tight") - plt.close(fig) - buf.seek(0) - return buf.read() - - -def _build_avg_bar(data: list[dict], xlabel: str = "Средний балл") -> bytes | None: - """Горизонтальный bar chart со средними баллами (поле 'avg').""" - if not data: - return None - labels = [_wrap_label(d["label"]) for d in data] - values = [d["avg"] for d in data] - n = len(labels) - colors = [_PALETTE[i % len(_PALETTE)] for i in range(n)] - - fig_h = max(5.5, n * 0.65 + 1.5) - fig, ax = plt.subplots(figsize=(14, fig_h)) - fig.patch.set_facecolor("#FFFFFF") - ax.set_facecolor("#F4F6F8") - - bars = ax.barh(range(n), values, color=colors, height=0.6, edgecolor="white", linewidth=0.5) - for bar, v in zip(bars, values): - ax.text(v + max(values) * 0.01, bar.get_y() + bar.get_height() / 2, - f"{v:.1f}", va="center", ha="left", fontsize=11, color="#1E2A3A") - - ax.set_yticks(range(n)) - ax.set_yticklabels(labels, fontsize=11, color="#5A6779") - ax.invert_yaxis() - ax.set_xlabel(xlabel, fontsize=11, color="#5A6779") - ax.xaxis.set_tick_params(labelsize=11, colors="#5A6779") - ax.spines["top"].set_visible(False) - ax.spines["right"].set_visible(False) - ax.spines["left"].set_color("#DDE3EA") - ax.spines["bottom"].set_color("#DDE3EA") - ax.set_xlim(0, max(values) * 1.18 if max(values) > 0 else 1) - ax.grid(axis="x", color="#DDE3EA", linewidth=0.5, linestyle="--") - plt.tight_layout(pad=1.2) - buf = io.BytesIO() - fig.savefig(buf, format="png", dpi=130, bbox_inches="tight") - plt.close(fig) - buf.seek(0) - return buf.read() - - -def _build_pie_chart(data: list[dict]) -> bytes | None: - """Круговая диаграмма распределения ведущего типа.""" - if not data: - return None - labels = [d["label"] for d in data] - values = [d["count"] for d in data] - n_colors = len(labels) - if n_colors <= len(_PALETTE): - colors = _PALETTE[:n_colors] - else: - # Генерируем уникальные цвета через hsv colormap - cmap = matplotlib.colormaps["hsv"] - colors = [matplotlib.colors.to_hex(cmap(i / n_colors)) for i in range(n_colors)] - - # Размер фигуры растёт с числом элементов для читаемой легенды - legend_rows = math.ceil(n_colors / 2) - fig_h = max(7, legend_rows * 0.35 + 5) - fig, ax = plt.subplots(figsize=(11, fig_h)) - fig.patch.set_facecolor("#FFFFFF") - wedges, texts, autotexts = ax.pie( - values, - labels=None, - autopct="%1.1f%%", - colors=colors, - startangle=140, - wedgeprops=dict(edgecolor="white", linewidth=1.5), - pctdistance=0.78, - ) - for t in autotexts: - t.set_fontsize(9 if n_colors > 15 else 10) - t.set_color("#1E2A3A") - ncol = 3 if n_colors > 20 else 2 - ax.legend( - wedges, - [f"{l} ({v})" for l, v in zip(labels, values)], - loc="lower center", - bbox_to_anchor=(0.5, -0.05 - legend_rows * 0.04), - ncol=ncol, - fontsize=9, - frameon=False, - ) - plt.tight_layout(pad=1.5) - buf = io.BytesIO() - fig.savefig(buf, format="png", dpi=130, bbox_inches="tight") - plt.close(fig) - buf.seek(0) - return buf.read() - - -def _build_avg_line(data: list[dict]) -> bytes | None: - """Линейный тренд средних баллов по месяцам (поле 'avg').""" - if not data: - return None - labels = [d["label"] for d in data] - values = [d["avg"] for d in data] - n = len(labels) - - fig, ax = plt.subplots(figsize=(max(10, n * 1.2), 5.5)) - fig.patch.set_facecolor("#FFFFFF") - ax.set_facecolor("#F4F6F8") - ax.plot(range(n), values, color="#F85A40", linewidth=2.5, marker="o", - markersize=7, markerfacecolor="#F85A40", markeredgecolor="white", - markeredgewidth=1.5) - ax.fill_between(range(n), values, alpha=0.12, color="#F85A40") - for i, v in enumerate(values): - ax.text(i, v + max(values) * 0.03, f"{v:.1f}", ha="center", fontsize=11, color="#1E2A3A") - ax.set_xticks(range(n)) - ax.set_xticklabels(labels, rotation=35, ha="right", fontsize=11, color="#5A6779") - ax.yaxis.set_tick_params(labelsize=11, colors="#5A6779") - ax.set_ylabel("Средний балл", fontsize=11, color="#5A6779") - ax.spines["top"].set_visible(False) - ax.spines["right"].set_visible(False) - ax.spines["left"].set_color("#DDE3EA") - ax.spines["bottom"].set_color("#DDE3EA") - ax.set_ylim(0, max(values) * 1.25 if max(values) > 0 else 1) - ax.grid(axis="y", color="#DDE3EA", linewidth=0.5, linestyle="--") - plt.tight_layout(pad=1.2) - buf = io.BytesIO() - fig.savefig(buf, format="png", dpi=130, bbox_inches="tight") - plt.close(fig) - buf.seek(0) - return buf.read() - - -def _image_or_placeholder(img_bytes: bytes | None, width: int = 1100) -> ft.Control: - if img_bytes: - b64 = base64.b64encode(img_bytes).decode() - return ft.Image( - src=f"data:image/png;base64,{b64}", - fit=ft.BoxFit.FIT_WIDTH, - width=width, - ) - return ft.Container( - content=ft.Text("Нет данных", color=ds.colors.text_secondary, size=ds.s(13), italic=True), - alignment=ft.Alignment(0, 0), - height=ds.s(120), - width=width, - ) - - -class DashboardView(ft.Column): - def __init__(self, page: ft.Page): - self._page = page - - # ── Общие фильтры ──────────────────────────────────────────────────── - self._poll_dd = ds.SearchableDropdown(label="Тест", hint_text="Все тесты") - self._poll_dd.on_select = self._on_filter_change - - self._org_dd = ds.SearchableDropdown(label="Организация", hint_text="Все организации") - self._org_dd.on_select = self._on_filter_change - - self._group_dd = ds.SearchableDropdown(label="Группа", hint_text="Все группы") - self._group_dd.on_select = self._on_filter_change - - self._year_dd = ds.SearchableDropdown(label="Год", hint_text="Все годы") - self._year_dd.on_select = self._on_filter_change - - self._reset_btn = ft.TextButton( - "Сбросить фильтры", - icon=ft.Icons.FILTER_ALT_OFF, - style=ft.ButtonStyle(color=ds.colors.primary), - on_click=self._reset_filters, - ) - - self._refresh_btn = ft.FilledButton( - "Обновить", - icon=ft.Icons.REFRESH, - style=ft.ButtonStyle( - bgcolor={ft.ControlState.DEFAULT: ds.colors.primary, - ft.ControlState.HOVERED: ds.colors.primary_dark}, - color=ds.colors.text_on_primary, - shape=ft.RoundedRectangleBorder(radius=ds.s(8)), - ), - on_click=lambda e: page.run_task(self._refresh_current_tab), - ) - - # ── Summary карточки ───────────────────────────────────────────────── - self._card_total = ft.Text("—", color=ds.colors.text_primary, size=ds.s(26), weight=ft.FontWeight.BOLD) - self._card_polls = ft.Text("—", color=ds.colors.text_primary, size=ds.s(26), weight=ft.FontWeight.BOLD) - self._card_active = ft.Text("—", color=ds.colors.text_primary, size=ds.s(26), weight=ft.FontWeight.BOLD) - self._card_orgs = ft.Text("—", color=ds.colors.text_primary, size=ds.s(26), weight=ft.FontWeight.BOLD) - - # ── Таб 1: графики по прохождениям ─────────────────────────────────── - self._chart_poll = self._make_chart_box() - self._chart_org = self._make_chart_box() - self._chart_group = self._make_chart_box() - self._chart_month = self._make_chart_box() - - # ── Таб 2: результаты — фильтр по оси ──────────────────────────────── - self._dim_dd = ds.SearchableDropdown(label="Ось/Тип", hint_text="Выберите ось") - self._dim_dd.on_select = self._on_results_filter_change - - self._compare_by_dd = ds.SearchableDropdown(label="Сравнение по", hint_text="Организациям") - self._compare_by_dd.options = [ - ft.DropdownOption(key="organization", text="Организациям"), - ft.DropdownOption(key="group", text="Группам"), - ] - self._compare_by_dd.value = "organization" - self._compare_by_dd.on_select = self._on_results_filter_change - - self._chart_avg_dim = self._make_chart_box() - self._chart_leading = self._make_chart_box() - self._chart_compare = self._make_chart_box() - self._chart_trend = self._make_chart_box() - - # Таблица топ-пользователей - self._top_table = ft.DataTable( - columns=[ - ft.DataColumn(ft.Text("Участник", size=ds.s(12), weight=ft.FontWeight.W_600)), - ft.DataColumn(ft.Text("Тест", size=ds.s(12), weight=ft.FontWeight.W_600)), - ft.DataColumn(ft.Text("Балл", size=ds.s(12), weight=ft.FontWeight.W_600), numeric=True), - ], - rows=[], - border=ft.border.all(1, ds.colors.border), - border_radius=ds.s(8), - horizontal_lines=ft.BorderSide(1, ds.colors.border), - column_spacing=ds.s(20), - ) - - # ── Навигация по табам ──────────────────────────────────────────────── - self._current_tab = 0 - - # ── Содержимое табов ────────────────────────────────────────────────── - self._tab1_content = ft.Column( - spacing=ds.s(16), - controls=[ - ft.Container( - content=self._chart_card_wrap("По тестам", self._chart_poll), - padding=ft.padding.symmetric(horizontal=ds.s(16)), - ), - ft.Container( - content=self._chart_card_wrap("По организациям", self._chart_org), - padding=ft.padding.symmetric(horizontal=ds.s(16)), - ), - ft.Container( - content=self._chart_card_wrap("По группам", self._chart_group), - padding=ft.padding.symmetric(horizontal=ds.s(16)), - ), - ft.Container( - content=self._chart_card_wrap("Динамика по месяцам", self._chart_month), - padding=ft.padding.symmetric(horizontal=ds.s(16)), - margin=ft.margin.only(bottom=ds.s(24)), - ), - ], - ) - - self._tab2_content = ft.Column( - spacing=ds.s(16), - controls=[ - # Дополнительный фильтр по оси - ft.Container( - content=ft.Column( - spacing=ds.s(8), - controls=[ - ft.Text("Фильтр по оси/типу", color=ds.colors.text_secondary, - size=ds.s(12), weight=ft.FontWeight.W_600), - ft.Row( - wrap=True, spacing=ds.s(10), run_spacing=ds.s(10), - controls=[self._dim_dd, self._compare_by_dd], - ), - ], - ), - bgcolor=ds.colors.surface, - border_radius=ds.s(12), - padding=ft.padding.all(ds.s(16)), - margin=ft.margin.symmetric(horizontal=ds.s(16)), - border=ft.border.all(1, ds.colors.border), - ), - ft.Container( - content=self._chart_card_wrap("Средний балл по осям", self._chart_avg_dim), - padding=ft.padding.symmetric(horizontal=ds.s(16)), - ), - ft.Container( - content=self._chart_card_wrap("Распределение ведущего типа", self._chart_leading), - padding=ft.padding.symmetric(horizontal=ds.s(16)), - ), - ft.Container( - content=self._chart_card_wrap("Сравнение по группам/организациям", self._chart_compare), - padding=ft.padding.symmetric(horizontal=ds.s(16)), - ), - ft.Container( - content=self._chart_card_wrap("Динамика среднего балла", self._chart_trend), - padding=ft.padding.symmetric(horizontal=ds.s(16)), - ), - # Топ участников - ft.Container( - content=ft.Column( - spacing=ds.s(10), - controls=[ - ft.Text("Топ-10 участников по выбранной оси", - color=ds.colors.text_primary, size=ds.s(14), - weight=ft.FontWeight.W_600), - ft.Container(content=self._top_table, expand=True), - ], - ), - bgcolor=ds.colors.surface, - border_radius=ds.s(12), - padding=ft.padding.all(ds.s(16)), - margin=ft.margin.only(left=ds.s(16), right=ds.s(16), bottom=ds.s(24)), - border=ft.border.all(1, ds.colors.border), - ), - ], - ) - - # ── Tabs (TabBar + ручное переключение) ────────────────────────────── - self._tab_body = ft.Container(content=self._tab1_content, expand=True) - self._tabs_ctrl = ft.Tabs( - content=ft.Column( - expand=True, - spacing=0, - controls=[ - ft.TabBar( - tabs=[ - ft.Tab(label="Прохождения", icon=ft.Icons.BAR_CHART), - ft.Tab(label="Результаты тестов", icon=ft.Icons.INSIGHTS), - ], - scrollable=False, - ), - self._tab_body, - ], - ), - length=2, - selected_index=0, - on_change=self._on_tab_change, - expand=True, - ) - - super().__init__( - spacing=ds.s(16), - scroll=ft.ScrollMode.AUTO, - expand=True, - controls=[ - # Заголовок - ft.Container( - content=ft.Row( - controls=[ - ft.Text("ЦОПП", color=ds.colors.primary, size=ds.s(22), - weight=ft.FontWeight.BOLD), - ft.VerticalDivider(width=ds.s(16), color=ds.colors.border), - ft.Text("Статистика прохождений", color=ds.colors.text_primary, - size=ds.s(16)), - ], - vertical_alignment=ft.CrossAxisAlignment.CENTER, - ), - bgcolor=ds.colors.surface, - padding=ft.padding.symmetric(horizontal=ds.s(20), vertical=ds.s(14)), - border=ft.border.only(bottom=ft.BorderSide(1, ds.colors.border)), - ), - # Summary карточки - ft.Container( - content=ft.Row( - wrap=True, spacing=ds.s(12), run_spacing=ds.s(12), - controls=[ - self._summary_tile("Всего ответов", self._card_total, - ft.Icons.ASSIGNMENT_TURNED_IN, ds.colors.primary), - self._summary_tile("Тестов всего", self._card_polls, - ft.Icons.QUIZ, ds.colors.info), - self._summary_tile("Активных тестов", self._card_active, - ft.Icons.PLAY_CIRCLE, ds.colors.success), - self._summary_tile("Организаций с ответами", self._card_orgs, - ft.Icons.BUSINESS, ds.colors.accent), - ], - ), - padding=ft.padding.symmetric(horizontal=ds.s(16)), - ), - # Общие фильтры - ft.Container( - content=ft.Column( - spacing=ds.s(10), - controls=[ - ft.Text("Фильтры", color=ds.colors.text_secondary, - size=ds.s(12), weight=ft.FontWeight.W_600), - ft.Row( - wrap=True, spacing=ds.s(10), run_spacing=ds.s(10), - controls=[self._poll_dd, self._org_dd, - self._group_dd, self._year_dd], - ), - ft.Row(spacing=ds.s(8), - controls=[self._reset_btn, self._refresh_btn]), - ], - ), - bgcolor=ds.colors.surface, - border_radius=ds.s(12), - padding=ft.padding.all(ds.s(16)), - margin=ft.margin.symmetric(horizontal=ds.s(16)), - border=ft.border.all(1, ds.colors.border), - ), - # Вкладки - self._tabs_ctrl, - ], - ) - - # ── helpers ───────────────────────────────────────────────────────────── - - def _make_chart_box(self) -> ft.Container: - return ft.Container( - content=ft.ProgressRing(width=ds.s(24), height=ds.s(24)), - alignment=ft.Alignment(0, 0), - expand=True, - ) - - def _summary_tile(self, title: str, value_text: ft.Text, icon: str, color: str) -> ft.Container: - return ft.Container( - content=ft.Column( - spacing=ds.s(4), tight=True, - controls=[ - ft.Row(spacing=ds.s(6), controls=[ - ft.Icon(icon, color=color, size=ds.s(18)), - ft.Text(title, color=ds.colors.text_secondary, size=ds.s(11)), - ]), - value_text, - ], - ), - bgcolor=ds.colors.surface, - border_radius=ds.s(10), - padding=ft.padding.all(ds.s(14)), - border=ft.border.all(1, ds.colors.border), - width=ds.s(200), - ) - - def _chart_card_wrap(self, title: str, body: ft.Container) -> ft.Container: - return ft.Container( - content=ft.Column( - spacing=ds.s(10), tight=True, - controls=[ - ft.Text(title, color=ds.colors.text_primary, size=ds.s(14), - weight=ft.FontWeight.W_600), - body, - ], - ), - bgcolor=ds.colors.surface, - border_radius=ds.s(12), - padding=ft.padding.all(ds.s(16)), - border=ft.border.all(1, ds.colors.border), - expand=True, - ) - - # ── mounting ───────────────────────────────────────────────────────────── - - def _chart_width(self) -> int: - """Ширина графика = ширина страницы минус отступы.""" - try: - w = self.page.width or 1200 - except Exception: - w = 1200 - return max(600, int(w) - 80) - - def did_mount(self): - self.page.run_task(self._initial_load) - - async def _initial_load(self): - polls, orgs, groups, years, summary, dimensions = await asyncio.gather( - ac.get_polls(), - ac.get_organizations(), - ac.get_groups(), - ac.get_years(), - ac.get_summary(), - ac.get_dimensions(), - ) - - _dd_opt = lambda key, text: ft.DropdownOption(key=key, text=text) - - self._poll_dd.options = [_dd_opt(p["id"], p["title"]) for p in (polls or [])] - self._org_dd.options = [_dd_opt(o["id"], o["name_organization"]) for o in (orgs or [])] - self._group_dd.options = [_dd_opt(g["id"], g["name_group"]) for g in (groups or [])] - self._year_dd.options = [_dd_opt(str(y), str(y)) for y in (years or [])] - self._dim_dd.options = [_dd_opt(d, d) for d in (dimensions or [])] - if dimensions: - self._dim_dd.value = dimensions[0] - - self._card_total.value = str(summary.get("total_responses", "—")) - self._card_polls.value = str(summary.get("total_polls", "—")) - self._card_active.value = str(summary.get("active_polls", "—")) - self._card_orgs.value = str(summary.get("orgs_active", "—")) - - self.update() - await self._refresh_tab1() - await self._refresh_tab2() - - # ── tab switching ───────────────────────────────────────────────────────── - - def _on_tab_change(self, e): - self._current_tab = int(e.data) - self._tab_body.content = self._tab1_content if self._current_tab == 0 else self._tab2_content - self._tab_body.update() - - async def _refresh_current_tab(self): - if self._current_tab == 0: - await self._refresh_tab1() - else: - await self._refresh_tab2() - - # ── filters ─────────────────────────────────────────────────────────────── - - def _on_filter_change(self, e): - self.page.run_task(self._refresh_current_tab) - - def _on_results_filter_change(self, e): - self.page.run_task(self._refresh_tab2) - - def _reset_filters(self, e): - self._poll_dd.value = None - self._org_dd.value = None - self._group_dd.value = None - self._year_dd.value = None - self.update() - self.page.run_task(self._refresh_current_tab) - - def _get_filters(self) -> dict: - year_str = self._year_dd.value - return { - "poll_id": self._poll_dd.value, - "org_id": self._org_dd.value, - "group_id": self._group_dd.value, - "year": int(year_str) if year_str else None, - } - - # ── TAB 1: прохождения ─────────────────────────────────────────────────── - - async def _refresh_tab1(self): - f = self._get_filters() - spinner = lambda: ft.ProgressRing(width=ds.s(24), height=ds.s(24)) - for c in [self._chart_poll, self._chart_org, self._chart_group, self._chart_month]: - c.content = spinner() - self.update() - - d_poll, d_org, d_group, d_month = await asyncio.gather( - ac.get_chart_data("poll", **f), - ac.get_chart_data("organization", **f), - ac.get_chart_data("group", **f), - ac.get_chart_data("month", **f), - ) - loop = asyncio.get_event_loop() - img_poll, img_org, img_group, img_month = await asyncio.gather( - loop.run_in_executor(None, _build_chart_image, d_poll, "По тестам", "#F85A40"), - loop.run_in_executor(None, _build_chart_image, d_org, "По организациям", "#007FBD"), - loop.run_in_executor(None, _build_chart_image, d_group, "По группам", "#85C446"), - loop.run_in_executor(None, _build_line_chart_image, d_month), - ) - w = self._chart_width() - self._chart_poll.content = _image_or_placeholder(img_poll, w) - self._chart_org.content = _image_or_placeholder(img_org, w) - self._chart_group.content = _image_or_placeholder(img_group, w) - self._chart_month.content = _image_or_placeholder(img_month, w) - self.update() - - # ── TAB 2: результаты ──────────────────────────────────────────────────── - - async def _refresh_tab2(self): - f = self._get_filters() - dim = self._dim_dd.value - by = self._compare_by_dd.value or "organization" - - spinner = lambda: ft.ProgressRing(width=ds.s(24), height=ds.s(24)) - for c in [self._chart_avg_dim, self._chart_leading, - self._chart_compare, self._chart_trend]: - c.content = spinner() - self._top_table.rows = [] - self.update() - - # Параллельная загрузка - d_avg, d_lead, d_top = await asyncio.gather( - ac.get_avg_by_dimension(**f), - ac.get_leading_type(**f), - ac.get_top_users(dim, **f) if dim else asyncio.sleep(0), - ) - if not dim: - d_top = [] - d_compare = await ac.get_compare_groups(dim or "", by=by, - poll_id=f["poll_id"], - year=f["year"]) if dim else [] - d_trend = await ac.get_dimension_trend(dim or "", - poll_id=f["poll_id"], - org_id=f["org_id"], - group_id=f["group_id"]) if dim else [] - - loop = asyncio.get_event_loop() - img_avg, img_lead, img_compare, img_trend = await asyncio.gather( - loop.run_in_executor(None, _build_avg_bar, d_avg, "Средний балл"), - loop.run_in_executor(None, _build_pie_chart, d_lead), - loop.run_in_executor(None, _build_avg_bar, d_compare, - f"Средний балл по {'организациям' if by == 'organization' else 'группам'}"), - loop.run_in_executor(None, _build_avg_line, d_trend), - ) - - w = self._chart_width() - self._chart_avg_dim.content = _image_or_placeholder(img_avg, w) - self._chart_leading.content = _image_or_placeholder(img_lead, w) - self._chart_compare.content = _image_or_placeholder(img_compare, w) - self._chart_trend.content = _image_or_placeholder(img_trend, w) - - # Таблица топ - self._top_table.rows = [ - ft.DataRow(cells=[ - ft.DataCell(ft.Text(row["name"], size=ds.s(12))), - ft.DataCell(ft.Text(row["poll"], size=ds.s(11), - color=ds.colors.text_secondary, - overflow=ft.TextOverflow.ELLIPSIS, - max_lines=1)), - ft.DataCell(ft.Text(str(row["value"]), size=ds.s(12), - weight=ft.FontWeight.BOLD, - color=ds.colors.primary)), - ]) - for row in (d_top or []) - ] - self.update() diff --git a/stats/designer.py b/stats/designer.py deleted file mode 100644 index e749205..0000000 --- a/stats/designer.py +++ /dev/null @@ -1,223 +0,0 @@ -""" -designer.py для stats-приложения. -Та же палитра и компоненты что в web/designer.py, адаптированные для дашборда. -""" -import flet as ft - -SCALE: float = 1.0 - - -def s(v: int | float) -> int: - return round(v * SCALE) - - -class colors: - primary = "#F85A40" - primary_dark = "#D94530" - primary_light = "#FFEAB7" - background = "#F4F6F8" - surface = "#FFFFFF" - surface_alt = "#F0F2F5" - accent = "#FFC845" - info = "#007FBD" - success = "#85C446" - text_primary = "#1E2A3A" - text_secondary = "#5A6779" - text_on_primary = "#FFFFFF" - border = "#DDE3EA" - - -class SearchableDropdown(ft.Container): - """Выпадающий список с поисковой строкой.""" - - def __init__(self, label: str = "", hint_text: str = "", options=None, on_select=None, width: int | None = None): - self._label_str = label - self._hint_str = hint_text - self._options: list = options or [] - self.on_select = on_select - self._value = None - - self._value_text = ft.Text( - hint_text, - color=colors.border, - size=s(14), - italic=True, - expand=True, - no_wrap=True, - overflow=ft.TextOverflow.ELLIPSIS, - ) - - super().__init__( - content=ft.Column( - spacing=s(2), - tight=True, - controls=[ - ft.Text(label, color=colors.text_secondary, size=s(12), weight=ft.FontWeight.W_500), - ft.Row( - controls=[ - self._value_text, - ft.Icon(ft.Icons.ARROW_DROP_DOWN, color=colors.text_secondary, size=s(20)), - ], - alignment=ft.MainAxisAlignment.SPACE_BETWEEN, - vertical_alignment=ft.CrossAxisAlignment.CENTER, - ), - ], - ), - bgcolor=colors.surface, - border=ft.border.all(1, colors.border), - border_radius=s(10), - padding=ft.padding.symmetric(horizontal=s(12), vertical=s(10)), - on_click=self._open_dialog, - ink=True, - width=width or s(220), - ) - - @property - def value(self): - return self._value - - @value.setter - def value(self, val): - self._value = val - if val is None: - self._value_text.value = self._hint_str - self._value_text.color = colors.border - self._value_text.italic = True - else: - for opt in self._options: - if opt.key == val: - self._value_text.value = opt.text - self._value_text.color = colors.text_primary - self._value_text.italic = False - break - try: - if self.page: - self.update() - except RuntimeError: - pass - - @property - def options(self): - return self._options - - @options.setter - def options(self, opts): - self._options = opts or [] - - @property - def hint_text(self): - return self._hint_str - - @hint_text.setter - def hint_text(self, val): - self._hint_str = val - if self._value is None: - self._value_text.value = val - if self.page: - self.update() - - def _open_dialog(self, e): - search_field = ft.TextField( - label="Поиск", - hint_text="Введите для поиска...", - prefix_icon=ft.Icons.SEARCH, - autofocus=True, - filled=True, - bgcolor=colors.surface, - border_color=colors.border, - focused_border_color=colors.primary, - border_radius=s(8), - text_style=ft.TextStyle(color=colors.text_primary, size=s(14)), - ) - items_list = ft.ListView(spacing=0, expand=True) - - def _build_items(query: str = ""): - q = query.lower() - items_list.controls = [ - ft.ListTile( - title=ft.Text(opt.text, color=colors.text_primary, size=s(13)), - hover_color=colors.primary_light, - on_click=lambda ev, o=opt: _select(o), - ) - for opt in self._options - if not q or q in opt.text.lower() - ] - - def _on_search(ev): - _build_items(search_field.value or "") - items_list.update() - - def _select(option): - self._value = option.key - self._value_text.value = option.text - self._value_text.color = colors.text_primary - self._value_text.italic = False - dlg.open = False - self.page.update() - self.update() - if self.on_select: - self.on_select(type("_E", (), {"control": self})()) - - search_field.on_change = _on_search - _build_items() - - dlg = ft.AlertDialog( - title=ft.Text(self._label_str, color=colors.text_primary, size=s(15), weight=ft.FontWeight.W_600), - content=ft.Container( - content=ft.Column( - controls=[search_field, ft.Divider(color=colors.border), items_list], - spacing=s(8), - tight=True, - ), - height=s(320), - width=s(380), - ), - bgcolor=colors.surface, - shape=ft.RoundedRectangleBorder(radius=s(12)), - ) - self.page.overlay.append(dlg) - dlg.open = True - self.page.update() - - -def stat_card(title: str, value: str, icon: str, color: str) -> ft.Container: - """Карточка с одной цифрой для summary-панели.""" - return ft.Container( - content=ft.Column( - spacing=s(6), - tight=True, - controls=[ - ft.Row( - spacing=s(8), - controls=[ - ft.Icon(icon, color=color, size=s(20)), - ft.Text(title, color=colors.text_secondary, size=s(12)), - ], - ), - ft.Text(value, color=colors.text_primary, size=s(28), weight=ft.FontWeight.BOLD), - ], - ), - bgcolor=colors.surface, - border_radius=s(12), - padding=ft.padding.all(s(16)), - border=ft.border.all(1, colors.border), - expand=True, - ) - - -def chart_card(title: str, body: ft.Control) -> ft.Container: - """Обёртка-карточка для графика.""" - return ft.Container( - content=ft.Column( - spacing=s(12), - controls=[ - ft.Text(title, color=colors.text_primary, size=s(15), weight=ft.FontWeight.W_600), - body, - ], - ), - bgcolor=colors.surface, - border_radius=s(12), - padding=ft.padding.all(s(16)), - border=ft.border.all(1, colors.border), - expand=True, - ) diff --git a/stats/main.py b/stats/main.py deleted file mode 100644 index 33b3a11..0000000 --- a/stats/main.py +++ /dev/null @@ -1,19 +0,0 @@ -import flet as ft -import designer as ds -from dashboard import DashboardView -from starlette.applications import Starlette -from starlette.routing import Mount - - -async def main(page: ft.Page): - page.title = "ЦОПП — Статистика" - page.bgcolor = ds.colors.background - page.padding = ft.padding.all(0) - page.scroll = ft.ScrollMode.HIDDEN - - view = DashboardView(page) - page.add(view) - - -_flet_app = ft.run(main, export_asgi_app=True, assets_dir="assets") -app = Starlette(routes=[Mount("/", _flet_app)])