From 02681340c516ceef4674872e0884a754d6ec6376 Mon Sep 17 00:00:00 2001 From: SERTY Date: Thu, 11 Dec 2025 05:20:53 +0300 Subject: [PATCH] added front - react+ts ocr improved --- API_DOCS.md | 324 +- cmd/main.go | 1 + internal/domain/catalog/entity.go | 50 +- internal/domain/ocr/entity.go | 42 +- internal/infrastructure/db/postgres.go | 3 + .../repository/catalog/postgres.go | 52 +- .../infrastructure/repository/ocr/postgres.go | 81 +- internal/infrastructure/rms/client.go | 58 + internal/infrastructure/rms/dto.go | 35 +- internal/services/ocr/service.go | 76 +- internal/services/sync/service.go | 22 +- internal/transport/http/handlers/ocr.go | 32 +- project_dump.py | 119 +- rmser-view/.env | 3 + rmser-view/.gitignore | 24 + rmser-view/README.md | 73 + rmser-view/eslint.config.js | 23 + rmser-view/index.html | 16 + rmser-view/package-lock.json | 4604 +++++++++++++++++ rmser-view/package.json | 37 + rmser-view/public/vite.svg | 1 + rmser-view/src/App.tsx | 27 + .../src/components/layout/AppLayout.tsx | 57 + .../src/components/layout/Providers.tsx | 43 + .../src/components/ocr/AddMatchForm.tsx | 143 + .../src/components/ocr/CatalogSelect.tsx | 56 + rmser-view/src/components/ocr/MatchList.tsx | 79 + .../recommendations/RecommendationCard.tsx | 50 + rmser-view/src/hooks/useOcr.ts | 48 + rmser-view/src/hooks/useRecommendations.ts | 12 + rmser-view/src/main.tsx | 12 + rmser-view/src/pages/Dashboard.tsx | 59 + rmser-view/src/pages/OcrLearning.tsx | 52 + rmser-view/src/services/api.ts | 67 + rmser-view/src/services/types.ts | 98 + rmser-view/tsconfig.app.json | 28 + rmser-view/tsconfig.json | 7 + rmser-view/tsconfig.node.json | 26 + rmser-view/vite.config.ts | 13 + 39 files changed, 6286 insertions(+), 267 deletions(-) create mode 100644 rmser-view/.env create mode 100644 rmser-view/.gitignore create mode 100644 rmser-view/README.md create mode 100644 rmser-view/eslint.config.js create mode 100644 rmser-view/index.html create mode 100644 rmser-view/package-lock.json create mode 100644 rmser-view/package.json create mode 100644 rmser-view/public/vite.svg create mode 100644 rmser-view/src/App.tsx create mode 100644 rmser-view/src/components/layout/AppLayout.tsx create mode 100644 rmser-view/src/components/layout/Providers.tsx create mode 100644 rmser-view/src/components/ocr/AddMatchForm.tsx create mode 100644 rmser-view/src/components/ocr/CatalogSelect.tsx create mode 100644 rmser-view/src/components/ocr/MatchList.tsx create mode 100644 rmser-view/src/components/recommendations/RecommendationCard.tsx create mode 100644 rmser-view/src/hooks/useOcr.ts create mode 100644 rmser-view/src/hooks/useRecommendations.ts create mode 100644 rmser-view/src/main.tsx create mode 100644 rmser-view/src/pages/Dashboard.tsx create mode 100644 rmser-view/src/pages/OcrLearning.tsx create mode 100644 rmser-view/src/services/api.ts create mode 100644 rmser-view/src/services/types.ts create mode 100644 rmser-view/tsconfig.app.json create mode 100644 rmser-view/tsconfig.json create mode 100644 rmser-view/tsconfig.node.json create mode 100644 rmser-view/vite.config.ts diff --git a/API_DOCS.md b/API_DOCS.md index abc7a64..1b59557 100644 --- a/API_DOCS.md +++ b/API_DOCS.md @@ -1,161 +1,164 @@ -# API Documentation: RMSER Backend +# RMSER Backend API Documentation (v2.0) -## 1. Общая информация +**Дата обновления:** Текущая +**Base URL:** `http://localhost:8080/api` -* **Base URL:** `http://localhost:8080/api` -* **Формат данных:** JSON. -* **CORS:** Разрешены запросы с `localhost:5173` (и любых других источников в режиме dev). -* **Auth:** На данный момент эндпоинты открыты. В будущем предполагается передача токена в заголовке `Authorization: Bearer `. +## 1. Типы данных (TypeScript Interfaces) -### Обработка ошибок -В случае ошибки сервер возвращает HTTP код 4xx/5xx и JSON: -```json -{ - "error": "Описание ошибки" -} -``` - ---- - -## 2. TypeScript Интерфейсы (Models) - -Используйте эти интерфейсы для типизации данных на клиенте. +Используйте эти интерфейсы для строгой типизации на клиенте. ```typescript -// --- Общие типы --- - -// UUID (строка) +// Базовые типы type UUID = string; -// Decimal (деньги/вес приходят строкой, чтобы избежать потери точность в JS) -type Decimal = string; +// --- Каталог и Фасовки --- -// --- Каталог и OCR --- +export interface MeasureUnit { + id: UUID; + name: string; // "кг", "л", "шт", "порц" + code: string; +} + +export interface ProductContainer { + id: UUID; + name: string; // "Коробка", "Пачка 200г" + count: number; // Коэффициент пересчета в базовые единицы (напр. 0.2 или 12.0) +} -// Товар для выпадающего списка (Autosuggest) export interface CatalogItem { id: UUID; name: string; - code: string; // Артикул или код быстрого набора + code: string; + measure_unit: string; // Название базовой единицы (из MainUnit) + containers: ProductContainer[]; // Доступные фасовки } -// Связь "Текст из чека" -> "Товар iiko" -export interface ProductMatch { - raw_name: string; // Текст из OCR (ключ) - product_id: UUID; - product?: CatalogItem; // Вложенный объект (при чтении) - updated_at: string; // ISO Date +// --- Матчинг (Обучение) --- + +export interface MatchRequest { + raw_name: string; // Текст из чека + product_id: UUID; // ID товара iiko + quantity: number; // Количество (по умолчанию 1.0) + container_id?: UUID; // Опционально: ID фасовки, если выбрана } -// --- Рекомендации (Аналитика) --- - -export interface Recommendation { - ID: UUID; - Type: string; // Код проблемы (UNUSED_IN_RECIPES, NO_INCOMING, etc.) - ProductID: UUID; - ProductName: string; - Reason: string; // Человекочитаемое описание проблемы - CreatedAt: string; +export interface SavedMatch { + raw_name: string; + product: CatalogItem; // Вложенный объект товара + quantity: number; // Сохраненный коэффициент/количество + container_id?: UUID; + container?: ProductContainer; // Вложенный объект фасовки (для отображения имени) + updated_at: string; } -// --- Накладные --- +// --- Нераспознанное --- -export interface InvoiceItemRequest { - product_id: UUID; - amount: number; // или string, если нужна высокая точность - price: number; -} - -export interface CreateInvoiceRequest { - document_number: string; - date_incoming: string; // Format: "YYYY-MM-DD" - supplier_id: UUID; - store_id: UUID; - items: InvoiceItemRequest[]; +export interface UnmatchedItem { + raw_name: string; + count: number; // Сколько раз встречалось в чеках + last_seen: string; // ISO Date } ``` --- -## 3. Эндпоинты +## 2. Эндпоинты: OCR и Обучение -### 📊 Аналитика (Рекомендации) +### `GET /ocr/catalog` +Получить полный справочник товаров для поиска. +Теперь включает **единицы измерения** и **фасовки**. -#### Получить список рекомендаций -Возвращает список выявленных проблем в учете (товары без техкарт, ингредиенты без закупок и т.д.). Данные обновляются фоновым процессом на бэкенде. - -* **URL:** `/recommendations` -* **Method:** `GET` * **Response:** `200 OK` ```json [ { - "ID": "uuid...", - "Type": "UNUSED_IN_RECIPES", - "ProductID": "uuid...", - "ProductName": "Петрушка с/м", - "Reason": "Товар не используется ни в одной техкарте", - "CreatedAt": "2023-10-27T10:00:00Z" - } -] -``` - ---- - -### 👁 OCR и Обучение (Matching) - -#### 1. Получить справочник товаров (для селекта) -Используется для построения локального индекса поиска (autocomplete) на фронтенде, чтобы привязывать позиции. Возвращает только активные товары (`GOODS`). - -* **URL:** `/ocr/catalog` -* **Method:** `GET` -* **Response:** `200 OK` - -```json -[ - { - "id": "607a1e96-f539-45d2-8709-3919f94bdc3e", - "name": "Молоко Домик в Деревне 3.2%", - "code": "00123" + "id": "uuid-butter...", + "name": "Масло сливочное 82%", + "code": "00123", + "measure_unit": "кг", + "containers": [ + { + "id": "uuid-pack...", + "name": "Пачка 180г", + "count": 0.180 + }, + { + "id": "uuid-box...", + "name": "Коробка (20 пачек)", + "count": 3.6 + } + ] }, - ... + { + "id": "uuid-milk...", + "name": "Молоко 3.2%", + "code": "00124", + "measure_unit": "л", + "containers": [] // Пустой массив, если фасовок нет + } ] ``` -#### 2. Получить таблицу обучения (Matches) -Возвращает список уже созданных связей "Грязное название из чека" -> "Чистый товар iiko". +--- + +### `GET /ocr/matches` +Получить список уже обученных позиций. Используется для отображения таблицы "Ранее сохраненные связи". -* **URL:** `/ocr/matches` -* **Method:** `GET` * **Response:** `200 OK` ```json [ { - "raw_name": "молоко двд 3,2", - "product_id": "607a1e96-f539-45d2-8709-3919f94bdc3e", + "raw_name": "Масло слив. 3 пачки", + "product_id": "uuid-butter...", "product": { - "ID": "607a1e96-f539-45d2-8709-3919f94bdc3e", - "Name": "Молоко Домик в Деревне 3.2%", - ... + "id": "uuid-butter...", + "name": "Масло сливочное 82%", + "measure_unit": "кг" + // ...остальные поля product }, - "updated_at": "..." + "quantity": 3, + "container_id": "uuid-pack...", + "container": { + "id": "uuid-pack...", + "name": "Пачка 180г", + "count": 0.180 + }, + "updated_at": "2023-10-27T10:00:00Z" } ] ``` +> **Логика отображения на фронте:** +> Если `container` пришел (не null) -> отображаем: `Quantity` x `Container.Name` (3 x Пачка 180г). +> Если `container` null -> отображаем: `Quantity` `Product.MeasureUnit` (0.54 кг). -#### 3. Создать/Обновить привязку -Сохраняет правило: "Если в чеке встретишь этот текст, считай это вот этим товаром". +--- + +### `POST /ocr/match` +Создать или обновить привязку. -* **URL:** `/ocr/match` -* **Method:** `POST` * **Body:** +**Вариант 1: Базовая единица (без фасовки)** +Пользователь выбрал "Петрушка", ввел "0.5" (кг). ```json { - "raw_name": "молоко двд 3,2", - "product_id": "607a1e96-f539-45d2-8709-3919f94bdc3e" + "raw_name": "петрушка вес", + "product_id": "uuid-parsley...", + "quantity": 0.5 + // container_id не передаем или null +} +``` + +**Вариант 2: С фасовкой** +Пользователь выбрал "Масло", выбрал фасовку "Коробка", ввел "2" (штуки). +```json +{ + "raw_name": "масло коробка", + "product_id": "uuid-butter...", + "quantity": 2, + "container_id": "uuid-box..." } ``` @@ -163,78 +166,79 @@ export interface CreateInvoiceRequest { --- -### 📄 Накладные (Invoices) +### `GET /ocr/unmatched` +Получить список частых нераспознанных товаров. +Используется для автодополнения (Suggest) в поле ввода "Текст из чека", чтобы пользователь не вводил название вручную. -#### Отправить накладную в iikoRMS -Создает черновик приходной накладной в системе iiko. +* **Response:** `200 OK` -* **URL:** `/invoices/send` -* **Method:** `POST` -* **Body:** +```json +[ + { + "raw_name": "Хлеб Бородинский нар.", + "count": 12, + "last_seen": "2023-10-27T12:00:00Z" + }, + { + "raw_name": "Пакет майка", + "count": 5, + "last_seen": "2023-10-26T09:00:00Z" + } +] +``` + +--- + +## 3. Эндпоинты: Накладные (Invoices) + +### `POST /invoices/send` +Отправка накладной в iiko. + +**Важно:** При формировании накладной на фронте, вы должны пересчитывать количество в базовые единицы, либо (в будущем) мы научим бэкенд принимать ID фасовки в `items`. +На данный момент API ожидает `amount` и `price` уже приведенными к единому знаменателю, либо iiko сама разберется, если мы передадим ContainerID (этот функционал в разработке на стороне `rms_client`, пока шлите базовые единицы). ```json { - "document_number": "INV-12345", + "document_number": "INV-100", "date_incoming": "2023-10-27", - "supplier_id": "uuid-supplier...", - "store_id": "uuid-store...", + "supplier_id": "uuid...", + "store_id": "uuid...", "items": [ { - "product_id": "uuid-product...", - "amount": 10.5, - "price": 150.00 + "product_id": "uuid...", + "amount": 1.5, // 1.5 кг + "price": 100 // Цена за кг } ] } ``` -* **Response:** `200 OK` +--- + +## 4. Эндпоинты: Аналитика + +### `GET /recommendations` +Возвращает список проблем (товары без техкарт, без закупок и т.д.). ```json -{ - "status": "ok", - "created_number": "000123" // Номер документа, присвоенный iiko -} +[ + { + "ID": "uuid...", + "Type": "UNUSED_IN_RECIPES", + "ProductID": "uuid...", + "ProductName": "Лист салата", + "Reason": "Товар не используется ни в одной техкарте", + "CreatedAt": "..." + } +] ``` --- -### ⚙️ System - -#### Healthcheck -Проверка доступности API. - -* **URL:** `http://localhost:8080/health` -* **Method:** `GET` -* **Response:** +## 5. System +### `GET /health` +Проверка статуса. ```json -{ - "status": "ok", - "time": "2023-10-27T12:34:56+03:00" -} -``` - ---- - -## 4. Сценарии использования (Frontend Workflow) - -### Сценарий А: "Обучение" (раздел Settings / OCR Learning) -1. При загрузке страницы вызвать `GET /api/ocr/matches` и отобразить таблицу. -2. Вызвать `GET /api/ocr/catalog` и сохранить в памяти для быстрого поиска (Combobox/Select). -3. Пользователь может добавить новую связь вручную: - * Вводит текст (например, "Хлеб Бородинский"). - * Выбирает товар из выпадающего списка. - * Нажимает "Save" -> вызывается `POST /api/ocr/match`. - * Таблица обновляется. - -### Сценарий Б: "Дашборд аналитика" -1. При загрузке главной страницы вызвать `GET /api/recommendations`. -2. Сгруппировать массив по полю `Type` или `Reason`. -3. Отобразить карточки: "Товары без техкарт (5 шт)", "Ингредиенты без закупок (12 шт)". - -### Сценарий В: "Создание накладной" (Пока ручное) -*Пока нет загрузки фото через Web, предполагается ручной ввод или редактирование данных, полученных иным путем.* -1. Форма ввода номера, даты. -2. Таблица товаров (добавление строк через поиск по каталогу). -3. Кнопка "Отправить в iiko" -> `POST /api/invoices/send`. \ No newline at end of file +{"status": "ok", "time": "..."} +``` \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 1f94fa3..0d84203 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -161,6 +161,7 @@ func main() { api.GET("/ocr/catalog", ocrHandler.GetCatalog) api.GET("/ocr/matches", ocrHandler.GetMatches) api.POST("/ocr/match", ocrHandler.SaveMatch) + api.GET("/ocr/unmatched", ocrHandler.GetUnmatched) } // Простой хелсчек diff --git a/internal/domain/catalog/entity.go b/internal/domain/catalog/entity.go index c4b500c..c46a27e 100644 --- a/internal/domain/catalog/entity.go +++ b/internal/domain/catalog/entity.go @@ -7,27 +7,51 @@ import ( "github.com/shopspring/decimal" ) +// MeasureUnit - Единица измерения (kg, l, pcs) +type MeasureUnit struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"` + Name string `gorm:"type:varchar(50);not null" json:"name"` + Code string `gorm:"type:varchar(50)" json:"code"` +} + +// ProductContainer - Фасовка (упаковка) товара +type ProductContainer struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"` + ProductID uuid.UUID `gorm:"type:uuid;index;not null" json:"product_id"` + Name string `gorm:"type:varchar(100);not null" json:"name"` + Count decimal.Decimal `gorm:"type:numeric(19,4);not null" json:"count"` // Коэфф. пересчета +} + // Product - Номенклатура type Product struct { - ID uuid.UUID `gorm:"type:uuid;primary_key;"` - ParentID *uuid.UUID `gorm:"type:uuid;index"` - Name string `gorm:"type:varchar(255);not null"` - Type string `gorm:"type:varchar(50);index"` // GOODS, DISH, PREPARED, etc. - Num string `gorm:"type:varchar(50)"` - Code string `gorm:"type:varchar(50)"` - UnitWeight decimal.Decimal `gorm:"type:numeric(19,4)"` - UnitCapacity decimal.Decimal `gorm:"type:numeric(19,4)"` - IsDeleted bool `gorm:"default:false"` + ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"` + ParentID *uuid.UUID `gorm:"type:uuid;index" json:"parent_id"` + Name string `gorm:"type:varchar(255);not null" json:"name"` + Type string `gorm:"type:varchar(50);index" json:"type"` // GOODS, DISH, PREPARED + Num string `gorm:"type:varchar(50)" json:"num"` + Code string `gorm:"type:varchar(50)" json:"code"` + UnitWeight decimal.Decimal `gorm:"type:numeric(19,4)" json:"unit_weight"` + UnitCapacity decimal.Decimal `gorm:"type:numeric(19,4)" json:"unit_capacity"` - Parent *Product `gorm:"foreignKey:ParentID"` - Children []*Product `gorm:"foreignKey:ParentID"` + // Связь с единицей измерения + MainUnitID *uuid.UUID `gorm:"type:uuid;index" json:"main_unit_id"` + MainUnit *MeasureUnit `gorm:"foreignKey:MainUnitID" json:"main_unit,omitempty"` - CreatedAt time.Time - UpdatedAt time.Time + // Фасовки + Containers []ProductContainer `gorm:"foreignKey:ProductID;constraint:OnDelete:CASCADE" json:"containers,omitempty"` + + IsDeleted bool `gorm:"default:false" json:"is_deleted"` + + Parent *Product `gorm:"foreignKey:ParentID" json:"-"` + Children []*Product `gorm:"foreignKey:ParentID" json:"-"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // Repository интерфейс для каталога type Repository interface { + SaveMeasureUnits(units []MeasureUnit) error SaveProducts(products []Product) error GetAll() ([]Product, error) GetActiveGoods() ([]Product, error) diff --git a/internal/domain/ocr/entity.go b/internal/domain/ocr/entity.go index b88fc59..2fbdadf 100644 --- a/internal/domain/ocr/entity.go +++ b/internal/domain/ocr/entity.go @@ -3,32 +3,44 @@ package ocr import ( "time" - "github.com/google/uuid" "rmser/internal/domain/catalog" + + "github.com/google/uuid" + "github.com/shopspring/decimal" ) // ProductMatch связывает текст из чека с конкретным товаром в iiko type ProductMatch struct { - // RawName - распознанный текст (ключ). - // Лучше хранить в нижнем регистре и без лишних пробелов. - RawName string `gorm:"type:varchar(255);primary_key"` + RawName string `gorm:"type:varchar(255);primary_key" json:"raw_name"` + ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id"` + Product catalog.Product `gorm:"foreignKey:ProductID" json:"product"` - ProductID uuid.UUID `gorm:"type:uuid;not null;index"` + // Количество и фасовки + Quantity decimal.Decimal `gorm:"type:numeric(19,4);default:1" json:"quantity"` + ContainerID *uuid.UUID `gorm:"type:uuid;index" json:"container_id"` - // Product - связь для GORM - Product catalog.Product `gorm:"foreignKey:ProductID"` + // Для подгрузки данных о фасовке при чтении + Container *catalog.ProductContainer `gorm:"foreignKey:ContainerID" json:"container,omitempty"` - UpdatedAt time.Time - CreatedAt time.Time + UpdatedAt time.Time `json:"updated_at"` + CreatedAt time.Time `json:"created_at"` +} + +// UnmatchedItem хранит строки, которые не удалось распознать, для подсказок +type UnmatchedItem struct { + RawName string `gorm:"type:varchar(255);primary_key" json:"raw_name"` + Count int `gorm:"default:1" json:"count"` // Сколько раз встречалось + LastSeen time.Time `json:"last_seen"` } type Repository interface { - // SaveMatch сохраняет или обновляет привязку - SaveMatch(rawName string, productID uuid.UUID) error + // SaveMatch теперь принимает quantity и containerID + SaveMatch(rawName string, productID uuid.UUID, quantity decimal.Decimal, containerID *uuid.UUID) error - // FindMatch ищет товар по точному совпадению названия - FindMatch(rawName string) (*uuid.UUID, error) - - // GetAllMatches возвращает все существующие привязки + FindMatch(rawName string) (*ProductMatch, error) // Возвращаем полную структуру, чтобы получить qty GetAllMatches() ([]ProductMatch, error) + + UpsertUnmatched(rawName string) error + GetTopUnmatched(limit int) ([]UnmatchedItem, error) + DeleteUnmatched(rawName string) error } diff --git a/internal/infrastructure/db/postgres.go b/internal/infrastructure/db/postgres.go index a57c4a6..70efb4f 100644 --- a/internal/infrastructure/db/postgres.go +++ b/internal/infrastructure/db/postgres.go @@ -46,6 +46,8 @@ func NewPostgresDB(dsn string) *gorm.DB { // 4. Автомиграция err = db.AutoMigrate( &catalog.Product{}, + &catalog.MeasureUnit{}, + &catalog.ProductContainer{}, &recipes.Recipe{}, &recipes.RecipeItem{}, &invoices.Invoice{}, @@ -53,6 +55,7 @@ func NewPostgresDB(dsn string) *gorm.DB { &operations.StoreOperation{}, &recommendations.Recommendation{}, &ocr.ProductMatch{}, + &ocr.UnmatchedItem{}, ) if err != nil { panic(fmt.Sprintf("ошибка миграции БД: %v", err)) diff --git a/internal/infrastructure/repository/catalog/postgres.go b/internal/infrastructure/repository/catalog/postgres.go index 4759fce..ec1055f 100644 --- a/internal/infrastructure/repository/catalog/postgres.go +++ b/internal/infrastructure/repository/catalog/postgres.go @@ -16,22 +16,53 @@ func NewRepository(db *gorm.DB) catalog.Repository { return &pgRepository{db: db} } -func (r *pgRepository) SaveProducts(products []catalog.Product) error { - // Сортировка (родители -> дети) - sorted := sortProductsByHierarchy(products) +func (r *pgRepository) SaveMeasureUnits(units []catalog.MeasureUnit) error { + if len(units) == 0 { + return nil + } return r.db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "id"}}, UpdateAll: true, - }).CreateInBatches(sorted, 100).Error + }).CreateInBatches(units, 100).Error +} + +func (r *pgRepository) SaveProducts(products []catalog.Product) error { + sorted := sortProductsByHierarchy(products) + return r.db.Transaction(func(tx *gorm.DB) error { + // 1. Сохраняем продукты (без контейнеров, чтобы ускорить и не дублировать) + if err := tx.Omit("Containers").Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "id"}}, + UpdateAll: true, + }).CreateInBatches(sorted, 100).Error; err != nil { + return err + } + + // 2. Собираем все контейнеры в один слайс + var allContainers []catalog.ProductContainer + for _, p := range products { + allContainers = append(allContainers, p.Containers...) + } + + // 3. Сохраняем контейнеры + if len(allContainers) > 0 { + if err := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "id"}}, + UpdateAll: true, + }).CreateInBatches(allContainers, 100).Error; err != nil { + return err + } + } + return nil + }) } func (r *pgRepository) GetAll() ([]catalog.Product, error) { var products []catalog.Product - err := r.db.Find(&products).Error + err := r.db.Preload("MainUnit").Find(&products).Error return products, err } -// Вспомогательная функция сортировки +// Вспомогательная функция сортировки (оставляем как была) func sortProductsByHierarchy(products []catalog.Product) []catalog.Product { if len(products) == 0 { return products @@ -73,11 +104,14 @@ func sortProductsByHierarchy(products []catalog.Product) []catalog.Product { return result } -// GetActiveGoods возвращает только активные товары (не удаленные, тип GOODS) +// GetActiveGoods возвращает только активные товары c подгруженной единицей измерения +// GetActiveGoods оптимизирован: подгружаем Units и Containers func (r *pgRepository) GetActiveGoods() ([]catalog.Product, error) { var products []catalog.Product - // iikoRMS: GOODS - товары, PREPARED - заготовки (иногда их тоже покупают) - err := r.db.Where("is_deleted = ? AND type IN ?", false, []string{"GOODS"}). + err := r.db. + Preload("MainUnit"). + Preload("Containers"). // <-- Подгружаем фасовки + Where("is_deleted = ? AND type IN ?", false, []string{"GOODS"}). Order("name ASC"). Find(&products).Error return products, err diff --git a/internal/infrastructure/repository/ocr/postgres.go b/internal/infrastructure/repository/ocr/postgres.go index 635d344..be8fca4 100644 --- a/internal/infrastructure/repository/ocr/postgres.go +++ b/internal/infrastructure/repository/ocr/postgres.go @@ -2,6 +2,7 @@ package ocr import ( "strings" + "time" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -9,6 +10,7 @@ import ( "rmser/internal/domain/ocr" "github.com/google/uuid" + "github.com/shopspring/decimal" ) type pgRepository struct { @@ -19,39 +21,88 @@ func NewRepository(db *gorm.DB) ocr.Repository { return &pgRepository{db: db} } -func (r *pgRepository) SaveMatch(rawName string, productID uuid.UUID) error { +func (r *pgRepository) SaveMatch(rawName string, productID uuid.UUID, quantity decimal.Decimal, containerID *uuid.UUID) error { normalized := strings.ToLower(strings.TrimSpace(rawName)) match := ocr.ProductMatch{ - RawName: normalized, - ProductID: productID, + RawName: normalized, + ProductID: productID, + Quantity: quantity, + ContainerID: containerID, } - // Upsert: если такая строка уже была, обновляем ссылку на товар - return r.db.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "raw_name"}}, - DoUpdates: clause.AssignmentColumns([]string{"product_id", "updated_at"}), - }).Create(&match).Error + return r.db.Transaction(func(tx *gorm.DB) error { + if err := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "raw_name"}}, + DoUpdates: clause.AssignmentColumns([]string{"product_id", "quantity", "container_id", "updated_at"}), + }).Create(&match).Error; err != nil { + return err + } + + if err := tx.Where("raw_name = ?", normalized).Delete(&ocr.UnmatchedItem{}).Error; err != nil { + return err + } + return nil + }) } -func (r *pgRepository) FindMatch(rawName string) (*uuid.UUID, error) { +func (r *pgRepository) FindMatch(rawName string) (*ocr.ProductMatch, error) { normalized := strings.ToLower(strings.TrimSpace(rawName)) var match ocr.ProductMatch - err := r.db.Where("raw_name = ?", normalized).First(&match).Error + // Preload Container на случай, если нам сразу нужна инфа + err := r.db.Preload("Container").Where("raw_name = ?", normalized).First(&match).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } return nil, err } - - return &match.ProductID, nil + return &match, nil } func (r *pgRepository) GetAllMatches() ([]ocr.ProductMatch, error) { var matches []ocr.ProductMatch - // Preload("Product") загружает связанную сущность товара, - // чтобы мы видели не только ID, но и название товара из каталога. - err := r.db.Preload("Product").Order("updated_at DESC").Find(&matches).Error + // Подгружаем Товар, Единицу и Фасовку + err := r.db. + Preload("Product"). + Preload("Product.MainUnit"). + Preload("Container"). + Order("updated_at DESC"). + Find(&matches).Error return matches, err } + +// UpsertUnmatched увеличивает счетчик встречаемости +func (r *pgRepository) UpsertUnmatched(rawName string) error { + normalized := strings.ToLower(strings.TrimSpace(rawName)) + if normalized == "" { + return nil + } + + // Используем сырой SQL или GORM upsert expression + // PostgreSQL: INSERT ... ON CONFLICT DO UPDATE SET count = count + 1 + item := ocr.UnmatchedItem{ + RawName: normalized, + Count: 1, + LastSeen: time.Now(), + } + + return r.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "raw_name"}}, + DoUpdates: clause.Assignments(map[string]interface{}{ + "count": gorm.Expr("unmatched_items.count + 1"), + "last_seen": time.Now(), + }), + }).Create(&item).Error +} + +func (r *pgRepository) GetTopUnmatched(limit int) ([]ocr.UnmatchedItem, error) { + var items []ocr.UnmatchedItem + err := r.db.Order("count DESC, last_seen DESC").Limit(limit).Find(&items).Error + return items, err +} + +func (r *pgRepository) DeleteUnmatched(rawName string) error { + normalized := strings.ToLower(strings.TrimSpace(rawName)) + return r.db.Where("raw_name = ?", normalized).Delete(&ocr.UnmatchedItem{}).Error +} diff --git a/internal/infrastructure/rms/client.go b/internal/infrastructure/rms/client.go index 5a7dfc5..bc80413 100644 --- a/internal/infrastructure/rms/client.go +++ b/internal/infrastructure/rms/client.go @@ -32,6 +32,7 @@ type ClientI interface { Auth() error Logout() error FetchCatalog() ([]catalog.Product, error) + FetchMeasureUnits() ([]catalog.MeasureUnit, error) FetchRecipes(dateFrom, dateTo time.Time) ([]recipes.Recipe, error) FetchInvoices(from, to time.Time) ([]invoices.Invoice, error) FetchStoreOperations(presetID string, from, to time.Time) ([]StoreReportItemXML, error) @@ -295,6 +296,29 @@ func (c *Client) FetchCatalog() ([]catalog.Product, error) { parentID = &pid } } + + // Обработка MainUnit + var mainUnitID *uuid.UUID + if p.MainUnit != nil { + if uid, err := uuid.Parse(*p.MainUnit); err == nil { + mainUnitID = &uid + } + } + + // Маппинг фасовок + var containers []catalog.ProductContainer + for _, contDto := range p.Containers { + cID, err := uuid.Parse(contDto.ID) + if err == nil { + containers = append(containers, catalog.ProductContainer{ + ID: cID, + ProductID: id, + Name: contDto.Name, + Count: decimal.NewFromFloat(contDto.Count), + }) + } + } + products = append(products, catalog.Product{ ID: id, ParentID: parentID, @@ -304,6 +328,8 @@ func (c *Client) FetchCatalog() ([]catalog.Product, error) { Type: p.Type, UnitWeight: decimal.NewFromFloat(p.UnitWeight), UnitCapacity: decimal.NewFromFloat(p.UnitCapacity), + MainUnitID: mainUnitID, + Containers: containers, IsDeleted: p.Deleted, }) } @@ -311,6 +337,38 @@ func (c *Client) FetchCatalog() ([]catalog.Product, error) { return products, nil } +// FetchMeasureUnits загружает справочник единиц измерения +func (c *Client) FetchMeasureUnits() ([]catalog.MeasureUnit, error) { + // rootType=MeasureUnit согласно документации iiko + resp, err := c.doRequest("GET", "/resto/api/v2/entities/list", map[string]string{ + "rootType": "MeasureUnit", + "includeDeleted": "false", + }) + if err != nil { + return nil, fmt.Errorf("get measure units error: %w", err) + } + defer resp.Body.Close() + + var dtos []GenericEntityDTO + if err := json.NewDecoder(resp.Body).Decode(&dtos); err != nil { + return nil, fmt.Errorf("json decode error: %w", err) + } + + var result []catalog.MeasureUnit + for _, d := range dtos { + id, err := uuid.Parse(d.ID) + if err != nil { + continue + } + result = append(result, catalog.MeasureUnit{ + ID: id, + Name: d.Name, + Code: d.Code, + }) + } + return result, nil +} + func (c *Client) FetchRecipes(dateFrom, dateTo time.Time) ([]recipes.Recipe, error) { params := map[string]string{ "dateFrom": dateFrom.Format("2006-01-02"), diff --git a/internal/infrastructure/rms/dto.go b/internal/infrastructure/rms/dto.go index 29088f1..97f20af 100644 --- a/internal/infrastructure/rms/dto.go +++ b/internal/infrastructure/rms/dto.go @@ -7,15 +7,32 @@ import ( // --- JSON DTOs (V2 API) --- type ProductDTO struct { - ID string `json:"id"` - ParentID *string `json:"parent"` // Может быть null - Name string `json:"name"` - Num string `json:"num"` // Артикул - Code string `json:"code"` // Код быстрого набора - Type string `json:"type"` // GOODS, DISH, PREPARED, etc. - UnitWeight float64 `json:"unitWeight"` - UnitCapacity float64 `json:"unitCapacity"` - Deleted bool `json:"deleted"` + ID string `json:"id"` + ParentID *string `json:"parent"` // Может быть null + Name string `json:"name"` + Num string `json:"num"` // Артикул + Code string `json:"code"` // Код быстрого набора + Type string `json:"type"` // GOODS, DISH, PREPARED, etc. + UnitWeight float64 `json:"unitWeight"` + UnitCapacity float64 `json:"unitCapacity"` + MainUnit *string `json:"mainUnit"` + Containers []ContainerDTO `json:"containers"` + Deleted bool `json:"deleted"` +} + +// GenericEntityDTO используется для простых справочников (MeasureUnit и др.) +type GenericEntityDTO struct { + ID string `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + Deleted bool `json:"deleted"` +} + +// ContainerDTO - фасовка из iiko +type ContainerDTO struct { + ID string `json:"id"` + Name string `json:"name"` // Название фасовки (напр. "Коробка") + Count float64 `json:"count"` // Сколько базовых единиц в фасовке } type GroupDTO struct { diff --git a/internal/services/ocr/service.go b/internal/services/ocr/service.go index 9a4b384..24ead05 100644 --- a/internal/services/ocr/service.go +++ b/internal/services/ocr/service.go @@ -42,8 +42,6 @@ func (s *Service) ProcessReceiptImage(ctx context.Context, imgData []byte) ([]Pr } var processed []ProcessedItem - - // 2. Обрабатываем каждую строку for _, rawItem := range rawResult.Items { item := ProcessedItem{ RawName: rawItem.RawName, @@ -52,26 +50,24 @@ func (s *Service) ProcessReceiptImage(ctx context.Context, imgData []byte) ([]Pr Sum: decimal.NewFromFloat(rawItem.Sum), } - // 3. Ищем соответствие - // Сначала проверяем таблицу ручного обучения (product_matches) - matchID, err := s.ocrRepo.FindMatch(rawItem.RawName) + match, err := s.ocrRepo.FindMatch(rawItem.RawName) if err != nil { logger.Log.Error("db error finding match", zap.Error(err)) } - if matchID != nil { - // Нашли в обучении - item.ProductID = matchID + if match != nil { + item.ProductID = &match.ProductID item.IsMatched = true item.MatchSource = "learned" + // Здесь мы могли бы подтянуть quantity/container из матча, + // но пока фронт сам это сделает, запросив /ocr/matches или получив подсказку. } else { - // Если не нашли, пробуем найти точное совпадение по имени в каталоге (на всякий случай) - // (В реальном проекте тут может быть нечеткий поиск, но пока точный) + if err := s.ocrRepo.UpsertUnmatched(rawItem.RawName); err != nil { + logger.Log.Warn("failed to save unmatched", zap.Error(err)) + } } - processed = append(processed, item) } - return processed, nil } @@ -87,14 +83,21 @@ type ProcessedItem struct { MatchSource string // "learned", "auto", "manual" } -// ProductForIndex DTO для внешнего сервиса -type ProductForIndex struct { - ID string `json:"id"` - Name string `json:"name"` - Code string `json:"code"` +type ContainerForIndex struct { + ID string `json:"id"` + Name string `json:"name"` + Count float64 `json:"count"` } -// GetCatalogForIndexing возвращает список товаров для построения индекса +type ProductForIndex struct { + ID string `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + MeasureUnit string `json:"measure_unit"` + Containers []ContainerForIndex `json:"containers"` +} + +// GetCatalogForIndexing - возвращает облегченный каталог func (s *Service) GetCatalogForIndexing() ([]ProductForIndex, error) { products, err := s.catalogRepo.GetActiveGoods() if err != nil { @@ -103,18 +106,35 @@ func (s *Service) GetCatalogForIndexing() ([]ProductForIndex, error) { result := make([]ProductForIndex, 0, len(products)) for _, p := range products { + uom := "" + if p.MainUnit != nil { + uom = p.MainUnit.Name + } + + var conts []ContainerForIndex + for _, c := range p.Containers { + cnt, _ := c.Count.Float64() + conts = append(conts, ContainerForIndex{ + ID: c.ID.String(), + Name: c.Name, + Count: cnt, + }) + } + result = append(result, ProductForIndex{ - ID: p.ID.String(), - Name: p.Name, - Code: p.Code, + ID: p.ID.String(), + Name: p.Name, + Code: p.Code, + MeasureUnit: uom, + Containers: conts, }) } return result, nil } -// SaveMapping сохраняет связь "Текст из чека" -> "Наш товар" -func (s *Service) SaveMapping(rawName string, productID uuid.UUID) error { - return s.ocrRepo.SaveMatch(rawName, productID) +// SaveMapping сохраняет привязку с количеством и фасовкой +func (s *Service) SaveMapping(rawName string, productID uuid.UUID, quantity decimal.Decimal, containerID *uuid.UUID) error { + return s.ocrRepo.SaveMatch(rawName, productID, quantity, containerID) } // GetKnownMatches возвращает список всех обученных связей @@ -122,8 +142,14 @@ func (s *Service) GetKnownMatches() ([]ocr.ProductMatch, error) { return s.ocrRepo.GetAllMatches() } +// GetUnmatchedItems возвращает список частых нераспознанных строк +func (s *Service) GetUnmatchedItems() ([]ocr.UnmatchedItem, error) { + // Берем топ 50 нераспознанных + return s.ocrRepo.GetTopUnmatched(50) +} + // FindKnownMatch ищет, знаем ли мы уже этот товар -func (s *Service) FindKnownMatch(rawName string) (*uuid.UUID, error) { +func (s *Service) FindKnownMatch(rawName string) (*ocr.ProductMatch, error) { return s.ocrRepo.FindMatch(rawName) } diff --git a/internal/services/sync/service.go b/internal/services/sync/service.go index 4231fe9..7dcb02d 100644 --- a/internal/services/sync/service.go +++ b/internal/services/sync/service.go @@ -49,8 +49,15 @@ func NewService( // SyncCatalog загружает номенклатуру и сохраняет в БД func (s *Service) SyncCatalog() error { - logger.Log.Info("Начало синхронизации номенклатуры") + logger.Log.Info("Начало синхронизации каталога...") + // 1. Сначала Единицы измерения (чтобы FK не ругался) + if err := s.syncMeasureUnits(); err != nil { + return err + } + + // 2. Товары + logger.Log.Info("Запрос товаров из RMS...") products, err := s.rmsClient.FetchCatalog() if err != nil { return fmt.Errorf("ошибка получения каталога из RMS: %w", err) @@ -64,6 +71,19 @@ func (s *Service) SyncCatalog() error { return nil } +func (s *Service) syncMeasureUnits() error { + logger.Log.Info("Синхронизация единиц измерения...") + units, err := s.rmsClient.FetchMeasureUnits() + if err != nil { + return fmt.Errorf("ошибка получения ед.изм: %w", err) + } + if err := s.catalogRepo.SaveMeasureUnits(units); err != nil { + return fmt.Errorf("ошибка сохранения ед.изм: %w", err) + } + logger.Log.Info("Единицы измерения обновлены", zap.Int("count", len(units))) + return nil +} + // SyncRecipes загружает техкарты за указанный период (или за последние 30 дней по умолчанию) func (s *Service) SyncRecipes() error { logger.Log.Info("Начало синхронизации техкарт") diff --git a/internal/transport/http/handlers/ocr.go b/internal/transport/http/handlers/ocr.go index d0a1204..9614d7e 100644 --- a/internal/transport/http/handlers/ocr.go +++ b/internal/transport/http/handlers/ocr.go @@ -5,6 +5,7 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/shopspring/decimal" "go.uber.org/zap" ocrService "rmser/internal/services/ocr" @@ -31,8 +32,10 @@ func (h *OCRHandler) GetCatalog(c *gin.Context) { } type MatchRequest struct { - RawName string `json:"raw_name" binding:"required"` - ProductID string `json:"product_id" binding:"required"` + RawName string `json:"raw_name" binding:"required"` + ProductID string `json:"product_id" binding:"required"` + Quantity float64 `json:"quantity"` + ContainerID *string `json:"container_id"` } // SaveMatch сохраняет привязку (обучение) @@ -49,7 +52,19 @@ func (h *OCRHandler) SaveMatch(c *gin.Context) { return } - if err := h.service.SaveMapping(req.RawName, pID); err != nil { + qty := decimal.NewFromFloat(1.0) + if req.Quantity > 0 { + qty = decimal.NewFromFloat(req.Quantity) + } + + var contID *uuid.UUID + if req.ContainerID != nil && *req.ContainerID != "" { + if uid, err := uuid.Parse(*req.ContainerID); err == nil { + contID = &uid + } + } + + if err := h.service.SaveMapping(req.RawName, pID, qty, contID); err != nil { logger.Log.Error("Ошибка сохранения матчинга", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -69,3 +84,14 @@ func (h *OCRHandler) GetMatches(c *gin.Context) { c.JSON(http.StatusOK, matches) } + +// GetUnmatched возвращает список нераспознанных позиций для подсказок +func (h *OCRHandler) GetUnmatched(c *gin.Context) { + items, err := h.service.GetUnmatchedItems() + if err != nil { + logger.Log.Error("Ошибка получения списка unmatched", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, items) +} diff --git a/project_dump.py b/project_dump.py index d68c7ec..0d0021c 100644 --- a/project_dump.py +++ b/project_dump.py @@ -4,11 +4,14 @@ project_tree = ''' . +├── API_DOCS.md +├── Dockerfile ├── cmd │ └── main.go ├── config │ └── config.go ├── config.yaml +├── docker-compose.yml ├── go.mod ├── go.sum ├── internal @@ -18,6 +21,8 @@ project_tree = ''' │ │ ├── interfaces.go │ │ ├── invoices │ │ │ └── entity.go +│ │ ├── ocr +│ │ │ └── entity.go │ │ ├── operations │ │ │ └── entity.go │ │ ├── recipes @@ -27,6 +32,9 @@ project_tree = ''' │ ├── infrastructure │ │ ├── db │ │ │ └── postgres.go +│ │ ├── ocr_client +│ │ │ ├── client.go +│ │ │ └── dto.go │ │ ├── redis │ │ │ └── client.go │ │ ├── repository @@ -34,6 +42,8 @@ project_tree = ''' │ │ │ │ └── postgres.go │ │ │ ├── invoices │ │ │ │ └── postgres.go +│ │ │ ├── ocr +│ │ │ │ └── postgres.go │ │ │ ├── operations │ │ │ │ └── postgres.go │ │ │ ├── recipes @@ -43,39 +53,114 @@ project_tree = ''' │ │ └── rms │ │ ├── client.go │ │ └── dto.go -│ └── services -│ ├── recommend -│ │ └── service.go -│ └── sync -│ └── service.go +│ ├── services +│ │ ├── invoices +│ │ │ └── service.go +│ │ ├── ocr +│ │ │ └── service.go +│ │ ├── recommend +│ │ │ └── service.go +│ │ └── sync +│ │ └── service.go +│ └── transport +│ ├── http +│ │ └── handlers +│ │ ├── invoices.go +│ │ ├── ocr.go +│ │ └── recommendations.go +│ └── telegram +│ └── bot.go +├── mini-app +├── ocr-service +│ ├── .dockerignore +│ ├── Dockerfile +│ ├── imgproc.py +│ ├── main.py +│ ├── ocr.py +│ ├── parser.py +│ ├── qr_manager.py +│ ├── requirements.txt +│ └── system-prompt.md ├── pack_go_files.py -└── pkg - └── logger - └── logger.go +├── pkg +│ └── logger +│ └── logger.go +└── rmser-view + ├── .env + ├── README.md + ├── eslint.config.js + ├── index.html + ├── pack_react_files.py + ├── package-lock.json + ├── package.json + ├── public + │ └── vite.svg + ├── src + │ ├── App.tsx + │ ├── components + │ │ ├── invoices + │ │ ├── layout + │ │ │ ├── AppLayout.tsx + │ │ │ └── Providers.tsx + │ │ ├── ocr + │ │ │ ├── AddMatchForm.tsx + │ │ │ ├── CatalogSelect.tsx + │ │ │ └── MatchList.tsx + │ │ ├── recommendations + │ │ │ └── RecommendationCard.tsx + │ │ └── ui + │ ├── hooks + │ │ ├── useOcr.ts + │ │ └── useRecommendations.ts + │ ├── main.tsx + │ ├── pages + │ │ ├── Dashboard.tsx + │ │ └── OcrLearning.tsx + │ ├── services + │ │ ├── api.ts + │ │ └── types.ts + │ ├── stores + │ └── utils + ├── tsconfig.app.json + ├── tsconfig.json + ├── tsconfig.node.json + └── vite.config.ts ''' project_files = { - "cmd/main.go": "package main\n\nimport (\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"go.uber.org/zap\"\n\n\t\"rmser/config\"\n\t\"rmser/internal/infrastructure/db\"\n\n\t// \"rmser/internal/infrastructure/redis\"\n\tcatalogPkg \"rmser/internal/infrastructure/repository/catalog\"\n\tinvoicesPkg \"rmser/internal/infrastructure/repository/invoices\"\n\topsRepoPkg \"rmser/internal/infrastructure/repository/operations\"\n\trecipesPkg \"rmser/internal/infrastructure/repository/recipes\"\n\trecRepoPkg \"rmser/internal/infrastructure/repository/recommendations\"\n\t\"rmser/internal/infrastructure/rms\"\n\trecServicePkg \"rmser/internal/services/recommend\"\n\t\"rmser/internal/services/sync\"\n\t\"rmser/pkg/logger\"\n)\n\nfunc main() {\n\t// 1. Загрузка конфигурации\n\tcfg, err := config.LoadConfig(\".\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Ошибка загрузки конфига: %v\", err)\n\t}\n\n\t// 2. Инициализация логгера\n\tlogger.Init(cfg.App.Mode)\n\tdefer logger.Log.Sync()\n\n\tlogger.Log.Info(\"Запуск приложения rmser\", zap.String(\"mode\", cfg.App.Mode))\n\n\t// 3a. Подключение Redis (Новое)\n\t// redisClient, err := redis.NewClient(cfg.Redis.Addr, cfg.Redis.Password, cfg.Redis.DB)\n\t// if err != nil {\n\t// \tlogger.Log.Fatal(\"Ошибка подключения к Redis\", zap.Error(err))\n\t// }\n\n\t// 3. Подключение к БД (PostgreSQL)\n\tdatabase := db.NewPostgresDB(cfg.DB.DSN)\n\n\t// 4. Инициализация слоев\n\trmsClient := rms.NewClient(cfg.RMS.BaseURL, cfg.RMS.Login, cfg.RMS.Password)\n\tcatalogRepo := catalogPkg.NewRepository(database)\n\trecipesRepo := recipesPkg.NewRepository(database)\n\tinvoicesRepo := invoicesPkg.NewRepository(database)\n\topsRepo := opsRepoPkg.NewRepository(database)\n\trecRepo := recRepoPkg.NewRepository(database)\n\n\tsyncService := sync.NewService(rmsClient, catalogRepo, recipesRepo, invoicesRepo, opsRepo)\n\trecService := recServicePkg.NewService(recRepo)\n\n\t// --- БЛОК ПРОВЕРКИ СИНХРОНИЗАЦИИ (Run-once on start) ---\n\tgo func() {\n\t\tlogger.Log.Info(\">>> Запуск тестовой синхронизации...\")\n\n\t\t// 1. Каталог\n\t\tif err := syncService.SyncCatalog(); err != nil {\n\t\t\tlogger.Log.Error(\"Ошибка синхронизации каталога\", zap.Error(err))\n\t\t} else {\n\t\t\tlogger.Log.Info(\"<<< Каталог успешно синхронизирован\")\n\t\t}\n\n\t\t// 2. Техкарты\n\t\tif err := syncService.SyncRecipes(); err != nil {\n\t\t\tlogger.Log.Error(\"Ошибка синхронизации техкарт\", zap.Error(err))\n\t\t} else {\n\t\t\tlogger.Log.Info(\"<<< Техкарты успешно синхронизированы\")\n\t\t}\n\n\t\t// 3. Накладные\n\t\tif err := syncService.SyncInvoices(); err != nil {\n\t\t\tlogger.Log.Error(\"Ошибка синхронизации накладных\", zap.Error(err))\n\t\t} else {\n\t\t\tlogger.Log.Info(\"<<< Накладные успешно синхронизированы\")\n\t\t}\n\t\t// 4. Складские операции\n\t\tif err := syncService.SyncStoreOperations(); err != nil {\n\t\t\tlogger.Log.Error(\"Ошибка синхронизации операций\", zap.Error(err))\n\t\t} else {\n\t\t\tlogger.Log.Info(\"<<< Операции успешно синхронизированы\")\n\t\t}\n\t\tlogger.Log.Info(\">>> Запуск расчета рекомендаций...\")\n\t\tif err := recService.RefreshRecommendations(); err != nil {\n\t\t\tlogger.Log.Error(\"Ошибка расчета рекомендаций\", zap.Error(err))\n\t\t} else {\n\t\t\t// Для отладки можно вывести пару штук\n\t\t\trecs, _ := recService.GetRecommendations()\n\t\t\tlogger.Log.Info(\"<<< Анализ завершен\", zap.Int(\"found\", len(recs)))\n\t\t}\n\t}()\n\t// -------------------------------------------------------\n\n\t// 5. Запуск HTTP сервера (Gin)\n\tif cfg.App.Mode == \"release\" {\n\t\tgin.SetMode(gin.ReleaseMode)\n\t}\n\tr := gin.Default()\n\n\t// Простой хелсчек\n\tr.GET(\"/health\", func(c *gin.Context) {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": \"ok\",\n\t\t\t\"time\": time.Now().Format(time.RFC3339),\n\t\t})\n\t})\n\n\tlogger.Log.Info(\"Сервер запускается\", zap.String(\"port\", cfg.App.Port))\n\tif err := r.Run(\":\" + cfg.App.Port); err != nil {\n\t\tlogger.Log.Fatal(\"Ошибка запуска сервера\", zap.Error(err))\n\t}\n}\n", - "config.yaml": "app:\n port: \"8080\"\n mode: \"debug\" # debug выводит цветные логи\n drop_tables: true\n\ndb:\n dsn: \"host=postgres user=rmser password=mhrcadmin994525 dbname=rmser_db port=5432 sslmode=disable TimeZone=Europe/Moscow\"\n\nredis:\n addr: \"localhost:6379\"\n password: \"\"\n db: 0\n\nrms:\n base_url: \"https://rest-mesto-vstrechi.iiko.it\" # Например http://95.12.34.56:8080\n login: \"MH\"\n password: \"MhLevfqkexit632597\" # Пароль в открытом виде (приложение само хеширует)", - "config/config.go": "package config\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/spf13/viper\"\n)\n\ntype Config struct {\n\tApp AppConfig\n\tDB DBConfig\n\tRedis RedisConfig\n\tRMS RMSConfig\n}\n\ntype AppConfig struct {\n\tPort string `mapstructure:\"port\"`\n\tMode string `mapstructure:\"mode\"` // debug/release\n\tDropTables bool `mapstructure:\"drop_tables\"`\n}\n\ntype DBConfig struct {\n\tDSN string `mapstructure:\"dsn\"`\n}\n\ntype RedisConfig struct {\n\tAddr string `mapstructure:\"addr\"`\n\tPassword string `mapstructure:\"password\"`\n\tDB int `mapstructure:\"db\"`\n}\n\ntype RMSConfig struct {\n\tBaseURL string `mapstructure:\"base_url\"`\n\tLogin string `mapstructure:\"login\"`\n\tPassword string `mapstructure:\"password\"` // Исходный пароль, хеширование будет в клиенте\n}\n\n// LoadConfig загружает конфигурацию из файла и переменных окружения\nfunc LoadConfig(path string) (*Config, error) {\n\tviper.AddConfigPath(path)\n\tviper.SetConfigName(\"config\")\n\tviper.SetConfigType(\"yaml\")\n\n\tviper.AutomaticEnv()\n\tviper.SetEnvKeyReplacer(strings.NewReplacer(\".\", \"_\"))\n\n\tif err := viper.ReadInConfig(); err != nil {\n\t\treturn nil, fmt.Errorf(\"ошибка чтения конфига: %w\", err)\n\t}\n\n\tvar cfg Config\n\tif err := viper.Unmarshal(&cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"ошибка анмаршалинга конфига: %w\", err)\n\t}\n\n\treturn &cfg, nil\n}\n", - "go.mod": "module rmser\n\ngo 1.25.1\n\nrequire (\n\tgithub.com/gin-gonic/gin v1.11.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/jackc/pgx/v5 v5.6.0\n\tgithub.com/redis/go-redis/v9 v9.17.1\n\tgithub.com/shopspring/decimal v1.4.0\n\tgithub.com/spf13/viper v1.21.0\n\tgo.uber.org/zap v1.27.1\n\tgorm.io/driver/postgres v1.6.0\n\tgorm.io/gorm v1.31.1\n)\n\nrequire (\n\tgithub.com/bytedance/sonic v1.14.0 // indirect\n\tgithub.com/bytedance/sonic/loader v0.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/cloudwego/base64x v0.1.6 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.8 // indirect\n\tgithub.com/gin-contrib/sse v1.1.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.27.0 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.4.0 // indirect\n\tgithub.com/goccy/go-json v0.10.2 // indirect\n\tgithub.com/goccy/go-yaml v1.18.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.2 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/jinzhu/now v1.1.5 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/quic-go/qpack v0.5.1 // indirect\n\tgithub.com/quic-go/quic-go v0.54.0 // indirect\n\tgithub.com/sagikazarmark/locafero v0.11.0 // indirect\n\tgithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.3.0 // indirect\n\tgo.uber.org/mock v0.5.0 // indirect\n\tgo.uber.org/multierr v1.10.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/arch v0.20.0 // indirect\n\tgolang.org/x/crypto v0.40.0 // indirect\n\tgolang.org/x/mod v0.26.0 // indirect\n\tgolang.org/x/net v0.42.0 // indirect\n\tgolang.org/x/sync v0.16.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n\tgolang.org/x/tools v0.35.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.9 // indirect\n)\n", - "internal/domain/catalog/entity.go": "package catalog\n\nimport (\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/shopspring/decimal\"\n)\n\n// Product - Номенклатура\ntype Product struct {\n\tID uuid.UUID `gorm:\"type:uuid;primary_key;\"`\n\tParentID *uuid.UUID `gorm:\"type:uuid;index\"`\n\tName string `gorm:\"type:varchar(255);not null\"`\n\tType string `gorm:\"type:varchar(50);index\"` // GOODS, DISH, PREPARED, etc.\n\tNum string `gorm:\"type:varchar(50)\"`\n\tCode string `gorm:\"type:varchar(50)\"`\n\tUnitWeight decimal.Decimal `gorm:\"type:numeric(19,4)\"`\n\tUnitCapacity decimal.Decimal `gorm:\"type:numeric(19,4)\"`\n\tIsDeleted bool `gorm:\"default:false\"`\n\n\tParent *Product `gorm:\"foreignKey:ParentID\"`\n\tChildren []*Product `gorm:\"foreignKey:ParentID\"`\n\n\tCreatedAt time.Time\n\tUpdatedAt time.Time\n}\n\n// Repository интерфейс для каталога\ntype Repository interface {\n\tSaveProducts(products []Product) error\n\tGetAll() ([]Product, error)\n}\n", + "API_DOCS.md": "# API Documentation: RMSER Backend\n\n## 1. Общая информация\n\n* **Base URL:** `http://localhost:8080/api`\n* **Формат данных:** JSON.\n* **CORS:** Разрешены запросы с `localhost:5173` (и любых других источников в режиме dev).\n* **Auth:** На данный момент эндпоинты открыты. В будущем предполагается передача токена в заголовке `Authorization: Bearer `.\n\n### Обработка ошибок\nВ случае ошибки сервер возвращает HTTP код 4xx/5xx и JSON:\n```json\n{\n \"error\": \"Описание ошибки\"\n}\n```\n\n---\n\n## 2. TypeScript Интерфейсы (Models)\n\nИспользуйте эти интерфейсы для типизации данных на клиенте.\n\n```typescript\n// --- Общие типы ---\n\n// UUID (строка)\ntype UUID = string;\n\n// Decimal (деньги/вес приходят строкой, чтобы избежать потери точность в JS)\ntype Decimal = string; \n\n// --- Каталог и OCR ---\n\n// Товар для выпадающего списка (Autosuggest)\nexport interface CatalogItem {\n id: UUID;\n name: string;\n code: string; // Артикул или код быстрого набора\n}\n\n// Связь \"Текст из чека\" -> \"Товар iiko\"\nexport interface ProductMatch {\n raw_name: string; // Текст из OCR (ключ)\n product_id: UUID;\n product?: CatalogItem; // Вложенный объект (при чтении)\n updated_at: string; // ISO Date\n}\n\n// --- Рекомендации (Аналитика) ---\n\nexport interface Recommendation {\n ID: UUID;\n Type: string; // Код проблемы (UNUSED_IN_RECIPES, NO_INCOMING, etc.)\n ProductID: UUID;\n ProductName: string;\n Reason: string; // Человекочитаемое описание проблемы\n CreatedAt: string;\n}\n\n// --- Накладные ---\n\nexport interface InvoiceItemRequest {\n product_id: UUID;\n amount: number; // или string, если нужна высокая точность\n price: number;\n}\n\nexport interface CreateInvoiceRequest {\n document_number: string;\n date_incoming: string; // Format: \"YYYY-MM-DD\"\n supplier_id: UUID;\n store_id: UUID;\n items: InvoiceItemRequest[];\n}\n```\n\n---\n\n## 3. Эндпоинты\n\n### 📊 Аналитика (Рекомендации)\n\n#### Получить список рекомендаций\nВозвращает список выявленных проблем в учете (товары без техкарт, ингредиенты без закупок и т.д.). Данные обновляются фоновым процессом на бэкенде.\n\n* **URL:** `/recommendations`\n* **Method:** `GET`\n* **Response:** `200 OK`\n\n```json\n[\n {\n \"ID\": \"uuid...\",\n \"Type\": \"UNUSED_IN_RECIPES\",\n \"ProductID\": \"uuid...\",\n \"ProductName\": \"Петрушка с/м\",\n \"Reason\": \"Товар не используется ни в одной техкарте\",\n \"CreatedAt\": \"2023-10-27T10:00:00Z\"\n }\n]\n```\n\n---\n\n### 👁 OCR и Обучение (Matching)\n\n#### 1. Получить справочник товаров (для селекта)\nИспользуется для построения локального индекса поиска (autocomplete) на фронтенде, чтобы привязывать позиции. Возвращает только активные товары (`GOODS`).\n\n* **URL:** `/ocr/catalog`\n* **Method:** `GET`\n* **Response:** `200 OK`\n\n```json\n[\n {\n \"id\": \"607a1e96-f539-45d2-8709-3919f94bdc3e\",\n \"name\": \"Молоко Домик в Деревне 3.2%\",\n \"code\": \"00123\"\n },\n ...\n]\n```\n\n#### 2. Получить таблицу обучения (Matches)\nВозвращает список уже созданных связей \"Грязное название из чека\" -> \"Чистый товар iiko\".\n\n* **URL:** `/ocr/matches`\n* **Method:** `GET`\n* **Response:** `200 OK`\n\n```json\n[\n {\n \"raw_name\": \"молоко двд 3,2\",\n \"product_id\": \"607a1e96-f539-45d2-8709-3919f94bdc3e\",\n \"product\": {\n \"ID\": \"607a1e96-f539-45d2-8709-3919f94bdc3e\",\n \"Name\": \"Молоко Домик в Деревне 3.2%\",\n ...\n },\n \"updated_at\": \"...\"\n }\n]\n```\n\n#### 3. Создать/Обновить привязку\nСохраняет правило: \"Если в чеке встретишь этот текст, считай это вот этим товаром\".\n\n* **URL:** `/ocr/match`\n* **Method:** `POST`\n* **Body:**\n\n```json\n{\n \"raw_name\": \"молоко двд 3,2\", \n \"product_id\": \"607a1e96-f539-45d2-8709-3919f94bdc3e\"\n}\n```\n\n* **Response:** `200 OK` -> `{\"status\": \"saved\"}`\n\n---\n\n### 📄 Накладные (Invoices)\n\n#### Отправить накладную в iikoRMS\nСоздает черновик приходной накладной в системе iiko.\n\n* **URL:** `/invoices/send`\n* **Method:** `POST`\n* **Body:**\n\n```json\n{\n \"document_number\": \"INV-12345\",\n \"date_incoming\": \"2023-10-27\",\n \"supplier_id\": \"uuid-supplier...\",\n \"store_id\": \"uuid-store...\",\n \"items\": [\n {\n \"product_id\": \"uuid-product...\",\n \"amount\": 10.5,\n \"price\": 150.00\n }\n ]\n}\n```\n\n* **Response:** `200 OK`\n\n```json\n{\n \"status\": \"ok\",\n \"created_number\": \"000123\" // Номер документа, присвоенный iiko\n}\n```\n\n---\n\n### ⚙️ System\n\n#### Healthcheck\nПроверка доступности API.\n\n* **URL:** `http://localhost:8080/health`\n* **Method:** `GET`\n* **Response:**\n\n```json\n{\n \"status\": \"ok\",\n \"time\": \"2023-10-27T12:34:56+03:00\"\n}\n```\n\n---\n\n## 4. Сценарии использования (Frontend Workflow)\n\n### Сценарий А: \"Обучение\" (раздел Settings / OCR Learning)\n1. При загрузке страницы вызвать `GET /api/ocr/matches` и отобразить таблицу.\n2. Вызвать `GET /api/ocr/catalog` и сохранить в памяти для быстрого поиска (Combobox/Select).\n3. Пользователь может добавить новую связь вручную:\n * Вводит текст (например, \"Хлеб Бородинский\").\n * Выбирает товар из выпадающего списка.\n * Нажимает \"Save\" -> вызывается `POST /api/ocr/match`.\n * Таблица обновляется.\n\n### Сценарий Б: \"Дашборд аналитика\"\n1. При загрузке главной страницы вызвать `GET /api/recommendations`.\n2. Сгруппировать массив по полю `Type` или `Reason`.\n3. Отобразить карточки: \"Товары без техкарт (5 шт)\", \"Ингредиенты без закупок (12 шт)\".\n\n### Сценарий В: \"Создание накладной\" (Пока ручное)\n*Пока нет загрузки фото через Web, предполагается ручной ввод или редактирование данных, полученных иным путем.*\n1. Форма ввода номера, даты.\n2. Таблица товаров (добавление строк через поиск по каталогу).\n3. Кнопка \"Отправить в iiko\" -> `POST /api/invoices/send`.", + "cmd/main.go": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/gin-contrib/cors\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/google/uuid\"\n\t\"github.com/shopspring/decimal\"\n\t\"go.uber.org/zap\"\n\n\t\"rmser/config\"\n\t\"rmser/internal/domain/catalog\"\n\t\"rmser/internal/domain/invoices\"\n\t\"rmser/internal/infrastructure/db\"\n\t\"rmser/internal/infrastructure/ocr_client\"\n\ttgBot \"rmser/internal/transport/telegram\"\n\n\t// Репозитории (инфраструктура)\n\tcatalogPkg \"rmser/internal/infrastructure/repository/catalog\"\n\tinvoicesPkg \"rmser/internal/infrastructure/repository/invoices\"\n\tocrRepoPkg \"rmser/internal/infrastructure/repository/ocr\"\n\topsRepoPkg \"rmser/internal/infrastructure/repository/operations\"\n\trecipesPkg \"rmser/internal/infrastructure/repository/recipes\"\n\trecRepoPkg \"rmser/internal/infrastructure/repository/recommendations\"\n\n\t\"rmser/internal/infrastructure/rms\"\n\tinvServicePkg \"rmser/internal/services/invoices\" // Сервис накладных\n\tocrServicePkg \"rmser/internal/services/ocr\"\n\trecServicePkg \"rmser/internal/services/recommend\"\n\t\"rmser/internal/services/sync\"\n\t\"rmser/internal/transport/http/handlers\" // Хендлеры\n\t\"rmser/pkg/logger\"\n)\n\nfunc main() {\n\t// 1. Загрузка конфигурации\n\tcfg, err := config.LoadConfig(\".\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Ошибка загрузки конфига: %v\", err)\n\t}\n\t// OCR Client\n\tpyClient := ocr_client.NewClient(cfg.OCR.ServiceURL)\n\n\t// 2. Инициализация логгера\n\tlogger.Init(cfg.App.Mode)\n\tdefer logger.Log.Sync()\n\n\tlogger.Log.Info(\"Запуск приложения rmser\", zap.String(\"mode\", cfg.App.Mode))\n\n\t// 3a. Подключение Redis (Новое)\n\t// redisClient, err := redis.NewClient(cfg.Redis.Addr, cfg.Redis.Password, cfg.Redis.DB)\n\t// if err != nil {\n\t// \tlogger.Log.Fatal(\"Ошибка подключения к Redis\", zap.Error(err))\n\t// }\n\n\t// 3. Подключение к БД (PostgreSQL)\n\tdatabase := db.NewPostgresDB(cfg.DB.DSN)\n\n\t// 4. Инициализация слоев\n\trmsClient := rms.NewClient(cfg.RMS.BaseURL, cfg.RMS.Login, cfg.RMS.Password)\n\tcatalogRepo := catalogPkg.NewRepository(database)\n\trecipesRepo := recipesPkg.NewRepository(database)\n\tinvoicesRepo := invoicesPkg.NewRepository(database)\n\topsRepo := opsRepoPkg.NewRepository(database)\n\trecRepo := recRepoPkg.NewRepository(database)\n\tocrRepo := ocrRepoPkg.NewRepository(database)\n\n\tsyncService := sync.NewService(rmsClient, catalogRepo, recipesRepo, invoicesRepo, opsRepo)\n\trecService := recServicePkg.NewService(recRepo)\n\tocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, pyClient)\n\tinvoiceService := invServicePkg.NewService(rmsClient)\n\n\t// --- Инициализация Handler'ов ---\n\tinvoiceHandler := handlers.NewInvoiceHandler(invoiceService)\n\tocrHandler := handlers.NewOCRHandler(ocrService)\n\trecommendHandler := handlers.NewRecommendationsHandler(recService)\n\n\t// --- БЛОК ПРОВЕРКИ СИНХРОНИЗАЦИИ (Run-once on start) ---\n\tgo func() {\n\t\tlogger.Log.Info(\">>> Запуск тестовой синхронизации...\")\n\n\t\t// 1. Каталог\n\t\tif err := syncService.SyncCatalog(); err != nil {\n\t\t\tlogger.Log.Error(\"Ошибка синхронизации каталога\", zap.Error(err))\n\t\t} else {\n\t\t\tlogger.Log.Info(\"<<< Каталог успешно синхронизирован\")\n\t\t}\n\n\t\t// 2. Техкарты\n\t\tif err := syncService.SyncRecipes(); err != nil {\n\t\t\tlogger.Log.Error(\"Ошибка синхронизации техкарт\", zap.Error(err))\n\t\t} else {\n\t\t\tlogger.Log.Info(\"<<< Техкарты успешно синхронизированы\")\n\t\t}\n\n\t\t// 3. Накладные\n\t\tif err := syncService.SyncInvoices(); err != nil {\n\t\t\tlogger.Log.Error(\"Ошибка синхронизации накладных\", zap.Error(err))\n\t\t} else {\n\t\t\tlogger.Log.Info(\"<<< Накладные успешно синхронизированы\")\n\t\t}\n\t\t// 4. Складские операции\n\t\tif err := syncService.SyncStoreOperations(); err != nil {\n\t\t\tlogger.Log.Error(\"Ошибка синхронизации операций\", zap.Error(err))\n\t\t} else {\n\t\t\tlogger.Log.Info(\"<<< Операции успешно синхронизированы\")\n\t\t}\n\t\tlogger.Log.Info(\">>> Запуск расчета рекомендаций...\")\n\t\tif err := recService.RefreshRecommendations(); err != nil {\n\t\t\tlogger.Log.Error(\"Ошибка расчета рекомендаций\", zap.Error(err))\n\t\t} else {\n\t\t\t// Для отладки можно вывести пару штук\n\t\t\trecs, _ := recService.GetRecommendations()\n\t\t\tlogger.Log.Info(\"<<< Анализ завершен\", zap.Int(\"found\", len(recs)))\n\t\t}\n\t\t// === ТЕСТ ОТПРАВКИ НАКЛАДНОЙ ===\n\t\t// Запускаем через небольшую паузу, чтобы логи не перемешались\n\t\t// time.Sleep(2 * time.Second)\n\t\t// runManualInvoiceTest(rmsClient, catalogRepo)\n\t\t// ===============================\n\t}()\n\t// -------------------------------------------------------\n\t// Запуск бота (в отдельной горутине, т.к. Start() блокирует поток)\n\tif cfg.Telegram.Token != \"\" {\n\t\tbot, err := tgBot.NewBot(cfg.Telegram, ocrService)\n\t\tif err != nil {\n\t\t\tlogger.Log.Fatal(\"Ошибка создания Telegram бота\", zap.Error(err))\n\t\t}\n\t\tgo bot.Start()\n\t\tdefer bot.Stop() // Graceful shutdown\n\t} else {\n\t\tlogger.Log.Warn(\"Telegram token не задан, бот не запущен\")\n\t}\n\n\t// 5. Запуск HTTP сервера (Gin)\n\tif cfg.App.Mode == \"release\" {\n\t\tgin.SetMode(gin.ReleaseMode)\n\t}\n\tr := gin.Default()\n\n\t// --- Настройка CORS ---\n\t// Разрешаем запросы с любых источников для разработки Frontend\n\tcorsConfig := cors.DefaultConfig()\n\tcorsConfig.AllowAllOrigins = true // В продакшене заменить на AllowOrigins: []string{\"http://domain.com\"}\n\tcorsConfig.AllowMethods = []string{\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"HEAD\", \"OPTIONS\"}\n\tcorsConfig.AllowHeaders = []string{\"Origin\", \"Content-Length\", \"Content-Type\", \"Authorization\"}\n\tr.Use(cors.New(corsConfig))\n\n\tapi := r.Group(\"/api\")\n\t{\n\t\t// Invoices\n\t\tapi.POST(\"/invoices/send\", invoiceHandler.SendInvoice)\n\n\t\t// Recommendations\n\t\tapi.GET(\"/recommendations\", recommendHandler.GetRecommendations)\n\n\t\t// OCR\n\t\tapi.GET(\"/ocr/catalog\", ocrHandler.GetCatalog)\n\t\tapi.GET(\"/ocr/matches\", ocrHandler.GetMatches)\n\t\tapi.POST(\"/ocr/match\", ocrHandler.SaveMatch)\n\t}\n\n\t// Простой хелсчек\n\tr.GET(\"/health\", func(c *gin.Context) {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\": \"ok\",\n\t\t\t\"time\": time.Now().Format(time.RFC3339),\n\t\t})\n\t})\n\n\tlogger.Log.Info(\"Сервер запускается\", zap.String(\"port\", cfg.App.Port))\n\tif err := r.Run(\":\" + cfg.App.Port); err != nil {\n\t\tlogger.Log.Fatal(\"Ошибка запуска сервера\", zap.Error(err))\n\t}\n}\n\n// runManualInvoiceTest создает тестовую накладную и отправляет её в RMS\nfunc runManualInvoiceTest(client rms.ClientI, catRepo catalog.Repository) {\n\tlogger.Log.Info(\">>> [TEST] Начало теста создания накладной...\")\n\n\t// === НАСТРОЙКИ ТЕСТА ===\n\tconst (\n\t\tMyStoreGUID = \"1239d270-1bbe-f64f-b7ea-5f00518ef508\" // <-- ВАШ СКЛАД\n\t\tMySupplierGUID = \"780aa87e-2688-4f99-915b-7924ca392ac1\" // <-- ВАШ ПОСТАВЩИК\n\t\tMyProductGUID = \"607a1e96-f539-45d2-8709-3919f94bdc3e\" // <-- ВАШ ТОВАР (Опционально)\n\t)\n\t// =======================\n\n\t// 1. Поиск товара\n\tvar targetProduct catalog.Product\n\tvar found bool\n\n\t// Если задан конкретный ID товара в константе - ищем его\n\tif MyProductGUID != \"00000000-0000-0000-0000-000000000000\" {\n\t\tproducts, _ := catRepo.GetAll() // В реальном коде лучше GetByID, но у нас repo пока простой\n\t\ttargetUUID := uuid.MustParse(MyProductGUID)\n\t\tfor _, p := range products {\n\t\t\tif p.ID == targetUUID {\n\t\t\t\ttargetProduct = p\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tlogger.Log.Error(\"[TEST] Товар с указанным MyProductGUID не найден в локальной БД\")\n\t\t\treturn\n\t\t}\n\t} else {\n\t\t// Иначе ищем первый попавшийся GOODS\n\t\tproducts, err := catRepo.GetAll()\n\t\tif err != nil || len(products) == 0 {\n\t\t\tlogger.Log.Error(\"[TEST] БД пуста. Выполните SyncCatalog.\")\n\t\t\treturn\n\t\t}\n\n\t\tfor _, p := range products {\n\t\t\t// Ищем именно товар (GOODS) или заготовку (PREPARED), но не группу и не блюдо, если нужно\n\t\t\tif p.Type == \"GOODS\" {\n\t\t\t\ttargetProduct = p\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tlogger.Log.Warn(\"[TEST] Не найдено товаров с типом GOODS. Берем первый попавшийся.\")\n\t\t\ttargetProduct = products[0]\n\t\t}\n\t}\n\n\tlogger.Log.Info(\"[TEST] Используем товар\",\n\t\tzap.String(\"name\", targetProduct.Name),\n\t\tzap.String(\"type\", targetProduct.Type),\n\t\tzap.String(\"uuid\", targetProduct.ID.String()),\n\t)\n\n\t// 2. Парсинг ID Склада и Поставщика\n\tstoreID := uuid.Nil\n\tif MyStoreGUID != \"00000000-0000-0000-0000-000000000000\" {\n\t\tstoreID = uuid.MustParse(MyStoreGUID)\n\t} else {\n\t\tlogger.Log.Warn(\"[TEST] ID склада не задан! iiko вернет ошибку валидации.\")\n\t\tstoreID = uuid.New() // Рандом, чтобы XML собрался\n\t}\n\n\tsupplierID := uuid.Nil\n\tif MySupplierGUID != \"00000000-0000-0000-0000-000000000000\" {\n\t\tsupplierID = uuid.MustParse(MySupplierGUID)\n\t} else {\n\t\tlogger.Log.Warn(\"[TEST] ID поставщика не задан!\")\n\t\tsupplierID = uuid.New()\n\t}\n\n\t// 3. Формируем накладную\n\ttestInv := invoices.Invoice{\n\t\tID: uuid.Nil,\n\t\tDocumentNumber: fmt.Sprintf(\"TEST-%d\", time.Now().Unix()%10000),\n\t\tDateIncoming: time.Now(),\n\t\tSupplierID: supplierID,\n\t\tDefaultStoreID: storeID,\n\t\tStatus: \"NEW\",\n\t\tItems: []invoices.InvoiceItem{\n\t\t\t{\n\t\t\t\tProductID: targetProduct.ID,\n\t\t\t\tAmount: decimal.NewFromFloat(5.0),\n\t\t\t\tPrice: decimal.NewFromFloat(100.0),\n\t\t\t\tSum: decimal.NewFromFloat(500.0),\n\t\t\t},\n\t\t},\n\t}\n\n\t// 4. Отправляем\n\tlogger.Log.Info(\"[TEST] Отправка запроса в RMS...\")\n\tdocNum, err := client.CreateIncomingInvoice(testInv)\n\tif err != nil {\n\t\tlogger.Log.Error(\"[TEST] Ошибка отправки накладной\", zap.Error(err))\n\t} else {\n\t\tlogger.Log.Info(\"[TEST] УСПЕХ! Накладная создана.\", zap.String(\"doc_number\", docNum))\n\t}\n}\n", + "config.yaml": "app:\n port: \"8080\"\n mode: \"debug\" # debug выводит цветные логи\n drop_tables: true\n\ndb:\n dsn: \"host=postgres user=rmser password=mhrcadmin994525 dbname=rmser_db port=5432 sslmode=disable TimeZone=Europe/Moscow\"\n\nredis:\n addr: \"localhost:6379\"\n password: \"\"\n db: 0\n\nrms:\n base_url: \"https://rest-mesto-vstrechi.iiko.it\" # Например http://95.12.34.56:8080\n login: \"MH\"\n password: \"MhLevfqkexit632597\" # Пароль в открытом виде (приложение само хеширует)\n\nocr:\n service_url: \"http://ocr-service:5005\"\n\ntelegram:\n token: \"7858843765:AAE5HBQHbef4fGLoMDV91bhHZQFcnsBDcv4\"\n admin_ids: [665599275]", + "config/config.go": "package config\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/spf13/viper\"\n)\n\ntype Config struct {\n\tApp AppConfig\n\tDB DBConfig\n\tRedis RedisConfig\n\tRMS RMSConfig\n\tOCR OCRConfig\n\tTelegram TelegramConfig\n}\n\ntype AppConfig struct {\n\tPort string `mapstructure:\"port\"`\n\tMode string `mapstructure:\"mode\"` // debug/release\n\tDropTables bool `mapstructure:\"drop_tables\"`\n}\n\ntype DBConfig struct {\n\tDSN string `mapstructure:\"dsn\"`\n}\n\ntype RedisConfig struct {\n\tAddr string `mapstructure:\"addr\"`\n\tPassword string `mapstructure:\"password\"`\n\tDB int `mapstructure:\"db\"`\n}\n\ntype OCRConfig struct {\n\tServiceURL string `mapstructure:\"service_url\"`\n}\n\ntype RMSConfig struct {\n\tBaseURL string `mapstructure:\"base_url\"`\n\tLogin string `mapstructure:\"login\"`\n\tPassword string `mapstructure:\"password\"` // Исходный пароль, хеширование будет в клиенте\n}\n\ntype TelegramConfig struct {\n\tToken string `mapstructure:\"token\"`\n\tAdminIDs []int64 `mapstructure:\"admin_ids\"`\n}\n\n// LoadConfig загружает конфигурацию из файла и переменных окружения\nfunc LoadConfig(path string) (*Config, error) {\n\tviper.AddConfigPath(path)\n\tviper.SetConfigName(\"config\")\n\tviper.SetConfigType(\"yaml\")\n\n\tviper.AutomaticEnv()\n\tviper.SetEnvKeyReplacer(strings.NewReplacer(\".\", \"_\"))\n\n\tif err := viper.ReadInConfig(); err != nil {\n\t\treturn nil, fmt.Errorf(\"ошибка чтения конфига: %w\", err)\n\t}\n\n\tvar cfg Config\n\tif err := viper.Unmarshal(&cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"ошибка анмаршалинга конфига: %w\", err)\n\t}\n\n\treturn &cfg, nil\n}\n", + "go.mod": "module rmser\n\ngo 1.25.1\n\nrequire (\n\tgithub.com/gin-contrib/cors v1.7.6\n\tgithub.com/gin-gonic/gin v1.11.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/jackc/pgx/v5 v5.6.0\n\tgithub.com/redis/go-redis/v9 v9.17.1\n\tgithub.com/shopspring/decimal v1.4.0\n\tgithub.com/spf13/viper v1.21.0\n\tgo.uber.org/zap v1.27.1\n\tgopkg.in/telebot.v3 v3.3.8\n\tgorm.io/driver/postgres v1.6.0\n\tgorm.io/gorm v1.31.1\n)\n\nrequire (\n\tgithub.com/bytedance/sonic v1.14.0 // indirect\n\tgithub.com/bytedance/sonic/loader v0.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/cloudwego/base64x v0.1.6 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.9 // indirect\n\tgithub.com/gin-contrib/sse v1.1.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.27.0 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.4.0 // indirect\n\tgithub.com/goccy/go-json v0.10.5 // indirect\n\tgithub.com/goccy/go-yaml v1.18.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.2 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/jinzhu/now v1.1.5 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/quic-go/qpack v0.5.1 // indirect\n\tgithub.com/quic-go/quic-go v0.54.0 // indirect\n\tgithub.com/sagikazarmark/locafero v0.11.0 // indirect\n\tgithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.3.0 // indirect\n\tgo.uber.org/mock v0.5.0 // indirect\n\tgo.uber.org/multierr v1.10.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/arch v0.20.0 // indirect\n\tgolang.org/x/crypto v0.40.0 // indirect\n\tgolang.org/x/mod v0.26.0 // indirect\n\tgolang.org/x/net v0.42.0 // indirect\n\tgolang.org/x/sync v0.16.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n\tgolang.org/x/tools v0.35.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.9 // indirect\n)\n", + "internal/domain/catalog/entity.go": "package catalog\n\nimport (\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/shopspring/decimal\"\n)\n\n// Product - Номенклатура\ntype Product struct {\n\tID uuid.UUID `gorm:\"type:uuid;primary_key;\"`\n\tParentID *uuid.UUID `gorm:\"type:uuid;index\"`\n\tName string `gorm:\"type:varchar(255);not null\"`\n\tType string `gorm:\"type:varchar(50);index\"` // GOODS, DISH, PREPARED, etc.\n\tNum string `gorm:\"type:varchar(50)\"`\n\tCode string `gorm:\"type:varchar(50)\"`\n\tUnitWeight decimal.Decimal `gorm:\"type:numeric(19,4)\"`\n\tUnitCapacity decimal.Decimal `gorm:\"type:numeric(19,4)\"`\n\tIsDeleted bool `gorm:\"default:false\"`\n\n\tParent *Product `gorm:\"foreignKey:ParentID\"`\n\tChildren []*Product `gorm:\"foreignKey:ParentID\"`\n\n\tCreatedAt time.Time\n\tUpdatedAt time.Time\n}\n\n// Repository интерфейс для каталога\ntype Repository interface {\n\tSaveProducts(products []Product) error\n\tGetAll() ([]Product, error)\n\tGetActiveGoods() ([]Product, error)\n}\n", "internal/domain/interfaces.go": "package domain\n\nimport (\n\t\"rmser/internal/domain/catalog\"\n\t\"rmser/internal/domain/invoices\"\n\t\"rmser/internal/domain/recipes\"\n\t\"time\"\n)\n\ntype Repository interface {\n\t// Catalog\n\tSaveProducts(products []catalog.Product) error\n\n\t// Recipes\n\tSaveRecipes(recipes []recipes.Recipe) error\n\n\t// Invoices\n\tGetLastInvoiceDate() (*time.Time, error)\n\tSaveInvoices(invoices []invoices.Invoice) error\n}\n", "internal/domain/invoices/entity.go": "package invoices\n\nimport (\n\t\"time\"\n\n\t\"rmser/internal/domain/catalog\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/shopspring/decimal\"\n)\n\n// Invoice - Приходная накладная\ntype Invoice struct {\n\tID uuid.UUID `gorm:\"type:uuid;primary_key;\"`\n\tDocumentNumber string `gorm:\"type:varchar(100);index\"`\n\tDateIncoming time.Time `gorm:\"index\"`\n\tSupplierID uuid.UUID `gorm:\"type:uuid;index\"`\n\tDefaultStoreID uuid.UUID `gorm:\"type:uuid;index\"`\n\tStatus string `gorm:\"type:varchar(50)\"`\n\n\tItems []InvoiceItem `gorm:\"foreignKey:InvoiceID;constraint:OnDelete:CASCADE\"`\n\n\tCreatedAt time.Time\n\tUpdatedAt time.Time\n}\n\n// InvoiceItem - Позиция накладной\ntype InvoiceItem struct {\n\tID uuid.UUID `gorm:\"type:uuid;primary_key;default:gen_random_uuid()\"`\n\tInvoiceID uuid.UUID `gorm:\"type:uuid;not null;index\"`\n\tProductID uuid.UUID `gorm:\"type:uuid;not null\"`\n\tAmount decimal.Decimal `gorm:\"type:numeric(19,4);not null\"`\n\tPrice decimal.Decimal `gorm:\"type:numeric(19,4);not null\"`\n\tSum decimal.Decimal `gorm:\"type:numeric(19,4);not null\"`\n\tVatSum decimal.Decimal `gorm:\"type:numeric(19,4)\"`\n\n\tProduct catalog.Product `gorm:\"foreignKey:ProductID\"`\n}\n\ntype Repository interface {\n\tGetLastInvoiceDate() (*time.Time, error)\n\tSaveInvoices(invoices []Invoice) error\n}\n", + "internal/domain/ocr/entity.go": "package ocr\n\nimport (\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"rmser/internal/domain/catalog\"\n)\n\n// ProductMatch связывает текст из чека с конкретным товаром в iiko\ntype ProductMatch struct {\n\t// RawName - распознанный текст (ключ).\n\t// Лучше хранить в нижнем регистре и без лишних пробелов.\n\tRawName string `gorm:\"type:varchar(255);primary_key\"`\n\n\tProductID uuid.UUID `gorm:\"type:uuid;not null;index\"`\n\n\t// Product - связь для GORM\n\tProduct catalog.Product `gorm:\"foreignKey:ProductID\"`\n\n\tUpdatedAt time.Time\n\tCreatedAt time.Time\n}\n\ntype Repository interface {\n\t// SaveMatch сохраняет или обновляет привязку\n\tSaveMatch(rawName string, productID uuid.UUID) error\n\n\t// FindMatch ищет товар по точному совпадению названия\n\tFindMatch(rawName string) (*uuid.UUID, error)\n\n\t// GetAllMatches возвращает все существующие привязки\n\tGetAllMatches() ([]ProductMatch, error)\n}\n", "internal/domain/operations/entity.go": "package operations\n\nimport (\n\t\"time\"\n\n\t\"rmser/internal/domain/catalog\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/shopspring/decimal\"\n)\n\ntype OperationType string\n\nconst (\n\tOpTypePurchase OperationType = \"PURCHASE\" // Закупка (Приход)\n\tOpTypeUsage OperationType = \"USAGE\" // Расход (Реализация + Списание)\n\tOpTypeUnknown OperationType = \"UNKNOWN\" // Прочее (Инвентаризация, Перемещения - игнорируем пока)\n)\n\n// StoreOperation - запись из складского отчета\ntype StoreOperation struct {\n\tID uuid.UUID `gorm:\"type:uuid;primary_key;default:gen_random_uuid()\"`\n\tProductID uuid.UUID `gorm:\"type:uuid;not null;index\"`\n\n\t// Наш внутренний, \"очищенный\" тип операции\n\tOpType OperationType `gorm:\"type:varchar(50);index\"`\n\n\t// Raw данные из iiko для отладки и детализации\n\tDocumentType string `gorm:\"type:varchar(100);index\"` // INCOMING_INVOICE, etc.\n\tTransactionType string `gorm:\"type:varchar(100)\"` // INVOICE, WRITEOFF, etc.\n\tDocumentNumber string `gorm:\"type:varchar(100)\"`\n\n\tAmount decimal.Decimal `gorm:\"type:numeric(19,4)\"`\n\tSum decimal.Decimal `gorm:\"type:numeric(19,4)\"`\n\tCost decimal.Decimal `gorm:\"type:numeric(19,4)\"`\n\n\t// Период синхронизации (для перезаписи данных)\n\tPeriodFrom time.Time `gorm:\"index\"`\n\tPeriodTo time.Time `gorm:\"index\"`\n\n\tProduct catalog.Product `gorm:\"foreignKey:ProductID\"`\n\n\tCreatedAt time.Time\n}\n\ntype Repository interface {\n\tSaveOperations(ops []StoreOperation, opType OperationType, dateFrom, dateTo time.Time) error\n}\n", "internal/domain/recipes/entity.go": "package recipes\n\nimport (\n\t\"time\"\n\n\t\"rmser/internal/domain/catalog\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/shopspring/decimal\"\n)\n\n// Recipe - Технологическая карта\ntype Recipe struct {\n\tID uuid.UUID `gorm:\"type:uuid;primary_key;\"`\n\tProductID uuid.UUID `gorm:\"type:uuid;not null;index\"`\n\tDateFrom time.Time `gorm:\"index\"`\n\tDateTo *time.Time\n\n\tProduct catalog.Product `gorm:\"foreignKey:ProductID\"`\n\tItems []RecipeItem `gorm:\"foreignKey:RecipeID;constraint:OnDelete:CASCADE\"`\n}\n\n// RecipeItem - Ингредиент\ntype RecipeItem struct {\n\tID uuid.UUID `gorm:\"type:uuid;primary_key;default:gen_random_uuid()\"`\n\tRecipeID uuid.UUID `gorm:\"type:uuid;not null;index\"`\n\tProductID uuid.UUID `gorm:\"type:uuid;not null;index\"`\n\tAmountIn decimal.Decimal `gorm:\"type:numeric(19,4);not null\"`\n\tAmountOut decimal.Decimal `gorm:\"type:numeric(19,4);not null\"`\n\n\tProduct catalog.Product `gorm:\"foreignKey:ProductID\"`\n}\n\ntype Repository interface {\n\tSaveRecipes(recipes []Recipe) error\n}\n", "internal/domain/recommendations/entity.go": "package recommendations\n\nimport (\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n)\n\n// Типы проблем\nconst (\n\tTypeUnused = \"UNUSED_IN_RECIPES\" // Товар не используется в техкартах\n\tTypeNoIncoming = \"NO_INCOMING\" // Ингредиент (GOODS) в техкарте, но нет приходов\n\tTypeStale = \"STALE_GOODS\" // Есть приходы, но нет продаж\n\tTypeDishInRecipe = \"DISH_IN_RECIPE\" // Блюдо (DISH) в составе другого блюда\n\tTypePurchasedButUnused = \"PURCHASED_BUT_UNUSED\" // Активно закупается, но нет в техкартах\n\tTypeUsageNoIncoming = \"USAGE_NO_INCOMING\" // Есть расходы, но нет приходов\n)\n\n// Recommendation - Результат анализа\ntype Recommendation struct {\n\tID uuid.UUID `gorm:\"type:uuid;primary_key;default:gen_random_uuid()\"`\n\tType string `gorm:\"type:varchar(50);index\"`\n\tProductID uuid.UUID `gorm:\"type:uuid;index\"`\n\tProductName string `gorm:\"type:varchar(255)\"`\n\tReason string `gorm:\"type:text\"`\n\n\tCreatedAt time.Time\n}\n\n// Repository отвечает за аналитические выборки и хранение результатов\ntype Repository interface {\n\t// Методы анализа (возвращают список структур, но не пишут в БД)\n\tFindUnusedGoods() ([]Recommendation, error)\n\tFindNoIncomingIngredients(days int) ([]Recommendation, error)\n\tFindStaleGoods(days int) ([]Recommendation, error)\n\tFindDishesInRecipes() ([]Recommendation, error)\n\tFindPurchasedButUnused(days int) ([]Recommendation, error)\n\tFindUsageWithoutPurchase(days int) ([]Recommendation, error)\n\n\t// Методы \"Кэша\" в БД\n\tSaveAll(items []Recommendation) error // Удаляет старые и пишет новые\n\tGetAll() ([]Recommendation, error)\n}\n", - "internal/infrastructure/db/postgres.go": "package db\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"regexp\"\n\t\"rmser/internal/domain/catalog\"\n\t\"rmser/internal/domain/invoices\"\n\t\"rmser/internal/domain/operations\"\n\t\"rmser/internal/domain/recipes\"\n\t\"rmser/internal/domain/recommendations\"\n\t\"time\"\n\n\t_ \"github.com/jackc/pgx/v5/stdlib\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/logger\"\n)\n\nfunc NewPostgresDB(dsn string) *gorm.DB {\n\t// 1. Проверка и создание БД перед основным подключением\n\tensureDBExists(dsn)\n\n\t// 2. Настройка логгера GORM\n\tnewLogger := logger.New(\n\t\tlog.New(os.Stdout, \"\\r\\n\", log.LstdFlags),\n\t\tlogger.Config{\n\t\t\tSlowThreshold: time.Second,\n\t\t\tLogLevel: logger.Warn,\n\t\t\tIgnoreRecordNotFoundError: true,\n\t\t\tColorful: true,\n\t\t},\n\t)\n\n\t// 3. Основное подключение\n\tdb, err := gorm.Open(postgres.Open(dsn), &gorm.Config{\n\t\tLogger: newLogger,\n\t})\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"не удалось подключиться к БД: %v\", err))\n\t}\n\n\t// 4. Автомиграция\n\terr = db.AutoMigrate(\n\t\t&catalog.Product{},\n\t\t&recipes.Recipe{},\n\t\t&recipes.RecipeItem{},\n\t\t&invoices.Invoice{},\n\t\t&invoices.InvoiceItem{},\n\t\t&operations.StoreOperation{},\n\t\t&recommendations.Recommendation{},\n\t)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"ошибка миграции БД: %v\", err))\n\t}\n\n\treturn db\n}\n\n// ensureDBExists подключается к системной БД 'postgres' и создает целевую, если её нет\nfunc ensureDBExists(fullDSN string) {\n\t// Регулярка для извлечения имени базы из DSN (ищем dbname=... )\n\tre := regexp.MustCompile(`dbname=([^\\s]+)`)\n\tmatches := re.FindStringSubmatch(fullDSN)\n\n\tif len(matches) < 2 {\n\t\t// Если не нашли dbname, возможно формат URL (postgres://...),\n\t\t// пропускаем авто-создание, полагаемся на ошибку драйвера\n\t\treturn\n\t}\n\n\ttargetDB := matches[1]\n\n\t// Заменяем целевую БД на системную 'postgres' для подключения\n\tmaintenanceDSN := re.ReplaceAllString(fullDSN, \"dbname=postgres\")\n\n\t// Используем стандартный sql драйвер через pgx (который под капотом у gorm/postgres)\n\t// Важно: нам не нужен GORM здесь, нужен чистый SQL для CREATE DATABASE\n\tdb, err := sql.Open(\"pgx\", maintenanceDSN)\n\tif err != nil {\n\t\t// Если не вышло подключиться к postgres, просто выходим,\n\t\t// основная ошибка вылетит при попытке gorm.Open\n\t\tlog.Printf(\"[WARN] Не удалось подключиться к системной БД для проверки: %v\", err)\n\t\treturn\n\t}\n\tdefer db.Close()\n\n\t// Проверяем существование базы\n\tvar exists bool\n\tcheckSQL := fmt.Sprintf(\"SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = '%s')\", targetDB)\n\terr = db.QueryRow(checkSQL).Scan(&exists)\n\tif err != nil {\n\t\tlog.Printf(\"[WARN] Ошибка проверки существования БД: %v\", err)\n\t\treturn\n\t}\n\n\tif !exists {\n\t\tlog.Printf(\"[INFO] База данных '%s' не найдена. Создаю...\", targetDB)\n\t\t// CREATE DATABASE не может быть выполнен в транзакции, поэтому Exec\n\t\t_, err = db.Exec(fmt.Sprintf(\"CREATE DATABASE \\\"%s\\\"\", targetDB))\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"не удалось создать базу данных %s: %v\", targetDB, err))\n\t\t}\n\t\tlog.Printf(\"[INFO] База данных '%s' успешно создана\", targetDB)\n\t}\n}\n", + "internal/infrastructure/db/postgres.go": "package db\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"regexp\"\n\t\"rmser/internal/domain/catalog\"\n\t\"rmser/internal/domain/invoices\"\n\t\"rmser/internal/domain/ocr\"\n\t\"rmser/internal/domain/operations\"\n\t\"rmser/internal/domain/recipes\"\n\t\"rmser/internal/domain/recommendations\"\n\t\"time\"\n\n\t_ \"github.com/jackc/pgx/v5/stdlib\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/logger\"\n)\n\nfunc NewPostgresDB(dsn string) *gorm.DB {\n\t// 1. Проверка и создание БД перед основным подключением\n\tensureDBExists(dsn)\n\n\t// 2. Настройка логгера GORM\n\tnewLogger := logger.New(\n\t\tlog.New(os.Stdout, \"\\r\\n\", log.LstdFlags),\n\t\tlogger.Config{\n\t\t\tSlowThreshold: time.Second,\n\t\t\tLogLevel: logger.Warn,\n\t\t\tIgnoreRecordNotFoundError: true,\n\t\t\tColorful: true,\n\t\t},\n\t)\n\n\t// 3. Основное подключение\n\tdb, err := gorm.Open(postgres.Open(dsn), &gorm.Config{\n\t\tLogger: newLogger,\n\t})\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"не удалось подключиться к БД: %v\", err))\n\t}\n\n\t// 4. Автомиграция\n\terr = db.AutoMigrate(\n\t\t&catalog.Product{},\n\t\t&recipes.Recipe{},\n\t\t&recipes.RecipeItem{},\n\t\t&invoices.Invoice{},\n\t\t&invoices.InvoiceItem{},\n\t\t&operations.StoreOperation{},\n\t\t&recommendations.Recommendation{},\n\t\t&ocr.ProductMatch{},\n\t)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"ошибка миграции БД: %v\", err))\n\t}\n\n\treturn db\n}\n\n// ensureDBExists подключается к системной БД 'postgres' и создает целевую, если её нет\nfunc ensureDBExists(fullDSN string) {\n\t// Регулярка для извлечения имени базы из DSN (ищем dbname=... )\n\tre := regexp.MustCompile(`dbname=([^\\s]+)`)\n\tmatches := re.FindStringSubmatch(fullDSN)\n\n\tif len(matches) < 2 {\n\t\t// Если не нашли dbname, возможно формат URL (postgres://...),\n\t\t// пропускаем авто-создание, полагаемся на ошибку драйвера\n\t\treturn\n\t}\n\n\ttargetDB := matches[1]\n\n\t// Заменяем целевую БД на системную 'postgres' для подключения\n\tmaintenanceDSN := re.ReplaceAllString(fullDSN, \"dbname=postgres\")\n\n\t// Используем стандартный sql драйвер через pgx (который под капотом у gorm/postgres)\n\t// Важно: нам не нужен GORM здесь, нужен чистый SQL для CREATE DATABASE\n\tdb, err := sql.Open(\"pgx\", maintenanceDSN)\n\tif err != nil {\n\t\t// Если не вышло подключиться к postgres, просто выходим,\n\t\t// основная ошибка вылетит при попытке gorm.Open\n\t\tlog.Printf(\"[WARN] Не удалось подключиться к системной БД для проверки: %v\", err)\n\t\treturn\n\t}\n\tdefer db.Close()\n\n\t// Проверяем существование базы\n\tvar exists bool\n\tcheckSQL := fmt.Sprintf(\"SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = '%s')\", targetDB)\n\terr = db.QueryRow(checkSQL).Scan(&exists)\n\tif err != nil {\n\t\tlog.Printf(\"[WARN] Ошибка проверки существования БД: %v\", err)\n\t\treturn\n\t}\n\n\tif !exists {\n\t\tlog.Printf(\"[INFO] База данных '%s' не найдена. Создаю...\", targetDB)\n\t\t// CREATE DATABASE не может быть выполнен в транзакции, поэтому Exec\n\t\t_, err = db.Exec(fmt.Sprintf(\"CREATE DATABASE \\\"%s\\\"\", targetDB))\n\t\tif err != nil {\n\t\t\tpanic(fmt.Sprintf(\"не удалось создать базу данных %s: %v\", targetDB, err))\n\t\t}\n\t\tlog.Printf(\"[INFO] База данных '%s' успешно создана\", targetDB)\n\t}\n}\n", + "internal/infrastructure/ocr_client/client.go": "package ocr_client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"time\"\n)\n\ntype Client struct {\n\tpythonServiceURL string\n\thttpClient *http.Client\n}\n\nfunc NewClient(pythonServiceURL string) *Client {\n\treturn &Client{\n\t\tpythonServiceURL: pythonServiceURL,\n\t\thttpClient: &http.Client{\n\t\t\t// OCR может быть долгим, ставим таймаут побольше (например, 30 сек)\n\t\t\tTimeout: 30 * time.Second,\n\t\t},\n\t}\n}\n\n// ProcessImage отправляет изображение в Python и возвращает сырые данные\nfunc (c *Client) ProcessImage(ctx context.Context, imageData []byte, filename string) (*RecognitionResult, error) {\n\t// 1. Создаем буфер для multipart формы\n\tbody := &bytes.Buffer{}\n\twriter := multipart.NewWriter(body)\n\n\t// Создаем заголовок части вручную, чтобы прописать Content-Type: image/jpeg\n\th := make(textproto.MIMEHeader)\n\th.Set(\"Content-Disposition\", fmt.Sprintf(`form-data; name=\"image\"; filename=\"%s\"`, filename))\n\th.Set(\"Content-Type\", \"image/jpeg\") // Явно указываем, что это картинка\n\n\tpart, err := writer.CreatePart(h)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create part error: %w\", err)\n\t}\n\n\t// Записываем байты картинки\n\tif _, err := io.Copy(part, bytes.NewReader(imageData)); err != nil {\n\t\treturn nil, fmt.Errorf(\"copy file error: %w\", err)\n\t}\n\n\t// Закрываем writer, чтобы записать boundary\n\tif err := writer.Close(); err != nil {\n\t\treturn nil, fmt.Errorf(\"writer close error: %w\", err)\n\t}\n\n\t// 2. Создаем запрос\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", c.pythonServiceURL+\"/recognize\", body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create request error: %w\", err)\n\t}\n\n\t// Важно: Content-Type с boundary\n\treq.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\n\t// 3. Отправляем\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ocr service request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, _ := io.ReadAll(resp.Body)\n\t\treturn nil, fmt.Errorf(\"ocr service error (code %d): %s\", resp.StatusCode, string(bodyBytes))\n\t}\n\n\t// 4. Парсим ответ\n\tvar result RecognitionResult\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn nil, fmt.Errorf(\"json decode error: %w\", err)\n\t}\n\n\treturn &result, nil\n}\n", + "internal/infrastructure/ocr_client/dto.go": "package ocr_client\n\n// RecognitionResult - ответ от Python сервиса\ntype RecognitionResult struct {\n\tItems []RecognizedItem `json:\"items\"`\n}\n\ntype RecognizedItem struct {\n\tRawName string `json:\"raw_name\"` // Текст названия из чека\n\tAmount float64 `json:\"amount\"` // Кол-во\n\tPrice float64 `json:\"price\"` // Цена\n\tSum float64 `json:\"sum\"` // Сумма\n}\n", "internal/infrastructure/redis/client.go": "package redis\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype Client struct {\n\trdb *redis.Client\n}\n\nfunc NewClient(addr, password string, dbIndex int) (*Client, error) {\n\trdb := redis.NewClient(&redis.Options{\n\t\tAddr: addr,\n\t\tPassword: password,\n\t\tDB: dbIndex,\n\t})\n\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\tif err := rdb.Ping(ctx).Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"ошибка подключения к Redis: %w\", err)\n\t}\n\n\treturn &Client{rdb: rdb}, nil\n}\n\n// Set сохраняет значение (структуру) в JSON\nfunc (c *Client) Set(ctx context.Context, key string, value any, ttl time.Duration) error {\n\tbytes, err := json.Marshal(value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"json marshal error: %w\", err)\n\t}\n\treturn c.rdb.Set(ctx, key, bytes, ttl).Err()\n}\n\n// Get загружает значение в переданный указатель dest\nfunc (c *Client) Get(ctx context.Context, key string, dest any) error {\n\tval, err := c.rdb.Get(ctx, key).Result()\n\tif err != nil {\n\t\tif err == redis.Nil {\n\t\t\treturn nil // Ключ не найден, не считаем ошибкой\n\t\t}\n\t\treturn err\n\t}\n\treturn json.Unmarshal([]byte(val), dest)\n}\n", - "internal/infrastructure/repository/catalog/postgres.go": "package catalog\n\nimport (\n\t\"rmser/internal/domain/catalog\"\n\n\t\"github.com/google/uuid\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n)\n\ntype pgRepository struct {\n\tdb *gorm.DB\n}\n\nfunc NewRepository(db *gorm.DB) catalog.Repository {\n\treturn &pgRepository{db: db}\n}\n\nfunc (r *pgRepository) SaveProducts(products []catalog.Product) error {\n\t// Сортировка (родители -> дети)\n\tsorted := sortProductsByHierarchy(products)\n\treturn r.db.Clauses(clause.OnConflict{\n\t\tColumns: []clause.Column{{Name: \"id\"}},\n\t\tUpdateAll: true,\n\t}).CreateInBatches(sorted, 100).Error\n}\n\nfunc (r *pgRepository) GetAll() ([]catalog.Product, error) {\n\tvar products []catalog.Product\n\terr := r.db.Find(&products).Error\n\treturn products, err\n}\n\n// Вспомогательная функция сортировки\nfunc sortProductsByHierarchy(products []catalog.Product) []catalog.Product {\n\tif len(products) == 0 {\n\t\treturn products\n\t}\n\tchildrenMap := make(map[uuid.UUID][]catalog.Product)\n\tvar roots []catalog.Product\n\tallIDs := make(map[uuid.UUID]struct{}, len(products))\n\n\tfor _, p := range products {\n\t\tallIDs[p.ID] = struct{}{}\n\t}\n\n\tfor _, p := range products {\n\t\tif p.ParentID == nil {\n\t\t\troots = append(roots, p)\n\t\t} else {\n\t\t\tif _, exists := allIDs[*p.ParentID]; exists {\n\t\t\t\tchildrenMap[*p.ParentID] = append(childrenMap[*p.ParentID], p)\n\t\t\t} else {\n\t\t\t\troots = append(roots, p)\n\t\t\t}\n\t\t}\n\t}\n\n\tresult := make([]catalog.Product, 0, len(products))\n\tqueue := roots\n\tfor len(queue) > 0 {\n\t\tcurrent := queue[0]\n\t\tqueue = queue[1:]\n\t\tresult = append(result, current)\n\t\tif children, ok := childrenMap[current.ID]; ok {\n\t\t\tqueue = append(queue, children...)\n\t\t\tdelete(childrenMap, current.ID)\n\t\t}\n\t}\n\tfor _, remaining := range childrenMap {\n\t\tresult = append(result, remaining...)\n\t}\n\treturn result\n}\n", + "internal/infrastructure/repository/catalog/postgres.go": "package catalog\n\nimport (\n\t\"rmser/internal/domain/catalog\"\n\n\t\"github.com/google/uuid\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n)\n\ntype pgRepository struct {\n\tdb *gorm.DB\n}\n\nfunc NewRepository(db *gorm.DB) catalog.Repository {\n\treturn &pgRepository{db: db}\n}\n\nfunc (r *pgRepository) SaveProducts(products []catalog.Product) error {\n\t// Сортировка (родители -> дети)\n\tsorted := sortProductsByHierarchy(products)\n\treturn r.db.Clauses(clause.OnConflict{\n\t\tColumns: []clause.Column{{Name: \"id\"}},\n\t\tUpdateAll: true,\n\t}).CreateInBatches(sorted, 100).Error\n}\n\nfunc (r *pgRepository) GetAll() ([]catalog.Product, error) {\n\tvar products []catalog.Product\n\terr := r.db.Find(&products).Error\n\treturn products, err\n}\n\n// Вспомогательная функция сортировки\nfunc sortProductsByHierarchy(products []catalog.Product) []catalog.Product {\n\tif len(products) == 0 {\n\t\treturn products\n\t}\n\tchildrenMap := make(map[uuid.UUID][]catalog.Product)\n\tvar roots []catalog.Product\n\tallIDs := make(map[uuid.UUID]struct{}, len(products))\n\n\tfor _, p := range products {\n\t\tallIDs[p.ID] = struct{}{}\n\t}\n\n\tfor _, p := range products {\n\t\tif p.ParentID == nil {\n\t\t\troots = append(roots, p)\n\t\t} else {\n\t\t\tif _, exists := allIDs[*p.ParentID]; exists {\n\t\t\t\tchildrenMap[*p.ParentID] = append(childrenMap[*p.ParentID], p)\n\t\t\t} else {\n\t\t\t\troots = append(roots, p)\n\t\t\t}\n\t\t}\n\t}\n\n\tresult := make([]catalog.Product, 0, len(products))\n\tqueue := roots\n\tfor len(queue) > 0 {\n\t\tcurrent := queue[0]\n\t\tqueue = queue[1:]\n\t\tresult = append(result, current)\n\t\tif children, ok := childrenMap[current.ID]; ok {\n\t\t\tqueue = append(queue, children...)\n\t\t\tdelete(childrenMap, current.ID)\n\t\t}\n\t}\n\tfor _, remaining := range childrenMap {\n\t\tresult = append(result, remaining...)\n\t}\n\treturn result\n}\n\n// GetActiveGoods возвращает только активные товары (не удаленные, тип GOODS)\nfunc (r *pgRepository) GetActiveGoods() ([]catalog.Product, error) {\n\tvar products []catalog.Product\n\t// iikoRMS: GOODS - товары, PREPARED - заготовки (иногда их тоже покупают)\n\terr := r.db.Where(\"is_deleted = ? AND type IN ?\", false, []string{\"GOODS\"}).\n\t\tOrder(\"name ASC\").\n\t\tFind(&products).Error\n\treturn products, err\n}\n", "internal/infrastructure/repository/invoices/postgres.go": "package invoices\n\nimport (\n\t\"time\"\n\n\t\"rmser/internal/domain/invoices\"\n\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n)\n\ntype pgRepository struct {\n\tdb *gorm.DB\n}\n\nfunc NewRepository(db *gorm.DB) invoices.Repository {\n\treturn &pgRepository{db: db}\n}\n\nfunc (r *pgRepository) GetLastInvoiceDate() (*time.Time, error) {\n\tvar inv invoices.Invoice\n\terr := r.db.Order(\"date_incoming DESC\").First(&inv).Error\n\tif err != nil {\n\t\tif err == gorm.ErrRecordNotFound {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn &inv.DateIncoming, nil\n}\n\nfunc (r *pgRepository) SaveInvoices(list []invoices.Invoice) error {\n\treturn r.db.Transaction(func(tx *gorm.DB) error {\n\t\tfor _, inv := range list {\n\t\t\tif err := tx.Omit(\"Items\").Clauses(clause.OnConflict{\n\t\t\t\tColumns: []clause.Column{{Name: \"id\"}},\n\t\t\t\tUpdateAll: true,\n\t\t\t}).Create(&inv).Error; err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := tx.Where(\"invoice_id = ?\", inv.ID).Delete(&invoices.InvoiceItem{}).Error; err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif len(inv.Items) > 0 {\n\t\t\t\tif err := tx.Create(&inv.Items).Error; err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n", + "internal/infrastructure/repository/ocr/postgres.go": "package ocr\n\nimport (\n\t\"strings\"\n\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\n\t\"rmser/internal/domain/ocr\"\n\n\t\"github.com/google/uuid\"\n)\n\ntype pgRepository struct {\n\tdb *gorm.DB\n}\n\nfunc NewRepository(db *gorm.DB) ocr.Repository {\n\treturn &pgRepository{db: db}\n}\n\nfunc (r *pgRepository) SaveMatch(rawName string, productID uuid.UUID) error {\n\tnormalized := strings.ToLower(strings.TrimSpace(rawName))\n\tmatch := ocr.ProductMatch{\n\t\tRawName: normalized,\n\t\tProductID: productID,\n\t}\n\n\t// Upsert: если такая строка уже была, обновляем ссылку на товар\n\treturn r.db.Clauses(clause.OnConflict{\n\t\tColumns: []clause.Column{{Name: \"raw_name\"}},\n\t\tDoUpdates: clause.AssignmentColumns([]string{\"product_id\", \"updated_at\"}),\n\t}).Create(&match).Error\n}\n\nfunc (r *pgRepository) FindMatch(rawName string) (*uuid.UUID, error) {\n\tnormalized := strings.ToLower(strings.TrimSpace(rawName))\n\tvar match ocr.ProductMatch\n\n\terr := r.db.Where(\"raw_name = ?\", normalized).First(&match).Error\n\tif err != nil {\n\t\tif err == gorm.ErrRecordNotFound {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn &match.ProductID, nil\n}\n\nfunc (r *pgRepository) GetAllMatches() ([]ocr.ProductMatch, error) {\n\tvar matches []ocr.ProductMatch\n\t// Preload(\"Product\") загружает связанную сущность товара,\n\t// чтобы мы видели не только ID, но и название товара из каталога.\n\terr := r.db.Preload(\"Product\").Order(\"updated_at DESC\").Find(&matches).Error\n\treturn matches, err\n}\n", "internal/infrastructure/repository/operations/postgres.go": "package operations\n\nimport (\n\t\"time\"\n\n\t\"rmser/internal/domain/operations\"\n\n\t\"gorm.io/gorm\"\n)\n\ntype pgRepository struct {\n\tdb *gorm.DB\n}\n\nfunc NewRepository(db *gorm.DB) operations.Repository {\n\treturn &pgRepository{db: db}\n}\n\nfunc (r *pgRepository) SaveOperations(ops []operations.StoreOperation, opType operations.OperationType, dateFrom, dateTo time.Time) error {\n\treturn r.db.Transaction(func(tx *gorm.DB) error {\n\t\t// 1. Удаляем старые записи этого типа, которые пересекаются с периодом.\n\t\t// Так как отчет агрегированный, мы привязываемся к периоду \"с\" и \"по\".\n\t\t// Упрощение: удаляем всё, где PeriodFrom совпадает с текущей выгрузкой,\n\t\t// предполагая, что мы всегда грузим одними и теми же квантами (например, месяц или неделя).\n\t\t// Для надежности удалим всё, что попадает в диапазон.\n\t\tif err := tx.Where(\"op_type = ? AND period_from >= ? AND period_to <= ?\", opType, dateFrom, dateTo).\n\t\t\tDelete(&operations.StoreOperation{}).Error; err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// 2. Вставляем новые\n\t\tif len(ops) > 0 {\n\t\t\tif err := tx.CreateInBatches(ops, 500).Error; err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n", "internal/infrastructure/repository/recipes/postgres.go": "package recipes\n\nimport (\n\t\"rmser/internal/domain/recipes\"\n\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n)\n\ntype pgRepository struct {\n\tdb *gorm.DB\n}\n\nfunc NewRepository(db *gorm.DB) recipes.Repository {\n\treturn &pgRepository{db: db}\n}\n\nfunc (r *pgRepository) SaveRecipes(list []recipes.Recipe) error {\n\treturn r.db.Transaction(func(tx *gorm.DB) error {\n\t\tfor _, recipe := range list {\n\t\t\tif err := tx.Omit(\"Items\").Clauses(clause.OnConflict{\n\t\t\t\tColumns: []clause.Column{{Name: \"id\"}},\n\t\t\t\tUpdateAll: true,\n\t\t\t}).Create(&recipe).Error; err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := tx.Where(\"recipe_id = ?\", recipe.ID).Delete(&recipes.RecipeItem{}).Error; err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif len(recipe.Items) > 0 {\n\t\t\t\tif err := tx.Create(&recipe.Items).Error; err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n", "internal/infrastructure/repository/recommendations/postgres.go": "package recommendations\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"rmser/internal/domain/operations\"\n\t\"rmser/internal/domain/recommendations\"\n\n\t\"gorm.io/gorm\"\n)\n\ntype pgRepository struct {\n\tdb *gorm.DB\n}\n\nfunc NewRepository(db *gorm.DB) recommendations.Repository {\n\treturn &pgRepository{db: db}\n}\n\n// --- Методы Хранения ---\n\nfunc (r *pgRepository) SaveAll(items []recommendations.Recommendation) error {\n\treturn r.db.Transaction(func(tx *gorm.DB) error {\n\t\tif err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Delete(&recommendations.Recommendation{}).Error; err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(items) > 0 {\n\t\t\tif err := tx.CreateInBatches(items, 100).Error; err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc (r *pgRepository) GetAll() ([]recommendations.Recommendation, error) {\n\tvar items []recommendations.Recommendation\n\terr := r.db.Find(&items).Error\n\treturn items, err\n}\n\n// --- Методы Аналитики ---\n\n// 1. Товары (GOODS/PREPARED), не используемые в техкартах\nfunc (r *pgRepository) FindUnusedGoods() ([]recommendations.Recommendation, error) {\n\tvar results []recommendations.Recommendation\n\n\tquery := `\n\t\tSELECT \n\t\t\tp.id as product_id, \n\t\t\tp.name as product_name,\n\t\t\t'Товар не используется ни в одной техкарте' as reason,\n\t\t\t? as type\n\t\tFROM products p\n\t\tWHERE p.type IN ('GOODS', 'PREPARED') \n\t\t AND p.is_deleted = false -- Проверка на удаление\n\t\t AND p.id NOT IN (\n\t\t SELECT DISTINCT product_id FROM recipe_items\n\t\t )\n\t\t AND p.id NOT IN (\n\t\t SELECT DISTINCT product_id FROM recipes\n\t\t )\n\t\tORDER BY p.name ASC\n\t`\n\n\tif err := r.db.Raw(query, recommendations.TypeUnused).Scan(&results).Error; err != nil {\n\t\treturn nil, err\n\t}\n\treturn results, nil\n}\n\n// 2. Закупается, но нет в техкартах\nfunc (r *pgRepository) FindPurchasedButUnused(days int) ([]recommendations.Recommendation, error) {\n\tvar results []recommendations.Recommendation\n\tdateFrom := time.Now().AddDate(0, 0, -days)\n\n\tquery := `\n\t\tSELECT DISTINCT\n\t\t\tp.id as product_id,\n\t\t\tp.name as product_name,\n\t\t\t'Товар активно закупается, но не включен ни в одну техкарту' as reason,\n\t\t\t? as type\n\t\tFROM store_operations so\n\t\tJOIN products p ON so.product_id = p.id\n\t\tWHERE \n\t\t\tso.op_type = ? \n\t\t\tAND so.period_from >= ?\n\t\t\tAND p.is_deleted = false -- Проверка на удаление\n\t\t\tAND p.id NOT IN ( \n\t\t\t\tSELECT DISTINCT product_id FROM recipe_items\n\t\t\t)\n\t\tORDER BY p.name ASC\n\t`\n\n\tif err := r.db.Raw(query, recommendations.TypePurchasedButUnused, operations.OpTypePurchase, dateFrom).Scan(&results).Error; err != nil {\n\t\treturn nil, err\n\t}\n\treturn results, nil\n}\n\n// 3. Ингредиенты в актуальных техкартах без закупок\nfunc (r *pgRepository) FindNoIncomingIngredients(days int) ([]recommendations.Recommendation, error) {\n\tvar results []recommendations.Recommendation\n\tdateFrom := time.Now().AddDate(0, 0, -days)\n\n\tquery := `\n\t\tSELECT\n\t\t\tp.id as product_id,\n\t\t\tp.name as product_name,\n\t\t\t'Нет закупок (' || ? || ' дн). Входит в: ' || STRING_AGG(DISTINCT parent.name, ', ') as reason,\n\t\t\t? as type\n\t\tFROM recipe_items ri\n\t\tJOIN recipes r ON ri.recipe_id = r.id\n\t\tJOIN products p ON ri.product_id = p.id\n\t\tJOIN products parent ON r.product_id = parent.id\n\t\tWHERE \n\t\t\t(r.date_to IS NULL OR r.date_to >= CURRENT_DATE)\n\t\t\tAND p.type = 'GOODS'\n\t\t\tAND p.is_deleted = false -- Сам ингредиент не удален\n\t\t\tAND parent.is_deleted = false -- Блюдо, в которое он входит, не удалено\n\t\t\tAND p.id NOT IN (\n\t\t\t\tSELECT product_id \n\t\t\t\tFROM store_operations \n\t\t\t\tWHERE op_type = ? \n\t\t\t\t AND period_from >= ?\n\t\t\t)\n\t\tGROUP BY p.id, p.name\n\t\tORDER BY p.name ASC\n\t`\n\n\tif err := r.db.Raw(query, strconv.Itoa(days), recommendations.TypeNoIncoming, operations.OpTypePurchase, dateFrom).Scan(&results).Error; err != nil {\n\t\treturn nil, err\n\t}\n\treturn results, nil\n}\n\n// 4. Товары, которые закупаем, но не расходуем (\"Висяки\")\nfunc (r *pgRepository) FindStaleGoods(days int) ([]recommendations.Recommendation, error) {\n\tvar results []recommendations.Recommendation\n\tdateFrom := time.Now().AddDate(0, 0, -days)\n\n\tquery := `\n\t\tSELECT DISTINCT\n\t\t\tp.id as product_id,\n\t\t\tp.name as product_name,\n\t\t\t? as reason,\n\t\t\t? as type\n\t\tFROM store_operations so\n\t\tJOIN products p ON so.product_id = p.id\n\t\tWHERE \n\t\t\tso.op_type = ? \n\t\t\tAND so.period_from >= ?\n\t\t\tAND p.is_deleted = false -- Проверка на удаление\n\t\t\tAND p.id NOT IN ( \n\t\t\t\tSELECT product_id \n\t\t\t\tFROM store_operations \n\t\t\t\tWHERE op_type = ? \n\t\t\t\t AND period_from >= ?\n\t\t\t)\n\t\tORDER BY p.name ASC\n\t`\n\n\treason := fmt.Sprintf(\"Были закупки, но нет расхода за %d дн.\", days)\n\n\tif err := r.db.Raw(query, reason, recommendations.TypeStale, operations.OpTypePurchase, dateFrom, operations.OpTypeUsage, dateFrom).\n\t\tScan(&results).Error; err != nil {\n\t\treturn nil, err\n\t}\n\treturn results, nil\n}\n\n// 5. Блюдо используется в техкарте другого блюда\nfunc (r *pgRepository) FindDishesInRecipes() ([]recommendations.Recommendation, error) {\n\tvar results []recommendations.Recommendation\n\n\tquery := `\n\t\tSELECT DISTINCT\n\t\t\tchild.id as product_id,\n\t\t\tchild.name as product_name,\n\t\t\t'Является Блюдом (DISH), но указан ингредиентом в: ' || parent.name as reason,\n\t\t\t? as type\n\t\tFROM recipe_items ri\n\t\tJOIN products child ON ri.product_id = child.id\n\t\tJOIN recipes r ON ri.recipe_id = r.id\n\t\tJOIN products parent ON r.product_id = parent.id\n\t\tWHERE \n\t\t\tchild.type = 'DISH'\n\t\t\tAND child.is_deleted = false -- Вложенное блюдо не удалено\n\t\t\tAND parent.is_deleted = false -- Родительское блюдо не удалено\n\t\t\tAND (r.date_to IS NULL OR r.date_to >= CURRENT_DATE)\n\t\tORDER BY child.name ASC\n\t`\n\n\tif err := r.db.Raw(query, recommendations.TypeDishInRecipe).Scan(&results).Error; err != nil {\n\t\treturn nil, err\n\t}\n\treturn results, nil\n}\n\n// 6. Есть расход (Usage), но нет прихода (Purchase)\nfunc (r *pgRepository) FindUsageWithoutPurchase(days int) ([]recommendations.Recommendation, error) {\n\tvar results []recommendations.Recommendation\n\tdateFrom := time.Now().AddDate(0, 0, -days)\n\n\tquery := `\n\t\tSELECT DISTINCT\n\t\t\tp.id as product_id,\n\t\t\tp.name as product_name,\n\t\t\t? as reason,\n\t\t\t? as type\n\t\tFROM store_operations so\n\t\tJOIN products p ON so.product_id = p.id\n\t\tWHERE \n\t\t\tso.op_type = ? -- Есть расход (продажа/списание)\n\t\t\tAND so.period_from >= ?\n\t\t\tAND p.type = 'GOODS' -- Только для товаров\n\t\t\tAND p.is_deleted = false -- Товар жив\n\t\t\tAND p.id NOT IN ( -- Но не было закупок\n\t\t\t\tSELECT product_id \n\t\t\t\tFROM store_operations \n\t\t\t\tWHERE op_type = ? \n\t\t\t\t AND period_from >= ?\n\t\t\t)\n\t\tORDER BY p.name ASC\n\t`\n\n\treason := fmt.Sprintf(\"Товар расходуется (продажи/списания), но не закупался последние %d дн.\", days)\n\n\t// Аргументы: reason, type, OpUsage, date, OpPurchase, date\n\tif err := r.db.Raw(query,\n\t\treason,\n\t\trecommendations.TypeUsageNoIncoming,\n\t\toperations.OpTypeUsage,\n\t\tdateFrom,\n\t\toperations.OpTypePurchase,\n\t\tdateFrom,\n\t).Scan(&results).Error; err != nil {\n\t\treturn nil, err\n\t}\n\treturn results, nil\n}\n", - "internal/infrastructure/rms/client.go": "package rms\n\nimport (\n\t\"crypto/sha1\"\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/shopspring/decimal\"\n\t\"go.uber.org/zap\"\n\n\t\"rmser/internal/domain/catalog\"\n\t\"rmser/internal/domain/invoices\"\n\t\"rmser/internal/domain/recipes\"\n\t\"rmser/pkg/logger\"\n)\n\nconst (\n\ttokenTTL = 45 * time.Minute // Время жизни токена до принудительного обновления\n)\n\n// ClientI интерфейс\ntype ClientI interface {\n\tAuth() error\n\tLogout() error\n\tFetchCatalog() ([]catalog.Product, error)\n\tFetchRecipes(dateFrom, dateTo time.Time) ([]recipes.Recipe, error)\n\tFetchInvoices(from, to time.Time) ([]invoices.Invoice, error)\n\tFetchStoreOperations(presetID string, from, to time.Time) ([]StoreReportItemXML, error)\n}\n\ntype Client struct {\n\tbaseURL string\n\tlogin string\n\tpasswordHash string\n\thttpClient *http.Client\n\n\t// Защита токена для конкурентного доступа\n\tmu sync.RWMutex\n\ttoken string\n\ttokenCreatedAt time.Time\n}\n\nfunc NewClient(baseURL, login, password string) *Client {\n\th := sha1.New()\n\th.Write([]byte(password))\n\tpassHash := fmt.Sprintf(\"%x\", h.Sum(nil))\n\n\treturn &Client{\n\t\tbaseURL: baseURL,\n\t\tlogin: login,\n\t\tpasswordHash: passHash,\n\t\thttpClient: &http.Client{Timeout: 60 * time.Second},\n\t}\n}\n\n// Auth выполняет вход\nfunc (c *Client) Auth() error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\treturn c.authUnsafe()\n}\n\n// authUnsafe - внутренняя логика авторизации без блокировок (вызывается внутри Lock)\nfunc (c *Client) authUnsafe() error {\n\tendpoint := c.baseURL + \"/resto/api/auth\"\n\n\tdata := url.Values{}\n\tdata.Set(\"login\", c.login)\n\tdata.Set(\"pass\", c.passwordHash)\n\n\treq, err := http.NewRequest(\"POST\", endpoint, strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ошибка создания запроса auth: %w\", err)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ошибка сети auth: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"ошибка авторизации (code %d): %s\", resp.StatusCode, string(body))\n\t}\n\n\tc.token = string(body)\n\tc.tokenCreatedAt = time.Now() // Запоминаем время получения\n\tlogger.Log.Info(\"RMS: Успешная авторизация\", zap.String(\"token_preview\", c.token[:5]+\"...\"))\n\treturn nil\n}\n\n// Logout освобождает лицензию\nfunc (c *Client) Logout() error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\treturn c.logoutUnsafe()\n}\n\n// logoutUnsafe - внутренняя логика логаута\nfunc (c *Client) logoutUnsafe() error {\n\tif c.token == \"\" {\n\t\treturn nil\n\t}\n\n\tendpoint := c.baseURL + \"/resto/api/logout\"\n\tdata := url.Values{}\n\tdata.Set(\"key\", c.token)\n\n\treq, err := http.NewRequest(\"POST\", endpoint, strings.NewReader(data.Encode()))\n\tif err == nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\tresp, err := c.httpClient.Do(req)\n\t\tif err == nil {\n\t\t\tdefer resp.Body.Close()\n\t\t\tif resp.StatusCode == http.StatusOK {\n\t\t\t\tlogger.Log.Info(\"RMS: Токен освобожден\")\n\t\t\t} else {\n\t\t\t\tlogger.Log.Warn(\"RMS: Ошибка освобождения токена\", zap.Int(\"code\", resp.StatusCode))\n\t\t\t}\n\t\t}\n\t}\n\n\t// Сбрасываем токен в любом случае, даже если запрос не прошел (он все равно протухнет)\n\tc.token = \"\"\n\tc.tokenCreatedAt = time.Time{}\n\treturn nil\n}\n\n// ensureToken проверяет срок жизни токена и обновляет его при необходимости\nfunc (c *Client) ensureToken() error {\n\tc.mu.RLock()\n\ttoken := c.token\n\tcreatedAt := c.tokenCreatedAt\n\tc.mu.RUnlock()\n\n\t// Если токена нет или он протух\n\tif token == \"\" || time.Since(createdAt) > tokenTTL {\n\t\tc.mu.Lock()\n\t\tdefer c.mu.Unlock()\n\n\t\t// Double check locking (вдруг другая горутина уже обновила)\n\t\tif c.token != \"\" && time.Since(c.tokenCreatedAt) <= tokenTTL {\n\t\t\treturn nil\n\t\t}\n\n\t\tif c.token != \"\" {\n\t\t\tlogger.Log.Info(\"RMS: Время жизни токена истекло (>45 мин), пересоздание...\")\n\t\t\t_ = c.logoutUnsafe() // Пытаемся освободить старый\n\t\t}\n\n\t\treturn c.authUnsafe()\n\t}\n\treturn nil\n}\n\n// doRequest выполняет запрос с автоматическим управлением токеном\nfunc (c *Client) doRequest(method, path string, queryParams map[string]string) (*http.Response, error) {\n\t// 1. Проверка времени жизни (45 минут)\n\tif err := c.ensureToken(); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Читаем токен под RLock\n\tc.mu.RLock()\n\tcurrentToken := c.token\n\tc.mu.RUnlock()\n\n\tbuildURL := func() string {\n\t\tu, _ := url.Parse(c.baseURL + path)\n\t\tq := u.Query()\n\t\tq.Set(\"key\", currentToken)\n\t\tfor k, v := range queryParams {\n\t\t\tq.Set(k, v)\n\t\t}\n\t\tu.RawQuery = q.Encode()\n\t\treturn u.String()\n\t}\n\n\treq, _ := http.NewRequest(method, buildURL(), nil)\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 2. Реактивная обработка 401 (если сервер перезагрузился или убил сессию раньше времени)\n\tif resp.StatusCode == http.StatusUnauthorized {\n\t\tresp.Body.Close()\n\t\tlogger.Log.Warn(\"RMS: Получен 401 Unauthorized, принудительная ре-авторизация...\")\n\n\t\tc.mu.Lock()\n\t\t// Сбрасываем токен и логинимся заново\n\t\tc.token = \"\"\n\t\tauthErr := c.authUnsafe()\n\t\tc.mu.Unlock()\n\n\t\tif authErr != nil {\n\t\t\treturn nil, authErr\n\t\t}\n\n\t\t// Повторяем запрос с новым токеном\n\t\tc.mu.RLock()\n\t\tcurrentToken = c.token\n\t\tc.mu.RUnlock()\n\n\t\treq, _ = http.NewRequest(method, buildURL(), nil)\n\t\treturn c.httpClient.Do(req)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tdefer resp.Body.Close()\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn nil, fmt.Errorf(\"api error: code=%d, body=%s\", resp.StatusCode, string(body))\n\t}\n\n\treturn resp, nil\n}\n\n// --- Методы получения данных (без изменений логики парсинга) ---\n\nfunc (c *Client) FetchCatalog() ([]catalog.Product, error) {\n\tvar products []catalog.Product\n\n\t// Группы\n\trespGroups, err := c.doRequest(\"GET\", \"/resto/api/v2/entities/products/group/list\", map[string]string{\n\t\t\"includeDeleted\": \"true\",\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get groups error: %w\", err)\n\t}\n\tdefer respGroups.Body.Close()\n\n\tvar groupDTOs []GroupDTO\n\tif err := json.NewDecoder(respGroups.Body).Decode(&groupDTOs); err != nil {\n\t\treturn nil, fmt.Errorf(\"json decode groups error: %w\", err)\n\t}\n\n\t// Товары\n\trespProds, err := c.doRequest(\"GET\", \"/resto/api/v2/entities/products/list\", map[string]string{\n\t\t\"includeDeleted\": \"true\",\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get products error: %w\", err)\n\t}\n\tdefer respProds.Body.Close()\n\n\tvar prodDTOs []ProductDTO\n\tif err := json.NewDecoder(respProds.Body).Decode(&prodDTOs); err != nil {\n\t\treturn nil, fmt.Errorf(\"json decode products error: %w\", err)\n\t}\n\n\t// Маппинг групп\n\tfor _, g := range groupDTOs {\n\t\tid, _ := uuid.Parse(g.ID)\n\t\tvar parentID *uuid.UUID\n\t\tif g.ParentID != nil {\n\t\t\tif pid, err := uuid.Parse(*g.ParentID); err == nil {\n\t\t\t\tparentID = &pid\n\t\t\t}\n\t\t}\n\t\tproducts = append(products, catalog.Product{\n\t\t\tID: id,\n\t\t\tParentID: parentID,\n\t\t\tName: g.Name,\n\t\t\tNum: g.Num,\n\t\t\tCode: g.Code,\n\t\t\tType: \"GROUP\",\n\t\t\tIsDeleted: g.Deleted,\n\t\t})\n\t}\n\n\t// Маппинг товаров\n\tfor _, p := range prodDTOs {\n\t\tid, _ := uuid.Parse(p.ID)\n\t\tvar parentID *uuid.UUID\n\t\tif p.ParentID != nil {\n\t\t\tif pid, err := uuid.Parse(*p.ParentID); err == nil {\n\t\t\t\tparentID = &pid\n\t\t\t}\n\t\t}\n\t\tproducts = append(products, catalog.Product{\n\t\t\tID: id,\n\t\t\tParentID: parentID,\n\t\t\tName: p.Name,\n\t\t\tNum: p.Num,\n\t\t\tCode: p.Code,\n\t\t\tType: p.Type,\n\t\t\tUnitWeight: decimal.NewFromFloat(p.UnitWeight),\n\t\t\tUnitCapacity: decimal.NewFromFloat(p.UnitCapacity),\n\t\t\tIsDeleted: p.Deleted,\n\t\t})\n\t}\n\n\treturn products, nil\n}\n\nfunc (c *Client) FetchRecipes(dateFrom, dateTo time.Time) ([]recipes.Recipe, error) {\n\tparams := map[string]string{\n\t\t\"dateFrom\": dateFrom.Format(\"2006-01-02\"),\n\t}\n\tif !dateTo.IsZero() {\n\t\tparams[\"dateTo\"] = dateTo.Format(\"2006-01-02\")\n\t}\n\n\tresp, err := c.doRequest(\"GET\", \"/resto/api/v2/assemblyCharts/getAll\", params)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get recipes error: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar apiResp AssemblyChartsResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"json decode recipes error: %w\", err)\n\t}\n\n\tvar allrecipes []recipes.Recipe\n\tfor _, chart := range apiResp.AssemblyCharts {\n\t\trID, _ := uuid.Parse(chart.ID)\n\t\tpID, _ := uuid.Parse(chart.AssembledProductID)\n\n\t\tdf, _ := time.Parse(\"2006-01-02\", chart.DateFrom)\n\t\tvar dt *time.Time\n\t\tif chart.DateTo != nil {\n\t\t\tif t, err := time.Parse(\"2006-01-02\", *chart.DateTo); err == nil {\n\t\t\t\tdt = &t\n\t\t\t}\n\t\t}\n\n\t\tvar items []recipes.RecipeItem\n\t\tfor _, item := range chart.Items {\n\t\t\tiPID, _ := uuid.Parse(item.ProductID)\n\n\t\t\t// FIX: Генерируем уникальный ID для каждой строки в нашей БД,\n\t\t\t// чтобы избежать конфликтов PK при переиспользовании строк в iiko.\n\t\t\titems = append(items, recipes.RecipeItem{\n\t\t\t\tID: uuid.New(),\n\t\t\t\tRecipeID: rID,\n\t\t\t\tProductID: iPID,\n\t\t\t\tAmountIn: decimal.NewFromFloat(item.AmountIn),\n\t\t\t\tAmountOut: decimal.NewFromFloat(item.AmountOut),\n\t\t\t})\n\t\t}\n\n\t\tallrecipes = append(allrecipes, recipes.Recipe{\n\t\t\tID: rID,\n\t\t\tProductID: pID,\n\t\t\tDateFrom: df,\n\t\t\tDateTo: dt,\n\t\t\tItems: items,\n\t\t})\n\t}\n\n\treturn allrecipes, nil\n}\n\nfunc (c *Client) FetchInvoices(from, to time.Time) ([]invoices.Invoice, error) {\n\tparams := map[string]string{\n\t\t\"from\": from.Format(\"2006-01-02\"),\n\t\t\"to\": to.Format(\"2006-01-02\"),\n\t\t\"currentYear\": \"false\",\n\t}\n\n\tresp, err := c.doRequest(\"GET\", \"/resto/api/documents/export/incomingInvoice\", params)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get invoices error: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar xmlData IncomingInvoiceListXML\n\tif err := xml.NewDecoder(resp.Body).Decode(&xmlData); err != nil {\n\t\treturn nil, fmt.Errorf(\"xml decode invoices error: %w\", err)\n\t}\n\n\tvar allinvoices []invoices.Invoice\n\tfor _, doc := range xmlData.Documents {\n\t\tdocID, _ := uuid.Parse(doc.ID)\n\t\tsupID, _ := uuid.Parse(doc.Supplier)\n\t\tstoreID, _ := uuid.Parse(doc.DefaultStore)\n\t\tdateInc, _ := time.Parse(\"2006-01-02T15:04:05\", doc.DateIncoming)\n\n\t\tvar items []invoices.InvoiceItem\n\t\tfor _, it := range doc.Items {\n\t\t\tpID, _ := uuid.Parse(it.Product)\n\t\t\titems = append(items, invoices.InvoiceItem{\n\t\t\t\tInvoiceID: docID,\n\t\t\t\tProductID: pID,\n\t\t\t\tAmount: decimal.NewFromFloat(it.Amount),\n\t\t\t\tPrice: decimal.NewFromFloat(it.Price),\n\t\t\t\tSum: decimal.NewFromFloat(it.Sum),\n\t\t\t\tVatSum: decimal.NewFromFloat(it.VatSum),\n\t\t\t})\n\t\t}\n\n\t\tallinvoices = append(allinvoices, invoices.Invoice{\n\t\t\tID: docID,\n\t\t\tDocumentNumber: doc.DocumentNumber,\n\t\t\tDateIncoming: dateInc,\n\t\t\tSupplierID: supID,\n\t\t\tDefaultStoreID: storeID,\n\t\t\tStatus: doc.Status,\n\t\t\tItems: items,\n\t\t})\n\t}\n\n\treturn allinvoices, nil\n}\n\n// FetchStoreOperations загружает складской отчет по ID пресета\nfunc (c *Client) FetchStoreOperations(presetID string, from, to time.Time) ([]StoreReportItemXML, error) {\n\tparams := map[string]string{\n\t\t\"presetId\": presetID,\n\t\t\"dateFrom\": from.Format(\"02.01.2006\"), // В документации формат DD.MM.YYYY\n\t\t\"dateTo\": to.Format(\"02.01.2006\"),\n\t}\n\n\tresp, err := c.doRequest(\"GET\", \"/resto/api/reports/storeOperations\", params)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"fetch store operations error: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar report StoreReportResponse\n\tif err := xml.NewDecoder(resp.Body).Decode(&report); err != nil {\n\t\t// Иногда RMS возвращает пустой ответ или ошибку текстом при отсутствии данных\n\t\treturn nil, fmt.Errorf(\"xml decode store operations error: %w\", err)\n\t}\n\n\treturn report.Items, nil\n}\n", - "internal/infrastructure/rms/dto.go": "package rms\n\nimport (\n\t\"encoding/xml\"\n)\n\n// --- JSON DTOs (V2 API) ---\n\ntype ProductDTO struct {\n\tID string `json:\"id\"`\n\tParentID *string `json:\"parent\"` // Может быть null\n\tName string `json:\"name\"`\n\tNum string `json:\"num\"` // Артикул\n\tCode string `json:\"code\"` // Код быстрого набора\n\tType string `json:\"type\"` // GOODS, DISH, PREPARED, etc.\n\tUnitWeight float64 `json:\"unitWeight\"`\n\tUnitCapacity float64 `json:\"unitCapacity\"`\n\tDeleted bool `json:\"deleted\"`\n}\n\ntype GroupDTO struct {\n\tID string `json:\"id\"`\n\tParentID *string `json:\"parent\"`\n\tName string `json:\"name\"`\n\tNum string `json:\"num\"`\n\tCode string `json:\"code\"`\n\tDescription string `json:\"description\"`\n\tDeleted bool `json:\"deleted\"`\n}\n\ntype AssemblyChartsResponse struct {\n\tAssemblyCharts []AssemblyChartDTO `json:\"assemblyCharts\"`\n\t// preparedCharts и другие поля пока опускаем, если не нужны для базового импорта\n}\n\ntype AssemblyChartDTO struct {\n\tID string `json:\"id\"`\n\tAssembledProductID string `json:\"assembledProductId\"`\n\tDateFrom string `json:\"dateFrom\"` // Format: \"2018-01-29\" (yyyy-MM-dd)\n\tDateTo *string `json:\"dateTo\"` // Nullable\n\tItems []AssemblyItemDTO `json:\"items\"`\n}\n\ntype AssemblyItemDTO struct {\n\tID string `json:\"id\"` // Добавили поле ID строки техкарты\n\tProductID string `json:\"productId\"`\n\tAmountIn float64 `json:\"amountIn\"`\n\tAmountOut float64 `json:\"amountOut\"`\n}\n\n// --- XML DTOs (Legacy API) ---\n\ntype IncomingInvoiceListXML struct {\n\tXMLName xml.Name `xml:\"incomingInvoiceDtoes\"`\n\tDocuments []IncomingInvoiceXML `xml:\"document\"`\n}\n\ntype IncomingInvoiceXML struct {\n\tID string `xml:\"id\"`\n\tDocumentNumber string `xml:\"documentNumber\"`\n\tDateIncoming string `xml:\"dateIncoming\"` // Format: yyyy-MM-ddTHH:mm:ss\n\tStatus string `xml:\"status\"` // PROCESSED, NEW, DELETED\n\tSupplier string `xml:\"supplier\"` // GUID\n\tDefaultStore string `xml:\"defaultStore\"` // GUID\n\tItems []InvoiceItemXML `xml:\"items>item\"`\n}\n\ntype InvoiceItemXML struct {\n\tProduct string `xml:\"product\"` // GUID\n\tAmount float64 `xml:\"amount\"` // Количество в основных единицах\n\tPrice float64 `xml:\"price\"` // Цена за единицу\n\tSum float64 `xml:\"sum\"` // Сумма без скидки (обычно)\n\tVatSum float64 `xml:\"vatSum\"` // Сумма НДС\n}\n\n// --- XML DTOs (Store Reports) ---\n\ntype StoreReportResponse struct {\n\tXMLName xml.Name `xml:\"storeReportItemDtoes\"`\n\tItems []StoreReportItemXML `xml:\"storeReportItemDto\"`\n}\n\ntype StoreReportItemXML struct {\n\t// Основные идентификаторы\n\tProductID string `xml:\"product\"` // GUID товара\n\tProductGroup string `xml:\"productGroup\"` // GUID группы\n\tStore string `xml:\"primaryStore\"` // GUID склада\n\tDocumentID string `xml:\"documentId\"` // GUID документа\n\tDocumentNum string `xml:\"documentNum\"` // Номер документа (строка)\n\n\t// Типы (ENUMs)\n\tDocumentType string `xml:\"documentType\"` // Например: INCOMING_INVOICE\n\tTransactionType string `xml:\"type\"` // Например: INVOICE, WRITEOFF\n\n\t// Финансы и количество\n\tAmount float64 `xml:\"amount\"` // Количество\n\tSum float64 `xml:\"sum\"` // Сумма с НДС\n\tSumWithoutNds float64 `xml:\"sumWithoutNds\"` // Сумма без НДС\n\tCost float64 `xml:\"cost\"` // Себестоимость\n\n\t// Флаги и даты (используем строки для дат, так как парсинг делаем в сервисе)\n\tIncoming bool `xml:\"incoming\"`\n\tDate string `xml:\"date\"`\n\tOperationalDate string `xml:\"operationalDate\"`\n}\n", + "internal/infrastructure/rms/client.go": "package rms\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha1\"\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/shopspring/decimal\"\n\t\"go.uber.org/zap\"\n\n\t\"rmser/internal/domain/catalog\"\n\t\"rmser/internal/domain/invoices\"\n\t\"rmser/internal/domain/recipes\"\n\t\"rmser/pkg/logger\"\n)\n\nconst (\n\ttokenTTL = 45 * time.Minute // Время жизни токена до принудительного обновления\n)\n\n// ClientI интерфейс\ntype ClientI interface {\n\tAuth() error\n\tLogout() error\n\tFetchCatalog() ([]catalog.Product, error)\n\tFetchRecipes(dateFrom, dateTo time.Time) ([]recipes.Recipe, error)\n\tFetchInvoices(from, to time.Time) ([]invoices.Invoice, error)\n\tFetchStoreOperations(presetID string, from, to time.Time) ([]StoreReportItemXML, error)\n\tCreateIncomingInvoice(inv invoices.Invoice) (string, error)\n}\n\ntype Client struct {\n\tbaseURL string\n\tlogin string\n\tpasswordHash string\n\thttpClient *http.Client\n\n\t// Защита токена для конкурентного доступа\n\tmu sync.RWMutex\n\ttoken string\n\ttokenCreatedAt time.Time\n}\n\nfunc NewClient(baseURL, login, password string) *Client {\n\th := sha1.New()\n\th.Write([]byte(password))\n\tpassHash := fmt.Sprintf(\"%x\", h.Sum(nil))\n\n\treturn &Client{\n\t\tbaseURL: baseURL,\n\t\tlogin: login,\n\t\tpasswordHash: passHash,\n\t\thttpClient: &http.Client{Timeout: 60 * time.Second},\n\t}\n}\n\n// Auth выполняет вход\nfunc (c *Client) Auth() error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\treturn c.authUnsafe()\n}\n\n// authUnsafe - внутренняя логика авторизации без блокировок (вызывается внутри Lock)\nfunc (c *Client) authUnsafe() error {\n\tendpoint := c.baseURL + \"/resto/api/auth\"\n\n\tdata := url.Values{}\n\tdata.Set(\"login\", c.login)\n\tdata.Set(\"pass\", c.passwordHash)\n\n\treq, err := http.NewRequest(\"POST\", endpoint, strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ошибка создания запроса auth: %w\", err)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ошибка сети auth: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"ошибка авторизации (code %d): %s\", resp.StatusCode, string(body))\n\t}\n\n\tc.token = string(body)\n\tc.tokenCreatedAt = time.Now() // Запоминаем время получения\n\tlogger.Log.Info(\"RMS: Успешная авторизация\", zap.String(\"token_preview\", c.token[:5]+\"...\"))\n\treturn nil\n}\n\n// Logout освобождает лицензию\nfunc (c *Client) Logout() error {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\n\treturn c.logoutUnsafe()\n}\n\n// logoutUnsafe - внутренняя логика логаута\nfunc (c *Client) logoutUnsafe() error {\n\tif c.token == \"\" {\n\t\treturn nil\n\t}\n\n\tendpoint := c.baseURL + \"/resto/api/logout\"\n\tdata := url.Values{}\n\tdata.Set(\"key\", c.token)\n\n\treq, err := http.NewRequest(\"POST\", endpoint, strings.NewReader(data.Encode()))\n\tif err == nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t\tresp, err := c.httpClient.Do(req)\n\t\tif err == nil {\n\t\t\tdefer resp.Body.Close()\n\t\t\tif resp.StatusCode == http.StatusOK {\n\t\t\t\tlogger.Log.Info(\"RMS: Токен освобожден\")\n\t\t\t} else {\n\t\t\t\tlogger.Log.Warn(\"RMS: Ошибка освобождения токена\", zap.Int(\"code\", resp.StatusCode))\n\t\t\t}\n\t\t}\n\t}\n\n\t// Сбрасываем токен в любом случае, даже если запрос не прошел (он все равно протухнет)\n\tc.token = \"\"\n\tc.tokenCreatedAt = time.Time{}\n\treturn nil\n}\n\n// ensureToken проверяет срок жизни токена и обновляет его при необходимости\nfunc (c *Client) ensureToken() error {\n\tc.mu.RLock()\n\ttoken := c.token\n\tcreatedAt := c.tokenCreatedAt\n\tc.mu.RUnlock()\n\n\t// Если токена нет или он протух\n\tif token == \"\" || time.Since(createdAt) > tokenTTL {\n\t\tc.mu.Lock()\n\t\tdefer c.mu.Unlock()\n\n\t\t// Double check locking (вдруг другая горутина уже обновила)\n\t\tif c.token != \"\" && time.Since(c.tokenCreatedAt) <= tokenTTL {\n\t\t\treturn nil\n\t\t}\n\n\t\tif c.token != \"\" {\n\t\t\tlogger.Log.Info(\"RMS: Время жизни токена истекло (>45 мин), пересоздание...\")\n\t\t\t_ = c.logoutUnsafe() // Пытаемся освободить старый\n\t\t}\n\n\t\treturn c.authUnsafe()\n\t}\n\treturn nil\n}\n\n// doRequest выполняет запрос с автоматическим управлением токеном\nfunc (c *Client) doRequest(method, path string, queryParams map[string]string) (*http.Response, error) {\n\t// 1. Проверка времени жизни (45 минут)\n\tif err := c.ensureToken(); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Читаем токен под RLock\n\tc.mu.RLock()\n\tcurrentToken := c.token\n\tc.mu.RUnlock()\n\n\tbuildURL := func() string {\n\t\tu, _ := url.Parse(c.baseURL + path)\n\t\tq := u.Query()\n\t\tq.Set(\"key\", currentToken)\n\t\tfor k, v := range queryParams {\n\t\t\tq.Set(k, v)\n\t\t}\n\t\tu.RawQuery = q.Encode()\n\t\treturn u.String()\n\t}\n\n\treq, _ := http.NewRequest(method, buildURL(), nil)\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 2. Реактивная обработка 401 (если сервер перезагрузился или убил сессию раньше времени)\n\tif resp.StatusCode == http.StatusUnauthorized {\n\t\tresp.Body.Close()\n\t\tlogger.Log.Warn(\"RMS: Получен 401 Unauthorized, принудительная ре-авторизация...\")\n\n\t\tc.mu.Lock()\n\t\t// Сбрасываем токен и логинимся заново\n\t\tc.token = \"\"\n\t\tauthErr := c.authUnsafe()\n\t\tc.mu.Unlock()\n\n\t\tif authErr != nil {\n\t\t\treturn nil, authErr\n\t\t}\n\n\t\t// Повторяем запрос с новым токеном\n\t\tc.mu.RLock()\n\t\tcurrentToken = c.token\n\t\tc.mu.RUnlock()\n\n\t\treq, _ = http.NewRequest(method, buildURL(), nil)\n\t\treturn c.httpClient.Do(req)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tdefer resp.Body.Close()\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn nil, fmt.Errorf(\"api error: code=%d, body=%s\", resp.StatusCode, string(body))\n\t}\n\n\treturn resp, nil\n}\n\n// --- Методы получения данных (без изменений логики парсинга) ---\n\nfunc (c *Client) FetchCatalog() ([]catalog.Product, error) {\n\tvar products []catalog.Product\n\n\t// Группы\n\trespGroups, err := c.doRequest(\"GET\", \"/resto/api/v2/entities/products/group/list\", map[string]string{\n\t\t\"includeDeleted\": \"true\",\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get groups error: %w\", err)\n\t}\n\tdefer respGroups.Body.Close()\n\n\tvar groupDTOs []GroupDTO\n\tif err := json.NewDecoder(respGroups.Body).Decode(&groupDTOs); err != nil {\n\t\treturn nil, fmt.Errorf(\"json decode groups error: %w\", err)\n\t}\n\n\t// Товары\n\trespProds, err := c.doRequest(\"GET\", \"/resto/api/v2/entities/products/list\", map[string]string{\n\t\t\"includeDeleted\": \"true\",\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get products error: %w\", err)\n\t}\n\tdefer respProds.Body.Close()\n\n\tvar prodDTOs []ProductDTO\n\tif err := json.NewDecoder(respProds.Body).Decode(&prodDTOs); err != nil {\n\t\treturn nil, fmt.Errorf(\"json decode products error: %w\", err)\n\t}\n\n\t// Маппинг групп\n\tfor _, g := range groupDTOs {\n\t\tid, _ := uuid.Parse(g.ID)\n\t\tvar parentID *uuid.UUID\n\t\tif g.ParentID != nil {\n\t\t\tif pid, err := uuid.Parse(*g.ParentID); err == nil {\n\t\t\t\tparentID = &pid\n\t\t\t}\n\t\t}\n\t\tproducts = append(products, catalog.Product{\n\t\t\tID: id,\n\t\t\tParentID: parentID,\n\t\t\tName: g.Name,\n\t\t\tNum: g.Num,\n\t\t\tCode: g.Code,\n\t\t\tType: \"GROUP\",\n\t\t\tIsDeleted: g.Deleted,\n\t\t})\n\t}\n\n\t// Маппинг товаров\n\tfor _, p := range prodDTOs {\n\t\tid, _ := uuid.Parse(p.ID)\n\t\tvar parentID *uuid.UUID\n\t\tif p.ParentID != nil {\n\t\t\tif pid, err := uuid.Parse(*p.ParentID); err == nil {\n\t\t\t\tparentID = &pid\n\t\t\t}\n\t\t}\n\t\tproducts = append(products, catalog.Product{\n\t\t\tID: id,\n\t\t\tParentID: parentID,\n\t\t\tName: p.Name,\n\t\t\tNum: p.Num,\n\t\t\tCode: p.Code,\n\t\t\tType: p.Type,\n\t\t\tUnitWeight: decimal.NewFromFloat(p.UnitWeight),\n\t\t\tUnitCapacity: decimal.NewFromFloat(p.UnitCapacity),\n\t\t\tIsDeleted: p.Deleted,\n\t\t})\n\t}\n\n\treturn products, nil\n}\n\nfunc (c *Client) FetchRecipes(dateFrom, dateTo time.Time) ([]recipes.Recipe, error) {\n\tparams := map[string]string{\n\t\t\"dateFrom\": dateFrom.Format(\"2006-01-02\"),\n\t}\n\tif !dateTo.IsZero() {\n\t\tparams[\"dateTo\"] = dateTo.Format(\"2006-01-02\")\n\t}\n\n\tresp, err := c.doRequest(\"GET\", \"/resto/api/v2/assemblyCharts/getAll\", params)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get recipes error: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar apiResp AssemblyChartsResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"json decode recipes error: %w\", err)\n\t}\n\n\tvar allrecipes []recipes.Recipe\n\tfor _, chart := range apiResp.AssemblyCharts {\n\t\trID, _ := uuid.Parse(chart.ID)\n\t\tpID, _ := uuid.Parse(chart.AssembledProductID)\n\n\t\tdf, _ := time.Parse(\"2006-01-02\", chart.DateFrom)\n\t\tvar dt *time.Time\n\t\tif chart.DateTo != nil {\n\t\t\tif t, err := time.Parse(\"2006-01-02\", *chart.DateTo); err == nil {\n\t\t\t\tdt = &t\n\t\t\t}\n\t\t}\n\n\t\tvar items []recipes.RecipeItem\n\t\tfor _, item := range chart.Items {\n\t\t\tiPID, _ := uuid.Parse(item.ProductID)\n\n\t\t\t// FIX: Генерируем уникальный ID для каждой строки в нашей БД,\n\t\t\t// чтобы избежать конфликтов PK при переиспользовании строк в iiko.\n\t\t\titems = append(items, recipes.RecipeItem{\n\t\t\t\tID: uuid.New(),\n\t\t\t\tRecipeID: rID,\n\t\t\t\tProductID: iPID,\n\t\t\t\tAmountIn: decimal.NewFromFloat(item.AmountIn),\n\t\t\t\tAmountOut: decimal.NewFromFloat(item.AmountOut),\n\t\t\t})\n\t\t}\n\n\t\tallrecipes = append(allrecipes, recipes.Recipe{\n\t\t\tID: rID,\n\t\t\tProductID: pID,\n\t\t\tDateFrom: df,\n\t\t\tDateTo: dt,\n\t\t\tItems: items,\n\t\t})\n\t}\n\n\treturn allrecipes, nil\n}\n\nfunc (c *Client) FetchInvoices(from, to time.Time) ([]invoices.Invoice, error) {\n\tparams := map[string]string{\n\t\t\"from\": from.Format(\"2006-01-02\"),\n\t\t\"to\": to.Format(\"2006-01-02\"),\n\t\t\"currentYear\": \"false\",\n\t}\n\n\tresp, err := c.doRequest(\"GET\", \"/resto/api/documents/export/incomingInvoice\", params)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get invoices error: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar xmlData IncomingInvoiceListXML\n\tif err := xml.NewDecoder(resp.Body).Decode(&xmlData); err != nil {\n\t\treturn nil, fmt.Errorf(\"xml decode invoices error: %w\", err)\n\t}\n\n\tvar allinvoices []invoices.Invoice\n\tfor _, doc := range xmlData.Documents {\n\t\tdocID, _ := uuid.Parse(doc.ID)\n\t\tsupID, _ := uuid.Parse(doc.Supplier)\n\t\tstoreID, _ := uuid.Parse(doc.DefaultStore)\n\t\tdateInc, _ := time.Parse(\"2006-01-02T15:04:05\", doc.DateIncoming)\n\n\t\tvar items []invoices.InvoiceItem\n\t\tfor _, it := range doc.Items {\n\t\t\tpID, _ := uuid.Parse(it.Product)\n\t\t\titems = append(items, invoices.InvoiceItem{\n\t\t\t\tInvoiceID: docID,\n\t\t\t\tProductID: pID,\n\t\t\t\tAmount: decimal.NewFromFloat(it.Amount),\n\t\t\t\tPrice: decimal.NewFromFloat(it.Price),\n\t\t\t\tSum: decimal.NewFromFloat(it.Sum),\n\t\t\t\tVatSum: decimal.NewFromFloat(it.VatSum),\n\t\t\t})\n\t\t}\n\n\t\tallinvoices = append(allinvoices, invoices.Invoice{\n\t\t\tID: docID,\n\t\t\tDocumentNumber: doc.DocumentNumber,\n\t\t\tDateIncoming: dateInc,\n\t\t\tSupplierID: supID,\n\t\t\tDefaultStoreID: storeID,\n\t\t\tStatus: doc.Status,\n\t\t\tItems: items,\n\t\t})\n\t}\n\n\treturn allinvoices, nil\n}\n\n// FetchStoreOperations загружает складской отчет по ID пресета\nfunc (c *Client) FetchStoreOperations(presetID string, from, to time.Time) ([]StoreReportItemXML, error) {\n\tparams := map[string]string{\n\t\t\"presetId\": presetID,\n\t\t\"dateFrom\": from.Format(\"02.01.2006\"), // В документации формат DD.MM.YYYY\n\t\t\"dateTo\": to.Format(\"02.01.2006\"),\n\t}\n\n\tresp, err := c.doRequest(\"GET\", \"/resto/api/reports/storeOperations\", params)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"fetch store operations error: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar report StoreReportResponse\n\tif err := xml.NewDecoder(resp.Body).Decode(&report); err != nil {\n\t\t// Иногда RMS возвращает пустой ответ или ошибку текстом при отсутствии данных\n\t\treturn nil, fmt.Errorf(\"xml decode store operations error: %w\", err)\n\t}\n\n\treturn report.Items, nil\n}\n\n// CreateIncomingInvoice отправляет накладную в iiko\nfunc (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) {\n\t// 1. Маппинг Domain -> XML DTO\n\treqDTO := IncomingInvoiceImportXML{\n\t\tDocumentNumber: inv.DocumentNumber,\n\t\tDateIncoming: inv.DateIncoming.Format(\"02.01.2006\"),\n\t\tDefaultStore: inv.DefaultStoreID.String(),\n\t\tSupplier: inv.SupplierID.String(),\n\t\tStatus: \"NEW\",\n\t\tComment: \"Loaded via RMSER OCR\",\n\t}\n\n\tif inv.ID != uuid.Nil {\n\t\treqDTO.ID = inv.ID.String()\n\t}\n\n\tfor i, item := range inv.Items {\n\t\tamount, _ := item.Amount.Float64()\n\t\tprice, _ := item.Price.Float64()\n\t\tsum, _ := item.Sum.Float64()\n\n\t\treqDTO.ItemsWrapper.Items = append(reqDTO.ItemsWrapper.Items, IncomingInvoiceImportItemXML{\n\t\t\tProductID: item.ProductID.String(),\n\t\t\tAmount: amount,\n\t\t\tPrice: price,\n\t\t\tSum: sum,\n\t\t\tNum: i + 1,\n\t\t\tStore: inv.DefaultStoreID.String(),\n\t\t})\n\t}\n\n\t// 2. Маршалинг в XML\n\txmlBytes, err := xml.Marshal(reqDTO)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"xml marshal error: %w\", err)\n\t}\n\t// Добавляем XML header вручную\n\txmlPayload := []byte(xml.Header + string(xmlBytes))\n\n\t// 3. Получение токена\n\tif err := c.ensureToken(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tc.mu.RLock()\n\ttoken := c.token\n\tc.mu.RUnlock()\n\n\t// 4. Формирование URL\n\tendpoint, _ := url.Parse(c.baseURL + \"/resto/api/documents/import/incomingInvoice\")\n\tq := endpoint.Query()\n\tq.Set(\"key\", token)\n\tendpoint.RawQuery = q.Encode()\n\n\tfullURL := endpoint.String()\n\n\t// --- ЛОГИРОВАНИЕ ЗАПРОСА (URL + BODY) ---\n\t// Логируем как Info, чтобы точно увидеть в консоли при отладке\n\tlogger.Log.Info(\"RMS POST Request Debug\",\n\t\tzap.String(\"method\", \"POST\"),\n\t\tzap.String(\"url\", fullURL),\n\t\tzap.String(\"body_payload\", string(xmlPayload)),\n\t)\n\t// ----------------------------------------\n\n\t// 5. Отправка\n\treq, err := http.NewRequest(\"POST\", fullURL, bytes.NewReader(xmlPayload))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/xml\")\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"network error: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\t// Читаем ответ\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Логируем ответ для симметрии\n\tlogger.Log.Info(\"RMS POST Response Debug\",\n\t\tzap.Int(\"status_code\", resp.StatusCode),\n\t\tzap.String(\"response_body\", string(respBody)),\n\t)\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"http error %d: %s\", resp.StatusCode, string(respBody))\n\t}\n\n\tvar result DocumentValidationResult\n\tif err := xml.Unmarshal(respBody, &result); err != nil {\n\t\treturn \"\", fmt.Errorf(\"xml response unmarshal error: %w\", err)\n\t}\n\n\tif !result.Valid {\n\t\tlogger.Log.Warn(\"RMS Invoice Import Failed\",\n\t\t\tzap.String(\"error\", result.ErrorMessage),\n\t\t\tzap.String(\"additional\", result.AdditionalInfo),\n\t\t)\n\t\treturn \"\", fmt.Errorf(\"iiko validation failed: %s (info: %s)\", result.ErrorMessage, result.AdditionalInfo)\n\t}\n\n\treturn result.DocumentNumber, nil\n}\n", + "internal/infrastructure/rms/dto.go": "package rms\n\nimport (\n\t\"encoding/xml\"\n)\n\n// --- JSON DTOs (V2 API) ---\n\ntype ProductDTO struct {\n\tID string `json:\"id\"`\n\tParentID *string `json:\"parent\"` // Может быть null\n\tName string `json:\"name\"`\n\tNum string `json:\"num\"` // Артикул\n\tCode string `json:\"code\"` // Код быстрого набора\n\tType string `json:\"type\"` // GOODS, DISH, PREPARED, etc.\n\tUnitWeight float64 `json:\"unitWeight\"`\n\tUnitCapacity float64 `json:\"unitCapacity\"`\n\tDeleted bool `json:\"deleted\"`\n}\n\ntype GroupDTO struct {\n\tID string `json:\"id\"`\n\tParentID *string `json:\"parent\"`\n\tName string `json:\"name\"`\n\tNum string `json:\"num\"`\n\tCode string `json:\"code\"`\n\tDescription string `json:\"description\"`\n\tDeleted bool `json:\"deleted\"`\n}\n\ntype AssemblyChartsResponse struct {\n\tAssemblyCharts []AssemblyChartDTO `json:\"assemblyCharts\"`\n\t// preparedCharts и другие поля пока опускаем, если не нужны для базового импорта\n}\n\ntype AssemblyChartDTO struct {\n\tID string `json:\"id\"`\n\tAssembledProductID string `json:\"assembledProductId\"`\n\tDateFrom string `json:\"dateFrom\"` // Format: \"2018-01-29\" (yyyy-MM-dd)\n\tDateTo *string `json:\"dateTo\"` // Nullable\n\tItems []AssemblyItemDTO `json:\"items\"`\n}\n\ntype AssemblyItemDTO struct {\n\tID string `json:\"id\"` // Добавили поле ID строки техкарты\n\tProductID string `json:\"productId\"`\n\tAmountIn float64 `json:\"amountIn\"`\n\tAmountOut float64 `json:\"amountOut\"`\n}\n\n// --- XML DTOs (Legacy API) ---\n\ntype IncomingInvoiceListXML struct {\n\tXMLName xml.Name `xml:\"incomingInvoiceDtoes\"`\n\tDocuments []IncomingInvoiceXML `xml:\"document\"`\n}\n\ntype IncomingInvoiceXML struct {\n\tID string `xml:\"id\"`\n\tDocumentNumber string `xml:\"documentNumber\"`\n\tDateIncoming string `xml:\"dateIncoming\"` // Format: yyyy-MM-ddTHH:mm:ss\n\tStatus string `xml:\"status\"` // PROCESSED, NEW, DELETED\n\tSupplier string `xml:\"supplier\"` // GUID\n\tDefaultStore string `xml:\"defaultStore\"` // GUID\n\tItems []InvoiceItemXML `xml:\"items>item\"`\n}\n\ntype InvoiceItemXML struct {\n\tProduct string `xml:\"product\"` // GUID\n\tAmount float64 `xml:\"amount\"` // Количество в основных единицах\n\tPrice float64 `xml:\"price\"` // Цена за единицу\n\tSum float64 `xml:\"sum\"` // Сумма без скидки (обычно)\n\tVatSum float64 `xml:\"vatSum\"` // Сумма НДС\n}\n\n// --- XML DTOs (Store Reports) ---\n\ntype StoreReportResponse struct {\n\tXMLName xml.Name `xml:\"storeReportItemDtoes\"`\n\tItems []StoreReportItemXML `xml:\"storeReportItemDto\"`\n}\n\ntype StoreReportItemXML struct {\n\t// Основные идентификаторы\n\tProductID string `xml:\"product\"` // GUID товара\n\tProductGroup string `xml:\"productGroup\"` // GUID группы\n\tStore string `xml:\"primaryStore\"` // GUID склада\n\tDocumentID string `xml:\"documentId\"` // GUID документа\n\tDocumentNum string `xml:\"documentNum\"` // Номер документа (строка)\n\n\t// Типы (ENUMs)\n\tDocumentType string `xml:\"documentType\"` // Например: INCOMING_INVOICE\n\tTransactionType string `xml:\"type\"` // Например: INVOICE, WRITEOFF\n\n\t// Финансы и количество\n\tAmount float64 `xml:\"amount\"` // Количество\n\tSum float64 `xml:\"sum\"` // Сумма с НДС\n\tSumWithoutNds float64 `xml:\"sumWithoutNds\"` // Сумма без НДС\n\tCost float64 `xml:\"cost\"` // Себестоимость\n\n\t// Флаги и даты (используем строки для дат, так как парсинг делаем в сервисе)\n\tIncoming bool `xml:\"incoming\"`\n\tDate string `xml:\"date\"`\n\tOperationalDate string `xml:\"operationalDate\"`\n}\n\n// --- XML DTOs (Import API) ---\n\n// IncomingInvoiceImportXML описывает структуру для POST запроса импорта\ntype IncomingInvoiceImportXML struct {\n\tXMLName xml.Name `xml:\"document\"`\n\tID string `xml:\"id,omitempty\"` // GUID, если редактируем\n\tDocumentNumber string `xml:\"documentNumber,omitempty\"`\n\tDateIncoming string `xml:\"dateIncoming,omitempty\"` // Format: dd.MM.yyyy\n\tInvoice string `xml:\"invoice,omitempty\"` // Номер счет-фактуры\n\tDefaultStore string `xml:\"defaultStore\"` // GUID склада (обязательно)\n\tSupplier string `xml:\"supplier\"` // GUID поставщика (обязательно)\n\tComment string `xml:\"comment,omitempty\"`\n\tStatus string `xml:\"status,omitempty\"` // NEW, PROCESSED\n\tItemsWrapper struct {\n\t\tItems []IncomingInvoiceImportItemXML `xml:\"item\"`\n\t} `xml:\"items\"`\n}\n\ntype IncomingInvoiceImportItemXML struct {\n\tProductID string `xml:\"product\"` // GUID товара\n\tAmount float64 `xml:\"amount\"` // Кол-во в базовых единицах\n\tPrice float64 `xml:\"price\"` // Цена за единицу\n\tSum float64 `xml:\"sum,omitempty\"`\n\tStore string `xml:\"store\"` // GUID склада\n\t// Поля ниже можно опустить, если iiko должна сама подтянуть их из карточки товара\n\t// или если мы работаем в базовых единицах.\n\tAmountUnit string `xml:\"amountUnit,omitempty\"` // GUID единицы измерения\n\tNum int `xml:\"num,omitempty\"` // Номер строки\n}\n\n// DocumentValidationResult описывает ответ сервера при импорте\ntype DocumentValidationResult struct {\n\tXMLName xml.Name `xml:\"documentValidationResult\"`\n\tValid bool `xml:\"valid\"`\n\tWarning bool `xml:\"warning\"`\n\tDocumentNumber string `xml:\"documentNumber\"`\n\tOtherSuggestedNumber string `xml:\"otherSuggestedNumber\"`\n\tErrorMessage string `xml:\"errorMessage\"`\n\tAdditionalInfo string `xml:\"additionalInfo\"`\n}\n", + "internal/services/invoices/service.go": "package invoices\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/shopspring/decimal\"\n\t\"go.uber.org/zap\"\n\n\t\"rmser/internal/domain/invoices\"\n\t\"rmser/internal/infrastructure/rms\"\n\t\"rmser/pkg/logger\"\n)\n\ntype Service struct {\n\trmsClient rms.ClientI\n\t// Здесь можно добавить репозитории каталога и контрагентов для валидации,\n\t// но для краткости пока опустим глубокую валидацию.\n}\n\nfunc NewService(rmsClient rms.ClientI) *Service {\n\treturn &Service{\n\t\trmsClient: rmsClient,\n\t}\n}\n\n// CreateRequestDTO - структура входящего JSON запроса от фронта/OCR\ntype CreateRequestDTO struct {\n\tDocumentNumber string `json:\"document_number\"`\n\tDateIncoming string `json:\"date_incoming\"` // YYYY-MM-DD\n\tSupplierID uuid.UUID `json:\"supplier_id\"`\n\tStoreID uuid.UUID `json:\"store_id\"`\n\tItems []struct {\n\t\tProductID uuid.UUID `json:\"product_id\"`\n\t\tAmount decimal.Decimal `json:\"amount\"`\n\t\tPrice decimal.Decimal `json:\"price\"`\n\t} `json:\"items\"`\n}\n\n// SendInvoiceToRMS валидирует DTO, собирает доменную модель и отправляет в RMS\nfunc (s *Service) SendInvoiceToRMS(req CreateRequestDTO) (string, error) {\n\t// 1. Базовая валидация\n\tif len(req.Items) == 0 {\n\t\treturn \"\", fmt.Errorf(\"список товаров пуст\")\n\t}\n\n\tdateInc, err := time.Parse(\"2006-01-02\", req.DateIncoming)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"неверный формат даты (ожидается YYYY-MM-DD): %v\", err)\n\t}\n\n\t// 2. Сборка доменной модели\n\tinv := invoices.Invoice{\n\t\tID: uuid.Nil, // Новый документ\n\t\tDocumentNumber: req.DocumentNumber,\n\t\tDateIncoming: dateInc,\n\t\tSupplierID: req.SupplierID,\n\t\tDefaultStoreID: req.StoreID,\n\t\tStatus: \"NEW\",\n\t\tItems: make([]invoices.InvoiceItem, 0, len(req.Items)),\n\t}\n\n\tfor _, itemDTO := range req.Items {\n\t\tsum := itemDTO.Amount.Mul(itemDTO.Price) // Пересчитываем сумму\n\n\t\tinv.Items = append(inv.Items, invoices.InvoiceItem{\n\t\t\tProductID: itemDTO.ProductID,\n\t\t\tAmount: itemDTO.Amount,\n\t\t\tPrice: itemDTO.Price,\n\t\t\tSum: sum,\n\t\t})\n\t}\n\n\t// 3. Отправка через клиент\n\tlogger.Log.Info(\"Отправка накладной в RMS\",\n\t\tzap.String(\"supplier\", req.SupplierID.String()),\n\t\tzap.Int(\"items_count\", len(inv.Items)))\n\n\tdocNum, err := s.rmsClient.CreateIncomingInvoice(inv)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn docNum, nil\n}\n", + "internal/services/ocr/service.go": "package ocr\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/shopspring/decimal\"\n\t\"go.uber.org/zap\"\n\n\t\"rmser/internal/domain/catalog\"\n\t\"rmser/internal/domain/ocr\"\n\t\"rmser/internal/infrastructure/ocr_client\"\n\t\"rmser/pkg/logger\"\n)\n\ntype Service struct {\n\tocrRepo ocr.Repository\n\tcatalogRepo catalog.Repository\n\tpyClient *ocr_client.Client // Клиент к Python сервису\n}\n\nfunc NewService(\n\tocrRepo ocr.Repository,\n\tcatalogRepo catalog.Repository,\n\tpyClient *ocr_client.Client,\n) *Service {\n\treturn &Service{\n\t\tocrRepo: ocrRepo,\n\t\tcatalogRepo: catalogRepo,\n\t\tpyClient: pyClient,\n\t}\n}\n\n// ProcessReceiptImage - основной метод: Картинка -> Распознавание -> Матчинг\nfunc (s *Service) ProcessReceiptImage(ctx context.Context, imgData []byte) ([]ProcessedItem, error) {\n\t// 1. Отправляем в Python\n\trawResult, err := s.pyClient.ProcessImage(ctx, imgData, \"receipt.jpg\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"python ocr error: %w\", err)\n\t}\n\n\tvar processed []ProcessedItem\n\n\t// 2. Обрабатываем каждую строку\n\tfor _, rawItem := range rawResult.Items {\n\t\titem := ProcessedItem{\n\t\t\tRawName: rawItem.RawName,\n\t\t\tAmount: decimal.NewFromFloat(rawItem.Amount),\n\t\t\tPrice: decimal.NewFromFloat(rawItem.Price),\n\t\t\tSum: decimal.NewFromFloat(rawItem.Sum),\n\t\t}\n\n\t\t// 3. Ищем соответствие\n\t\t// Сначала проверяем таблицу ручного обучения (product_matches)\n\t\tmatchID, err := s.ocrRepo.FindMatch(rawItem.RawName)\n\t\tif err != nil {\n\t\t\tlogger.Log.Error(\"db error finding match\", zap.Error(err))\n\t\t}\n\n\t\tif matchID != nil {\n\t\t\t// Нашли в обучении\n\t\t\titem.ProductID = matchID\n\t\t\titem.IsMatched = true\n\t\t\titem.MatchSource = \"learned\"\n\t\t} else {\n\t\t\t// Если не нашли, пробуем найти точное совпадение по имени в каталоге (на всякий случай)\n\t\t\t// (В реальном проекте тут может быть нечеткий поиск, но пока точный)\n\t\t}\n\n\t\tprocessed = append(processed, item)\n\t}\n\n\treturn processed, nil\n}\n\n// ProcessedItem - результат обработки одной строки чека\ntype ProcessedItem struct {\n\tRawName string\n\tAmount decimal.Decimal\n\tPrice decimal.Decimal\n\tSum decimal.Decimal\n\n\tIsMatched bool\n\tProductID *uuid.UUID\n\tMatchSource string // \"learned\", \"auto\", \"manual\"\n}\n\n// ProductForIndex DTO для внешнего сервиса\ntype ProductForIndex struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tCode string `json:\"code\"`\n}\n\n// GetCatalogForIndexing возвращает список товаров для построения индекса\nfunc (s *Service) GetCatalogForIndexing() ([]ProductForIndex, error) {\n\tproducts, err := s.catalogRepo.GetActiveGoods()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := make([]ProductForIndex, 0, len(products))\n\tfor _, p := range products {\n\t\tresult = append(result, ProductForIndex{\n\t\t\tID: p.ID.String(),\n\t\t\tName: p.Name,\n\t\t\tCode: p.Code,\n\t\t})\n\t}\n\treturn result, nil\n}\n\n// SaveMapping сохраняет связь \"Текст из чека\" -> \"Наш товар\"\nfunc (s *Service) SaveMapping(rawName string, productID uuid.UUID) error {\n\treturn s.ocrRepo.SaveMatch(rawName, productID)\n}\n\n// GetKnownMatches возвращает список всех обученных связей\nfunc (s *Service) GetKnownMatches() ([]ocr.ProductMatch, error) {\n\treturn s.ocrRepo.GetAllMatches()\n}\n\n// FindKnownMatch ищет, знаем ли мы уже этот товар\nfunc (s *Service) FindKnownMatch(rawName string) (*uuid.UUID, error) {\n\treturn s.ocrRepo.FindMatch(rawName)\n}\n\n// SearchProducts ищет товары в БД по части названия (для ручного выбора в боте)\nfunc (s *Service) SearchProducts(query string) ([]catalog.Product, error) {\n\t// Этот метод нужно поддержать в репозитории, пока сделаем заглушку или фильтрацию в памяти\n\t// Для MVP добавим метод SearchByName в интерфейс репозитория\n\tall, err := s.catalogRepo.GetActiveGoods()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Простейший поиск в памяти (для начала хватит)\n\tquery = strings.ToLower(query)\n\tvar result []catalog.Product\n\tfor _, p := range all {\n\t\tif strings.Contains(strings.ToLower(p.Name), query) {\n\t\t\tresult = append(result, p)\n\t\t\tif len(result) >= 10 { // Ограничим выдачу\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn result, nil\n}\n", "internal/services/recommend/service.go": "package recommend\n\nimport (\n\t\"go.uber.org/zap\"\n\n\t\"rmser/internal/domain/recommendations\"\n\t\"rmser/pkg/logger\"\n)\n\nconst (\n\tAnalyzeDaysNoIncoming = 90 // Ищем ингредиенты без закупок за 30 дней\n\tAnalyzeDaysStale = 90 // Ищем неликвид за 60 дней\n)\n\ntype Service struct {\n\trepo recommendations.Repository\n}\n\nfunc NewService(repo recommendations.Repository) *Service {\n\treturn &Service{repo: repo}\n}\n\n// RefreshRecommendations выполняет анализ и сохраняет результаты в БД\nfunc (s *Service) RefreshRecommendations() error {\n\tlogger.Log.Info(\"Запуск пересчета рекомендаций...\")\n\n\tvar all []recommendations.Recommendation\n\n\t// 1. Unused\n\tif unused, err := s.repo.FindUnusedGoods(); err == nil {\n\t\tall = append(all, unused...)\n\t} else {\n\t\tlogger.Log.Error(\"Ошибка unused\", zap.Error(err))\n\t}\n\n\t// 2. Purchased but Unused\n\tif purchUnused, err := s.repo.FindPurchasedButUnused(AnalyzeDaysNoIncoming); err == nil {\n\t\tall = append(all, purchUnused...)\n\t} else {\n\t\tlogger.Log.Error(\"Ошибка purchased_unused\", zap.Error(err))\n\t}\n\n\t// 3. No Incoming (Ингредиенты без закупок)\n\tif noInc, err := s.repo.FindNoIncomingIngredients(AnalyzeDaysNoIncoming); err == nil {\n\t\tall = append(all, noInc...)\n\t} else {\n\t\tlogger.Log.Error(\"Ошибка no_incoming\", zap.Error(err))\n\t}\n\n\t// 4. Usage without Purchase (Расход без прихода) <-- НОВОЕ\n\tif usageNoPurch, err := s.repo.FindUsageWithoutPurchase(AnalyzeDaysNoIncoming); err == nil {\n\t\tall = append(all, usageNoPurch...)\n\t} else {\n\t\tlogger.Log.Error(\"Ошибка usage_no_purchase\", zap.Error(err))\n\t}\n\n\t// 5. Stale (Неликвид)\n\tif stale, err := s.repo.FindStaleGoods(AnalyzeDaysStale); err == nil {\n\t\tall = append(all, stale...)\n\t} else {\n\t\tlogger.Log.Error(\"Ошибка stale\", zap.Error(err))\n\t}\n\n\t// 6. Dish in Recipe\n\tif dishInRec, err := s.repo.FindDishesInRecipes(); err == nil {\n\t\tall = append(all, dishInRec...)\n\t} else {\n\t\tlogger.Log.Error(\"Ошибка dish_in_recipe\", zap.Error(err))\n\t}\n\n\t// Сохраняем\n\tif err := s.repo.SaveAll(all); err != nil {\n\t\treturn err\n\t}\n\n\tlogger.Log.Info(\"Рекомендации обновлены\", zap.Int(\"total_count\", len(all)))\n\treturn nil\n}\n\nfunc (s *Service) GetRecommendations() ([]recommendations.Recommendation, error) {\n\treturn s.repo.GetAll()\n}\n", "internal/services/sync/service.go": "package sync\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"go.uber.org/zap\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/shopspring/decimal\"\n\n\t\"rmser/internal/domain/catalog\"\n\t\"rmser/internal/domain/invoices\"\n\t\"rmser/internal/domain/operations\"\n\t\"rmser/internal/domain/recipes\"\n\t\"rmser/internal/infrastructure/rms\"\n\t\"rmser/pkg/logger\"\n)\n\nconst (\n\t// Пресеты от пользователя\n\tPresetPurchases = \"1a3297e1-cb05-55dc-98a7-c13f13bc85a7\" // Закупки\n\tPresetUsage = \"24d9402e-2d01-eca1-ebeb-7981f7d1cb86\" // Расход\n)\n\ntype Service struct {\n\trmsClient rms.ClientI\n\tcatalogRepo catalog.Repository\n\trecipeRepo recipes.Repository\n\tinvoiceRepo invoices.Repository\n\topRepo operations.Repository\n}\n\nfunc NewService(\n\trmsClient rms.ClientI,\n\tcatalogRepo catalog.Repository,\n\trecipeRepo recipes.Repository,\n\tinvoiceRepo invoices.Repository,\n\topRepo operations.Repository,\n) *Service {\n\treturn &Service{\n\t\trmsClient: rmsClient,\n\t\tcatalogRepo: catalogRepo,\n\t\trecipeRepo: recipeRepo,\n\t\tinvoiceRepo: invoiceRepo,\n\t\topRepo: opRepo,\n\t}\n}\n\n// SyncCatalog загружает номенклатуру и сохраняет в БД\nfunc (s *Service) SyncCatalog() error {\n\tlogger.Log.Info(\"Начало синхронизации номенклатуры\")\n\n\tproducts, err := s.rmsClient.FetchCatalog()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ошибка получения каталога из RMS: %w\", err)\n\t}\n\n\tif err := s.catalogRepo.SaveProducts(products); err != nil {\n\t\treturn fmt.Errorf(\"ошибка сохранения продуктов в БД: %w\", err)\n\t}\n\n\tlogger.Log.Info(\"Синхронизация номенклатуры завершена\", zap.Int(\"count\", len(products)))\n\treturn nil\n}\n\n// SyncRecipes загружает техкарты за указанный период (или за последние 30 дней по умолчанию)\nfunc (s *Service) SyncRecipes() error {\n\tlogger.Log.Info(\"Начало синхронизации техкарт\")\n\n\t// RMS требует dateFrom. Берем широкий диапазон, например, с начала года или фиксированную дату,\n\t// либо можно сделать конфигурируемым. Для примера берем -3 месяца от текущей даты.\n\t// В реальном проде лучше брать дату последнего изменения, если API поддерживает revision,\n\t// но V2 API iiko часто требует полной перезагрузки актуальных карт.\n\tdateFrom := time.Now().AddDate(0, -3, 0)\n\tdateTo := time.Now() // +1 месяц вперед на случай будущих меню\n\n\trecipes, err := s.rmsClient.FetchRecipes(dateFrom, dateTo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ошибка получения техкарт из RMS: %w\", err)\n\t}\n\n\tif err := s.recipeRepo.SaveRecipes(recipes); err != nil {\n\t\treturn fmt.Errorf(\"ошибка сохранения техкарт в БД: %w\", err)\n\t}\n\n\tlogger.Log.Info(\"Синхронизация техкарт завершена\", zap.Int(\"count\", len(recipes)))\n\treturn nil\n}\n\n// SyncInvoices загружает накладные. Если в базе пусто, грузит за последние N дней.\nfunc (s *Service) SyncInvoices() error {\n\tlogger.Log.Info(\"Начало синхронизации накладных\")\n\n\tlastDate, err := s.invoiceRepo.GetLastInvoiceDate()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ошибка получения даты последней накладной: %w\", err)\n\t}\n\n\tvar from time.Time\n\tto := time.Now()\n\n\tif lastDate != nil {\n\t\t// Берем следующий день после последней загрузки или тот же день, чтобы обновить изменения\n\t\tfrom = *lastDate\n\t} else {\n\t\t// Дефолтная загрузка за 30 дней назад\n\t\tfrom = time.Now().AddDate(0, 0, -30)\n\t}\n\n\tlogger.Log.Info(\"Запрос накладных\", zap.Time(\"from\", from), zap.Time(\"to\", to))\n\n\tinvoices, err := s.rmsClient.FetchInvoices(from, to)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ошибка получения накладных из RMS: %w\", err)\n\t}\n\n\tif len(invoices) == 0 {\n\t\tlogger.Log.Info(\"Новых накладных не найдено\")\n\t\treturn nil\n\t}\n\n\tif err := s.invoiceRepo.SaveInvoices(invoices); err != nil {\n\t\treturn fmt.Errorf(\"ошибка сохранения накладных в БД: %w\", err)\n\t}\n\n\tlogger.Log.Info(\"Синхронизация накладных завершена\", zap.Int(\"count\", len(invoices)))\n\treturn nil\n}\n\n// classifyOperation определяет тип операции на основе DocumentType\nfunc classifyOperation(docType string) operations.OperationType {\n\tswitch docType {\n\t// === ПРИХОД (PURCHASE) ===\n\tcase \"INCOMING_INVOICE\": // Приходная накладная\n\t\treturn operations.OpTypePurchase\n\tcase \"INCOMING_SERVICE\": // Акт приема услуг (редко товары, но бывает)\n\t\treturn operations.OpTypePurchase\n\n\t// === РАСХОД (USAGE) ===\n\tcase \"SALES_DOCUMENT\": // Акт реализации (продажа)\n\t\treturn operations.OpTypeUsage\n\tcase \"WRITEOFF_DOCUMENT\": // Акт списания (порча, проработки)\n\t\treturn operations.OpTypeUsage\n\tcase \"OUTGOING_INVOICE\": // Расходная накладная\n\t\treturn operations.OpTypeUsage\n\tcase \"SESSION_ACCEPTANCE\": // Принятие смены (иногда агрегирует продажи)\n\t\treturn operations.OpTypeUsage\n\tcase \"DISASSEMBLE_DOCUMENT\": // Акт разбора (расход целого)\n\t\treturn operations.OpTypeUsage\n\n\t// === Спорные/Игнорируемые ===\n\t// RETURNED_INVOICE (Возвратная накладная) - технически это уменьшение прихода,\n\t// но для рекомендаций \"что мы покупаем\" лучше обрабатывать отдельно или как минус-purchase.\n\t// Пока отнесем к UNKNOWN, чтобы не портить статистику чистого прихода,\n\t// либо можно считать как Purchase с отрицательным Amount (если XML дает минус).\n\tcase \"RETURNED_INVOICE\":\n\t\treturn operations.OpTypeUnknown\n\n\tcase \"INTERNAL_TRANSFER\":\n\t\treturn operations.OpTypeUnknown // Перемещение нас не интересует в рамках рекомендаций \"купил/продал\"\n\tcase \"INCOMING_INVENTORY\":\n\t\treturn operations.OpTypeUnknown // Инвентаризация\n\n\tdefault:\n\t\treturn operations.OpTypeUnknown\n\t}\n}\n\nfunc (s *Service) SyncStoreOperations() error {\n\tdateTo := time.Now()\n\tdateFrom := dateTo.AddDate(0, 0, -30)\n\n\t// 1. Синхронизируем Закупки (PresetPurchases)\n\t// Мы передаем OpTypePurchase, чтобы репозиторий знал, какую \"полку\" очистить перед записью.\n\tif err := s.syncReport(PresetPurchases, operations.OpTypePurchase, dateFrom, dateTo); err != nil {\n\t\treturn fmt.Errorf(\"ошибка синхронизации закупок: %w\", err)\n\t}\n\n\t// 2. Синхронизируем Расход (PresetUsage)\n\tif err := s.syncReport(PresetUsage, operations.OpTypeUsage, dateFrom, dateTo); err != nil {\n\t\treturn fmt.Errorf(\"ошибка синхронизации расхода: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (s *Service) syncReport(presetID string, targetOpType operations.OperationType, from, to time.Time) error {\n\tlogger.Log.Info(\"Запрос отчета RMS\", zap.String(\"preset\", presetID))\n\n\titems, err := s.rmsClient.FetchStoreOperations(presetID, from, to)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar ops []operations.StoreOperation\n\tfor _, item := range items {\n\t\t// 1. Валидация товара\n\t\tpID, err := uuid.Parse(item.ProductID)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 2. Определение реального типа операции\n\t\trealOpType := classifyOperation(item.DocumentType)\n\n\t\t// 3. Фильтрация \"мусора\"\n\t\t// Если мы грузим отчет \"Закупки\", но туда попало \"Перемещение\" (из-за кривого пресета),\n\t\t// мы это пропустим. Либо если документ неизвестного типа.\n\t\tif realOpType == operations.OpTypeUnknown {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Важно: Мы сохраняем только то, что соответствует целевому типу этапа синхронизации.\n\t\t// Если в пресете \"Закупки\" попалась \"Реализация\", мы не должны писать её в \"Закупки\",\n\t\t// и не должны писать в \"Расход\" (так как мы сейчас чистим \"Закупки\").\n\t\tif realOpType != targetOpType {\n\t\t\tcontinue\n\t\t}\n\n\t\tops = append(ops, operations.StoreOperation{\n\t\t\tProductID: pID,\n\t\t\tOpType: realOpType,\n\t\t\tDocumentType: item.DocumentType,\n\t\t\tTransactionType: item.TransactionType,\n\t\t\tDocumentNumber: item.DocumentNum,\n\t\t\tAmount: decimal.NewFromFloat(item.Amount),\n\t\t\tSum: decimal.NewFromFloat(item.Sum),\n\t\t\tCost: decimal.NewFromFloat(item.Cost),\n\t\t\tPeriodFrom: from,\n\t\t\tPeriodTo: to,\n\t\t})\n\t}\n\n\tif err := s.opRepo.SaveOperations(ops, targetOpType, from, to); err != nil {\n\t\treturn err\n\t}\n\n\tlogger.Log.Info(\"Отчет сохранен\",\n\t\tzap.String(\"op_type\", string(targetOpType)),\n\t\tzap.Int(\"received\", len(items)),\n\t\tzap.Int(\"saved\", len(ops)))\n\treturn nil\n}\n", + "internal/transport/http/handlers/invoices.go": "package handlers\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"go.uber.org/zap\"\n\n\tinvService \"rmser/internal/services/invoices\"\n\t\"rmser/pkg/logger\"\n)\n\ntype InvoiceHandler struct {\n\tservice *invService.Service\n}\n\nfunc NewInvoiceHandler(service *invService.Service) *InvoiceHandler {\n\treturn &InvoiceHandler{service: service}\n}\n\n// SendInvoice godoc\n// @Summary Создать приходную накладную в iikoRMS\n// @Description Принимает JSON с данными накладной и отправляет их в iiko\n// @Tags invoices\n// @Accept json\n// @Produce json\n// @Param input body invService.CreateRequestDTO true \"Invoice Data\"\n// @Success 200 {object} map[string]string \"created_number\"\n// @Failure 400 {object} map[string]string\n// @Failure 500 {object} map[string]string\nfunc (h *InvoiceHandler) SendInvoice(c *gin.Context) {\n\tvar req invService.CreateRequestDTO\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"Неверный формат JSON: \" + err.Error()})\n\t\treturn\n\t}\n\n\tdocNum, err := h.service.SendInvoiceToRMS(req)\n\tif err != nil {\n\t\tlogger.Log.Error(\"Ошибка отправки накладной\", zap.Error(err))\n\t\t// Возвращаем 502 Bad Gateway, т.к. ошибка скорее всего на стороне RMS\n\t\tc.JSON(http.StatusBadGateway, gin.H{\"error\": err.Error()})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"status\": \"ok\",\n\t\t\"created_number\": docNum,\n\t})\n}\n", + "internal/transport/http/handlers/ocr.go": "package handlers\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/google/uuid\"\n\t\"go.uber.org/zap\"\n\n\tocrService \"rmser/internal/services/ocr\"\n\t\"rmser/pkg/logger\"\n)\n\ntype OCRHandler struct {\n\tservice *ocrService.Service\n}\n\nfunc NewOCRHandler(service *ocrService.Service) *OCRHandler {\n\treturn &OCRHandler{service: service}\n}\n\n// GetCatalog возвращает список товаров для OCR сервиса\nfunc (h *OCRHandler) GetCatalog(c *gin.Context) {\n\titems, err := h.service.GetCatalogForIndexing()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Ошибка получения каталога для OCR\", zap.Error(err))\n\t\tc.JSON(http.StatusInternalServerError, gin.H{\"error\": err.Error()})\n\t\treturn\n\t}\n\tc.JSON(http.StatusOK, items)\n}\n\ntype MatchRequest struct {\n\tRawName string `json:\"raw_name\" binding:\"required\"`\n\tProductID string `json:\"product_id\" binding:\"required\"`\n}\n\n// SaveMatch сохраняет привязку (обучение)\nfunc (h *OCRHandler) SaveMatch(c *gin.Context) {\n\tvar req MatchRequest\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": err.Error()})\n\t\treturn\n\t}\n\n\tpID, err := uuid.Parse(req.ProductID)\n\tif err != nil {\n\t\tc.JSON(http.StatusBadRequest, gin.H{\"error\": \"invalid product_id format\"})\n\t\treturn\n\t}\n\n\tif err := h.service.SaveMapping(req.RawName, pID); err != nil {\n\t\tlogger.Log.Error(\"Ошибка сохранения матчинга\", zap.Error(err))\n\t\tc.JSON(http.StatusInternalServerError, gin.H{\"error\": err.Error()})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\"status\": \"saved\"})\n}\n\n// GetMatches возвращает список всех обученных связей\nfunc (h *OCRHandler) GetMatches(c *gin.Context) {\n\tmatches, err := h.service.GetKnownMatches()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Ошибка получения списка матчей\", zap.Error(err))\n\t\tc.JSON(http.StatusInternalServerError, gin.H{\"error\": err.Error()})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, matches)\n}\n", + "internal/transport/http/handlers/recommendations.go": "package handlers\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"go.uber.org/zap\"\n\n\t\"rmser/internal/services/recommend\"\n\t\"rmser/pkg/logger\"\n)\n\ntype RecommendationsHandler struct {\n\tservice *recommend.Service\n}\n\nfunc NewRecommendationsHandler(service *recommend.Service) *RecommendationsHandler {\n\treturn &RecommendationsHandler{service: service}\n}\n\n// GetRecommendations godoc\n// @Summary Получить список рекомендаций\n// @Description Возвращает сгенерированные рекомендации (проблемные зоны учета)\n// @Tags recommendations\n// @Produce json\n// @Success 200 {array} recommendations.Recommendation\n// @Failure 500 {object} map[string]string\nfunc (h *RecommendationsHandler) GetRecommendations(c *gin.Context) {\n\trecs, err := h.service.GetRecommendations()\n\tif err != nil {\n\t\tlogger.Log.Error(\"Ошибка получения рекомендаций\", zap.Error(err))\n\t\tc.JSON(http.StatusInternalServerError, gin.H{\"error\": err.Error()})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, recs)\n}\n", + "internal/transport/telegram/bot.go": "package telegram\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"go.uber.org/zap\"\n\ttele \"gopkg.in/telebot.v3\"\n\t\"gopkg.in/telebot.v3/middleware\"\n\n\t\"rmser/config\"\n\t\"rmser/internal/services/ocr\"\n\t\"rmser/pkg/logger\"\n)\n\ntype Bot struct {\n\tb *tele.Bot\n\tocrService *ocr.Service\n\tadminIDs map[int64]struct{}\n}\n\nfunc NewBot(cfg config.TelegramConfig, ocrService *ocr.Service) (*Bot, error) {\n\tpref := tele.Settings{\n\t\tToken: cfg.Token,\n\t\tPoller: &tele.LongPoller{Timeout: 10 * time.Second},\n\t\tOnError: func(err error, c tele.Context) {\n\t\t\tlogger.Log.Error(\"Telegram error\", zap.Error(err))\n\t\t},\n\t}\n\n\tb, err := tele.NewBot(pref)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tadmins := make(map[int64]struct{})\n\tfor _, id := range cfg.AdminIDs {\n\t\tadmins[id] = struct{}{}\n\t}\n\n\tbot := &Bot{\n\t\tb: b,\n\t\tocrService: ocrService,\n\t\tadminIDs: admins,\n\t}\n\n\tbot.initHandlers()\n\treturn bot, nil\n}\n\nfunc (bot *Bot) Start() {\n\tlogger.Log.Info(\"Запуск Telegram бота...\")\n\tbot.b.Start()\n}\n\nfunc (bot *Bot) Stop() {\n\tbot.b.Stop()\n}\n\n// Middleware для проверки прав (только админы)\nfunc (bot *Bot) authMiddleware(next tele.HandlerFunc) tele.HandlerFunc {\n\treturn func(c tele.Context) error {\n\t\tif len(bot.adminIDs) > 0 {\n\t\t\tif _, ok := bot.adminIDs[c.Sender().ID]; !ok {\n\t\t\t\treturn c.Send(\"⛔ У вас нет доступа к этому боту.\")\n\t\t\t}\n\t\t}\n\t\treturn next(c)\n\t}\n}\n\nfunc (bot *Bot) initHandlers() {\n\tbot.b.Use(middleware.Logger())\n\tbot.b.Use(bot.authMiddleware)\n\n\tbot.b.Handle(\"/start\", func(c tele.Context) error {\n\t\treturn c.Send(\"👋 Привет! Я RMSER Bot.\\nОтправь мне фото накладной или чека, и я попробую его распознать.\")\n\t})\n\n\tbot.b.Handle(tele.OnPhoto, bot.handlePhoto)\n}\n\nfunc (bot *Bot) handlePhoto(c tele.Context) error {\n\t// 1. Скачиваем фото\n\tphoto := c.Message().Photo\n\t// Берем файл самого высокого качества (последний в массиве, но telebot дает удобный доступ)\n\tfile, err := bot.b.FileByID(photo.FileID)\n\tif err != nil {\n\t\treturn c.Send(\"Ошибка доступа к файлу.\")\n\t}\n\n\t// Читаем тело файла\n\tfileURL := fmt.Sprintf(\"https://api.telegram.org/file/bot%s/%s\", bot.b.Token, file.FilePath)\n\tresp, err := http.Get(fileURL)\n\tif err != nil {\n\t\treturn c.Send(\"Ошибка скачивания файла.\")\n\t}\n\tdefer resp.Body.Close()\n\n\timgData, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn c.Send(\"Ошибка чтения файла.\")\n\t}\n\n\tc.Send(\"⏳ Обрабатываю чек через OCR...\")\n\n\t// 2. Отправляем в сервис\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\n\titems, err := bot.ocrService.ProcessReceiptImage(ctx, imgData)\n\tif err != nil {\n\t\tlogger.Log.Error(\"OCR processing failed\", zap.Error(err))\n\t\treturn c.Send(\"❌ Ошибка распознавания: \" + err.Error())\n\t}\n\n\t// 3. Формируем отчет\n\tvar sb strings.Builder\n\tsb.WriteString(fmt.Sprintf(\"🧾 Результат (%d поз.):\\n\\n\", len(items)))\n\n\tmatchedCount := 0\n\tfor _, item := range items {\n\t\tif item.IsMatched {\n\t\t\tmatchedCount++\n\t\t\tsb.WriteString(fmt.Sprintf(\"✅ %s\\n └ %s x %s = %s\\n\",\n\t\t\t\titem.RawName, item.Amount, item.Price, item.Sum))\n\t\t} else {\n\t\t\tsb.WriteString(fmt.Sprintf(\"❓ %s\\n └ Нет привязки!\\n\", item.RawName))\n\t\t}\n\t}\n\n\tsb.WriteString(fmt.Sprintf(\"\\nРаспознано: %d/%d\", matchedCount, len(items)))\n\n\t// Тут можно добавить кнопки, если что-то не распознано\n\t// Но для начала просто текст\n\treturn c.Send(sb.String(), tele.ModeHTML)\n}\n", + "ocr-service/system-prompt.md": "Вот подробный системный промпт (System Definition), который описывает архитектуру, логику и контракт работы твоего OCR-сервиса.\n\nСохрани этот текст как **`SYSTEM_PROMPT.md`** или в документацию проекта (Confluence/Wiki). К нему стоит обращаться при разработке API-клиентов, тестировании или доработке логики.\n\n---\n\n# System Definition: RMSER OCR Service\n\n## 1. Роль и Назначение\n**RMSER OCR Service** — это специализированный микросервис на базе FastAPI, предназначенный для извлечения структурированных данных (товарных позиций) из изображений кассовых чеков РФ.\n\nСервис реализует **Гибридную Стратегию Распознавания**, отдавая приоритет получению верифицированных данных через ФНС, и используя оптическое распознавание (OCR) только как запасной вариант (fallback).\n\n## 2. Логика Обработки (Workflow)\n\nПри получении `POST /recognize` с изображением, сервис выполняет действия в строгой последовательности:\n\n### Этап А: Поиск QR-кода (Priority 1)\n1. **Детекция:** Сервис сканирует изображение на наличие QR-кода (библиотека `pyzbar`).\n2. **Декодирование:** Извлекает сырую строку чека (формат: `t=YYYYMMDD...&s=SUM...&fn=...`).\n3. **Запрос к API:** Отправляет сырые данные в API `proverkacheka.com` (или аналог).\n4. **Результат:**\n * Если API возвращает успех: Возвращает идеальный список товаров.\n * **Метаданные ответа:** `source: \"qr_api\"`.\n\n### Этап Б: Оптическое Распознавание (Fallback Strategy)\n*Запускается только если QR-код не найден или API вернул ошибку.*\n\n1. **Препроцессинг (OpenCV):**\n * Поиск контуров документа.\n * Выравнивание перспективы (Perspective Warp).\n * Бинаризация (Adaptive Threshold) для подготовки к Tesseract.\n2. **OCR (Tesseract):** Извлечение сырого текста (rus+eng).\n3. **Парсинг (Regex):**\n * Поиск строк, содержащих паттерны цен (например, `120.00 * 2 = 240.00`).\n * Привязка текстового описания (названия товара) к найденным ценам.\n4. **Результат:** Возвращает список товаров, найденных эвристическим путем.\n * **Метаданные ответа:** `source: \"ocr\"`.\n\n## 3. Контракт API (Interface)\n\n### Входные данные\n* **Endpoint:** `POST /recognize`\n* **Format:** `multipart/form-data`\n* **Field:** `image` (binary file: jpg, png, heic, etc.)\n\n### Выходные данные (JSON)\nСервис всегда возвращает объект `RecognitionResult`:\n\n```json\n{\n \"source\": \"qr_api\", // или \"ocr\"\n \"items\": [\n {\n \"raw_name\": \"Молоко Домик в Деревне 3.2%\", // Название товара\n \"amount\": 2.0, // Количество\n \"price\": 89.99, // Цена за единицу\n \"sum\": 179.98 // Общая сумма позиции\n },\n {\n \"raw_name\": \"Пакет-майка\",\n \"amount\": 1.0,\n \"price\": 5.00,\n \"sum\": 5.00\n }\n ],\n \"raw_text\": \"...\" // Сырой текст (для отладки) или содержимое QR\n}\n```\n\n## 4. Технический Стек и Зависимости\n* **Runtime:** Python 3.10+\n* **Web Framework:** FastAPI + Uvicorn\n* **Computer Vision:** OpenCV (`cv2`) — обработка изображений.\n* **OCR Engine:** Tesseract OCR 5 (`pytesseract`) — движок распознавания текста.\n* **QR Decoding:** `pyzbar` + `libzbar0`.\n* **External API:** `proverkacheka.com` (требует валидный токен).\n\n## 5. Ограничения и Известные Проблемы\n1. **Качество OCR:** В режиме `ocr` точность зависит от качества фото (освещение, помятость). Возможны ошибки в символах `3/8`, `1/7`, `З/3`.\n2. **Зависимость от API:** Для работы режима `qr_api` необходим доступ в интернет и оплаченный токен провайдера.\n3. **Скорость:** Режим `qr_api` работает быстрее (0.5-1.5 сек). Режим `ocr` может занимать 2-4 сек в зависимости от разрешения фото.\n\n## 6. Инструкции для Интеграции\nПри встраивании сервиса в общую систему (например, Telegram-бот или Backend приложения):\n1. Всегда проверяйте поле `source`. Если `source == \"ocr\"`, помечайте данные для пользователя как \"Требующие проверки\" (Draft). Если `source == \"qr_api\"`, данные можно считать верифицированными.\n2. Если массив `items` пустой, значит сервис не смог распознать чек (ни QR, ни текст не прочитался). Предложите пользователю переснять фото.", "pkg/logger/logger.go": "package logger\n\nimport (\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n)\n\nvar Log *zap.Logger\n\nfunc Init(mode string) {\n\tvar config zap.Config\n\n\tif mode == \"release\" {\n\t\tconfig = zap.NewProductionConfig()\n\t} else {\n\t\tconfig = zap.NewDevelopmentConfig()\n\t\tconfig.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder\n\t}\n\n\tvar err error\n\tLog, err = config.Build()\n\tif err != nil {\n\t\tpanic(\"не удалось инициализировать логгер: \" + err.Error())\n\t}\n}", } diff --git a/rmser-view/.env b/rmser-view/.env new file mode 100644 index 0000000..da119fb --- /dev/null +++ b/rmser-view/.env @@ -0,0 +1,3 @@ +# Используем относительный путь. +# Браузер сам подставит текущий домен: https://rmser.your-domain.com/api +VITE_API_URL=/api \ No newline at end of file diff --git a/rmser-view/.gitignore b/rmser-view/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/rmser-view/.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/rmser-view/README.md b/rmser-view/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/rmser-view/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 [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## 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/rmser-view/eslint.config.js b/rmser-view/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/rmser-view/eslint.config.js @@ -0,0 +1,23 @@ +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: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/rmser-view/index.html b/rmser-view/index.html new file mode 100644 index 0000000..febc0f1 --- /dev/null +++ b/rmser-view/index.html @@ -0,0 +1,16 @@ + + + + + + + RMSer App + + + + +
+ + + + \ No newline at end of file diff --git a/rmser-view/package-lock.json b/rmser-view/package-lock.json new file mode 100644 index 0000000..baa290f --- /dev/null +++ b/rmser-view/package-lock.json @@ -0,0 +1,4604 @@ +{ + "name": "rmser-view", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rmser-view", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-query": "^5.90.12", + "@twa-dev/sdk": "^8.0.2", + "antd": "^6.1.0", + "axios": "^1.13.2", + "clsx": "^2.1.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.10.1", + "zustand": "^5.0.9" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@ant-design/colors": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.0.tgz", + "integrity": "sha512-6YzkKCw30EI/E9kHOIXsQDHmMvTllT8STzjMb4K2qzit33RW2pqCJP0sk+hidBntXxE+Vz4n1+RvCTfBw6OErw==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.0.1.tgz", + "integrity": "sha512-Lw1Z4cUQxdMmTNir67gU0HCpTl5TtkKCJPZ6UBvCqzcOTl/QmMFB6qAEoj8qFl0CuZDX9qQYa3m9+rEKfaBSbA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.0.2.tgz", + "integrity": "sha512-Mq3Hm6fJuQeFNKSp3+yT4bjuhVbdrsyXE2RyfpJFL0xiYNZdaJ6oFaE3zFrzmHbmvTd2Wp3HCbRtkD4fU+v2ZA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^2.0.1", + "@babel/runtime": "^7.23.2", + "@rc-component/util": "^1.4.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.0.tgz", + "integrity": "sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA==", + "license": "MIT", + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.0.tgz", + "integrity": "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-2.0.0.tgz", + "integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "clsx": "^2.1.1", + "json2mq": "^0.2.0", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@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.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@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.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "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.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "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.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.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/@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/@rc-component/async-validator": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", + "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/cascader": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.9.0.tgz", + "integrity": "sha512-2jbthe1QZrMBgtCvNKkJFjZYC3uKl4N/aYm5SsMvO3T+F+qRT1CGsSM9bXnh1rLj7jDk/GK0natShWF/jinhWQ==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.3.0", + "@rc-component/tree": "~1.1.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/checkbox": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-1.0.1.tgz", + "integrity": "sha512-08yTH8m+bSm8TOqbybbJ9KiAuIATti6bDs2mVeSfu4QfEnyeF6X0enHVvD1NEAyuBWEAo56QtLe++MYs2D9XiQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/collapse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.1.2.tgz", + "integrity": "sha512-ilBYk1dLLJHu5Q74dF28vwtKUYQ42ZXIIDmqTuVy4rD8JQVvkXOs+KixVNbweyuIEtJYJ7+t+9GVD9dPc6N02w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.0.3.tgz", + "integrity": "sha512-V7gFF9O7o5XwIWafdbOtqI4BUUkEUkgdBwp6favy3xajMX/2dDqytFaiXlcwrpq6aRyPLp5dKLAG5RFKLXMeGA==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.1.tgz", + "integrity": "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/dialog": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.5.1.tgz", + "integrity": "sha512-by4Sf/a3azcb89WayWuwG19/Y312xtu8N81HoVQQtnsBDylfs+dog98fTAvLinnpeoWG52m/M7QLRW6fXR3l1g==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.3", + "@rc-component/portal": "^2.0.0", + "@rc-component/util": "^1.0.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/drawer": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.3.0.tgz", + "integrity": "sha512-rE+sdXEmv2W25VBQ9daGbnb4J4hBIEKmdbj0b3xpY+K7TUmLXDIlSnoXraIbFZdGyek9WxxGKK887uRnFgI+pQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.0.0", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dropdown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.2.tgz", + "integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/@rc-component/form": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.4.0.tgz", + "integrity": "sha512-C8MN/2wIaW9hSrCCtJmcgCkWTQNIspN7ARXLFA4F8PGr8Qxk39U5pS3kRK51/bUJNhb/fEtdFnaViLlISGKI2A==", + "license": "MIT", + "dependencies": { + "@rc-component/async-validator": "^5.0.3", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/image": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.5.3.tgz", + "integrity": "sha512-/NR7QW9uCN8Ugar+xsHZOPvzPySfEhcW2/vLcr7VPRM+THZMrllMRv7LAUgW7ikR+Z67Ab67cgPp5K5YftpJsQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/portal": "^2.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/input": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.1.2.tgz", + "integrity": "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/input-number": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@rc-component/input-number/-/input-number-1.6.2.tgz", + "integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==", + "license": "MIT", + "dependencies": { + "@rc-component/mini-decimal": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mentions": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.6.0.tgz", + "integrity": "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.1.0", + "@rc-component/menu": "~1.2.0", + "@rc-component/textarea": "~1.1.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/menu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.2.0.tgz", + "integrity": "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/motion": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.1.6.tgz", + "integrity": "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz", + "integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/notification": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-1.2.0.tgz", + "integrity": "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/overflow": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/overflow/-/overflow-1.0.0.tgz", + "integrity": "sha512-GSlBeoE0XTBi5cf3zl8Qh7Uqhn7v8RrlJ8ajeVpEkNe94HWy5l5BQ0Mwn2TVUq9gdgbfEMUmTX7tJFAg7mz0Rw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/pagination": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.2.0.tgz", + "integrity": "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/picker": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.8.0.tgz", + "integrity": "sha512-ek4efrIy+peC8WFJg6Lg7c+WNkykr+wUGQGBNoKmlF0K752aIJuaPcBj6p8CceT9vSJ9gOeeclQCBQIFWVDk1A==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/trigger": "^3.6.15", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/@rc-component/portal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-2.0.1.tgz", + "integrity": "sha512-46KYuA7Udb1LAaLIdDrfmDz3wzyeEZxIURJCn+heoQVbhtW5PQkhBSQtRus+DUdsknmTFQulxSnqrbX3CI4yXw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/progress": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/progress/-/progress-1.0.2.tgz", + "integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", + "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/rate": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/rate/-/rate-1.0.1.tgz", + "integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/resize-observer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.0.1.tgz", + "integrity": "sha512-r+w+Mz1EiueGk1IgjB3ptNXLYSLZ5vnEfKHH+gfgj7JMupftyzvUUl3fRcMZe5uMM04x0n8+G2o/c6nlO2+Wag==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/segmented": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rc-component/segmented/-/segmented-1.2.3.tgz", + "integrity": "sha512-L7G4S6zUpqHclOXK0wKKN2/VyqHa9tfDNxkoFjWOTPtQ0ROFaBwZhbf1+9sdZfIFkxJkpcShAmDOMEIBaFFqkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/select": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.3.5.tgz", + "integrity": "sha512-A2QVOWDfRoLgHwPHrCGx1G42dYntOk+nsT6SX4ADCoagqu4bcxceJPbYvVKkfMYSIwgtfu+tDhPk3Z5gz8944g==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/slider": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.0.1.tgz", + "integrity": "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/steps": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rc-component/steps/-/steps-1.2.2.tgz", + "integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rc-component/switch/-/switch-1.0.3.tgz", + "integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/table": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.9.0.tgz", + "integrity": "sha512-cq3P9FkD+F3eglkFYhBuNlHclg+r4jY8+ZIgK7zbEFo6IwpnA77YL/Gq4ensLw9oua3zFCTA6JDu6YgBei0TxA==", + "license": "MIT", + "dependencies": { + "@rc-component/context": "^2.0.1", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.1.0", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tabs": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.7.0.tgz", + "integrity": "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==", + "license": "MIT", + "dependencies": { + "@rc-component/dropdown": "~1.0.0", + "@rc-component/menu": "~1.2.0", + "@rc-component/motion": "^1.1.3", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/textarea": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/textarea/-/textarea-1.1.2.tgz", + "integrity": "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.1.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tooltip": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz", + "integrity": "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.7.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.2.1.tgz", + "integrity": "sha512-BUCrVikGJsXli38qlJ+h2WyDD6dYxzDA9dV3o0ij6gYhAq6ooT08SUMWOikva9v4KZ2BEuluGl5bPcsjrSoBgQ==", + "license": "MIT", + "dependencies": { + "@rc-component/portal": "^2.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.1.0.tgz", + "integrity": "sha512-HZs3aOlvFgQdgrmURRc/f4IujiNBf4DdEeXUlkS0lPoLlx9RoqsZcF0caXIAMVb+NaWqKtGQDnrH8hqLCN5zlA==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/util": "^1.2.1", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/tree-select": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.4.0.tgz", + "integrity": "sha512-I3UAlO2hNqy9CSKc8EBaESgnmKk2QaRzuZ2XHZGFCgsSMkGl06mdF97sVfROM02YIb64ocgLKefsjE0Ch4ocwQ==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.3.0", + "@rc-component/tree": "~1.1.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/trigger": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.7.1.tgz", + "integrity": "sha512-+YNP8FywxKJpdqzlAp6TN8UbSK6YsQtIs3kI13mHfm87qi3qUd5Q9AGW8Unfv76kXFUSu7U7D0FygRsGH+6MiA==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/upload": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.0.tgz", + "integrity": "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/util": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.6.0.tgz", + "integrity": "sha512-YbjuIVAm8InCnXVoA4n6G+uh31yESTxQ6fSY2frZ2/oMSvktoB+bumFUfNN7RKh7YeOkZgOvN2suGtEDhJSX0A==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/virtual-list": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.0.2.tgz", + "integrity": "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.12", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.12.tgz", + "integrity": "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.12.tgz", + "integrity": "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@twa-dev/sdk": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@twa-dev/sdk/-/sdk-8.0.2.tgz", + "integrity": "sha512-Pp5GxnxP2blboVZFiM9aWjs4cb8IpW3x2jP3kLOMvIqy0jzNUTuFHkwHtx+zEvh/UcF2F+wmS8G6ebIA0XPXcg==", + "license": "MIT", + "dependencies": { + "@twa-dev/types": "^8.0.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@twa-dev/types": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@twa-dev/types/-/types-8.0.2.tgz", + "integrity": "sha512-ICQ6n4NaUPPzV3/GzflVQS6Nnu5QX2vr9OlOG8ZkFf3rSJXzRKazrLAbZlVhCPPWkIW3MMuELPsE6tByrA49qA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.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/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/node": { + "version": "24.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.2.tgz", + "integrity": "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "devOptional": true, + "license": "MIT", + "peer": true, + "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==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", + "integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/type-utils": "8.49.0", + "@typescript-eslint/utils": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.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.49.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.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.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz", + "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", + "debug": "^4.3.4" + }, + "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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz", + "integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.49.0", + "@typescript-eslint/types": "^8.49.0", + "debug": "^4.3.4" + }, + "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.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz", + "integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.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.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz", + "integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz", + "integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/utils": "8.49.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz", + "integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==", + "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.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz", + "integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.49.0", + "@typescript-eslint/tsconfig-utils": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.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.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz", + "integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz", + "integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.49.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "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.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antd": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.1.0.tgz", + "integrity": "sha512-RIe4W5saaL9SWgvqCcvz6LZta/KwT50B0YF7xYiWVZh0Gqfw2rJAsOMcp202Hxgm+YiyoSp4QqqvexKhuGGarw==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.0", + "@ant-design/cssinjs": "^2.0.1", + "@ant-design/cssinjs-utils": "^2.0.2", + "@ant-design/fast-color": "^3.0.0", + "@ant-design/icons": "^6.1.0", + "@ant-design/react-slick": "~2.0.0", + "@babel/runtime": "^7.28.4", + "@rc-component/cascader": "~1.9.0", + "@rc-component/checkbox": "~1.0.1", + "@rc-component/collapse": "~1.1.2", + "@rc-component/color-picker": "~3.0.3", + "@rc-component/dialog": "~1.5.1", + "@rc-component/drawer": "~1.3.0", + "@rc-component/dropdown": "~1.0.2", + "@rc-component/form": "~1.4.0", + "@rc-component/image": "~1.5.2", + "@rc-component/input": "~1.1.2", + "@rc-component/input-number": "~1.6.2", + "@rc-component/mentions": "~1.6.0", + "@rc-component/menu": "~1.2.0", + "@rc-component/motion": "~1.1.6", + "@rc-component/mutate-observer": "^2.0.1", + "@rc-component/notification": "~1.2.0", + "@rc-component/pagination": "~1.2.0", + "@rc-component/picker": "~1.8.0", + "@rc-component/progress": "~1.0.2", + "@rc-component/qrcode": "~1.1.1", + "@rc-component/rate": "~1.0.1", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/segmented": "~1.2.3", + "@rc-component/select": "~1.3.2", + "@rc-component/slider": "~1.0.1", + "@rc-component/steps": "~1.2.2", + "@rc-component/switch": "~1.0.3", + "@rc-component/table": "~1.9.0", + "@rc-component/tabs": "~1.7.0", + "@rc-component/textarea": "~1.1.2", + "@rc-component/tooltip": "~1.4.0", + "@rc-component/tour": "~2.2.1", + "@rc-component/tree": "~1.1.0", + "@rc-component/tree-select": "~1.4.0", + "@rc-component/trigger": "^3.7.1", + "@rc-component/upload": "~1.1.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.11", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "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/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", + "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "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": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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/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==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT", + "peer": true + }, + "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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "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": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.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", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "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" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "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/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==", + "dev": true, + "license": "MIT" + }, + "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/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/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/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.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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/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/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/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "license": "MIT" + }, + "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/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.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "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-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/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^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/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/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.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "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/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.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-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "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/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/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.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "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/react": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz", + "integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==", + "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.10.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.1.tgz", + "integrity": "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==", + "license": "MIT", + "dependencies": { + "react-router": "7.10.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "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/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "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/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "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/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.49.0.tgz", + "integrity": "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.49.0", + "@typescript-eslint/parser": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/utils": "8.49.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", + "typescript": ">=4.8.4 <6.0.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/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "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/vite": { + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", + "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "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", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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/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/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/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/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "dev": true, + "license": "MIT", + "peer": true, + "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/zustand": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz", + "integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==", + "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/rmser-view/package.json b/rmser-view/package.json new file mode 100644 index 0000000..6728f92 --- /dev/null +++ b/rmser-view/package.json @@ -0,0 +1,37 @@ +{ + "name": "rmser-view", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.12", + "@twa-dev/sdk": "^8.0.2", + "antd": "^6.1.0", + "axios": "^1.13.2", + "clsx": "^2.1.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.10.1", + "zustand": "^5.0.9" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/rmser-view/public/vite.svg b/rmser-view/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/rmser-view/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rmser-view/src/App.tsx b/rmser-view/src/App.tsx new file mode 100644 index 0000000..c03b02f --- /dev/null +++ b/rmser-view/src/App.tsx @@ -0,0 +1,27 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { Providers } from './components/layout/Providers'; +import { AppLayout } from './components/layout/AppLayout'; +import { Dashboard } from './pages/Dashboard'; // Импортируем созданную страницу +import { OcrLearning } from './pages/OcrLearning'; + +// Заглушки для остальных страниц пока оставим +const InvoicesPage = () =>

Список накладных

; + +function App() { + return ( + + + + }> + } /> {/* Используем компонент */} + } /> + } /> + } /> + + + + + ); +} + +export default App; \ No newline at end of file diff --git a/rmser-view/src/components/layout/AppLayout.tsx b/rmser-view/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..15aab8e --- /dev/null +++ b/rmser-view/src/components/layout/AppLayout.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Layout, Menu, theme } from 'antd'; +import { Outlet, useNavigate, useLocation } from 'react-router-dom'; +import { BarChartOutlined, ScanOutlined, FileTextOutlined } from '@ant-design/icons'; + +const { Header, Content, Footer } = Layout; + +export const AppLayout: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + + // Получаем токены темы (чтобы подстроить AntD под Telegram можно позже настроить ConfigProvider) + const { + token: { colorBgContainer, borderRadiusLG }, + } = theme.useToken(); + + // Определяем активный пункт меню + const selectedKey = location.pathname === '/' ? 'dashboard' + : location.pathname.startsWith('/ocr') ? 'ocr' + : location.pathname.startsWith('/invoices') ? 'invoices' + : 'dashboard'; + + const menuItems = [ + { key: 'dashboard', icon: , label: 'Дашборд', onClick: () => navigate('/') }, + { key: 'ocr', icon: , label: 'Обучение', onClick: () => navigate('/ocr') }, + { key: 'invoices', icon: , label: 'Накладные', onClick: () => navigate('/invoices') }, + ]; + + return ( + +
+ +
+ +
+ +
+
+
+ RMSer ©{new Date().getFullYear()} +
+
+ ); +}; \ No newline at end of file diff --git a/rmser-view/src/components/layout/Providers.tsx b/rmser-view/src/components/layout/Providers.tsx new file mode 100644 index 0000000..9144a0a --- /dev/null +++ b/rmser-view/src/components/layout/Providers.tsx @@ -0,0 +1,43 @@ +import React, { useEffect, useState } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import WebApp from '@twa-dev/sdk'; + +// Настройка клиента React Query +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, // Не перезапрашивать при переключении вкладок + retry: 1, + }, + }, +}); + +interface ProvidersProps { + children: React.ReactNode; +} + +export const Providers: React.FC = ({ children }) => { + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + // Инициализация Telegram Mini App + WebApp.ready(); + WebApp.expand(); // Разворачиваем на весь экран + + // Подстраиваем цвет хедера под тему Telegram + WebApp.setHeaderColor('secondary_bg_color'); + + // eslint-disable-next-line react-hooks/set-state-in-effect + setIsReady(true); + }, []); + + if (!isReady) { + return
Loading Telegram SDK...
; + } + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/rmser-view/src/components/ocr/AddMatchForm.tsx b/rmser-view/src/components/ocr/AddMatchForm.tsx new file mode 100644 index 0000000..d9c9147 --- /dev/null +++ b/rmser-view/src/components/ocr/AddMatchForm.tsx @@ -0,0 +1,143 @@ +import React, { useState, useMemo } from 'react'; // Убрали useEffect +import { Card, Button, Flex, AutoComplete, InputNumber, Typography, Select } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import { CatalogSelect } from './CatalogSelect'; +import type { CatalogItem, UnmatchedItem } from '../../services/types'; + +const { Text } = Typography; + +interface Props { + catalog: CatalogItem[]; + unmatched?: UnmatchedItem[]; + onSave: (rawName: string, productId: string, quantity: number, containerId?: string) => void; + isLoading: boolean; +} + +export const AddMatchForm: React.FC = ({ catalog, unmatched = [], onSave, isLoading }) => { + const [rawName, setRawName] = useState(''); + const [selectedProduct, setSelectedProduct] = useState(undefined); + const [quantity, setQuantity] = useState(1); + const [selectedContainer, setSelectedContainer] = useState(null); + + const unmatchedOptions = useMemo(() => { + return unmatched.map(item => ({ + value: item.raw_name, + label: item.count ? `${item.raw_name} (${item.count} шт)` : item.raw_name + })); + }, [unmatched]); + + const selectedCatalogItem = useMemo(() => { + if (!selectedProduct) return null; + return catalog.find(item => item.id === selectedProduct || item.ID === selectedProduct); + }, [selectedProduct, catalog]); + + // Хендлер смены товара: сразу сбрасываем фасовку + const handleProductChange = (val: string) => { + setSelectedProduct(val); + setSelectedContainer(null); + }; + + // Мемоизируем список контейнеров, чтобы он был стабильной зависимостью + const containers = useMemo(() => { + return selectedCatalogItem?.containers || selectedCatalogItem?.Containers || []; + }, [selectedCatalogItem]); + + const baseUom = selectedCatalogItem?.measure_unit || selectedCatalogItem?.MeasureUnit || 'ед.'; + + const currentUomName = useMemo(() => { + if (selectedContainer) { + const cont = containers.find(c => c.id === selectedContainer); + return cont ? cont.name : baseUom; + } + return baseUom; + }, [selectedContainer, containers, baseUom]); + + const handleSubmit = () => { + if (rawName.trim() && selectedProduct && quantity && quantity > 0) { + onSave(rawName, selectedProduct, quantity, selectedContainer || undefined); + + setRawName(''); + setSelectedProduct(undefined); + setQuantity(1); + setSelectedContainer(null); + } + }; + + const isButtonDisabled = !rawName.trim() || !selectedProduct || !quantity || quantity <= 0 || isLoading; + + return ( + + +
+
Текст из чека:
+ + !inputValue || (option?.value as string).toLowerCase().includes(inputValue.toLowerCase()) + } + style={{ width: '100%' }} + /> +
+ +
+
Товар в iiko:
+ +
+ + {containers.length > 0 && ( +
+
Единица измерения / Фасовка:
+ + ); +}; \ No newline at end of file diff --git a/rmser-view/src/components/ocr/MatchList.tsx b/rmser-view/src/components/ocr/MatchList.tsx new file mode 100644 index 0000000..6c77cfe --- /dev/null +++ b/rmser-view/src/components/ocr/MatchList.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { List, Typography, Tag, Input, Empty } from 'antd'; +import { ArrowRightOutlined, SearchOutlined } from '@ant-design/icons'; +import type { ProductMatch } from '../../services/types'; + +const { Text } = Typography; + +interface Props { + matches: ProductMatch[]; +} + +export const MatchList: React.FC = ({ matches }) => { + const [searchText, setSearchText] = React.useState(''); + + const filteredData = matches.filter(item => { + const raw = (item.raw_name || item.RawName || '').toLowerCase(); + const prod = item.product || item.Product; + const prodName = (prod?.name || prod?.Name || '').toLowerCase(); + const search = searchText.toLowerCase(); + return raw.includes(search) || prodName.includes(search); + }); + + return ( +
+ } + style={{ marginBottom: 12 }} + value={searchText} + onChange={e => setSearchText(e.target.value)} + allowClear + /> + + }} + pagination={{ pageSize: 10, size: "small", simple: true }} + renderItem={(item) => { + // Унификация полей + const rawName = item.raw_name || item.RawName || 'Без названия'; + const product = item.product || item.Product; + const productName = product?.name || product?.Name || 'Товар не найден'; + const qty = item.quantity || item.Quantity || 1; + + // Логика отображения Единицы или Фасовки + const container = item.container || item.Container; + let displayUnit = ''; + + if (container) { + // Если есть фасовка: "Пачка 180г" + displayUnit = container.name; + } else { + // Иначе базовая ед.: "кг" + displayUnit = product?.measure_unit || product?.MeasureUnit || 'ед.'; + } + + return ( + +
+ Чек + {rawName} +
+
+ + + {productName} + + x {qty} {displayUnit} + + +
+
+ ); + }} + /> +
+ ); +}; \ No newline at end of file diff --git a/rmser-view/src/components/recommendations/RecommendationCard.tsx b/rmser-view/src/components/recommendations/RecommendationCard.tsx new file mode 100644 index 0000000..beee379 --- /dev/null +++ b/rmser-view/src/components/recommendations/RecommendationCard.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Card, Tag, Typography, Button } from 'antd'; +import { WarningOutlined, InfoCircleOutlined } from '@ant-design/icons'; +import type { Recommendation } from '../../services/types'; + +const { Text, Paragraph } = Typography; + +interface Props { + item: Recommendation; +} + +export const RecommendationCard: React.FC = ({ item }) => { + // Выбираем цвет тега в зависимости от типа проблемы + const getTagColor = (type: string) => { + switch (type) { + case 'UNUSED_IN_RECIPES': return 'volcano'; + case 'NO_INCOMING': return 'gold'; + default: return 'blue'; + } + }; + + const getIcon = (type: string) => { + return type === 'UNUSED_IN_RECIPES' ? : ; + }; + + return ( + + {getIcon(item.Type)} + {item.ProductName} +
+ } + extra={{item.Type}} + style={{ marginBottom: 12, boxShadow: '0 2px 8px rgba(0,0,0,0.05)' }} + > + + {item.Reason} + +
+ + {new Date(item.CreatedAt).toLocaleDateString()} + + {/* Кнопка действия (заглушка на будущее) */} + +
+
+ ); +}; \ No newline at end of file diff --git a/rmser-view/src/hooks/useOcr.ts b/rmser-view/src/hooks/useOcr.ts new file mode 100644 index 0000000..378ab60 --- /dev/null +++ b/rmser-view/src/hooks/useOcr.ts @@ -0,0 +1,48 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { api } from '../services/api'; +import type { MatchRequest, ProductMatch, CatalogItem, UnmatchedItem } from '../services/types'; +import { message } from 'antd'; + +export const useOcr = () => { + const queryClient = useQueryClient(); + + const catalogQuery = useQuery({ + queryKey: ['catalog'], + queryFn: api.getCatalogItems, + staleTime: 1000 * 60 * 5, + }); + + const matchesQuery = useQuery({ + queryKey: ['matches'], + queryFn: api.getMatches, + }); + + const unmatchedQuery = useQuery({ + queryKey: ['unmatched'], + queryFn: api.getUnmatched, + staleTime: 0, + }); + + const createMatchMutation = useMutation({ + // Теперь типы совпадают, any не нужен + mutationFn: (newMatch: MatchRequest) => api.createMatch(newMatch), + onSuccess: () => { + message.success('Связь сохранена'); + queryClient.invalidateQueries({ queryKey: ['matches'] }); + queryClient.invalidateQueries({ queryKey: ['unmatched'] }); + }, + onError: () => { + message.error('Ошибка при сохранении'); + }, + }); + + return { + catalog: catalogQuery.data || [], + matches: matchesQuery.data || [], + unmatched: unmatchedQuery.data || [], + isLoading: catalogQuery.isPending || matchesQuery.isPending, + isError: catalogQuery.isError || matchesQuery.isError, + createMatch: createMatchMutation.mutate, + isCreating: createMatchMutation.isPending, + }; +}; \ No newline at end of file diff --git a/rmser-view/src/hooks/useRecommendations.ts b/rmser-view/src/hooks/useRecommendations.ts new file mode 100644 index 0000000..447abd6 --- /dev/null +++ b/rmser-view/src/hooks/useRecommendations.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '../services/api'; +import type { Recommendation } from '../services/types'; + +export const useRecommendations = () => { + return useQuery({ + queryKey: ['recommendations'], + queryFn: api.getRecommendations, + // Обновлять данные каждые 30 секунд, если вкладка активна + refetchInterval: 30000, + }); +}; \ No newline at end of file diff --git a/rmser-view/src/main.tsx b/rmser-view/src/main.tsx new file mode 100644 index 0000000..2769960 --- /dev/null +++ b/rmser-view/src/main.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +// Если есть глобальные стили, они подключаются тут. +// Если файла index.css нет, убери эту строку. +// import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) \ No newline at end of file diff --git a/rmser-view/src/pages/Dashboard.tsx b/rmser-view/src/pages/Dashboard.tsx new file mode 100644 index 0000000..e84e85f --- /dev/null +++ b/rmser-view/src/pages/Dashboard.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Typography, Row, Col, Statistic, Spin, Alert, Empty } from 'antd'; +import { useRecommendations } from '../hooks/useRecommendations'; +import { RecommendationCard } from '../components/recommendations/RecommendationCard'; + +const { Title } = Typography; + +export const Dashboard: React.FC = () => { + const { data: recommendations, isPending, isError, error } = useRecommendations(); + + if (isPending) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( + + ); + } + + // Группировка для статистики + const unusedCount = recommendations?.filter(r => r.Type === 'UNUSED_IN_RECIPES').length || 0; + const noIncomingCount = recommendations?.filter(r => r.Type === 'NO_INCOMING').length || 0; + + return ( +
+ Сводка проблем + + {/* Блок статистики */} + + + + + + + + + + Рекомендации ({recommendations?.length}) + + {recommendations && recommendations.length > 0 ? ( + recommendations.map((rec) => ( + + )) + ) : ( + + )} +
+ ); +}; \ No newline at end of file diff --git a/rmser-view/src/pages/OcrLearning.tsx b/rmser-view/src/pages/OcrLearning.tsx new file mode 100644 index 0000000..10de08f --- /dev/null +++ b/rmser-view/src/pages/OcrLearning.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { Spin, Alert } from 'antd'; +import { useOcr } from '../hooks/useOcr'; +import { AddMatchForm } from '../components/ocr/AddMatchForm'; +import { MatchList } from '../components/ocr/MatchList'; + +export const OcrLearning: React.FC = () => { + const { + catalog, + matches, + unmatched, + isLoading, + isError, + createMatch, + isCreating + } = useOcr(); + + if (isLoading && matches.length === 0) { + return ( +
+ + Загрузка справочников... +
+ ); + } + + if (isError) { + return ( + + ); + } + + return ( +
+ createMatch({ + raw_name: raw, + product_id: prodId, + quantity: qty, + container_id: contId + })} + isLoading={isCreating} + /> + +

Обученные позиции ({matches.length})

+ +
+ ); +}; \ No newline at end of file diff --git a/rmser-view/src/services/api.ts b/rmser-view/src/services/api.ts new file mode 100644 index 0000000..68f3514 --- /dev/null +++ b/rmser-view/src/services/api.ts @@ -0,0 +1,67 @@ +import axios from 'axios'; +import type { + CatalogItem, + CreateInvoiceRequest, + MatchRequest, // Используем новый тип + HealthResponse, + InvoiceResponse, + ProductMatch, + Recommendation, + UnmatchedItem +} from './types'; + +// Базовый URL +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api'; + +const apiClient = axios.create({ + baseURL: API_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +apiClient.interceptors.response.use( + (response) => response, + (error) => { + console.error('API Error:', error); + return Promise.reject(error); + } +); + +export const api = { + checkHealth: async (): Promise => { + const { data } = await apiClient.get('/health'); + return data; + }, + + getRecommendations: async (): Promise => { + const { data } = await apiClient.get('/recommendations'); + return data; + }, + + getCatalogItems: async (): Promise => { + const { data } = await apiClient.get('/ocr/catalog'); + return data; + }, + + getMatches: async (): Promise => { + const { data } = await apiClient.get('/ocr/matches'); + return data; + }, + + getUnmatched: async (): Promise => { + const { data } = await apiClient.get('/ocr/unmatched'); + return data; + }, + + // Обновили тип аргумента payload + createMatch: async (payload: MatchRequest): Promise<{ status: string }> => { + const { data } = await apiClient.post<{ status: string }>('/ocr/match', payload); + return data; + }, + + createInvoice: async (payload: CreateInvoiceRequest): Promise => { + const { data } = await apiClient.post('/invoices/send', payload); + return data; + }, +}; \ No newline at end of file diff --git a/rmser-view/src/services/types.ts b/rmser-view/src/services/types.ts new file mode 100644 index 0000000..048c8bc --- /dev/null +++ b/rmser-view/src/services/types.ts @@ -0,0 +1,98 @@ +// --- Общие типы --- + +export type UUID = string; + +// --- Каталог и Фасовки (API v2.0) --- + +export interface ProductContainer { + id: UUID; + name: string; // "Пачка 180г" + count: number; // 0.180 +} + +export interface CatalogItem { + // Основные поля (snake_case) + id: UUID; + name: string; + code: string; + measure_unit: string; // "кг", "л" + containers: ProductContainer[]; // Массив фасовок + + // Fallback (на всякий случай) + ID?: UUID; + Name?: string; + Code?: string; + MeasureUnit?: string; + Containers?: ProductContainer[]; +} + +// --- Матчинг (Обучение) --- + +export interface MatchRequest { + raw_name: string; + product_id: UUID; + quantity: number; + container_id?: UUID; // Новое поле +} + +export interface ProductMatch { + // snake_case (v2.0) + raw_name: string; + product_id: UUID; + product?: CatalogItem; + quantity: number; + container_id?: UUID; + container?: ProductContainer; + updated_at: string; + + // Fallback + RawName?: string; + ProductID?: UUID; + Product?: CatalogItem; + Quantity?: number; + ContainerId?: UUID; + Container?: ProductContainer; +} + +// --- Нераспознанное --- + +export interface UnmatchedItem { + raw_name: string; + count: number; + last_seen: string; +} + +// --- Остальные типы (без изменений) --- + +export interface Recommendation { + ID: UUID; + Type: string; + ProductID: UUID; + ProductName: string; + Reason: string; + CreatedAt: string; +} + +export interface InvoiceItemRequest { + product_id: UUID; + amount: number; + price: number; +} + +export interface CreateInvoiceRequest { + document_number: string; + date_incoming: string; + supplier_id: UUID; + store_id: UUID; + items: InvoiceItemRequest[]; +} + +export interface InvoiceResponse { + status: string; + created_number: string; +} + +export interface HealthResponse { + status: string; + time: string; +} \ No newline at end of file diff --git a/rmser-view/tsconfig.app.json b/rmser-view/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/rmser-view/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/rmser-view/tsconfig.json b/rmser-view/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/rmser-view/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/rmser-view/tsconfig.node.json b/rmser-view/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/rmser-view/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/rmser-view/vite.config.ts b/rmser-view/vite.config.ts new file mode 100644 index 0000000..08dd04c --- /dev/null +++ b/rmser-view/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + host: true, + port: 5173, + // Разрешаем наш домен + allowedHosts: ['rmser.serty.top'], + } +}) \ No newline at end of file