commit fa3003407c99819283a378a1091710d4cd49af34 Author: neizbejnoezlo <137374284+neizbejnoezlo@users.noreply.github.com> Date: Wed Jul 15 10:57:43 2026 +0700 Начало бэкенда и сырой фронтенд на моках diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..6181c8d --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,6 @@ +DB_HOST=localhost:55432 +DB_NAME=fleet_ble_monitor +DB_USER=fleet_user +DB_PASSWORD=fleet_password +SERVER_PORT=8080 +SEED_ENABLED=true diff --git a/backend/.gradle/9.6.1/checksums/checksums.lock b/backend/.gradle/9.6.1/checksums/checksums.lock new file mode 100644 index 0000000..237bd1c Binary files /dev/null and b/backend/.gradle/9.6.1/checksums/checksums.lock differ diff --git a/backend/.gradle/9.6.1/checksums/md5-checksums.bin b/backend/.gradle/9.6.1/checksums/md5-checksums.bin new file mode 100644 index 0000000..9eeb3cf Binary files /dev/null and b/backend/.gradle/9.6.1/checksums/md5-checksums.bin differ diff --git a/backend/.gradle/9.6.1/checksums/sha1-checksums.bin b/backend/.gradle/9.6.1/checksums/sha1-checksums.bin new file mode 100644 index 0000000..5af3dae Binary files /dev/null and b/backend/.gradle/9.6.1/checksums/sha1-checksums.bin differ diff --git a/backend/.gradle/9.6.1/executionHistory/executionHistory.bin b/backend/.gradle/9.6.1/executionHistory/executionHistory.bin new file mode 100644 index 0000000..a6bfbe9 Binary files /dev/null and b/backend/.gradle/9.6.1/executionHistory/executionHistory.bin differ diff --git a/backend/.gradle/9.6.1/executionHistory/executionHistory.lock b/backend/.gradle/9.6.1/executionHistory/executionHistory.lock new file mode 100644 index 0000000..c24da6d Binary files /dev/null and b/backend/.gradle/9.6.1/executionHistory/executionHistory.lock differ diff --git a/backend/.gradle/9.6.1/fileChanges/last-build.bin b/backend/.gradle/9.6.1/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/backend/.gradle/9.6.1/fileChanges/last-build.bin differ diff --git a/backend/.gradle/9.6.1/fileHashes/fileHashes.bin b/backend/.gradle/9.6.1/fileHashes/fileHashes.bin new file mode 100644 index 0000000..942f785 Binary files /dev/null and b/backend/.gradle/9.6.1/fileHashes/fileHashes.bin differ diff --git a/backend/.gradle/9.6.1/fileHashes/fileHashes.lock b/backend/.gradle/9.6.1/fileHashes/fileHashes.lock new file mode 100644 index 0000000..0d1f292 Binary files /dev/null and b/backend/.gradle/9.6.1/fileHashes/fileHashes.lock differ diff --git a/backend/.gradle/9.6.1/fileHashes/resourceHashesCache.bin b/backend/.gradle/9.6.1/fileHashes/resourceHashesCache.bin new file mode 100644 index 0000000..df59583 Binary files /dev/null and b/backend/.gradle/9.6.1/fileHashes/resourceHashesCache.bin differ diff --git a/backend/.gradle/9.6.1/gc.properties b/backend/.gradle/9.6.1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/backend/.gradle/9.6.1/kotlin-dsl-plugin-entries/kotlin-dsl-plugin-entries.lock b/backend/.gradle/9.6.1/kotlin-dsl-plugin-entries/kotlin-dsl-plugin-entries.lock new file mode 100644 index 0000000..0ee49fa Binary files /dev/null and b/backend/.gradle/9.6.1/kotlin-dsl-plugin-entries/kotlin-dsl-plugin-entries.lock differ diff --git a/backend/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/backend/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..db38c25 Binary files /dev/null and b/backend/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/backend/.gradle/buildOutputCleanup/cache.properties b/backend/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..b1b4a8d --- /dev/null +++ b/backend/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Tue Jul 14 15:10:51 NOVT 2026 +gradle.version=9.6.1 diff --git a/backend/.gradle/buildOutputCleanup/outputFiles.bin b/backend/.gradle/buildOutputCleanup/outputFiles.bin new file mode 100644 index 0000000..3f4833a Binary files /dev/null and b/backend/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/backend/.gradle/file-system.probe b/backend/.gradle/file-system.probe new file mode 100644 index 0000000..c01b10c Binary files /dev/null and b/backend/.gradle/file-system.probe differ diff --git a/backend/.gradle/vcs-1/gc.properties b/backend/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/backend/.idea/.gitignore b/backend/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/backend/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/backend/.idea/.name b/backend/.idea/.name new file mode 100644 index 0000000..f8ea675 --- /dev/null +++ b/backend/.idea/.name @@ -0,0 +1 @@ +fleet-ble-monitor-backend \ No newline at end of file diff --git a/backend/.idea/compiler.xml b/backend/.idea/compiler.xml new file mode 100644 index 0000000..e5a8359 --- /dev/null +++ b/backend/.idea/compiler.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/backend/.idea/gradle.xml b/backend/.idea/gradle.xml new file mode 100644 index 0000000..f2c1963 --- /dev/null +++ b/backend/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/backend/.idea/kotlinNotebook.xml b/backend/.idea/kotlinNotebook.xml new file mode 100644 index 0000000..dfd5d96 --- /dev/null +++ b/backend/.idea/kotlinNotebook.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/backend/.idea/kotlinc.xml b/backend/.idea/kotlinc.xml new file mode 100644 index 0000000..7ca2a83 --- /dev/null +++ b/backend/.idea/kotlinc.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/backend/.idea/misc.xml b/backend/.idea/misc.xml new file mode 100644 index 0000000..58e94fc --- /dev/null +++ b/backend/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + {} + + + \ No newline at end of file diff --git a/backend/.idea/vcs.xml b/backend/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/backend/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/.kotlin/errors/errors-1784081388339.log b/backend/.kotlin/errors/errors-1784081388339.log new file mode 100644 index 0000000..afc4f91 --- /dev/null +++ b/backend/.kotlin/errors/errors-1784081388339.log @@ -0,0 +1,3 @@ +kotlin version: 2.4.0 +error message: Daemon compilation failed + diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..064531f --- /dev/null +++ b/backend/README.md @@ -0,0 +1,161 @@ +# Fleet BLE Monitor Backend + +Backend starter для системы Fleet BLE Monitor. + +Стек: + +- Kotlin +- Spring Boot +- Spring GraphQL +- Spring Data JPA +- PostgreSQL +- Docker Compose для локальной базы + +## Быстрый локальный запуск + +Из папки `backend`: + +```powershell +cd docker +docker compose up -d +cd .. +.\gradlew.bat clean bootRun +``` + +После запуска приложение доступно по адресу: + +```text +http://localhost:8080/api +``` + +GraphiQL: + +```text +http://localhost:8080/api/graphiql +``` + +GraphQL endpoint: + +```text +http://localhost:8080/api/graphql +``` + +## Локальная PostgreSQL + +Docker Compose поднимает контейнер: + +```text +fleet-ble-monitor-postgres +``` + +Внутри контейнера PostgreSQL слушает порт `5432`, но на хост проброшен порт `55432`. + +Это сделано специально, чтобы локальный запуск не конфликтовал с установленным на машине PostgreSQL, который часто уже занимает `localhost:5432`. + +Параметры подключения по умолчанию: + +```text +DB_HOST=localhost:55432 +DB_NAME=fleet_ble_monitor +DB_USER=fleet_user +DB_PASSWORD=fleet_password +SERVER_PORT=8080 +SEED_ENABLED=true +``` + +Эти же значения указаны в `.env.example`. + +Spring использует переменные: + +- `DB_HOST` +- `DB_NAME` +- `DB_USER` +- `DB_PASSWORD` + +Если переменные не заданы, используются дефолты из `src/main/resources/application.yml`. + +## Полный сброс локальной базы + +Если контейнер уже создавался раньше с другими `POSTGRES_USER` / `POSTGRES_PASSWORD`, PostgreSQL сохранит старые учетные данные в Docker volume. В таком случае изменение `docker-compose.yml` уже не поменяет пароль существующей базы. + +Для полного сброса локальной базы: + +```powershell +cd docker +docker compose down -v +docker compose up -d +cd .. +.\gradlew.bat clean bootRun +``` + +Команда `docker compose down -v` удаляет локальный volume PostgreSQL. Все локальные данные будут потеряны. + +## Тестовые пользователи + +Seed-данные создаются при первом запуске, если база пустая и `SEED_ENABLED=true`. + +```text +admin / 123456 +admin@fleet.local / 123456 + +user / 123456 +user@fleet.local / 123456 +``` + +## Пример login mutation + +```graphql +mutation { + login(payload: { loginOrEmail: "admin", password: "123456" }) { + token + user { + id + login + email + role + } + } +} +``` + +Для запросов, требующих авторизации, передавай токен в HTTP header: + +```text +Authorization: Bearer +``` + +## Пример списка АТТ + +```graphql +query { + getMachines( + page: 0 + pageSize: 20 + sortDirection: ASC + sortField: NAME + ) { + totalElements + page { + id + name + licensePlate + lastKnownState + beacon { + identifier + macAddress + status + } + } + } +} +``` + +## Что доделывать дальше + +- нормальную авторизацию через JWT/AD/LDAP; +- миграции Flyway/Liquibase вместо `ddl-auto: update`; +- полноценный алгоритм распознавания состояний из телеметрии; +- генерацию XLSX/PDF на backend; +- scheduler рассылки отчетов; +- права доступа на уровне GraphQL resolver/use-case. + diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts new file mode 100644 index 0000000..0dd7c8a --- /dev/null +++ b/backend/build.gradle.kts @@ -0,0 +1,47 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("org.springframework.boot") version "4.1.0" + id("io.spring.dependency-management") version "1.1.7" + + kotlin("jvm") version "2.4.0" + kotlin("plugin.spring") version "2.4.0" + kotlin("plugin.jpa") version "2.4.0" +} + +group = "llc.arma" +version = "0.1.0" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +kotlin { + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict") + jvmTarget.set(JvmTarget.JVM_17) + } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-graphql") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("com.graphql-java:graphql-java-extended-scalars:24.0") + + runtimeOnly("org.postgresql:postgresql") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.graphql:spring-graphql-test") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/backend/build/classes/kotlin/main/META-INF/llc.arma_fleet-ble-monitor-backend.kotlin_module b/backend/build/classes/kotlin/main/META-INF/llc.arma_fleet-ble-monitor-backend.kotlin_module new file mode 100644 index 0000000..1ec3feb Binary files /dev/null and b/backend/build/classes/kotlin/main/META-INF/llc.arma_fleet-ble-monitor-backend.kotlin_module differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/FleetBleMonitorApplication.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/FleetBleMonitorApplication.class new file mode 100644 index 0000000..f50afa9 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/FleetBleMonitorApplication.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/FleetBleMonitorApplicationKt.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/FleetBleMonitorApplicationKt.class new file mode 100644 index 0000000..6cb8944 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/FleetBleMonitorApplicationKt.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/config/DataSeeder.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/config/DataSeeder.class new file mode 100644 index 0000000..4066ca8 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/config/DataSeeder.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/config/GraphQlConfig.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/config/GraphQlConfig.class new file mode 100644 index 0000000..efe3f95 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/config/GraphQlConfig.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/BaseEntity.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/BaseEntity.class new file mode 100644 index 0000000..5483fdd Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/BaseEntity.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/BeaconEntity.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/BeaconEntity.class new file mode 100644 index 0000000..193989d Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/BeaconEntity.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/BeaconStatus.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/BeaconStatus.class new file mode 100644 index 0000000..306163f Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/BeaconStatus.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/BeaconTelemetryPacketEntity.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/BeaconTelemetryPacketEntity.class new file mode 100644 index 0000000..6099776 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/BeaconTelemetryPacketEntity.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/DepartmentEntity.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/DepartmentEntity.class new file mode 100644 index 0000000..6377453 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/DepartmentEntity.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineCalibrationRuleEntity.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineCalibrationRuleEntity.class new file mode 100644 index 0000000..3bcffa8 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineCalibrationRuleEntity.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineEntity.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineEntity.class new file mode 100644 index 0000000..e785b86 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineEntity.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineSortField.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineSortField.class new file mode 100644 index 0000000..db31590 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineSortField.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineStateCode.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineStateCode.class new file mode 100644 index 0000000..7d28c46 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineStateCode.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineStateDefinitionEntity.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineStateDefinitionEntity.class new file mode 100644 index 0000000..dfa9687 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineStateDefinitionEntity.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineStateIntervalEntity.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineStateIntervalEntity.class new file mode 100644 index 0000000..fb7804e Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineStateIntervalEntity.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineTypeEntity.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineTypeEntity.class new file mode 100644 index 0000000..6f8735b Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/MachineTypeEntity.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/ReportDeliveryStatus.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/ReportDeliveryStatus.class new file mode 100644 index 0000000..2e6892d Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/ReportDeliveryStatus.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/ReportFormat.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/ReportFormat.class new file mode 100644 index 0000000..9c081df Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/ReportFormat.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/ReportPeriod.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/ReportPeriod.class new file mode 100644 index 0000000..fcee52b Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/ReportPeriod.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/SortDirection.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/SortDirection.class new file mode 100644 index 0000000..32fcb9b Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/SortDirection.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/UserEntity.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/UserEntity.class new file mode 100644 index 0000000..1f3ce1c Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/UserEntity.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/UserRole.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/UserRole.class new file mode 100644 index 0000000..81b4153 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/UserRole.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/UserSortField.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/UserSortField.class new file mode 100644 index 0000000..bb3c016 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/entity/UserSortField.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/BeaconRepository.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/BeaconRepository.class new file mode 100644 index 0000000..9e3293a Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/BeaconRepository.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/BeaconTelemetryPacketRepository.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/BeaconTelemetryPacketRepository.class new file mode 100644 index 0000000..2a4ec82 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/BeaconTelemetryPacketRepository.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/DepartmentRepository.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/DepartmentRepository.class new file mode 100644 index 0000000..935fe8b Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/DepartmentRepository.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineCalibrationRuleRepository.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineCalibrationRuleRepository.class new file mode 100644 index 0000000..7c18f55 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineCalibrationRuleRepository.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineRepository.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineRepository.class new file mode 100644 index 0000000..29c4c41 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineRepository.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineStateDefinitionRepository.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineStateDefinitionRepository.class new file mode 100644 index 0000000..371ef2f Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineStateDefinitionRepository.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineStateIntervalRepository.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineStateIntervalRepository.class new file mode 100644 index 0000000..dcb0682 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineStateIntervalRepository.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineTypeRepository.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineTypeRepository.class new file mode 100644 index 0000000..392a671 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/MachineTypeRepository.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/UserRepository.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/UserRepository.class new file mode 100644 index 0000000..a312338 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/data/repository/UserRepository.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/AuthService.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/AuthService.class new file mode 100644 index 0000000..0d1a6d4 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/AuthService.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/FleetReportService.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/FleetReportService.class new file mode 100644 index 0000000..9dbefee Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/FleetReportService.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/MachineAnalyticsService.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/MachineAnalyticsService.class new file mode 100644 index 0000000..10ad453 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/MachineAnalyticsService.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/MachineService$WhenMappings.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/MachineService$WhenMappings.class new file mode 100644 index 0000000..58ae009 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/MachineService$WhenMappings.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/MachineService.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/MachineService.class new file mode 100644 index 0000000..fcdda07 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/MachineService.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/MachineStateService.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/MachineStateService.class new file mode 100644 index 0000000..748f3b2 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/MachineStateService.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/ReferenceService.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/ReferenceService.class new file mode 100644 index 0000000..2b0f6a0 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/ReferenceService.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/TelemetryService.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/TelemetryService.class new file mode 100644 index 0000000..c9c8323 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/TelemetryService.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/UserService$WhenMappings.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/UserService$WhenMappings.class new file mode 100644 index 0000000..96bccda Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/UserService$WhenMappings.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/UserService.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/UserService.class new file mode 100644 index 0000000..63102db Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/domain/service/UserService.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/AuthGraphQlController.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/AuthGraphQlController.class new file mode 100644 index 0000000..f71a4b8 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/AuthGraphQlController.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/FleetReportGraphQlController.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/FleetReportGraphQlController.class new file mode 100644 index 0000000..c3cdf43 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/FleetReportGraphQlController.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/MachineGraphQlController.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/MachineGraphQlController.class new file mode 100644 index 0000000..f2312f2 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/MachineGraphQlController.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/MachineStateGraphQlController.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/MachineStateGraphQlController.class new file mode 100644 index 0000000..b1d666d Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/MachineStateGraphQlController.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/ReferenceGraphQlController.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/ReferenceGraphQlController.class new file mode 100644 index 0000000..2911f96 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/ReferenceGraphQlController.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/TelemetryGraphQlController.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/TelemetryGraphQlController.class new file mode 100644 index 0000000..b339146 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/TelemetryGraphQlController.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/UserGraphQlController.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/UserGraphQlController.class new file mode 100644 index 0000000..af866b1 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/graphql/UserGraphQlController.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/AuthPayload.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/AuthPayload.class new file mode 100644 index 0000000..ff3cf2b Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/AuthPayload.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/BeaconTelemetryPayload.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/BeaconTelemetryPayload.class new file mode 100644 index 0000000..dc53213 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/BeaconTelemetryPayload.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/CreateMachineCalibrationRulePayload.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/CreateMachineCalibrationRulePayload.class new file mode 100644 index 0000000..3d76fa0 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/CreateMachineCalibrationRulePayload.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/CreateMachinePayload.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/CreateMachinePayload.class new file mode 100644 index 0000000..62d9de2 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/CreateMachinePayload.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/CreateMachineStateDefinitionPayload.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/CreateMachineStateDefinitionPayload.class new file mode 100644 index 0000000..c4804b1 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/CreateMachineStateDefinitionPayload.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/CreateUserPayload.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/CreateUserPayload.class new file mode 100644 index 0000000..9aef390 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/CreateUserPayload.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/FleetDailyReport.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/FleetDailyReport.class new file mode 100644 index 0000000..71071aa Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/FleetDailyReport.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/FleetDailyReportHistoryRow.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/FleetDailyReportHistoryRow.class new file mode 100644 index 0000000..e1ef467 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/FleetDailyReportHistoryRow.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/FleetDailyReportMachineRow.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/FleetDailyReportMachineRow.class new file mode 100644 index 0000000..e40ce7b Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/FleetDailyReportMachineRow.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/FleetDailyReportSummary.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/FleetDailyReportSummary.class new file mode 100644 index 0000000..8c1dab9 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/FleetDailyReportSummary.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/GenerateFleetDailyReportPayload.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/GenerateFleetDailyReportPayload.class new file mode 100644 index 0000000..6fe3ac1 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/GenerateFleetDailyReportPayload.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/LoginPayload.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/LoginPayload.class new file mode 100644 index 0000000..b0dbbf9 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/LoginPayload.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/MachineDailyUsageStat.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/MachineDailyUsageStat.class new file mode 100644 index 0000000..fe8a62a Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/MachineDailyUsageStat.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/MachineStateHistoryPage.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/MachineStateHistoryPage.class new file mode 100644 index 0000000..a068d77 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/MachineStateHistoryPage.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/MachineUsageSummary.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/MachineUsageSummary.class new file mode 100644 index 0000000..7a334d2 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/MachineUsageSummary.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/MachinesPage.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/MachinesPage.class new file mode 100644 index 0000000..2ba12f9 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/MachinesPage.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/RefreshSessionPayload.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/RefreshSessionPayload.class new file mode 100644 index 0000000..cff7f96 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/RefreshSessionPayload.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/ReportFile.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/ReportFile.class new file mode 100644 index 0000000..e3977be Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/ReportFile.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UpdateMachineCalibrationRulePayload.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UpdateMachineCalibrationRulePayload.class new file mode 100644 index 0000000..3d16a12 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UpdateMachineCalibrationRulePayload.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UpdateMachinePayload.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UpdateMachinePayload.class new file mode 100644 index 0000000..4f22243 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UpdateMachinePayload.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UpdateMachineStateDefinitionPayload.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UpdateMachineStateDefinitionPayload.class new file mode 100644 index 0000000..1a19fe0 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UpdateMachineStateDefinitionPayload.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UpdateUserPayload.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UpdateUserPayload.class new file mode 100644 index 0000000..de52078 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UpdateUserPayload.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UsersPage.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UsersPage.class new file mode 100644 index 0000000..88edfb3 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/presentation/model/UsersPage.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/AuthTokenStore$AuthSession.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/AuthTokenStore$AuthSession.class new file mode 100644 index 0000000..f48b75e Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/AuthTokenStore$AuthSession.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/AuthTokenStore$TokenRecord.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/AuthTokenStore$TokenRecord.class new file mode 100644 index 0000000..24c4557 Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/AuthTokenStore$TokenRecord.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/AuthTokenStore.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/AuthTokenStore.class new file mode 100644 index 0000000..c84160a Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/AuthTokenStore.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/DemoTokenAuthFilter.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/DemoTokenAuthFilter.class new file mode 100644 index 0000000..b1f52ce Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/DemoTokenAuthFilter.class differ diff --git a/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/SecurityConfig.class b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/SecurityConfig.class new file mode 100644 index 0000000..857d3ff Binary files /dev/null and b/backend/build/classes/kotlin/main/llc/arma/fleet/monitor/security/SecurityConfig.class differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab new file mode 100644 index 0000000..8aad32b Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab.keystream new file mode 100644 index 0000000..08e7df1 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab.keystream.len new file mode 100644 index 0000000..eb52963 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab.len new file mode 100644 index 0000000..2a17e6e Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab.values.at new file mode 100644 index 0000000..5875372 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab_i.len new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/outputs-generated-for-plugins.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab new file mode 100644 index 0000000..8aad32b Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab.keystream new file mode 100644 index 0000000..08e7df1 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab.keystream.len new file mode 100644 index 0000000..eb52963 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab.len new file mode 100644 index 0000000..2a17e6e Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab.values.at new file mode 100644 index 0000000..5875372 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab_i.len new file mode 100644 index 0000000..1b1cb4d Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/compilerPluginFiles/sources-referenced-by-plugins.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab new file mode 100644 index 0000000..72b6964 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream new file mode 100644 index 0000000..103cb01 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len new file mode 100644 index 0000000..57e1942 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len new file mode 100644 index 0000000..41d6c24 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at new file mode 100644 index 0000000..c28750e Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i new file mode 100644 index 0000000..6a8b5aa Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len new file mode 100644 index 0000000..131e265 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/inputs/source-to-output.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab new file mode 100644 index 0000000..1854504 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream new file mode 100644 index 0000000..d218022 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len new file mode 100644 index 0000000..311d94c Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len new file mode 100644 index 0000000..3e5b6a5 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at new file mode 100644 index 0000000..298c4e5 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i new file mode 100644 index 0000000..cac2046 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len new file mode 100644 index 0000000..131e265 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab new file mode 100644 index 0000000..170d07d Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream new file mode 100644 index 0000000..d218022 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len new file mode 100644 index 0000000..311d94c Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len new file mode 100644 index 0000000..3e5b6a5 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at new file mode 100644 index 0000000..b12b01a Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i new file mode 100644 index 0000000..cac2046 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len new file mode 100644 index 0000000..131e265 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab new file mode 100644 index 0000000..a08bf83 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream new file mode 100644 index 0000000..137983a Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len new file mode 100644 index 0000000..e9f38fd Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len new file mode 100644 index 0000000..7ce6409 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at new file mode 100644 index 0000000..47d42d5 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i new file mode 100644 index 0000000..a5294eb Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len new file mode 100644 index 0000000..131e265 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab new file mode 100644 index 0000000..bdf584a Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab.keystream new file mode 100644 index 0000000..bc736b4 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab.keystream.len new file mode 100644 index 0000000..002e02c Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab.len new file mode 100644 index 0000000..2a17e6e Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab.values.at new file mode 100644 index 0000000..46d6744 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab_i b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab_i new file mode 100644 index 0000000..22c5e20 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab_i differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab_i.len new file mode 100644 index 0000000..131e265 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab new file mode 100644 index 0000000..13dc384 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream new file mode 100644 index 0000000..b13dcd5 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len new file mode 100644 index 0000000..08251e3 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len new file mode 100644 index 0000000..ed45ee1 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values new file mode 100644 index 0000000..a8f2e22 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at new file mode 100644 index 0000000..c2b5bc3 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.s b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.s new file mode 100644 index 0000000..a323663 --- /dev/null +++ b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.s @@ -0,0 +1 @@ +ё \ No newline at end of file diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i new file mode 100644 index 0000000..6befb34 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len new file mode 100644 index 0000000..131e265 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab new file mode 100644 index 0000000..f434c23 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream new file mode 100644 index 0000000..103cb01 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len new file mode 100644 index 0000000..57e1942 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len new file mode 100644 index 0000000..41d6c24 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at new file mode 100644 index 0000000..cbfdff4 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i new file mode 100644 index 0000000..6a8b5aa Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len new file mode 100644 index 0000000..131e265 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab new file mode 100644 index 0000000..18995ec Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream new file mode 100644 index 0000000..61b5b5c Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream.len new file mode 100644 index 0000000..393366c Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.len new file mode 100644 index 0000000..ec8f944 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at new file mode 100644 index 0000000..263324d Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i new file mode 100644 index 0000000..5b41895 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i.len new file mode 100644 index 0000000..131e265 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab new file mode 100644 index 0000000..9d444b8 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream new file mode 100644 index 0000000..7efa756 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len new file mode 100644 index 0000000..418febb Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.len new file mode 100644 index 0000000..8d483a8 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at new file mode 100644 index 0000000..54a7edb Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i new file mode 100644 index 0000000..28bcde4 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i.len new file mode 100644 index 0000000..131e265 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/counters.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/counters.tab new file mode 100644 index 0000000..672070d --- /dev/null +++ b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/counters.tab @@ -0,0 +1,2 @@ +44 +0 \ No newline at end of file diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab new file mode 100644 index 0000000..0e6a69c Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream new file mode 100644 index 0000000..103cb01 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len new file mode 100644 index 0000000..57e1942 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len new file mode 100644 index 0000000..41d6c24 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at new file mode 100644 index 0000000..d3fcc7e Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i new file mode 100644 index 0000000..6a8b5aa Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len new file mode 100644 index 0000000..131e265 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/file-to-id.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab new file mode 100644 index 0000000..4b5b150 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream new file mode 100644 index 0000000..132a271 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len new file mode 100644 index 0000000..79ad34c Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len new file mode 100644 index 0000000..41d6c24 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at new file mode 100644 index 0000000..0fdfb93 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i new file mode 100644 index 0000000..1601c02 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len new file mode 100644 index 0000000..131e265 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab new file mode 100644 index 0000000..df0fc4f Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream new file mode 100644 index 0000000..dcacb66 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len new file mode 100644 index 0000000..ec8f3c0 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.len new file mode 100644 index 0000000..b63aaac Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at new file mode 100644 index 0000000..269afab Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i new file mode 100644 index 0000000..4b56f8e Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len new file mode 100644 index 0000000..131e265 Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/caches-jvm/lookups/lookups.tab_i.len differ diff --git a/backend/build/kotlin/compileKotlin/cacheable/last-build.bin b/backend/build/kotlin/compileKotlin/cacheable/last-build.bin new file mode 100644 index 0000000..e643f3b Binary files /dev/null and b/backend/build/kotlin/compileKotlin/cacheable/last-build.bin differ diff --git a/backend/build/kotlin/compileKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin b/backend/build/kotlin/compileKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin new file mode 100644 index 0000000..a0eda8b Binary files /dev/null and b/backend/build/kotlin/compileKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin differ diff --git a/backend/build/resolvedMainClassName b/backend/build/resolvedMainClassName new file mode 100644 index 0000000..2523021 --- /dev/null +++ b/backend/build/resolvedMainClassName @@ -0,0 +1 @@ +llc.arma.fleet.monitor.FleetBleMonitorApplicationKt \ No newline at end of file diff --git a/backend/build/resources/main/application.yml b/backend/build/resources/main/application.yml new file mode 100644 index 0000000..8978c50 --- /dev/null +++ b/backend/build/resources/main/application.yml @@ -0,0 +1,43 @@ +spring: + application: + name: fleet-ble-monitor-backend + + graphql: + path: /graphql + schema: + locations: classpath:graphql/ + file-extensions: .graphqls + graphiql: + enabled: true + path: /graphiql + cors: + allow-credentials: true + allowed-origin-patterns: + - "*" + + datasource: + url: jdbc:postgresql://${DB_HOST:localhost:55432}/${DB_NAME:fleet_ble_monitor} + username: ${DB_USER:fleet_user} + password: ${DB_PASSWORD:fleet_password} + driver-class-name: org.postgresql.Driver + + jpa: + open-in-view: false + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + +server: + port: ${SERVER_PORT:8080} + servlet: + context-path: /api + +app: + seed: + enabled: ${SEED_ENABLED:true} + auth: + demo-token-ttl-minutes: ${DEMO_TOKEN_TTL_MINUTES:720} + access-token-ttl-minutes: ${ACCESS_TOKEN_TTL_MINUTES:720} + refresh-token-ttl-minutes: ${REFRESH_TOKEN_TTL_MINUTES:10080} diff --git a/backend/build/resources/main/graphql/schema.graphqls b/backend/build/resources/main/graphql/schema.graphqls new file mode 100644 index 0000000..006775a --- /dev/null +++ b/backend/build/resources/main/graphql/schema.graphqls @@ -0,0 +1,431 @@ +scalar DateTime +scalar Date +scalar Long + +type Query { + currentUser: User + + getUsers( + page: Int! + pageSize: Int + query: String + roles: [UserRole!] + sortDirection: SortDirection! + sortField: UserSortField! + ): UsersPage! + + getUser(id: Long!): User + + getMachines( + page: Int! + pageSize: Int + query: String + sortDirection: SortDirection! + sortField: MachineSortField! + ): MachinesPage! + + getMachine(id: Long!): Machine + + getMachineTypes: [MachineType!]! + getDepartments: [Department!]! + getBeacons: [Beacon!]! + getFreeBeacons: [Beacon!]! + + getBeaconTelemetry( + beaconId: Long + machineId: Long + startDate: DateTime! + endDate: DateTime! + ): [BeaconTelemetryPacket!]! + + getMachineStateDefinitions: [MachineStateDefinition!]! + getMachineCalibrationRules(machineId: Long!): [MachineCalibrationRule!]! + + getMachineStateHistory( + machineId: Long! + startDate: DateTime! + endDate: DateTime! + page: Int + pageSize: Int + ): MachineStateHistoryPage! + + getMachineUsageSummary( + machineId: Long! + startDate: DateTime! + endDate: DateTime! + ): MachineUsageSummary! + + getUnrecognizedTelemetryIntervals( + machineId: Long! + startDate: DateTime! + endDate: DateTime! + ): [MachineStateInterval!]! + + getMachineDailyUsageStats( + machineId: Long! + startDate: DateTime! + endDate: DateTime! + ): [MachineDailyUsageStat!]! + + getFleetDailyReport(date: Date!): FleetDailyReport! + + createFleetDailyReportDownloadUrl( + payload: GenerateFleetDailyReportPayload! + ): ReportFile! +} + +type Mutation { + login(payload: LoginPayload!): AuthPayload! + refreshSession(payload: RefreshSessionPayload!): AuthPayload! + logout: Boolean! + + createUser(payload: CreateUserPayload!): User! + updateUser(id: Long!, payload: UpdateUserPayload!): User! + deleteUser(id: Long!): Boolean! + + createMachine(payload: CreateMachinePayload!): Machine! + updateMachine(id: Long!, payload: UpdateMachinePayload!): Machine! + deleteMachine(id: Long!): Boolean! + + bindBeaconToMachine(machineId: Long!, beaconId: Long!): Machine! + unbindBeaconFromMachine(machineId: Long!): Machine! + + registerBeaconTelemetryPacket(payload: BeaconTelemetryPayload!): Boolean! + + createMachineType(name: String!): MachineType! + updateMachineType(id: Long!, name: String!): MachineType! + deleteMachineType(id: Long!): Boolean! + + createDepartment(name: String!): Department! + updateDepartment(id: Long!, name: String!): Department! + deleteDepartment(id: Long!): Boolean! + + createMachineStateDefinition(payload: CreateMachineStateDefinitionPayload!): MachineStateDefinition! + updateMachineStateDefinition(id: Long!, payload: UpdateMachineStateDefinitionPayload!): MachineStateDefinition! + deleteMachineStateDefinition(id: Long!): Boolean! + + createMachineCalibrationRule(payload: CreateMachineCalibrationRulePayload!): MachineCalibrationRule! + updateMachineCalibrationRule(id: Long!, payload: UpdateMachineCalibrationRulePayload!): MachineCalibrationRule! + deleteMachineCalibrationRule(id: Long!): Boolean! + + recognizeMachineStates(machineId: Long!, startDate: DateTime!, endDate: DateTime!): [MachineStateInterval!]! +} + +type AuthPayload { + user: User! + accessToken: String! + refreshToken: String! + token: String! +} + +type User { + id: Long! + lastName: String! + firstName: String! + middleName: String + email: String! + login: String! + role: UserRole! + active: Boolean! + createdAt: DateTime! + updatedAt: DateTime! +} + +enum UserRole { + ADMIN + USER +} + +type UsersPage { + totalPages: Int! + totalElements: Long! + page: [User!]! +} + +enum UserSortField { + ID + NAME + DATE +} + +enum SortDirection { + ASC + DESC +} + +input LoginPayload { + loginOrEmail: String! + password: String! +} + +input RefreshSessionPayload { + refreshToken: String! +} + +input CreateUserPayload { + lastName: String! + firstName: String! + middleName: String + email: String! + login: String! + password: String! + role: UserRole! +} + +input UpdateUserPayload { + lastName: String + firstName: String + middleName: String + email: String + login: String + password: String + role: UserRole + active: Boolean +} + +type Machine { + id: Long! + name: String! + licensePlate: String! + type: MachineType! + department: Department! + description: String + beacon: Beacon + lastKnownState: MachineStateCode! + lastDataReceivedAt: DateTime + createdAt: DateTime! + updatedAt: DateTime! +} + +type MachineType { + id: Long! + name: String! + description: String + createdAt: DateTime! + updatedAt: DateTime! +} + +type Department { + id: Long! + name: String! + description: String + createdAt: DateTime! + updatedAt: DateTime! +} + +enum MachineStateCode { + IDLE + ENGINE_ON + MOVING + NO_DATA +} + +type MachinesPage { + totalPages: Int! + totalElements: Long! + page: [Machine!]! +} + +enum MachineSortField { + ID + NAME + DATE +} + +input CreateMachinePayload { + name: String! + licensePlate: String! + typeId: Long! + departmentId: Long! + description: String +} + +input UpdateMachinePayload { + name: String + licensePlate: String + typeId: Long + departmentId: Long + description: String +} + +type Beacon { + id: Long! + identifier: String! + macAddress: String! + status: BeaconStatus! + boundMachine: Machine + lastSeenAt: DateTime + createdAt: DateTime! + updatedAt: DateTime! +} + +enum BeaconStatus { + FREE + BOUND +} + +type BeaconTelemetryPacket { + id: Long! + beacon: Beacon! + machine: Machine + mac: String! + vibration: Float! + duration: Int! + timeMillis: Long! + receivedAt: DateTime! +} + +input BeaconTelemetryPayload { + mac: String! + vibration: Float! + duration: Int! + timeMillis: Long! +} + +type MachineStateDefinition { + id: Long! + name: String! + icon: String! + color: String! + description: String + isSystem: Boolean! + createdAt: DateTime! + updatedAt: DateTime! +} + +input CreateMachineStateDefinitionPayload { + name: String! + icon: String! + color: String! + description: String +} + +input UpdateMachineStateDefinitionPayload { + name: String + icon: String + color: String + description: String +} + +type MachineCalibrationRule { + id: Long! + machine: Machine! + stateDefinition: MachineStateDefinition! + vibrationMin: Float! + vibrationMax: Float! + createdAt: DateTime! + updatedAt: DateTime! +} + +input CreateMachineCalibrationRulePayload { + machineId: Long! + stateDefinitionId: Long! + vibrationMin: Float! + vibrationMax: Float! +} + +input UpdateMachineCalibrationRulePayload { + stateDefinitionId: Long + vibrationMin: Float + vibrationMax: Float +} + +type MachineStateInterval { + id: Long! + machine: Machine! + stateDefinition: MachineStateDefinition + stateCode: MachineStateCode + startedAt: DateTime! + endedAt: DateTime + durationSeconds: Int! + vibrationMin: Float + vibrationMax: Float + vibrationAvg: Float + sampleCount: Int! + isNoData: Boolean! +} + +type MachineStateHistoryPage { + totalPages: Int! + totalElements: Long! + page: [MachineStateInterval!]! +} + +type MachineUsageSummary { + machine: Machine! + periodStart: DateTime! + periodEnd: DateTime! + movingSeconds: Int! + engineOnSeconds: Int! + idleSeconds: Int! + noDataSeconds: Int! + stateChangesCount: Int! + utilizationPercent: Int! +} + +type MachineDailyUsageStat { + machine: Machine! + date: String! + movingSeconds: Int! + engineOnSeconds: Int! + idleSeconds: Int! + noDataSeconds: Int! + utilizationPercent: Int! +} + +type FleetDailyReport { + date: Date! + generatedAt: DateTime! + summary: FleetDailyReportSummary! + machines: [FleetDailyReportMachineRow!]! + history: [FleetDailyReportHistoryRow!]! +} + +type FleetDailyReportSummary { + totalMachines: Int! + boundMachines: Int! + unboundMachines: Int! + machinesWithMovement: Int! + machinesWithoutData: Int! + movingSeconds: Int! + engineOnSeconds: Int! + idleSeconds: Int! + noDataSeconds: Int! + stateChangesCount: Int! +} + +type FleetDailyReportMachineRow { + machine: Machine! + movingSeconds: Int! + engineOnSeconds: Int! + idleSeconds: Int! + noDataSeconds: Int! + stateChangesCount: Int! + utilizationPercent: Int! +} + +type FleetDailyReportHistoryRow { + machine: Machine! + startedAt: DateTime! + endedAt: DateTime + stateDefinition: MachineStateDefinition + stateCode: MachineStateCode + durationSeconds: Int! +} + +enum ReportFormat { + PDF + XLSX +} + +input GenerateFleetDailyReportPayload { + date: Date! + format: ReportFormat! +} + +type ReportFile { + fileName: String! + mimeType: String! + downloadUrl: String! + expiresAt: DateTime +} diff --git a/backend/docker/docker-compose.yml b/backend/docker/docker-compose.yml new file mode 100644 index 0000000..3c04a4f --- /dev/null +++ b/backend/docker/docker-compose.yml @@ -0,0 +1,16 @@ +services: + postgres: + image: postgres:16 + container_name: fleet-ble-monitor-postgres + restart: unless-stopped + environment: + POSTGRES_DB: fleet_ble_monitor + POSTGRES_USER: fleet_user + POSTGRES_PASSWORD: fleet_password + ports: + - "${POSTGRES_HOST_PORT:-55432}:5432" + volumes: + - fleet_ble_monitor_pg:/var/lib/postgresql/data + +volumes: + fleet_ble_monitor_pg: diff --git a/backend/gradle/wrapper/gradle-wrapper.jar b/backend/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/backend/gradle/wrapper/gradle-wrapper.jar differ diff --git a/backend/gradle/wrapper/gradle-wrapper.properties b/backend/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a351597 --- /dev/null +++ b/backend/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/backend/gradlew b/backend/gradlew new file mode 100644 index 0000000..1aa94a4 --- /dev/null +++ b/backend/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/backend/gradlew.bat b/backend/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/backend/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend/settings.gradle.kts b/backend/settings.gradle.kts new file mode 100644 index 0000000..e31b678 --- /dev/null +++ b/backend/settings.gradle.kts @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + } +} + +rootProject.name = "fleet-ble-monitor-backend" diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/FleetBleMonitorApplication.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/FleetBleMonitorApplication.kt new file mode 100644 index 0000000..c83f7fe --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/FleetBleMonitorApplication.kt @@ -0,0 +1,11 @@ +package llc.arma.fleet.monitor + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class FleetBleMonitorApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/config/DataSeeder.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/config/DataSeeder.kt new file mode 100644 index 0000000..678c618 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/config/DataSeeder.kt @@ -0,0 +1,153 @@ +package llc.arma.fleet.monitor.config + +import llc.arma.fleet.monitor.data.entity.BeaconEntity +import llc.arma.fleet.monitor.data.entity.BeaconStatus +import llc.arma.fleet.monitor.data.entity.DepartmentEntity +import llc.arma.fleet.monitor.data.entity.MachineEntity +import llc.arma.fleet.monitor.data.entity.MachineStateCode +import llc.arma.fleet.monitor.data.entity.MachineStateDefinitionEntity +import llc.arma.fleet.monitor.data.entity.MachineStateIntervalEntity +import llc.arma.fleet.monitor.data.entity.MachineTypeEntity +import llc.arma.fleet.monitor.data.entity.UserEntity +import llc.arma.fleet.monitor.data.entity.UserRole +import llc.arma.fleet.monitor.data.repository.BeaconRepository +import llc.arma.fleet.monitor.data.repository.DepartmentRepository +import llc.arma.fleet.monitor.data.repository.MachineRepository +import llc.arma.fleet.monitor.data.repository.MachineStateDefinitionRepository +import llc.arma.fleet.monitor.data.repository.MachineStateIntervalRepository +import llc.arma.fleet.monitor.data.repository.MachineTypeRepository +import llc.arma.fleet.monitor.data.repository.UserRepository +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.CommandLineRunner +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Component +import java.time.OffsetDateTime + +@Component +class DataSeeder( + @Value("\${app.seed.enabled:true}") private val enabled: Boolean, + private val userRepository: UserRepository, + private val machineTypeRepository: MachineTypeRepository, + private val departmentRepository: DepartmentRepository, + private val beaconRepository: BeaconRepository, + private val machineRepository: MachineRepository, + private val stateDefinitionRepository: MachineStateDefinitionRepository, + private val stateIntervalRepository: MachineStateIntervalRepository, + private val passwordEncoder: PasswordEncoder, +) : CommandLineRunner { + override fun run(vararg args: String) { + if (!enabled || userRepository.count() > 0) return + + userRepository.saveAll( + listOf( + UserEntity( + lastName = "Админов", + firstName = "Алексей", + middleName = "Сергеевич", + email = "admin@fleet.local", + login = "admin", + passwordHash = encodePassword("123456"), + role = UserRole.ADMIN, + ), + UserEntity( + lastName = "Пользователев", + firstName = "Иван", + middleName = "Петрович", + email = "user@fleet.local", + login = "user", + passwordHash = encodePassword("123456"), + role = UserRole.USER, + ), + ), + ) + + val roller = machineTypeRepository.save(MachineTypeEntity(name = "Каток", description = "Дорожная техника")) + val dumpTruck = machineTypeRepository.save(MachineTypeEntity(name = "Самосвал", description = "Грузовая техника")) + val loader = machineTypeRepository.save(MachineTypeEntity(name = "Погрузчик", description = "Погрузочная техника")) + val road = departmentRepository.save(DepartmentEntity(name = "Дорожный участок")) + val mine = departmentRepository.save(DepartmentEntity(name = "Рудничный участок")) + + val beacon1 = beaconRepository.save(BeaconEntity(identifier = "BLE-TRK-0001", macAddress = "E4:5F:01:AA:10:01", status = BeaconStatus.BOUND, lastSeenAt = OffsetDateTime.now().minusMinutes(5))) + val beacon2 = beaconRepository.save(BeaconEntity(identifier = "BLE-TRK-0002", macAddress = "E4:5F:01:AA:10:02", status = BeaconStatus.BOUND, lastSeenAt = OffsetDateTime.now().minusMinutes(18))) + beaconRepository.save(BeaconEntity(identifier = "BLE-TRK-0003", macAddress = "E4:5F:01:AA:10:03", status = BeaconStatus.FREE)) + + val machine1 = machineRepository.save( + MachineEntity( + name = "Каток Hamm 3412", + licensePlate = "E402PB 154", + type = roller, + department = road, + description = "Уплотнение технологических дорог после отсыпки.", + beacon = beacon1, + lastKnownState = MachineStateCode.ENGINE_ON, + lastDataReceivedAt = beacon1.lastSeenAt, + ), + ) + machineRepository.save( + MachineEntity( + name = "КамАЗ 6520", + licensePlate = "A123BC 42", + type = dumpTruck, + department = mine, + beacon = beacon2, + lastKnownState = MachineStateCode.MOVING, + lastDataReceivedAt = beacon2.lastSeenAt, + ), + ) + machineRepository.save( + MachineEntity( + name = "Погрузчик Cat 950", + licensePlate = "T777TO 42", + type = loader, + department = mine, + lastKnownState = MachineStateCode.NO_DATA, + ), + ) + + val idle = stateDefinitionRepository.save(MachineStateDefinitionEntity(name = "Стоит", icon = "pause", color = "gray", isSystem = true)) + val engineOn = stateDefinitionRepository.save(MachineStateDefinitionEntity(name = "Заведена", icon = "engine", color = "orange", isSystem = true)) + val moving = stateDefinitionRepository.save(MachineStateDefinitionEntity(name = "В движении", icon = "route", color = "green", isSystem = true)) + val noData = stateDefinitionRepository.save(MachineStateDefinitionEntity(name = "Нет данных", icon = "signal-off", color = "red", isSystem = true)) + + val base = OffsetDateTime.now().withHour(8).withMinute(0).withSecond(0).withNano(0) + stateIntervalRepository.saveAll( + listOf( + interval(machine1, engineOn, MachineStateCode.ENGINE_ON, base, 15 * 60), + interval(machine1, moving, MachineStateCode.MOVING, base.plusMinutes(15), 75 * 60), + interval(machine1, idle, MachineStateCode.IDLE, base.plusMinutes(90), 40 * 60), + interval(machine1, moving, MachineStateCode.MOVING, base.plusMinutes(130), 95 * 60), + interval(machine1, engineOn, MachineStateCode.ENGINE_ON, base.plusMinutes(225), 30 * 60), + interval(machine1, idle, MachineStateCode.IDLE, base.plusMinutes(255), 45 * 60), + interval(machine1, moving, MachineStateCode.MOVING, base.plusMinutes(300), 140 * 60), + interval(machine1, noData, MachineStateCode.NO_DATA, base.plusMinutes(440), 20 * 60, true), + interval(machine1, idle, MachineStateCode.IDLE, base.plusMinutes(460), 80 * 60), + ), + ) + } + + private fun encodePassword(password: String): String { + return passwordEncoder.encode(password) + ?: error("Password encoder returned null") + } + + private fun interval( + machine: MachineEntity, + definition: MachineStateDefinitionEntity, + code: MachineStateCode, + start: OffsetDateTime, + durationSeconds: Int, + noData: Boolean = false, + ): MachineStateIntervalEntity = MachineStateIntervalEntity( + machine = machine, + stateDefinition = definition, + stateCode = code, + startedAt = start, + endedAt = start.plusSeconds(durationSeconds.toLong()), + durationSeconds = durationSeconds, + vibrationMin = if (noData) null else 10.0, + vibrationMax = if (noData) null else 150.0, + vibrationAvg = if (noData) null else 60.0, + sampleCount = if (noData) 0 else durationSeconds / 10, + isNoData = noData, + ) +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/config/GraphQlConfig.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/config/GraphQlConfig.kt new file mode 100644 index 0000000..4b9d074 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/config/GraphQlConfig.kt @@ -0,0 +1,27 @@ +package llc.arma.fleet.monitor.config + +import graphql.scalars.ExtendedScalars +import graphql.schema.GraphQLScalarType +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.graphql.execution.RuntimeWiringConfigurer +import java.time.LocalDate +import java.time.OffsetDateTime + +@Configuration +class GraphQlConfig { + @Bean + fun runtimeWiringConfigurer(): RuntimeWiringConfigurer = RuntimeWiringConfigurer { wiringBuilder -> + wiringBuilder.scalar(ExtendedScalars.GraphQLLong) + wiringBuilder.scalar(dateTimeScalar()) + wiringBuilder.scalar(dateScalar()) + } + + private fun dateTimeScalar(): GraphQLScalarType = ExtendedScalars.newAliasedScalar("DateTime") + .aliasedScalar(ExtendedScalars.DateTime) + .build() + + private fun dateScalar(): GraphQLScalarType = ExtendedScalars.newAliasedScalar("Date") + .aliasedScalar(ExtendedScalars.Date) + .build() +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/BaseEntity.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/BaseEntity.kt new file mode 100644 index 0000000..0df1287 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/BaseEntity.kt @@ -0,0 +1,28 @@ +package llc.arma.fleet.monitor.data.entity + +import jakarta.persistence.Column +import jakarta.persistence.MappedSuperclass +import jakarta.persistence.PrePersist +import jakarta.persistence.PreUpdate +import java.time.OffsetDateTime + +@MappedSuperclass +abstract class BaseEntity { + @Column(nullable = false, updatable = false) + var createdAt: OffsetDateTime = OffsetDateTime.now() + + @Column(nullable = false) + var updatedAt: OffsetDateTime = OffsetDateTime.now() + + @PrePersist + fun onCreate() { + val now = OffsetDateTime.now() + createdAt = now + updatedAt = now + } + + @PreUpdate + fun onUpdate() { + updatedAt = OffsetDateTime.now() + } +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/BeaconEntity.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/BeaconEntity.kt new file mode 100644 index 0000000..661c72a --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/BeaconEntity.kt @@ -0,0 +1,37 @@ +package llc.arma.fleet.monitor.data.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.OneToOne +import jakarta.persistence.Table +import java.time.OffsetDateTime + +@Entity +@Table(name = "beacons") +class BeaconEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + @Column(nullable = false, unique = true) + var identifier: String = "", + + @Column(nullable = false, unique = true) + var macAddress: String = "", + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + var status: BeaconStatus = BeaconStatus.FREE, + + @OneToOne(mappedBy = "beacon", fetch = FetchType.LAZY) + var boundMachine: MachineEntity? = null, + + @Column(nullable = true) + var lastSeenAt: OffsetDateTime? = null, +) : BaseEntity() diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/BeaconTelemetryPacketEntity.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/BeaconTelemetryPacketEntity.kt new file mode 100644 index 0000000..141152a --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/BeaconTelemetryPacketEntity.kt @@ -0,0 +1,43 @@ +package llc.arma.fleet.monitor.data.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import java.time.OffsetDateTime + +@Entity +@Table(name = "beacon_telemetry_packets") +class BeaconTelemetryPacketEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "beacon_id", nullable = false) + var beacon: BeaconEntity = BeaconEntity(), + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "machine_id", nullable = true) + var machine: MachineEntity? = null, + + @Column(nullable = false) + var mac: String = "", + + @Column(nullable = false) + var vibration: Double = 0.0, + + @Column(nullable = false) + var duration: Int = 0, + + @Column(nullable = false) + var timeMillis: Long = 0, + + @Column(nullable = false) + var receivedAt: OffsetDateTime = OffsetDateTime.now(), +) diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/DepartmentEntity.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/DepartmentEntity.kt new file mode 100644 index 0000000..dd8a629 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/DepartmentEntity.kt @@ -0,0 +1,22 @@ +package llc.arma.fleet.monitor.data.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table + +@Entity +@Table(name = "departments") +class DepartmentEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + @Column(nullable = false, unique = true) + var name: String = "", + + @Column(nullable = true, length = 2048) + var description: String? = null, +) : BaseEntity() diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/Enums.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/Enums.kt new file mode 100644 index 0000000..85234e3 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/Enums.kt @@ -0,0 +1,19 @@ +package llc.arma.fleet.monitor.data.entity + +enum class UserRole { ADMIN, USER } + +enum class MachineStateCode { IDLE, ENGINE_ON, MOVING, NO_DATA } + +enum class BeaconStatus { FREE, BOUND } + +enum class ReportFormat { PDF, XLSX } + +enum class ReportPeriod { PREVIOUS_DAY } + +enum class ReportDeliveryStatus { CREATED, GENERATING, SENT, FAILED } + +enum class SortDirection { ASC, DESC } + +enum class UserSortField { ID, NAME, DATE } + +enum class MachineSortField { ID, NAME, DATE } diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineCalibrationRuleEntity.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineCalibrationRuleEntity.kt new file mode 100644 index 0000000..966cedc --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineCalibrationRuleEntity.kt @@ -0,0 +1,33 @@ +package llc.arma.fleet.monitor.data.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table + +@Entity +@Table(name = "machine_calibration_rules") +class MachineCalibrationRuleEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "machine_id", nullable = false) + var machine: MachineEntity = MachineEntity(), + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "state_definition_id", nullable = false) + var stateDefinition: MachineStateDefinitionEntity = MachineStateDefinitionEntity(), + + @Column(nullable = false) + var vibrationMin: Double = 0.0, + + @Column(nullable = false) + var vibrationMax: Double = 0.0, +) : BaseEntity() diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineEntity.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineEntity.kt new file mode 100644 index 0000000..2209466 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineEntity.kt @@ -0,0 +1,51 @@ +package llc.arma.fleet.monitor.data.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToOne +import jakarta.persistence.Table +import java.time.OffsetDateTime + +@Entity +@Table(name = "machines") +class MachineEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + @Column(nullable = false) + var name: String = "", + + @Column(nullable = false, unique = true) + var licensePlate: String = "", + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "type_id", nullable = false) + var type: MachineTypeEntity = MachineTypeEntity(), + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "department_id", nullable = false) + var department: DepartmentEntity = DepartmentEntity(), + + @Column(nullable = true, length = 4096) + var description: String? = null, + + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "beacon_id", nullable = true) + var beacon: BeaconEntity? = null, + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + var lastKnownState: MachineStateCode = MachineStateCode.NO_DATA, + + @Column(nullable = true) + var lastDataReceivedAt: OffsetDateTime? = null, +) : BaseEntity() diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineStateDefinitionEntity.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineStateDefinitionEntity.kt new file mode 100644 index 0000000..6b8e186 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineStateDefinitionEntity.kt @@ -0,0 +1,31 @@ +package llc.arma.fleet.monitor.data.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table + +@Entity +@Table(name = "machine_state_definitions") +class MachineStateDefinitionEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + @Column(nullable = false, unique = true) + var name: String = "", + + @Column(nullable = false) + var icon: String = "", + + @Column(nullable = false) + var color: String = "", + + @Column(nullable = true, length = 2048) + var description: String? = null, + + @Column(nullable = false) + var isSystem: Boolean = false, +) : BaseEntity() diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineStateIntervalEntity.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineStateIntervalEntity.kt new file mode 100644 index 0000000..db92fcd --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineStateIntervalEntity.kt @@ -0,0 +1,58 @@ +package llc.arma.fleet.monitor.data.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import java.time.OffsetDateTime + +@Entity +@Table(name = "machine_state_intervals") +class MachineStateIntervalEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "machine_id", nullable = false) + var machine: MachineEntity = MachineEntity(), + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "state_definition_id", nullable = true) + var stateDefinition: MachineStateDefinitionEntity? = null, + + @Enumerated(EnumType.STRING) + @Column(nullable = true) + var stateCode: MachineStateCode? = null, + + @Column(nullable = false) + var startedAt: OffsetDateTime = OffsetDateTime.now(), + + @Column(nullable = true) + var endedAt: OffsetDateTime? = null, + + @Column(nullable = false) + var durationSeconds: Int = 0, + + @Column(nullable = true) + var vibrationMin: Double? = null, + + @Column(nullable = true) + var vibrationMax: Double? = null, + + @Column(nullable = true) + var vibrationAvg: Double? = null, + + @Column(nullable = false) + var sampleCount: Int = 0, + + @Column(nullable = false) + var isNoData: Boolean = false, +) : BaseEntity() diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineTypeEntity.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineTypeEntity.kt new file mode 100644 index 0000000..abe28e8 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/MachineTypeEntity.kt @@ -0,0 +1,22 @@ +package llc.arma.fleet.monitor.data.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table + +@Entity +@Table(name = "machine_types") +class MachineTypeEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + @Column(nullable = false, unique = true) + var name: String = "", + + @Column(nullable = true, length = 2048) + var description: String? = null, +) : BaseEntity() diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/UserEntity.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/UserEntity.kt new file mode 100644 index 0000000..32f9224 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/entity/UserEntity.kt @@ -0,0 +1,43 @@ +package llc.arma.fleet.monitor.data.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table + +@Entity +@Table(name = "users") +class UserEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + + @Column(nullable = false) + var lastName: String = "", + + @Column(nullable = false) + var firstName: String = "", + + @Column(nullable = true) + var middleName: String? = null, + + @Column(nullable = false, unique = true) + var email: String = "", + + @Column(nullable = false, unique = true) + var login: String = "", + + @Column(nullable = false) + var passwordHash: String = "", + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + var role: UserRole = UserRole.USER, + + @Column(nullable = false) + var active: Boolean = true, +) : BaseEntity() diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/BeaconRepository.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/BeaconRepository.kt new file mode 100644 index 0000000..9703b7a --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/BeaconRepository.kt @@ -0,0 +1,10 @@ +package llc.arma.fleet.monitor.data.repository + +import llc.arma.fleet.monitor.data.entity.BeaconEntity +import llc.arma.fleet.monitor.data.entity.BeaconStatus +import org.springframework.data.jpa.repository.JpaRepository + +interface BeaconRepository : JpaRepository { + fun findByMacAddress(macAddress: String): BeaconEntity? + fun findByStatus(status: BeaconStatus): List +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/BeaconTelemetryPacketRepository.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/BeaconTelemetryPacketRepository.kt new file mode 100644 index 0000000..d4b4d09 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/BeaconTelemetryPacketRepository.kt @@ -0,0 +1,10 @@ +package llc.arma.fleet.monitor.data.repository + +import llc.arma.fleet.monitor.data.entity.BeaconTelemetryPacketEntity +import org.springframework.data.jpa.repository.JpaRepository +import java.time.OffsetDateTime + +interface BeaconTelemetryPacketRepository : JpaRepository { + fun findByMachineIdAndReceivedAtBetween(machineId: Long, start: OffsetDateTime, end: OffsetDateTime): List + fun findByBeaconIdAndReceivedAtBetween(beaconId: Long, start: OffsetDateTime, end: OffsetDateTime): List +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/DepartmentRepository.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/DepartmentRepository.kt new file mode 100644 index 0000000..890db46 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/DepartmentRepository.kt @@ -0,0 +1,8 @@ +package llc.arma.fleet.monitor.data.repository + +import llc.arma.fleet.monitor.data.entity.DepartmentEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface DepartmentRepository : JpaRepository { + fun findByName(name: String): DepartmentEntity? +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineCalibrationRuleRepository.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineCalibrationRuleRepository.kt new file mode 100644 index 0000000..a43d9f7 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineCalibrationRuleRepository.kt @@ -0,0 +1,8 @@ +package llc.arma.fleet.monitor.data.repository + +import llc.arma.fleet.monitor.data.entity.MachineCalibrationRuleEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface MachineCalibrationRuleRepository : JpaRepository { + fun findByMachineId(machineId: Long): List +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineRepository.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineRepository.kt new file mode 100644 index 0000000..1bd5432 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineRepository.kt @@ -0,0 +1,15 @@ +package llc.arma.fleet.monitor.data.repository + +import llc.arma.fleet.monitor.data.entity.MachineEntity +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository + +interface MachineRepository : JpaRepository { + fun existsByLicensePlate(licensePlate: String): Boolean + fun findByNameContainingIgnoreCaseOrLicensePlateContainingIgnoreCase( + name: String, + licensePlate: String, + pageable: Pageable, + ): Page +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineStateDefinitionRepository.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineStateDefinitionRepository.kt new file mode 100644 index 0000000..4a7314d --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineStateDefinitionRepository.kt @@ -0,0 +1,8 @@ +package llc.arma.fleet.monitor.data.repository + +import llc.arma.fleet.monitor.data.entity.MachineStateDefinitionEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface MachineStateDefinitionRepository : JpaRepository { + fun findByName(name: String): MachineStateDefinitionEntity? +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineStateIntervalRepository.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineStateIntervalRepository.kt new file mode 100644 index 0000000..ca4f7cc --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineStateIntervalRepository.kt @@ -0,0 +1,22 @@ +package llc.arma.fleet.monitor.data.repository + +import llc.arma.fleet.monitor.data.entity.MachineStateIntervalEntity +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import java.time.OffsetDateTime + +interface MachineStateIntervalRepository : JpaRepository { + fun findByMachineIdAndStartedAtLessThanEqualAndEndedAtGreaterThanEqual( + machineId: Long, + endDate: OffsetDateTime, + startDate: OffsetDateTime, + ): List + + fun findByMachineIdAndStartedAtBetween( + machineId: Long, + startDate: OffsetDateTime, + endDate: OffsetDateTime, + pageable: Pageable, + ): Page +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineTypeRepository.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineTypeRepository.kt new file mode 100644 index 0000000..c5d6450 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/MachineTypeRepository.kt @@ -0,0 +1,8 @@ +package llc.arma.fleet.monitor.data.repository + +import llc.arma.fleet.monitor.data.entity.MachineTypeEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface MachineTypeRepository : JpaRepository { + fun findByName(name: String): MachineTypeEntity? +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/UserRepository.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/UserRepository.kt new file mode 100644 index 0000000..678bee9 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/data/repository/UserRepository.kt @@ -0,0 +1,58 @@ +package llc.arma.fleet.monitor.data.repository + +import llc.arma.fleet.monitor.data.entity.UserEntity +import llc.arma.fleet.monitor.data.entity.UserRole +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param + +interface UserRepository : JpaRepository { + fun findByLogin(login: String): UserEntity? + fun findByEmail(email: String): UserEntity? + fun findByLoginOrEmail(login: String, email: String): UserEntity? + fun existsByLogin(login: String): Boolean + fun existsByEmail(email: String): Boolean + fun existsByLoginAndIdNot(login: String, id: Long): Boolean + fun existsByEmailAndIdNot(email: String, id: Long): Boolean + + @Query( + """ + select user from UserEntity user + where ( + lower(user.lastName) like lower(concat('%', :query, '%')) + or lower(user.firstName) like lower(concat('%', :query, '%')) + or lower(user.email) like lower(concat('%', :query, '%')) + or lower(user.login) like lower(concat('%', :query, '%')) + ) + """, + ) + fun findByLastNameContainingIgnoreCaseOrFirstNameContainingIgnoreCaseOrEmailContainingIgnoreCaseOrLoginContainingIgnoreCase( + @Param("query") query: String, + pageable: Pageable, + ): Page + + fun findByRoleIn( + roles: List, + pageable: Pageable, + ): Page + + @Query( + """ + select user from UserEntity user + where user.role in :roles + and ( + lower(user.lastName) like lower(concat('%', :query, '%')) + or lower(user.firstName) like lower(concat('%', :query, '%')) + or lower(user.email) like lower(concat('%', :query, '%')) + or lower(user.login) like lower(concat('%', :query, '%')) + ) + """, + ) + fun searchByQueryAndRoleIn( + @Param("query") query: String, + @Param("roles") roles: List, + pageable: Pageable, + ): Page +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/AuthService.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/AuthService.kt new file mode 100644 index 0000000..a6a0e28 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/AuthService.kt @@ -0,0 +1,65 @@ +package llc.arma.fleet.monitor.domain.service + +import llc.arma.fleet.monitor.data.entity.UserEntity +import llc.arma.fleet.monitor.data.repository.UserRepository +import llc.arma.fleet.monitor.presentation.model.AuthPayload +import llc.arma.fleet.monitor.presentation.model.LoginPayload +import llc.arma.fleet.monitor.presentation.model.RefreshSessionPayload +import llc.arma.fleet.monitor.security.AuthTokenStore +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service + +@Service +class AuthService( + private val userRepository: UserRepository, + private val passwordEncoder: PasswordEncoder, + private val tokenStore: AuthTokenStore, +) { + fun login(payload: LoginPayload): AuthPayload { + val user = userRepository.findByLoginOrEmail(payload.loginOrEmail, payload.loginOrEmail) + ?: throw IllegalArgumentException("Пользователь не найден") + + if (!user.active) { + throw IllegalArgumentException("Пользователь заблокирован") + } + + if (!passwordEncoder.matches(payload.password, user.passwordHash)) { + throw IllegalArgumentException("Неверный пароль") + } + + return createAuthPayload(user) + } + + fun refreshSession(payload: RefreshSessionPayload): AuthPayload { + val session = tokenStore.refreshSession(payload.refreshToken) + ?: throw IllegalArgumentException("Refresh token is invalid") + + val user = userRepository.findById(session.userId).orElse(null) + ?: throw IllegalArgumentException("User not found") + + if (!user.active) { + throw IllegalArgumentException("User is blocked") + } + + return AuthPayload( + user = user, + accessToken = session.accessToken, + refreshToken = session.refreshToken, + ) + } + + fun getCurrentUser(userId: Long?): UserEntity? { + if (userId == null) return null + return userRepository.findById(userId).orElse(null) + } + + private fun createAuthPayload(user: UserEntity): AuthPayload { + val session = tokenStore.createSession(user.id!!) + + return AuthPayload( + user = user, + accessToken = session.accessToken, + refreshToken = session.refreshToken, + ) + } +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/FleetReportService.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/FleetReportService.kt new file mode 100644 index 0000000..3f628a3 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/FleetReportService.kt @@ -0,0 +1,87 @@ +package llc.arma.fleet.monitor.domain.service + +import llc.arma.fleet.monitor.data.entity.BeaconStatus +import llc.arma.fleet.monitor.data.entity.MachineStateCode +import llc.arma.fleet.monitor.data.repository.MachineRepository +import llc.arma.fleet.monitor.data.repository.MachineStateIntervalRepository +import llc.arma.fleet.monitor.presentation.model.FleetDailyReport +import llc.arma.fleet.monitor.presentation.model.FleetDailyReportHistoryRow +import llc.arma.fleet.monitor.presentation.model.FleetDailyReportMachineRow +import llc.arma.fleet.monitor.presentation.model.FleetDailyReportSummary +import llc.arma.fleet.monitor.presentation.model.GenerateFleetDailyReportPayload +import llc.arma.fleet.monitor.presentation.model.ReportFile +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.OffsetDateTime +import java.time.ZoneOffset + +@Service +class FleetReportService( + private val machineRepository: MachineRepository, + private val intervalRepository: MachineStateIntervalRepository, +) { + fun getFleetDailyReport(date: LocalDate): FleetDailyReport { + val machines = machineRepository.findAll() + val start = date.atStartOfDay().atOffset(ZoneOffset.UTC) + val end = date.plusDays(1).atStartOfDay().atOffset(ZoneOffset.UTC).minusNanos(1) + val rows = machines.map { machine -> + val intervals = intervalRepository.findByMachineIdAndStartedAtLessThanEqualAndEndedAtGreaterThanEqual(machine.id!!, end, start) + val moving = intervals.filter { it.stateCode == MachineStateCode.MOVING }.sumOf { it.durationSeconds } + val engineOn = intervals.filter { it.stateCode == MachineStateCode.ENGINE_ON }.sumOf { it.durationSeconds } + val idle = intervals.filter { it.stateCode == MachineStateCode.IDLE }.sumOf { it.durationSeconds } + val noData = intervals.filter { it.isNoData || it.stateCode == MachineStateCode.NO_DATA }.sumOf { it.durationSeconds } + val known = moving + engineOn + idle + FleetDailyReportMachineRow( + machine = machine, + movingSeconds = moving, + engineOnSeconds = engineOn, + idleSeconds = idle, + noDataSeconds = noData, + stateChangesCount = intervals.size, + utilizationPercent = if (known == 0) 0 else ((moving.toDouble() / known.toDouble()) * 100).toInt(), + ) + } + val history = machines.flatMap { machine -> + intervalRepository.findByMachineIdAndStartedAtLessThanEqualAndEndedAtGreaterThanEqual(machine.id!!, end, start) + .map { interval -> + FleetDailyReportHistoryRow( + machine = machine, + startedAt = interval.startedAt, + endedAt = interval.endedAt, + stateDefinition = interval.stateDefinition, + stateCode = interval.stateCode, + durationSeconds = interval.durationSeconds, + ) + } + } + return FleetDailyReport( + date = date, + generatedAt = OffsetDateTime.now(), + summary = FleetDailyReportSummary( + totalMachines = machines.size, + boundMachines = machines.count { it.beacon?.status == BeaconStatus.BOUND }, + unboundMachines = machines.count { it.beacon == null }, + machinesWithMovement = rows.count { it.movingSeconds > 0 }, + machinesWithoutData = rows.count { it.noDataSeconds > 0 }, + movingSeconds = rows.sumOf { it.movingSeconds }, + engineOnSeconds = rows.sumOf { it.engineOnSeconds }, + idleSeconds = rows.sumOf { it.idleSeconds }, + noDataSeconds = rows.sumOf { it.noDataSeconds }, + stateChangesCount = rows.sumOf { it.stateChangesCount }, + ), + machines = rows, + history = history, + ) + } + + fun createFleetDailyReportDownloadUrl(payload: GenerateFleetDailyReportPayload): ReportFile = + ReportFile( + fileName = "fleet-daily-report-${payload.date}.${payload.format.name.lowercase()}", + mimeType = when (payload.format.name) { + "XLSX" -> "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + else -> "application/pdf" + }, + downloadUrl = "/api/reports/download/mock/${payload.date}.${payload.format.name.lowercase()}", + expiresAt = OffsetDateTime.now().plusHours(1), + ) +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/MachineAnalyticsService.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/MachineAnalyticsService.kt new file mode 100644 index 0000000..ffc1a5f --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/MachineAnalyticsService.kt @@ -0,0 +1,92 @@ +package llc.arma.fleet.monitor.domain.service + +import llc.arma.fleet.monitor.data.entity.MachineStateCode +import llc.arma.fleet.monitor.data.entity.MachineStateIntervalEntity +import llc.arma.fleet.monitor.data.repository.MachineRepository +import llc.arma.fleet.monitor.data.repository.MachineStateIntervalRepository +import llc.arma.fleet.monitor.presentation.model.MachineDailyUsageStat +import llc.arma.fleet.monitor.presentation.model.MachineStateHistoryPage +import llc.arma.fleet.monitor.presentation.model.MachineUsageSummary +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.OffsetDateTime + +@Service +class MachineAnalyticsService( + private val machineRepository: MachineRepository, + private val stateIntervalRepository: MachineStateIntervalRepository, +) { + fun getMachineStateHistory( + machineId: Long, + startDate: OffsetDateTime, + endDate: OffsetDateTime, + page: Int?, + pageSize: Int?, + ): MachineStateHistoryPage { + val pageable = PageRequest.of( + (page ?: 0).coerceAtLeast(0), + (pageSize ?: 50).coerceIn(1, 200), + Sort.by(Sort.Direction.DESC, "startedAt"), + ) + val result = stateIntervalRepository.findByMachineIdAndStartedAtBetween(machineId, startDate, endDate, pageable) + return MachineStateHistoryPage(result.totalPages, result.totalElements, result.content) + } + + fun getMachineUsageSummary(machineId: Long, startDate: OffsetDateTime, endDate: OffsetDateTime): MachineUsageSummary { + val machine = machineRepository.findById(machineId).orElseThrow { IllegalArgumentException("Машина не найдена") } + val intervals = stateIntervalRepository.findByMachineIdAndStartedAtLessThanEqualAndEndedAtGreaterThanEqual(machineId, endDate, startDate) + val moving = intervals.durationFor(MachineStateCode.MOVING) + val engineOn = intervals.durationFor(MachineStateCode.ENGINE_ON) + val idle = intervals.durationFor(MachineStateCode.IDLE) + val noData = intervals.filter { it.isNoData || it.stateCode == MachineStateCode.NO_DATA }.sumOf { it.durationSeconds } + val known = moving + engineOn + idle + val utilization = if (known == 0) 0 else ((moving.toDouble() / known.toDouble()) * 100).toInt() + + return MachineUsageSummary( + machine = machine, + periodStart = startDate, + periodEnd = endDate, + movingSeconds = moving, + engineOnSeconds = engineOn, + idleSeconds = idle, + noDataSeconds = noData, + stateChangesCount = intervals.size, + utilizationPercent = utilization, + ) + } + + fun getMachineDailyUsageStats(machineId: Long, startDate: OffsetDateTime, endDate: OffsetDateTime): List { + val machine = machineRepository.findById(machineId).orElseThrow { IllegalArgumentException("Машина не найдена") } + val result = mutableListOf() + var date = startDate.toLocalDate() + val lastDate = endDate.toLocalDate() + while (!date.isAfter(lastDate)) { + val dayStart = date.atStartOfDay().atOffset(startDate.offset) + val dayEnd = date.plusDays(1).atStartOfDay().atOffset(startDate.offset).minusNanos(1) + val summary = getMachineUsageSummary(machineId, dayStart, dayEnd) + result.add( + MachineDailyUsageStat( + machine = machine, + date = date.toString(), + movingSeconds = summary.movingSeconds, + engineOnSeconds = summary.engineOnSeconds, + idleSeconds = summary.idleSeconds, + noDataSeconds = summary.noDataSeconds, + utilizationPercent = summary.utilizationPercent, + ), + ) + date = date.plusDays(1) + } + return result + } + + fun getUnrecognizedTelemetryIntervals(machineId: Long, startDate: OffsetDateTime, endDate: OffsetDateTime): List = + stateIntervalRepository + .findByMachineIdAndStartedAtLessThanEqualAndEndedAtGreaterThanEqual(machineId, endDate, startDate) + .filter { it.stateDefinition == null && !it.isNoData } + + private fun List.durationFor(state: MachineStateCode): Int = + filter { it.stateCode == state }.sumOf { it.durationSeconds } +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/MachineService.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/MachineService.kt new file mode 100644 index 0000000..051615e --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/MachineService.kt @@ -0,0 +1,134 @@ +package llc.arma.fleet.monitor.domain.service + +import llc.arma.fleet.monitor.data.entity.BeaconStatus +import llc.arma.fleet.monitor.data.entity.MachineEntity +import llc.arma.fleet.monitor.data.entity.MachineSortField +import llc.arma.fleet.monitor.data.entity.MachineStateCode +import llc.arma.fleet.monitor.data.entity.SortDirection +import llc.arma.fleet.monitor.data.repository.BeaconRepository +import llc.arma.fleet.monitor.data.repository.DepartmentRepository +import llc.arma.fleet.monitor.data.repository.MachineRepository +import llc.arma.fleet.monitor.data.repository.MachineTypeRepository +import llc.arma.fleet.monitor.presentation.model.CreateMachinePayload +import llc.arma.fleet.monitor.presentation.model.MachinesPage +import llc.arma.fleet.monitor.presentation.model.UpdateMachinePayload +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort +import org.springframework.stereotype.Service +import java.time.OffsetDateTime + +@Service +class MachineService( + private val machineRepository: MachineRepository, + private val machineTypeRepository: MachineTypeRepository, + private val departmentRepository: DepartmentRepository, + private val beaconRepository: BeaconRepository, +) { + fun getMachines( + page: Int, + pageSize: Int?, + query: String?, + sortDirection: SortDirection, + sortField: MachineSortField, + ): MachinesPage { + val pageable = PageRequest.of( + page.coerceAtLeast(0), + (pageSize ?: 20).coerceIn(1, 100), + Sort.by( + if (sortDirection == SortDirection.ASC) Sort.Direction.ASC else Sort.Direction.DESC, + when (sortField) { + MachineSortField.ID -> "id" + MachineSortField.NAME -> "name" + MachineSortField.DATE -> "createdAt" + }, + ), + ) + + val result = if (query.isNullOrBlank()) { + machineRepository.findAll(pageable) + } else { + machineRepository.findByNameContainingIgnoreCaseOrLicensePlateContainingIgnoreCase(query, query, pageable) + } + + return MachinesPage( + totalPages = result.totalPages, + totalElements = result.totalElements, + page = result.content, + ) + } + + fun getMachine(id: Long): MachineEntity? = machineRepository.findById(id).orElse(null) + + fun createMachine(payload: CreateMachinePayload): MachineEntity { + require(!machineRepository.existsByLicensePlate(payload.licensePlate)) { "Госномер уже используется" } + + val type = machineTypeRepository.findById(payload.typeId) + .orElseThrow { IllegalArgumentException("Тип машины не найден") } + val department = departmentRepository.findById(payload.departmentId) + .orElseThrow { IllegalArgumentException("Подразделение не найдено") } + + return machineRepository.save( + MachineEntity( + name = payload.name, + licensePlate = payload.licensePlate, + type = type, + department = department, + description = payload.description, + lastKnownState = MachineStateCode.NO_DATA, + ), + ) + } + + fun updateMachine(id: Long, payload: UpdateMachinePayload): MachineEntity { + val machine = machineRepository.findById(id).orElseThrow { IllegalArgumentException("Машина не найдена") } + + payload.name?.let { machine.name = it } + payload.licensePlate?.let { machine.licensePlate = it } + payload.typeId?.let { + machine.type = machineTypeRepository.findById(it).orElseThrow { IllegalArgumentException("Тип машины не найден") } + } + payload.departmentId?.let { + machine.department = departmentRepository.findById(it).orElseThrow { IllegalArgumentException("Подразделение не найдено") } + } + if (payload.description != null) machine.description = payload.description + + return machineRepository.save(machine) + } + + fun deleteMachine(id: Long): Boolean { + if (!machineRepository.existsById(id)) return false + machineRepository.deleteById(id) + return true + } + + fun bindBeaconToMachine(machineId: Long, beaconId: Long): MachineEntity { + val machine = machineRepository.findById(machineId).orElseThrow { IllegalArgumentException("Машина не найдена") } + val beacon = beaconRepository.findById(beaconId).orElseThrow { IllegalArgumentException("Маяк не найден") } + + require(beacon.status == BeaconStatus.FREE || machine.beacon?.id == beacon.id) { "Маяк уже привязан" } + + machine.beacon?.let { previousBeacon -> + previousBeacon.status = BeaconStatus.FREE + beaconRepository.save(previousBeacon) + } + + beacon.status = BeaconStatus.BOUND + beacon.lastSeenAt = beacon.lastSeenAt ?: OffsetDateTime.now() + machine.beacon = beacon + machine.lastDataReceivedAt = beacon.lastSeenAt + + beaconRepository.save(beacon) + return machineRepository.save(machine) + } + + fun unbindBeaconFromMachine(machineId: Long): MachineEntity { + val machine = machineRepository.findById(machineId).orElseThrow { IllegalArgumentException("Машина не найдена") } + val beacon = machine.beacon ?: return machine + + beacon.status = BeaconStatus.FREE + machine.beacon = null + + beaconRepository.save(beacon) + return machineRepository.save(machine) + } +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/MachineStateService.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/MachineStateService.kt new file mode 100644 index 0000000..de70b1e --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/MachineStateService.kt @@ -0,0 +1,95 @@ +package llc.arma.fleet.monitor.domain.service + +import llc.arma.fleet.monitor.data.entity.MachineCalibrationRuleEntity +import llc.arma.fleet.monitor.data.entity.MachineStateDefinitionEntity +import llc.arma.fleet.monitor.data.repository.MachineCalibrationRuleRepository +import llc.arma.fleet.monitor.data.repository.MachineRepository +import llc.arma.fleet.monitor.data.repository.MachineStateDefinitionRepository +import llc.arma.fleet.monitor.presentation.model.CreateMachineCalibrationRulePayload +import llc.arma.fleet.monitor.presentation.model.CreateMachineStateDefinitionPayload +import llc.arma.fleet.monitor.presentation.model.UpdateMachineCalibrationRulePayload +import llc.arma.fleet.monitor.presentation.model.UpdateMachineStateDefinitionPayload +import org.springframework.stereotype.Service + +@Service +class MachineStateService( + private val stateDefinitionRepository: MachineStateDefinitionRepository, + private val calibrationRuleRepository: MachineCalibrationRuleRepository, + private val machineRepository: MachineRepository, +) { + fun getMachineStateDefinitions(): List = stateDefinitionRepository.findAll() + + fun createMachineStateDefinition(payload: CreateMachineStateDefinitionPayload): MachineStateDefinitionEntity = + stateDefinitionRepository.save( + MachineStateDefinitionEntity( + name = payload.name, + icon = payload.icon, + color = payload.color, + description = payload.description, + isSystem = false, + ), + ) + + fun updateMachineStateDefinition(id: Long, payload: UpdateMachineStateDefinitionPayload): MachineStateDefinitionEntity { + val definition = stateDefinitionRepository.findById(id).orElseThrow { IllegalArgumentException("Состояние не найдено") } + payload.name?.let { definition.name = it } + payload.icon?.let { definition.icon = it } + payload.color?.let { definition.color = it } + if (payload.description != null) definition.description = payload.description + return stateDefinitionRepository.save(definition) + } + + fun deleteMachineStateDefinition(id: Long): Boolean { + val definition = stateDefinitionRepository.findById(id).orElse(null) ?: return false + require(!definition.isSystem) { "Системное состояние нельзя удалить" } + stateDefinitionRepository.delete(definition) + return true + } + + fun getMachineCalibrationRules(machineId: Long): List = + calibrationRuleRepository.findByMachineId(machineId) + + fun createMachineCalibrationRule(payload: CreateMachineCalibrationRulePayload): MachineCalibrationRuleEntity { + validateRule(payload.machineId, payload.vibrationMin, payload.vibrationMax, null) + val machine = machineRepository.findById(payload.machineId).orElseThrow { IllegalArgumentException("Машина не найдена") } + val definition = stateDefinitionRepository.findById(payload.stateDefinitionId).orElseThrow { IllegalArgumentException("Состояние не найдено") } + + return calibrationRuleRepository.save( + MachineCalibrationRuleEntity( + machine = machine, + stateDefinition = definition, + vibrationMin = payload.vibrationMin, + vibrationMax = payload.vibrationMax, + ), + ) + } + + fun updateMachineCalibrationRule(id: Long, payload: UpdateMachineCalibrationRulePayload): MachineCalibrationRuleEntity { + val rule = calibrationRuleRepository.findById(id).orElseThrow { IllegalArgumentException("Правило не найдено") } + val nextMin = payload.vibrationMin ?: rule.vibrationMin + val nextMax = payload.vibrationMax ?: rule.vibrationMax + validateRule(rule.machine.id!!, nextMin, nextMax, id) + + payload.stateDefinitionId?.let { + rule.stateDefinition = stateDefinitionRepository.findById(it).orElseThrow { IllegalArgumentException("Состояние не найдено") } + } + payload.vibrationMin?.let { rule.vibrationMin = it } + payload.vibrationMax?.let { rule.vibrationMax = it } + return calibrationRuleRepository.save(rule) + } + + fun deleteMachineCalibrationRule(id: Long): Boolean { + if (!calibrationRuleRepository.existsById(id)) return false + calibrationRuleRepository.deleteById(id) + return true + } + + private fun validateRule(machineId: Long, min: Double, max: Double, currentRuleId: Long?) { + require(min <= max) { "Минимальное значение не может быть больше максимального" } + val rules = calibrationRuleRepository.findByMachineId(machineId) + val hasIntersection = rules.any { rule -> + rule.id != currentRuleId && min <= rule.vibrationMax && max >= rule.vibrationMin + } + require(!hasIntersection) { "Диапазон пересекается с существующим правилом" } + } +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/ReferenceService.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/ReferenceService.kt new file mode 100644 index 0000000..3c5ec70 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/ReferenceService.kt @@ -0,0 +1,46 @@ +package llc.arma.fleet.monitor.domain.service + +import llc.arma.fleet.monitor.data.entity.BeaconEntity +import llc.arma.fleet.monitor.data.entity.BeaconStatus +import llc.arma.fleet.monitor.data.entity.DepartmentEntity +import llc.arma.fleet.monitor.data.entity.MachineTypeEntity +import llc.arma.fleet.monitor.data.repository.BeaconRepository +import llc.arma.fleet.monitor.data.repository.DepartmentRepository +import llc.arma.fleet.monitor.data.repository.MachineTypeRepository +import org.springframework.stereotype.Service + +@Service +class ReferenceService( + private val machineTypeRepository: MachineTypeRepository, + private val departmentRepository: DepartmentRepository, + private val beaconRepository: BeaconRepository, +) { + fun getMachineTypes(): List = machineTypeRepository.findAll() + fun getDepartments(): List = departmentRepository.findAll() + fun getBeacons(): List = beaconRepository.findAll() + fun getFreeBeacons(): List = beaconRepository.findByStatus(BeaconStatus.FREE) + + fun createMachineType(name: String): MachineTypeEntity = machineTypeRepository.save(MachineTypeEntity(name = name)) + fun updateMachineType(id: Long, name: String): MachineTypeEntity { + val type = machineTypeRepository.findById(id).orElseThrow { IllegalArgumentException("Тип машины не найден") } + type.name = name + return machineTypeRepository.save(type) + } + fun deleteMachineType(id: Long): Boolean { + if (!machineTypeRepository.existsById(id)) return false + machineTypeRepository.deleteById(id) + return true + } + + fun createDepartment(name: String): DepartmentEntity = departmentRepository.save(DepartmentEntity(name = name)) + fun updateDepartment(id: Long, name: String): DepartmentEntity { + val department = departmentRepository.findById(id).orElseThrow { IllegalArgumentException("Подразделение не найдено") } + department.name = name + return departmentRepository.save(department) + } + fun deleteDepartment(id: Long): Boolean { + if (!departmentRepository.existsById(id)) return false + departmentRepository.deleteById(id) + return true + } +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/TelemetryService.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/TelemetryService.kt new file mode 100644 index 0000000..a352200 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/TelemetryService.kt @@ -0,0 +1,51 @@ +package llc.arma.fleet.monitor.domain.service + +import llc.arma.fleet.monitor.data.entity.BeaconTelemetryPacketEntity +import llc.arma.fleet.monitor.data.repository.BeaconRepository +import llc.arma.fleet.monitor.data.repository.BeaconTelemetryPacketRepository +import llc.arma.fleet.monitor.data.repository.MachineRepository +import llc.arma.fleet.monitor.presentation.model.BeaconTelemetryPayload +import org.springframework.stereotype.Service +import java.time.Instant +import java.time.OffsetDateTime +import java.time.ZoneOffset + +@Service +class TelemetryService( + private val beaconRepository: BeaconRepository, + private val machineRepository: MachineRepository, + private val packetRepository: BeaconTelemetryPacketRepository, +) { + fun registerBeaconTelemetryPacket(payload: BeaconTelemetryPayload): Boolean { + val beacon = beaconRepository.findByMacAddress(payload.mac) ?: return false + val machine = machineRepository.findAll().firstOrNull { it.beacon?.id == beacon.id } + val receivedAt = OffsetDateTime.ofInstant(Instant.ofEpochMilli(payload.timeMillis), ZoneOffset.UTC) + + beacon.lastSeenAt = receivedAt + machine?.lastDataReceivedAt = receivedAt + beaconRepository.save(beacon) + if (machine != null) machineRepository.save(machine) + + packetRepository.save( + BeaconTelemetryPacketEntity( + beacon = beacon, + machine = machine, + mac = payload.mac, + vibration = payload.vibration, + duration = payload.duration, + timeMillis = payload.timeMillis, + receivedAt = receivedAt, + ), + ) + + return true + } + + fun getBeaconTelemetry(beaconId: Long?, machineId: Long?, startDate: OffsetDateTime, endDate: OffsetDateTime): List { + return when { + machineId != null -> packetRepository.findByMachineIdAndReceivedAtBetween(machineId, startDate, endDate) + beaconId != null -> packetRepository.findByBeaconIdAndReceivedAtBetween(beaconId, startDate, endDate) + else -> packetRepository.findAll().filter { it.receivedAt in startDate..endDate } + } + } +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/UserService.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/UserService.kt new file mode 100644 index 0000000..01f631a --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/domain/service/UserService.kt @@ -0,0 +1,121 @@ +package llc.arma.fleet.monitor.domain.service + +import llc.arma.fleet.monitor.data.entity.SortDirection +import llc.arma.fleet.monitor.data.entity.UserEntity +import llc.arma.fleet.monitor.data.entity.UserRole +import llc.arma.fleet.monitor.data.entity.UserSortField +import llc.arma.fleet.monitor.data.repository.UserRepository +import llc.arma.fleet.monitor.presentation.model.CreateUserPayload +import llc.arma.fleet.monitor.presentation.model.UpdateUserPayload +import llc.arma.fleet.monitor.presentation.model.UsersPage +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service + +@Service +class UserService( + private val userRepository: UserRepository, + private val passwordEncoder: PasswordEncoder, +) { + fun getUsers( + page: Int, + pageSize: Int?, + query: String?, + roles: List?, + sortDirection: SortDirection, + sortField: UserSortField, + ): UsersPage { + val pageable = PageRequest.of( + page.coerceAtLeast(0), + (pageSize ?: 20).coerceIn(1, 100), + Sort.by( + if (sortDirection == SortDirection.ASC) Sort.Direction.ASC else Sort.Direction.DESC, + when (sortField) { + UserSortField.ID -> "id" + UserSortField.NAME -> "lastName" + UserSortField.DATE -> "createdAt" + }, + ), + ) + + val allowedRoles = roles?.takeIf { it.isNotEmpty() } + val normalizedQuery = query?.trim()?.takeIf { it.isNotBlank() } + + val result = when { + normalizedQuery != null && allowedRoles != null -> { + userRepository.searchByQueryAndRoleIn(normalizedQuery, allowedRoles, pageable) + } + + normalizedQuery != null -> { + userRepository.findByLastNameContainingIgnoreCaseOrFirstNameContainingIgnoreCaseOrEmailContainingIgnoreCaseOrLoginContainingIgnoreCase( + normalizedQuery, + pageable, + ) + } + + allowedRoles != null -> { + userRepository.findByRoleIn(allowedRoles, pageable) + } + + else -> { + userRepository.findAll(pageable) + } + } + + return UsersPage( + totalPages = result.totalPages, + totalElements = result.totalElements, + page = result.content, + ) + } + + fun getUser(id: Long): UserEntity? = userRepository.findById(id).orElse(null) + + fun createUser(payload: CreateUserPayload): UserEntity { + require(!userRepository.existsByEmail(payload.email)) { "Email уже используется" } + require(!userRepository.existsByLogin(payload.login)) { "Логин уже используется" } + + return userRepository.save( + UserEntity( + lastName = payload.lastName, + firstName = payload.firstName, + middleName = payload.middleName, + email = payload.email, + login = payload.login, + passwordHash = encodePassword(payload.password), + role = payload.role, + ), + ) + } + + private fun encodePassword(password: String): String = + passwordEncoder.encode(password) ?: error("Password encoder returned null") + + fun updateUser(id: Long, payload: UpdateUserPayload): UserEntity { + val user = userRepository.findById(id).orElseThrow { IllegalArgumentException("Пользователь не найден") } + + payload.lastName?.let { user.lastName = it } + payload.firstName?.let { user.firstName = it } + if (payload.middleName != null) user.middleName = payload.middleName + payload.email?.let { + require(!userRepository.existsByEmailAndIdNot(it, id)) { "Email уже используется" } + user.email = it + } + payload.login?.let { + require(!userRepository.existsByLoginAndIdNot(it, id)) { "Логин уже используется" } + user.login = it + } + payload.password?.let { user.passwordHash = encodePassword(it) } + payload.role?.let { user.role = it } + payload.active?.let { user.active = it } + + return userRepository.save(user) + } + + fun deleteUser(id: Long): Boolean { + if (!userRepository.existsById(id)) return false + userRepository.deleteById(id) + return true + } +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/AuthGraphQlController.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/AuthGraphQlController.kt new file mode 100644 index 0000000..5b70856 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/AuthGraphQlController.kt @@ -0,0 +1,42 @@ +package llc.arma.fleet.monitor.presentation.graphql + +import llc.arma.fleet.monitor.data.entity.UserEntity +import llc.arma.fleet.monitor.domain.service.AuthService +import llc.arma.fleet.monitor.presentation.model.AuthPayload +import llc.arma.fleet.monitor.presentation.model.LoginPayload +import llc.arma.fleet.monitor.presentation.model.RefreshSessionPayload +import llc.arma.fleet.monitor.security.AuthTokenStore +import jakarta.servlet.http.HttpServletRequest +import org.springframework.graphql.data.method.annotation.Argument +import org.springframework.graphql.data.method.annotation.MutationMapping +import org.springframework.graphql.data.method.annotation.QueryMapping +import org.springframework.stereotype.Controller +import java.security.Principal + +@Controller +class AuthGraphQlController( + private val authService: AuthService, + private val tokenStore: AuthTokenStore, +) { + @MutationMapping + fun login(@Argument payload: LoginPayload): AuthPayload = authService.login(payload) + + @MutationMapping + fun refreshSession(@Argument payload: RefreshSessionPayload): AuthPayload = authService.refreshSession(payload) + + @MutationMapping + fun logout(request: HttpServletRequest): Boolean { + tokenStore.revoke(extractToken(request)) + + return true + } + + @QueryMapping + fun currentUser(principal: Principal?): UserEntity? = authService.getCurrentUser(principal?.name?.toLongOrNull()) + + private fun extractToken(request: HttpServletRequest): String? { + return request.getHeader("Authorization") + ?.removePrefix("Bearer ") + ?: request.cookies?.firstOrNull { it.name == "Access-token" }?.value + } +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/FleetReportGraphQlController.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/FleetReportGraphQlController.kt new file mode 100644 index 0000000..0df93cd --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/FleetReportGraphQlController.kt @@ -0,0 +1,22 @@ +package llc.arma.fleet.monitor.presentation.graphql + +import llc.arma.fleet.monitor.domain.service.FleetReportService +import llc.arma.fleet.monitor.presentation.model.FleetDailyReport +import llc.arma.fleet.monitor.presentation.model.GenerateFleetDailyReportPayload +import llc.arma.fleet.monitor.presentation.model.ReportFile +import org.springframework.graphql.data.method.annotation.Argument +import org.springframework.graphql.data.method.annotation.QueryMapping +import org.springframework.stereotype.Controller +import java.time.LocalDate + +@Controller +class FleetReportGraphQlController( + private val fleetReportService: FleetReportService, +) { + @QueryMapping + fun getFleetDailyReport(@Argument date: LocalDate): FleetDailyReport = fleetReportService.getFleetDailyReport(date) + + @QueryMapping + fun createFleetDailyReportDownloadUrl(@Argument payload: GenerateFleetDailyReportPayload): ReportFile = + fleetReportService.createFleetDailyReportDownloadUrl(payload) +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/MachineGraphQlController.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/MachineGraphQlController.kt new file mode 100644 index 0000000..0e726af --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/MachineGraphQlController.kt @@ -0,0 +1,45 @@ +package llc.arma.fleet.monitor.presentation.graphql + +import llc.arma.fleet.monitor.data.entity.MachineEntity +import llc.arma.fleet.monitor.data.entity.MachineSortField +import llc.arma.fleet.monitor.data.entity.SortDirection +import llc.arma.fleet.monitor.domain.service.MachineService +import llc.arma.fleet.monitor.presentation.model.CreateMachinePayload +import llc.arma.fleet.monitor.presentation.model.MachinesPage +import llc.arma.fleet.monitor.presentation.model.UpdateMachinePayload +import org.springframework.graphql.data.method.annotation.Argument +import org.springframework.graphql.data.method.annotation.MutationMapping +import org.springframework.graphql.data.method.annotation.QueryMapping +import org.springframework.stereotype.Controller + +@Controller +class MachineGraphQlController( + private val machineService: MachineService, +) { + @QueryMapping + fun getMachines( + @Argument page: Int, + @Argument pageSize: Int?, + @Argument query: String?, + @Argument sortDirection: SortDirection, + @Argument sortField: MachineSortField, + ): MachinesPage = machineService.getMachines(page, pageSize, query, sortDirection, sortField) + + @QueryMapping + fun getMachine(@Argument id: Long): MachineEntity? = machineService.getMachine(id) + + @MutationMapping + fun createMachine(@Argument payload: CreateMachinePayload): MachineEntity = machineService.createMachine(payload) + + @MutationMapping + fun updateMachine(@Argument id: Long, @Argument payload: UpdateMachinePayload): MachineEntity = machineService.updateMachine(id, payload) + + @MutationMapping + fun deleteMachine(@Argument id: Long): Boolean = machineService.deleteMachine(id) + + @MutationMapping + fun bindBeaconToMachine(@Argument machineId: Long, @Argument beaconId: Long): MachineEntity = machineService.bindBeaconToMachine(machineId, beaconId) + + @MutationMapping + fun unbindBeaconFromMachine(@Argument machineId: Long): MachineEntity = machineService.unbindBeaconFromMachine(machineId) +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/MachineStateGraphQlController.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/MachineStateGraphQlController.kt new file mode 100644 index 0000000..01df468 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/MachineStateGraphQlController.kt @@ -0,0 +1,86 @@ +package llc.arma.fleet.monitor.presentation.graphql + +import llc.arma.fleet.monitor.data.entity.MachineCalibrationRuleEntity +import llc.arma.fleet.monitor.data.entity.MachineStateDefinitionEntity +import llc.arma.fleet.monitor.domain.service.MachineAnalyticsService +import llc.arma.fleet.monitor.domain.service.MachineStateService +import llc.arma.fleet.monitor.presentation.model.CreateMachineCalibrationRulePayload +import llc.arma.fleet.monitor.presentation.model.CreateMachineStateDefinitionPayload +import llc.arma.fleet.monitor.presentation.model.MachineStateHistoryPage +import llc.arma.fleet.monitor.presentation.model.MachineUsageSummary +import llc.arma.fleet.monitor.presentation.model.UpdateMachineCalibrationRulePayload +import llc.arma.fleet.monitor.presentation.model.UpdateMachineStateDefinitionPayload +import org.springframework.graphql.data.method.annotation.Argument +import org.springframework.graphql.data.method.annotation.MutationMapping +import org.springframework.graphql.data.method.annotation.QueryMapping +import org.springframework.stereotype.Controller +import java.time.OffsetDateTime + +@Controller +class MachineStateGraphQlController( + private val machineStateService: MachineStateService, + private val machineAnalyticsService: MachineAnalyticsService, +) { + @QueryMapping + fun getMachineStateDefinitions(): List = machineStateService.getMachineStateDefinitions() + + @QueryMapping + fun getMachineCalibrationRules(@Argument machineId: Long): List = + machineStateService.getMachineCalibrationRules(machineId) + + @QueryMapping + fun getMachineStateHistory( + @Argument machineId: Long, + @Argument startDate: OffsetDateTime, + @Argument endDate: OffsetDateTime, + @Argument page: Int?, + @Argument pageSize: Int?, + ): MachineStateHistoryPage = machineAnalyticsService.getMachineStateHistory(machineId, startDate, endDate, page, pageSize) + + @QueryMapping + fun getMachineUsageSummary( + @Argument machineId: Long, + @Argument startDate: OffsetDateTime, + @Argument endDate: OffsetDateTime, + ): MachineUsageSummary = machineAnalyticsService.getMachineUsageSummary(machineId, startDate, endDate) + + @QueryMapping + fun getUnrecognizedTelemetryIntervals( + @Argument machineId: Long, + @Argument startDate: OffsetDateTime, + @Argument endDate: OffsetDateTime, + ) = machineAnalyticsService.getUnrecognizedTelemetryIntervals(machineId, startDate, endDate) + + @QueryMapping + fun getMachineDailyUsageStats( + @Argument machineId: Long, + @Argument startDate: OffsetDateTime, + @Argument endDate: OffsetDateTime, + ) = machineAnalyticsService.getMachineDailyUsageStats(machineId, startDate, endDate) + + @MutationMapping + fun createMachineStateDefinition(@Argument payload: CreateMachineStateDefinitionPayload): MachineStateDefinitionEntity = + machineStateService.createMachineStateDefinition(payload) + + @MutationMapping + fun updateMachineStateDefinition(@Argument id: Long, @Argument payload: UpdateMachineStateDefinitionPayload): MachineStateDefinitionEntity = + machineStateService.updateMachineStateDefinition(id, payload) + + @MutationMapping + fun deleteMachineStateDefinition(@Argument id: Long): Boolean = machineStateService.deleteMachineStateDefinition(id) + + @MutationMapping + fun createMachineCalibrationRule(@Argument payload: CreateMachineCalibrationRulePayload): MachineCalibrationRuleEntity = + machineStateService.createMachineCalibrationRule(payload) + + @MutationMapping + fun updateMachineCalibrationRule(@Argument id: Long, @Argument payload: UpdateMachineCalibrationRulePayload): MachineCalibrationRuleEntity = + machineStateService.updateMachineCalibrationRule(id, payload) + + @MutationMapping + fun deleteMachineCalibrationRule(@Argument id: Long): Boolean = machineStateService.deleteMachineCalibrationRule(id) + + @MutationMapping + fun recognizeMachineStates(@Argument machineId: Long, @Argument startDate: OffsetDateTime, @Argument endDate: OffsetDateTime) = + machineAnalyticsService.getUnrecognizedTelemetryIntervals(machineId, startDate, endDate) +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/ReferenceGraphQlController.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/ReferenceGraphQlController.kt new file mode 100644 index 0000000..4d347b3 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/ReferenceGraphQlController.kt @@ -0,0 +1,45 @@ +package llc.arma.fleet.monitor.presentation.graphql + +import llc.arma.fleet.monitor.data.entity.BeaconEntity +import llc.arma.fleet.monitor.data.entity.DepartmentEntity +import llc.arma.fleet.monitor.data.entity.MachineTypeEntity +import llc.arma.fleet.monitor.domain.service.ReferenceService +import org.springframework.graphql.data.method.annotation.Argument +import org.springframework.graphql.data.method.annotation.MutationMapping +import org.springframework.graphql.data.method.annotation.QueryMapping +import org.springframework.stereotype.Controller + +@Controller +class ReferenceGraphQlController( + private val referenceService: ReferenceService, +) { + @QueryMapping + fun getMachineTypes(): List = referenceService.getMachineTypes() + + @QueryMapping + fun getDepartments(): List = referenceService.getDepartments() + + @QueryMapping + fun getBeacons(): List = referenceService.getBeacons() + + @QueryMapping + fun getFreeBeacons(): List = referenceService.getFreeBeacons() + + @MutationMapping + fun createMachineType(@Argument name: String): MachineTypeEntity = referenceService.createMachineType(name) + + @MutationMapping + fun updateMachineType(@Argument id: Long, @Argument name: String): MachineTypeEntity = referenceService.updateMachineType(id, name) + + @MutationMapping + fun deleteMachineType(@Argument id: Long): Boolean = referenceService.deleteMachineType(id) + + @MutationMapping + fun createDepartment(@Argument name: String): DepartmentEntity = referenceService.createDepartment(name) + + @MutationMapping + fun updateDepartment(@Argument id: Long, @Argument name: String): DepartmentEntity = referenceService.updateDepartment(id, name) + + @MutationMapping + fun deleteDepartment(@Argument id: Long): Boolean = referenceService.deleteDepartment(id) +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/TelemetryGraphQlController.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/TelemetryGraphQlController.kt new file mode 100644 index 0000000..08b23c2 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/TelemetryGraphQlController.kt @@ -0,0 +1,27 @@ +package llc.arma.fleet.monitor.presentation.graphql + +import llc.arma.fleet.monitor.data.entity.BeaconTelemetryPacketEntity +import llc.arma.fleet.monitor.domain.service.TelemetryService +import llc.arma.fleet.monitor.presentation.model.BeaconTelemetryPayload +import org.springframework.graphql.data.method.annotation.Argument +import org.springframework.graphql.data.method.annotation.MutationMapping +import org.springframework.graphql.data.method.annotation.QueryMapping +import org.springframework.stereotype.Controller +import java.time.OffsetDateTime + +@Controller +class TelemetryGraphQlController( + private val telemetryService: TelemetryService, +) { + @QueryMapping + fun getBeaconTelemetry( + @Argument beaconId: Long?, + @Argument machineId: Long?, + @Argument startDate: OffsetDateTime, + @Argument endDate: OffsetDateTime, + ): List = telemetryService.getBeaconTelemetry(beaconId, machineId, startDate, endDate) + + @MutationMapping + fun registerBeaconTelemetryPacket(@Argument payload: BeaconTelemetryPayload): Boolean = + telemetryService.registerBeaconTelemetryPacket(payload) +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/UserGraphQlController.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/UserGraphQlController.kt new file mode 100644 index 0000000..cbaab22 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/graphql/UserGraphQlController.kt @@ -0,0 +1,41 @@ +package llc.arma.fleet.monitor.presentation.graphql + +import llc.arma.fleet.monitor.data.entity.SortDirection +import llc.arma.fleet.monitor.data.entity.UserEntity +import llc.arma.fleet.monitor.data.entity.UserRole +import llc.arma.fleet.monitor.data.entity.UserSortField +import llc.arma.fleet.monitor.domain.service.UserService +import llc.arma.fleet.monitor.presentation.model.CreateUserPayload +import llc.arma.fleet.monitor.presentation.model.UpdateUserPayload +import llc.arma.fleet.monitor.presentation.model.UsersPage +import org.springframework.graphql.data.method.annotation.Argument +import org.springframework.graphql.data.method.annotation.MutationMapping +import org.springframework.graphql.data.method.annotation.QueryMapping +import org.springframework.stereotype.Controller + +@Controller +class UserGraphQlController( + private val userService: UserService, +) { + @QueryMapping + fun getUsers( + @Argument page: Int, + @Argument pageSize: Int?, + @Argument query: String?, + @Argument roles: List?, + @Argument sortDirection: SortDirection, + @Argument sortField: UserSortField, + ): UsersPage = userService.getUsers(page, pageSize, query, roles, sortDirection, sortField) + + @QueryMapping + fun getUser(@Argument id: Long): UserEntity? = userService.getUser(id) + + @MutationMapping + fun createUser(@Argument payload: CreateUserPayload): UserEntity = userService.createUser(payload) + + @MutationMapping + fun updateUser(@Argument id: Long, @Argument payload: UpdateUserPayload): UserEntity = userService.updateUser(id, payload) + + @MutationMapping + fun deleteUser(@Argument id: Long): Boolean = userService.deleteUser(id) +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/model/AuthModels.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/model/AuthModels.kt new file mode 100644 index 0000000..1bffea9 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/model/AuthModels.kt @@ -0,0 +1,41 @@ +package llc.arma.fleet.monitor.presentation.model + +import llc.arma.fleet.monitor.data.entity.UserEntity +import llc.arma.fleet.monitor.data.entity.UserRole + +data class LoginPayload( + val loginOrEmail: String, + val password: String, +) + +data class RefreshSessionPayload( + val refreshToken: String, +) + +data class AuthPayload( + val user: UserEntity, + val accessToken: String, + val refreshToken: String, + val token: String = accessToken, +) + +data class CreateUserPayload( + val lastName: String, + val firstName: String, + val middleName: String?, + val email: String, + val login: String, + val password: String, + val role: UserRole, +) + +data class UpdateUserPayload( + val lastName: String?, + val firstName: String?, + val middleName: String?, + val email: String?, + val login: String?, + val password: String?, + val role: UserRole?, + val active: Boolean?, +) diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/model/MachineModels.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/model/MachineModels.kt new file mode 100644 index 0000000..3d9cfa6 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/model/MachineModels.kt @@ -0,0 +1,96 @@ +package llc.arma.fleet.monitor.presentation.model + +import llc.arma.fleet.monitor.data.entity.MachineEntity +import llc.arma.fleet.monitor.data.entity.UserEntity +import llc.arma.fleet.monitor.data.entity.MachineStateIntervalEntity +import java.time.OffsetDateTime + +data class CreateMachinePayload( + val name: String, + val licensePlate: String, + val typeId: Long, + val departmentId: Long, + val description: String?, +) + +data class UpdateMachinePayload( + val name: String?, + val licensePlate: String?, + val typeId: Long?, + val departmentId: Long?, + val description: String?, +) + +data class BeaconTelemetryPayload( + val mac: String, + val vibration: Double, + val duration: Int, + val timeMillis: Long, +) + +data class CreateMachineStateDefinitionPayload( + val name: String, + val icon: String, + val color: String, + val description: String?, +) + +data class UpdateMachineStateDefinitionPayload( + val name: String?, + val icon: String?, + val color: String?, + val description: String?, +) + +data class CreateMachineCalibrationRulePayload( + val machineId: Long, + val stateDefinitionId: Long, + val vibrationMin: Double, + val vibrationMax: Double, +) + +data class UpdateMachineCalibrationRulePayload( + val stateDefinitionId: Long?, + val vibrationMin: Double?, + val vibrationMax: Double?, +) + +data class UsersPage( + val totalPages: Int, + val totalElements: Long, + val page: List, +) + +data class MachinesPage( + val totalPages: Int, + val totalElements: Long, + val page: List, +) + +data class MachineStateHistoryPage( + val totalPages: Int, + val totalElements: Long, + val page: List, +) + +data class MachineUsageSummary( + val machine: MachineEntity, + val periodStart: OffsetDateTime, + val periodEnd: OffsetDateTime, + val movingSeconds: Int, + val engineOnSeconds: Int, + val idleSeconds: Int, + val noDataSeconds: Int, + val stateChangesCount: Int, + val utilizationPercent: Int, +) + +data class MachineDailyUsageStat( + val machine: MachineEntity, + val date: String, + val movingSeconds: Int, + val engineOnSeconds: Int, + val idleSeconds: Int, + val noDataSeconds: Int, + val utilizationPercent: Int, +) diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/model/ReportModels.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/model/ReportModels.kt new file mode 100644 index 0000000..be71822 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/presentation/model/ReportModels.kt @@ -0,0 +1,60 @@ +package llc.arma.fleet.monitor.presentation.model + +import llc.arma.fleet.monitor.data.entity.MachineEntity +import llc.arma.fleet.monitor.data.entity.MachineStateCode +import llc.arma.fleet.monitor.data.entity.MachineStateDefinitionEntity +import llc.arma.fleet.monitor.data.entity.ReportFormat +import java.time.LocalDate +import java.time.OffsetDateTime + +data class GenerateFleetDailyReportPayload( + val date: LocalDate, + val format: ReportFormat, +) + +data class ReportFile( + val fileName: String, + val mimeType: String, + val downloadUrl: String, + val expiresAt: OffsetDateTime?, +) + +data class FleetDailyReport( + val date: LocalDate, + val generatedAt: OffsetDateTime, + val summary: FleetDailyReportSummary, + val machines: List, + val history: List, +) + +data class FleetDailyReportSummary( + val totalMachines: Int, + val boundMachines: Int, + val unboundMachines: Int, + val machinesWithMovement: Int, + val machinesWithoutData: Int, + val movingSeconds: Int, + val engineOnSeconds: Int, + val idleSeconds: Int, + val noDataSeconds: Int, + val stateChangesCount: Int, +) + +data class FleetDailyReportMachineRow( + val machine: MachineEntity, + val movingSeconds: Int, + val engineOnSeconds: Int, + val idleSeconds: Int, + val noDataSeconds: Int, + val stateChangesCount: Int, + val utilizationPercent: Int, +) + +data class FleetDailyReportHistoryRow( + val machine: MachineEntity, + val startedAt: OffsetDateTime, + val endedAt: OffsetDateTime?, + val stateDefinition: MachineStateDefinitionEntity?, + val stateCode: MachineStateCode?, + val durationSeconds: Int, +) diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/security/AuthTokenStore.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/security/AuthTokenStore.kt new file mode 100644 index 0000000..458bd40 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/security/AuthTokenStore.kt @@ -0,0 +1,95 @@ +package llc.arma.fleet.monitor.security + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.time.OffsetDateTime +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +@Component +class AuthTokenStore( + @Value("\${app.auth.access-token-ttl-minutes:720}") private val accessTokenTtlMinutes: Long, + @Value("\${app.auth.refresh-token-ttl-minutes:10080}") + private val refreshTokenTtlMinutes: Long, +) { + private data class TokenRecord( + val userId: Long, + val expiresAt: OffsetDateTime, + ) + + data class AuthSession( + val userId: Long, + val accessToken: String, + val refreshToken: String, + ) + + private val accessTokens = ConcurrentHashMap() + private val refreshTokens = ConcurrentHashMap() + + fun createToken(userId: Long): String { + val session = createSession(userId) + + return session.accessToken + } + + fun createSession(userId: Long): AuthSession { + val accessToken = generateToken() + val refreshToken = generateToken() + val now = OffsetDateTime.now() + + accessTokens[accessToken] = TokenRecord( + userId = userId, + expiresAt = now.plusMinutes(accessTokenTtlMinutes), + ) + refreshTokens[refreshToken] = TokenRecord( + userId = userId, + expiresAt = now.plusMinutes(refreshTokenTtlMinutes), + ) + + return AuthSession( + userId = userId, + accessToken = accessToken, + refreshToken = refreshToken, + ) + } + + fun refreshSession(refreshToken: String?): AuthSession? { + val record = resolveToken(refreshTokens, refreshToken) ?: return null + + if (!refreshToken.isNullOrBlank()) { + refreshTokens.remove(refreshToken) + } + + return createSession(record.userId) + } + + fun resolveUserId(token: String?): Long? { + return resolveToken(accessTokens, token)?.userId + } + + fun revoke(token: String?) { + if (!token.isNullOrBlank()) { + accessTokens.remove(token) + refreshTokens.remove(token) + } + } + + private fun resolveToken( + store: ConcurrentHashMap, + token: String?, + ): TokenRecord? { + if (token.isNullOrBlank()) return null + val record = store[token] ?: return null + + if (record.expiresAt.isBefore(OffsetDateTime.now())) { + store.remove(token) + return null + } + + return record + } + + private fun generateToken(): String { + return UUID.randomUUID().toString().replace("-", "") + } +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/security/DemoTokenAuthFilter.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/security/DemoTokenAuthFilter.kt new file mode 100644 index 0000000..9c024a2 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/security/DemoTokenAuthFilter.kt @@ -0,0 +1,36 @@ +package llc.arma.fleet.monitor.security + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class DemoTokenAuthFilter( + private val tokenStore: AuthTokenStore, +) : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain, + ) { + val token = request.getHeader("Authorization") + ?.removePrefix("Bearer ") + ?: request.cookies?.firstOrNull { it.name == "Access-token" }?.value + + val userId = tokenStore.resolveUserId(token) + if (userId != null) { + SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken( + userId.toString(), + null, + listOf(SimpleGrantedAuthority("AUTHENTICATED")), + ) + } + + filterChain.doFilter(request, response) + } +} diff --git a/backend/src/main/kotlin/llc/arma/fleet/monitor/security/SecurityConfig.kt b/backend/src/main/kotlin/llc/arma/fleet/monitor/security/SecurityConfig.kt new file mode 100644 index 0000000..61e42e5 --- /dev/null +++ b/backend/src/main/kotlin/llc/arma/fleet/monitor/security/SecurityConfig.kt @@ -0,0 +1,28 @@ +package llc.arma.fleet.monitor.security + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + +@Configuration +class SecurityConfig( + private val demoTokenAuthFilter: DemoTokenAuthFilter, +) { + @Bean + fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() + + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain = + http + .csrf { it.disable() } + .cors { } + .authorizeHttpRequests { requests -> + requests.anyRequest().permitAll() + } + .addFilterBefore(demoTokenAuthFilter, UsernamePasswordAuthenticationFilter::class.java) + .build() +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..8978c50 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,43 @@ +spring: + application: + name: fleet-ble-monitor-backend + + graphql: + path: /graphql + schema: + locations: classpath:graphql/ + file-extensions: .graphqls + graphiql: + enabled: true + path: /graphiql + cors: + allow-credentials: true + allowed-origin-patterns: + - "*" + + datasource: + url: jdbc:postgresql://${DB_HOST:localhost:55432}/${DB_NAME:fleet_ble_monitor} + username: ${DB_USER:fleet_user} + password: ${DB_PASSWORD:fleet_password} + driver-class-name: org.postgresql.Driver + + jpa: + open-in-view: false + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + +server: + port: ${SERVER_PORT:8080} + servlet: + context-path: /api + +app: + seed: + enabled: ${SEED_ENABLED:true} + auth: + demo-token-ttl-minutes: ${DEMO_TOKEN_TTL_MINUTES:720} + access-token-ttl-minutes: ${ACCESS_TOKEN_TTL_MINUTES:720} + refresh-token-ttl-minutes: ${REFRESH_TOKEN_TTL_MINUTES:10080} diff --git a/backend/src/main/resources/graphql/schema.graphqls b/backend/src/main/resources/graphql/schema.graphqls new file mode 100644 index 0000000..006775a --- /dev/null +++ b/backend/src/main/resources/graphql/schema.graphqls @@ -0,0 +1,431 @@ +scalar DateTime +scalar Date +scalar Long + +type Query { + currentUser: User + + getUsers( + page: Int! + pageSize: Int + query: String + roles: [UserRole!] + sortDirection: SortDirection! + sortField: UserSortField! + ): UsersPage! + + getUser(id: Long!): User + + getMachines( + page: Int! + pageSize: Int + query: String + sortDirection: SortDirection! + sortField: MachineSortField! + ): MachinesPage! + + getMachine(id: Long!): Machine + + getMachineTypes: [MachineType!]! + getDepartments: [Department!]! + getBeacons: [Beacon!]! + getFreeBeacons: [Beacon!]! + + getBeaconTelemetry( + beaconId: Long + machineId: Long + startDate: DateTime! + endDate: DateTime! + ): [BeaconTelemetryPacket!]! + + getMachineStateDefinitions: [MachineStateDefinition!]! + getMachineCalibrationRules(machineId: Long!): [MachineCalibrationRule!]! + + getMachineStateHistory( + machineId: Long! + startDate: DateTime! + endDate: DateTime! + page: Int + pageSize: Int + ): MachineStateHistoryPage! + + getMachineUsageSummary( + machineId: Long! + startDate: DateTime! + endDate: DateTime! + ): MachineUsageSummary! + + getUnrecognizedTelemetryIntervals( + machineId: Long! + startDate: DateTime! + endDate: DateTime! + ): [MachineStateInterval!]! + + getMachineDailyUsageStats( + machineId: Long! + startDate: DateTime! + endDate: DateTime! + ): [MachineDailyUsageStat!]! + + getFleetDailyReport(date: Date!): FleetDailyReport! + + createFleetDailyReportDownloadUrl( + payload: GenerateFleetDailyReportPayload! + ): ReportFile! +} + +type Mutation { + login(payload: LoginPayload!): AuthPayload! + refreshSession(payload: RefreshSessionPayload!): AuthPayload! + logout: Boolean! + + createUser(payload: CreateUserPayload!): User! + updateUser(id: Long!, payload: UpdateUserPayload!): User! + deleteUser(id: Long!): Boolean! + + createMachine(payload: CreateMachinePayload!): Machine! + updateMachine(id: Long!, payload: UpdateMachinePayload!): Machine! + deleteMachine(id: Long!): Boolean! + + bindBeaconToMachine(machineId: Long!, beaconId: Long!): Machine! + unbindBeaconFromMachine(machineId: Long!): Machine! + + registerBeaconTelemetryPacket(payload: BeaconTelemetryPayload!): Boolean! + + createMachineType(name: String!): MachineType! + updateMachineType(id: Long!, name: String!): MachineType! + deleteMachineType(id: Long!): Boolean! + + createDepartment(name: String!): Department! + updateDepartment(id: Long!, name: String!): Department! + deleteDepartment(id: Long!): Boolean! + + createMachineStateDefinition(payload: CreateMachineStateDefinitionPayload!): MachineStateDefinition! + updateMachineStateDefinition(id: Long!, payload: UpdateMachineStateDefinitionPayload!): MachineStateDefinition! + deleteMachineStateDefinition(id: Long!): Boolean! + + createMachineCalibrationRule(payload: CreateMachineCalibrationRulePayload!): MachineCalibrationRule! + updateMachineCalibrationRule(id: Long!, payload: UpdateMachineCalibrationRulePayload!): MachineCalibrationRule! + deleteMachineCalibrationRule(id: Long!): Boolean! + + recognizeMachineStates(machineId: Long!, startDate: DateTime!, endDate: DateTime!): [MachineStateInterval!]! +} + +type AuthPayload { + user: User! + accessToken: String! + refreshToken: String! + token: String! +} + +type User { + id: Long! + lastName: String! + firstName: String! + middleName: String + email: String! + login: String! + role: UserRole! + active: Boolean! + createdAt: DateTime! + updatedAt: DateTime! +} + +enum UserRole { + ADMIN + USER +} + +type UsersPage { + totalPages: Int! + totalElements: Long! + page: [User!]! +} + +enum UserSortField { + ID + NAME + DATE +} + +enum SortDirection { + ASC + DESC +} + +input LoginPayload { + loginOrEmail: String! + password: String! +} + +input RefreshSessionPayload { + refreshToken: String! +} + +input CreateUserPayload { + lastName: String! + firstName: String! + middleName: String + email: String! + login: String! + password: String! + role: UserRole! +} + +input UpdateUserPayload { + lastName: String + firstName: String + middleName: String + email: String + login: String + password: String + role: UserRole + active: Boolean +} + +type Machine { + id: Long! + name: String! + licensePlate: String! + type: MachineType! + department: Department! + description: String + beacon: Beacon + lastKnownState: MachineStateCode! + lastDataReceivedAt: DateTime + createdAt: DateTime! + updatedAt: DateTime! +} + +type MachineType { + id: Long! + name: String! + description: String + createdAt: DateTime! + updatedAt: DateTime! +} + +type Department { + id: Long! + name: String! + description: String + createdAt: DateTime! + updatedAt: DateTime! +} + +enum MachineStateCode { + IDLE + ENGINE_ON + MOVING + NO_DATA +} + +type MachinesPage { + totalPages: Int! + totalElements: Long! + page: [Machine!]! +} + +enum MachineSortField { + ID + NAME + DATE +} + +input CreateMachinePayload { + name: String! + licensePlate: String! + typeId: Long! + departmentId: Long! + description: String +} + +input UpdateMachinePayload { + name: String + licensePlate: String + typeId: Long + departmentId: Long + description: String +} + +type Beacon { + id: Long! + identifier: String! + macAddress: String! + status: BeaconStatus! + boundMachine: Machine + lastSeenAt: DateTime + createdAt: DateTime! + updatedAt: DateTime! +} + +enum BeaconStatus { + FREE + BOUND +} + +type BeaconTelemetryPacket { + id: Long! + beacon: Beacon! + machine: Machine + mac: String! + vibration: Float! + duration: Int! + timeMillis: Long! + receivedAt: DateTime! +} + +input BeaconTelemetryPayload { + mac: String! + vibration: Float! + duration: Int! + timeMillis: Long! +} + +type MachineStateDefinition { + id: Long! + name: String! + icon: String! + color: String! + description: String + isSystem: Boolean! + createdAt: DateTime! + updatedAt: DateTime! +} + +input CreateMachineStateDefinitionPayload { + name: String! + icon: String! + color: String! + description: String +} + +input UpdateMachineStateDefinitionPayload { + name: String + icon: String + color: String + description: String +} + +type MachineCalibrationRule { + id: Long! + machine: Machine! + stateDefinition: MachineStateDefinition! + vibrationMin: Float! + vibrationMax: Float! + createdAt: DateTime! + updatedAt: DateTime! +} + +input CreateMachineCalibrationRulePayload { + machineId: Long! + stateDefinitionId: Long! + vibrationMin: Float! + vibrationMax: Float! +} + +input UpdateMachineCalibrationRulePayload { + stateDefinitionId: Long + vibrationMin: Float + vibrationMax: Float +} + +type MachineStateInterval { + id: Long! + machine: Machine! + stateDefinition: MachineStateDefinition + stateCode: MachineStateCode + startedAt: DateTime! + endedAt: DateTime + durationSeconds: Int! + vibrationMin: Float + vibrationMax: Float + vibrationAvg: Float + sampleCount: Int! + isNoData: Boolean! +} + +type MachineStateHistoryPage { + totalPages: Int! + totalElements: Long! + page: [MachineStateInterval!]! +} + +type MachineUsageSummary { + machine: Machine! + periodStart: DateTime! + periodEnd: DateTime! + movingSeconds: Int! + engineOnSeconds: Int! + idleSeconds: Int! + noDataSeconds: Int! + stateChangesCount: Int! + utilizationPercent: Int! +} + +type MachineDailyUsageStat { + machine: Machine! + date: String! + movingSeconds: Int! + engineOnSeconds: Int! + idleSeconds: Int! + noDataSeconds: Int! + utilizationPercent: Int! +} + +type FleetDailyReport { + date: Date! + generatedAt: DateTime! + summary: FleetDailyReportSummary! + machines: [FleetDailyReportMachineRow!]! + history: [FleetDailyReportHistoryRow!]! +} + +type FleetDailyReportSummary { + totalMachines: Int! + boundMachines: Int! + unboundMachines: Int! + machinesWithMovement: Int! + machinesWithoutData: Int! + movingSeconds: Int! + engineOnSeconds: Int! + idleSeconds: Int! + noDataSeconds: Int! + stateChangesCount: Int! +} + +type FleetDailyReportMachineRow { + machine: Machine! + movingSeconds: Int! + engineOnSeconds: Int! + idleSeconds: Int! + noDataSeconds: Int! + stateChangesCount: Int! + utilizationPercent: Int! +} + +type FleetDailyReportHistoryRow { + machine: Machine! + startedAt: DateTime! + endedAt: DateTime + stateDefinition: MachineStateDefinition + stateCode: MachineStateCode + durationSeconds: Int! +} + +enum ReportFormat { + PDF + XLSX +} + +input GenerateFleetDailyReportPayload { + date: Date! + format: ReportFormat! +} + +type ReportFile { + fileName: String! + mimeType: String! + downloadUrl: String! + expiresAt: DateTime +} diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..4c9df2d --- /dev/null +++ b/frontend/.env @@ -0,0 +1,4 @@ +VITE_GRAPHQL_API_URL=/api/graphql +VITE_MAPTILER_KEY=IorSzMRqcNUCYzcXZhi6 +VITE_AUTH_ENABLED=true +VITE_BACKEND_URL=http://localhost:8080/api diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/codegen.ts b/frontend/codegen.ts new file mode 100644 index 0000000..4615d2a --- /dev/null +++ b/frontend/codegen.ts @@ -0,0 +1,24 @@ +import type { CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = { + schema: '../backend/src/main/resources/graphql/schema.graphqls', + documents: ['src/shared/api/graphql/operations/**/*.{ts,tsx,graphql}'], + generates: { + 'src/shared/api/graphql/generated/': { + preset: 'client', + presetConfig: { + fragmentMasking: false, + }, + config: { + useTypeImports: true, + scalars: { + Date: 'string', + DateTime: 'string', + Long: 'number', + }, + }, + }, + }, +} + +export default config diff --git a/frontend/docs/backend-domain-model.md b/frontend/docs/backend-domain-model.md new file mode 100644 index 0000000..12da685 --- /dev/null +++ b/frontend/docs/backend-domain-model.md @@ -0,0 +1,930 @@ +# Fleet BLE Monitor Backend Contract Draft + +Черновик backend-модели, GraphQL-like API и use-cases для `fleet-ble-monitor`. + +Документ написан в формате, близком к GraphQL Docs: у сущности сразу перечислены поля, рядом указаны связанные enum/input/page-типы. Это не финальная схема, а база для обсуждения со старшим. + +## Общие скаляры + +```graphql +scalar DateTime +scalar Date +``` + +`DateTime` — ISO-строка даты и времени. + +`Date` — календарная дата в формате `YYYY-MM-DD`. + +## User + +Пользователь системы. Используется для авторизации, прав доступа и как получатель отчетов. + +```graphql +type User { + id: ID! + lastName: String! + firstName: String! + middleName: String + email: String! + login: String! + role: UserRole! + createdAt: DateTime! + updatedAt: DateTime +} +``` + +### UserRole + +Роль пользователя. + +```graphql +enum UserRole { + ADMIN + USER +} +``` + +`ADMIN` — администратор. + +`USER` — пользователь. + +### UsersPage + +Страница пользователей для таблицы. + +```graphql +type UsersPage { + totalPages: Int! + totalElements: Int! + page: [User!]! +} +``` + +### UserSortField + +Поле сортировки пользователей. + +```graphql +enum UserSortField { + ID + NAME + DATE +} +``` + +### SortDirection + +Общее направление сортировки. + +```graphql +enum SortDirection { + ASC + DESC +} +``` + +### LoginPayload + +```graphql +input LoginPayload { + loginOrEmail: String! + password: String! +} +``` + +### CreateUserPayload + +```graphql +input CreateUserPayload { + lastName: String! + firstName: String! + middleName: String + email: String! + login: String! + password: String! + role: UserRole! +} +``` + +### UpdateUserPayload + +```graphql +input UpdateUserPayload { + lastName: String + firstName: String + middleName: String + email: String + login: String + password: String + role: UserRole +} +``` + +## Machine + +АТТ / единица техники. Основная бизнес-сущность мониторинга. + +```graphql +type Machine { + id: ID! + name: String! + licensePlate: String! + type: MachineType! + department: Department! + description: String + beacon: Beacon + lastKnownState: MachineStateCode! + lastDataReceivedAt: DateTime + createdAt: DateTime! + updatedAt: DateTime! +} +``` + +### MachineType + +Справочник типов АТТ. + +```graphql +type MachineType { + id: ID! + name: String! + description: String + createdAt: DateTime! + updatedAt: DateTime! +} +``` + +Примеры: каток, самосвал, погрузчик, экскаватор. + +### Department + +Справочник подразделений, участков или объектов. + +```graphql +type Department { + id: ID! + name: String! + description: String + createdAt: DateTime! + updatedAt: DateTime! +} +``` + +### MachineStateCode + +Последнее известное базовое состояние АТТ. + +```graphql +enum MachineStateCode { + IDLE + ENGINE_ON + MOVING + NO_DATA +} +``` + +`IDLE` — стоит. + +`ENGINE_ON` — заведена. + +`MOVING` — в движении. + +`NO_DATA` — нет данных. + +### MachinesPage + +```graphql +type MachinesPage { + totalPages: Int! + totalElements: Int! + page: [Machine!]! +} +``` + +### MachineSortField + +```graphql +enum MachineSortField { + ID + NAME + DATE +} +``` + +### CreateMachinePayload + +```graphql +input CreateMachinePayload { + name: String! + licensePlate: String! + typeId: ID! + departmentId: ID! + description: String +} +``` + +### UpdateMachinePayload + +```graphql +input UpdateMachinePayload { + name: String + licensePlate: String + typeId: ID + departmentId: ID + description: String +} +``` + +## Beacon + +BLE-маяк. Хранит идентификатор, MAC-адрес, статус привязки, последнюю связь. + +```graphql +type Beacon { + id: ID! + identifier: String! + macAddress: String! + status: BeaconStatus! + boundMachine: Machine + lastSeenAt: DateTime + createdAt: DateTime! + updatedAt: DateTime! +} +``` + +### BeaconStatus + +```graphql +enum BeaconStatus { + FREE + BOUND +} +``` + +`FREE` — маяк свободен. + +`BOUND` — маяк привязан к АТТ. + +### BeaconTelemetryPacket + +Сырое сообщение от маяка. + +```graphql +type BeaconTelemetryPacket { + id: ID! + beacon: Beacon! + machine: Machine + mac: String! + vibration: Float! + duration: Int! + timeMillis: Long! + receivedAt: DateTime! +} +``` + +### BeaconTelemetryPayload + +Payload входящего сообщения от маяка. + +```graphql +input BeaconTelemetryPayload { + mac: String! + vibration: Float! + duration: Int! + timeMillis: Long! +} +``` + +`duration` — длительность замера/пакета в секундах. + +`timeMillis` — время события на стороне маяка или шлюза. + +## MachineStateDefinition + +Справочник состояний АТТ без привязки к конкретным значениям акселерометра. + +```graphql +type MachineStateDefinition { + id: ID! + name: String! + icon: String! + color: String! + description: String + isSystem: Boolean! + createdAt: DateTime! + updatedAt: DateTime! +} +``` + +Примеры: + +- `Стоит` +- `Заведена` +- `В движении` +- `Работа с нагрузкой` +- `Нет данных` + +`Нет данных` — системное состояние, его нельзя удалять и нельзя использовать в ручном правиле калибровки. + +### CreateMachineStateDefinitionPayload + +```graphql +input CreateMachineStateDefinitionPayload { + name: String! + icon: String! + color: String! + description: String +} +``` + +### UpdateMachineStateDefinitionPayload + +```graphql +input UpdateMachineStateDefinitionPayload { + name: String + icon: String + color: String + description: String +} +``` + +## MachineCalibrationRule + +Правило калибровки для конкретной АТТ. Связывает диапазон значений вибрации с состоянием из справочника. + +```graphql +type MachineCalibrationRule { + id: ID! + machine: Machine! + stateDefinition: MachineStateDefinition! + vibrationMin: Float! + vibrationMax: Float! + createdAt: DateTime! + updatedAt: DateTime! +} +``` + +### CreateMachineCalibrationRulePayload + +```graphql +input CreateMachineCalibrationRulePayload { + machineId: ID! + stateDefinitionId: ID! + vibrationMin: Float! + vibrationMax: Float! +} +``` + +### UpdateMachineCalibrationRulePayload + +```graphql +input UpdateMachineCalibrationRulePayload { + stateDefinitionId: ID + vibrationMin: Float + vibrationMax: Float +} +``` + +## MachineStateInterval + +Распознанный интервал состояния АТТ. + +```graphql +type MachineStateInterval { + id: ID! + machine: Machine! + stateDefinition: MachineStateDefinition + stateCode: MachineStateCode + startedAt: DateTime! + endedAt: DateTime + durationSeconds: Int! + vibrationMin: Float + vibrationMax: Float + vibrationAvg: Float + sampleCount: Int! + isNoData: Boolean! +} +``` + +Если `stateDefinition` равен `null`, участок считается нераспознанным. + +Если `isNoData = true`, участок относится к системному состоянию `Нет данных`. + +### MachineStateHistoryPage + +```graphql +type MachineStateHistoryPage { + totalPages: Int! + totalElements: Int! + page: [MachineStateInterval!]! +} +``` + +## MachineUsageSummary + +Краткая статистика по АТТ за период. + +```graphql +type MachineUsageSummary { + machine: Machine! + periodStart: DateTime! + periodEnd: DateTime! + movingSeconds: Int! + engineOnSeconds: Int! + idleSeconds: Int! + noDataSeconds: Int! + stateChangesCount: Int! + utilizationPercent: Int! +} +``` + +## FleetReportSchedule + +Настройка ежедневной рассылки отчета по парку АТТ. + +```graphql +type FleetReportSchedule { + id: ID! + name: String! + recipients: [User!]! + reportFormat: ReportFormat! + sendTime: String! + period: ReportPeriod! + timezone: String! + enabled: Boolean! + createdAt: DateTime! + updatedAt: DateTime! +} +``` + +### ReportFormat + +Формат отчета. + +```graphql +enum ReportFormat { + PDF + XLSX +} +``` + +### ReportPeriod + +Период отчета. + +```graphql +enum ReportPeriod { + PREVIOUS_DAY +} +``` + +`PREVIOUS_DAY` — предыдущий календарный день относительно timezone расписания. + +### CreateFleetReportSchedulePayload + +```graphql +input CreateFleetReportSchedulePayload { + recipientUserIds: [ID!]! + reportFormat: ReportFormat! + sendTime: String! +} +``` + +### UpdateFleetReportSchedulePayload + +```graphql +input UpdateFleetReportSchedulePayload { + recipientUserIds: [ID!] + reportFormat: ReportFormat + sendTime: String + enabled: Boolean +} +``` + +## FleetReportDelivery + +История конкретной попытки отправки отчета. + +```graphql +type FleetReportDelivery { + id: ID! + schedule: FleetReportSchedule + reportDate: Date! + reportFormat: ReportFormat! + recipients: [User!]! + status: ReportDeliveryStatus! + fileName: String + errorMessage: String + createdAt: DateTime! + sentAt: DateTime +} +``` + +### ReportDeliveryStatus + +```graphql +enum ReportDeliveryStatus { + CREATED + GENERATING + SENT + FAILED +} +``` + +`CREATED` — задача создана. + +`GENERATING` — отчет формируется. + +`SENT` — отчет отправлен. + +`FAILED` — ошибка генерации или отправки. + +## FleetDailyReport + +Доменная модель дневного отчета по парку АТТ. + +```graphql +type FleetDailyReport { + date: Date! + generatedAt: DateTime! + summary: FleetDailyReportSummary! + machines: [FleetDailyReportMachineRow!]! + history: [FleetDailyReportHistoryRow!]! +} +``` + +### FleetDailyReportSummary + +```graphql +type FleetDailyReportSummary { + totalMachines: Int! + boundMachines: Int! + unboundMachines: Int! + machinesWithMovement: Int! + machinesWithoutData: Int! + movingSeconds: Int! + engineOnSeconds: Int! + idleSeconds: Int! + noDataSeconds: Int! + stateChangesCount: Int! +} +``` + +### FleetDailyReportMachineRow + +```graphql +type FleetDailyReportMachineRow { + machine: Machine! + movingSeconds: Int! + engineOnSeconds: Int! + idleSeconds: Int! + noDataSeconds: Int! + stateChangesCount: Int! + utilizationPercent: Int! +} +``` + +### FleetDailyReportHistoryRow + +```graphql +type FleetDailyReportHistoryRow { + machine: Machine! + startedAt: DateTime! + endedAt: DateTime! + stateDefinition: MachineStateDefinition + stateCode: MachineStateCode + durationSeconds: Int! +} +``` + +### GenerateFleetDailyReportPayload + +```graphql +input GenerateFleetDailyReportPayload { + date: Date! + format: ReportFormat! +} +``` + +### ReportFile + +Ссылка на готовый файл отчета. + +```graphql +type ReportFile { + fileName: String! + mimeType: String! + downloadUrl: String! + expiresAt: DateTime +} +``` + +Физическая отдача файла может быть не через GraphQL, а через REST endpoint или object storage URL. GraphQL в таком случае возвращает `downloadUrl`. + +## Query + +```graphql +type Query { + currentUser: User + + getUsers( + page: Int! + pageSize: Int + query: String + roles: [UserRole!] + sortDirection: SortDirection! + sortField: UserSortField! + ): UsersPage! + + getUser(id: ID!): User + + getMachines( + page: Int! + pageSize: Int + query: String + sortDirection: SortDirection! + sortField: MachineSortField! + ): MachinesPage! + + getMachine(id: ID!): Machine + + getMachineTypes: [MachineType!]! + + getDepartments: [Department!]! + + getBeacons: [Beacon!]! + + getFreeBeacons: [Beacon!]! + + getBeaconTelemetry( + beaconId: ID + machineId: ID + startDate: DateTime! + endDate: DateTime! + ): [BeaconTelemetryPacket!]! + + getMachineStateDefinitions: [MachineStateDefinition!]! + + getMachineCalibrationRules(machineId: ID!): [MachineCalibrationRule!]! + + getMachineStateHistory( + machineId: ID! + startDate: DateTime! + endDate: DateTime! + page: Int + pageSize: Int + ): MachineStateHistoryPage! + + getMachineUsageSummary( + machineId: ID! + startDate: DateTime! + endDate: DateTime! + ): MachineUsageSummary! + + getUnrecognizedTelemetryIntervals( + machineId: ID! + startDate: DateTime! + endDate: DateTime! + ): [MachineStateInterval!]! + + getFleetReportSchedules: [FleetReportSchedule!]! + + getFleetReportDeliveries( + scheduleId: ID + dateFrom: Date + dateTo: Date + ): [FleetReportDelivery!]! + + getFleetDailyReport(date: Date!): FleetDailyReport! + + createFleetDailyReportDownloadUrl( + payload: GenerateFleetDailyReportPayload! + ): ReportFile! +} +``` + +## Mutation + +```graphql +type Mutation { + login(payload: LoginPayload!): User! + + logout: Boolean! + + createUser(payload: CreateUserPayload!): User! + + updateUser( + id: ID! + payload: UpdateUserPayload! + ): User! + + deleteUser(id: ID!): Boolean! + + createMachine(payload: CreateMachinePayload!): Machine! + + updateMachine( + id: ID! + payload: UpdateMachinePayload! + ): Machine! + + deleteMachine(id: ID!): Boolean! + + bindBeaconToMachine( + machineId: ID! + beaconId: ID! + ): Machine! + + unbindBeaconFromMachine(machineId: ID!): Machine! + + registerBeaconTelemetryPacket( + payload: BeaconTelemetryPayload! + ): Boolean! + + createMachineType(name: String!): MachineType! + + updateMachineType( + id: ID! + name: String! + ): MachineType! + + deleteMachineType(id: ID!): Boolean! + + createDepartment(name: String!): Department! + + updateDepartment( + id: ID! + name: String! + ): Department! + + deleteDepartment(id: ID!): Boolean! + + createMachineStateDefinition( + payload: CreateMachineStateDefinitionPayload! + ): MachineStateDefinition! + + updateMachineStateDefinition( + id: ID! + payload: UpdateMachineStateDefinitionPayload! + ): MachineStateDefinition! + + deleteMachineStateDefinition(id: ID!): Boolean! + + createMachineCalibrationRule( + payload: CreateMachineCalibrationRulePayload! + ): MachineCalibrationRule! + + updateMachineCalibrationRule( + id: ID! + payload: UpdateMachineCalibrationRulePayload! + ): MachineCalibrationRule! + + deleteMachineCalibrationRule(id: ID!): Boolean! + + recognizeMachineStates( + machineId: ID! + startDate: DateTime! + endDate: DateTime! + ): [MachineStateInterval!]! + + createFleetReportSchedule( + payload: CreateFleetReportSchedulePayload! + ): FleetReportSchedule! + + updateFleetReportSchedule( + id: ID! + payload: UpdateFleetReportSchedulePayload! + ): FleetReportSchedule! + + deleteFleetReportSchedule(id: ID!): Boolean! + + sendFleetDailyReport( + scheduleId: ID! + reportDate: Date! + ): FleetReportDelivery! +} +``` + +## Use-Cases + +### Пользователи + +`LoginUserUseCase` — авторизует пользователя по логину/email и паролю. + +`GetCurrentUserUseCase` — возвращает текущего авторизованного пользователя. + +`GetUsersUseCase` — возвращает список пользователей с поиском, сортировкой и пагинацией. + +`CreateUserUseCase` — создает пользователя. + +`UpdateUserUseCase` — редактирует данные пользователя. + +`DeleteUserUseCase` — удаляет пользователя или блокирует, если будет выбран soft-delete подход. + +### АТТ + +`GetMachinesUseCase` — возвращает список АТТ с поиском, сортировкой и пагинацией. + +`GetMachineByIdUseCase` — возвращает карточку конкретной АТТ. + +`CreateMachineUseCase` — создает новую АТТ. + +`UpdateMachineUseCase` — редактирует данные АТТ. + +`DeleteMachineUseCase` — удаляет АТТ или переводит в архив. + +`BindBeaconToMachineUseCase` — привязывает свободный маяк к АТТ. + +`UnbindBeaconFromMachineUseCase` — отвязывает маяк от АТТ. + +### Маяки и телеметрия + +`RegisterBeaconTelemetryPacketUseCase` — принимает сырое сообщение от маяка и сохраняет его. + +`ResolveBeaconByMacUseCase` — находит маяк по MAC-адресу из входящего пакета. + +`AttachTelemetryPacketToMachineUseCase` — определяет, к какой АТТ относится пакет, если маяк привязан. + +`GetBeaconTelemetryUseCase` — возвращает сырые показания маяка за период. + +`GetFreeBeaconsUseCase` — возвращает список свободных маяков. + +### Калибровка + +`GetMachineStateDefinitionsUseCase` — возвращает справочник состояний АТТ. + +`CreateMachineStateDefinitionUseCase` — создает новое состояние в справочнике. + +`UpdateMachineStateDefinitionUseCase` — редактирует состояние справочника. + +`DeleteMachineStateDefinitionUseCase` — удаляет состояние, если оно не используется правилами. + +`GetMachineCalibrationRulesUseCase` — возвращает правила калибровки конкретной АТТ. + +`CreateMachineCalibrationRuleUseCase` — создает правило калибровки для АТТ. + +`UpdateMachineCalibrationRuleUseCase` — редактирует правило калибровки. + +`DeleteMachineCalibrationRuleUseCase` — удаляет правило калибровки. + +`ValidateMachineCalibrationRuleUseCase` — проверяет правило на конфликты с уже существующими диапазонами. + +`RecognizeMachineStatesUseCase` — по сырым данным акселерометра и правилам калибровки строит интервалы состояний. + +`GetUnrecognizedTelemetryIntervalsUseCase` — возвращает участки телеметрии, которые не попали ни под одно правило. + +### История и аналитика + +`GetMachineStateHistoryUseCase` — возвращает историю состояний АТТ за период. + +`GetMachineUsageSummaryUseCase` — собирает краткую статистику по АТТ за период. + +`GetMachineDailyUsageStatsUseCase` — возвращает дневную статистику по АТТ для графиков. + +`AggregateMachineStateIntervalsUseCase` — агрегирует интервалы состояний из телеметрии. + +### Отчеты + +`GetFleetReportSchedulesUseCase` — возвращает настройки рассылок отчетов. + +`CreateFleetReportScheduleUseCase` — создает настройку ежедневной рассылки отчета. + +`UpdateFleetReportScheduleUseCase` — редактирует настройку рассылки. + +`DeleteFleetReportScheduleUseCase` — удаляет настройку рассылки. + +`GenerateFleetDailyReportUseCase` — собирает дневной отчет по парку АТТ за выбранную дату. + +`ExportFleetDailyReportToPdfUseCase` — формирует PDF-файл дневного отчета. + +`ExportFleetDailyReportToXlsxUseCase` — формирует Excel-файл дневного отчета. + +`DownloadFleetDailyReportUseCase` — отдает файл отчета пользователю по запросу из интерфейса. + +`RunScheduledFleetReportsUseCase` — периодически запускает активные расписания рассылок. + +`SendFleetDailyReportUseCase` — генерирует отчет и отправляет его выбранным пользователям. + +`CreateFleetReportDeliveryUseCase` — создает запись о попытке отправки отчета. + +`MarkFleetReportDeliveryAsSentUseCase` — помечает отправку как успешную. + +`MarkFleetReportDeliveryAsFailedUseCase` — сохраняет ошибку отправки. + +## Доменные сервисы + +`MachineStateRecognitionService` — определяет состояние АТТ по телеметрии и правилам калибровки. + +`MachineAnalyticsService` — считает агрегаты: время в движении, заведена, стояла, нет данных, смены состояний. + +`FleetDailyReportBuilder` — собирает доменную модель дневного отчета. + +`FleetReportExporter` — общий интерфейс для экспорта отчета в файл. + +`PdfFleetReportExporter` — экспортирует отчет в PDF. + +`XlsxFleetReportExporter` — экспортирует отчет в Excel. + +`MailService` — отправляет письма с вложениями. + +`ReportScheduler` — запускает ежедневные рассылки по расписанию. + diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..2a0aeac --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,26 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + rules: { + 'react-hooks/set-state-in-effect': 'warn', + 'react-refresh/only-export-components': 'warn', + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..d84e34b --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Fleet BLE Monitor + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..08c0ac8 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,7662 @@ +{ + "name": "fleet-ble-monitor", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fleet-ble-monitor", + "version": "0.0.0", + "dependencies": { + "@apollo/client": "^4.1.9", + "@hookform/resolvers": "^5.2.2", + "@internationalized/date": "^3.12.1", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-popover": "^1.1.15", + "@tanstack/react-table": "^8.21.3", + "date-fns": "^4.1.0", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.6", + "framer-motion": "^12.40.0", + "graphql": "^16.13.2", + "leaflet": "^1.9.4", + "lucide-react": "^1.9.0", + "qrcode.react": "^4.2.0", + "react": "^19.2.5", + "react-day-picker": "^9.14.0", + "react-dom": "^19.2.5", + "react-easy-crop": "^5.5.7", + "react-hook-form": "^7.73.1", + "react-leaflet": "^5.0.0", + "react-router-dom": "^7.14.2", + "sass": "^1.99.0", + "simplebar": "^6.3.3", + "simplebar-react": "^3.3.2", + "zod": "^4.3.6", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@graphql-codegen/cli": "^7.2.0", + "@graphql-codegen/client-preset": "^6.1.0", + "@graphql-typed-document-node/core": "^3.2.0", + "@types/leaflet": "^1.9.21", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-basic-ssl": "^2.3.0", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.10" + } + }, + "node_modules/@apollo/client": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.1.9.tgz", + "integrity": "sha512-qfpkQD51tdU/7iAR6aLb4w9o/L7I475DluWHRb61U/3Q0AH29nNOxOBHjBbWDdf16ncPOoQuxne1sEs2NjqBFw==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", + "@wry/equality": "^0.5.6", + "@wry/trie": "^0.5.0", + "graphql-tag": "^2.12.6", + "optimism": "^0.18.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "graphql": "^16.0.0", + "graphql-ws": "^5.5.5 || ^6.0.3", + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc", + "react-dom": "^17.0.0 || ^18.0.0 || >=19.0.0-rc", + "rxjs": "^7.3.0", + "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" + }, + "peerDependenciesMeta": { + "graphql-ws": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "subscriptions-transport-ws": { + "optional": true + } + } + }, + "node_modules/@ardatan/relay-compiler": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-13.0.1.tgz", + "integrity": "sha512-afG3YPwuSA0E5foouZusz5GlXKs74dObv4cuWyLyfKsYFj2r7oGRNB28v18HvwuLSQtQFCi+DpIe0TZkgQDYyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "immutable": "^5.1.5", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "graphql": "*" + } + }, + "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-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "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/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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/@date-fns/tz": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz", + "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@envelop/core": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.5.1.tgz", + "integrity": "sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@envelop/instrumentation": "^1.0.0", + "@envelop/types": "^5.2.1", + "@whatwg-node/promise-helpers": "^1.2.4", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@envelop/instrumentation": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@envelop/instrumentation/-/instrumentation-1.0.0.tgz", + "integrity": "sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.2.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@envelop/types": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@envelop/types/-/types-5.2.1.tgz", + "integrity": "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@fastify/busboy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@graphql-codegen/add": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-7.1.0.tgz", + "integrity": "sha512-bytJg1kel5zfgK3JSYbGwtpbNe6F9OPZSR6DiMDe9RVxblAgl6w4zEEPd/mM3rhNJ1VmGYLbNnf5e1eUfXQEbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/cli": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-7.2.0.tgz", + "integrity": "sha512-JPJw2vquEIpO3b8XJyxFVTrYi6WRn/OKu/SlzQA+IwAVT7GZPeG+AHmfRXAvpVMj31899nTpQYEQGUxx3ZqubQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.18.13", + "@babel/template": "^7.18.10", + "@babel/types": "^7.18.13", + "@graphql-codegen/client-preset": "^6.1.0", + "@graphql-codegen/core": "^6.2.0", + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-tools/apollo-engine-loader": "^8.0.28", + "@graphql-tools/code-file-loader": "^8.1.28", + "@graphql-tools/git-loader": "^8.0.32", + "@graphql-tools/github-loader": "^9.0.6", + "@graphql-tools/graphql-file-loader": "^8.1.11", + "@graphql-tools/json-file-loader": "^8.0.26", + "@graphql-tools/load": "^8.1.8", + "@graphql-tools/merge": "^9.0.6", + "@graphql-tools/url-loader": "^9.0.6", + "@graphql-tools/utils": "^11.2.0", + "@inquirer/prompts": "^8.3.2", + "@whatwg-node/fetch": "^0.10.0", + "chalk": "^5.6.0", + "cosmiconfig": "^9.0.0", + "debounce": "^3.0.0", + "detect-indent": "^7.0.0", + "graphql-config": "^5.1.6", + "is-glob": "^4.0.1", + "jiti": "^2.3.0", + "json-to-pretty-yaml": "^1.2.2", + "listr2": "^10.2.1", + "log-symbols": "^7.0.0", + "micromatch": "^4.0.5", + "shell-quote": "^1.7.3", + "string-env-interpolation": "^1.0.1", + "ts-log": "^3.0.0", + "tslib": "^2.4.0", + "yaml": "^2.3.1", + "yargs": "^18.0.0" + }, + "bin": { + "gql-gen": "esm/bin.js", + "graphql-code-generator": "esm/bin.js", + "graphql-codegen": "esm/bin.js", + "graphql-codegen-cjs": "cjs/bin.js", + "graphql-codegen-esm": "esm/bin.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@parcel/watcher": "^2.1.0", + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@parcel/watcher": { + "optional": true + } + } + }, + "node_modules/@graphql-codegen/client-preset": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-6.1.0.tgz", + "integrity": "sha512-mGmBuwrOU5oRoaWFodx8g9xu1jecYIiydqvk88QsAIsyMcZwuoybs1lyne85TovpBHjH5CC2wnZGsbDQfcgOCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/template": "^7.20.7", + "@graphql-codegen/add": "^7.1.0", + "@graphql-codegen/gql-tag-operations": "^6.1.0", + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-codegen/typed-document-node": "^7.1.0", + "@graphql-codegen/typescript": "^6.1.0", + "@graphql-codegen/typescript-operations": "^6.1.0", + "@graphql-codegen/visitor-plugin-common": "^7.2.0", + "@graphql-tools/documents": "^1.0.0", + "@graphql-tools/utils": "^11.2.0", + "@graphql-typed-document-node/core": "3.2.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", + "graphql-sock": "^1.0.0" + }, + "peerDependenciesMeta": { + "graphql-sock": { + "optional": true + } + } + }, + "node_modules/@graphql-codegen/core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/core/-/core-6.2.0.tgz", + "integrity": "sha512-RZadhhwYhuy2ZdIGK40vYVBMzXEFGkCC+58MUC/F2af/gKznEYNzHgmNBUBCk/BTklyUsNu0mIXmyGE4tTA0PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-tools/schema": "^10.0.0", + "@graphql-tools/utils": "^11.2.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/gql-tag-operations": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-6.1.0.tgz", + "integrity": "sha512-AmMcZFwonufvWJnQm7I0lBxKpAm+35BcCrOOvUlBoviohiR17aPoTGAOaNAEtpcpI86lnZ9m9AXUdiKMdm8nnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-codegen/visitor-plugin-common": "^7.2.0", + "@graphql-tools/utils": "^11.2.0", + "auto-bind": "^5.0.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/plugin-helpers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-7.1.0.tgz", + "integrity": "sha512-ieJH7kZ5oSZKBPJs7CvHMrFY/CLYLklqv74ir93qMwRna6geZsbIMoJzTDBXohxcQTITiProiYSGrEtZjIpYGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.2.0", + "change-case-all": "^2.1.0", + "common-tags": "1.8.2", + "import-from": "4.0.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/schema-ast": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/schema-ast/-/schema-ast-6.1.0.tgz", + "integrity": "sha512-/xuGkM5gUNFRoaQLumKbENdX7Hc8ha49z9OXsEZY8E+46mMjqzXGF0NtCJ892cmoX7EUgI5c8T+LZqS2upx2Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-tools/utils": "^11.2.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/typed-document-node": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-7.1.0.tgz", + "integrity": "sha512-V6H+ItyqXtYY+JQb76LAoN627Xfzpn29/ifwCFAv61iEepzNzh86sa+yZclflr0G8LDmhcVY5hpPJd3a1qbOfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-codegen/visitor-plugin-common": "^7.2.0", + "auto-bind": "^5.0.0", + "change-case-all": "^2.1.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/typescript": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-6.1.0.tgz", + "integrity": "sha512-2Hu3111O/AwV28Ap7tNsixlmXSAJuQbQArQklx+IC/tNswpckZnCfmlcBtTJrGU1+mJXEneJXGfb2XWvKjbhlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-codegen/schema-ast": "^6.1.0", + "@graphql-codegen/visitor-plugin-common": "^7.2.0", + "auto-bind": "^5.0.0", + "tslib": "~2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/typescript-operations": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-6.1.0.tgz", + "integrity": "sha512-zv0ohBJqLP1G/Kgiq1MhLTdNmakChtJivpaMaBmuPz9gasKWJkc9MhxuVytF6xS27PkJOh81TNM9srAkqpdejA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-codegen/schema-ast": "^6.1.0", + "@graphql-codegen/visitor-plugin-common": "^7.2.0", + "auto-bind": "^5.0.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", + "graphql-sock": "^1.0.0" + }, + "peerDependenciesMeta": { + "graphql-sock": { + "optional": true + } + } + }, + "node_modules/@graphql-codegen/visitor-plugin-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-7.2.0.tgz", + "integrity": "sha512-qqtTY8taONuxlR0HvD8z+zgWC3CfJ47M2rATx+geWNIN8v8Itc5TflHYxjkWAtNL6E3q7WZRY5P7se+2S3EjBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-tools/optimize": "^2.0.0", + "@graphql-tools/relay-operation-optimizer": "^7.1.1", + "@graphql-tools/utils": "^11.2.0", + "auto-bind": "^5.0.0", + "change-case-all": "^2.1.0", + "dependency-graph": "^1.0.0", + "graphql-tag": "^2.11.0", + "parse-filepath": "^1.0.2", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-hive/signal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@graphql-hive/signal/-/signal-2.0.0.tgz", + "integrity": "sha512-Pz8wB3K0iU6ae9S1fWfsmJX24CcGeTo6hE7T44ucmV/ALKRj+bxClmqrYcDT7v3f0d12Rh4FAXBb6gon+WkDpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@graphql-tools/apollo-engine-loader": { + "version": "8.0.32", + "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.32.tgz", + "integrity": "sha512-717/RCJQd0RDuv50rqGtIR7KrAR2h/fWYhBeVnSq5zYPyMOVxpSCoB1dHYws3fFENar021mgHyGNcPwJO5Fb3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.2.0", + "@whatwg-node/fetch": "^0.10.13", + "sync-fetch": "0.6.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/batch-execute": { + "version": "10.0.9", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-10.0.9.tgz", + "integrity": "sha512-khIgAPlyaWJ3dVX6SsqOkABZCH1Gii32WHn3xMzavupsxPCfb/9G3zjdswptzTFrOcZ92dWo7MXvwNFkRfNN4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.0.0", + "@whatwg-node/promise-helpers": "^1.3.2", + "dataloader": "^2.2.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/code-file-loader": { + "version": "8.1.34", + "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.1.34.tgz", + "integrity": "sha512-MOyXRh94EvBVIhlS8kh3aRhQiOdPfiCZvfd7g5/t7cW3VMa+Z0sfuNI5r7E2VxXZkZUsJ6yRj56EwAPY5fR4Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/graphql-tag-pluck": "8.3.33", + "@graphql-tools/utils": "^11.2.0", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/delegate": { + "version": "12.0.19", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-12.0.19.tgz", + "integrity": "sha512-TFDKalXQub5dV7FO1y+3kwwJm337jwUFkTmXHlmYAtTZTxDuSVeHc74OfnepFe+6zEDriHqhHKvxSq5TpHCTvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/batch-execute": "^10.0.9", + "@graphql-tools/executor": "^1.4.13", + "@graphql-tools/schema": "^10.0.29", + "@graphql-tools/utils": "^11.0.0", + "@repeaterjs/repeater": "^3.0.6", + "@whatwg-node/promise-helpers": "^1.3.2", + "dataloader": "^2.2.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/documents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/documents/-/documents-1.0.1.tgz", + "integrity": "sha512-aweoMH15wNJ8g7b2r4C4WRuJxZ0ca8HtNO54rkye/3duxTkW4fGBEutCx03jCIr5+a1l+4vFJNP859QnAVBVCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.5.5.tgz", + "integrity": "sha512-JAdn4G+ehthnGdJABS+wO6LxAIcoGPiSRHIz1ECYLtaQGkpFIdqH6BLUbZcToX/W/nmOG0kTFLoepCGkugbsBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.2.0", + "@graphql-typed-document-node/core": "^3.2.0", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-common": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-1.0.6.tgz", + "integrity": "sha512-23/K5C+LSlHDI0mj2SwCJ33RcELCcyDUgABm1Z8St7u/4Z5+95i925H/NAjUyggRjiaY8vYtNiMOPE49aPX1sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@envelop/core": "^5.4.0", + "@graphql-tools/utils": "^11.0.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-graphql-ws": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-3.1.5.tgz", + "integrity": "sha512-WXRsfwu9AkrORD9nShrd61OwwxeQ5+eXYcABRR3XPONFIS8pWQfDJGGqxql9/227o/s0DV5SIfkBURb5Knzv+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/executor-common": "^1.0.6", + "@graphql-tools/utils": "^11.0.0", + "@whatwg-node/disposablestack": "^0.0.6", + "graphql-ws": "^6.0.6", + "isows": "^1.0.7", + "tslib": "^2.8.1", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-http": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-3.3.0.tgz", + "integrity": "sha512-IkKXIjSg9U8MNsQUBVJAXE4+LSxaQ0cs7p5JTALLGDABY1o17vPDRwWALsX81AXD5dY27ihi/+OhGMueW/Fopg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-hive/signal": "^2.0.0", + "@graphql-tools/executor-common": "^1.0.6", + "@graphql-tools/utils": "^11.0.0", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/fetch": "^0.10.13", + "@whatwg-node/promise-helpers": "^1.3.2", + "meros": "^1.3.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-legacy-ws": { + "version": "1.1.30", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.1.30.tgz", + "integrity": "sha512-sCY1Fn5M7/1eMbFuvEwzGqAdjdkKECUWbaTO1lo3RQVyHC8HUYe/DB/tEoQfT7TQarauX9KjXsSf2uqCyJyOOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.2.0", + "@types/ws": "^8.0.0", + "isomorphic-ws": "^5.0.0", + "tslib": "^2.4.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/git-loader": { + "version": "8.0.38", + "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-8.0.38.tgz", + "integrity": "sha512-uY6/Mth0nv35Jg6VSwZA0x19oALxuLnS9wU9r6L1QUDqMQib5agvbSjUGwond8vDXM0U5ZeDDnuyvpGz9yMH5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/graphql-tag-pluck": "8.3.33", + "@graphql-tools/utils": "^11.2.0", + "is-glob": "4.0.3", + "micromatch": "^4.0.8", + "tslib": "^2.4.0", + "unixify": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/github-loader": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-9.1.4.tgz", + "integrity": "sha512-+ul7Y5f7a5r9nc0KWaDt+Rf0NdRzU3L4njvVURcZM2oxn2RcHIVLZQBMjglO/Q8vgC+IUBtQ7C0llgBOBHiQKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/executor-http": "^3.3.0", + "@graphql-tools/graphql-tag-pluck": "^8.3.33", + "@graphql-tools/utils": "^11.2.0", + "@whatwg-node/fetch": "^0.10.13", + "@whatwg-node/promise-helpers": "^1.0.0", + "sync-fetch": "0.6.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/graphql-file-loader": { + "version": "8.1.16", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.1.16.tgz", + "integrity": "sha512-1OFMnmadqwAalty2nka8EpejM1T9QCgtNApGMTvIsz141y9lfK3Xj9lNTgsJk+u2kyjsEYyo3F+tc6wEqMdqTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/import": "^7.1.16", + "@graphql-tools/utils": "^11.2.0", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/graphql-tag-pluck": { + "version": "8.3.33", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.33.tgz", + "integrity": "sha512-12nfDKx1wpe5Zd2VhUCuZKaNzd0/ZQf1X5M8jHkRloGA61k+vr7dTDdlWrWcGDr1EufuoviL+e1UqGxD8D9PWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.7", + "@babel/parser": "^7.29.3", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "@graphql-tools/utils": "^11.2.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/import": { + "version": "7.1.16", + "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.1.16.tgz", + "integrity": "sha512-vx2v9qqiQF15RK3O68kaqSXDQ/++xU4qkcNBCME3Y82qLGsKNPNJT8alklWlAJAvFs7LrXrCr+ZqbidO4pYaxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.2.0", + "resolve-from": "5.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/json-file-loader": { + "version": "8.0.30", + "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.30.tgz", + "integrity": "sha512-Ij97Ij+Uz3hvuetRTLX8v/d3ZyT2i17nyZN1maCTHW3tOypUevnuW7TKJcX0rOCsiP0juIcnnG91hcKsBh0Ktw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.2.0", + "globby": "^11.0.3", + "tslib": "^2.4.0", + "unixify": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/load": { + "version": "8.1.13", + "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.1.13.tgz", + "integrity": "sha512-XstLIkH2gDVk3xlzbfNyR+MFGTaCop2rJSEyShXTnIy4Sh+DoNM6re8eXzZPIOs+N33kuOWm6e0EGE9tGpHUNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/schema": "^10.0.36", + "@graphql-tools/utils": "^11.2.0", + "p-limit": "3.1.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/merge": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.2.0.tgz", + "integrity": "sha512-kChDH/sOxm3TCICup5NgTiz/aJBk+5GAuC0lfJS8FcRfsqZvlCkNwpsYTGAATBCJMrgZ9+csRugCjS97jPcNMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.2.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/optimize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-2.0.0.tgz", + "integrity": "sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/relay-operation-optimizer": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.1.6.tgz", + "integrity": "sha512-uh4ikON4vCb95M8pjGRckucCAm7eq6TB9FXP0zZByaSO6dFkTR1jKrOSWrJ2qx+IR8B6bVaR/Zst9vGs26ZjXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ardatan/relay-compiler": "^13.0.1", + "@graphql-tools/utils": "^11.2.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/schema": { + "version": "10.0.36", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.36.tgz", + "integrity": "sha512-g8S5aLirZInoi3NojzmBxwsZfVawOE0UBlVWYe8kDAR0FxS0riBDiyW7JnlAKayooHMRAMwGaze4RQU3VTTyig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/merge": "^9.2.0", + "@graphql-tools/utils": "^11.2.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/url-loader": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-9.1.4.tgz", + "integrity": "sha512-w65oPIvUxdJ8eSydIfG2jllbBY5+lijG1mCY22kpGcp/gqPUdH+L6TIr81ZeTZIWnV2DioSP7g3rLE4JGW6uyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/executor-graphql-ws": "^3.1.4", + "@graphql-tools/executor-http": "^3.3.0", + "@graphql-tools/executor-legacy-ws": "^1.1.30", + "@graphql-tools/utils": "^11.2.0", + "@graphql-tools/wrap": "^11.1.1", + "@types/ws": "^8.0.0", + "@whatwg-node/fetch": "^0.10.13", + "@whatwg-node/promise-helpers": "^1.0.0", + "isomorphic-ws": "^5.0.0", + "sync-fetch": "0.6.0", + "tslib": "^2.4.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/utils": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.2.0.tgz", + "integrity": "sha512-eu9h1R3j/wWc4rvmYJF5AKtlwniDzstrZ/c6KSz+HdI+n7I7iog9xyKmBfpUwSbG1TqPNZBzWjFMkzdYOKq6Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/wrap": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-11.1.18.tgz", + "integrity": "sha512-qSstY1hw8rY3bTvy8NlRrJ4QOLtPdo6ykiv4y9EHaeYtNO3TWiIs0nLbUZX2F9wKT/i7T1aMbVysfeQoNmJvdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/delegate": "^12.0.19", + "@graphql-tools/schema": "^10.0.29", + "@graphql-tools/utils": "^11.0.0", + "@whatwg-node/promise-helpers": "^1.3.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", + "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz", + "integrity": "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", + "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", + "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/external-editor": "^3.0.3", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", + "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", + "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", + "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", + "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", + "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.2.tgz", + "integrity": "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.1", + "@inquirer/confirm": "^6.1.1", + "@inquirer/editor": "^5.2.2", + "@inquirer/expand": "^5.1.1", + "@inquirer/input": "^5.1.2", + "@inquirer/number": "^4.1.1", + "@inquirer/password": "^5.1.1", + "@inquirer/rawlist": "^5.3.1", + "@inquirer/search": "^4.2.1", + "@inquirer/select": "^5.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", + "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", + "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", + "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@internationalized/date": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.1.tgz", + "integrity": "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "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/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/@repeaterjs/repeater": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.1.0.tgz", + "integrity": "sha512-TaoVksZRSx2KWYYpyLQtMQXXeS98VsgZImzW65xmiVgbYhXLk+aEsmzPLirqVuE4/XuUapH2iMtxUzaBNDzdSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tabby_ai/hijri-converter": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz", + "integrity": "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", + "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/type-utils": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", + "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.0", + "@typescript-eslint/types": "^8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.0", + "@typescript-eslint/tsconfig-utils": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz", + "integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@whatwg-node/disposablestack": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz", + "integrity": "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/fetch": { + "version": "0.10.13", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.13.tgz", + "integrity": "sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@whatwg-node/node-fetch": "^0.8.3", + "urlpattern-polyfill": "^10.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/node-fetch": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.8.6.tgz", + "integrity": "sha512-BDMdYFcerLQkwA2RTldxOqRCs6ZQD1S7UgP3pUdGUkcbgTrP/V5ko77ZkCww9DHmC4lpoYuwigGfQYj285gMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^3.1.1", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/promise-helpers": "^1.3.2", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/promise-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz", + "integrity": "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@wry/caches": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", + "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/context": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", + "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/equality": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz", + "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/trie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz", + "integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001805", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", + "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/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/change-case-all": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/change-case-all/-/change-case-all-2.1.0.tgz", + "integrity": "sha512-v6b0WWWkZUMHVuYk82l+WROgkUm4qEN2w5hKRNWtEOYwWqUGoi8C6xH0l1RLF1EoWqDFK6MFclmN3od6ws3/uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "change-case": "^5.2.0", + "sponge-case": "^2.0.2", + "swap-case": "^3.0.2", + "title-case": "^3.0.3" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.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/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-inspect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz", + "integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, + "node_modules/debounce": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-3.0.0.tgz", + "integrity": "sha512-64byRbF0/AirwbuHqB3/ZpMG9/nckDa6ZA0yd6UnaQNwbbemCOwvz2sL5sjXLHhZHADyiwLm0M5qMhltUUx+TA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/detect-indent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.2.tgz", + "integrity": "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/echarts-for-react": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.6.tgz", + "integrity": "sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "size-sensor": "^1.0.1" + }, + "peerDependencies": { + "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "react": "^15.0.0 || >=16.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.391", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.391.tgz", + "integrity": "sha512-YmCu4856jkgKT1Nh6fwRdeVrM6Ydf/fBnq51tpmSfX+jOcUMTxh31yH6hjKScRenhB2oDSvA9oooxcpjogPeig==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "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/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", + "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/framer-motion": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz", + "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.40.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "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/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphql": { + "version": "16.13.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", + "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-config": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-5.1.6.tgz", + "integrity": "sha512-fCkYnm4Kdq3un0YIM4BCZHVR5xl0UeLP6syxxO7KAstdY7QVyVvTHP0kRPDYEP1v08uwtJVgis5sj3IOTLOniQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/graphql-file-loader": "^8.0.0", + "@graphql-tools/json-file-loader": "^8.0.0", + "@graphql-tools/load": "^8.1.0", + "@graphql-tools/merge": "^9.0.0", + "@graphql-tools/url-loader": "^9.0.0", + "@graphql-tools/utils": "^11.0.0", + "cosmiconfig": "^8.1.0", + "jiti": "^2.0.0", + "minimatch": "^10.0.0", + "string-env-interpolation": "^1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "cosmiconfig-toml-loader": "^1.0.0", + "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "cosmiconfig-toml-loader": { + "optional": true + } + } + }, + "node_modules/graphql-config/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/graphql-ws": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.1.0.tgz", + "integrity": "sha512-7ft6KWkuaLnLABwzEIimjUMeF0iByo2ThD6q0MICgsvp6nDuT5ppubKzEHniu8Kmlp5GNsLgr5dil8JMrIwUEQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@fastify/websocket": "^10 || ^11", + "crossws": "~0.3", + "graphql": "^15.10.1 || ^16 || ^17", + "ws": "^8" + }, + "peerDependenciesMeta": { + "@fastify/websocket": { + "optional": true + }, + "crossws": { + "optional": true + }, + "ws": { + "optional": true + } + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-4.0.0.tgz", + "integrity": "sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-to-pretty-yaml": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz", + "integrity": "sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "remedial": "^1.0.7", + "remove-trailing-spaces": "^1.0.6" + }, + "engines": { + "node": ">= 0.2.0" + } + }, + "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/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.2.tgz", + "integrity": "sha512-JtNtbZj8q5BnDMR7trpwvwk3RIrANtIVzEUm8w7amp6xelLgyuq+4WZoTH913XaQAoH/cNdYhaNzBPA2U3xbDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^10.0.0" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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==", + "dev": true, + "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/lucide-react": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.9.0.tgz", + "integrity": "sha512-6qVAmbgCjcJz7sAGSPSSJ++RAwjlK2XCbRrZKv63Ciko1KT8jX0//CXxgI3jg2HlJu8tADqdYlNDebmYjeoruA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/meros": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.3.2.tgz", + "integrity": "sha512-Q3mobPbvEx7XbwhnC1J1r60+5H6EZyNccdzSz0eGexJRwouUtTZxPVRGdqKtxlpD84ScK4+tIGldkqDtCKdI0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=13" + }, + "peerDependencies": { + "@types/node": ">=13" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/motion-dom": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz", + "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, + "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/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "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/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-wheel": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz", + "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==", + "license": "BSD-3-Clause" + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optimism": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.1.tgz", + "integrity": "sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==", + "license": "MIT", + "dependencies": { + "@wry/caches": "^1.0.0", + "@wry/context": "^0.7.0", + "@wry/trie": "^0.5.0", + "tslib": "^2.3.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.14.0.tgz", + "integrity": "sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "@tabby_ai/hijri-converter": "1.0.5", + "date-fns": "^4.1.0", + "date-fns-jalali": "4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-easy-crop": { + "version": "5.5.7", + "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.5.7.tgz", + "integrity": "sha512-kYo4NtMeXFQB7h1U+h5yhUkE46WQbQdq7if54uDlbMdZHdRgNehfvaFrXnFw5NR1PNoUOJIfTwLnWmEx/MaZnA==", + "license": "MIT", + "dependencies": { + "normalize-wheel": "^1.0.1", + "tslib": "^2.0.1" + }, + "peerDependencies": { + "react": ">=16.4.0", + "react-dom": ">=16.4.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.73.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.73.1.tgz", + "integrity": "sha512-VAfVYOPcx3piiEVQy95vyFmBwbVUsP/AUIN+mpFG8h11yshDd444nn0VyfaGWSRnhOLVgiDu7HIuBtAIzxn9dA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", + "integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz", + "integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/remedial": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz", + "integrity": "sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "engines": { + "node": "*" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, + "node_modules/remove-trailing-spaces": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/remove-trailing-spaces/-/remove-trailing-spaces-1.0.9.tgz", + "integrity": "sha512-xzG7w5IRijvIkHIjDk65URsJJ7k4J95wmcArY5PRcmjldIOl7oTvG8+X2Ag690R7SfwiOcHrWZKVc1Pp5WIOzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "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/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simplebar": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/simplebar/-/simplebar-6.3.3.tgz", + "integrity": "sha512-ni9cIiA8GRitiaenV7A/gh8nTStiVqO3gLxrfCMmq6CSy/pB+fz+QxqvJLo142QHb2Tvgvr8ILwZxf3qzAq7pg==", + "license": "MIT", + "dependencies": { + "simplebar-core": "^1.3.2" + } + }, + "node_modules/simplebar-core": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/simplebar-core/-/simplebar-core-1.3.2.tgz", + "integrity": "sha512-qKgTTuTqapjsFGkNhCjyPhysnbZGpQqNmjk0nOYjFN5ordC/Wjvg+RbYCyMSnW60l/Z0ZS82GbNltly6PMUH1w==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "lodash-es": "^4.17.21" + } + }, + "node_modules/simplebar-react": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/simplebar-react/-/simplebar-react-3.3.2.tgz", + "integrity": "sha512-ZsgcQhKLtt5ra0BRIJeApfkTBQCa1vUPA/WXI4HcYReFt+oCEOvdVz6rR/XsGJcKxTlCRPmdGx1uJIUChupo+A==", + "license": "MIT", + "dependencies": { + "simplebar-core": "^1.3.2" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/size-sensor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.3.tgz", + "integrity": "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==", + "license": "ISC" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sponge-case": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-2.0.3.tgz", + "integrity": "sha512-i4h9ZGRfxV6Xw3mpZSFOfbXjf0cQcYmssGWutgNIfFZ2VM+YIWfD71N/kjjwK6X/AAHzBr+rciEcn/L34S8TGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-env-interpolation": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz", + "integrity": "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/swap-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-3.0.3.tgz", + "integrity": "sha512-6p4op8wE9CQv7uDFzulI6YXUw4lD9n4oQierdbFThEKVWVQcbQcUjdP27W8XE7V4QnWmnq9jueSHceyyQnqQVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/sync-fetch": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.6.0.tgz", + "integrity": "sha512-IELLEvzHuCfc1uTsshPK58ViSdNqXxlml1U+fmwJIKLYKOr/rAtBrorE2RYm5IHaMpDNlmC0fr1LAvdXvyheEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^3.3.2", + "timeout-signal": "^2.0.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/timeout-signal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/timeout-signal/-/timeout-signal-2.0.0.tgz", + "integrity": "sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/title-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", + "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-log": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/ts-log/-/ts-log-3.0.2.tgz", + "integrity": "sha512-esq6hx2lM66sQV1YcFkIYTqrWWabmqBqobKHyn1CswdI5FgfQhkmiKiRWVGBNlIbdjBxEIkNvMIwLKKPgRYZLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20", + "npm": ">=10" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", + "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.0", + "@typescript-eslint/parser": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unixify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz", + "integrity": "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "normalize-path": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..068ea76 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,62 @@ +{ + "name": "fleet-ble-monitor", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host 192.168.1.91", + "graphql:codegen": "graphql-codegen --config codegen.ts", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@apollo/client": "^4.1.9", + "@hookform/resolvers": "^5.2.2", + "@internationalized/date": "^3.12.1", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-popover": "^1.1.15", + "@tanstack/react-table": "^8.21.3", + "date-fns": "^4.1.0", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.6", + "framer-motion": "^12.40.0", + "graphql": "^16.13.2", + "leaflet": "^1.9.4", + "lucide-react": "^1.9.0", + "qrcode.react": "^4.2.0", + "react": "^19.2.5", + "react-day-picker": "^9.14.0", + "react-dom": "^19.2.5", + "react-easy-crop": "^5.5.7", + "react-hook-form": "^7.73.1", + "react-leaflet": "^5.0.0", + "react-router-dom": "^7.14.2", + "sass": "^1.99.0", + "simplebar": "^6.3.3", + "simplebar-react": "^3.3.2", + "zod": "^4.3.6", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@graphql-codegen/cli": "^7.2.0", + "@graphql-codegen/client-preset": "^6.1.0", + "@graphql-typed-document-node/core": "^3.2.0", + "@types/leaflet": "^1.9.21", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-basic-ssl": "^2.3.0", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.10" + } +} diff --git a/frontend/public/devices/армафон3.3+.webp b/frontend/public/devices/армафон3.3+.webp new file mode 100644 index 0000000..c526803 Binary files /dev/null and b/frontend/public/devices/армафон3.3+.webp differ diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..7ea007a --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/public/photo_2025-03-29_20-05-51.jpg b/frontend/public/photo_2025-03-29_20-05-51.jpg new file mode 100644 index 0000000..bfcaa5f Binary files /dev/null and b/frontend/public/photo_2025-03-29_20-05-51.jpg differ diff --git a/frontend/src/App.scss b/frontend/src/App.scss new file mode 100644 index 0000000..1114bd7 --- /dev/null +++ b/frontend/src/App.scss @@ -0,0 +1,186 @@ +@use './shared/styles/variables' as *; + +.counter { + font-size: $font-size-16; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid $transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: $font-size-16; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid $transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..30b7f0a --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,21 @@ +//import reactLogo from './assets/react.svg' +//import viteLogo from './assets/vite.svg' +//import heroImg from './assets/hero.png' +import './App.scss' + +function App() { + //const [count, setCount] = useState(0) + + return ( + <> +
+ +
+ +
+
+ + ) +} + +export default App diff --git a/frontend/src/app/layouts/AppLayout.scss b/frontend/src/app/layouts/AppLayout.scss new file mode 100644 index 0000000..cadcbce --- /dev/null +++ b/frontend/src/app/layouts/AppLayout.scss @@ -0,0 +1,288 @@ +@use '../../shared/styles/variables' as *; + +.app-layout { + display: flex; + width: 100%; + min-width: 0; + height: 100vh; + min-height: 100vh; + overflow: hidden; + background: + radial-gradient( + 52% 42% at 16% 18%, + rgba(45, 140, 255, 0.055) 0%, + transparent 72% + ), + radial-gradient( + 50% 42% at 88% 16%, + rgba(3, 29, 154, 0.055) 0%, + transparent 74% + ), + radial-gradient( + 54% 44% at 14% 80%, + rgba(51, 179, 67, 0.06) 0%, + transparent 72% + ), + radial-gradient( + 56% 46% at 86% 78%, + rgba(66, 93, 232, 0.075) 0%, + transparent 74% + ), + radial-gradient( + 74% 44% at 50% 106%, + rgba(3, 29, 154, 0.075) 0%, + rgba(66, 93, 232, 0.04) 42%, + transparent 78% + ), + linear-gradient( + 180deg, + #eff2f7 0%, + #f8fafc 40%, + #f4f7fb 70%, + #eff2f7 100% + ); + + [data-theme='dark'] & { + --color-bg: #242a33; + + background: + radial-gradient( + 54% 46% at 16% 18%, + rgba(45, 140, 255, 0.085) 0%, + transparent 72% + ), + radial-gradient( + 52% 44% at 86% 16%, + rgba(99, 102, 241, 0.085) 0%, + transparent 72% + ), + radial-gradient( + 54% 44% at 14% 78%, + rgba(24, 184, 155, 0.07) 0%, + transparent 72% + ), + radial-gradient( + 58% 48% at 86% 78%, + rgba(45, 140, 255, 0.095) 0%, + transparent 74% + ), + radial-gradient( + 76% 46% at 50% 106%, + rgba(45, 140, 255, 0.16) 0%, + rgba(72, 96, 180, 0.085) 42%, + transparent 78% + ), + linear-gradient( + 180deg, + #242a33 0%, + #242a33 12%, + #222832 18%, + #1f252e 30%, + #1d232c 55%, + #171b22 100% + ); + } +} + +.app-layout__sidebar { + flex: 0 0 auto; +} + +.app-layout__content { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; + min-height: 0; + height: 100vh; + max-height: 100vh; + //padding: 0px 20px 20px 20px; +} + +.app-layout__scroll { + flex: 1 1 auto; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + + .simplebar-content { + min-height: 100%; + box-sizing: border-box; + padding: + $layout-page-padding-top + $layout-page-padding-right + $layout-page-padding-bottom + $layout-page-padding-left; + + display: flex; + flex-direction: column; + } + + .simplebar-track.simplebar-vertical { + top: 74px; + right: -8px; + bottom: 8px; + width: 16px; + } + + .simplebar-scrollbar::before { + background: $gray50; + border-radius: 8px; + opacity: 0.65; + } +} + +.app-layout__route-content { + display: flex; + flex: 1 0 auto; + min-width: 0; + min-height: 0; + flex-direction: column; +} + +.app-layout__unsupported { + display: none !important; +} + +.app-layout__drawer-overlay { + position: fixed; + inset: 0; + z-index: 800; + background: $color-backdrop-soft; + backdrop-filter: blur(4px); + + &[data-state='open'] { + animation: appLayoutDrawerOverlayShow 0.18s ease both; + } + + &[data-state='closed'] { + animation: appLayoutDrawerOverlayHide 0.18s ease both; + } +} + +.app-layout__drawer { + position: fixed; + inset: 0 auto 0 0; + z-index: 801; + margin: 10px; + width: min(320px, calc(100vw - 32px)); + padding: 0; + border: none; + border-radius: 20px; + //background: $color-bg; + box-shadow: $shadow-modal-dark; + overflow: hidden; + + &[data-state='open'] { + animation: appLayoutDrawerShow 0.24s cubic-bezier(0.34, 1.02, 0.64, 1) both; + } + + &[data-state='closed'] { + animation: appLayoutDrawerHide 0.2s cubic-bezier(0.4, 0, 0.2, 1) both; + } +} + +.app-layout__drawer-scroll { + width: 100%; + height: 100%; + max-height: 100%; + + .simplebar-track.simplebar-vertical { + width: 10px; + right: 2px; + } + + .simplebar-scrollbar::before { + background: $gray50; + opacity: 0.55; + } +} + +.app-layout__drawer-title { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +@include wide-down { + .app-layout__sidebar { + display: none; + } + + .app-layout__content { + height: 100vh; + max-height: 100vh; + box-sizing: border-box; + } +} + +@media (max-width: 1279.98px) { + .app-layout { + min-height: 100vh; + } + + .app-layout__sidebar, + .app-layout__content, + .app-layout__drawer-overlay, + .app-layout__drawer { + display: none; + } + + .app-layout__unsupported { + display: flex !important; + min-height: 100vh !important; + padding: 32px !important; + background: $color-bg; + } +} + +@keyframes appLayoutDrawerOverlayShow { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes appLayoutDrawerOverlayHide { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@keyframes appLayoutDrawerShow { + from { + opacity: 0; + transform: translateX(-16px); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes appLayoutDrawerHide { + from { + opacity: 1; + transform: translateX(0); + } + + to { + opacity: 0; + transform: translateX(-16px); + } +} diff --git a/frontend/src/app/layouts/AppLayout.tsx b/frontend/src/app/layouts/AppLayout.tsx new file mode 100644 index 0000000..d901847 --- /dev/null +++ b/frontend/src/app/layouts/AppLayout.tsx @@ -0,0 +1,99 @@ +import { + useEffect, + useRef, + useState, + type Dispatch, + type SetStateAction, +} from 'react' +import { Outlet } from 'react-router-dom' +import * as Dialog from '@radix-ui/react-dialog' +import SimpleBar from 'simplebar-react' +import type SimpleBarCore from 'simplebar-core' +import 'simplebar-react/dist/simplebar.min.css' +import { Sidebar } from '../../widgets/Sidebar/Sidebar' +import './AppLayout.scss' +import { Navbar } from '../../widgets/Navbar/Navbar' +import { EmptyState } from '../../shared/ui/EmptyState/EmptyState' +import type { NavbarBreadcrumb } from '../../widgets/Navbar/Navbar' + +export type AppLayoutOutletContext = { + setNavbarBreadcrumbs: Dispatch> +} + +export function AppLayout() { + const [isSidebarDrawerOpen, setIsSidebarDrawerOpen] = useState(false) + const contentScrollRef = useRef(null) + const [isContentScrolled, setIsContentScrolled] = useState(false) + const [navbarBreadcrumbs, setNavbarBreadcrumbs] = useState< + NavbarBreadcrumb[] + >([]) + + useEffect(() => { + const scrollElement = contentScrollRef.current?.getScrollElement() + + if (!scrollElement) return undefined + const scrollContainer = scrollElement + + function handleScroll() { + setIsContentScrolled(scrollContainer.scrollTop > 0) + } + + handleScroll() + scrollContainer.addEventListener('scroll', handleScroll, { + passive: true, + }) + + return () => { + scrollContainer.removeEventListener('scroll', handleScroll) + } + }, []) + + return ( +
+
+ +
+ +
+ + setIsSidebarDrawerOpen(true)} + /> +
+ +
+
+
+ + + + + + + Навигация + + + + setIsSidebarDrawerOpen(false)} + /> + + + + + + +
+ ) +} diff --git a/frontend/src/app/router/router.tsx b/frontend/src/app/router/router.tsx new file mode 100644 index 0000000..99f79ce --- /dev/null +++ b/frontend/src/app/router/router.tsx @@ -0,0 +1,68 @@ +// src/app/router/router.tsx +import { lazy, Suspense, type ReactNode } from 'react' +import { createBrowserRouter, Navigate } from 'react-router-dom' + +import { AppLayout } from '../layouts/AppLayout' + +const EmployeesPage = lazy(() => + import('../../pages/EmployeesPage/EmployeesPage').then((module) => ({ + default: module.EmployeesPage, + })), +) + +const MachinesPage = lazy(() => + import('../../pages/MachinesPage/MachinesPage').then((module) => ({ + default: module.MachinesPage, + })), +) + +const MachinePage = lazy(() => + import('../../pages/MachinePage/MachinePage').then((module) => ({ + default: module.MachinePage, + })), +) + +function lazyRoute(element: ReactNode) { + return {element} +} + +export const router = createBrowserRouter([ + { + path: '/', + element: , + children: [ + { + index: true, + element: , + }, + { + path: 'users', + element: lazyRoute(), + }, + { + path: 'machines', + element: lazyRoute(), + }, + { + path: 'machines/:machineId', + element: lazyRoute(), + }, + { + path: 'employees', + element: , + }, + { + path: 'devices/*', + element: , + }, + { + path: 'map', + element: , + }, + { + path: '*', + element: , + }, + ], + }, +]) diff --git a/frontend/src/assets/Logo.svg b/frontend/src/assets/Logo.svg new file mode 100644 index 0000000..529828a --- /dev/null +++ b/frontend/src/assets/Logo.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/LogoIcon.svg b/frontend/src/assets/LogoIcon.svg new file mode 100644 index 0000000..7ea007a --- /dev/null +++ b/frontend/src/assets/LogoIcon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/LogoIconWhite.svg b/frontend/src/assets/LogoIconWhite.svg new file mode 100644 index 0000000..a8611c7 --- /dev/null +++ b/frontend/src/assets/LogoIconWhite.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/LogoWhite.svg b/frontend/src/assets/LogoWhite.svg new file mode 100644 index 0000000..5e584aa --- /dev/null +++ b/frontend/src/assets/LogoWhite.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/empty_state.svg b/frontend/src/assets/empty_state.svg new file mode 100644 index 0000000..f753506 --- /dev/null +++ b/frontend/src/assets/empty_state.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/src/assets/fonts/Montserrat-VariableFont_wght.ttf b/frontend/src/assets/fonts/Montserrat-VariableFont_wght.ttf new file mode 100644 index 0000000..451e692 Binary files /dev/null and b/frontend/src/assets/fonts/Montserrat-VariableFont_wght.ttf differ diff --git a/frontend/src/assets/icons/Apps.tsx b/frontend/src/assets/icons/Apps.tsx new file mode 100644 index 0000000..fc1bdd8 --- /dev/null +++ b/frontend/src/assets/icons/Apps.tsx @@ -0,0 +1,21 @@ +import type { SVGProps } from 'react' + +type IconProps = SVGProps + +export function Apps({ className, ...props }: IconProps) { + return ( + + + + + + + ) +} diff --git a/frontend/src/assets/icons/Block.tsx b/frontend/src/assets/icons/Block.tsx new file mode 100644 index 0000000..adeda89 --- /dev/null +++ b/frontend/src/assets/icons/Block.tsx @@ -0,0 +1,11 @@ +import type { SVGProps } from 'react' + +type IconProps = SVGProps + +export function BlockIcon({ className, ...props }: IconProps) { + return ( + + + + ) +} \ No newline at end of file diff --git a/frontend/src/assets/icons/Bluetooth.tsx b/frontend/src/assets/icons/Bluetooth.tsx new file mode 100644 index 0000000..98f222a --- /dev/null +++ b/frontend/src/assets/icons/Bluetooth.tsx @@ -0,0 +1,11 @@ +import type { SVGProps } from 'react' + +type IconProps = SVGProps + +export function BluetoothIcon({ className, ...props }: IconProps) { + return ( + + + + ) +} \ No newline at end of file diff --git a/frontend/src/assets/icons/Camera.tsx b/frontend/src/assets/icons/Camera.tsx new file mode 100644 index 0000000..9d5b1f9 --- /dev/null +++ b/frontend/src/assets/icons/Camera.tsx @@ -0,0 +1,11 @@ +import type { SVGProps } from 'react' + +type IconProps = SVGProps + +export function CameraIcon({ className, ...props }: IconProps) { + return ( + + + + ) +} \ No newline at end of file diff --git a/frontend/src/assets/icons/Gps.tsx b/frontend/src/assets/icons/Gps.tsx new file mode 100644 index 0000000..0537a18 --- /dev/null +++ b/frontend/src/assets/icons/Gps.tsx @@ -0,0 +1,13 @@ +import type { SVGProps } from 'react' + +type IconProps = SVGProps + +export function GpsIcon({ className, ...props }: IconProps) { + return ( + + + + + + ) +} \ No newline at end of file diff --git a/frontend/src/assets/icons/Kiosk.tsx b/frontend/src/assets/icons/Kiosk.tsx new file mode 100644 index 0000000..b92361f --- /dev/null +++ b/frontend/src/assets/icons/Kiosk.tsx @@ -0,0 +1,11 @@ +import type { SVGProps } from 'react' + +type IconProps = SVGProps + +export function KioskIcon({ className, ...props }: IconProps) { + return ( + + + + ) +} \ No newline at end of file diff --git a/frontend/src/assets/icons/LoadingTwotoneLoop.tsx b/frontend/src/assets/icons/LoadingTwotoneLoop.tsx new file mode 100644 index 0000000..c6eb68f --- /dev/null +++ b/frontend/src/assets/icons/LoadingTwotoneLoop.tsx @@ -0,0 +1,32 @@ +import type { SVGProps } from 'react' + +type IconProps = SVGProps + +export function LoadingTwotoneLoopIcon({ className, ...props }: IconProps) { + return ( + + + + + + + ) +} diff --git a/frontend/src/assets/icons/Message.tsx b/frontend/src/assets/icons/Message.tsx new file mode 100644 index 0000000..e774849 --- /dev/null +++ b/frontend/src/assets/icons/Message.tsx @@ -0,0 +1,11 @@ +import type { SVGProps } from 'react' + +type IconProps = SVGProps + +export function MessageIcon({ className, ...props }: IconProps) { + return ( + + + + ) +} \ No newline at end of file diff --git a/frontend/src/assets/icons/Reboot.tsx b/frontend/src/assets/icons/Reboot.tsx new file mode 100644 index 0000000..1fc624a --- /dev/null +++ b/frontend/src/assets/icons/Reboot.tsx @@ -0,0 +1,11 @@ +import type { SVGProps } from 'react' + +type IconProps = SVGProps + +export function RebootIcon({ className, ...props }: IconProps) { + return ( + + + + ) +} \ No newline at end of file diff --git a/frontend/src/assets/icons/Sim.tsx b/frontend/src/assets/icons/Sim.tsx new file mode 100644 index 0000000..5c36140 --- /dev/null +++ b/frontend/src/assets/icons/Sim.tsx @@ -0,0 +1,12 @@ +import type { SVGProps } from 'react' + +type IconProps = SVGProps + +export function SimIcon({ className, ...props }: IconProps) { + return ( + + + + + ) +} \ No newline at end of file diff --git a/frontend/src/assets/icons/Volume.tsx b/frontend/src/assets/icons/Volume.tsx new file mode 100644 index 0000000..86c0e0d --- /dev/null +++ b/frontend/src/assets/icons/Volume.tsx @@ -0,0 +1,18 @@ +import type { SVGProps } from 'react' + +type IconProps = SVGProps + +export function VolumeIcon({ className, ...props }: IconProps) { + return ( + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/frontend/src/assets/icons/Wifi.tsx b/frontend/src/assets/icons/Wifi.tsx new file mode 100644 index 0000000..ac4a75f --- /dev/null +++ b/frontend/src/assets/icons/Wifi.tsx @@ -0,0 +1,11 @@ +import type { SVGProps } from 'react' + +type IconProps = SVGProps + +export function WifiIcon({ className, ...props }: IconProps) { + return ( + + + + ) +} \ No newline at end of file diff --git a/frontend/src/assets/no_items.svg b/frontend/src/assets/no_items.svg new file mode 100644 index 0000000..ab5b250 --- /dev/null +++ b/frontend/src/assets/no_items.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/src/entities/beacon/api/beaconsService.ts b/frontend/src/entities/beacon/api/beaconsService.ts new file mode 100644 index 0000000..014d4a6 --- /dev/null +++ b/frontend/src/entities/beacon/api/beaconsService.ts @@ -0,0 +1,58 @@ +import { mockBeacons } from '../mock/mockBeacons' +import type { Beacon } from '../model/types' + +const BEACONS_STORAGE_KEY = 'fleet-ble-monitor.beacons' + +function cloneBeacon(beacon: Beacon): Beacon { + return { ...beacon } +} + +export function readBeacons(): Beacon[] { + if (typeof window === 'undefined') { + return mockBeacons.map(cloneBeacon) + } + + const storedBeacons = window.localStorage.getItem(BEACONS_STORAGE_KEY) + + if (!storedBeacons) { + const initialBeacons = mockBeacons.map(cloneBeacon) + window.localStorage.setItem(BEACONS_STORAGE_KEY, JSON.stringify(initialBeacons)) + + return initialBeacons + } + + try { + return (JSON.parse(storedBeacons) as Beacon[]).map(cloneBeacon) + } catch { + const initialBeacons = mockBeacons.map(cloneBeacon) + window.localStorage.setItem(BEACONS_STORAGE_KEY, JSON.stringify(initialBeacons)) + + return initialBeacons + } +} + +export function writeBeacons(beacons: Beacon[]) { + if (typeof window === 'undefined') return + + window.localStorage.setItem(BEACONS_STORAGE_KEY, JSON.stringify(beacons)) + window.dispatchEvent(new Event('beacons:change')) +} + +export const beaconsService = { + async getBeacons(): Promise { + return readBeacons().map(cloneBeacon) + }, + + async getFreeBeacons(): Promise { + return readBeacons() + .filter((beacon) => beacon.status === 'FREE') + .map(cloneBeacon) + }, + + async getBeaconById(id: number | string): Promise { + const numericId = Number(id) + const beacon = readBeacons().find((item) => item.id === numericId) + + return beacon ? cloneBeacon(beacon) : null + }, +} diff --git a/frontend/src/entities/beacon/mock/mockBeaconVibrationSamples.ts b/frontend/src/entities/beacon/mock/mockBeaconVibrationSamples.ts new file mode 100644 index 0000000..ac0451f --- /dev/null +++ b/frontend/src/entities/beacon/mock/mockBeaconVibrationSamples.ts @@ -0,0 +1,141 @@ +import type { BeaconVibrationSample } from '../model/types' + +type VibrationMode = 'idle' | 'engine' | 'moving' | 'load' + +type VibrationSegment = { + mode: VibrationMode + startMinute: number + endMinute: number +} + +type MachineVibrationProfile = { + mac: string + idle: [number, number] + engine: [number, number] + moving: [number, number] + load: [number, number] + segments: VibrationSegment[] +} + +const baseDayUtc = Date.UTC(2026, 6, 9) +const dayMs = 24 * 60 * 60 * 1000 +const minuteMs = 60 * 1000 +const sampleStepMinutes = 15 +const sampleDurationSeconds = sampleStepMinutes * 60 + +const profiles: MachineVibrationProfile[] = [ + { + mac: 'E4:5F:01:AA:10:01', + idle: [0.015, 0.055], + engine: [0.11, 0.24], + moving: [0.38, 0.95], + load: [0.7, 1.15], + segments: [ + { mode: 'idle', startMinute: 45, endMinute: 95 }, + { mode: 'engine', startMinute: 95, endMinute: 125 }, + { mode: 'moving', startMinute: 125, endMinute: 280 }, + { mode: 'idle', startMinute: 280, endMinute: 335 }, + { mode: 'engine', startMinute: 335, endMinute: 365 }, + { mode: 'load', startMinute: 365, endMinute: 540 }, + { mode: 'moving', startMinute: 540, endMinute: 650 }, + { mode: 'engine', startMinute: 650, endMinute: 690 }, + { mode: 'idle', startMinute: 690, endMinute: 750 }, + ], + }, + { + mac: 'E4:5F:01:AA:10:09', + idle: [0.02, 0.075], + engine: [0.18, 0.38], + moving: [0.55, 1.18], + load: [0.95, 1.68], + segments: [ + { mode: 'idle', startMinute: 35, endMinute: 80 }, + { mode: 'engine', startMinute: 80, endMinute: 120 }, + { mode: 'load', startMinute: 120, endMinute: 245 }, + { mode: 'idle', startMinute: 245, endMinute: 320 }, + { mode: 'engine', startMinute: 320, endMinute: 360 }, + { mode: 'load', startMinute: 360, endMinute: 535 }, + { mode: 'moving', startMinute: 535, endMinute: 630 }, + { mode: 'engine', startMinute: 630, endMinute: 680 }, + { mode: 'idle', startMinute: 680, endMinute: 735 }, + ], + }, + { + mac: 'E4:5F:01:AA:10:10', + idle: [0.006, 0.035], + engine: [0.052, 0.155], + moving: [0.19, 0.58], + load: [0.34, 0.78], + segments: [ + { mode: 'idle', startMinute: 70, endMinute: 110 }, + { mode: 'engine', startMinute: 110, endMinute: 135 }, + { mode: 'moving', startMinute: 135, endMinute: 225 }, + { mode: 'idle', startMinute: 225, endMinute: 285 }, + { mode: 'engine', startMinute: 285, endMinute: 315 }, + { mode: 'moving', startMinute: 315, endMinute: 430 }, + { mode: 'idle', startMinute: 430, endMinute: 520 }, + { mode: 'engine', startMinute: 520, endMinute: 548 }, + { mode: 'load', startMinute: 548, endMinute: 625 }, + { mode: 'moving', startMinute: 625, endMinute: 700 }, + ], + }, +] + +function getValueInRange( + range: [number, number], + machineIndex: number, + dayOffset: number, + sampleIndex: number, +) { + const [min, max] = range + const seed = ( + machineIndex * 31 + + Math.abs(dayOffset) * 17 + + sampleIndex * 11 + ) % 100 + const ratio = seed / 100 + const wave = Math.sin((sampleIndex + machineIndex * 3) / 3) * 0.08 + const value = min + (max - min) * Math.min(1, Math.max(0, ratio + wave)) + + return Number(value.toFixed(3)) +} + +function buildSamples() { + let nextId = 1 + const samples: BeaconVibrationSample[] = [] + + profiles.forEach((profile, machineIndex) => { + for (let dayOffset = -4; dayOffset <= 0; dayOffset += 1) { + const shift = ((machineIndex + Math.abs(dayOffset)) % 3) * 5 + + profile.segments.forEach((segment) => { + for ( + let minute = segment.startMinute + shift; + minute < segment.endMinute + shift; + minute += sampleStepMinutes + ) { + const timeMillis = baseDayUtc + dayOffset * dayMs + minute * minuteMs + + samples.push({ + id: nextId, + mac: profile.mac, + vibration: getValueInRange( + profile[segment.mode], + machineIndex, + dayOffset, + nextId, + ), + duration: sampleDurationSeconds, + timeMillis, + }) + + nextId += 1 + } + }) + } + }) + + return samples +} + +export const mockBeaconVibrationSamples: BeaconVibrationSample[] = buildSamples() diff --git a/frontend/src/entities/beacon/mock/mockBeacons.ts b/frontend/src/entities/beacon/mock/mockBeacons.ts new file mode 100644 index 0000000..1fe25f8 --- /dev/null +++ b/frontend/src/entities/beacon/mock/mockBeacons.ts @@ -0,0 +1,108 @@ +import type { Beacon } from '../model/types' + +export const mockBeacons: Beacon[] = [ + { + id: 1, + identifier: 'BLE-TRK-0001', + macAddress: 'E4:5F:01:AA:10:01', + status: 'BOUND', + boundMachineId: 1, + lastSeenAt: '2026-07-09T06:14:00.000Z', + }, + { + id: 2, + identifier: 'BLE-TRK-0002', + macAddress: 'E4:5F:01:AA:10:02', + status: 'BOUND', + boundMachineId: 2, + lastSeenAt: '2026-07-09T06:06:00.000Z', + }, + { + id: 3, + identifier: 'BLE-TRK-0003', + macAddress: 'E4:5F:01:AA:10:03', + status: 'BOUND', + boundMachineId: 3, + lastSeenAt: '2026-07-09T05:47:00.000Z', + }, + { + id: 4, + identifier: 'BLE-TRK-0004', + macAddress: 'E4:5F:01:AA:10:04', + status: 'BOUND', + boundMachineId: 5, + lastSeenAt: '2026-07-09T06:18:00.000Z', + }, + { + id: 5, + identifier: 'BLE-TRK-0005', + macAddress: 'E4:5F:01:AA:10:05', + status: 'BOUND', + boundMachineId: 6, + lastSeenAt: '2026-07-09T04:50:00.000Z', + }, + { + id: 6, + identifier: 'BLE-TRK-0006', + macAddress: 'E4:5F:01:AA:10:06', + status: 'BOUND', + boundMachineId: 7, + lastSeenAt: '2026-07-09T06:01:00.000Z', + }, + { + id: 7, + identifier: 'BLE-TRK-0007', + macAddress: 'E4:5F:01:AA:10:07', + status: 'BOUND', + boundMachineId: 9, + lastSeenAt: '2026-07-07T12:25:00.000Z', + }, + { + id: 8, + identifier: 'BLE-TRK-0008', + macAddress: 'E4:5F:01:AA:10:08', + status: 'BOUND', + boundMachineId: 10, + lastSeenAt: '2026-07-09T06:11:00.000Z', + }, + { + id: 9, + identifier: 'BLE-TRK-0009', + macAddress: 'E4:5F:01:AA:10:09', + status: 'BOUND', + boundMachineId: 12, + lastSeenAt: '2026-07-09T05:58:00.000Z', + }, + { + id: 10, + identifier: 'BLE-TRK-0010', + macAddress: 'E4:5F:01:AA:10:10', + status: 'BOUND', + boundMachineId: 11, + lastSeenAt: '2026-07-09T11:40:00.000Z', + }, + { + id: 11, + identifier: 'BLE-TRK-0011', + macAddress: 'E4:5F:01:AA:10:11', + status: 'FREE', + boundMachineId: null, + lastSeenAt: '2026-07-08T17:12:00.000Z', + }, + { + id: 12, + identifier: 'BLE-TRK-0012', + macAddress: 'E4:5F:01:AA:10:12', + status: 'FREE', + boundMachineId: null, + lastSeenAt: null, + }, + { + id: 13, + identifier: 'BLE-TRK-0013', + macAddress: 'E4:5F:01:AA:10:13', + status: 'FREE', + boundMachineId: null, + lastSeenAt: '2026-07-09T03:44:00.000Z', + }, +] diff --git a/frontend/src/entities/beacon/model/types.ts b/frontend/src/entities/beacon/model/types.ts new file mode 100644 index 0000000..bf220e7 --- /dev/null +++ b/frontend/src/entities/beacon/model/types.ts @@ -0,0 +1,18 @@ +export type BeaconStatus = 'FREE' | 'BOUND' + +export type Beacon = { + id: number + identifier: string + macAddress: string + status: BeaconStatus + boundMachineId: number | null + lastSeenAt: string | null +} + +export type BeaconVibrationSample = { + id: number + mac: string + vibration: number + duration: number + timeMillis: number +} diff --git a/frontend/src/entities/device/api/device.graphql.ts b/frontend/src/entities/device/api/device.graphql.ts new file mode 100644 index 0000000..31d641e --- /dev/null +++ b/frontend/src/entities/device/api/device.graphql.ts @@ -0,0 +1,390 @@ +import { gql } from '@apollo/client' + +export const GET_PHONES_QUERY = gql` + query GetPhones( + $page: Int! + $query: String + $locked: Boolean + $needMaintenance: Boolean + $networkStatus: [PhoneNetworkStatus!] + $orgs: [ID!] + $sortDirection: SortDirection! + $sortField: PhoneSortField! + ) { + getPhones( + page: $page + query: $query + locked: $locked + needMaintenance: $needMaintenance + networkStatus: $networkStatus + orgs: $orgs + sortDirection: $sortDirection + sortField: $sortField + ) { + page { + id + imei + imei2 + serial + networkStatus + + lastLocation { + alt + date + lat + lng + } + + org { + id + logoUrl + name + + policy { + bluetooth + bluetoothEditable + camera + cameraEditable + GPS + gpsEditable + sim + simEditable + offlineTimeThreshold + } + } + + orgId + + policy { + bluetooth + camera + GPS + sim + locked + } + + techState { + batteryLevel + needMaintenance + temperature + } + } + + totalElements + totalPages + } + } +` + +export const GET_PHONES_TABS_STATS_QUERY = gql` + query GetPhonesTabsStats($query: String, $orgs: [ID!]) { + totalPhones: getPhones( + page: 0 + query: $query + orgs: $orgs + sortDirection: DESC + sortField: ID + ) { + totalElements + } + + onlinePhones: getPhones( + page: 0 + query: $query + networkStatus: [Online] + orgs: $orgs + sortDirection: DESC + sortField: ID + ) { + totalElements + } + + lockedPhones: getPhones( + page: 0 + query: $query + locked: true + orgs: $orgs + sortDirection: DESC + sortField: ID + ) { + totalElements + } + + lostPhones: getPhones( + page: 0 + query: $query + networkStatus: [Lost] + orgs: $orgs + sortDirection: DESC + sortField: ID + ) { + totalElements + } + + maintenancePhones: getPhones( + page: 0 + query: $query + needMaintenance: true + orgs: $orgs + sortDirection: DESC + sortField: ID + ) { + totalElements + } + } +` + +export const GET_PHONE_QUERY = gql` + query GetPhone($id: Int!) { + getPhone(id: $id) { + id + imei + imei2 + networkStatus + + lastLocation { + alt + date + lat + lng + } + + org { + creationDate + id + name + policy { + bluetooth + bluetoothEditable + camera + cameraEditable + GPS + gpsEditable + sim + simEditable + offlineTimeThreshold + } + } + + orgId + + policy { + bluetooth + camera + GPS + sim + locked + } + + registerDate + serial + + techState { + batteryCycles + batteryLevel + batteryRemainingCapacity + hits + needMaintenance + overheats + worktime + } + } + } +` + +export const GET_PHONE_GPS_TRACK_QUERY = gql` + query GetPhoneGpsTrack($phoneId: ID!, $startDate: Float!, $endDate: Float) { + getPhoneGpsTrack(phoneId: $phoneId, startDate: $startDate, endDate: $endDate) { + alt + date + lat + lng + } + } +` + +export const GET_PHONE_PACKAGES_QUERY = gql` + query GetPhonePackages($phoneId: ID!) { + getPhonePackages(phoneId: $phoneId) { + category + iconUrl + installDate + name + packageName + version + versionName + } + } +` + +export const GET_TELEMETRY_QUERY = gql` + query GetTelemetry( + $phoneId: Int! + $packagesPhoneId: ID! + $startDate: Float! + $endDate: Float + ) { + getTelemetry(phoneId: $phoneId, startDate: $startDate, endDate: $endDate) { + batteryCapacity + batteryCycles + batteryLevel + date + temperature + } + + getPhoneStateEvents(phoneId: $packagesPhoneId, startDate: $startDate, endDate: $endDate) { + date + phoneId + type + } + + getPhonePackagesUseEvents( + phoneId: $packagesPhoneId + startDate: $startDate + endDate: $endDate + ) { + events { + type + packageName + date + } + + phonePackage { + category + iconUrl + installDate + name + packageName + version + versionName + } + } + } +` + +export const GET_DEVICE_PAGE_QUERY = gql` + query GetDevicePage( + $id: Int! + $phoneId: ID! + $telemetryStartDate: Float! + $telemetryEndDate: Float + $gpsStartDate: Float! + $gpsEndDate: Float + ) { + getPhone(id: $id) { + id + imei + imei2 + networkStatus + + lastLocation { + alt + date + lat + lng + } + + org { + creationDate + id + name + policy { + bluetooth + bluetoothEditable + camera + cameraEditable + GPS + gpsEditable + sim + simEditable + offlineTimeThreshold + } + } + + orgId + + policy { + bluetooth + camera + GPS + sim + locked + } + + registerDate + serial + + techState { + batteryCycles + batteryLevel + batteryRemainingCapacity + hits + needMaintenance + overheats + temperature + worktime + } +} + + getTelemetry( + phoneId: $id + startDate: $telemetryStartDate + endDate: $telemetryEndDate + ) { + batteryCapacity + batteryCycles + batteryLevel + date + temperature + } + + getPhoneGpsTrack( + phoneId: $phoneId + startDate: $gpsStartDate + endDate: $gpsEndDate + ) { + alt + date + lat + lng + } + } +` + +export const CREATE_PHONE_REGISTRATION_TOKEN_MUTATION = gql` + mutation CreatePhoneRegistrationToken { + createPhoneRegistrationToken { + address + expiresIn + token + } + } +` + +export const CHANGE_PHONE_POLICY_MUTATION = gql` + mutation ChangePhonePolicy( + $id: ID! + $bluetooth: ServiceUseOption! + $camera: ComponentUseOption! + $GPS: ServiceUseOption! + $sim: ServiceUseOption! + $locked: Boolean! + ) { + changePhonePolicy( + id: $id + policy: { + bluetooth: $bluetooth + camera: $camera + GPS: $GPS + sim: $sim + locked: $locked + } + ) { + bluetooth + camera + GPS + sim + locked + } + } +` diff --git a/frontend/src/entities/device/lib/phonePolicy.ts b/frontend/src/entities/device/lib/phonePolicy.ts new file mode 100644 index 0000000..565e07c --- /dev/null +++ b/frontend/src/entities/device/lib/phonePolicy.ts @@ -0,0 +1,85 @@ +import type { + ComponentUseOption, + LegacyPhoneUsePolicyOption, + PhoneUsePolicyOption, + ServiceUseOption, +} from '../model/types' + +type PhonePolicyValue = + | PhoneUsePolicyOption + | LegacyPhoneUsePolicyOption + | boolean + | null + | undefined + +export function isPhonePolicyOptionEnabled(value: PhonePolicyValue) { + if (typeof value === 'boolean') return value + + return value === 'Enabled' || value === 'Allowed' +} + +export function booleanToPhonePolicyOption( + enabled: boolean, +): ServiceUseOption { + return enabled ? 'Enabled' : 'Disabled' +} + +export function booleanToServiceUseOption(enabled: boolean): ServiceUseOption { + return enabled ? 'Enabled' : 'Disabled' +} + +export function booleanToComponentUseOption( + enabled: boolean, +): ComponentUseOption { + return enabled ? 'Allowed' : 'Disallowed' +} + +export function normalizeServiceUseOption( + value?: ServiceUseOption | LegacyPhoneUsePolicyOption | boolean | null, + fallbackBoolean?: boolean | null, +): ServiceUseOption { + if (value === 'Disabled' || value === 'Enabled' || value === 'Unspecified') { + return value + } + + if (value === 'Allowed') return 'Enabled' + if (value === 'Disallowed') return 'Disabled' + if (value === 'Changeable') return 'Unspecified' + + if (typeof value === 'boolean') { + return booleanToServiceUseOption(value) + } + + if (typeof fallbackBoolean === 'boolean') { + return booleanToServiceUseOption(fallbackBoolean) + } + + return 'Unspecified' +} + +export function normalizeComponentUseOption( + value?: + | ComponentUseOption + | ServiceUseOption + | LegacyPhoneUsePolicyOption + | boolean + | null, + fallbackBoolean?: boolean | null, +): ComponentUseOption { + if (value === 'Allowed' || value === 'Disallowed') { + return value + } + + if (value === 'Enabled') return 'Allowed' + if (value === 'Disabled') return 'Disallowed' + + if (typeof value === 'boolean') { + return booleanToComponentUseOption(value) + } + + if (typeof fallbackBoolean === 'boolean') { + return booleanToComponentUseOption(fallbackBoolean) + } + + return 'Disallowed' +} diff --git a/frontend/src/entities/device/model/types.ts b/frontend/src/entities/device/model/types.ts new file mode 100644 index 0000000..ea39e04 --- /dev/null +++ b/frontend/src/entities/device/model/types.ts @@ -0,0 +1,268 @@ +export type DeviceLocation = { + alt: number + date: number + lat: number + lng: number +} + +export type ServiceUseOption = + | 'Disabled' + | 'Enabled' + | 'Unspecified' + +export type ComponentUseOption = + | 'Allowed' + | 'Disallowed' + +export type PhoneUsePolicyOption = ServiceUseOption | ComponentUseOption + +export type LegacyPhoneUsePolicyOption = + | 'Allowed' + | 'Disallowed' + | 'Changeable' + +export type GroupUsePolicy = { + bluetooth?: ServiceUseOption | LegacyPhoneUsePolicyOption + bluetoothEditable?: boolean + camera?: ComponentUseOption | LegacyPhoneUsePolicyOption + cameraEditable?: boolean + GPS?: ServiceUseOption | LegacyPhoneUsePolicyOption + gpsEditable?: boolean + sim?: ServiceUseOption | LegacyPhoneUsePolicyOption + simEditable?: boolean + offlineTimeThreshold?: number + date?: number +} + +export type DevicePolicy = { + bluetooth?: ServiceUseOption | LegacyPhoneUsePolicyOption + camera?: ComponentUseOption | LegacyPhoneUsePolicyOption + GPS?: ServiceUseOption | LegacyPhoneUsePolicyOption + sim?: ServiceUseOption | LegacyPhoneUsePolicyOption + canUseBluetooth?: boolean + canUseCamera?: boolean + canUseGPS?: boolean + canUseSim?: boolean + locked?: boolean +} + +export type DeviceTechState = { + batteryCycles: number | null + batteryLevel: number | null + batteryRemainingCapacity: number | null + hits: number | null + needMaintenance: boolean + overheats: number | null + temperature?: number | null + worktime: number | null +} + +export type DeviceNetworkStatus = 'Online' | 'Offline' | 'Lost' + +export type DeviceOrganisation = { + creationDate?: number + id: number + logoUrl?: string | null + name: string + policy?: GroupUsePolicy | null +} + +export type Device = { + id: number + imei: string + imei2: string + serial: string + networkStatus?: DeviceNetworkStatus + orgId: number + registerDate: number + org: DeviceOrganisation | null + policy: DevicePolicy | null + techState: DeviceTechState | null + lastLocation: DeviceLocation | null +} + +export type GetPhonesData = { + getPhones: { + page: Device[] + totalElements: number + totalPages: number + } +} + +export type GetPhonesVariables = { + page: number + query?: string + locked?: boolean + needMaintenance?: boolean + networkStatus?: DeviceNetworkStatus[] + orgs?: string[] + sortDirection: 'ASC' | 'DESC' + sortField: 'ID' | 'Serial' | 'Date' +} + +type GetPhonesStatsPage = { + totalElements: number +} + +export type GetPhonesTabsStatsData = { + totalPhones: GetPhonesStatsPage + onlinePhones: GetPhonesStatsPage + lockedPhones: GetPhonesStatsPage + lostPhones: GetPhonesStatsPage + maintenancePhones: GetPhonesStatsPage +} + +export type GetPhonesTabsStatsVariables = { + query?: string + orgs?: string[] +} + +export type GetPhoneData = { + getPhone: Device | null +} + +export type GetPhoneVariables = { + id: number +} + +export type DeviceTelemetryItem = { + batteryCapacity: number | null + batteryCycles: number | null + batteryLevel: number | null + date: number + temperature: number | null +} + +export type DeviceTelemetryEventItem = { + date: number; + type: 'event'; + eventType: DevicePhoneStateEventType; + label: string; +}; + +export type DeviceHistoryChartItem = + | DeviceTelemetryItem + | DeviceTelemetryEventItem; + +export type DevicePhoneStateEventType = + | 'ScreenOn' + | 'ScreenOff' + | 'EnterNetwork' + | 'LeaveNetwork' + | 'PowerOn' + | 'PowerOff' + | 'Hit' + | 'Overheat' + | 'PowerAC' + | 'PowerUSB' + | 'PowerWireless' + | 'PowerDisconnected' + +export type DevicePhoneStateEvent = { + date: number + phoneId: string + type: DevicePhoneStateEventType +} + +export type DevicePackage = { + category: string | null + iconUrl: string | null + installDate: number | null + name: string | null + packageName: string + version: number | null + versionName: string | null +} + +export type GetPhonePackagesData = { + getPhonePackages: DevicePackage[] +} + +export type GetPhonePackagesVariables = { + phoneId: string +} + +export type DevicePackageUseEventType = + | 'ResumeActivity' + | 'PauseActivity' + | 'StopActivity' + | string + +export type DevicePackageUseEvent = { + date: number + packageName: string + type: DevicePackageUseEventType +} + +export type DevicePackageUseEventsGroup = { + events: DevicePackageUseEvent[] + phonePackage: DevicePackage | null +} + +export type GetTelemetryData = { + getTelemetry: DeviceTelemetryItem[] + getPhoneStateEvents: DevicePhoneStateEvent[] + getPhonePackagesUseEvents: DevicePackageUseEventsGroup[] +} + +export type GetTelemetryVariables = { + phoneId: number + packagesPhoneId: string + startDate: number + endDate?: number +} + +export type DeviceGpsTrackPoint = { + alt: number + date: number + lat: number + lng: number +} + +export type GetPhoneGpsTrackData = { + getPhoneGpsTrack: DeviceGpsTrackPoint[] +} + +export type GetPhoneGpsTrackVariables = { + phoneId: string + startDate: number + endDate?: number +} + +export type GetDevicePageData = { + getPhone: Device | null + getTelemetry: DeviceTelemetryItem[] + getPhoneGpsTrack: DeviceGpsTrackPoint[] +} + +export type GetDevicePageVariables = { + id: number + phoneId: string + telemetryStartDate: number + telemetryEndDate?: number + gpsStartDate: number + gpsEndDate?: number +} + +export type ChangePhonePolicyData = { + changePhonePolicy: DevicePolicy +} + +export type ChangePhonePolicyVariables = { + id: string + bluetooth: ServiceUseOption + camera: ComponentUseOption + GPS: ServiceUseOption + sim: ServiceUseOption + locked: boolean +} + +export type CreatePhoneRegistrationTokenData = { + createPhoneRegistrationToken: { + address: string + expiresIn: number + token: string + } +} + +export type CreatePhoneRegistrationTokenVariables = Record diff --git a/frontend/src/entities/employee/api/employee.graphql.ts b/frontend/src/entities/employee/api/employee.graphql.ts new file mode 100644 index 0000000..890ba25 --- /dev/null +++ b/frontend/src/entities/employee/api/employee.graphql.ts @@ -0,0 +1,325 @@ +import { gql } from '@apollo/client' + +export const GET_USERS_QUERY = gql` + query GetUsers( + $page: Int! + $query: String + $orgs: [ID!] + $roles: [Role!] + $sortDirection: SortDirection! + $sortField: UserSortField! + ) { + getUsers( + page: $page + query: $query + orgs: $orgs + roles: $roles + sortDirection: $sortDirection + sortField: $sortField + ) { + totalPages + totalElements + page { + avatarUrl + firstName + id + lastName + middleName + org { + creationDate + id + logoUrl + name + } + orgId + role + username + } + } + } +` + +export const GET_ORGANISATION_QUERY = gql` + query GetOrganisation($id: ID!) { + getOrganisation(id: $id) { + creationDate + id + name + logoUrl + policy { + bluetooth + bluetoothEditable + camera + cameraEditable + GPS + gpsEditable + sim + simEditable + offlineTimeThreshold + } + } + } +` + +export const DELETE_ORGANISATION_MUTATION = gql` + mutation DeleteOrganisation($id: ID!) { + deleteOrganisation(id: $id) + } +` + +export const GET_ORGANISATIONS_QUERY = gql` + query GetOrganisations( + $page: Int! + $query: String + $sortDirection: SortDirection! + $sortField: OrganisationSortField! + ) { + getOrganisations( + page: $page + query: $query + sortDirection: $sortDirection + sortField: $sortField + ) { + totalPages + totalElements + page { + creationDate + id + name + logoUrl + } + } + } +` + +export const CREATE_ORGANISATION_MUTATION = gql` + mutation CreateOrganisation( + $name: String! + $bluetooth: ServiceUseOption! + $bluetoothEditable: Boolean! + $camera: ComponentUseOption! + $cameraEditable: Boolean! + $GPS: ServiceUseOption! + $gpsEditable: Boolean! + $sim: ServiceUseOption! + $simEditable: Boolean! + $offlineTimeThreshold: Float! + ) { + createOrganisation( + name: $name + policy: { + bluetooth: $bluetooth + bluetoothEditable: $bluetoothEditable + camera: $camera + cameraEditable: $cameraEditable + GPS: $GPS + gpsEditable: $gpsEditable + sim: $sim + simEditable: $simEditable + offlineTimeThreshold: $offlineTimeThreshold + } + ) { + id + name + } + } +` + +export const CHANGE_ORGANISATION_MUTATION = gql` + mutation ChangeOrganisation( + $id: ID! + $name: String! + $bluetooth: ServiceUseOption! + $bluetoothEditable: Boolean! + $camera: ComponentUseOption! + $cameraEditable: Boolean! + $GPS: ServiceUseOption! + $gpsEditable: Boolean! + $sim: ServiceUseOption! + $simEditable: Boolean! + $offlineTimeThreshold: Float! + ) { + changeOrganisation( + id: $id + name: $name + policy: { + bluetooth: $bluetooth + bluetoothEditable: $bluetoothEditable + camera: $camera + cameraEditable: $cameraEditable + GPS: $GPS + gpsEditable: $gpsEditable + sim: $sim + simEditable: $simEditable + offlineTimeThreshold: $offlineTimeThreshold + } + ) { + creationDate + id + name + } + } +` + +export const CHANGE_ORGANISATION_POLICY_MUTATION = gql` + mutation ChangeOrganisationPolicy( + $id: ID! + $name: String! + $bluetooth: ServiceUseOption! + $bluetoothEditable: Boolean! + $camera: ComponentUseOption! + $cameraEditable: Boolean! + $GPS: ServiceUseOption! + $gpsEditable: Boolean! + $sim: ServiceUseOption! + $simEditable: Boolean! + $offlineTimeThreshold: Float! + ) { + changeOrganisation( + id: $id + name: $name + policy: { + bluetooth: $bluetooth + bluetoothEditable: $bluetoothEditable + camera: $camera + cameraEditable: $cameraEditable + GPS: $GPS + gpsEditable: $gpsEditable + sim: $sim + simEditable: $simEditable + offlineTimeThreshold: $offlineTimeThreshold + } + ) { + creationDate + id + name + } + } +` + +export const CREATE_USER_MUTATION = gql` + mutation createUser( + $orgId: ID! + $firstName: String! + $lastName: String! + $middleName: String! + $username: String! + $password: String! + $role: Role! + ) { + createUser( + payload: { + orgId: $orgId + firstName: $firstName + lastName: $lastName + middleName: $middleName + username: $username + password: $password + role: $role + } + ) { + id + firstName + lastName + middleName + orgId + role + org { + id + name + } + } + } +` + +export const CHANGE_USER_MUTATION = gql` + mutation ChangeUser( + $userId: ID! + $orgId: ID! + $firstName: String! + $lastName: String! + $middleName: String! + $username: String! + $role: Role! + ) { + changeUser( + userId: $userId + form: { + orgId: $orgId + firstName: $firstName + lastName: $lastName + middleName: $middleName + username: $username + role: $role + } + ) { + avatarUrl + firstName + id + lastName + middleName + org { + creationDate + id + logoUrl + name + } + orgId + role + username + } + } +` + +export const CHANGE_USER_WITH_PASSWORD_MUTATION = gql` + mutation ChangeUserWithPassword( + $userId: ID! + $orgId: ID! + $firstName: String! + $lastName: String! + $middleName: String! + $username: String! + $password: String! + $role: Role! + ) { + changeUser( + userId: $userId + form: { + orgId: $orgId + firstName: $firstName + lastName: $lastName + middleName: $middleName + username: $username + password: $password + role: $role + } + ) { + avatarUrl + firstName + id + lastName + middleName + org { + creationDate + id + logoUrl + name + } + orgId + role + username + } + } +` + +export const CREATE_UPLOAD_USER_AVATAR_URL_MUTATION = gql` + mutation CreateUploadUserAvatarUrl($userId: ID!) { + createUploadUserAvatarUrl(userId: $userId) + } +` + +export const CREATE_UPLOAD_ORGANISATION_LOGO_URL_MUTATION = gql` + mutation CreateUploadOrganisationLogoUrl($orgId: ID!) { + createUploadOrganisationLogoUrl(orgId: $orgId) + } +` diff --git a/frontend/src/entities/employee/api/usersService.ts b/frontend/src/entities/employee/api/usersService.ts new file mode 100644 index 0000000..3aff740 --- /dev/null +++ b/frontend/src/entities/employee/api/usersService.ts @@ -0,0 +1,322 @@ +import { apolloClient } from '../../../shared/api/apolloClient' +import { + clearAuthStorage, + CURRENT_USER_STORAGE_KEY, +} from '../../../shared/api/authStorage' +import type { + CreateUserPayload as ApiCreateUserPayload, + SortDirection as ApiSortDirection, + UpdateUserPayload as ApiUpdateUserPayload, + UserFieldsFragment, + UserRole as ApiUserRole, + UserSortField as ApiUserSortField, +} from '../../../shared/api/graphql/generated/graphql' +import { + CREATE_USER_MUTATION, + DELETE_USER_MUTATION, + GET_USER_QUERY, + GET_USERS_QUERY, + UPDATE_USER_MUTATION, +} from '../../../shared/api/graphql/operations/users' +import type { + Employee, + EmployeeRole, + UserSortDirection, + UserSortField, +} from '../model/types' + +const LEGACY_USERS_STORAGE_KEY = 'fleet-ble-monitor.users' + +export type UsersQueryParams = { + page?: number + pageSize?: number + query?: string + roles?: EmployeeRole[] + sortDirection?: UserSortDirection + sortField?: UserSortField +} + +export type UsersPage = { + totalPages: number + totalElements: number + page: Employee[] +} + +export type CreateUserPayload = { + lastName: string + firstName: string + middleName: string + email: string + login: string + password: string + role: EmployeeRole + avatarUrl?: string | null +} + +export type UpdateUserPayload = Partial + +function emitUsersChange() { + if (typeof window === 'undefined') return + + window.dispatchEvent(new Event('users:change')) +} + +function cleanupLegacyUsersStorage() { + if (typeof window === 'undefined') return + + window.localStorage.removeItem(LEGACY_USERS_STORAGE_KEY) +} + +function mapUser(user: UserFieldsFragment): Employee { + return { + id: user.id, + lastName: user.lastName, + firstName: user.firstName, + middleName: user.middleName ?? '', + email: user.email, + login: user.login, + username: user.login, + role: user.role, + createdAt: user.createdAt, + avatarUrl: null, + orgId: 0, + org: null, + } +} + +function normalizeRole(role: EmployeeRole): ApiUserRole | null { + if (role === 'ADMIN' || role === 'Admin') return 'ADMIN' + if (role === 'USER' || role === 'User') return 'USER' + + return null +} + +function mapRoles(roles?: EmployeeRole[]): ApiUserRole[] | undefined { + const normalizedRoles = roles + ?.map(normalizeRole) + .filter((role): role is ApiUserRole => Boolean(role)) + + return normalizedRoles?.length ? normalizedRoles : undefined +} + +function mapSortDirection(sortDirection: UserSortDirection): ApiSortDirection { + return sortDirection === 'ASC' ? 'ASC' : 'DESC' +} + +function mapSortField(sortField: UserSortField): ApiUserSortField { + if (sortField === 'ID') return 'ID' + if (sortField === 'Name') return 'NAME' + + return 'DATE' +} + +function toCreateUserPayload(payload: CreateUserPayload): ApiCreateUserPayload { + const role = normalizeRole(payload.role) ?? 'USER' + + return { + lastName: payload.lastName.trim(), + firstName: payload.firstName.trim(), + middleName: payload.middleName.trim() || null, + email: payload.email.trim(), + login: payload.login.trim(), + password: payload.password, + role, + } +} + +function toUpdateUserPayload(payload: UpdateUserPayload): ApiUpdateUserPayload { + const nextPayload: ApiUpdateUserPayload = {} + + if (payload.lastName !== undefined) { + nextPayload.lastName = payload.lastName.trim() + } + + if (payload.firstName !== undefined) { + nextPayload.firstName = payload.firstName.trim() + } + + if (payload.middleName !== undefined) { + nextPayload.middleName = payload.middleName.trim() || null + } + + if (payload.email !== undefined) { + nextPayload.email = payload.email.trim() + } + + if (payload.login !== undefined) { + nextPayload.login = payload.login.trim() + } + + if (payload.password !== undefined) { + nextPayload.password = payload.password + } + + if (payload.role !== undefined) { + nextPayload.role = normalizeRole(payload.role) + } + + return nextPayload +} + +function logoutIfCurrentUserDeleted(deletedUserId: number) { + if (typeof window === 'undefined') return + + const storedCurrentUser = window.localStorage.getItem(CURRENT_USER_STORAGE_KEY) + + if (!storedCurrentUser) return + + try { + const currentUser = JSON.parse(storedCurrentUser) as Pick + + if (currentUser.id !== deletedUserId) return + } catch { + return + } + + clearAuthStorage() + void apolloClient.clearStore() + window.dispatchEvent(new Event('auth:logout')) + window.dispatchEvent(new Event('auth:change')) +} + +export const usersService = { + async getUsers({ + page = 0, + pageSize = 10, + query, + roles, + sortDirection = 'DESC', + sortField = 'Date', + }: UsersQueryParams = {}): Promise { + cleanupLegacyUsersStorage() + + const { data } = await apolloClient.query({ + query: GET_USERS_QUERY, + variables: { + page, + pageSize, + query: query?.trim() || undefined, + roles: mapRoles(roles), + sortDirection: mapSortDirection(sortDirection), + sortField: mapSortField(sortField), + }, + fetchPolicy: 'no-cache', + }) + + if (!data?.getUsers) { + throw new Error('Не удалось загрузить пользователей') + } + + return { + totalPages: data.getUsers.totalPages, + totalElements: data.getUsers.totalElements, + page: data.getUsers.page.map(mapUser), + } + }, + + async getUserById(id: number | string): Promise { + cleanupLegacyUsersStorage() + + const { data } = await apolloClient.query({ + query: GET_USER_QUERY, + variables: { + id: Number(id), + }, + fetchPolicy: 'no-cache', + }) + + if (!data) { + throw new Error('Не удалось загрузить пользователя') + } + + return data.getUser ? mapUser(data.getUser) : null + }, + + async findUserByLoginOrEmail(loginOrEmail: string): Promise { + const response = await usersService.getUsers({ + page: 0, + pageSize: 1, + query: loginOrEmail.trim(), + sortDirection: 'ASC', + sortField: 'ID', + }) + + const normalizedLoginOrEmail = loginOrEmail.trim().toLowerCase() + + return response.page.find((user) => { + return ( + user.email?.trim().toLowerCase() === normalizedLoginOrEmail || + user.login?.trim().toLowerCase() === normalizedLoginOrEmail || + user.username?.trim().toLowerCase() === normalizedLoginOrEmail + ) + }) ?? null + }, + + async createUser(payload: CreateUserPayload): Promise { + cleanupLegacyUsersStorage() + + const { data } = await apolloClient.mutate({ + mutation: CREATE_USER_MUTATION, + variables: { + payload: toCreateUserPayload(payload), + }, + fetchPolicy: 'no-cache', + }) + + if (!data?.createUser) { + throw new Error('Не удалось создать пользователя') + } + + emitUsersChange() + + return mapUser(data.createUser) + }, + + async updateUser( + id: number | string, + payload: UpdateUserPayload, + ): Promise { + cleanupLegacyUsersStorage() + + const { data } = await apolloClient.mutate({ + mutation: UPDATE_USER_MUTATION, + variables: { + id: Number(id), + payload: toUpdateUserPayload(payload), + }, + fetchPolicy: 'no-cache', + }) + + if (!data?.updateUser) { + throw new Error('Не удалось обновить пользователя') + } + + emitUsersChange() + + return mapUser(data.updateUser) + }, + + async deleteUser(id: number | string): Promise { + cleanupLegacyUsersStorage() + + const numericId = Number(id) + const { data } = await apolloClient.mutate({ + mutation: DELETE_USER_MUTATION, + variables: { + id: numericId, + }, + fetchPolicy: 'no-cache', + }) + + const isDeleted = Boolean(data?.deleteUser) + + if (!isDeleted) { + throw new Error('Пользователь не найден') + } + + logoutIfCurrentUserDeleted(numericId) + emitUsersChange() + + return true + }, +} diff --git a/frontend/src/entities/employee/mock/mockUsers.ts b/frontend/src/entities/employee/mock/mockUsers.ts new file mode 100644 index 0000000..67bcb2a --- /dev/null +++ b/frontend/src/entities/employee/mock/mockUsers.ts @@ -0,0 +1,79 @@ +import type { Employee } from '../model/types' + +export const mockUsers: Employee[] = [ + { + id: 1, + lastName: 'Админов', + firstName: 'Алексей', + middleName: 'Сергеевич', + email: 'admin@fleet.local', + login: 'admin', + username: 'admin', + password: '123456', + role: 'ADMIN', + createdAt: '2026-07-01T09:00:00.000Z', + avatarUrl: null, + orgId: 0, + org: null, + }, + { + id: 2, + lastName: 'Иванова', + firstName: 'Мария', + middleName: 'Петровна', + email: 'user@fleet.local', + login: 'user', + username: 'user', + password: '123456', + role: 'USER', + createdAt: '2026-07-02T10:30:00.000Z', + avatarUrl: null, + orgId: 0, + org: null, + }, + { + id: 3, + lastName: 'Смирнов', + firstName: 'Дмитрий', + middleName: 'Олегович', + email: 'dispatcher@fleet.local', + login: 'dispatcher', + username: 'dispatcher', + password: '123456', + role: 'USER', + createdAt: '2026-07-03T12:15:00.000Z', + avatarUrl: null, + orgId: 0, + org: null, + }, + { + id: 4, + lastName: 'Кузнецова', + firstName: 'Елена', + middleName: 'Андреевна', + email: 'manager@fleet.local', + login: 'manager', + username: 'manager', + password: '123456', + role: 'ADMIN', + createdAt: '2026-07-04T08:45:00.000Z', + avatarUrl: null, + orgId: 0, + org: null, + }, + { + id: 5, + lastName: 'Павлов', + firstName: 'Никита', + middleName: 'Игоревич', + email: 'mechanic@fleet.local', + login: 'mechanic', + username: 'mechanic', + password: '123456', + role: 'USER', + createdAt: '2026-07-05T14:20:00.000Z', + avatarUrl: null, + orgId: 0, + org: null, + }, +] diff --git a/frontend/src/entities/employee/model/types.ts b/frontend/src/entities/employee/model/types.ts new file mode 100644 index 0000000..06ba2f4 --- /dev/null +++ b/frontend/src/entities/employee/model/types.ts @@ -0,0 +1,241 @@ +export type EmployeeRole = 'USER' | 'ADMIN' | 'User' | 'Admin' | string + +export type Employee = { + avatarUrl?: string | null + id: number + firstName: string + lastName: string + middleName: string + email?: string + login?: string + password?: string + createdAt?: string + username?: string + orgId: number + role: EmployeeRole + org: { + creationDate?: number + id: number + logoUrl?: string | null + name: string + } | null +} + +export type UserSortDirection = 'ASC' | 'DESC' + +export type UserSortField = 'ID' | 'Name' | 'Date' + +export type ServiceUseOption = + | 'Disabled' + | 'Enabled' + | 'Unspecified' + +export type ComponentUseOption = + | 'Allowed' + | 'Disallowed' + +export type GroupUsePolicyOption = + | 'Allowed' + | 'Disallowed' + | 'Changeable' + +export type GetUsersData = { + getUsers: { + totalPages: number + totalElements: number + page: Employee[] + } +} + +export type GetUsersVariables = { + page: number + query?: string + orgs?: string[] + roles?: EmployeeRole[] + sortDirection: UserSortDirection + sortField: UserSortField +} + +export type OrganisationPolicy = { + bluetooth?: ServiceUseOption | GroupUsePolicyOption + bluetoothEditable?: boolean + camera?: ComponentUseOption | GroupUsePolicyOption + cameraEditable?: boolean + GPS?: ServiceUseOption | GroupUsePolicyOption + gpsEditable?: boolean + sim?: ServiceUseOption | GroupUsePolicyOption + simEditable?: boolean + canUseBluetooth?: boolean + canUseCamera?: boolean + canUseGPS?: boolean + canUseSim?: boolean + offlineTimeThreshold: number + date?: number +} + +export type Organisation = { + creationDate?: number + id: number + name: string + policy?: OrganisationPolicy | null + logoUrl?: string | null +} + +export type GetOrganisationData = { + getOrganisation: Organisation | null +} + +export type GetOrganisationVariables = { + id: string +} + +export type ChangeOrganisationData = { + changeOrganisation: { + id: number + name: string + } +} + +export type ChangeOrganisationVariables = { + id: string + name: string + bluetooth: ServiceUseOption + bluetoothEditable: boolean + camera: ComponentUseOption + cameraEditable: boolean + GPS: ServiceUseOption + gpsEditable: boolean + sim: ServiceUseOption + simEditable: boolean + offlineTimeThreshold: number +} + +export type ChangeOrganisationPolicyVariables = { + id: string + name: string + bluetooth: ServiceUseOption + bluetoothEditable: boolean + camera: ComponentUseOption + cameraEditable: boolean + GPS: ServiceUseOption + gpsEditable: boolean + sim: ServiceUseOption + simEditable: boolean + offlineTimeThreshold: number +} + +export type GetUsersPageData = { + getUsersPage: { + page: Employee[] + nextKey: string | null + } +} + +export type GetUsersPageVariables = { + key?: string +} + +export type GetOrganisationsData = { + getOrganisations: { + totalPages: number + totalElements: number + page: Organisation[] + } +} + +export type GetOrganisationsVariables = { + page: number + query?: string + sortDirection: OrganisationSortDirection + sortField: OrganisationSortField +} + +export type DeleteOrganisationData = { + deleteOrganisation: boolean +} + +export type DeleteOrganisationVariables = { + id: string +} + +export type CreateUserData = { + createUser: Employee +} + +export type CreateUserVariables = { + orgId: string + firstName: string + lastName: string + middleName: string + username: string + password: string + role: EmployeeRole +} + +export type ChangeUserData = { + changeUser: Employee +} + +export type ChangeUserVariables = { + userId: string + orgId: string + firstName: string + lastName: string + middleName: string + username: string + role: EmployeeRole +} + +export type ChangeUserWithPasswordVariables = ChangeUserVariables & { + password: string +} + +export type SignUpVariables = { + orgId: string + firstName: string + lastName: string + middleName: string + username: string + password: string + role: EmployeeRole +} + +export type CreateOrganisationData = { + createOrganisation: { + id: number + name: string + } +} + +export type CreateOrganisationVariables = { + name: string + bluetooth: ServiceUseOption + bluetoothEditable: boolean + camera: ComponentUseOption + cameraEditable: boolean + GPS: ServiceUseOption + gpsEditable: boolean + sim: ServiceUseOption + simEditable: boolean + offlineTimeThreshold: number +} + +export type OrganisationSortDirection = 'ASC' | 'DESC' + +export type OrganisationSortField = 'ID' | 'Name' | 'Date' + +export type CreateUploadUserAvatarUrlData = { + createUploadUserAvatarUrl: string +} + +export type CreateUploadUserAvatarUrlVariables = { + userId: string +} + +export type CreateUploadOrganisationLogoUrlData = { + createUploadOrganisationLogoUrl: string +} + +export type CreateUploadOrganisationLogoUrlVariables = { + orgId: string +} diff --git a/frontend/src/entities/machine/api/machineAnalyticsService.ts b/frontend/src/entities/machine/api/machineAnalyticsService.ts new file mode 100644 index 0000000..a0fcd47 --- /dev/null +++ b/frontend/src/entities/machine/api/machineAnalyticsService.ts @@ -0,0 +1,257 @@ +import { mockMachineDailyUsageStats } from '../mock/mockMachineDailyUsageStats' +import { mockMachineTelemetryIntervals } from '../mock/mockMachineTelemetryIntervals' +import type { + MachineAnalyticsDateRange, + MachineAnalyticsPeriod, + MachineDailyUsageStat, + MachineTelemetryInterval, + MachineUsageSummary, +} from '../model/types' + +type PeriodRange = { + start: number + end: number +} + +const hourMs = 60 * 60 * 1000 +const dayMs = 24 * hourMs +const fallbackNow = Date.UTC(2026, 6, 9, 12, 0, 0) +const shiftStartHourUtc = 1 +const shiftDurationMs = 12 * hourMs + +function cloneTelemetryInterval( + interval: MachineTelemetryInterval, +): MachineTelemetryInterval { + return { ...interval } +} + +function cloneDailyUsageStat(stat: MachineDailyUsageStat): MachineDailyUsageStat { + return { ...stat } +} + +function getDayStart(timestamp: number) { + const date = new Date(timestamp) + + return Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + ) +} + +function getIntervalEnd(interval: MachineTelemetryInterval) { + return new Date(interval.endedAt).getTime() +} + +function getLatestMachineTimestamp(machineId: number) { + const timestamps = mockMachineTelemetryIntervals + .filter((interval) => interval.machineId === machineId) + .map(getIntervalEnd) + + return Math.max(0, ...timestamps) || fallbackNow +} + +function getDefaultPeriodRange( + machineId: number, + period: MachineAnalyticsPeriod, +): PeriodRange { + const latestTimestamp = getLatestMachineTimestamp(machineId) + const dayStart = getDayStart(latestTimestamp) + + if (period === 'shift') { + const shiftStart = dayStart + shiftStartHourUtc * hourMs + + return { + start: shiftStart, + end: shiftStart + shiftDurationMs, + } + } + + if (period === 'week') { + return { + start: dayStart - 6 * dayMs, + end: dayStart + dayMs, + } + } + + if (period === 'month') { + return { + start: dayStart - 29 * dayMs, + end: dayStart + dayMs, + } + } + + return { + start: dayStart, + end: dayStart + dayMs, + } +} + +function getPeriodRange( + machineId: number, + period: MachineAnalyticsPeriod, + dateRange?: MachineAnalyticsDateRange, +): PeriodRange { + const defaultRange = getDefaultPeriodRange(machineId, period) + const start = dateRange?.dateFrom + ? new Date(dateRange.dateFrom).getTime() + : defaultRange.start + const end = dateRange?.dateTo + ? new Date(dateRange.dateTo).getTime() + : defaultRange.end + + return { + start, + end, + } +} + +function clipIntervalToRange( + interval: MachineTelemetryInterval, + range: PeriodRange, +): MachineTelemetryInterval | null { + const startedAt = new Date(interval.startedAt).getTime() + const endedAt = new Date(interval.endedAt).getTime() + const clippedStart = Math.max(startedAt, range.start) + const clippedEnd = Math.min(endedAt, range.end) + + if (clippedEnd <= clippedStart) return null + + return { + ...interval, + startedAt: new Date(clippedStart).toISOString(), + endedAt: new Date(clippedEnd).toISOString(), + durationSeconds: Math.floor((clippedEnd - clippedStart) / 1000), + } +} + +function getTelemetryIntervalsInRange( + machineId: number, + range: PeriodRange, +) { + return mockMachineTelemetryIntervals + .filter((interval) => interval.machineId === machineId) + .map((interval) => clipIntervalToRange(interval, range)) + .filter((interval): interval is MachineTelemetryInterval => Boolean(interval)) + .sort((first, second) => ( + new Date(first.startedAt).getTime() - + new Date(second.startedAt).getTime() + )) +} + +function buildSummary( + machineId: number, + period: MachineAnalyticsPeriod, + intervals: MachineTelemetryInterval[], +): MachineUsageSummary { + const totals = intervals.reduce( + (result, interval) => { + if (interval.state === 'MOVING') { + result.movingSeconds += interval.durationSeconds + } + + if (interval.state === 'ENGINE_ON') { + result.engineOnSeconds += interval.durationSeconds + } + + if (interval.state === 'IDLE') { + result.idleSeconds += interval.durationSeconds + } + + if (interval.state === 'NO_DATA') { + result.noDataSeconds += interval.durationSeconds + } + + return result + }, + { + movingSeconds: 0, + engineOnSeconds: 0, + idleSeconds: 0, + noDataSeconds: 0, + }, + ) + const totalKnownSeconds = + totals.movingSeconds + totals.engineOnSeconds + totals.idleSeconds + const utilizationPercent = totalKnownSeconds > 0 + ? Math.round((totals.movingSeconds / totalKnownSeconds) * 100) + : 0 + + return { + machineId, + period, + ...totals, + totalKnownSeconds, + utilizationPercent, + } +} + +function buildDailyStatFromIntervals( + machineId: number, + range: PeriodRange, + intervals: MachineTelemetryInterval[], +): MachineDailyUsageStat { + const summary = buildSummary(machineId, 'today', intervals) + + return { + machineId, + date: new Date(range.start).toISOString().slice(0, 10), + movingSeconds: summary.movingSeconds, + engineOnSeconds: summary.engineOnSeconds, + idleSeconds: summary.idleSeconds, + noDataSeconds: summary.noDataSeconds, + utilizationPercent: summary.utilizationPercent, + } +} + +export const machineAnalyticsService = { + async getMachineTelemetryIntervals( + machineId: number | string, + period: MachineAnalyticsPeriod, + dateRange?: MachineAnalyticsDateRange, + ): Promise { + const numericMachineId = Number(machineId) + const range = getPeriodRange(numericMachineId, period, dateRange) + + return getTelemetryIntervalsInRange(numericMachineId, range) + .map(cloneTelemetryInterval) + }, + + async getMachineUsageSummary( + machineId: number | string, + period: MachineAnalyticsPeriod, + dateRange?: MachineAnalyticsDateRange, + ): Promise { + const numericMachineId = Number(machineId) + const range = getPeriodRange(numericMachineId, period, dateRange) + const intervals = getTelemetryIntervalsInRange(numericMachineId, range) + + return buildSummary(numericMachineId, period, intervals) + }, + + async getMachineDailyUsageStats( + machineId: number | string, + period: MachineAnalyticsPeriod, + dateRange?: MachineAnalyticsDateRange, + ): Promise { + const numericMachineId = Number(machineId) + const range = getPeriodRange(numericMachineId, period, dateRange) + + if (period === 'shift') { + const intervals = getTelemetryIntervalsInRange(numericMachineId, range) + + return [buildDailyStatFromIntervals(numericMachineId, range, intervals)] + } + + return mockMachineDailyUsageStats + .filter((stat) => { + if (stat.machineId !== numericMachineId) return false + + const dateStart = new Date(`${stat.date}T00:00:00.000Z`).getTime() + + return dateStart >= range.start && dateStart < range.end + }) + .sort((first, second) => first.date.localeCompare(second.date)) + .map(cloneDailyUsageStat) + }, +} diff --git a/frontend/src/entities/machine/api/machineCalibrationService.ts b/frontend/src/entities/machine/api/machineCalibrationService.ts new file mode 100644 index 0000000..34af572 --- /dev/null +++ b/frontend/src/entities/machine/api/machineCalibrationService.ts @@ -0,0 +1,701 @@ +import { readBeacons } from '../../beacon/api/beaconsService' +import type { BeaconVibrationSample } from '../../beacon/model/types' +import { mockBeaconVibrationSamples } from '../../beacon/mock/mockBeaconVibrationSamples' +import { mockMachineCalibrationRules } from '../mock/mockMachineCalibrationRules' +import { mockMachineStateDefinitions } from '../mock/mockMachineStateDefinitions' +import type { + MachineCalibrationPeriod, + MachineCalibrationRule, + MachineStateDefinition, + RecognizedMachineStateInterval, +} from '../model/types' + +export type MachineStateDefinitionPayload = { + name: string + icon: string + color: string + description: string +} + +export type MachineCalibrationRulePayload = { + stateDefinitionId: number + vibrationMin: number + vibrationMax: number + minDurationSeconds?: number + priority?: number +} + +type PeriodRange = { + start: number + end: number +} + +type DraftRecognizedInterval = Omit + +const STATE_DEFINITIONS_STORAGE_KEY = + 'fleet-ble-monitor.machineStateDefinitions' +const CALIBRATION_RULES_STORAGE_KEY = + 'fleet-ble-monitor.machineCalibrationRules' +const aggregateRuleGap = 0.02 +const noDataDefinitionId = 4 +const noDataGapMs = 30 * 60 * 1000 +const mockMachineVibrationMacByMachineId: Record = { + 11: 'E4:5F:01:AA:10:10', +} + +function cloneStateDefinition( + definition: MachineStateDefinition, +): MachineStateDefinition { + return { ...definition } +} + +function cloneCalibrationRule( + rule: MachineCalibrationRule, +): MachineCalibrationRule { + return { ...rule } +} + +function cloneVibrationSample( + sample: BeaconVibrationSample, +): BeaconVibrationSample { + return { ...sample } +} + +function cloneRecognizedInterval( + interval: RecognizedMachineStateInterval, +): RecognizedMachineStateInterval { + return { ...interval } +} + +function readStateDefinitions(): MachineStateDefinition[] { + if (typeof window === 'undefined') { + return mockMachineStateDefinitions.map(cloneStateDefinition) + } + + const storedDefinitions = window.localStorage.getItem( + STATE_DEFINITIONS_STORAGE_KEY, + ) + + if (!storedDefinitions) { + const initialDefinitions = mockMachineStateDefinitions.map(cloneStateDefinition) + window.localStorage.setItem( + STATE_DEFINITIONS_STORAGE_KEY, + JSON.stringify(initialDefinitions), + ) + + return initialDefinitions + } + + try { + return (JSON.parse(storedDefinitions) as MachineStateDefinition[]) + .map(cloneStateDefinition) + } catch { + const initialDefinitions = mockMachineStateDefinitions.map(cloneStateDefinition) + window.localStorage.setItem( + STATE_DEFINITIONS_STORAGE_KEY, + JSON.stringify(initialDefinitions), + ) + + return initialDefinitions + } +} + +function writeStateDefinitions(definitions: MachineStateDefinition[]) { + if (typeof window === 'undefined') return + + window.localStorage.setItem( + STATE_DEFINITIONS_STORAGE_KEY, + JSON.stringify(definitions), + ) + window.dispatchEvent(new Event('machine-calibration:change')) +} + +function readCalibrationRules(): MachineCalibrationRule[] { + if (typeof window === 'undefined') { + return cloneCalibrationRulesWithAggregation(mockMachineCalibrationRules) + } + + const storedRules = window.localStorage.getItem(CALIBRATION_RULES_STORAGE_KEY) + + if (!storedRules) { + const initialRules = mockMachineCalibrationRules.map(cloneCalibrationRule) + window.localStorage.setItem( + CALIBRATION_RULES_STORAGE_KEY, + JSON.stringify(initialRules), + ) + + return cloneCalibrationRulesWithAggregation(initialRules) + } + + try { + return cloneCalibrationRulesWithAggregation( + JSON.parse(storedRules) as MachineCalibrationRule[], + ) + } catch { + const initialRules = mockMachineCalibrationRules.map(cloneCalibrationRule) + window.localStorage.setItem( + CALIBRATION_RULES_STORAGE_KEY, + JSON.stringify(initialRules), + ) + + return cloneCalibrationRulesWithAggregation(initialRules) + } +} + +function writeCalibrationRules(rules: MachineCalibrationRule[]) { + if (typeof window === 'undefined') return + + window.localStorage.setItem( + CALIBRATION_RULES_STORAGE_KEY, + JSON.stringify(rules), + ) + window.dispatchEvent(new Event('machine-calibration:change')) +} + +function normalizeStateDefinitionPayload(payload: MachineStateDefinitionPayload) { + return { + name: payload.name.trim(), + icon: payload.icon.trim() || 'Activity', + color: payload.color.trim() || '#2f80ed', + description: payload.description.trim(), + } +} + +function normalizeCalibrationRulePayload( + payload: MachineCalibrationRulePayload, + fallback?: Pick, +) { + const vibrationMin = Number(payload.vibrationMin) + const vibrationMax = Number(payload.vibrationMax) + + return { + stateDefinitionId: Number(payload.stateDefinitionId), + vibrationMin: Math.min(vibrationMin, vibrationMax), + vibrationMax: Math.max(vibrationMin, vibrationMax), + minDurationSeconds: Math.max( + 0, + Number(payload.minDurationSeconds ?? fallback?.minDurationSeconds ?? 0), + ), + priority: Math.max(0, Number(payload.priority ?? fallback?.priority ?? 0)), + } +} + +function canAggregateRules( + first: Pick, + second: Pick, +) { + return ( + first.vibrationMin <= second.vibrationMax + aggregateRuleGap && + second.vibrationMin <= first.vibrationMax + aggregateRuleGap + ) +} + +function hasRuleRangeIntersection( + first: Pick, + second: Pick, +) { + return ( + first.vibrationMin <= second.vibrationMax && + second.vibrationMin <= first.vibrationMax + ) +} + +function findConflictingCalibrationRule( + rule: MachineCalibrationRule, + rules: MachineCalibrationRule[], +) { + return rules.find((item) => ( + item.id !== rule.id && + item.machineId === rule.machineId && + item.stateDefinitionId !== rule.stateDefinitionId && + hasRuleRangeIntersection(item, rule) + )) ?? null +} + +function assertCalibrationRuleCanBeSaved( + rule: MachineCalibrationRule, + rules: MachineCalibrationRule[], +) { + if (rule.stateDefinitionId === noDataDefinitionId) { + throw new Error('Состояние "Нет данных" назначается автоматически') + } + + const conflictRule = findConflictingCalibrationRule(rule, rules) + + if (conflictRule) { + throw new Error( + `Диапазон пересекается с другим состоянием: ${conflictRule.vibrationMin.toFixed(3)} — ${conflictRule.vibrationMax.toFixed(3)}`, + ) + } +} + +function aggregateCalibrationRule( + baseRule: MachineCalibrationRule, + rules: MachineCalibrationRule[], +) { + let aggregatedRule = { ...baseRule } + let restRules = rules.filter((rule) => rule.id !== aggregatedRule.id) + let hasAggregatedRule = true + + while (hasAggregatedRule) { + hasAggregatedRule = false + + restRules = restRules.filter((rule) => { + if ( + rule.machineId !== aggregatedRule.machineId || + rule.stateDefinitionId !== aggregatedRule.stateDefinitionId || + !canAggregateRules(rule, aggregatedRule) + ) { + return true + } + + aggregatedRule = { + ...aggregatedRule, + createdAt: rule.createdAt < aggregatedRule.createdAt + ? rule.createdAt + : aggregatedRule.createdAt, + minDurationSeconds: Math.min( + aggregatedRule.minDurationSeconds, + rule.minDurationSeconds, + ), + priority: Math.min(aggregatedRule.priority, rule.priority), + updatedAt: new Date().toISOString(), + vibrationMax: Math.max(aggregatedRule.vibrationMax, rule.vibrationMax), + vibrationMin: Math.min(aggregatedRule.vibrationMin, rule.vibrationMin), + } + hasAggregatedRule = true + + return false + }) + } + + return { + rule: aggregatedRule, + rules: [...restRules, aggregatedRule], + } +} + +function aggregateCalibrationRulesList(rules: MachineCalibrationRule[]) { + return [...rules] + .sort((first, second) => ( + first.machineId - second.machineId || + first.stateDefinitionId - second.stateDefinitionId || + first.vibrationMin - second.vibrationMin || + first.priority - second.priority + )) + .reduce((result, rule) => { + const aggregateIndex = result.findIndex((item) => ( + item.machineId === rule.machineId && + item.stateDefinitionId === rule.stateDefinitionId && + canAggregateRules(item, rule) + )) + + if (aggregateIndex === -1) { + result.push({ ...rule }) + return result + } + + const aggregateRule = result[aggregateIndex] + + result[aggregateIndex] = { + ...aggregateRule, + createdAt: rule.createdAt < aggregateRule.createdAt + ? rule.createdAt + : aggregateRule.createdAt, + minDurationSeconds: Math.min( + aggregateRule.minDurationSeconds, + rule.minDurationSeconds, + ), + priority: Math.min(aggregateRule.priority, rule.priority), + updatedAt: rule.updatedAt > aggregateRule.updatedAt + ? rule.updatedAt + : aggregateRule.updatedAt, + vibrationMax: Math.max(aggregateRule.vibrationMax, rule.vibrationMax), + vibrationMin: Math.min(aggregateRule.vibrationMin, rule.vibrationMin), + } + + return result + }, []) +} + +function cloneCalibrationRulesWithAggregation(rules: MachineCalibrationRule[]) { + return aggregateCalibrationRulesList(rules).map(cloneCalibrationRule) +} + +function getMachineMacAddress(machineId: number) { + return readBeacons().find((beacon) => beacon.boundMachineId === machineId) + ?.macAddress ?? mockMachineVibrationMacByMachineId[machineId] ?? null +} + +function getPeriodRange( + period: MachineCalibrationPeriod, +): PeriodRange { + return { + start: period.periodStart.getTime(), + end: period.periodEnd.getTime(), + } +} + +function getSamplesForMachine( + machineId: number, + period: MachineCalibrationPeriod, +) { + const macAddress = getMachineMacAddress(machineId) + + if (!macAddress) return [] + + const range = getPeriodRange(period) + + return mockBeaconVibrationSamples + .filter((sample) => ( + sample.mac === macAddress && + sample.timeMillis >= range.start && + sample.timeMillis <= range.end + )) + .sort((first, second) => first.timeMillis - second.timeMillis) +} + +function findMatchingRule( + sample: BeaconVibrationSample, + rules: MachineCalibrationRule[], +) { + return [...rules] + .sort((first, second) => first.priority - second.priority) + .find((rule) => ( + sample.vibration >= rule.vibrationMin && + sample.vibration <= rule.vibrationMax + )) ?? null +} + +function createSampleInterval( + machineId: number, + sample: BeaconVibrationSample, + rule: MachineCalibrationRule | null, +): DraftRecognizedInterval { + return { + machineId, + stateDefinitionId: rule?.stateDefinitionId ?? null, + startedAt: new Date(sample.timeMillis).toISOString(), + endedAt: new Date(sample.timeMillis + sample.duration * 1000).toISOString(), + durationSeconds: sample.duration, + vibrationMin: sample.vibration, + vibrationMax: sample.vibration, + vibrationAvg: sample.vibration, + sampleCount: 1, + isNoData: false, + } +} + +function createNoDataInterval( + machineId: number, + startedAt: number, + endedAt: number, +): DraftRecognizedInterval { + return { + machineId, + stateDefinitionId: noDataDefinitionId, + startedAt: new Date(startedAt).toISOString(), + endedAt: new Date(endedAt).toISOString(), + durationSeconds: Math.floor((endedAt - startedAt) / 1000), + vibrationMin: null, + vibrationMax: null, + vibrationAvg: null, + sampleCount: 0, + isNoData: true, + } +} + +function canMergeIntervals( + current: DraftRecognizedInterval, + next: DraftRecognizedInterval, +) { + const currentEnd = new Date(current.endedAt).getTime() + const nextStart = new Date(next.startedAt).getTime() + + return ( + current.stateDefinitionId === next.stateDefinitionId && + current.isNoData === next.isNoData && + nextStart - currentEnd <= noDataGapMs + ) +} + +function mergeIntervals( + current: DraftRecognizedInterval, + next: DraftRecognizedInterval, +): DraftRecognizedInterval { + const totalSamples = current.sampleCount + next.sampleCount + const currentWeighted = (current.vibrationAvg ?? 0) * current.sampleCount + const nextWeighted = (next.vibrationAvg ?? 0) * next.sampleCount + + return { + ...current, + endedAt: next.endedAt, + durationSeconds: Math.floor(( + new Date(next.endedAt).getTime() - + new Date(current.startedAt).getTime() + ) / 1000), + vibrationMin: + current.vibrationMin === null || next.vibrationMin === null + ? current.vibrationMin ?? next.vibrationMin + : Math.min(current.vibrationMin, next.vibrationMin), + vibrationMax: + current.vibrationMax === null || next.vibrationMax === null + ? current.vibrationMax ?? next.vibrationMax + : Math.max(current.vibrationMax, next.vibrationMax), + vibrationAvg: totalSamples > 0 + ? Number(((currentWeighted + nextWeighted) / totalSamples).toFixed(3)) + : null, + sampleCount: totalSamples, + } +} + +function buildRecognizedIntervals( + machineId: number, + samples: BeaconVibrationSample[], + rules: MachineCalibrationRule[], +) { + const intervals: DraftRecognizedInterval[] = [] + + samples.forEach((sample, index) => { + if (index > 0) { + const previousSample = samples[index - 1] + const previousEnd = previousSample.timeMillis + previousSample.duration * 1000 + const gap = sample.timeMillis - previousEnd + + if (gap > noDataGapMs) { + intervals.push(createNoDataInterval(machineId, previousEnd, sample.timeMillis)) + } + } + + intervals.push(createSampleInterval( + machineId, + sample, + findMatchingRule(sample, rules), + )) + }) + + return intervals.reduce( + (result, interval) => { + const previous = result.at(-1) + + if (previous && canMergeIntervals(previous, interval)) { + result[result.length - 1] = { + ...mergeIntervals(previous, interval), + id: previous.id, + } + return result + } + + result.push({ + ...interval, + id: result.length + 1, + }) + + return result + }, + [], + ) +} + +export const machineCalibrationService = { + async getMachineStateDefinitions(): Promise { + return readStateDefinitions().map(cloneStateDefinition) + }, + + async getMachineStateDefinitionsSnapshot(): Promise { + return readStateDefinitions().map(cloneStateDefinition) + }, + + async restoreMachineStateDefinitions( + definitions: MachineStateDefinition[], + ): Promise { + const nextDefinitions = definitions.map(cloneStateDefinition) + + writeStateDefinitions(nextDefinitions) + + return nextDefinitions.map(cloneStateDefinition) + }, + + async createMachineStateDefinition( + payload: MachineStateDefinitionPayload, + ): Promise { + const definitions = readStateDefinitions() + const now = new Date().toISOString() + const normalizedPayload = normalizeStateDefinitionPayload(payload) + + if (!normalizedPayload.name) { + throw new Error('Название состояния обязательно') + } + + const definition: MachineStateDefinition = { + id: Math.max(0, ...definitions.map((item) => item.id)) + 1, + ...normalizedPayload, + isSystem: false, + createdAt: now, + updatedAt: now, + } + + writeStateDefinitions([...definitions, definition]) + + return cloneStateDefinition(definition) + }, + + async updateMachineStateDefinition( + id: number | string, + payload: MachineStateDefinitionPayload, + ): Promise { + const numericId = Number(id) + const definitions = readStateDefinitions() + const definitionIndex = definitions.findIndex((item) => item.id === numericId) + const normalizedPayload = normalizeStateDefinitionPayload(payload) + + if (!normalizedPayload.name) { + throw new Error('Название состояния обязательно') + } + + if (definitionIndex === -1) { + throw new Error('Состояние не найдено') + } + + if (numericId === noDataDefinitionId) { + throw new Error('Системное состояние "Нет данных" нельзя изменить') + } + + const nextDefinition: MachineStateDefinition = { + ...definitions[definitionIndex], + ...normalizedPayload, + updatedAt: new Date().toISOString(), + } + const nextDefinitions = [...definitions] + nextDefinitions[definitionIndex] = nextDefinition + + writeStateDefinitions(nextDefinitions) + + return cloneStateDefinition(nextDefinition) + }, + + async getMachineCalibrationRules( + machineId: number | string, + ): Promise { + const numericMachineId = Number(machineId) + + return readCalibrationRules() + .filter((rule) => rule.machineId === numericMachineId) + .sort((first, second) => first.priority - second.priority) + .map(cloneCalibrationRule) + }, + + async getCalibrationRulesSnapshot(): Promise { + return readCalibrationRules().map(cloneCalibrationRule) + }, + + async restoreCalibrationRules( + rules: MachineCalibrationRule[], + ): Promise { + const nextRules = rules.map(cloneCalibrationRule) + + writeCalibrationRules(nextRules) + + return nextRules.map(cloneCalibrationRule) + }, + + async createMachineCalibrationRule( + machineId: number | string, + payload: MachineCalibrationRulePayload, + ): Promise { + const numericMachineId = Number(machineId) + const rules = readCalibrationRules() + const nextPriority = rules + .filter((rule) => rule.machineId === numericMachineId) + .reduce((maxPriority, rule) => Math.max(maxPriority, rule.priority), 10) + 10 + const normalizedPayload = normalizeCalibrationRulePayload(payload, { + minDurationSeconds: 0, + priority: nextPriority, + }) + const now = new Date().toISOString() + + const rule: MachineCalibrationRule = { + id: Math.max(0, ...rules.map((item) => item.id)) + 1, + machineId: numericMachineId, + ...normalizedPayload, + createdAt: now, + updatedAt: now, + } + const aggregation = aggregateCalibrationRule(rule, rules) + + assertCalibrationRuleCanBeSaved(aggregation.rule, aggregation.rules) + + writeCalibrationRules(aggregation.rules) + + return cloneCalibrationRule(aggregation.rule) + }, + + async updateMachineCalibrationRule( + id: number | string, + payload: MachineCalibrationRulePayload, + ): Promise { + const numericId = Number(id) + const rules = readCalibrationRules() + const ruleIndex = rules.findIndex((item) => item.id === numericId) + + if (ruleIndex === -1) { + throw new Error('Правило калибровки не найдено') + } + + const normalizedPayload = normalizeCalibrationRulePayload( + payload, + rules[ruleIndex], + ) + + const nextRule: MachineCalibrationRule = { + ...rules[ruleIndex], + ...normalizedPayload, + updatedAt: new Date().toISOString(), + } + const nextRules = [...rules] + nextRules[ruleIndex] = nextRule + const aggregation = aggregateCalibrationRule(nextRule, nextRules) + + assertCalibrationRuleCanBeSaved(aggregation.rule, aggregation.rules) + + writeCalibrationRules(aggregation.rules) + + return cloneCalibrationRule(aggregation.rule) + }, + + async deleteMachineCalibrationRule(id: number | string): Promise { + const numericId = Number(id) + const rules = readCalibrationRules() + const nextRules = rules.filter((rule) => rule.id !== numericId) + + if (nextRules.length === rules.length) { + throw new Error('Правило калибровки не найдено') + } + + writeCalibrationRules(nextRules) + + return true + }, + + async getMachineVibrationSamples( + machineId: number | string, + period: MachineCalibrationPeriod, + ): Promise { + return getSamplesForMachine(Number(machineId), period) + .map(cloneVibrationSample) + }, + + async getRecognizedStateIntervals( + machineId: number | string, + period: MachineCalibrationPeriod, + ): Promise { + const numericMachineId = Number(machineId) + const [samples, rules] = await Promise.all([ + this.getMachineVibrationSamples(numericMachineId, period), + this.getMachineCalibrationRules(numericMachineId), + ]) + + return buildRecognizedIntervals(numericMachineId, samples, rules) + .map(cloneRecognizedInterval) + }, +} diff --git a/frontend/src/entities/machine/api/machineDictionariesService.ts b/frontend/src/entities/machine/api/machineDictionariesService.ts new file mode 100644 index 0000000..4ae0f6b --- /dev/null +++ b/frontend/src/entities/machine/api/machineDictionariesService.ts @@ -0,0 +1,19 @@ +import { + mockMachineDepartments, + mockMachineTypes, +} from '../mock/mockMachineDictionaries' +import type { MachineDictionaryOption } from '../model/types' + +function cloneOption(option: MachineDictionaryOption): MachineDictionaryOption { + return { ...option } +} + +export const machineDictionariesService = { + async getMachineTypes(): Promise { + return mockMachineTypes.map(cloneOption) + }, + + async getMachineDepartments(): Promise { + return mockMachineDepartments.map(cloneOption) + }, +} diff --git a/frontend/src/entities/machine/api/machinesService.ts b/frontend/src/entities/machine/api/machinesService.ts new file mode 100644 index 0000000..8d4fa84 --- /dev/null +++ b/frontend/src/entities/machine/api/machinesService.ts @@ -0,0 +1,362 @@ +import { readBeacons, writeBeacons } from '../../beacon/api/beaconsService' +import type { Beacon } from '../../beacon/model/types' +import { mockMachineStateHistory } from '../mock/mockMachineStateHistory' +import { mockMachines } from '../mock/mockMachines' +import type { + Machine, + MachineStateHistoryItem, + MachineSortDirection, + MachineSortField, +} from '../model/types' + +const MACHINES_STORAGE_KEY = 'fleet-ble-monitor.machines' + +export type MachinesQueryParams = { + page?: number + pageSize?: number + query?: string + sortDirection?: MachineSortDirection + sortField?: MachineSortField +} + +export type MachinesPage = { + totalPages: number + totalElements: number + page: Machine[] +} + +export type MachinePayload = { + name: string + licensePlate: string + type: string + department: string + description: string +} + +type StoredMachine = Machine & Record + +function cloneMachine(machine: Machine | StoredMachine): Machine { + const normalizedMachine = { ...machine } as StoredMachine + const legacyInventoryKey = 'inventory' + 'Number' + + delete normalizedMachine[legacyInventoryKey] + + return normalizedMachine as Machine +} + +function readMachines(): Machine[] { + if (typeof window === 'undefined') { + return mockMachines.map(cloneMachine) + } + + const storedMachines = window.localStorage.getItem(MACHINES_STORAGE_KEY) + + if (!storedMachines) { + const initialMachines = mockMachines.map(cloneMachine) + window.localStorage.setItem(MACHINES_STORAGE_KEY, JSON.stringify(initialMachines)) + + return initialMachines + } + + try { + return (JSON.parse(storedMachines) as StoredMachine[]).map(cloneMachine) + } catch { + const initialMachines = mockMachines.map(cloneMachine) + window.localStorage.setItem(MACHINES_STORAGE_KEY, JSON.stringify(initialMachines)) + + return initialMachines + } +} + +function writeMachines(machines: Machine[]) { + if (typeof window === 'undefined') return + + window.localStorage.setItem(MACHINES_STORAGE_KEY, JSON.stringify(machines)) + window.dispatchEvent(new Event('machines:change')) +} + +function assertBeaconCanBeBound( + beacons: Beacon[], + beaconId: number, + machineId: number, +) { + const beacon = beacons.find((item) => item.id === beaconId) + + if (!beacon) { + throw new Error('Маяк не найден') + } + + if ( + beacon.status === 'BOUND' && + beacon.boundMachineId !== null && + beacon.boundMachineId !== machineId + ) { + throw new Error('Маяк уже привязан к другой АТТ') + } +} + +function updateBeaconBinding( + beacons: Beacon[], + machineId: number, + nextBeaconId: number | null, +) { + return beacons.map((beacon) => { + if (beacon.boundMachineId === machineId) { + return { + ...beacon, + status: 'FREE', + boundMachineId: null, + } satisfies Beacon + } + + if (nextBeaconId !== null && beacon.id === nextBeaconId) { + return { + ...beacon, + status: 'BOUND', + boundMachineId: machineId, + } satisfies Beacon + } + + return beacon + }) +} + +function normalizeMachinePayload(payload: MachinePayload) { + return { + name: payload.name.trim(), + licensePlate: payload.licensePlate.trim(), + type: payload.type.trim(), + department: payload.department.trim(), + description: payload.description.trim(), + } +} + +function sortMachines( + machines: Machine[], + sortField: MachineSortField, + sortDirection: MachineSortDirection, +) { + const direction = sortDirection === 'ASC' ? 1 : -1 + + return [...machines].sort((first, second) => { + if (sortField === 'ID') { + return (first.id - second.id) * direction + } + + if (sortField === 'Date') { + return ( + (new Date(first.createdAt).getTime() - + new Date(second.createdAt).getTime()) * + direction + ) + } + + return first.name.localeCompare(second.name, 'ru') * direction + }) +} + +function cloneHistoryItem( + item: MachineStateHistoryItem, +): MachineStateHistoryItem { + return { ...item } +} + +export const machinesService = { + async getMachines({ + page = 0, + pageSize = 10, + query, + sortDirection = 'DESC', + sortField = 'Date', + }: MachinesQueryParams = {}): Promise { + const normalizedQuery = query?.trim().toLowerCase() ?? '' + + const filteredMachines = readMachines().filter((machine) => { + if (!normalizedQuery) return true + + return [ + machine.name, + machine.licensePlate, + machine.type, + machine.department, + ] + .filter(Boolean) + .join(' ') + .toLowerCase() + .includes(normalizedQuery) + }) + + const sortedMachines = sortMachines(filteredMachines, sortField, sortDirection) + const totalPages = Math.ceil(sortedMachines.length / pageSize) + const safePage = Math.min(Math.max(page, 0), Math.max(totalPages - 1, 0)) + const pageStart = safePage * pageSize + + return { + totalPages, + totalElements: sortedMachines.length, + page: sortedMachines.slice(pageStart, pageStart + pageSize).map(cloneMachine), + } + }, + + async getMachineById(id: number | string): Promise { + const numericId = Number(id) + const machine = readMachines().find((item) => item.id === numericId) + + return machine ? cloneMachine(machine) : null + }, + + async getMachineBeacon(id: number | string): Promise { + const numericId = Number(id) + const machine = readMachines().find((item) => item.id === numericId) + + if (!machine?.beaconId) return null + + const beacon = readBeacons().find((item) => ( + item.id === machine.beaconId || + item.boundMachineId === numericId + )) + + return beacon ? { ...beacon } : null + }, + + async getMachineStateHistory( + machineId: number | string, + ): Promise { + const numericMachineId = Number(machineId) + + return mockMachineStateHistory + .filter((item) => item.machineId === numericMachineId) + .sort((first, second) => ( + new Date(second.startedAt).getTime() - + new Date(first.startedAt).getTime() + )) + .map(cloneHistoryItem) + }, + + async createMachine(payload: MachinePayload): Promise { + const machines = readMachines() + const nextId = Math.max(0, ...machines.map((machine) => machine.id)) + 1 + const normalizedPayload = normalizeMachinePayload(payload) + const now = new Date().toISOString() + + const machine: Machine = { + id: nextId, + ...normalizedPayload, + beaconId: null, + lastKnownState: 'NO_DATA', + lastDataReceivedAt: null, + createdAt: now, + updatedAt: now, + } + + writeMachines([...machines, machine]) + + return cloneMachine(machine) + }, + + async updateMachine( + id: number | string, + payload: MachinePayload, + ): Promise { + const numericId = Number(id) + const machines = readMachines() + const machineIndex = machines.findIndex((machine) => machine.id === numericId) + + if (machineIndex === -1) { + throw new Error('АТТ не найдена') + } + + const normalizedPayload = normalizeMachinePayload(payload) + + const currentMachine = machines[machineIndex] + const nextMachine: Machine = { + ...currentMachine, + ...normalizedPayload, + updatedAt: new Date().toISOString(), + } + const nextMachines = [...machines] + nextMachines[machineIndex] = nextMachine + + writeMachines(nextMachines) + + return cloneMachine(nextMachine) + }, + + async deleteMachine(id: number | string): Promise { + const numericId = Number(id) + const machines = readMachines() + const nextMachines = machines.filter((machine) => machine.id !== numericId) + + if (nextMachines.length === machines.length) { + throw new Error('АТТ не найдена') + } + + writeMachines(nextMachines) + writeBeacons(updateBeaconBinding(readBeacons(), numericId, null)) + + return true + }, + + async bindBeacon( + machineId: number | string, + beaconId: number | string, + ): Promise { + const numericMachineId = Number(machineId) + const numericBeaconId = Number(beaconId) + const machines = readMachines() + const beacons = readBeacons() + const machineIndex = machines.findIndex((machine) => machine.id === numericMachineId) + + if (machineIndex === -1) { + throw new Error('АТТ не найдена') + } + + assertBeaconCanBeBound(beacons, numericBeaconId, numericMachineId) + + const currentMachine = machines[machineIndex] + const now = new Date().toISOString() + const nextMachine: Machine = { + ...currentMachine, + beaconId: numericBeaconId, + lastKnownState: + currentMachine.lastKnownState === 'NO_DATA' + ? 'IDLE' + : currentMachine.lastKnownState, + lastDataReceivedAt: currentMachine.lastDataReceivedAt ?? now, + updatedAt: now, + } + const nextMachines = [...machines] + nextMachines[machineIndex] = nextMachine + + writeMachines(nextMachines) + writeBeacons(updateBeaconBinding(beacons, numericMachineId, numericBeaconId)) + + return cloneMachine(nextMachine) + }, + + async unbindBeacon(machineId: number | string): Promise { + const numericMachineId = Number(machineId) + const machines = readMachines() + const machineIndex = machines.findIndex((machine) => machine.id === numericMachineId) + + if (machineIndex === -1) { + throw new Error('АТТ не найдена') + } + + const currentMachine = machines[machineIndex] + const nextMachine: Machine = { + ...currentMachine, + beaconId: null, + lastKnownState: 'NO_DATA', + lastDataReceivedAt: null, + updatedAt: new Date().toISOString(), + } + const nextMachines = [...machines] + nextMachines[machineIndex] = nextMachine + + writeMachines(nextMachines) + writeBeacons(updateBeaconBinding(readBeacons(), numericMachineId, null)) + + return cloneMachine(nextMachine) + }, +} diff --git a/frontend/src/entities/machine/lib/machineFormatters.ts b/frontend/src/entities/machine/lib/machineFormatters.ts new file mode 100644 index 0000000..b2e9b73 --- /dev/null +++ b/frontend/src/entities/machine/lib/machineFormatters.ts @@ -0,0 +1,49 @@ +import type { + BeaconBindingStatus, + MachineStateCode, +} from '../model/types' + +export function getMachineStateLabel(state: MachineStateCode) { + if (state === 'IDLE') return 'Стоит' + if (state === 'ENGINE_ON') return 'Заведена' + if (state === 'MOVING') return 'В движении' + if (state === 'NO_DATA') return 'Нет данных' + + return state +} + +export function getMachineStateClass(state: MachineStateCode) { + if (state === 'MOVING') return 'machines-status--blue' + if (state === 'ENGINE_ON') return 'machines-status--green' + if (state === 'NO_DATA') return 'machines-status--red' + + return 'machines-status--gray' +} + +export function getMachineBindingStatus(beaconId?: number | null): BeaconBindingStatus { + return beaconId ? 'BOUND' : 'UNBOUND' +} + +export function getMachineBindingLabel(status: BeaconBindingStatus) { + if (status === 'BOUND') return 'Маяк привязан' + + return 'Маяк не привязан' +} + +export function getMachineBindingClass(status: BeaconBindingStatus) { + return status === 'BOUND' + ? 'machines-status--green' + : 'machines-status--gray' +} + +export function formatMachineDateTime(date?: string | null) { + if (!date) return 'Нет данных' + + return new Intl.DateTimeFormat('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }).format(new Date(date)) +} diff --git a/frontend/src/entities/machine/mock/mockMachineCalibrationRules.ts b/frontend/src/entities/machine/mock/mockMachineCalibrationRules.ts new file mode 100644 index 0000000..ea63c60 --- /dev/null +++ b/frontend/src/entities/machine/mock/mockMachineCalibrationRules.ts @@ -0,0 +1,70 @@ +import type { MachineCalibrationRule } from '../model/types' + +export const mockMachineCalibrationRules: MachineCalibrationRule[] = [ + { + id: 1, + machineId: 1, + stateDefinitionId: 1, + vibrationMin: 0, + vibrationMax: 0.07, + minDurationSeconds: 60, + priority: 30, + createdAt: '2026-07-05T09:00:00.000Z', + updatedAt: '2026-07-05T09:00:00.000Z', + }, + { + id: 2, + machineId: 1, + stateDefinitionId: 2, + vibrationMin: 0.071, + vibrationMax: 0.28, + minDurationSeconds: 60, + priority: 20, + createdAt: '2026-07-05T09:05:00.000Z', + updatedAt: '2026-07-05T09:05:00.000Z', + }, + { + id: 3, + machineId: 1, + stateDefinitionId: 3, + vibrationMin: 0.281, + vibrationMax: 1.2, + minDurationSeconds: 60, + priority: 10, + createdAt: '2026-07-05T09:10:00.000Z', + updatedAt: '2026-07-05T09:10:00.000Z', + }, + { + id: 4, + machineId: 12, + stateDefinitionId: 1, + vibrationMin: 0, + vibrationMax: 0.09, + minDurationSeconds: 60, + priority: 30, + createdAt: '2026-07-05T10:00:00.000Z', + updatedAt: '2026-07-05T10:00:00.000Z', + }, + { + id: 5, + machineId: 12, + stateDefinitionId: 2, + vibrationMin: 0.091, + vibrationMax: 0.42, + minDurationSeconds: 60, + priority: 20, + createdAt: '2026-07-05T10:05:00.000Z', + updatedAt: '2026-07-05T10:05:00.000Z', + }, + { + id: 6, + machineId: 12, + stateDefinitionId: 5, + vibrationMin: 0.421, + vibrationMax: 1.8, + minDurationSeconds: 60, + priority: 10, + createdAt: '2026-07-05T10:10:00.000Z', + updatedAt: '2026-07-05T10:10:00.000Z', + }, +] diff --git a/frontend/src/entities/machine/mock/mockMachineDailyUsageStats.ts b/frontend/src/entities/machine/mock/mockMachineDailyUsageStats.ts new file mode 100644 index 0000000..72aec8c --- /dev/null +++ b/frontend/src/entities/machine/mock/mockMachineDailyUsageStats.ts @@ -0,0 +1,65 @@ +import type { + MachineDailyUsageStat, + MachineStateCode, + MachineTelemetryInterval, +} from '../model/types' +import { mockMachineTelemetryIntervals } from './mockMachineTelemetryIntervals' + +type DailyTotals = Omit + +const emptyStateTotals: Record> = { + MOVING: 'movingSeconds', + ENGINE_ON: 'engineOnSeconds', + IDLE: 'idleSeconds', + NO_DATA: 'noDataSeconds', +} + +function getIntervalDate(interval: MachineTelemetryInterval) { + return interval.startedAt.slice(0, 10) +} + +function getUtilizationPercent(total: DailyTotals) { + const knownSeconds = total.movingSeconds + total.engineOnSeconds + total.idleSeconds + + if (knownSeconds <= 0) return 0 + + return Math.round((total.movingSeconds / knownSeconds) * 100) +} + +function buildDailyUsageStats() { + const statsByMachineAndDate = new Map() + + mockMachineTelemetryIntervals.forEach((interval) => { + const date = getIntervalDate(interval) + const key = `${interval.machineId}-${date}` + const current = statsByMachineAndDate.get(key) ?? { + machineId: interval.machineId, + date, + movingSeconds: 0, + engineOnSeconds: 0, + idleSeconds: 0, + noDataSeconds: 0, + } + + current[emptyStateTotals[interval.state]] += interval.durationSeconds + statsByMachineAndDate.set(key, current) + }) + + return Array.from(statsByMachineAndDate.values()) + .map((total) => ({ + ...total, + utilizationPercent: getUtilizationPercent(total), + })) + .sort((first, second) => { + if (first.machineId !== second.machineId) { + return first.machineId - second.machineId + } + + return first.date.localeCompare(second.date) + }) +} + +export const mockMachineDailyUsageStats: MachineDailyUsageStat[] = buildDailyUsageStats() diff --git a/frontend/src/entities/machine/mock/mockMachineDictionaries.ts b/frontend/src/entities/machine/mock/mockMachineDictionaries.ts new file mode 100644 index 0000000..1830d0c --- /dev/null +++ b/frontend/src/entities/machine/mock/mockMachineDictionaries.ts @@ -0,0 +1,102 @@ +import type { MachineDictionaryOption } from '../model/types' + +export const mockMachineTypes: MachineDictionaryOption[] = [ + { + value: 'Самосвал', + label: 'Самосвал', + description: 'Перевозка породы, грунта и сыпучих материалов', + }, + { + value: 'Погрузчик', + label: 'Погрузчик', + description: 'Погрузка и перемещение материалов на площадке', + }, + { + value: 'Экскаватор', + label: 'Экскаватор', + description: 'Разработка, погрузка и вскрышные работы', + }, + { + value: 'Бульдозер', + label: 'Бульдозер', + description: 'Планировка и подготовка технологических дорог', + }, + { + value: 'Топливозаправщик', + label: 'Топливозаправщик', + description: 'Заправка техники на объектах', + }, + { + value: 'Автокран', + label: 'Автокран', + description: 'Монтажные и ремонтные работы', + }, + { + value: 'Грейдер', + label: 'Грейдер', + description: 'Профилирование и обслуживание дорог', + }, + { + value: 'Автобус', + label: 'Автобус', + description: 'Перевозка персонала', + }, + { + value: 'Пикап', + label: 'Пикап', + description: 'Оперативные выезды и сервисные задачи', + }, + { + value: 'Каток', + label: 'Каток', + description: 'Уплотнение дорожного полотна', + }, +] + +export const mockMachineDepartments: MachineDictionaryOption[] = [ + { + value: 'Карьер Северный', + label: 'Карьер Северный', + description: 'Основной северный участок добычи', + }, + { + value: 'Карьер Южный', + label: 'Карьер Южный', + description: 'Южный участок добычи и отгрузки', + }, + { + value: 'Склад инертных материалов', + label: 'Склад инертных материалов', + description: 'Площадка хранения и отгрузки', + }, + { + value: 'Участок вскрышных работ', + label: 'Участок вскрышных работ', + description: 'Линия вскрыши и подготовки горизонтов', + }, + { + value: 'Дорожный участок', + label: 'Дорожный участок', + description: 'Технологические дороги и проезды', + }, + { + value: 'Сервисная зона', + label: 'Сервисная зона', + description: 'Заправка, осмотр и обслуживание техники', + }, + { + value: 'Ремонтная база', + label: 'Ремонтная база', + description: 'Ремонтные и монтажные работы', + }, + { + value: 'Транспортный участок', + label: 'Транспортный участок', + description: 'Перевозка персонала и грузов', + }, + { + value: 'Служба механиков', + label: 'Служба механиков', + description: 'Оперативное обслуживание парка', + }, +] diff --git a/frontend/src/entities/machine/mock/mockMachineStateDefinitions.ts b/frontend/src/entities/machine/mock/mockMachineStateDefinitions.ts new file mode 100644 index 0000000..0b2a1d3 --- /dev/null +++ b/frontend/src/entities/machine/mock/mockMachineStateDefinitions.ts @@ -0,0 +1,54 @@ +import type { MachineStateDefinition } from '../model/types' + +export const mockMachineStateDefinitions: MachineStateDefinition[] = [ + { + id: 1, + name: 'Стоит', + icon: 'Timer', + color: '#8c96aa', + description: 'АТТ не движется, двигатель не даёт заметной вибрации.', + isSystem: true, + createdAt: '2026-07-01T00:00:00.000Z', + updatedAt: '2026-07-01T00:00:00.000Z', + }, + { + id: 2, + name: 'Заведена', + icon: 'Power', + color: '#ff9f43', + description: 'Двигатель включён, но активного движения нет.', + isSystem: true, + createdAt: '2026-07-01T00:00:00.000Z', + updatedAt: '2026-07-01T00:00:00.000Z', + }, + { + id: 3, + name: 'В движении', + icon: 'Route', + color: '#33b343', + description: 'АТТ движется или выполняет активный рабочий цикл.', + isSystem: true, + createdAt: '2026-07-01T00:00:00.000Z', + updatedAt: '2026-07-01T00:00:00.000Z', + }, + { + id: 4, + name: 'Нет данных', + icon: 'Radio', + color: '#e04f5f', + description: 'Маяк не передавал данные дольше допустимого интервала.', + isSystem: true, + createdAt: '2026-07-01T00:00:00.000Z', + updatedAt: '2026-07-01T00:00:00.000Z', + }, + { + id: 5, + name: 'Работа с нагрузкой', + icon: 'Activity', + color: '#2f80ed', + description: 'Повышенная вибрация при рабочей операции или нагрузке.', + isSystem: false, + createdAt: '2026-07-02T00:00:00.000Z', + updatedAt: '2026-07-02T00:00:00.000Z', + }, +] diff --git a/frontend/src/entities/machine/mock/mockMachineStateHistory.ts b/frontend/src/entities/machine/mock/mockMachineStateHistory.ts new file mode 100644 index 0000000..250865c --- /dev/null +++ b/frontend/src/entities/machine/mock/mockMachineStateHistory.ts @@ -0,0 +1,76 @@ +import type { MachineStateHistoryItem } from '../model/types' + +export const mockMachineStateHistory: MachineStateHistoryItem[] = [ + { + id: 1, + machineId: 1, + state: 'IDLE', + startedAt: '2026-07-09T02:10:00.000Z', + endedAt: '2026-07-09T03:05:00.000Z', + durationSeconds: 3300, + }, + { + id: 2, + machineId: 1, + state: 'ENGINE_ON', + startedAt: '2026-07-09T03:05:00.000Z', + endedAt: '2026-07-09T03:18:00.000Z', + durationSeconds: 780, + }, + { + id: 3, + machineId: 1, + state: 'MOVING', + startedAt: '2026-07-09T03:18:00.000Z', + endedAt: null, + durationSeconds: null, + }, + { + id: 4, + machineId: 2, + state: 'IDLE', + startedAt: '2026-07-09T01:40:00.000Z', + endedAt: '2026-07-09T04:20:00.000Z', + durationSeconds: 9600, + }, + { + id: 5, + machineId: 2, + state: 'ENGINE_ON', + startedAt: '2026-07-09T04:20:00.000Z', + endedAt: null, + durationSeconds: null, + }, + { + id: 6, + machineId: 5, + state: 'ENGINE_ON', + startedAt: '2026-07-09T04:55:00.000Z', + endedAt: '2026-07-09T05:08:00.000Z', + durationSeconds: 780, + }, + { + id: 7, + machineId: 5, + state: 'MOVING', + startedAt: '2026-07-09T05:08:00.000Z', + endedAt: null, + durationSeconds: null, + }, + { + id: 8, + machineId: 9, + state: 'MOVING', + startedAt: '2026-07-07T10:10:00.000Z', + endedAt: '2026-07-07T12:25:00.000Z', + durationSeconds: 8100, + }, + { + id: 9, + machineId: 9, + state: 'NO_DATA', + startedAt: '2026-07-07T12:25:00.000Z', + endedAt: null, + durationSeconds: null, + }, +] diff --git a/frontend/src/entities/machine/mock/mockMachineStates.ts b/frontend/src/entities/machine/mock/mockMachineStates.ts new file mode 100644 index 0000000..a9c4b7d --- /dev/null +++ b/frontend/src/entities/machine/mock/mockMachineStates.ts @@ -0,0 +1,28 @@ +import type { MachineState } from '../model/types' + +export const mockMachineStates: MachineState[] = [ + { + id: 1, + code: 'IDLE', + name: 'Стоит', + description: 'Машина находится без движения, двигатель не активен.', + }, + { + id: 2, + code: 'ENGINE_ON', + name: 'Заведена', + description: 'Двигатель включен, движение не зафиксировано.', + }, + { + id: 3, + code: 'MOVING', + name: 'В движении', + description: 'По данным маяка машина перемещается.', + }, + { + id: 4, + code: 'NO_DATA', + name: 'Нет данных', + description: 'По машине давно не поступали данные.', + }, +] diff --git a/frontend/src/entities/machine/mock/mockMachineTelemetryIntervals.ts b/frontend/src/entities/machine/mock/mockMachineTelemetryIntervals.ts new file mode 100644 index 0000000..4609037 --- /dev/null +++ b/frontend/src/entities/machine/mock/mockMachineTelemetryIntervals.ts @@ -0,0 +1,115 @@ +import type { + MachineStateCode, + MachineTelemetryInterval, +} from '../model/types' + +type TelemetryProfile = 'haul' | 'loader' | 'service' | 'no-data' + +type ScheduleItem = { + state: MachineStateCode + startMinute: number + endMinute: number +} + +const baseDayUtc = Date.UTC(2026, 6, 9) +const dayMs = 24 * 60 * 60 * 1000 +const minuteMs = 60 * 1000 + +const profileSchedules: Record = { + haul: [ + { state: 'NO_DATA', startMinute: 0, endMinute: 35 }, + { state: 'IDLE', startMinute: 35, endMinute: 70 }, + { state: 'ENGINE_ON', startMinute: 70, endMinute: 92 }, + { state: 'MOVING', startMinute: 92, endMinute: 255 }, + { state: 'IDLE', startMinute: 255, endMinute: 305 }, + { state: 'ENGINE_ON', startMinute: 305, endMinute: 326 }, + { state: 'MOVING', startMinute: 326, endMinute: 514 }, + { state: 'IDLE', startMinute: 514, endMinute: 560 }, + { state: 'MOVING', startMinute: 560, endMinute: 670 }, + { state: 'ENGINE_ON', startMinute: 670, endMinute: 692 }, + { state: 'IDLE', startMinute: 692, endMinute: 730 }, + ], + loader: [ + { state: 'NO_DATA', startMinute: 0, endMinute: 25 }, + { state: 'IDLE', startMinute: 25, endMinute: 86 }, + { state: 'ENGINE_ON', startMinute: 86, endMinute: 128 }, + { state: 'MOVING', startMinute: 128, endMinute: 238 }, + { state: 'ENGINE_ON', startMinute: 238, endMinute: 286 }, + { state: 'MOVING', startMinute: 286, endMinute: 390 }, + { state: 'IDLE', startMinute: 390, endMinute: 452 }, + { state: 'ENGINE_ON', startMinute: 452, endMinute: 512 }, + { state: 'MOVING', startMinute: 512, endMinute: 645 }, + { state: 'IDLE', startMinute: 645, endMinute: 720 }, + ], + service: [ + { state: 'NO_DATA', startMinute: 0, endMinute: 45 }, + { state: 'IDLE', startMinute: 45, endMinute: 170 }, + { state: 'ENGINE_ON', startMinute: 170, endMinute: 198 }, + { state: 'MOVING', startMinute: 198, endMinute: 305 }, + { state: 'IDLE', startMinute: 305, endMinute: 438 }, + { state: 'ENGINE_ON', startMinute: 438, endMinute: 476 }, + { state: 'MOVING', startMinute: 476, endMinute: 552 }, + { state: 'IDLE', startMinute: 552, endMinute: 710 }, + ], + 'no-data': [ + { state: 'NO_DATA', startMinute: 0, endMinute: 720 }, + ], +} + +const machineProfiles: Record = { + 1: 'haul', + 2: 'loader', + 3: 'service', + 4: 'no-data', + 5: 'service', + 6: 'service', + 7: 'loader', + 8: 'service', + 9: 'no-data', + 10: 'haul', + 11: 'service', + 12: 'loader', +} + +function toIso(dayOffset: number, minute: number) { + return new Date(baseDayUtc + dayOffset * dayMs + minute * minuteMs).toISOString() +} + +function getMinuteVariation(machineId: number, dayOffset: number) { + return ((machineId * 17 + Math.abs(dayOffset) * 13) % 25) - 12 +} + +function buildIntervals() { + let nextId = 1 + const intervals: MachineTelemetryInterval[] = [] + + Object.entries(machineProfiles).forEach(([machineIdValue, profile]) => { + const machineId = Number(machineIdValue) + + for (let dayOffset = -6; dayOffset <= 0; dayOffset += 1) { + const variation = getMinuteVariation(machineId, dayOffset) + + profileSchedules[profile].forEach((item) => { + const startMinute = Math.max(0, item.startMinute + variation) + const endMinute = Math.min(24 * 60, item.endMinute + variation) + + if (endMinute <= startMinute) return + + intervals.push({ + id: nextId, + machineId, + state: item.state, + startedAt: toIso(dayOffset, startMinute), + endedAt: toIso(dayOffset, endMinute), + durationSeconds: (endMinute - startMinute) * 60, + }) + + nextId += 1 + }) + } + }) + + return intervals +} + +export const mockMachineTelemetryIntervals: MachineTelemetryInterval[] = buildIntervals() diff --git a/frontend/src/entities/machine/mock/mockMachines.ts b/frontend/src/entities/machine/mock/mockMachines.ts new file mode 100644 index 0000000..1196560 --- /dev/null +++ b/frontend/src/entities/machine/mock/mockMachines.ts @@ -0,0 +1,160 @@ +import type { Machine } from '../model/types' + +export const mockMachines: Machine[] = [ + { + id: 1, + name: 'Самосвал БелАЗ 7555', + licensePlate: 'А421ТН 154', + type: 'Самосвал', + department: 'Карьер Северный', + description: 'Основная машина для вывоза породы с северного участка.', + beaconId: 1, + lastKnownState: 'MOVING', + lastDataReceivedAt: '2026-07-09T06:14:00.000Z', + createdAt: '2026-06-01T08:00:00.000Z', + updatedAt: '2026-07-09T06:14:00.000Z', + }, + { + id: 2, + name: 'Погрузчик XCMG LW500', + licensePlate: 'В098КМ 154', + type: 'Погрузчик', + department: 'Склад инертных материалов', + description: 'Работает на площадке отгрузки щебня.', + beaconId: 2, + lastKnownState: 'ENGINE_ON', + lastDataReceivedAt: '2026-07-09T06:06:00.000Z', + createdAt: '2026-06-03T08:30:00.000Z', + updatedAt: '2026-07-09T06:06:00.000Z', + }, + { + id: 3, + name: 'Экскаватор Komatsu PC400', + licensePlate: 'С734ОР 154', + type: 'Экскаватор', + department: 'Участок вскрышных работ', + description: 'Экскаватор на линии вскрыши.', + beaconId: 3, + lastKnownState: 'IDLE', + lastDataReceivedAt: '2026-07-09T05:47:00.000Z', + createdAt: '2026-06-04T09:00:00.000Z', + updatedAt: '2026-07-09T05:47:00.000Z', + }, + { + id: 4, + name: 'Бульдозер ЧТЗ Б10', + licensePlate: 'К512АА 154', + type: 'Бульдозер', + department: 'Дорожный участок', + description: 'Подготовка технологических дорог.', + beaconId: null, + lastKnownState: 'NO_DATA', + lastDataReceivedAt: null, + createdAt: '2026-06-06T10:15:00.000Z', + updatedAt: '2026-06-06T10:15:00.000Z', + }, + { + id: 5, + name: 'Топливозаправщик Урал NEXT', + licensePlate: 'М201ЕР 154', + type: 'Топливозаправщик', + department: 'Сервисная зона', + description: 'Заправка техники на удалённых участках.', + beaconId: 4, + lastKnownState: 'MOVING', + lastDataReceivedAt: '2026-07-09T06:18:00.000Z', + createdAt: '2026-06-08T11:30:00.000Z', + updatedAt: '2026-07-09T06:18:00.000Z', + }, + { + id: 6, + name: 'Автокран КС-55713', + licensePlate: 'Н884ВВ 154', + type: 'Автокран', + department: 'Ремонтная база', + description: 'Кран для монтажных и ремонтных работ.', + beaconId: 5, + lastKnownState: 'IDLE', + lastDataReceivedAt: '2026-07-09T04:50:00.000Z', + createdAt: '2026-06-10T07:45:00.000Z', + updatedAt: '2026-07-09T04:50:00.000Z', + }, + { + id: 7, + name: 'Грейдер ДЗ-98', + licensePlate: 'О337НС 154', + type: 'Грейдер', + department: 'Дорожный участок', + description: 'Планировка и обслуживание карьерных дорог.', + beaconId: 6, + lastKnownState: 'ENGINE_ON', + lastDataReceivedAt: '2026-07-09T06:01:00.000Z', + createdAt: '2026-06-12T08:20:00.000Z', + updatedAt: '2026-07-09T06:01:00.000Z', + }, + { + id: 8, + name: 'Вахтовый автобус НЕФАЗ', + licensePlate: 'Р619ХК 154', + type: 'Автобус', + department: 'Транспортный участок', + description: 'Перевозка сменного персонала.', + beaconId: null, + lastKnownState: 'IDLE', + lastDataReceivedAt: '2026-07-08T18:40:00.000Z', + createdAt: '2026-06-14T13:10:00.000Z', + updatedAt: '2026-07-08T18:40:00.000Z', + }, + { + id: 9, + name: 'Самосвал HOWO ZZ3407', + licensePlate: 'Т054КВ 154', + type: 'Самосвал', + department: 'Карьер Южный', + description: 'Резервная машина южного борта.', + beaconId: 7, + lastKnownState: 'NO_DATA', + lastDataReceivedAt: '2026-07-07T12:25:00.000Z', + createdAt: '2026-06-16T09:25:00.000Z', + updatedAt: '2026-07-07T12:25:00.000Z', + }, + { + id: 10, + name: 'Экскаватор Hitachi ZX470', + licensePlate: 'У771МХ 154', + type: 'Экскаватор', + department: 'Карьер Южный', + description: 'Работает на погрузке породы.', + beaconId: 8, + lastKnownState: 'MOVING', + lastDataReceivedAt: '2026-07-09T06:11:00.000Z', + createdAt: '2026-06-18T10:00:00.000Z', + updatedAt: '2026-07-09T06:11:00.000Z', + }, + { + id: 11, + name: 'Пикап УАЗ Профи', + licensePlate: 'Х315АМ 154', + type: 'Пикап', + department: 'Служба механиков', + description: 'Оперативные выезды механиков по территории.', + beaconId: 10, + lastKnownState: 'IDLE', + lastDataReceivedAt: '2026-07-09T11:40:00.000Z', + createdAt: '2026-06-20T12:30:00.000Z', + updatedAt: '2026-07-09T11:40:00.000Z', + }, + { + id: 12, + name: 'Каток Hamm 3412', + licensePlate: 'Е402РВ 154', + type: 'Каток', + department: 'Дорожный участок', + description: 'Уплотнение технологических дорог после отсыпки.', + beaconId: 9, + lastKnownState: 'ENGINE_ON', + lastDataReceivedAt: '2026-07-09T05:58:00.000Z', + createdAt: '2026-06-22T09:40:00.000Z', + updatedAt: '2026-07-09T05:58:00.000Z', + }, +] diff --git a/frontend/src/entities/machine/model/types.ts b/frontend/src/entities/machine/model/types.ts new file mode 100644 index 0000000..5900661 --- /dev/null +++ b/frontend/src/entities/machine/model/types.ts @@ -0,0 +1,126 @@ +export type MachineStateCode = 'IDLE' | 'ENGINE_ON' | 'MOVING' | 'NO_DATA' + +export type BeaconBindingStatus = 'BOUND' | 'UNBOUND' + +export type Machine = { + id: number + name: string + licensePlate: string + type: string + department: string + description: string + beaconId: number | null + lastKnownState: MachineStateCode + lastDataReceivedAt: string | null + createdAt: string + updatedAt: string +} + +export type MachineState = { + id: number + code: MachineStateCode + name: string + description: string +} + +export type MachineStateHistoryItem = { + id: number + machineId: number + state: MachineStateCode + startedAt: string + endedAt: string | null + durationSeconds: number | null +} + +export type MachineSortField = 'ID' | 'Name' | 'Date' + +export type MachineSortDirection = 'ASC' | 'DESC' + +export type MachineDictionaryOption = { + value: string + label: string + description?: string +} + +export type MachineAnalyticsPeriod = 'today' | 'shift' | 'week' | 'month' + +export type MachineAnalyticsDateRange = { + dateFrom?: string + dateTo?: string +} + +export type MachineTelemetryInterval = { + id: number + machineId: number + state: MachineStateCode + startedAt: string + endedAt: string + durationSeconds: number +} + +export type MachineDailyUsageStat = { + machineId: number + date: string + movingSeconds: number + engineOnSeconds: number + idleSeconds: number + noDataSeconds: number + utilizationPercent: number +} + +export type MachineUsageSummary = { + machineId: number + period: MachineAnalyticsPeriod + movingSeconds: number + engineOnSeconds: number + idleSeconds: number + noDataSeconds: number + totalKnownSeconds: number + utilizationPercent: number +} + +export type MachineCalibrationPeriodPreset = + 'today' | 'yesterday' | 'week' | 'month' | 'custom' + +export type MachineCalibrationPeriod = { + preset: MachineCalibrationPeriodPreset + periodStart: Date + periodEnd: Date +} + +export type MachineStateDefinition = { + id: number + name: string + icon: string + color: string + description: string + isSystem: boolean + createdAt: string + updatedAt: string +} + +export type MachineCalibrationRule = { + id: number + machineId: number + stateDefinitionId: number + vibrationMin: number + vibrationMax: number + minDurationSeconds: number + priority: number + createdAt: string + updatedAt: string +} + +export type RecognizedMachineStateInterval = { + id: number + machineId: number + stateDefinitionId: number | null + startedAt: string + endedAt: string + durationSeconds: number + vibrationMin: number | null + vibrationMax: number | null + vibrationAvg: number | null + sampleCount: number + isNoData: boolean +} diff --git a/frontend/src/entities/report/api/fleetReportSchedulesService.ts b/frontend/src/entities/report/api/fleetReportSchedulesService.ts new file mode 100644 index 0000000..a47aa6d --- /dev/null +++ b/frontend/src/entities/report/api/fleetReportSchedulesService.ts @@ -0,0 +1,178 @@ +import { mockFleetReportSchedules } from '../mock/mockFleetReportSchedules' +import type { + FleetReportFormat, + FleetReportPeriod, + FleetReportSchedule, +} from '../model/types' + +const FLEET_REPORT_SCHEDULES_STORAGE_KEY = 'fleet-ble-monitor.fleetReportSchedules' +const defaultTimezone = 'Asia/Novosibirsk' +const defaultPeriod: FleetReportPeriod = 'PREVIOUS_DAY' + +export type CreateFleetReportSchedulePayload = { + recipientUserIds: number[] + reportFormat: FleetReportFormat + sendTime: string + enabled: boolean +} + +function cloneSchedule(schedule: FleetReportSchedule): FleetReportSchedule { + return { + ...schedule, + recipientUserIds: [...schedule.recipientUserIds], + } +} + +function readSchedules(): FleetReportSchedule[] { + if (typeof window === 'undefined') { + return mockFleetReportSchedules.map(cloneSchedule) + } + + const storedSchedules = window.localStorage.getItem( + FLEET_REPORT_SCHEDULES_STORAGE_KEY, + ) + + if (!storedSchedules) { + const initialSchedules = mockFleetReportSchedules.map(cloneSchedule) + window.localStorage.setItem( + FLEET_REPORT_SCHEDULES_STORAGE_KEY, + JSON.stringify(initialSchedules), + ) + + return initialSchedules + } + + try { + const parsedSchedules = JSON.parse(storedSchedules) as FleetReportSchedule[] + + return parsedSchedules.map(cloneSchedule) + } catch { + const initialSchedules = mockFleetReportSchedules.map(cloneSchedule) + window.localStorage.setItem( + FLEET_REPORT_SCHEDULES_STORAGE_KEY, + JSON.stringify(initialSchedules), + ) + + return initialSchedules + } +} + +function writeSchedules(schedules: FleetReportSchedule[]) { + if (typeof window === 'undefined') return + + window.localStorage.setItem( + FLEET_REPORT_SCHEDULES_STORAGE_KEY, + JSON.stringify(schedules), + ) + window.dispatchEvent(new Event('fleet-report-schedules:change')) +} + +function assertValidSendTime(sendTime: string) { + if (!/^\d{2}:\d{2}$/.test(sendTime)) { + throw new Error('Укажите время отправки') + } + + const [hours, minutes] = sendTime.split(':').map(Number) + + if (hours > 23 || minutes > 59) { + throw new Error('Укажите корректное время отправки') + } +} + +export const fleetReportSchedulesService = { + async getSchedules(): Promise { + return readSchedules().map(cloneSchedule) + }, + + async createSchedule( + payload: CreateFleetReportSchedulePayload, + ): Promise { + const recipientUserIds = Array.from(new Set(payload.recipientUserIds)) + .filter((userId) => Number.isFinite(userId)) + + if (recipientUserIds.length === 0) { + throw new Error('Выберите хотя бы одного получателя') + } + + assertValidSendTime(payload.sendTime) + + const schedules = readSchedules() + const now = new Date().toISOString() + const nextId = Math.max(0, ...schedules.map((schedule) => schedule.id)) + 1 + const schedule: FleetReportSchedule = { + id: nextId, + name: 'Ежедневный отчет по парку АТТ', + recipientUserIds, + reportFormat: payload.reportFormat, + sendTime: payload.sendTime, + period: defaultPeriod, + timezone: defaultTimezone, + enabled: payload.enabled, + createdAt: now, + updatedAt: now, + } + + writeSchedules([...schedules, schedule]) + + return cloneSchedule(schedule) + }, + + async updateSchedule( + id: number | string, + payload: Partial, + ): Promise { + const numericId = Number(id) + const schedules = readSchedules() + const scheduleIndex = schedules.findIndex((schedule) => schedule.id === numericId) + + if (scheduleIndex === -1) { + throw new Error('Рассылка отчета не найдена') + } + + if (payload.sendTime) { + assertValidSendTime(payload.sendTime) + } + + const currentSchedule = schedules[scheduleIndex] + const recipientUserIds = payload.recipientUserIds + ? Array.from(new Set(payload.recipientUserIds)) + .filter((userId) => Number.isFinite(userId)) + : currentSchedule.recipientUserIds + + if (recipientUserIds.length === 0) { + throw new Error('Выберите хотя бы одного получателя') + } + + const nextSchedule: FleetReportSchedule = { + ...currentSchedule, + ...payload, + id: currentSchedule.id, + name: currentSchedule.name, + recipientUserIds, + period: currentSchedule.period, + timezone: currentSchedule.timezone, + createdAt: currentSchedule.createdAt, + updatedAt: new Date().toISOString(), + } + const nextSchedules = [...schedules] + nextSchedules[scheduleIndex] = nextSchedule + writeSchedules(nextSchedules) + + return cloneSchedule(nextSchedule) + }, + + async deleteSchedule(id: number | string): Promise { + const numericId = Number(id) + const schedules = readSchedules() + const nextSchedules = schedules.filter((schedule) => schedule.id !== numericId) + + if (nextSchedules.length === schedules.length) { + throw new Error('Рассылка отчета не найдена') + } + + writeSchedules(nextSchedules) + + return true + }, +} + diff --git a/frontend/src/entities/report/api/fleetReportService.ts b/frontend/src/entities/report/api/fleetReportService.ts new file mode 100644 index 0000000..5e5a078 --- /dev/null +++ b/frontend/src/entities/report/api/fleetReportService.ts @@ -0,0 +1,449 @@ +import { machineAnalyticsService } from '../../machine/api/machineAnalyticsService' +import { machinesService } from '../../machine/api/machinesService' +import type { + Machine, + MachineStateCode, + MachineTelemetryInterval, + MachineUsageSummary, +} from '../../machine/model/types' +import type { + FleetDailyReport, + FleetDailyReportHistoryRow, + FleetDailyReportMachineRow, + FleetReportFormat, + GeneratedFleetReport, +} from '../model/types' + +const dayMs = 24 * 60 * 60 * 1000 + +function getStateLabel(state: MachineStateCode) { + if (state === 'IDLE') return 'Стоит' + if (state === 'ENGINE_ON') return 'Заведена' + if (state === 'MOVING') return 'В движении' + if (state === 'NO_DATA') return 'Нет данных' + + return state +} + +function formatDate(date: string | number | Date) { + return new Intl.DateTimeFormat('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }).format(new Date(date)) +} + +function formatDateTime(date?: string | number | null) { + if (!date) return 'Нет данных' + + return new Intl.DateTimeFormat('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }).format(new Date(date)) +} + +function formatDuration(totalSeconds: number) { + const normalizedSeconds = Math.max(0, Math.floor(totalSeconds)) + const totalMinutes = Math.floor(normalizedSeconds / 60) + const hours = Math.floor(totalMinutes / 60) + const minutes = totalMinutes % 60 + + if (hours > 0) return `${hours} ч ${minutes} мин` + if (totalMinutes > 0) return `${totalMinutes} мин` + + return `${normalizedSeconds} сек` +} + +function formatPercent(value: number) { + return `${Math.round(value)}%` +} + +function escapeHtml(value: string | number | null | undefined) { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", ''') +} + +function normalizeDateValue(date: Date) { + return date.toISOString().slice(0, 10) +} + +function getDayRange(dateValue: string) { + const start = new Date(`${dateValue}T00:00:00.000Z`).getTime() + + return { + startIso: new Date(start).toISOString(), + endIso: new Date(start + dayMs).toISOString(), + } +} + +async function getAllMachines() { + const response = await machinesService.getMachines({ + page: 0, + pageSize: 1000, + sortDirection: 'ASC', + sortField: 'Name', + }) + + return response.page +} + +function getFallbackReportDate(machines: Machine[]) { + const latestDataTimestamp = Math.max( + 0, + ...machines + .map((machine) => ( + machine.lastDataReceivedAt + ? new Date(machine.lastDataReceivedAt).getTime() + : 0 + )), + ) + + if (latestDataTimestamp > 0) { + return normalizeDateValue(new Date(latestDataTimestamp)) + } + + return normalizeDateValue(new Date(Date.now() - dayMs)) +} + +function getMachineRow( + machine: Machine, + summary: MachineUsageSummary, + intervals: MachineTelemetryInterval[], +): FleetDailyReportMachineRow { + return { + machineId: machine.id, + name: machine.name, + licensePlate: machine.licensePlate, + type: machine.type, + department: machine.department, + beaconBound: Boolean(machine.beaconId), + lastKnownState: getStateLabel(machine.lastKnownState), + lastDataReceivedAt: formatDateTime(machine.lastDataReceivedAt), + movingSeconds: summary.movingSeconds, + engineOnSeconds: summary.engineOnSeconds, + idleSeconds: summary.idleSeconds, + noDataSeconds: summary.noDataSeconds, + stateChangesCount: intervals.length, + utilizationPercent: summary.utilizationPercent, + } +} + +function getHistoryRows( + machine: Machine, + intervals: MachineTelemetryInterval[], +): FleetDailyReportHistoryRow[] { + return intervals.map((interval) => ({ + machineId: machine.id, + machineName: machine.name, + licensePlate: machine.licensePlate, + startedAt: formatDateTime(interval.startedAt), + endedAt: formatDateTime(interval.endedAt), + state: getStateLabel(interval.state), + durationSeconds: interval.durationSeconds, + })) +} + +function getReportSummary( + machines: FleetDailyReportMachineRow[], +): FleetDailyReport['summary'] { + return machines.reduce( + (summary, machine) => ({ + totalMachines: summary.totalMachines + 1, + boundMachines: summary.boundMachines + (machine.beaconBound ? 1 : 0), + unboundMachines: summary.unboundMachines + (machine.beaconBound ? 0 : 1), + machinesWithMovement: + summary.machinesWithMovement + (machine.movingSeconds > 0 ? 1 : 0), + machinesWithoutData: + summary.machinesWithoutData + + ( + machine.movingSeconds + + machine.engineOnSeconds + + machine.idleSeconds === 0 + ? 1 + : 0 + ), + movingSeconds: summary.movingSeconds + machine.movingSeconds, + engineOnSeconds: summary.engineOnSeconds + machine.engineOnSeconds, + idleSeconds: summary.idleSeconds + machine.idleSeconds, + noDataSeconds: summary.noDataSeconds + machine.noDataSeconds, + stateChangesCount: summary.stateChangesCount + machine.stateChangesCount, + }), + { + totalMachines: 0, + boundMachines: 0, + unboundMachines: 0, + machinesWithMovement: 0, + machinesWithoutData: 0, + movingSeconds: 0, + engineOnSeconds: 0, + idleSeconds: 0, + noDataSeconds: 0, + stateChangesCount: 0, + }, + ) +} + +function renderSummaryTable(report: FleetDailyReport) { + const rows = [ + ['Дата отчета', formatDate(`${report.date}T00:00:00.000Z`)], + ['Сформирован', formatDateTime(report.generatedAt)], + ['Всего АТТ', report.summary.totalMachines], + ['С маяками', report.summary.boundMachines], + ['Без маяков', report.summary.unboundMachines], + ['АТТ в движении', report.summary.machinesWithMovement], + ['АТТ без данных', report.summary.machinesWithoutData], + ['В движении', formatDuration(report.summary.movingSeconds)], + ['Заведена', formatDuration(report.summary.engineOnSeconds)], + ['Стояла', formatDuration(report.summary.idleSeconds)], + ['Без данных', formatDuration(report.summary.noDataSeconds)], + ['Смен состояний', report.summary.stateChangesCount], + ] + + return ` + + + ${rows.map((row) => ` + + + + + `).join('')} + +
${escapeHtml(row[0])}${escapeHtml(row[1])}
+ ` +} + +function renderMachineTable(report: FleetDailyReport) { + return ` + + + + + + + + + + + + + + + + + + + + ${report.machines.map((machine) => ` + + + + + + + + + + + + + + + + `).join('')} + +
АТТГосномерТипПодразделениеМаякПоследний статусПоследние данныеВ движенииЗаведенаСтоялаБез данныхСмен состоянийКПИ
${escapeHtml(machine.name)}${escapeHtml(machine.licensePlate)}${escapeHtml(machine.type)}${escapeHtml(machine.department)}${machine.beaconBound ? 'Привязан' : 'Не привязан'}${escapeHtml(machine.lastKnownState)}${escapeHtml(machine.lastDataReceivedAt)}${escapeHtml(formatDuration(machine.movingSeconds))}${escapeHtml(formatDuration(machine.engineOnSeconds))}${escapeHtml(formatDuration(machine.idleSeconds))}${escapeHtml(formatDuration(machine.noDataSeconds))}${machine.stateChangesCount}${escapeHtml(formatPercent(machine.utilizationPercent))}
+ ` +} + +function renderHistoryTable(report: FleetDailyReport) { + return ` + + + + + + + + + + + + + ${report.history.map((item) => ` + + + + + + + + + `).join('')} + +
АТТГосномерНачалоКонецСостояниеДлительность
${escapeHtml(item.machineName)}${escapeHtml(item.licensePlate)}${escapeHtml(item.startedAt)}${escapeHtml(item.endedAt)}${escapeHtml(item.state)}${escapeHtml(formatDuration(item.durationSeconds))}
+ ` +} + +function renderReportHtml(report: FleetDailyReport, mode: 'excel' | 'print') { + return ` + + + + Отчет по парку АТТ за ${escapeHtml(formatDate(`${report.date}T00:00:00.000Z`))} + + + +

Отчет по парку АТТ

+

Период: ${escapeHtml(formatDate(`${report.date}T00:00:00.000Z`))}. Сформирован: ${escapeHtml(formatDateTime(report.generatedAt))}.

+ +

Сводка

+ ${renderSummaryTable(report)} + +

АТТ

+ ${renderMachineTable(report)} + +

История состояний

+ ${renderHistoryTable(report)} + + + ` +} + +function getExcelBlob(report: FleetDailyReport) { + const html = renderReportHtml(report, 'excel') + + return new Blob([html], { + type: 'application/vnd.ms-excel;charset=utf-8', + }) +} + +export const fleetReportService = { + async getDefaultDailyReportDate(): Promise { + const machines = await getAllMachines() + + return getFallbackReportDate(machines) + }, + + async buildDailyReport(date?: string): Promise { + const machines = await getAllMachines() + const reportDate = date ?? getFallbackReportDate(machines) + const range = getDayRange(reportDate) + const machineRows: FleetDailyReportMachineRow[] = [] + const historyRows: FleetDailyReportHistoryRow[] = [] + + for (const machine of machines) { + const [summary, intervals] = await Promise.all([ + machineAnalyticsService.getMachineUsageSummary(machine.id, 'today', { + dateFrom: range.startIso, + dateTo: range.endIso, + }), + machineAnalyticsService.getMachineTelemetryIntervals(machine.id, 'today', { + dateFrom: range.startIso, + dateTo: range.endIso, + }), + ]) + + machineRows.push(getMachineRow(machine, summary, intervals)) + historyRows.push(...getHistoryRows(machine, intervals)) + } + + return { + date: reportDate, + generatedAt: new Date().toISOString(), + summary: getReportSummary(machineRows), + machines: machineRows, + history: historyRows, + } + }, + + async generateDailyReport( + format: FleetReportFormat, + date?: string, + ): Promise { + const report = await this.buildDailyReport(date) + + if (format === 'PDF') { + return { + fileName: `fleet-att-daily-report-${report.date}.pdf`, + mimeType: 'text/html;charset=utf-8', + delivery: 'print', + html: renderReportHtml(report, 'print'), + } + } + + return { + fileName: `fleet-att-daily-report-${report.date}.xls`, + mimeType: 'application/vnd.ms-excel;charset=utf-8', + delivery: 'download', + blob: getExcelBlob(report), + } + }, +} + diff --git a/frontend/src/entities/report/mock/mockFleetReportSchedules.ts b/frontend/src/entities/report/mock/mockFleetReportSchedules.ts new file mode 100644 index 0000000..17e55c2 --- /dev/null +++ b/frontend/src/entities/report/mock/mockFleetReportSchedules.ts @@ -0,0 +1,4 @@ +import type { FleetReportSchedule } from '../model/types' + +export const mockFleetReportSchedules: FleetReportSchedule[] = [] + diff --git a/frontend/src/entities/report/model/types.ts b/frontend/src/entities/report/model/types.ts new file mode 100644 index 0000000..edb5ca4 --- /dev/null +++ b/frontend/src/entities/report/model/types.ts @@ -0,0 +1,70 @@ +export type FleetReportFormat = 'PDF' | 'XLSX' + +export type FleetReportPeriod = 'PREVIOUS_DAY' + +export type FleetReportSchedule = { + id: number + name: string + recipientUserIds: number[] + reportFormat: FleetReportFormat + sendTime: string + period: FleetReportPeriod + timezone: string + enabled: boolean + createdAt: string + updatedAt: string +} + +export type FleetDailyReportMachineRow = { + machineId: number + name: string + licensePlate: string + type: string + department: string + beaconBound: boolean + lastKnownState: string + lastDataReceivedAt: string + movingSeconds: number + engineOnSeconds: number + idleSeconds: number + noDataSeconds: number + stateChangesCount: number + utilizationPercent: number +} + +export type FleetDailyReportHistoryRow = { + machineId: number + machineName: string + licensePlate: string + startedAt: string + endedAt: string + state: string + durationSeconds: number +} + +export type FleetDailyReport = { + date: string + generatedAt: string + summary: { + totalMachines: number + boundMachines: number + unboundMachines: number + machinesWithMovement: number + machinesWithoutData: number + movingSeconds: number + engineOnSeconds: number + idleSeconds: number + noDataSeconds: number + stateChangesCount: number + } + machines: FleetDailyReportMachineRow[] + history: FleetDailyReportHistoryRow[] +} + +export type GeneratedFleetReport = { + fileName: string + mimeType: string + delivery: 'download' | 'print' + blob?: Blob + html?: string +} diff --git a/frontend/src/features/auth/api/auth.graphql.ts b/frontend/src/features/auth/api/auth.graphql.ts new file mode 100644 index 0000000..c76f24a --- /dev/null +++ b/frontend/src/features/auth/api/auth.graphql.ts @@ -0,0 +1,45 @@ +import { gql } from '@apollo/client' + +export const SIGN_IN_MUTATION = gql` + mutation SignIn($username: String!, $password: String!) { + signIn(username: $username, password: $password) { + id + role + } + } +` + +export const REFRESH_SESSION_MUTATION = gql` + mutation RefreshSession { + refreshSession { + id + firstName + lastName + middleName + orgId + role + org { + id + name + } + } + } +` + +export const CURRENT_USER_QUERY = gql` + query CurrentUser { + currentUser { + avatarUrl + id + role + firstName + middleName + lastName + username + org { + id + name + } + } + } +` \ No newline at end of file diff --git a/frontend/src/features/auth/api/authService.ts b/frontend/src/features/auth/api/authService.ts new file mode 100644 index 0000000..fa08ecc --- /dev/null +++ b/frontend/src/features/auth/api/authService.ts @@ -0,0 +1,181 @@ +import { apolloClient } from '../../../shared/api/apolloClient' +import { + clearAuthStorage, + CURRENT_USER_STORAGE_KEY, + getStoredAccessToken, + getStoredRefreshToken, + saveAuthTokens, +} from '../../../shared/api/authStorage' +import { + CURRENT_USER_QUERY, + LOGIN_MUTATION, + LOGOUT_MUTATION, + REFRESH_SESSION_MUTATION, +} from '../../../shared/api/graphql/operations/auth' +import type { Employee } from '../../../entities/employee/model/types' +import type { + CurrentUserQuery, + LoginMutation, + RefreshSessionMutation, +} from '../../../shared/api/graphql/generated/graphql' + +type AuthUser = + | LoginMutation['login']['user'] + | RefreshSessionMutation['refreshSession']['user'] + | NonNullable + +function toCurrentUser(user: AuthUser): Employee { + return { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + middleName: user.middleName ?? '', + email: user.email, + login: user.login, + username: user.login, + createdAt: user.createdAt, + avatarUrl: null, + orgId: 0, + role: user.role, + org: null, + } +} + +function saveCurrentUser(user: Employee) { + if (typeof window === 'undefined') return + + window.localStorage.setItem(CURRENT_USER_STORAGE_KEY, JSON.stringify(user)) +} + +function emitAuthChange() { + if (typeof window === 'undefined') return + + window.dispatchEvent(new Event('auth:change')) +} + +function emitLogout() { + if (typeof window === 'undefined') return + + window.dispatchEvent(new Event('auth:logout')) +} + +async function refreshCurrentSession(): Promise { + const refreshToken = getStoredRefreshToken() + + if (!refreshToken) return null + + const { data } = await apolloClient.mutate({ + mutation: REFRESH_SESSION_MUTATION, + variables: { + payload: { + refreshToken, + }, + }, + fetchPolicy: 'no-cache', + }) + + if (!data?.refreshSession) return null + + saveAuthTokens( + data.refreshSession.accessToken, + data.refreshSession.refreshToken, + ) + + const user = toCurrentUser(data.refreshSession.user) + saveCurrentUser(user) + + return user +} + +export const authService = { + async login(loginOrEmail: string, password: string): Promise { + const { data } = await apolloClient.mutate({ + mutation: LOGIN_MUTATION, + variables: { + payload: { + loginOrEmail, + password, + }, + }, + fetchPolicy: 'no-cache', + }) + + if (!data?.login) { + throw new Error('Не удалось войти') + } + + saveAuthTokens(data.login.accessToken, data.login.refreshToken) + + const user = toCurrentUser(data.login.user) + saveCurrentUser(user) + emitAuthChange() + + return user + }, + + logout() { + const accessToken = getStoredAccessToken() + + if (accessToken) { + void apolloClient.mutate({ + mutation: LOGOUT_MUTATION, + context: { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + fetchPolicy: 'no-cache', + }).catch(() => undefined) + } + + clearAuthStorage() + void apolloClient.clearStore() + emitLogout() + emitAuthChange() + }, + + async getCurrentUser(): Promise { + if (typeof window === 'undefined') return null + + if (!getStoredAccessToken() && !getStoredRefreshToken()) { + clearAuthStorage() + + return null + } + + try { + const { data } = await apolloClient.query({ + query: CURRENT_USER_QUERY, + fetchPolicy: 'no-cache', + }) + + if (data?.currentUser) { + const user = toCurrentUser(data.currentUser) + saveCurrentUser(user) + + return user + } + + const refreshedUser = await refreshCurrentSession() + + if (refreshedUser) { + emitAuthChange() + + return refreshedUser + } + + clearAuthStorage() + + return null + } catch { + clearAuthStorage() + emitLogout() + + return null + } + }, + + isAdmin(user: Employee | null) { + return user?.role === 'ADMIN' || user?.role === 'Admin' + }, +} diff --git a/frontend/src/features/auth/ui/AuthGate.tsx b/frontend/src/features/auth/ui/AuthGate.tsx new file mode 100644 index 0000000..64f89c5 --- /dev/null +++ b/frontend/src/features/auth/ui/AuthGate.tsx @@ -0,0 +1,67 @@ +import { useEffect, useState } from 'react' +import type { ReactNode } from 'react' + +import type { Employee } from '../../../entities/employee/model/types' +import { LoginPage } from '../../../pages/LoginPage/LoginPage' +import { authService } from '../api/authService' + +type AuthGateProps = { + children: ReactNode +} + +export function AuthGate({ children }: AuthGateProps) { + const [currentUser, setCurrentUser] = useState(null) + const [isCheckingAuth, setIsCheckingAuth] = useState(true) + + useEffect(() => { + let isMounted = true + + async function syncCurrentUser() { + const user = await authService.getCurrentUser() + + if (!isMounted) return + + setCurrentUser(user) + setIsCheckingAuth(false) + } + + syncCurrentUser() + + function handleAuthChange() { + setIsCheckingAuth(true) + syncCurrentUser() + } + + function handleLogout() { + setCurrentUser(null) + setIsCheckingAuth(false) + } + + window.addEventListener('auth:change', handleAuthChange) + window.addEventListener('auth:logout', handleLogout) + window.addEventListener('users:change', handleAuthChange) + + return () => { + isMounted = false + window.removeEventListener('auth:change', handleAuthChange) + window.removeEventListener('auth:logout', handleLogout) + window.removeEventListener('users:change', handleAuthChange) + } + }, []) + + if (isCheckingAuth) { + return
Проверка авторизации...
+ } + + if (!currentUser) { + return ( + { + setCurrentUser(await authService.getCurrentUser()) + }} + /> + ) + } + + return children +} diff --git a/frontend/src/index.scss b/frontend/src/index.scss new file mode 100644 index 0000000..134f995 --- /dev/null +++ b/frontend/src/index.scss @@ -0,0 +1,116 @@ +@use './shared/styles/variables' as *; +@use './shared/styles/themes'; +@use './shared/styles/table-surfaces'; + +@font-face { + font-family: 'Montserrat'; + src: url('./assets/fonts/Montserrat-VariableFont_wght.ttf') format('truetype'); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + +:root { + --sans: 'Montserrat', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;; + --heading: 'Montserrat', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;; + --mono: 'Montserrat', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;; + + font: var(--font-size-18) / 145% var(--sans); + letter-spacing: 0.18px; + color: var(--text); + background: $color-bg; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + @media (max-width: 1024px) { + font-size: $font-size-16; + } +} + +#root { + //width: 1126px; + max-width: 100%; + margin: 0 auto; + text-align: center; + border-inline: 1px solid var(--border); + min-height: 100svh; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +body { + margin: 0; + background: $color-bg; +} +html{ + background: $color-bg; +} + +::view-transition-old(root), +::view-transition-new(root) { + animation: none; + mix-blend-mode: normal; +} + +html.theme-transition-no-css-transitions *, +html.theme-transition-no-css-transitions *::before, +html.theme-transition-no-css-transitions *::after { + transition: none !important; +} + +button{ + font-family: inherit; +} +a{ + text-decoration: none; +} + +.simplebar-content{ + height: 100%; +} + +h1, +h2 { + font-family: var(--heading); + font-weight: 500; + color: $color-text-strong; +} + +h1 { + font-size: $font-size-32; + margin: 0; + @media (max-width: 1024px) { + font-size: $font-size-36; + margin: 20px 0; + } +} +h2 { + font-size: $font-size-24; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + @media (max-width: 1024px) { + font-size: $font-size-20; + } +} +p { + margin: 0; +} + +code, +.counter { + font-family: var(--mono); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); +} + +code { + font-size: $font-size-15; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..d132382 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,34 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.scss' +import 'react-day-picker/style.css' +import 'leaflet/dist/leaflet.css' +import { RouterProvider } from 'react-router-dom' +import { ApolloProvider } from '@apollo/client/react' +import { LazyMotion, domAnimation } from 'framer-motion' +import { apolloClient } from './shared/api/apolloClient' +import { router } from './app/router/router.tsx' +import { AuthGate } from './features/auth/ui/AuthGate' +import { initTheme } from './shared/theme/theme' + +const isAuthEnabled = import.meta.env.VITE_AUTH_ENABLED === 'true' + +initTheme() + +const app = isAuthEnabled ? ( + + + +) : ( + +) + +createRoot(document.getElementById('root')!).render( + + + + {app} + + + , +) diff --git a/frontend/src/pages/DevicePage/DevicePage.scss b/frontend/src/pages/DevicePage/DevicePage.scss new file mode 100644 index 0000000..10e7f05 --- /dev/null +++ b/frontend/src/pages/DevicePage/DevicePage.scss @@ -0,0 +1,871 @@ +@use '../../shared/styles/variables' as *; + +.device-page { + display: flex; + flex: 1; + min-height: calc(80% - 40px); + max-height: calc(100% - 40px); + height: auto; + flex-direction: column; + gap: 18px; + overflow: visible; + padding: 20px 20px 36px 20px; +} + +.device-page--apps { + flex: 1 1 0; + box-sizing: border-box; + min-width: 0; + min-height: 0; + height: 100%; + max-height: 100%; + overflow: visible; + + .device-page__motion-view { + overflow: visible; + } + + .device-page__apps-motion { + display: flex; + flex: 1 1 0; + min-width: 0; + min-height: 0; + height: 100%; + max-height: 100%; + flex-direction: column; + } + + .device-apps-view { + min-height: 0; + height: 100%; + } +} + +.device-page__grid { + display: grid; + grid-template-columns: 340px 340px minmax(360px, 1fr); + gap: 20px; + align-items: start; + width: 100%; + //min-height: 100%; +} + +.device-page__motion-view { + display: flex; + flex: 1 0 auto; + min-height: 0; + flex-direction: column; +} + +.device-card { + border-radius: 20px; + background: $color-surface; + backdrop-filter: blur(22px); + padding: 20px; + box-shadow: $shadow-card; +} + +.device-card--main { + grid-column: span 2; +} + +.device-page__details-row { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 20px; + align-items: start; + + >.device-card, + >.device-card-stats { + min-width: 0; + } +} + +.device-main { + display: flex; + flex-direction: row; + gap: 20px; +} + +.device-main__image { + height: calc(50% - 40px); + width: 27.1%; + padding: 20px; + border-radius: 14px; + background: $color-surface-subtle; + + display: flex; + align-items: center; + justify-content: center; + + background-color: $color-bg; + + img { + max-width: 100%; + max-height: 100%; + height: 100%; + width: auto; + object-fit: contain; + } +} + +.device-main__info { + display: flex; + flex-direction: column; + flex: 1; + text-align: left; + + h2 { + margin: 0 0 6px; + color: $color-text-strong; + font-size: $font-size-30; + font-weight: 600; + line-height: 1.1; + } + + p { + margin: 0; + color: $gray50; + font-size: $font-size-17; + font-weight: 500; + line-height: 1.3; + } +} + +.device-main__divider { + height: 1px; + margin: 12px 0 14px; + background: $gray20; +} + +.device-main__statuses { + display: flex; + flex-direction: column; + gap: 4px; +} + +.device-status { + display: inline-flex; + align-items: center; + gap: 9px; + + color: $color-text-strong; + font-size: $font-size-18; + font-weight: 500; + + svg { + padding: 4px; + width: 24px; + height: 24px; + border-radius: 8px; + background: $color-bg; + } + + &.is-success svg { + color: $green; + background: $color-success-bg; + } + + &.is-danger svg { + color: $red; + background: $color-danger-bg; + } + + &.is-muted svg { + color: $gray50; + } +} + +.device-main__bottom { + margin-top: 18px; +} + +.device-registered { + width: 100%; + padding: 0; + + display: inline-flex; + align-items: center; + gap: 10px; + + color: $color-text-strong; + font-size: $font-size-18; + font-weight: 500; + + .device-registered-info { + display: flex; + flex: 1; + align-items: center; + padding: 10px 0; + height: inherit; + justify-content: space-between; + border-bottom: 1px solid $gray20; + + a { + display: flex; + flex-direction: row; + align-items: center; + gap: 6px; + color: $gray50; + font-weight: 500; + text-decoration: none; + transition: color 0.2s ease; + + &:hover { + color: $color-primary-hover; + + svg { + color: $color-primary-hover; + } + } + + svg { + padding: 0; + background-color: transparent; + height: 20px; + width: auto; + margin-bottom: 2px; + transition: .2s ease; + } + } + } + + svg { + padding: 4px; + width: 24px; + height: 24px; + border-radius: 8px; + color: $gray50; + background: $color-bg; + } + + b { + color: $gray50; + font-weight: 500; + } + +} + +.device-card__actions { + margin-top: 20px; + + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; +} + +.device-history-btn { + + padding: 12px; + + border: none; + border-radius: 12px; + background: $color-primary-soft; + + color: $blue; + font-size: $font-size-18; + font-weight: 500; + cursor: pointer; + transition: .2s ease; + line-height: 1; + + &:hover { + background-color: $blue; + color: $color-text-inverse; + box-shadow: var(--shadow-primary); + } +} + +.device-delete-btn { + padding: 12px; + + border: none; + border-radius: 12px; + background: $color-danger-bg-strong; + + display: inline-flex; + align-items: center; + justify-content: center; + color: $red; + cursor: pointer; + transition: .2s ease; + + svg { + width: 18px; + height: 18px; + } + + &:hover { + background-color: $red; + color: $color-text-inverse; + box-shadow: var(--shadow-danger); + } +} + +.device-card__header { + margin-bottom: 14px; + + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + + p { + margin: 0; + color: $color-text-muted; + font-size: $font-size-15; + font-weight: 500; + } +} + +.device-card__title { + display: flex; + align-items: center; + gap: 10px; + + h3 { + margin: 0; + color: $color-text-heading; + font-size: $font-size-18; + font-weight: 600; + } + + svg { + padding: 4px; + width: 24px; + height: 24px; + border-radius: 9px; + background: $color-surface-hover; + color: $color-text-heading; + } +} + +.device-map { + display: flex; + flex-direction: column; + grid-column: span 1; + min-height: 272px; + height: calc(100% - 40px); +} + + +.device-permissions { + padding: 10px 20px; + display: flex; + flex: 1; + flex-direction: column; + justify-content: space-between; + height: calc(100% - 20px); +} + +.device-permission { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + + color: $color-text-heading; + font-size: $font-size-18; + font-weight: 500; + + &:nth-child(1) { + .device-permission__label { + border-top: none; + } + } + + svg { + padding: 4px; + width: 24px; + height: 24px; + border-radius: 8px; + color: $blue; + background: $color-surface-hover; + } +} + +.device-permission__label { + display: flex; + align-items: center; + justify-content: space-between; + flex: 1; + gap: 10px; + padding: 10px 0; + border-top: 1px solid $gray20; +} + +.device-card-stats { + display: flex; + flex-direction: column; + gap: 20px; +} + +.device-battery { + --device-battery-progress-empty: #cfd5df; + --device-battery-progress-danger: #e00000; + --device-battery-progress-warning: #ff8a34; + --device-battery-progress-strong: #031d9a; + + h3 { + text-align: left; + line-height: 1; + margin: 0 0 12px; + padding-bottom: 12px; + border-bottom: 1px solid $gray20; + + color: $color-text-heading; + font-size: $font-size-18; + font-weight: 600; + } + + .device-stats-card { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + &:nth-child(2) { + .device-stats-text { + border-top: 1px solid $gray20; + border-bottom: 1px solid $gray20; + padding: 8px 0; + } + } + + svg { + width: 24px; + height: 24px; + padding: 4px; + border-radius: 8px; + } + } + + .device-stats-text { + display: flex; + flex: 1; + flex-direction: row; + align-items: center; + justify-content: space-between; + } +} + +.device-battery__content { + display: grid; + grid-template-columns: 120px 1fr; + gap: 20px; + align-items: center; +} + +.device-battery__circle { + position: relative; + + width: 120px; + height: 120px; + flex: 0 0 120px; + + display: flex; + align-items: center; + justify-content: center; + + overflow: visible; + + span { + position: absolute; + inset: 0; + + display: flex; + align-items: center; + justify-content: center; + + color: $color-text; + font-size: $font-size-26; + font-weight: 650; + line-height: 1; + } +} + +.device-battery__circle-svg { + width: 120px; + height: 120px; + display: block; +} + +.device-battery__circle-track, +.device-battery__circle-progress { + fill: none; +} + +.device-battery__circle-track { + stroke: $gray20; + stroke-linecap: round; +} + +.device-battery__circle-progress { + stroke-linecap: round; + transition: + stroke-dashoffset 0.35s ease, + stroke 0.25s ease; +} + +.device-stats { + display: flex; + flex-direction: column; + gap: 10px; + + div { + display: grid; + grid-template-columns: 30px 1fr auto; + align-items: center; + gap: 10px; + + color: $color-text-heading; + font-size: $font-size-16; + font-weight: 500; + } + + svg { + padding: 5px; + width: 30px; + height: 30px; + border-radius: 9px; + color: $color-success-strong; + background: $color-success-bg; + } + + b { + color: $color-text-heading; + font-weight: 600; + } +} + +.device-impacts { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; + + color: $color-text-heading; + font-size: $font-size-18; + font-weight: 500; + + .device-stats-text { + display: flex; + flex: 1; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 20px; + } + + svg { + padding: 4px; + width: 24px; + height: 24px; + border-radius: 9px; + color: $green; + background: $color-success-bg; + } + + b { + font-weight: 600; + } +} + +.device-page__empty { + min-height: 260px; + border-radius: 18px; + background: $color-surface; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + + h2 { + margin: 0; + } + + button { + height: 40px; + padding: 0 16px; + border: none; + border-radius: 12px; + background: $blue; + color: $color-text-inverse; + cursor: pointer; + } +} + +[data-theme='dark'] { + .device-battery { + --device-battery-progress-empty: #36414f; + --device-battery-progress-danger: #ff4f5e; + --device-battery-progress-warning: #ff9f43; + --device-battery-progress-strong: #2d8cff; + } +} + + +@include desktop-down { + .device-page__grid { + display: grid; + grid-template-columns: 280px 280px minmax(380px, 1fr); + gap: 14px; + align-items: start; + + .device-page__details-row { + gap: 14px; + grid-template-columns: 1.25fr 1fr 1.25fr; + + .device-card-stats { + gap: 14px; + } + } + + .device-actions { + button { + font-size: var(--font-size-16); + padding: 12px; + gap: 12px; + + svg { + width: 28px; + height: 28px; + } + } + + .future-label { + font-size: var(--font-size-12); + padding: 8px 12px; + } + } + } + + .device-permission { + font-size: var(--font-size-16) !important; + + svg { + width: 20px; + height: 20px; + } + + &__label { + padding: 8px 0; + } + } + + .device-permissions__icon-btn { + width: 32px; + height: 32px; + border-radius: 10px; + } + + .device-battery__content { + grid-template-columns: 100px 1fr; + } + + .device-battery__circle { + width: 100px; + height: 100px; + + span { + font-size: var(--font-size-22); + } + } + + .device-stats { + gap: 8px; + + div { + font-size: var(--font-size-15); + } + + .device-stats-card { + svg { + width: 20px; + height: 20px; + } + } + } + + .device-impacts { + font-size: var(--font-size-16); + + svg { + width: 20px; + height: 20px; + } + } + + .device-main { + &__info { + h2 { + font-size: var(--font-size-28); + } + + p { + font-size: var(--font-size-15); + } + } + } + + .device-main__image { + width: 26.5%; + } + + .device-status { + font-size: var(--font-size-16); + + svg { + width: 20px; + height: 20px; + } + } + + .device-registered { + font-size: var(--font-size-16); + + svg { + height: 20px; + width: 20px; + } + + .device-registered-info { + padding: 8px 0; + + a { + svg { + width: 18px; + height: 18px; + } + } + } + } + + .device-card__actions { + margin-top: 18px; + } + + .device-history-btn { + font-size: var(--font-size-16); + } + + .device-delete-btn { + svg { + width: 16px; + height: 16px; + } + } + + .history-period-tabs__item { + font-size: var(--font-size-12); + height: 30px; + } + +} + +@include laptop-down { + .device-page { + gap: 14px; + } + + .device-page__grid { + grid-template-columns: 540px minmax(360px, 1fr);; + } + + .device-card--main { + grid-column: span 1; + } + + .device-page__grid, + .device-page__details-row { + gap: $layout-card-gap; + } + + .device-card { + padding: 18px; + height: calc(100% - 36px); + border-radius: 16px; + } + .device-card-stats{ + height: calc(100%); + } + .device-impacts{ + flex: 0; + } + .device-battery{ + display: flex; + flex-direction: column; + justify-content: space-between; + flex: 1; + } + .device-permissions{ + height: calc(100% - 12px); + padding: 8px 8px 4px 18px; + } + + .device-main { + gap: 16px; + } + + .device-main__image { + padding: 16px; + } + + .device-map { + min-height: 252px; + } + .history-period-tabs{ + display: none; + } + .device-card__title{ + gap: 8px; + svg{ + width: 22px; + height: 22px; + } + h3{ + font-size: var(--font-size-16); + } + } + .device-card__header{ + p{ + font-size: var(--font-size-14); + } + } +} + +@include tablet-down { + .device-main { + flex-direction: column; + } + + .device-main__image { + width: 100%; + height: 178px; + box-sizing: border-box; + } + + .device-main__info { + h2 { + font-size: $font-size-26; + } + } + + .device-map { + grid-column: span 1; + min-height: 300px; + height: auto; + } + + .device-page__details-row { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .device-card__actions { + gap: 10px; + } +} diff --git a/frontend/src/pages/DevicePage/DevicePage.tsx b/frontend/src/pages/DevicePage/DevicePage.tsx new file mode 100644 index 0000000..465d893 --- /dev/null +++ b/frontend/src/pages/DevicePage/DevicePage.tsx @@ -0,0 +1,438 @@ +import { + lazy, + Suspense, + useCallback, + useEffect, + useMemo, + useState, +} from 'react' +import { + useNavigate, + useOutletContext, + useParams, + useSearchParams, +} from 'react-router-dom' +import { AnimatePresence, m } from 'framer-motion' +import { useQuery } from '@apollo/client/react' + +import type { Device as PageDevice } from './types' + +import { GET_DEVICE_PAGE_QUERY } from '../../entities/device/api/device.graphql' +import type { + Device as ApiDevice, + DeviceNetworkStatus, + GetPhoneGpsTrackData, + GetTelemetryData, + GetDevicePageData, + GetDevicePageVariables, +} from '../../entities/device/model/types' + +import { DeviceMainCard } from './components/DeviceMainCard/DeviceMainCard' +import { DeviceMapCard } from './components/DeviceMapCard/DeviceMapCard' +import { DeviceActionsCard } from './components/DeviceActionsCard/DeviceActionsCard' +import { DevicePermissionsCard } from './components/DevicePermissionsCard/DevicePermissionsCard' +import { DeviceStatsCards } from './components/DeviceStatsCards/DeviceStatsCards' +import { DeviceAppsView } from './components/DeviceAppsView/DeviceAppsView' +import { pageSwapVariants, uiFadeTransition } from '../../shared/lib/motion' +import { isPhonePolicyOptionEnabled } from '../../entities/device/lib/phonePolicy' +import { EmptyState } from '../../shared/ui/EmptyState/EmptyState' + +import './DevicePage.scss' +import { DevicePageSkeleton } from './components/DevicePageSkeleton/DevicePageSkeleton' +import type { AppLayoutOutletContext } from '../../app/layouts/AppLayout' +import { + preloadOnIdle, + useLazyMount, +} from '../../shared/lib/lazyMount' + +const loadDeviceHistoryModal = () => + import('./components/DeviceHistoryModal/DeviceHistoryModal').then( + (module) => ({ + default: module.DeviceHistoryModal, + }), + ) + +const DeviceHistoryModal = lazy(loadDeviceHistoryModal) + +function formatLocationDate(timestamp: number) { + if (!timestamp) return 'Нет данных' + + return new Intl.DateTimeFormat('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }).format(new Date(timestamp)) +} + +function formatTechValue(value?: number | null) { + return typeof value === 'number' ? String(value) : undefined +} + +function getLatestTelemetryItem(telemetry: GetTelemetryData['getTelemetry']) { + if (!telemetry.length) return null + + return [...telemetry].sort((a, b) => b.date - a.date)[0] +} + +function getSortedGpsTrack(track: GetPhoneGpsTrackData['getPhoneGpsTrack']) { + return [...track].sort((a, b) => a.date - b.date) +} + +function getDeviceNetworkConnection(status?: DeviceNetworkStatus) { + if (status === 'Online') return 'online' + if (status === 'Lost') return 'offlineDanger' + + return 'offline' +} + +function getDeviceNetworkLabel(status?: DeviceNetworkStatus) { + if (status === 'Online') return 'В сети' + if (status === 'Lost') return 'Потерян' + + return 'Не в сети' +} + +function mapApiDeviceToPageDevice( + device: ApiDevice, + batteryLevel?: number, + gpsTrack: GetPhoneGpsTrackData['getPhoneGpsTrack'] = [], + telemetry: GetTelemetryData['getTelemetry'] = [], +): PageDevice { + const sortedGpsTrack = getSortedGpsTrack(gpsTrack) + const policy = device.policy + const techState = device.techState + const needMaintenance = techState?.needMaintenance ?? false + const bluetoothEnabled = isPhonePolicyOptionEnabled( + policy?.bluetooth ?? policy?.canUseBluetooth, + ) + const gpsEnabled = isPhonePolicyOptionEnabled(policy?.GPS ?? policy?.canUseGPS) + const cameraEnabled = isPhonePolicyOptionEnabled( + policy?.camera ?? policy?.canUseCamera, + ) + const simEnabled = isPhonePolicyOptionEnabled(policy?.sim ?? policy?.canUseSim) + const isLocked = policy?.locked ?? false + + const currentGpsPoint = + sortedGpsTrack.length > 0 + ? sortedGpsTrack[sortedGpsTrack.length - 1] + : null + + const routeGpsPoints = + sortedGpsTrack.length > 1 + ? sortedGpsTrack.slice(0, -1) + : [] + + const location = currentGpsPoint + ? { + lat: currentGpsPoint.lat, + lng: currentGpsPoint.lng, + } + : device.lastLocation + ? { + lat: device.lastLocation.lat, + lng: device.lastLocation.lng, + } + : undefined + + const lastLocationAt = currentGpsPoint + ? formatLocationDate(currentGpsPoint.date) + : device.lastLocation + ? formatLocationDate(device.lastLocation.date) + : 'Нет данных' + + return { + id: device.id, + + model: 'АРМАФОН S3.3+', + factoryNumber: device.serial || 'Заводской номер не указан', + imei: device.imei || 'IMEI не указан', + imei2: device.imei2 || 'IMEI 2 не указан', + serialNumber: device.serial || undefined, + + employee: device.org?.name ?? null, + organisation: device.org?.name, + organisationId: device.org?.id ?? device.orgId, + policy, + organisationPolicy: device.org?.policy ?? null, + + condition: needMaintenance ? 'inspection' : 'ok', + connection: getDeviceNetworkConnection(device.networkStatus), + connectionText: getDeviceNetworkLabel(device.networkStatus), + + workTime: null, + registeredAt: device.registerDate + ? formatLocationDate(device.registerDate) + : undefined, + + image: undefined, + + location, + lastLocationAt, + lastLocationDate: device.lastLocation?.date, + + route: routeGpsPoints.map((point) => ({ + lat: point.lat, + lng: point.lng, + time: formatLocationDate(point.date), + })), + + permissions: { + wifi: false, + bluetooth: bluetoothEnabled, + gps: gpsEnabled, + camera: cameraEnabled, + sim: simEnabled, + speaker: false, + locked: isLocked, + }, + + statusIcons: { + gps: gpsEnabled, + wifi: false, + bluetooth: bluetoothEnabled, + lock: isLocked, + camera: cameraEnabled, + sim: simEnabled, + sound: false, + kiosk: false, + }, + + battery: techState?.batteryLevel ?? batteryLevel, + batteryMaxCapacity: techState?.batteryRemainingCapacity ?? undefined, + telemetry, + chargeCycles: formatTechValue(techState?.batteryCycles), + totalWorkTime: techState?.worktime ?? null, + mediumImpacts: formatTechValue(techState?.hits), + overheats: formatTechValue(techState?.overheats), + } +} + +export function DevicePage() { + const { deviceId } = useParams() + const navigate = useNavigate() + const { setNavbarBreadcrumbs } = + useOutletContext() + const [searchParams, setSearchParams] = useSearchParams() + const [isHistoryOpen, setIsHistoryOpen] = useState(false) + const shouldMountHistoryModal = useLazyMount(isHistoryOpen) + + const numericDeviceId = Number(deviceId) + const isAppsView = searchParams.get('view') === 'apps' + + useEffect(() => { + return preloadOnIdle([loadDeviceHistoryModal]) + }, []) + + const { + data, + loading, + error, + refetch, + startPolling, + stopPolling, + } = useQuery< + GetDevicePageData, + GetDevicePageVariables + >(GET_DEVICE_PAGE_QUERY, { + variables: { + id: numericDeviceId, + phoneId: String(numericDeviceId), + telemetryStartDate: 0, + gpsStartDate: 0, + }, + skip: !numericDeviceId, + fetchPolicy: 'network-only', + pollInterval: 15000, + }) + + const handlePermissionsEditingChange = useCallback( + (isEditing: boolean) => { + if (isEditing) { + stopPolling() + return + } + + if (numericDeviceId) { + startPolling(15000) + } + }, + [numericDeviceId, startPolling, stopPolling], + ) + + const latestTelemetryItem = useMemo(() => { + return getLatestTelemetryItem(data?.getTelemetry ?? []) + }, [data]) + + const device = useMemo(() => { + if (!data?.getPhone) return null + + return mapApiDeviceToPageDevice( + data.getPhone, + latestTelemetryItem?.batteryLevel ?? undefined, + data.getPhoneGpsTrack ?? [], + data.getTelemetry ?? [], + ) + }, [data, latestTelemetryItem]) + + useEffect(() => { + if (!device) { + setNavbarBreadcrumbs([]) + return + } + + setNavbarBreadcrumbs([ + { + label: device.factoryNumber, + to: isAppsView ? `/devices/${device.id}` : undefined, + }, + ...(isAppsView ? [{ label: 'Приложения' }] : []), + ]) + + return () => { + setNavbarBreadcrumbs([]) + } + }, [device, isAppsView, setNavbarBreadcrumbs]) + + const handleOpenApps = useCallback(() => { + const nextParams = new URLSearchParams(searchParams) + nextParams.set('view', 'apps') + + setSearchParams(nextParams) + }, [searchParams, setSearchParams]) + + const handleCloseApps = useCallback(() => { + const nextParams = new URLSearchParams(searchParams) + nextParams.delete('view') + + setSearchParams(nextParams, { + replace: true, + }) + }, [searchParams, setSearchParams]) + + const handleOpenHistory = useCallback(() => { + setIsHistoryOpen(true) + }, []) + + const handleRefetchPolicy = useCallback(async () => { + const result = await refetch() + + return result.data?.getPhone?.policy + }, [refetch]) + + const handleLockStateChange = useCallback(async () => { + const result = await refetch() + + return result.data?.getPhone?.policy?.locked + }, [refetch]) + + if (!numericDeviceId) { + return ( +
+ + + +
+ ) + } + + if (loading && !device) { + return + } + + if (error || !device) { + return ( +
+ + + +
+ ) + } + + return ( +
+ + {isAppsView ? ( + + + + ) : ( + +
+ + + +
+ + + +
+
+
+ )} +
+ {shouldMountHistoryModal && ( + + + + )} +
+ ) +} diff --git a/frontend/src/pages/DevicePage/components/BatteryCircle/BatteryCircle.scss b/frontend/src/pages/DevicePage/components/BatteryCircle/BatteryCircle.scss new file mode 100644 index 0000000..c209ea7 --- /dev/null +++ b/frontend/src/pages/DevicePage/components/BatteryCircle/BatteryCircle.scss @@ -0,0 +1,44 @@ +@use '../../../../shared/styles/variables' as *; + +.battery-circle { + position: relative; + width: 180px; + height: 180px; + display: flex; + align-items: center; + justify-content: center; +} + +.battery-circle__svg { + width: 100%; + height: 100%; +} + +.battery-circle__track, +.battery-circle__progress { + fill: none; +} + +.battery-circle__track { + stroke: $color-border-accent; +} + +.battery-circle__progress { + stroke-linecap: round; + transition: + stroke-dashoffset 0.35s ease, + stroke 0.25s ease; +} + +.battery-circle__value { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + + font-size: $font-size-44; + font-weight: 500; + line-height: 1; + color: $black; +} diff --git a/frontend/src/pages/DevicePage/components/BatteryCircle/BatteryCircle.tsx b/frontend/src/pages/DevicePage/components/BatteryCircle/BatteryCircle.tsx new file mode 100644 index 0000000..3efb5ec --- /dev/null +++ b/frontend/src/pages/DevicePage/components/BatteryCircle/BatteryCircle.tsx @@ -0,0 +1,57 @@ +type BatteryCircleProps = { + value?: number | null +} + +function getBatteryColor(value: number) { + if (value <= 20) return '#E53935' + if (value <= 50) return '#F59E0B' + return '#0B28B8' +} + +export function BatteryCircle({ value }: BatteryCircleProps) { + const battery = typeof value === 'number' + ? Math.max(0, Math.min(100, value)) + : 0 + + const size = 180 + const strokeWidth = 14 + const radius = (size - strokeWidth) / 2 + const circumference = 2 * Math.PI * radius + + const dashOffset = circumference * (1 - battery / 100) + + return ( +
+ + + + + + + + {battery}% +
+ ) +} \ No newline at end of file diff --git a/frontend/src/pages/DevicePage/components/DeviceActionsCard/DeviceActionsCard.scss b/frontend/src/pages/DevicePage/components/DeviceActionsCard/DeviceActionsCard.scss new file mode 100644 index 0000000..f268ceb --- /dev/null +++ b/frontend/src/pages/DevicePage/components/DeviceActionsCard/DeviceActionsCard.scss @@ -0,0 +1,235 @@ +@use '../../../../shared/styles/variables' as *; + + +.device-actions { + display: flex; + flex-direction: column; + justify-content: space-between; + height: calc(100% - 40px); + + h3 { + line-height: 1; + margin: 0 0 12px; + padding-bottom: 12px; + border-bottom: 1px solid $color-border; + text-align: left; + color: $color-text-heading; + font-size: $font-size-18; + font-weight: 600; + } + + .actions-button-container { + display: flex; + flex-direction: column; + } + + button { + display: flex; + flex-direction: row; + gap: 14px; + align-items: center; + justify-content: flex-start; + width: 100%; + padding: 14px; + border: none; + border-radius: 12px; + background: $color-bg; + + display: flex; + align-items: center; + + color: $gray50; + font-size: $font-size-18; + font-weight: 550; + cursor: pointer; + transition: .2s ease; + small{ + font-weight: 500; + opacity: .8; + } + .actions-button-text{ + display: flex; + flex-direction: column; + align-items: flex-start; + } + + svg { + height: 32px; + width: 32px; + } + + &:not(:disabled):hover { + background-color: rgba($gray20-rgb, .9); + //color: $blue; + box-shadow: inset 0 0 0 1.5px rgba($gray50-rgb, .1); + } + + &:disabled { + opacity: .74; + cursor: not-allowed; + + &:hover { + background: $color-bg; + color: $gray50; + } + } + + &+button { + margin-top: 8px; + } + } + + .device-actions__lock-button { + overflow: hidden; + + .device-actions__lock-content { + position: relative; + display: grid; + width: 100%; + min-height: 40px; + min-width: 0; + overflow: hidden; + } + + .device-actions__lock-state { + grid-area: 1 / 1; + display: flex; + align-items: center; + gap: 14px; + width: 100%; + min-height: 40px; + min-width: 0; + } + + .device-actions__lock-icon { + flex: 0 0 32px; + transition: + color 0.2s ease, + transform 0.24s cubic-bezier(0.34, 1.42, 0.64, 1); + } + + &.is-loading { + opacity: 1; + cursor: wait; + + .device-actions__loading-icon { + color: $blue; + transform: none; + transform-box: fill-box; + transform-origin: center; + backface-visibility: hidden; + transition: color 0.2s ease; + will-change: transform; + animation: device-actions-loading-spin 0.85s linear infinite; + } + + &:disabled:hover { + background: $color-bg; + color: $gray50; + } + } + + &.is-lock:not(.is-loading) { + &:not(:disabled):hover { + .device-actions__lock-icon { + color: $red; + transform: translateY(-1px) scale(1.06); + } + } + } + + &.is-unlock:not(.is-loading) { + &:not(:disabled):hover { + .device-actions__lock-icon { + color: $blue; + transform: rotate(-8deg) translateY(-1px) scale(1.06); + } + } + } + } + + .device-actions__error { + margin-top: 10px; + padding: 7px 10px; + border-radius: 10px; + background: $color-danger-tint; + + color: $red; + font-size: $font-size-13; + font-weight: 600; + line-height: 1.25; + } + + .future-label{ + display: flex; + flex-direction: row; + align-items: center; + font-size: $font-size-14; + font-weight: 550; + padding: 10px 14px; + margin-top: 20px; + gap: 14px; + color: $blue; + background-color: $blue20; + border-radius: 14px; + svg{ + height: 20px; + width: auto; + } + } +} + +.device-actions__apps-icon { + transition: color 0.2s ease; + + rect { + transform-box: fill-box; + transform-origin: center; + transition: transform 0.22s ease; + } +} + +.device-actions__apps-button:not(:disabled):hover { + .device-actions__apps-icon { + color: $blue; + + rect:nth-child(2) { + --tile-pop-x: 1.7px; + --tile-pop-y: -1.7px; + --tile-rest-x: 0.8px; + --tile-rest-y: -0.8px; + animation: device-actions-app-tile 0.48s cubic-bezier(0.34, 1.42, 0.64, 1); + transform: translate(var(--tile-rest-x), var(--tile-rest-y)) scale(1); + } + } +} + +@keyframes device-actions-app-tile { + 0% { + transform: translate(0, 0) scale(1); + } + + 45% { + transform: translate(var(--tile-pop-x), var(--tile-pop-y)) scale(0.86); + } + + 100% { + transform: translate(var(--tile-rest-x, 0), var(--tile-rest-y, 0)) scale(1); + } +} + +@keyframes device-actions-loading-spin { + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: reduce) { + .device-actions { + .device-actions__lock-button.is-loading { + .device-actions__loading-icon { + animation: none; + } + } + } +} diff --git a/frontend/src/pages/DevicePage/components/DeviceActionsCard/DeviceActionsCard.tsx b/frontend/src/pages/DevicePage/components/DeviceActionsCard/DeviceActionsCard.tsx new file mode 100644 index 0000000..c24926c --- /dev/null +++ b/frontend/src/pages/DevicePage/components/DeviceActionsCard/DeviceActionsCard.tsx @@ -0,0 +1,272 @@ +import { memo, useEffect, useState } from 'react' +import { AnimatePresence, m, useReducedMotion } from 'framer-motion' +import { useMutation } from '@apollo/client/react' +import { CircleAlert, Unlock } from 'lucide-react' + +import { Apps } from '../../../../assets/icons/Apps' +import { BlockIcon } from '../../../../assets/icons/Block' +import { LoadingTwotoneLoopIcon } from '../../../../assets/icons/LoadingTwotoneLoop' +import type { Device } from '../../types' +import { + CHANGE_PHONE_POLICY_MUTATION, +} from '../../../../entities/device/api/device.graphql' +import type { + ChangePhonePolicyData, + ChangePhonePolicyVariables, +} from '../../../../entities/device/model/types' +import { + normalizeComponentUseOption, + normalizeServiceUseOption, +} from '../../../../entities/device/lib/phonePolicy' + +import './DeviceActionsCard.scss' + +type DeviceActionsCardProps = { + device: Device + onOpenApps: () => void + onLockStateChange?: () => Promise | boolean | null | void +} + +function getChangeLockVariables( + device: Device, + locked: boolean, +): ChangePhonePolicyVariables { + const policy = device.policy + const permissions = device.permissions + + return { + id: String(device.id), + bluetooth: normalizeServiceUseOption( + policy?.bluetooth ?? policy?.canUseBluetooth, + permissions?.bluetooth, + ), + camera: normalizeComponentUseOption( + policy?.camera ?? policy?.canUseCamera, + permissions?.camera, + ), + GPS: normalizeServiceUseOption( + policy?.GPS ?? policy?.canUseGPS, + permissions?.gps, + ), + sim: normalizeServiceUseOption( + policy?.sim ?? policy?.canUseSim, + permissions?.sim, + ), + locked, + } +} + +function areDeviceActionsCardPropsEqual( + prev: DeviceActionsCardProps, + next: DeviceActionsCardProps, +) { + const prevPolicy = prev.device.policy + const nextPolicy = next.device.policy + const prevPermissions = prev.device.permissions + const nextPermissions = next.device.permissions + + return ( + prev.onOpenApps === next.onOpenApps && + prev.onLockStateChange === next.onLockStateChange && + prev.device.id === next.device.id && + prev.device.connection === next.device.connection && + prevPolicy?.bluetooth === nextPolicy?.bluetooth && + prevPolicy?.camera === nextPolicy?.camera && + prevPolicy?.GPS === nextPolicy?.GPS && + prevPolicy?.sim === nextPolicy?.sim && + prevPolicy?.locked === nextPolicy?.locked && + prevPermissions?.bluetooth === nextPermissions?.bluetooth && + prevPermissions?.camera === nextPermissions?.camera && + prevPermissions?.gps === nextPermissions?.gps && + prevPermissions?.sim === nextPermissions?.sim && + prevPermissions?.locked === nextPermissions?.locked + ) +} + +export const DeviceActionsCard = memo(function DeviceActionsCard({ + device, + onOpenApps, + onLockStateChange, +}: DeviceActionsCardProps) { + const deviceLocked = device.permissions?.locked ?? device.policy?.locked ?? false + const isLost = device.connection === 'offlineDanger' + const [displayLocked, setDisplayLocked] = useState(deviceLocked) + const [pendingLockTarget, setPendingLockTarget] = useState(null) + const [lockError, setLockError] = useState(false) + const [changePhonePolicy] = useMutation< + ChangePhonePolicyData, + ChangePhonePolicyVariables + >(CHANGE_PHONE_POLICY_MUTATION) + const shouldReduceMotion = useReducedMotion() + const isLockLoading = pendingLockTarget !== null + const lockStateTransition = shouldReduceMotion + ? { duration: 0 } + : { + duration: 0.18, + ease: [0.34, 1.42, 0.64, 1] as [number, number, number, number], + } + const lockLoadingTarget = pendingLockTarget ?? !displayLocked + const lockButtonView = isLockLoading + ? { + key: 'loading', + icon: ( +