diff --git a/cmd/main.go b/cmd/main.go
index a2d9e6b..e61a1cd 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -1,6 +1,7 @@
package main
import (
+ "context"
"log"
"os"
"time"
@@ -17,8 +18,8 @@ import (
"rmser/internal/services/auth"
"rmser/internal/transport/http/middleware"
- "rmser/internal/transport/ws"
tgBot "rmser/internal/transport/telegram"
+ "rmser/internal/transport/ws"
// Repositories
accountPkg "rmser/internal/infrastructure/repository/account"
@@ -43,6 +44,7 @@ import (
photosServicePkg "rmser/internal/services/photos"
recServicePkg "rmser/internal/services/recommend"
"rmser/internal/services/sync"
+ "rmser/internal/services/worker"
// Handlers
"rmser/internal/transport/http/handlers"
@@ -100,13 +102,21 @@ func main() {
ykClient := yookassa.NewClient(cfg.YooKassa.ShopID, cfg.YooKassa.SecretKey)
billingService := billingServicePkg.NewService(billingRepo, accountRepo, ykClient)
- syncService := sync.NewService(rmsFactory, accountRepo, catalogRepo, recipesRepo, invoicesRepo, opsRepo, supplierRepo)
+ syncService := sync.NewService(rmsFactory, cryptoManager, accountRepo, catalogRepo, recipesRepo, invoicesRepo, opsRepo, supplierRepo)
+
+ // Создаем сервис рекомендаций до SyncWorker, так как он нужен для обновления рекомендаций
recService := recServicePkg.NewService(recRepo)
+
+ // 6.1 SyncWorker для фоновой синхронизации
+ syncWorker := worker.NewSyncWorker(syncService, accountRepo, rmsFactory, recService, logger.Log)
+ workerCtx, workerCancel := context.WithCancel(context.Background())
+ go syncWorker.Run(workerCtx)
+ defer workerCancel()
ocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, draftsRepo, accountRepo, photosRepo, pyClient, cfg.App.StoragePath)
// Устанавливаем DevIDs для OCR сервиса
ocrService.SetDevIDs(cfg.App.DevIDs)
draftsService := draftsServicePkg.NewService(draftsRepo, ocrRepo, catalogRepo, accountRepo, supplierRepo, photosRepo, invoicesRepo, rmsFactory, billingService)
- invoicesService := invoicesServicePkg.NewService(invoicesRepo, draftsRepo, supplierRepo, rmsFactory)
+ invoicesService := invoicesServicePkg.NewService(invoicesRepo, draftsRepo, supplierRepo, accountRepo, rmsFactory)
photosService := photosServicePkg.NewService(photosRepo, draftsRepo, accountRepo)
// 7. WebSocket сервер для desktop авторизации
@@ -121,7 +131,7 @@ func main() {
billingHandler := handlers.NewBillingHandler(billingService)
ocrHandler := handlers.NewOCRHandler(ocrService)
photosHandler := handlers.NewPhotosHandler(photosService)
- recommendHandler := handlers.NewRecommendationsHandler(recService)
+ recommendHandler := handlers.NewRecommendationsHandler(recService, accountRepo)
settingsHandler := handlers.NewSettingsHandler(accountRepo, catalogRepo)
settingsHandler.SetRMSFactory(rmsFactory)
invoicesHandler := handlers.NewInvoiceHandler(invoicesService, syncService)
@@ -215,6 +225,7 @@ func main() {
api.GET("/ocr/search", ocrHandler.SearchProducts)
// Invoices
+ api.GET("/invoices/stats", invoicesHandler.GetStats)
api.GET("/invoices/:id", invoicesHandler.GetInvoice)
api.POST("/invoices/sync", invoicesHandler.SyncInvoices)
@@ -225,8 +236,36 @@ func main() {
// Запускаем в горутине, чтобы не держать соединение
go func() {
if err := syncService.SyncAllData(userID, force); err != nil {
- logger.Log.Error("Manual sync failed", zap.Error(err))
+ logger.Log.Error("Manual sync failed",
+ zap.String("user_id", userID.String()),
+ zap.Error(err))
+ return
}
+
+ // Обновляем рекомендации после успешной синхронизации
+ // Получаем активный сервер пользователя
+ server, err := accountRepo.GetActiveServer(userID)
+ if err != nil {
+ logger.Log.Error("Ошибка получения активного сервера для обновления рекомендаций",
+ zap.String("user_id", userID.String()),
+ zap.Error(err))
+ return
+ }
+ if server != nil {
+ if err := recService.RefreshRecommendations(server.ID); err != nil {
+ logger.Log.Error("Ошибка обновления рекомендаций после ручной синхронизации",
+ zap.String("user_id", userID.String()),
+ zap.String("server_id", server.ID.String()),
+ zap.Error(err))
+ }
+ if err := accountRepo.UpdateLastSync(server.ID); err != nil {
+ logger.Log.Error("Ошибка обновления времени синхронизации",
+ zap.String("user_id", userID.String()),
+ zap.String("server_id", server.ID.String()),
+ zap.Error(err))
+ }
+ }
+
}()
c.JSON(200, gin.H{"status": "sync_started", "message": "Синхронизация запущена в фоне"})
})
diff --git a/internal/domain/account/entity.go b/internal/domain/account/entity.go
index 043d795..5be275d 100644
--- a/internal/domain/account/entity.go
+++ b/internal/domain/account/entity.go
@@ -4,6 +4,7 @@ import (
"time"
"github.com/google/uuid"
+ "gorm.io/gorm"
)
// Роли пользователей
@@ -75,6 +76,14 @@ type RMSServer struct {
// Stats
InvoiceCount int `gorm:"default:0" json:"invoice_count"`
+ // Sync settings
+ SyncInterval int `gorm:"default:360" json:"sync_interval"` // Интервал синхронизации в минутах (default: 6 часов)
+ LastSyncAt *time.Time `json:"last_sync_at"` // Время последней успешной синхронизации
+ LastActivityAt *time.Time `json:"last_activity_at"` // Время последнего действия пользователя
+
+ // Soft delete
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -134,4 +143,12 @@ type Repository interface {
// SetMuteDraftNotifications включает/выключает уведомления для пользователя
SetMuteDraftNotifications(userID, serverID uuid.UUID, mute bool) error
+
+ // === Синхронизация и активность ===
+ // UpdateLastActivity обновляет время последней активности пользователя на сервере
+ UpdateLastActivity(serverID uuid.UUID) error
+ // UpdateLastSync обновляет время последней успешной синхронизации
+ UpdateLastSync(serverID uuid.UUID) error
+ // GetServersForSync возвращает серверы, готовые для синхронизации
+ GetServersForSync(idleThreshold time.Duration) ([]RMSServer, error)
}
diff --git a/internal/domain/drafts/entity.go b/internal/domain/drafts/entity.go
index 6f7bdda..2edf795 100644
--- a/internal/domain/drafts/entity.go
+++ b/internal/domain/drafts/entity.go
@@ -82,6 +82,12 @@ type DraftInvoiceItem struct {
IsMatched bool `gorm:"default:false" json:"is_matched"`
}
+// LinkedDraftInfo содержит информацию о связанном черновике
+type LinkedDraftInfo struct {
+ DraftID uuid.UUID
+ PhotoURL string
+}
+
type Repository interface {
Create(draft *DraftInvoice) error
GetByID(id uuid.UUID) (*DraftInvoice, error)
@@ -102,6 +108,6 @@ type Repository interface {
// GetActive возвращает активные черновики для СЕРВЕРА (а не юзера)
GetActive(serverID uuid.UUID) ([]DraftInvoice, error)
- // GetRMSInvoiceIDToPhotoURLMap возвращает мапу rms_invoice_id -> sender_photo_url для сервера, где rms_invoice_id не NULL
- GetRMSInvoiceIDToPhotoURLMap(serverID uuid.UUID) (map[uuid.UUID]string, error)
+ // GetLinkedDraftsMap возвращает мапу rms_invoice_id -> LinkedDraftInfo для сервера, где rms_invoice_id не NULL
+ GetLinkedDraftsMap(serverID uuid.UUID) (map[uuid.UUID]LinkedDraftInfo, error)
}
diff --git a/internal/domain/invoices/entity.go b/internal/domain/invoices/entity.go
index 32ec187..4b280ce 100644
--- a/internal/domain/invoices/entity.go
+++ b/internal/domain/invoices/entity.go
@@ -47,4 +47,5 @@ type Repository interface {
GetByPeriod(serverID uuid.UUID, from, to time.Time) ([]Invoice, error)
SaveInvoices(invoices []Invoice) error
CountRecent(serverID uuid.UUID, days int) (int64, error)
+ GetStats(serverID uuid.UUID) (total int64, lastMonth int64, last24h int64, err error)
}
diff --git a/internal/domain/recommendations/entity.go b/internal/domain/recommendations/entity.go
index 9872280..8598999 100644
--- a/internal/domain/recommendations/entity.go
+++ b/internal/domain/recommendations/entity.go
@@ -19,6 +19,7 @@ const (
// Recommendation - Результат анализа
type Recommendation struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
+ RMSServerID uuid.UUID `gorm:"type:uuid;not null;index"`
Type string `gorm:"type:varchar(50);index"`
ProductID uuid.UUID `gorm:"type:uuid;index"`
ProductName string `gorm:"type:varchar(255)"`
@@ -29,15 +30,15 @@ type Recommendation struct {
// Repository отвечает за аналитические выборки и хранение результатов
type Repository interface {
- // Методы анализа (возвращают список структур, но не пишут в БД)
- FindUnusedGoods() ([]Recommendation, error)
- FindNoIncomingIngredients(days int) ([]Recommendation, error)
- FindStaleGoods(days int) ([]Recommendation, error)
- FindDishesInRecipes() ([]Recommendation, error)
- FindPurchasedButUnused(days int) ([]Recommendation, error)
- FindUsageWithoutPurchase(days int) ([]Recommendation, error)
+ // Методы анализа — добавить serverID
+ FindUnusedGoods(serverID uuid.UUID) ([]Recommendation, error)
+ FindNoIncomingIngredients(serverID uuid.UUID, days int) ([]Recommendation, error)
+ FindStaleGoods(serverID uuid.UUID, days int) ([]Recommendation, error)
+ FindDishesInRecipes(serverID uuid.UUID) ([]Recommendation, error)
+ FindPurchasedButUnused(serverID uuid.UUID, days int) ([]Recommendation, error)
+ FindUsageWithoutPurchase(serverID uuid.UUID, days int) ([]Recommendation, error)
- // Методы "Кэша" в БД
- SaveAll(items []Recommendation) error // Удаляет старые и пишет новые
- GetAll() ([]Recommendation, error)
+ // Методы хранения — добавить serverID
+ SaveAll(serverID uuid.UUID, items []Recommendation) error
+ GetAll(serverID uuid.UUID) ([]Recommendation, error)
}
diff --git a/internal/infrastructure/repository/account/postgres.go b/internal/infrastructure/repository/account/postgres.go
index 155bcdc..9d6a6c1 100644
--- a/internal/infrastructure/repository/account/postgres.go
+++ b/internal/infrastructure/repository/account/postgres.go
@@ -7,8 +7,10 @@ import (
"time"
"rmser/internal/domain/account"
+ "rmser/pkg/logger"
"github.com/google/uuid"
+ "go.uber.org/zap"
"gorm.io/gorm"
)
@@ -78,7 +80,8 @@ func (r *pgRepository) ConnectServer(userID uuid.UUID, rawURL, login, encryptedP
var created bool
err := r.db.Transaction(func(tx *gorm.DB) error {
- err := tx.Where("base_url = ?", cleanURL).First(&server).Error
+ // Сначала ищем среди удаленных серверов
+ err := tx.Unscoped().Where("base_url = ?", cleanURL).First(&server).Error
if err != nil && err != gorm.ErrRecordNotFound {
return err
}
@@ -100,8 +103,17 @@ func (r *pgRepository) ConnectServer(userID uuid.UUID, rawURL, login, encryptedP
return err
}
created = true
+ } else if server.DeletedAt.Valid {
+ // --- СЦЕНАРИЙ 2: ВОССТАНОВЛЕНИЕ УДАЛЕННОГО СЕРВЕРА ---
+ // Восстанавливаем сервер, сохраняя старые значения Balance, InvoiceCount и ID
+ server.Name = name
+ server.DeletedAt = gorm.DeletedAt{} // Сбрасываем deleted_at
+ if err := tx.Save(&server).Error; err != nil {
+ return err
+ }
+ created = true // При восстановлении пользователь становится владельцем
} else {
- // --- СЦЕНАРИЙ 2: СУЩЕСТВУЮЩИЙ СЕРВЕР ---
+ // --- СЦЕНАРИЙ 3: СУЩЕСТВУЮЩИЙ АКТИВНЫЙ СЕРВЕР ---
var userCount int64
tx.Model(&account.ServerUser{}).Where("server_id = ?", server.ID).Count(&userCount)
if userCount >= int64(server.MaxUsers) {
@@ -156,9 +168,92 @@ func (r *pgRepository) SaveServerSettings(server *account.RMSServer) error {
"root_group_guid": server.RootGroupGUID,
"auto_process": server.AutoProcess,
"max_users": server.MaxUsers,
+ "sync_interval": server.SyncInterval,
}).Error
}
+// UpdateLastActivity обновляет время последней активности пользователя
+func (r *pgRepository) UpdateLastActivity(serverID uuid.UUID) error {
+ result := r.db.Model(&account.RMSServer{}).
+ Where("id = ?", serverID).
+ Update("last_activity_at", gorm.Expr("NOW()"))
+
+ if result.Error != nil {
+ logger.Log.Error("Failed to update last_activity_at",
+ zap.String("server_id", serverID.String()),
+ zap.Error(result.Error))
+ return result.Error
+ }
+
+ if result.RowsAffected == 0 {
+ logger.Log.Warn("UpdateLastActivity: server not found",
+ zap.String("server_id", serverID.String()))
+ return fmt.Errorf("сервер не найден")
+ }
+
+ return nil
+}
+
+// UpdateLastSync обновляет время последней успешной синхронизации
+func (r *pgRepository) UpdateLastSync(serverID uuid.UUID) error {
+ result := r.db.Model(&account.RMSServer{}).
+ Where("id = ?", serverID).
+ Update("last_sync_at", gorm.Expr("NOW()"))
+
+ if result.Error != nil {
+ logger.Log.Error("Failed to update last_sync_at",
+ zap.String("server_id", serverID.String()),
+ zap.Error(result.Error))
+ return result.Error
+ }
+
+ if result.RowsAffected == 0 {
+ logger.Log.Warn("UpdateLastSync: server not found",
+ zap.String("server_id", serverID.String()))
+ return fmt.Errorf("сервер не найден")
+ }
+
+ return nil
+}
+
+// GetServersForSync возвращает серверы, готовые для синхронизации
+func (r *pgRepository) GetServersForSync(idleThreshold time.Duration) ([]account.RMSServer, error) {
+ var servers []account.RMSServer
+
+ // Конвертируем duration в минуты для SQL
+ idleMinutes := int(idleThreshold.Minutes())
+
+ query := `
+ SELECT * FROM rms_servers
+ WHERE
+ deleted_at IS NULL
+ AND (
+ -- Случай 1: Настало время периодической синхронизации
+ (EXTRACT(EPOCH FROM (NOW() - COALESCE(last_sync_at, '1970-01-01'::timestamp))) / 60) >= sync_interval
+ OR
+ -- Случай 2: Прошло N мин с последней активности, и активность была ПОЗЖЕ синхронизации
+ (
+ last_activity_at > last_sync_at
+ AND (EXTRACT(EPOCH FROM (NOW() - last_activity_at)) / 60) >= ?
+ )
+ )
+ `
+
+ err := r.db.Raw(query, idleMinutes).Scan(&servers).Error
+ if err != nil {
+ logger.Log.Error("Failed to get servers for sync",
+ zap.Int("idle_threshold_minutes", idleMinutes),
+ zap.Error(err))
+ return nil, err
+ }
+
+ logger.Log.Info("Servers ready for sync",
+ zap.Int("count", len(servers)),
+ zap.Int("idle_threshold_minutes", idleMinutes))
+
+ return servers, nil
+}
+
func (r *pgRepository) SetActiveServer(userID, serverID uuid.UUID) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Проверка доступа
@@ -252,7 +347,7 @@ func (r *pgRepository) GetAllAvailableServers(userID uuid.UUID) ([]account.RMSSe
}
func (r *pgRepository) DeleteServer(serverID uuid.UUID) error {
- // Полное удаление сервера и всех связей
+ // Мягкое удаление сервера и всех связей
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("server_id = ?", serverID).Delete(&account.ServerUser{}).Error; err != nil {
return err
diff --git a/internal/infrastructure/repository/drafts/postgres.go b/internal/infrastructure/repository/drafts/postgres.go
index 8ad1554..c66aaad 100644
--- a/internal/infrastructure/repository/drafts/postgres.go
+++ b/internal/infrastructure/repository/drafts/postgres.go
@@ -160,20 +160,23 @@ func (r *pgRepository) GetActive(serverID uuid.UUID) ([]drafts.DraftInvoice, err
return list, err
}
-func (r *pgRepository) GetRMSInvoiceIDToPhotoURLMap(serverID uuid.UUID) (map[uuid.UUID]string, error) {
+func (r *pgRepository) GetLinkedDraftsMap(serverID uuid.UUID) (map[uuid.UUID]drafts.LinkedDraftInfo, error) {
var draftsList []drafts.DraftInvoice
err := r.db.
- Select("rms_invoice_id", "sender_photo_url").
+ Select("id", "rms_invoice_id", "sender_photo_url").
Where("rms_server_id = ? AND rms_invoice_id IS NOT NULL", serverID).
Find(&draftsList).Error
if err != nil {
return nil, err
}
- result := make(map[uuid.UUID]string)
+ result := make(map[uuid.UUID]drafts.LinkedDraftInfo)
for _, d := range draftsList {
if d.RMSInvoiceID != nil {
- result[*d.RMSInvoiceID] = d.SenderPhotoURL
+ result[*d.RMSInvoiceID] = drafts.LinkedDraftInfo{
+ DraftID: d.ID,
+ PhotoURL: d.SenderPhotoURL,
+ }
}
}
return result, nil
diff --git a/internal/infrastructure/repository/invoices/postgres.go b/internal/infrastructure/repository/invoices/postgres.go
index 4782502..b80c9e4 100644
--- a/internal/infrastructure/repository/invoices/postgres.go
+++ b/internal/infrastructure/repository/invoices/postgres.go
@@ -87,3 +87,17 @@ func (r *pgRepository) CountRecent(serverID uuid.UUID, days int) (int64, error)
Count(&count).Error
return count, err
}
+
+func (r *pgRepository) GetStats(serverID uuid.UUID) (total int64, lastMonth int64, last24h int64, err error) {
+ query := `
+ SELECT
+ COUNT(*) FILTER (WHERE status != 'DELETED') as total,
+ COUNT(*) FILTER (WHERE status != 'DELETED' AND created_at >= NOW() - INTERVAL '1 month') as last_month,
+ COUNT(*) FILTER (WHERE status != 'DELETED' AND created_at >= NOW() - INTERVAL '24 hours') as last_24h
+ FROM invoices
+ WHERE rms_server_id = $1
+ `
+
+ err = r.db.Raw(query, serverID).Row().Scan(&total, &lastMonth, &last24h)
+ return total, lastMonth, last24h, err
+}
diff --git a/internal/infrastructure/repository/recommendations/postgres.go b/internal/infrastructure/repository/recommendations/postgres.go
index c10f294..188f9b0 100644
--- a/internal/infrastructure/repository/recommendations/postgres.go
+++ b/internal/infrastructure/repository/recommendations/postgres.go
@@ -3,12 +3,12 @@ package recommendations
import (
"fmt"
"strconv"
- "time"
+
+ "github.com/google/uuid"
+ "gorm.io/gorm"
"rmser/internal/domain/operations"
"rmser/internal/domain/recommendations"
-
- "gorm.io/gorm"
)
type pgRepository struct {
@@ -21,11 +21,18 @@ func NewRepository(db *gorm.DB) recommendations.Repository {
// --- Методы Хранения ---
-func (r *pgRepository) SaveAll(items []recommendations.Recommendation) error {
+func (r *pgRepository) SaveAll(serverID uuid.UUID, items []recommendations.Recommendation) error {
return r.db.Transaction(func(tx *gorm.DB) error {
- if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Delete(&recommendations.Recommendation{}).Error; err != nil {
+ // Удаляем только записи ЭТОГО сервера
+ if err := tx.Where("rms_server_id = ?", serverID).Delete(&recommendations.Recommendation{}).Error; err != nil {
return err
}
+
+ // Проставляем server_id для всех записей
+ for i := range items {
+ items[i].RMSServerID = serverID
+ }
+
if len(items) > 0 {
if err := tx.CreateInBatches(items, 100).Error; err != nil {
return err
@@ -35,16 +42,16 @@ func (r *pgRepository) SaveAll(items []recommendations.Recommendation) error {
})
}
-func (r *pgRepository) GetAll() ([]recommendations.Recommendation, error) {
+func (r *pgRepository) GetAll(serverID uuid.UUID) ([]recommendations.Recommendation, error) {
var items []recommendations.Recommendation
- err := r.db.Find(&items).Error
+ err := r.db.Where("rms_server_id = ?", serverID).Find(&items).Error
return items, err
}
// --- Методы Аналитики ---
// 1. Товары (GOODS/PREPARED), не используемые в техкартах
-func (r *pgRepository) FindUnusedGoods() ([]recommendations.Recommendation, error) {
+func (r *pgRepository) FindUnusedGoods(serverID uuid.UUID) ([]recommendations.Recommendation, error) {
var results []recommendations.Recommendation
query := `
@@ -54,27 +61,30 @@ func (r *pgRepository) FindUnusedGoods() ([]recommendations.Recommendation, erro
'Товар не используется ни в одной техкарте' as reason,
? as type
FROM products p
- WHERE p.type IN ('GOODS', 'PREPARED')
- AND p.is_deleted = false -- Проверка на удаление
+ WHERE p.rms_server_id = ?
+ AND p.type IN ('GOODS', 'PREPARED')
+ AND p.is_deleted = false
AND p.id NOT IN (
- SELECT DISTINCT product_id FROM recipe_items
+ SELECT DISTINCT ri.product_id FROM recipe_items ri
+ JOIN recipes r ON ri.recipe_id = r.id
+ WHERE r.rms_server_id = ?
)
AND p.id NOT IN (
- SELECT DISTINCT product_id FROM recipes
+ SELECT DISTINCT r.product_id FROM recipes r
+ WHERE r.rms_server_id = ?
)
ORDER BY p.name ASC
`
- if err := r.db.Raw(query, recommendations.TypeUnused).Scan(&results).Error; err != nil {
+ if err := r.db.Raw(query, recommendations.TypeUnused, serverID, serverID, serverID).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// 2. Закупается, но нет в техкартах
-func (r *pgRepository) FindPurchasedButUnused(days int) ([]recommendations.Recommendation, error) {
+func (r *pgRepository) FindPurchasedButUnused(serverID uuid.UUID, days int) ([]recommendations.Recommendation, error) {
var results []recommendations.Recommendation
- dateFrom := time.Now().AddDate(0, 0, -days)
query := `
SELECT DISTINCT
@@ -84,26 +94,33 @@ func (r *pgRepository) FindPurchasedButUnused(days int) ([]recommendations.Recom
? as type
FROM store_operations so
JOIN products p ON so.product_id = p.id
- WHERE
- so.op_type = ?
- AND so.period_from >= ?
- AND p.is_deleted = false -- Проверка на удаление
- AND p.id NOT IN (
- SELECT DISTINCT product_id FROM recipe_items
+ WHERE
+ so.rms_server_id = ?
+ AND so.op_type = ?
+ AND so.period_to >= CURRENT_DATE - INTERVAL '1 day'
+ AND p.is_deleted = false
+ AND p.id NOT IN (
+ SELECT DISTINCT ri.product_id FROM recipe_items ri
+ JOIN recipes r ON ri.recipe_id = r.id
+ WHERE r.rms_server_id = ?
)
ORDER BY p.name ASC
`
- if err := r.db.Raw(query, recommendations.TypePurchasedButUnused, operations.OpTypePurchase, dateFrom).Scan(&results).Error; err != nil {
+ if err := r.db.Raw(query,
+ recommendations.TypePurchasedButUnused,
+ serverID,
+ operations.OpTypePurchase,
+ serverID,
+ ).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// 3. Ингредиенты в актуальных техкартах без закупок
-func (r *pgRepository) FindNoIncomingIngredients(days int) ([]recommendations.Recommendation, error) {
+func (r *pgRepository) FindNoIncomingIngredients(serverID uuid.UUID, days int) ([]recommendations.Recommendation, error) {
var results []recommendations.Recommendation
- dateFrom := time.Now().AddDate(0, 0, -days)
query := `
SELECT
@@ -115,31 +132,38 @@ func (r *pgRepository) FindNoIncomingIngredients(days int) ([]recommendations.Re
JOIN recipes r ON ri.recipe_id = r.id
JOIN products p ON ri.product_id = p.id
JOIN products parent ON r.product_id = parent.id
- WHERE
- (r.date_to IS NULL OR r.date_to >= CURRENT_DATE)
+ WHERE
+ r.rms_server_id = ?
+ AND (r.date_to IS NULL OR r.date_to >= CURRENT_DATE)
AND p.type = 'GOODS'
- AND p.is_deleted = false -- Сам ингредиент не удален
- AND parent.is_deleted = false -- Блюдо, в которое он входит, не удалено
+ AND p.is_deleted = false
+ AND parent.is_deleted = false
AND p.id NOT IN (
- SELECT product_id
- FROM store_operations
- WHERE op_type = ?
- AND period_from >= ?
+ SELECT so.product_id
+ FROM store_operations so
+ WHERE so.rms_server_id = ?
+ AND so.op_type = ?
+ AND so.period_to >= CURRENT_DATE - INTERVAL '1 day'
)
GROUP BY p.id, p.name
ORDER BY p.name ASC
`
- if err := r.db.Raw(query, strconv.Itoa(days), recommendations.TypeNoIncoming, operations.OpTypePurchase, dateFrom).Scan(&results).Error; err != nil {
+ if err := r.db.Raw(query,
+ strconv.Itoa(days),
+ recommendations.TypeNoIncoming,
+ serverID,
+ serverID,
+ operations.OpTypePurchase,
+ ).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// 4. Товары, которые закупаем, но не расходуем ("Висяки")
-func (r *pgRepository) FindStaleGoods(days int) ([]recommendations.Recommendation, error) {
+func (r *pgRepository) FindStaleGoods(serverID uuid.UUID, days int) ([]recommendations.Recommendation, error) {
var results []recommendations.Recommendation
- dateFrom := time.Now().AddDate(0, 0, -days)
query := `
SELECT DISTINCT
@@ -149,30 +173,38 @@ func (r *pgRepository) FindStaleGoods(days int) ([]recommendations.Recommendatio
? as type
FROM store_operations so
JOIN products p ON so.product_id = p.id
- WHERE
- so.op_type = ?
- AND so.period_from >= ?
- AND p.is_deleted = false -- Проверка на удаление
- AND p.id NOT IN (
- SELECT product_id
- FROM store_operations
- WHERE op_type = ?
- AND period_from >= ?
+ WHERE
+ so.rms_server_id = ?
+ AND so.op_type = ?
+ AND so.period_to >= CURRENT_DATE - INTERVAL '1 day'
+ AND p.is_deleted = false
+ AND p.id NOT IN (
+ SELECT so2.product_id
+ FROM store_operations so2
+ WHERE so2.rms_server_id = ?
+ AND so2.op_type = ?
+ AND so2.period_to >= CURRENT_DATE - INTERVAL '1 day'
)
ORDER BY p.name ASC
`
reason := fmt.Sprintf("Были закупки, но нет расхода за %d дн.", days)
- if err := r.db.Raw(query, reason, recommendations.TypeStale, operations.OpTypePurchase, dateFrom, operations.OpTypeUsage, dateFrom).
- Scan(&results).Error; err != nil {
+ if err := r.db.Raw(query,
+ reason,
+ recommendations.TypeStale,
+ serverID,
+ operations.OpTypePurchase,
+ serverID,
+ operations.OpTypeUsage,
+ ).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// 5. Блюдо используется в техкарте другого блюда
-func (r *pgRepository) FindDishesInRecipes() ([]recommendations.Recommendation, error) {
+func (r *pgRepository) FindDishesInRecipes(serverID uuid.UUID) ([]recommendations.Recommendation, error) {
var results []recommendations.Recommendation
query := `
@@ -186,23 +218,23 @@ func (r *pgRepository) FindDishesInRecipes() ([]recommendations.Recommendation,
JOIN recipes r ON ri.recipe_id = r.id
JOIN products parent ON r.product_id = parent.id
WHERE
- child.type = 'DISH'
- AND child.is_deleted = false -- Вложенное блюдо не удалено
- AND parent.is_deleted = false -- Родительское блюдо не удалено
+ r.rms_server_id = ?
+ AND child.type = 'DISH'
+ AND child.is_deleted = false
+ AND parent.is_deleted = false
AND (r.date_to IS NULL OR r.date_to >= CURRENT_DATE)
ORDER BY child.name ASC
`
- if err := r.db.Raw(query, recommendations.TypeDishInRecipe).Scan(&results).Error; err != nil {
+ if err := r.db.Raw(query, recommendations.TypeDishInRecipe, serverID).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// 6. Есть расход (Usage), но нет прихода (Purchase)
-func (r *pgRepository) FindUsageWithoutPurchase(days int) ([]recommendations.Recommendation, error) {
+func (r *pgRepository) FindUsageWithoutPurchase(serverID uuid.UUID, days int) ([]recommendations.Recommendation, error) {
var results []recommendations.Recommendation
- dateFrom := time.Now().AddDate(0, 0, -days)
query := `
SELECT DISTINCT
@@ -212,30 +244,31 @@ func (r *pgRepository) FindUsageWithoutPurchase(days int) ([]recommendations.Rec
? as type
FROM store_operations so
JOIN products p ON so.product_id = p.id
- WHERE
- so.op_type = ? -- Есть расход (продажа/списание)
- AND so.period_from >= ?
- AND p.type = 'GOODS' -- Только для товаров
- AND p.is_deleted = false -- Товар жив
- AND p.id NOT IN ( -- Но не было закупок
- SELECT product_id
- FROM store_operations
- WHERE op_type = ?
- AND period_from >= ?
+ WHERE
+ so.rms_server_id = ?
+ AND so.op_type = ?
+ AND so.period_to >= CURRENT_DATE - INTERVAL '1 day'
+ AND p.type = 'GOODS'
+ AND p.is_deleted = false
+ AND p.id NOT IN (
+ SELECT so2.product_id
+ FROM store_operations so2
+ WHERE so2.rms_server_id = ?
+ AND so2.op_type = ?
+ AND so2.period_to >= CURRENT_DATE - INTERVAL '1 day'
)
ORDER BY p.name ASC
`
reason := fmt.Sprintf("Товар расходуется (продажи/списания), но не закупался последние %d дн.", days)
- // Аргументы: reason, type, OpUsage, date, OpPurchase, date
if err := r.db.Raw(query,
reason,
recommendations.TypeUsageNoIncoming,
+ serverID,
operations.OpTypeUsage,
- dateFrom,
+ serverID,
operations.OpTypePurchase,
- dateFrom,
).Scan(&results).Error; err != nil {
return nil, err
}
diff --git a/internal/infrastructure/rms/client.go b/internal/infrastructure/rms/client.go
index e91c0bc..041150b 100644
--- a/internal/infrastructure/rms/client.go
+++ b/internal/infrastructure/rms/client.go
@@ -40,6 +40,7 @@ type ClientI interface {
FetchInvoices(from, to time.Time) ([]invoices.Invoice, error)
FetchStoreOperations(presetID string, from, to time.Time) ([]StoreReportItemXML, error)
CreateIncomingInvoice(inv invoices.Invoice) (string, error)
+ UnprocessIncomingInvoice(inv invoices.Invoice) error
GetProductByID(id uuid.UUID) (*ProductFullDTO, error)
UpdateProduct(product ProductFullDTO) (*ProductFullDTO, error)
}
@@ -555,9 +556,24 @@ func (c *Client) FetchStoreOperations(presetID string, from, to time.Time) ([]St
return report.Items, nil
}
-// CreateIncomingInvoice отправляет накладную в iiko
-func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) {
- // 1. Маппинг Domain -> XML DTO
+// buildInvoiceXML формирует XML payload для накладной на основе доменной сущности
+func (c *Client) buildInvoiceXML(inv invoices.Invoice) ([]byte, error) {
+ // Защита от паники с recover
+ var panicErr error
+ defer func() {
+ if r := recover(); r != nil {
+ logger.Log.Error("Паника в buildInvoiceXML",
+ zap.Any("panic", r),
+ zap.Stack("stack"),
+ )
+ panicErr = fmt.Errorf("panic recovered: %v", r)
+ }
+ }()
+ if panicErr != nil {
+ return nil, panicErr
+ }
+
+ // Маппинг Domain -> XML DTO
// Статус по умолчанию NEW, если не передан
status := inv.Status
@@ -592,7 +608,21 @@ func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) {
reqDTO.ID = inv.ID.String()
}
+ // Логирование перед циклом по Items
+ logger.Log.Debug("Начинаем формирование XML для позиций накладной",
+ zap.Int("items_count", len(inv.Items)),
+ )
+
for i, item := range inv.Items {
+ // Проверка что продукт загружен (по полю ID)
+ if item.Product.ID == uuid.Nil {
+ logger.Log.Warn("Пропуск позиции: Product не загружен",
+ zap.String("product_id", item.ProductID.String()),
+ zap.Int("index", i),
+ )
+ continue
+ }
+
amount, _ := item.Amount.Float64()
price, _ := item.Price.Float64()
sum, _ := item.Sum.Float64()
@@ -610,18 +640,47 @@ func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) {
xmlItem.ContainerId = item.ContainerID.String()
}
+ // Проверка MainUnitID перед обращением
+ if item.Product.MainUnitID != nil {
+ xmlItem.AmountUnit = item.Product.MainUnitID.String()
+ }
+
+ // Логирование каждого добавленного item
+ logger.Log.Debug("Добавление позиции в XML",
+ zap.String("product_id", item.ProductID.String()),
+ zap.Float64("amount", amount),
+ zap.String("product_name", item.Product.Name),
+ )
+
reqDTO.ItemsWrapper.Items = append(reqDTO.ItemsWrapper.Items, xmlItem)
}
- // 2. Маршалинг в XML
+ // Маршалинг в XML
xmlBytes, err := xml.Marshal(reqDTO)
if err != nil {
- return "", fmt.Errorf("xml marshal error: %w", err)
+ return nil, fmt.Errorf("xml marshal error: %w", err)
}
// Добавляем XML header вручную
xmlPayload := []byte(xml.Header + string(xmlBytes))
- // 3. Получение токена
+ // Логирование XML перед отправкой
+ logger.Log.Debug("XML payload подготовлен",
+ zap.String("xml_payload", string(xmlPayload)),
+ zap.Int("payload_size", len(xmlPayload)),
+ )
+
+ return xmlPayload, nil
+}
+
+// CreateIncomingInvoice отправляет накладную в iiko
+func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) {
+ // 1. Формирование XML payload
+ xmlPayload, err := c.buildInvoiceXML(inv)
+ if err != nil {
+ return "", fmt.Errorf("ошибка формирования XML: %w", err)
+ }
+
+ // 2. Получение токена
if err := c.ensureToken(); err != nil {
return "", err
}
@@ -630,7 +689,7 @@ func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) {
token := c.token
c.mu.RUnlock()
- // 4. Формирование URL
+ // 3. Формирование URL
endpoint, _ := url.Parse(c.baseURL + "/resto/api/documents/import/incomingInvoice")
q := endpoint.Query()
q.Set("key", token)
@@ -646,7 +705,7 @@ func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) {
zap.String("body_payload", string(xmlPayload)),
)
- // 5. Отправка
+ // 4. Отправка
req, err := http.NewRequest("POST", fullURL, bytes.NewReader(xmlPayload))
if err != nil {
return "", err
@@ -666,9 +725,9 @@ func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) {
}
// Логируем ответ для симметрии
- logger.Log.Info("RMS POST Response Debug",
+ logger.Log.Debug("Получен ответ от iiko",
zap.Int("status_code", resp.StatusCode),
- zap.String("response_body", string(respBody)),
+ zap.String("raw_response", string(respBody)),
)
if resp.StatusCode != http.StatusOK {
@@ -691,6 +750,89 @@ func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) {
return result.DocumentNumber, nil
}
+// UnprocessIncomingInvoice выполняет распроведение накладной в iiko
+func (c *Client) UnprocessIncomingInvoice(inv invoices.Invoice) error {
+ // 1. Формирование XML payload
+ xmlPayload, err := c.buildInvoiceXML(inv)
+ if err != nil {
+ return fmt.Errorf("ошибка формирования XML: %w", err)
+ }
+
+ // 2. Получение токена
+ if err := c.ensureToken(); err != nil {
+ return err
+ }
+
+ c.mu.RLock()
+ token := c.token
+ c.mu.RUnlock()
+
+ // 3. Формирование URL
+ endpoint, _ := url.Parse(c.baseURL + "/resto/api/documents/unprocess/incomingInvoice")
+ q := endpoint.Query()
+ q.Set("key", token)
+ endpoint.RawQuery = q.Encode()
+
+ fullURL := endpoint.String()
+
+ // Логирование запроса
+ logger.Log.Info("RMS Unprocess Request",
+ zap.String("method", "POST"),
+ zap.String("url", fullURL),
+ zap.String("document_number", inv.DocumentNumber),
+ zap.String("invoice_id", inv.ID.String()),
+ )
+
+ // 4. Отправка POST запроса
+ req, err := http.NewRequest("POST", fullURL, bytes.NewReader(xmlPayload))
+ if err != nil {
+ return fmt.Errorf("ошибка создания запроса: %w", err)
+ }
+ req.Header.Set("Content-Type", "application/xml")
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return fmt.Errorf("ошибка сети: %w", err)
+ }
+ defer resp.Body.Close()
+
+ // Читаем ответ
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return fmt.Errorf("ошибка чтения ответа: %w", err)
+ }
+
+ // Логируем ответ
+ logger.Log.Debug("Получен ответ от iiko на распроведение",
+ zap.Int("status_code", resp.StatusCode),
+ zap.String("raw_response", string(respBody)),
+ )
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("http error %d: %s", resp.StatusCode, string(respBody))
+ }
+
+ // Проверка результата валидации
+ var result DocumentValidationResult
+ if err := xml.Unmarshal(respBody, &result); err != nil {
+ return fmt.Errorf("ошибка разбора XML ответа: %w", err)
+ }
+
+ if !result.Valid {
+ logger.Log.Warn("RMS Invoice Unprocess Failed",
+ zap.String("error", result.ErrorMessage),
+ zap.String("additional", result.AdditionalInfo),
+ )
+ return fmt.Errorf("распроведение не удалось: %s (info: %s)", result.ErrorMessage, result.AdditionalInfo)
+ }
+
+ logger.Log.Info("RMS Invoice Unprocess Success",
+ zap.String("document_number", result.DocumentNumber),
+ )
+
+ return nil
+}
+
// GetProductByID получает полную структуру товара по ID (через /list?ids=...)
func (c *Client) GetProductByID(id uuid.UUID) (*ProductFullDTO, error) {
// Параметр ids должен быть списком. iiko ожидает ids=UUID
diff --git a/internal/services/drafts/service.go b/internal/services/drafts/service.go
index 16170f5..dfae425 100644
--- a/internal/services/drafts/service.go
+++ b/internal/services/drafts/service.go
@@ -157,9 +157,6 @@ func (s *Service) UpdateDraftHeader(id uuid.UUID, storeID *uuid.UUID, supplierID
if err != nil {
return err
}
- if draft.Status == drafts.StatusCompleted {
- return errors.New("черновик уже отправлен")
- }
draft.StoreID = storeID
draft.SupplierID = supplierID
draft.DateIncoming = &date
@@ -227,7 +224,7 @@ func (s *Service) DeleteItem(draftID, itemID uuid.UUID) (float64, error) {
return sumFloat, nil
}
-// RecalculateItemFields - логика пересчета Qty/Price/Sum
+// RecalculateItemFields - логика пересчета Q->P->S->Q (Quantity -> Price -> Sum -> Quantity) с использованием decimal для точности
func (s *Service) RecalculateItemFields(item *drafts.DraftInvoiceItem, editedField drafts.EditedField) {
if item.LastEditedField1 != editedField {
item.LastEditedField2 = item.LastEditedField1
@@ -265,6 +262,29 @@ func (s *Service) RecalculateItemFields(item *drafts.DraftInvoiceItem, editedFie
case drafts.FieldSum:
item.Sum = item.Quantity.Mul(item.Price)
}
+
+ // Дополнительная проверка для гарантии консистентности всех полей (Q->P->S->Q)
+ // Используется только для обеспечения точности, не влияет на логику выбора пересчитываемого поля
+ if !item.Price.IsZero() && !item.Quantity.IsZero() {
+ calculatedSum := item.Quantity.Mul(item.Price)
+ if !calculatedSum.Equal(item.Sum) {
+ item.Sum = calculatedSum
+ }
+ }
+
+ if !item.Price.IsZero() && !item.Sum.IsZero() {
+ calculatedQuantity := item.Sum.Div(item.Price)
+ if !calculatedQuantity.Equal(item.Quantity) {
+ item.Quantity = calculatedQuantity
+ }
+ }
+
+ if !item.Quantity.IsZero() && !item.Sum.IsZero() {
+ calculatedPrice := item.Sum.Div(item.Quantity)
+ if !calculatedPrice.Equal(item.Price) {
+ item.Price = calculatedPrice
+ }
+ }
}
// UpdateItem обновлен для поддержки динамического пересчета
@@ -293,17 +313,10 @@ func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, co
}
}
- field := drafts.EditedField(editedField)
- switch field {
- case drafts.FieldQuantity:
- currentItem.Quantity = qty
- case drafts.FieldPrice:
- currentItem.Price = price
- case drafts.FieldSum:
- currentItem.Sum = sum
- }
-
- s.RecalculateItemFields(currentItem, field)
+ // Просто присваиваем значения от фронтенда без пересчета
+ currentItem.Quantity = qty
+ currentItem.Price = price
+ currentItem.Sum = sum
if draft.Status == drafts.StatusCanceled {
draft.Status = drafts.StatusReadyToVerify
@@ -311,20 +324,18 @@ func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, co
}
updates := map[string]interface{}{
- "product_id": currentItem.ProductID,
- "container_id": currentItem.ContainerID,
- "quantity": currentItem.Quantity,
- "price": currentItem.Price,
- "sum": currentItem.Sum,
- "last_edited_field1": currentItem.LastEditedField1,
- "last_edited_field2": currentItem.LastEditedField2,
- "is_matched": currentItem.IsMatched,
+ "product_id": currentItem.ProductID,
+ "container_id": currentItem.ContainerID,
+ "quantity": currentItem.Quantity,
+ "price": currentItem.Price,
+ "sum": currentItem.Sum,
+ "is_matched": currentItem.IsMatched,
}
return s.draftRepo.UpdateItem(itemID, updates)
}
-func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
+func (s *Service) CommitDraft(draftID, userID uuid.UUID, isProcessed bool) (string, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil {
return "", fmt.Errorf("active server not found: %w", err)
@@ -347,17 +358,13 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
return "", errors.New("черновик принадлежит другому серверу")
}
- if draft.Status == drafts.StatusCompleted {
- return "", errors.New("накладная уже отправлена")
- }
-
client, err := s.rmsFactory.GetClientForUser(userID)
if err != nil {
return "", err
}
targetStatus := "NEW"
- if server.AutoProcess {
+ if isProcessed {
targetStatus = "PROCESSED"
}
@@ -373,6 +380,11 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
Items: make([]invoices.InvoiceItem, 0, len(draft.Items)),
}
+ // Если черновик уже был отправлен ранее, передаем RMSInvoiceID для обновления
+ if draft.RMSInvoiceID != nil {
+ inv.ID = *draft.RMSInvoiceID
+ }
+
for _, dItem := range draft.Items {
if dItem.ProductID == nil {
continue
@@ -405,6 +417,12 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
Price: priceToSend,
Sum: sum,
ContainerID: dItem.ContainerID,
+ Product: func() catalog.Product {
+ if dItem.Product != nil {
+ return *dItem.Product
+ }
+ return catalog.Product{}
+ }(),
}
inv.Items = append(inv.Items, invItem)
}
@@ -415,7 +433,22 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
docNum, err := client.CreateIncomingInvoice(inv)
if err != nil {
- return "", err
+ // Если накладная уже проведена, пробуем распровести и повторить
+ if strings.Contains(err.Error(), "Changing processed") {
+ logger.Log.Info("Накладная проведена, выполняю распроведение...", zap.String("doc_num", draft.DocumentNumber))
+
+ if unprocessErr := client.UnprocessIncomingInvoice(inv); unprocessErr != nil {
+ return "", fmt.Errorf("не удалось распровести накладную: %w", unprocessErr)
+ }
+
+ // Повторяем попытку создания накладной после распроведения
+ docNum, err = client.CreateIncomingInvoice(inv)
+ if err != nil {
+ return "", err
+ }
+ } else {
+ return "", err
+ }
}
invoices, err := client.FetchInvoices(*draft.DateIncoming, *draft.DateIncoming)
@@ -434,6 +467,7 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
for _, invoice := range invoices {
if invoice.DocumentNumber == docNum {
draft.RMSInvoiceID = &invoice.ID
+ draft.DocumentNumber = invoice.DocumentNumber
found = true
break
}
@@ -555,19 +589,20 @@ func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID,
}
type UnifiedInvoiceDTO struct {
- ID uuid.UUID `json:"id"`
- Type string `json:"type"`
- DocumentNumber string `json:"document_number"`
- IncomingNumber string `json:"incoming_number"`
- DateIncoming time.Time `json:"date_incoming"`
- Status string `json:"status"`
- TotalSum float64 `json:"total_sum"`
- StoreName string `json:"store_name"`
- ItemsCount int `json:"items_count"`
- CreatedAt time.Time `json:"created_at"`
- IsAppCreated bool `json:"is_app_created"`
- PhotoURL string `json:"photo_url"`
- ItemsPreview string `json:"items_preview"`
+ ID uuid.UUID `json:"id"`
+ Type string `json:"type"`
+ DocumentNumber string `json:"document_number"`
+ IncomingNumber string `json:"incoming_number"`
+ DateIncoming time.Time `json:"date_incoming"`
+ Status string `json:"status"`
+ TotalSum float64 `json:"total_sum"`
+ StoreName string `json:"store_name"`
+ ItemsCount int `json:"items_count"`
+ CreatedAt time.Time `json:"created_at"`
+ IsAppCreated bool `json:"is_app_created"`
+ PhotoURL string `json:"photo_url"`
+ ItemsPreview string `json:"items_preview"`
+ DraftID *uuid.UUID `json:"draft_id,omitempty"` // ID черновика для SYNCED накладных
}
func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]UnifiedInvoiceDTO, error) {
@@ -586,7 +621,7 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
return nil, err
}
- photoMap, err := s.draftRepo.GetRMSInvoiceIDToPhotoURLMap(server.ID)
+ linkedDraftsMap, err := s.draftRepo.GetLinkedDraftsMap(server.ID)
if err != nil {
return nil, err
}
@@ -647,9 +682,11 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
isAppCreated := false
photoURL := ""
- if url, exists := photoMap[inv.ID]; exists {
+ var draftID *uuid.UUID
+ if linkedInfo, exists := linkedDraftsMap[inv.ID]; exists {
isAppCreated = true
- photoURL = url
+ photoURL = linkedInfo.PhotoURL
+ draftID = &linkedInfo.DraftID
}
var itemsPreview string
@@ -679,6 +716,7 @@ func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]Unifie
IsAppCreated: isAppCreated,
PhotoURL: photoURL,
ItemsPreview: itemsPreview,
+ DraftID: draftID,
})
}
@@ -739,6 +777,14 @@ func (s *Service) CreateDraft(userID uuid.UUID) (*drafts.DraftInvoice, error) {
return nil, err
}
+ // Обновляем время последней активности сервера
+ if err := s.accountRepo.UpdateLastActivity(server.ID); err != nil {
+ logger.Log.Warn("Не удалось обновить время активности",
+ zap.String("server_id", server.ID.String()),
+ zap.Error(err))
+ // Не возвращаем ошибку - это некритично
+ }
+
return draft, nil
}
@@ -967,11 +1013,6 @@ func (s *Service) SaveDraftFull(draftID, userID uuid.UUID, req drafts.UpdateDraf
return err
}
- // Проверяем, что черновик не завершен
- if draft.Status == drafts.StatusCompleted {
- return errors.New("черновик уже отправлен")
- }
-
// Обновляем шапку черновика, если переданы поля
headerUpdated := false
@@ -1084,54 +1125,17 @@ func (s *Service) SaveDraftFull(draftID, userID uuid.UUID, req drafts.UpdateDraf
}
}
- // Определяем, какое поле редактируется
- editedField := itemReq.EditedField
- if editedField == "" {
- if itemReq.Sum != nil {
- editedField = "sum"
- } else if itemReq.Price != nil {
- editedField = "price"
- } else if itemReq.Quantity != nil {
- editedField = "quantity"
- }
- }
-
- // Обновляем числовые поля
- qty := decimal.Zero
+ // Просто присваиваем значения от фронтенда без пересчета
if itemReq.Quantity != nil {
- qty = decimal.NewFromFloat(*itemReq.Quantity)
- } else {
- qty = currentItem.Quantity
+ currentItem.Quantity = decimal.NewFromFloat(*itemReq.Quantity)
}
-
- price := decimal.Zero
if itemReq.Price != nil {
- price = decimal.NewFromFloat(*itemReq.Price)
- } else {
- price = currentItem.Price
+ currentItem.Price = decimal.NewFromFloat(*itemReq.Price)
}
-
- sum := decimal.Zero
if itemReq.Sum != nil {
- sum = decimal.NewFromFloat(*itemReq.Sum)
- } else {
- sum = currentItem.Sum
+ currentItem.Sum = decimal.NewFromFloat(*itemReq.Sum)
}
- // Применяем изменения в зависимости от редактируемого поля
- field := drafts.EditedField(editedField)
- switch field {
- case drafts.FieldQuantity:
- currentItem.Quantity = qty
- case drafts.FieldPrice:
- currentItem.Price = price
- case drafts.FieldSum:
- currentItem.Sum = sum
- }
-
- // Пересчитываем поля
- s.RecalculateItemFields(currentItem, field)
-
// Обновляем статус черновика, если он был отменен
if draft.Status == drafts.StatusCanceled {
draft.Status = drafts.StatusReadyToVerify
@@ -1142,14 +1146,12 @@ func (s *Service) SaveDraftFull(draftID, userID uuid.UUID, req drafts.UpdateDraf
// Сохраняем обновленную позицию
updates := map[string]interface{}{
- "product_id": currentItem.ProductID,
- "container_id": currentItem.ContainerID,
- "quantity": currentItem.Quantity,
- "price": currentItem.Price,
- "sum": currentItem.Sum,
- "last_edited_field1": currentItem.LastEditedField1,
- "last_edited_field2": currentItem.LastEditedField2,
- "is_matched": currentItem.IsMatched,
+ "product_id": currentItem.ProductID,
+ "container_id": currentItem.ContainerID,
+ "quantity": currentItem.Quantity,
+ "price": currentItem.Price,
+ "sum": currentItem.Sum,
+ "is_matched": currentItem.IsMatched,
}
if err := s.draftRepo.UpdateItem(itemID, updates); err != nil {
@@ -1158,5 +1160,13 @@ func (s *Service) SaveDraftFull(draftID, userID uuid.UUID, req drafts.UpdateDraf
}
}
+ // Обновляем время последней активности сервера
+ if err := s.accountRepo.UpdateLastActivity(draft.RMSServerID); err != nil {
+ logger.Log.Warn("Не удалось обновить время активности",
+ zap.String("server_id", draft.RMSServerID.String()),
+ zap.Error(err))
+ // Не возвращаем ошибку - это некритично
+ }
+
return nil
}
diff --git a/internal/services/invoices/service.go b/internal/services/invoices/service.go
index b1dd549..0d2e1b7 100644
--- a/internal/services/invoices/service.go
+++ b/internal/services/invoices/service.go
@@ -8,6 +8,7 @@ import (
"github.com/shopspring/decimal"
"go.uber.org/zap"
+ "rmser/internal/domain/account"
"rmser/internal/domain/drafts"
invDomain "rmser/internal/domain/invoices"
"rmser/internal/domain/suppliers"
@@ -19,16 +20,18 @@ type Service struct {
repo invDomain.Repository
draftsRepo drafts.Repository
supplierRepo suppliers.Repository
+ accountRepo account.Repository
rmsFactory *rms.Factory
// Здесь можно добавить репозитории каталога и контрагентов для валидации,
// но для краткости пока опустим глубокую валидацию.
}
-func NewService(repo invDomain.Repository, draftsRepo drafts.Repository, supplierRepo suppliers.Repository, rmsFactory *rms.Factory) *Service {
+func NewService(repo invDomain.Repository, draftsRepo drafts.Repository, supplierRepo suppliers.Repository, accountRepo account.Repository, rmsFactory *rms.Factory) *Service {
return &Service{
repo: repo,
draftsRepo: draftsRepo,
supplierRepo: supplierRepo,
+ accountRepo: accountRepo,
rmsFactory: rmsFactory,
}
}
@@ -99,6 +102,13 @@ func (s *Service) SendInvoiceToRMS(req CreateRequestDTO, userID uuid.UUID) (stri
return docNum, nil
}
+// InvoiceStatsDTO - DTO для статистики накладных
+type InvoiceStatsDTO struct {
+ Total int64 `json:"total"`
+ LastMonth int64 `json:"last_month"`
+ Last24h int64 `json:"last_24h"`
+}
+
// InvoiceDetailsDTO - DTO для ответа на запрос деталей накладной
type InvoiceDetailsDTO struct {
ID uuid.UUID `json:"id"`
@@ -145,7 +155,7 @@ func (s *Service) GetInvoice(id uuid.UUID) (*InvoiceDetailsDTO, error) {
Number: inv.DocumentNumber,
Date: inv.DateIncoming.Format("2006-01-02"),
Status: "COMPLETED", // Для синхронизированных накладных статус всегда COMPLETED
- Items: make([]struct {
+ Items: make([]struct {
Name string `json:"name"`
Quantity float64 `json:"quantity"`
Price float64 `json:"price"`
@@ -166,3 +176,32 @@ func (s *Service) GetInvoice(id uuid.UUID) (*InvoiceDetailsDTO, error) {
return dto, nil
}
+
+// GetStats возвращает статистику по накладным для пользователя
+func (s *Service) GetStats(userID uuid.UUID) (*InvoiceStatsDTO, error) {
+ // Получаем активный сервер пользователя
+ server, err := s.accountRepo.GetActiveServer(userID)
+ if err != nil {
+ return nil, fmt.Errorf("ошибка получения активного сервера: %w", err)
+ }
+
+ if server == nil {
+ return &InvoiceStatsDTO{
+ Total: 0,
+ LastMonth: 0,
+ Last24h: 0,
+ }, nil
+ }
+
+ // Получаем статистику из репозитория
+ total, lastMonth, last24h, err := s.repo.GetStats(server.ID)
+ if err != nil {
+ return nil, fmt.Errorf("ошибка получения статистики: %w", err)
+ }
+
+ return &InvoiceStatsDTO{
+ Total: total,
+ LastMonth: lastMonth,
+ Last24h: last24h,
+ }, nil
+}
diff --git a/internal/services/recommend/service.go b/internal/services/recommend/service.go
index 5157e2f..d50f5b2 100644
--- a/internal/services/recommend/service.go
+++ b/internal/services/recommend/service.go
@@ -1,6 +1,7 @@
package recommend
import (
+ "github.com/google/uuid"
"go.uber.org/zap"
"rmser/internal/domain/recommendations"
@@ -20,56 +21,56 @@ func NewService(repo recommendations.Repository) *Service {
return &Service{repo: repo}
}
-// RefreshRecommendations выполняет анализ и сохраняет результаты в БД
-func (s *Service) RefreshRecommendations() error {
- logger.Log.Info("Запуск пересчета рекомендаций...")
+// RefreshRecommendations выполняет анализ и сохраняет результаты в БД для конкретного сервера
+func (s *Service) RefreshRecommendations(serverID uuid.UUID) error {
+ logger.Log.Info("Запуск пересчета рекомендаций...", zap.String("server_id", serverID.String()))
var all []recommendations.Recommendation
// 1. Unused
- if unused, err := s.repo.FindUnusedGoods(); err == nil {
+ if unused, err := s.repo.FindUnusedGoods(serverID); err == nil {
all = append(all, unused...)
} else {
logger.Log.Error("Ошибка unused", zap.Error(err))
}
// 2. Purchased but Unused
- if purchUnused, err := s.repo.FindPurchasedButUnused(AnalyzeDaysNoIncoming); err == nil {
+ if purchUnused, err := s.repo.FindPurchasedButUnused(serverID, AnalyzeDaysNoIncoming); err == nil {
all = append(all, purchUnused...)
} else {
logger.Log.Error("Ошибка purchased_unused", zap.Error(err))
}
// 3. No Incoming (Ингредиенты без закупок)
- if noInc, err := s.repo.FindNoIncomingIngredients(AnalyzeDaysNoIncoming); err == nil {
+ if noInc, err := s.repo.FindNoIncomingIngredients(serverID, AnalyzeDaysNoIncoming); err == nil {
all = append(all, noInc...)
} else {
logger.Log.Error("Ошибка no_incoming", zap.Error(err))
}
- // 4. Usage without Purchase (Расход без прихода) <-- НОВОЕ
- if usageNoPurch, err := s.repo.FindUsageWithoutPurchase(AnalyzeDaysNoIncoming); err == nil {
+ // 4. Usage without Purchase (Расход без прихода)
+ if usageNoPurch, err := s.repo.FindUsageWithoutPurchase(serverID, AnalyzeDaysNoIncoming); err == nil {
all = append(all, usageNoPurch...)
} else {
logger.Log.Error("Ошибка usage_no_purchase", zap.Error(err))
}
// 5. Stale (Неликвид)
- if stale, err := s.repo.FindStaleGoods(AnalyzeDaysStale); err == nil {
+ if stale, err := s.repo.FindStaleGoods(serverID, AnalyzeDaysStale); err == nil {
all = append(all, stale...)
} else {
logger.Log.Error("Ошибка stale", zap.Error(err))
}
// 6. Dish in Recipe
- if dishInRec, err := s.repo.FindDishesInRecipes(); err == nil {
+ if dishInRec, err := s.repo.FindDishesInRecipes(serverID); err == nil {
all = append(all, dishInRec...)
} else {
logger.Log.Error("Ошибка dish_in_recipe", zap.Error(err))
}
// Сохраняем
- if err := s.repo.SaveAll(all); err != nil {
+ if err := s.repo.SaveAll(serverID, all); err != nil {
return err
}
@@ -77,6 +78,7 @@ func (s *Service) RefreshRecommendations() error {
return nil
}
-func (s *Service) GetRecommendations() ([]recommendations.Recommendation, error) {
- return s.repo.GetAll()
+// GetRecommendations возвращает рекомендации для конкретного сервера
+func (s *Service) GetRecommendations(serverID uuid.UUID) ([]recommendations.Recommendation, error) {
+ return s.repo.GetAll(serverID)
}
diff --git a/internal/services/sync/service.go b/internal/services/sync/service.go
index da3ae68..baeb33a 100644
--- a/internal/services/sync/service.go
+++ b/internal/services/sync/service.go
@@ -15,6 +15,7 @@ import (
"rmser/internal/domain/recipes"
"rmser/internal/domain/suppliers"
"rmser/internal/infrastructure/rms"
+ "rmser/pkg/crypto"
"rmser/pkg/logger"
)
@@ -24,17 +25,19 @@ const (
)
type Service struct {
- rmsFactory *rms.Factory
- accountRepo account.Repository
- catalogRepo catalog.Repository
- recipeRepo recipes.Repository
- invoiceRepo invoices.Repository
- opRepo operations.Repository
- supplierRepo suppliers.Repository
+ rmsFactory *rms.Factory
+ cryptoManager *crypto.CryptoManager
+ accountRepo account.Repository
+ catalogRepo catalog.Repository
+ recipeRepo recipes.Repository
+ invoiceRepo invoices.Repository
+ opRepo operations.Repository
+ supplierRepo suppliers.Repository
}
func NewService(
rmsFactory *rms.Factory,
+ cryptoManager *crypto.CryptoManager,
accountRepo account.Repository,
catalogRepo catalog.Repository,
recipeRepo recipes.Repository,
@@ -43,16 +46,73 @@ func NewService(
supplierRepo suppliers.Repository,
) *Service {
return &Service{
- rmsFactory: rmsFactory,
- accountRepo: accountRepo,
- catalogRepo: catalogRepo,
- recipeRepo: recipeRepo,
- invoiceRepo: invoiceRepo,
- opRepo: opRepo,
- supplierRepo: supplierRepo,
+ rmsFactory: rmsFactory,
+ cryptoManager: cryptoManager,
+ accountRepo: accountRepo,
+ catalogRepo: catalogRepo,
+ recipeRepo: recipeRepo,
+ invoiceRepo: invoiceRepo,
+ opRepo: opRepo,
+ supplierRepo: supplierRepo,
}
}
+// SyncAllDataForServer запускает полную синхронизацию для конкретного сервера
+func (s *Service) SyncAllDataForServer(serverID uuid.UUID, force bool) error {
+ logger.Log.Info("Запуск синхронизации по серверу", zap.String("server_id", serverID.String()), zap.Bool("force", force))
+
+ // 1. Получаем информацию о сервере
+ server, err := s.accountRepo.GetServerByID(serverID)
+ if err != nil || server == nil {
+ return fmt.Errorf("server not found: %s", serverID)
+ }
+
+ // 2. Получаем креды владельца сервера для подключения
+ baseURL, login, encryptedPass, err := s.getOwnerCredentials(serverID)
+ if err != nil {
+ return fmt.Errorf("failed to get owner credentials: %w", err)
+ }
+
+ // 3. Расшифровываем пароль
+ plainPass, err := s.cryptoManager.Decrypt(encryptedPass)
+ if err != nil {
+ return fmt.Errorf("failed to decrypt password: %w", err)
+ }
+
+ // 4. Создаем клиент RMS
+ client := s.rmsFactory.CreateClientFromRawCredentials(baseURL, login, plainPass)
+
+ return s.syncAllWithClient(client, serverID, force)
+}
+
+// getOwnerCredentials возвращает учетные данные владельца сервера
+func (s *Service) getOwnerCredentials(serverID uuid.UUID) (url, login, encryptedPass string, err error) {
+ // Находим владельца сервера
+ users, err := s.accountRepo.GetServerUsers(serverID)
+ if err != nil {
+ return "", "", "", err
+ }
+
+ var ownerLink *account.ServerUser
+ for i := range users {
+ if users[i].Role == account.RoleOwner {
+ ownerLink = &users[i]
+ break
+ }
+ }
+
+ if ownerLink == nil {
+ return "", "", "", fmt.Errorf("owner not found for server %s", serverID)
+ }
+
+ // Если у владельца есть личные креды - используем их
+ if ownerLink.Login != "" && ownerLink.EncryptedPassword != "" {
+ return ownerLink.Server.BaseURL, ownerLink.Login, ownerLink.EncryptedPassword, nil
+ }
+
+ return "", "", "", fmt.Errorf("owner has no credentials for server %s", serverID)
+}
+
// SyncAllData запускает полную синхронизацию для конкретного пользователя
func (s *Service) SyncAllData(userID uuid.UUID, force bool) error {
logger.Log.Info("Запуск синхронизации", zap.String("user_id", userID.String()), zap.Bool("force", force))
@@ -68,6 +128,12 @@ func (s *Service) SyncAllData(userID uuid.UUID, force bool) error {
}
serverID := server.ID
+ return s.syncAllWithClient(client, serverID, force)
+}
+
+// syncAllWithClient выполняет синхронизацию с готовым клиентом
+func (s *Service) syncAllWithClient(client rms.ClientI, serverID uuid.UUID, force bool) error {
+
// 2. Справочники
if err := s.syncStores(client, serverID); err != nil {
logger.Log.Error("Sync Stores failed", zap.Error(err))
@@ -96,13 +162,12 @@ func (s *Service) SyncAllData(userID uuid.UUID, force bool) error {
logger.Log.Error("Sync Invoices failed", zap.Error(err))
}
- // 7. Складские операции (тяжелый запрос)
- // Для MVP можно отключить, если долго грузится
- // if err := s.SyncStoreOperations(client, serverID); err != nil {
- // logger.Log.Error("Sync Operations failed", zap.Error(err))
- // }
+ // 7. Складские операции
+ if err := s.SyncStoreOperations(client, serverID); err != nil {
+ logger.Log.Error("Sync Operations failed", zap.Error(err))
+ }
- logger.Log.Info("Синхронизация завершена", zap.String("user_id", userID.String()))
+ logger.Log.Info("Синхронизация завершена", zap.String("server_id", serverID.String()))
return nil
}
@@ -236,7 +301,7 @@ func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID, force bool) er
// SyncStoreOperations публичный, если нужно вызывать отдельно
func (s *Service) SyncStoreOperations(c rms.ClientI, serverID uuid.UUID) error {
dateTo := time.Now()
- dateFrom := dateTo.AddDate(0, 0, -30)
+ dateFrom := dateTo.AddDate(0, 0, -90) // 90 дней — соответствует периоду анализа рекомендаций
if err := s.syncReport(c, serverID, PresetPurchases, operations.OpTypePurchase, dateFrom, dateTo); err != nil {
return fmt.Errorf("purchases sync error: %w", err)
diff --git a/internal/services/worker/sync_worker.go b/internal/services/worker/sync_worker.go
new file mode 100644
index 0000000..42cb9c6
--- /dev/null
+++ b/internal/services/worker/sync_worker.go
@@ -0,0 +1,136 @@
+package worker
+
+import (
+ "context"
+ "time"
+
+ "github.com/google/uuid"
+ "go.uber.org/zap"
+
+ "rmser/internal/domain/account"
+ "rmser/internal/infrastructure/rms"
+ "rmser/internal/services/recommend"
+)
+
+// SyncService интерфейс для синхронизации данных
+type SyncService interface {
+ // SyncAllDataForServer синхронизирует данные для конкретного сервера
+ SyncAllDataForServer(serverID uuid.UUID, force bool) error
+}
+
+// SyncWorker фоновый процесс для автоматической синхронизации данных с iiko серверами
+type SyncWorker struct {
+ syncService SyncService // сервис для синхронизации
+ accountRepo account.Repository // репозиторий для работы с серверами
+ rmsFactory *rms.Factory // фабрика для создания клиентов RMS
+ recService *recommend.Service // сервис рекомендаций
+ logger *zap.Logger
+ tickerInterval time.Duration // интервал проверки (например, 1 минута)
+ idleThreshold time.Duration // порог простоя (10 минут)
+}
+
+// NewSyncWorker создает новый экземпляр SyncWorker
+func NewSyncWorker(
+ syncService SyncService,
+ accountRepo account.Repository,
+ rmsFactory *rms.Factory,
+ recService *recommend.Service,
+ logger *zap.Logger,
+) *SyncWorker {
+ return &SyncWorker{
+ syncService: syncService,
+ accountRepo: accountRepo,
+ rmsFactory: rmsFactory,
+ recService: recService,
+ logger: logger,
+ tickerInterval: 1 * time.Minute,
+ idleThreshold: 10 * time.Minute,
+ }
+}
+
+// Run запускает фоновый процесс синхронизации
+func (w *SyncWorker) Run(ctx context.Context) {
+ w.logger.Info("Запуск SyncWorker",
+ zap.Duration("ticker_interval", w.tickerInterval),
+ zap.Duration("idle_threshold", w.idleThreshold))
+
+ ticker := time.NewTicker(w.tickerInterval)
+ defer ticker.Stop()
+
+ // Первый запуск сразу
+ w.processSync(ctx)
+
+ for {
+ select {
+ case <-ctx.Done():
+ w.logger.Info("Остановка SyncWorker")
+ return
+ case <-ticker.C:
+ w.processSync(ctx)
+ }
+ }
+}
+
+// processSync обрабатывает синхронизацию для всех серверов, готовых к синхронизации
+func (w *SyncWorker) processSync(ctx context.Context) {
+ // Получаем серверы, готовые для синхронизации
+ servers, err := w.accountRepo.GetServersForSync(w.idleThreshold)
+ if err != nil {
+ w.logger.Error("Ошибка получения серверов для синхронизации", zap.Error(err))
+ return
+ }
+
+ if len(servers) == 0 {
+ return
+ }
+
+ w.logger.Info("Найдены серверы для синхронизации",
+ zap.Int("count", len(servers)))
+
+ for _, server := range servers {
+ // Обрабатываем каждый сервер в отдельной горутине
+ go w.syncServer(ctx, server)
+ }
+}
+
+// syncServer выполняет синхронизацию для конкретного сервера
+func (w *SyncWorker) syncServer(ctx context.Context, server account.RMSServer) {
+ defer func() {
+ if r := recover(); r != nil {
+ w.logger.Error("Паника при синхронизации сервера",
+ zap.String("server_id", server.ID.String()),
+ zap.Any("recover", r))
+ }
+ }()
+
+ w.logger.Info("Начало синхронизации сервера",
+ zap.String("server_id", server.ID.String()),
+ zap.String("server_name", server.Name))
+
+ // Вызываем синхронизацию через syncService
+ err := w.syncService.SyncAllDataForServer(server.ID, false)
+ if err != nil {
+ w.logger.Error("Ошибка синхронизации сервера",
+ zap.String("server_id", server.ID.String()),
+ zap.Error(err))
+ return
+ }
+
+ // Обновляем время последней синхронизации
+ err = w.accountRepo.UpdateLastSync(server.ID)
+ if err != nil {
+ w.logger.Error("Ошибка обновления времени синхронизации",
+ zap.String("server_id", server.ID.String()),
+ zap.Error(err))
+ }
+
+ // Обновляем рекомендации после успешной синхронизации
+ if err := w.recService.RefreshRecommendations(server.ID); err != nil {
+ w.logger.Error("Ошибка обновления рекомендаций",
+ zap.String("server_id", server.ID.String()),
+ zap.Error(err))
+ }
+
+ w.logger.Info("Синхронизация сервера завершена успешно",
+ zap.String("server_id", server.ID.String()))
+}
diff --git a/internal/transport/http/handlers/drafts.go b/internal/transport/http/handlers/drafts.go
index dce574b..f85e3fb 100644
--- a/internal/transport/http/handlers/drafts.go
+++ b/internal/transport/http/handlers/drafts.go
@@ -203,50 +203,98 @@ type CommitRequestDTO struct {
SupplierID string `json:"supplier_id"`
Comment string `json:"comment"`
IncomingDocNum string `json:"incoming_document_number"`
+ IsProcessed bool `json:"is_processed"`
}
func (h *DraftsHandler) CommitDraft(c *gin.Context) {
- userID := c.MustGet("userID").(uuid.UUID)
+ // Защита от паники
+ defer func() {
+ if r := recover(); r != nil {
+ logger.Log.Error("CRITICAL PANIC in CommitDraft Handler",
+ zap.Any("panic", r),
+ zap.Stack("stack"),
+ )
+ c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Internal Server Error: %v", r)})
+ }
+ }()
+
+ logger.Log.Info("--- HANDLER: Start CommitDraft ---", zap.String("path", c.Request.URL.Path))
+
+ userID, ok := c.Get("userID")
+ if !ok {
+ logger.Log.Error("HANDLER: UserID missing in context")
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
+ return
+ }
+ userUUID := userID.(uuid.UUID)
+ logger.Log.Info("HANDLER: UserID extracted", zap.String("user_id", userUUID.String()))
+
draftID, err := uuid.Parse(c.Param("id"))
if err != nil {
+ logger.Log.Warn("HANDLER: Invalid DraftID", zap.String("param", c.Param("id")), zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid draft id"})
return
}
+ logger.Log.Info("HANDLER: DraftID parsed", zap.String("draft_id", draftID.String()))
var req CommitRequestDTO
if err := c.ShouldBindJSON(&req); err != nil {
+ logger.Log.Error("HANDLER: JSON Binding failed", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
+ logger.Log.Info("HANDLER: Payload bound",
+ zap.String("date_incoming", req.DateIncoming),
+ zap.String("store_id", req.StoreID),
+ zap.String("supplier_id", req.SupplierID),
+ zap.String("incoming_doc_num", req.IncomingDocNum),
+ zap.Bool("is_processed", req.IsProcessed),
+ )
date, err := time.Parse("2006-01-02", req.DateIncoming)
if err != nil {
+ logger.Log.Error("HANDLER: Date parsing failed", zap.String("date", req.DateIncoming), zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid date format"})
return
}
+
storeID, err := uuid.Parse(req.StoreID)
if err != nil {
+ logger.Log.Error("HANDLER: StoreID parsing failed", zap.String("store_id", req.StoreID), zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid store id"})
return
}
+
supplierID, err := uuid.Parse(req.SupplierID)
if err != nil {
+ logger.Log.Error("HANDLER: SupplierID parsing failed", zap.String("supplier_id", req.SupplierID), zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid supplier id"})
return
}
+ logger.Log.Info("HANDLER: Calling UpdateDraftHeader...",
+ zap.String("draft_id", draftID.String()),
+ zap.String("store_id", storeID.String()),
+ zap.String("supplier_id", supplierID.String()),
+ zap.Time("date", date),
+ )
+
if err := h.service.UpdateDraftHeader(draftID, &storeID, &supplierID, date, req.Comment, req.IncomingDocNum); err != nil {
+ logger.Log.Error("HANDLER: UpdateDraftHeader failed", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update header: " + err.Error()})
return
}
- docNum, err := h.service.CommitDraft(draftID, userID)
+ logger.Log.Info("HANDLER: Calling CommitDraft service...", zap.String("draft_id", draftID.String()), zap.String("user_id", userUUID.String()))
+
+ docNum, err := h.service.CommitDraft(draftID, userUUID, req.IsProcessed)
if err != nil {
- logger.Log.Error("Commit failed", zap.Error(err))
+ logger.Log.Warn("HANDLER: CommitDraft service failed", zap.Error(err))
c.JSON(http.StatusBadGateway, gin.H{"error": "RMS error: " + err.Error()})
return
}
+ logger.Log.Info("HANDLER: Success!", zap.String("doc_num", docNum))
c.JSON(http.StatusOK, gin.H{"status": "completed", "document_number": docNum})
}
diff --git a/internal/transport/http/handlers/invoices.go b/internal/transport/http/handlers/invoices.go
index 3264fa6..62312e6 100644
--- a/internal/transport/http/handlers/invoices.go
+++ b/internal/transport/http/handlers/invoices.go
@@ -110,3 +110,23 @@ func (h *InvoiceHandler) SyncInvoices(c *gin.Context) {
"message": "Синхронизация запущена",
})
}
+
+// GetStats godoc
+// @Summary Получить статистику по накладным
+// @Description Возвращает статистику по накладным для текущего пользователя
+// @Tags invoices
+// @Produce json
+// @Success 200 {object} invService.InvoiceStatsDTO
+// @Failure 500 {object} map[string]string
+func (h *InvoiceHandler) GetStats(c *gin.Context) {
+ userID := c.MustGet("userID").(uuid.UUID)
+
+ stats, err := h.service.GetStats(userID)
+ if err != nil {
+ logger.Log.Error("Ошибка получения статистики", zap.Error(err))
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Ошибка получения статистики"})
+ return
+ }
+
+ c.JSON(http.StatusOK, stats)
+}
diff --git a/internal/transport/http/handlers/recommendations.go b/internal/transport/http/handlers/recommendations.go
index d7b9df7..69f577d 100644
--- a/internal/transport/http/handlers/recommendations.go
+++ b/internal/transport/http/handlers/recommendations.go
@@ -4,29 +4,56 @@ import (
"net/http"
"github.com/gin-gonic/gin"
+ "github.com/google/uuid"
"go.uber.org/zap"
+ "rmser/internal/domain/account"
"rmser/internal/services/recommend"
"rmser/pkg/logger"
)
type RecommendationsHandler struct {
- service *recommend.Service
+ service *recommend.Service
+ accountRepo account.Repository
}
-func NewRecommendationsHandler(service *recommend.Service) *RecommendationsHandler {
- return &RecommendationsHandler{service: service}
+func NewRecommendationsHandler(service *recommend.Service, accountRepo account.Repository) *RecommendationsHandler {
+ return &RecommendationsHandler{
+ service: service,
+ accountRepo: accountRepo,
+ }
}
// GetRecommendations godoc
// @Summary Получить список рекомендаций
-// @Description Возвращает сгенерированные рекомендации (проблемные зоны учета)
+// @Description Возвращает сгенерированные рекомендации (проблемные зоны учета) для активного сервера
// @Tags recommendations
// @Produce json
// @Success 200 {array} recommendations.Recommendation
// @Failure 500 {object} map[string]string
func (h *RecommendationsHandler) GetRecommendations(c *gin.Context) {
- recs, err := h.service.GetRecommendations()
+ userID := c.MustGet("userID").(uuid.UUID)
+
+ // Получаем активный сервер пользователя
+ server, err := h.accountRepo.GetActiveServer(userID)
+ if err != nil {
+ logger.Log.Error("Ошибка получения активного сервера", zap.Error(err))
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get active server"})
+ return
+ }
+ if server == nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "no active server"})
+ return
+ }
+
+ // Сначала обновляем рекомендации
+ if err := h.service.RefreshRecommendations(server.ID); err != nil {
+ logger.Log.Error("Ошибка обновления рекомендаций", zap.Error(err))
+ // Не прерываем выполнение, продолжаем с текущими данными
+ }
+
+ // Затем получаем рекомендации
+ recs, err := h.service.GetRecommendations(server.ID)
if err != nil {
logger.Log.Error("Ошибка получения рекомендаций", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
diff --git a/internal/transport/http/handlers/settings.go b/internal/transport/http/handlers/settings.go
index 19a6007..e68ce5d 100644
--- a/internal/transport/http/handlers/settings.go
+++ b/internal/transport/http/handlers/settings.go
@@ -2,6 +2,7 @@ package handlers
import (
"net/http"
+ "time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -48,13 +49,16 @@ func (h *SettingsHandler) SetNotifier(n Notifier) {
// SettingsResponse - DTO для отдачи настроек
type SettingsResponse struct {
- ID string `json:"id"`
- Name string `json:"name"`
- BaseURL string `json:"base_url"`
- DefaultStoreID *string `json:"default_store_id"` // Nullable
- RootGroupID *string `json:"root_group_id"` // Nullable
- AutoConduct bool `json:"auto_conduct"`
- Role string `json:"role"` // OWNER, ADMIN, OPERATOR
+ ID string `json:"id"`
+ Name string `json:"name"`
+ BaseURL string `json:"base_url"`
+ DefaultStoreID *string `json:"default_store_id"` // Nullable
+ RootGroupID *string `json:"root_group_id"` // Nullable
+ AutoConduct bool `json:"auto_conduct"`
+ Role string `json:"role"` // OWNER, ADMIN, OPERATOR
+ SyncInterval int `json:"sync_interval"` // Интервал синхронизации в минутах
+ LastSyncAt *time.Time `json:"last_sync_at"` // Время последней синхронизации
+ LastActivityAt *time.Time `json:"last_activity_at"` // Время последней активности
}
// GetSettings возвращает настройки активного сервера + роль пользователя
@@ -77,11 +81,14 @@ func (h *SettingsHandler) GetSettings(c *gin.Context) {
}
resp := SettingsResponse{
- ID: server.ID.String(),
- Name: server.Name,
- BaseURL: server.BaseURL,
- AutoConduct: server.AutoProcess,
- Role: string(role),
+ ID: server.ID.String(),
+ Name: server.Name,
+ BaseURL: server.BaseURL,
+ AutoConduct: server.AutoProcess,
+ Role: string(role),
+ SyncInterval: server.SyncInterval,
+ LastSyncAt: server.LastSyncAt,
+ LastActivityAt: server.LastActivityAt,
}
if server.DefaultStoreID != nil {
@@ -96,16 +103,17 @@ func (h *SettingsHandler) GetSettings(c *gin.Context) {
c.JSON(http.StatusOK, resp)
}
-// UpdateSettingsDTO
+// UpdateSettingsDTO - DTO для частичного обновления настроек (PATCH-семантика)
type UpdateSettingsDTO struct {
- Name string `json:"name"`
- DefaultStoreID string `json:"default_store_id"`
- RootGroupID string `json:"root_group_id"`
- AutoProcess bool `json:"auto_process"`
- AutoConduct bool `json:"auto_conduct"`
+ Name *string `json:"name"`
+ DefaultStoreID *string `json:"default_store_id"`
+ RootGroupID *string `json:"root_group_id"`
+ AutoProcess *bool `json:"auto_process"` // Legacy для обратной совместимости
+ AutoConduct *bool `json:"auto_conduct"` // Новое поле
+ SyncInterval *int `json:"sync_interval,omitempty"` // Интервал синхронизации в минутах (5 - 10080)
}
-// UpdateSettings сохраняет настройки
+// UpdateSettings сохраняет настройки с PATCH-семантикой
func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
@@ -115,6 +123,11 @@ func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
return
}
+ // Логирование полученных данных для отладки
+ logger.Log.Info("Получен запрос на обновление настроек",
+ zap.Any("request", req),
+ )
+
server, err := h.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "No active server"})
@@ -132,31 +145,56 @@ func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
return
}
- if req.Name != "" {
- server.Name = req.Name
+ // Обновление имени (только если передано)
+ if req.Name != nil {
+ server.Name = *req.Name
}
- if req.AutoConduct {
- server.AutoProcess = true
- } else {
- server.AutoProcess = req.AutoProcess || req.AutoConduct
+ // Обновление флага авто-проведения
+ if req.AutoConduct != nil {
+ server.AutoProcess = *req.AutoConduct
+ } else if req.AutoProcess != nil {
+ // Fallback для старых клиентов, которые используют legacy поле
+ server.AutoProcess = *req.AutoProcess
}
- if req.DefaultStoreID != "" {
- if uid, err := uuid.Parse(req.DefaultStoreID); err == nil {
- server.DefaultStoreID = &uid
+ // Обновление интервала синхронизации
+ if req.SyncInterval != nil {
+ // Валидация диапазона: от 5 минут до 1 недели (10080 минут)
+ if *req.SyncInterval < 5 || *req.SyncInterval > 10080 {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "sync_interval должен быть от 5 минут до 1 недели (10080 минут)"})
+ return
}
- } else {
- server.DefaultStoreID = nil
+ server.SyncInterval = *req.SyncInterval
}
- if req.RootGroupID != "" {
- if uid, err := uuid.Parse(req.RootGroupID); err == nil {
- server.RootGroupGUID = &uid
+ // Обновление DefaultStoreID
+ if req.DefaultStoreID != nil {
+ if *req.DefaultStoreID == "" {
+ // Пустая строка -> сбрасываем в nil
+ server.DefaultStoreID = nil
+ } else {
+ // UUID -> обновляем
+ if uid, err := uuid.Parse(*req.DefaultStoreID); err == nil {
+ server.DefaultStoreID = &uid
+ }
}
- } else {
- server.RootGroupGUID = nil
}
+ // Если nil -> не трогаем текущее значение
+
+ // Обновление RootGroupID
+ if req.RootGroupID != nil {
+ if *req.RootGroupID == "" {
+ // Пустая строка -> сбрасываем в nil
+ server.RootGroupGUID = nil
+ } else {
+ // UUID -> обновляем
+ if uid, err := uuid.Parse(*req.RootGroupID); err == nil {
+ server.RootGroupGUID = &uid
+ }
+ }
+ }
+ // Если nil -> не трогаем текущее значение
if err := h.accountRepo.SaveServerSettings(server); err != nil {
logger.Log.Error("Failed to save settings", zap.Error(err))
diff --git a/internal/transport/telegram/bot.go b/internal/transport/telegram/bot.go
index 5fc65bf..bd0b018 100644
--- a/internal/transport/telegram/bot.go
+++ b/internal/transport/telegram/bot.go
@@ -202,17 +202,17 @@ func (bot *Bot) registrationMiddleware(next tele.HandlerFunc) tele.HandlerFunc {
func (bot *Bot) handleStartCommand(c tele.Context) error {
payload := c.Message().Payload
-
+
// Обработка desktop авторизации
if payload != "" && strings.HasPrefix(payload, "auth_") {
sessionID := strings.TrimPrefix(payload, "auth_")
telegramID := c.Sender().ID
-
+
logger.Log.Info("Обработка desktop авторизации",
zap.String("session_id", sessionID),
zap.Int64("telegram_id", telegramID),
)
-
+
if err := bot.authService.ConfirmDesktopAuth(sessionID, telegramID); err != nil {
logger.Log.Error("Ошибка подтверждения desktop авторизации",
zap.String("session_id", sessionID),
@@ -221,10 +221,10 @@ func (bot *Bot) handleStartCommand(c tele.Context) error {
)
return c.Send("❌ Ошибка авторизации. Попробуйте снова.", tele.ModeHTML)
}
-
+
return c.Send("✅ Авторизация успешна! Вы можете вернуться в приложение.", tele.ModeHTML)
}
-
+
if payload != "" && strings.HasPrefix(payload, "invite_") {
return bot.handleInviteLink(c, strings.TrimPrefix(payload, "invite_"))
}
@@ -417,19 +417,70 @@ func (bot *Bot) renderServersMenu(c tele.Context) error {
}
role, _ := bot.accountRepo.GetUserRole(userDB.ID, s.ID)
label := fmt.Sprintf("%s %s (%s)", icon, s.Name, role)
- btn := menu.Data(label, "set_server_"+s.ID.String())
+ btn := menu.Data(label, "srv_menu_"+s.ID.String())
rows = append(rows, menu.Row(btn))
}
btnAdd := menu.Data("➕ Добавить сервер", "act_add_server")
- btnDel := menu.Data("⚙️ Управление / Удаление", "act_del_server_menu")
btnBack := menu.Data("🔙 Назад", "nav_main")
- rows = append(rows, menu.Row(btnAdd, btnDel))
+ rows = append(rows, menu.Row(btnAdd))
rows = append(rows, menu.Row(btnBack))
menu.Inline(rows...)
- txt := fmt.Sprintf("🖥 Ваши серверы (%d):\n\nНажмите на сервер, чтобы сделать его активным.", len(servers))
+ txt := fmt.Sprintf("🖥 Ваши серверы (%d):\n\nНажмите на сервер для управления.", len(servers))
+ return c.EditOrSend(txt, menu, tele.ModeHTML)
+}
+
+// renderServerMenu показывает подменю управления конкретным сервером
+func (bot *Bot) renderServerMenu(c tele.Context, serverID uuid.UUID) error {
+ userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID)
+ server, err := bot.accountRepo.GetServerByID(serverID)
+ if err != nil {
+ return c.Send("Ошибка: сервер не найден")
+ }
+
+ role, _ := bot.accountRepo.GetUserRole(userDB.ID, server.ID)
+ activeServer, _ := bot.accountRepo.GetActiveServer(userDB.ID)
+ isActive := activeServer != nil && activeServer.ID == server.ID
+
+ menu := &tele.ReplyMarkup{}
+ var rows []tele.Row
+
+ // Кнопка "Выбрать активным" (доступна всем)
+ if !isActive {
+ btnSetActive := menu.Data("✅ Выбрать активным", "srv_set_active_"+server.ID.String())
+ rows = append(rows, menu.Row(btnSetActive))
+ } else {
+ btnActive := menu.Data("🟢 Активный сервер", "noop")
+ rows = append(rows, menu.Row(btnActive))
+ }
+
+ // Кнопка "Показать URL/Логин" (доступна Admin и Owner)
+ if role == account.RoleOwner || role == account.RoleAdmin {
+ btnShowCreds := menu.Data("👁 Показать URL/Логин", "srv_show_creds_"+server.ID.String())
+ btnInvite := menu.Data("📩 Пригласить сотрудника", fmt.Sprintf("gen_invite_%s", server.ID.String()))
+ rows = append(rows, menu.Row(btnShowCreds, btnInvite))
+ }
+
+ // Кнопка "Обновить логин-пароль" (только Owner)
+ if role == account.RoleOwner {
+ btnUpdateCreds := menu.Data("✏️ Обновить логин-пароль", "srv_update_creds_"+server.ID.String())
+ rows = append(rows, menu.Row(btnUpdateCreds))
+ }
+
+ // Кнопка "Удалить сервер" (только Owner)
+ if role == account.RoleOwner {
+ btnDelete := menu.Data("❌ Удалить сервер", "srv_delete_"+server.ID.String())
+ rows = append(rows, menu.Row(btnDelete))
+ }
+
+ btnBack := menu.Data("🔙 Назад к списку", "nav_servers")
+ rows = append(rows, menu.Row(btnBack))
+ menu.Inline(rows...)
+
+ txt := fmt.Sprintf("⚙️ Управление сервером\n\n🏢 Название: %s\n🔗 URL: %s\n👤 Ваша роль: %s",
+ server.Name, server.BaseURL, role)
return c.EditOrSend(txt, menu, tele.ModeHTML)
}
@@ -536,6 +587,131 @@ func (bot *Bot) handleCallback(c tele.Context) error {
return bot.handleBillingCallbacks(c, data, userDB)
}
+ // Обработка кнопок подменю сервера
+ if strings.HasPrefix(data, "srv_menu_") {
+ serverIDStr := strings.TrimPrefix(data, "srv_menu_")
+ serverIDStr = strings.TrimSpace(serverIDStr)
+ if idx := strings.Index(serverIDStr, "|"); idx != -1 {
+ serverIDStr = serverIDStr[:idx]
+ }
+ targetID := parseUUID(serverIDStr)
+ return bot.renderServerMenu(c, targetID)
+ }
+
+ if strings.HasPrefix(data, "srv_set_active_") {
+ serverIDStr := strings.TrimPrefix(data, "srv_set_active_")
+ serverIDStr = strings.TrimSpace(serverIDStr)
+ if idx := strings.Index(serverIDStr, "|"); idx != -1 {
+ serverIDStr = serverIDStr[:idx]
+ }
+ targetID := parseUUID(serverIDStr)
+ if err := bot.accountRepo.SetActiveServer(userDB.ID, targetID); err != nil {
+ logger.Log.Error("Failed to set active server", zap.Error(err))
+ return c.Respond(&tele.CallbackResponse{Text: "Ошибка: доступ запрещен"})
+ }
+ bot.rmsFactory.ClearCacheForUser(userDB.ID)
+ c.Respond(&tele.CallbackResponse{Text: "✅ Сервер выбран активным"})
+ return bot.renderServerMenu(c, targetID)
+ }
+
+ if strings.HasPrefix(data, "srv_show_creds_") {
+ serverIDStr := strings.TrimPrefix(data, "srv_show_creds_")
+ serverIDStr = strings.TrimSpace(serverIDStr)
+ if idx := strings.Index(serverIDStr, "|"); idx != -1 {
+ serverIDStr = serverIDStr[:idx]
+ }
+ targetID := parseUUID(serverIDStr)
+ server, err := bot.accountRepo.GetServerByID(targetID)
+ if err != nil {
+ return c.Respond(&tele.CallbackResponse{Text: "Ошибка: сервер не найден"})
+ }
+ role, _ := bot.accountRepo.GetUserRole(userDB.ID, server.ID)
+ if role != account.RoleOwner && role != account.RoleAdmin {
+ return c.Respond(&tele.CallbackResponse{Text: "Ошибка: недостаточно прав"})
+ }
+ // Получаем личные креды пользователя через GetServerUsers
+ serverUsers, err := bot.accountRepo.GetServerUsers(server.ID)
+ if err != nil {
+ return c.Respond(&tele.CallbackResponse{Text: "Ошибка получения данных"})
+ }
+ var login string
+ for _, su := range serverUsers {
+ if su.UserID == userDB.ID {
+ login = su.Login
+ break
+ }
+ }
+ if login == "" {
+ return c.Respond(&tele.CallbackResponse{Text: "У вас нет сохраненных учетных данных"})
+ }
+ c.Respond()
+ return c.Send(fmt.Sprintf("🔑 Учетные данные сервера\n\n🏢 Название: %s\n🔗 URL: %s\n👤 Логин: %s\n🔒 Пароль: ***скрыт***",
+ server.Name, server.BaseURL, login), tele.ModeHTML)
+ }
+
+ if strings.HasPrefix(data, "srv_update_creds_") {
+ serverIDStr := strings.TrimPrefix(data, "srv_update_creds_")
+ serverIDStr = strings.TrimSpace(serverIDStr)
+ if idx := strings.Index(serverIDStr, "|"); idx != -1 {
+ serverIDStr = serverIDStr[:idx]
+ }
+ targetID := parseUUID(serverIDStr)
+ server, err := bot.accountRepo.GetServerByID(targetID)
+ if err != nil {
+ return c.Respond(&tele.CallbackResponse{Text: "Ошибка: сервер не найден"})
+ }
+ role, _ := bot.accountRepo.GetUserRole(userDB.ID, server.ID)
+ if role != account.RoleOwner {
+ return c.Respond(&tele.CallbackResponse{Text: "Ошибка: только владелец может обновлять учетные данные"})
+ }
+ // Сохраняем ID сервера в контексте FSM
+ bot.fsm.UpdateContext(c.Sender().ID, func(ctx *UserContext) {
+ ctx.EditingServerID = server.ID
+ ctx.TempURL = server.BaseURL
+ })
+ bot.fsm.SetState(c.Sender().ID, StateUpdateServerLogin)
+ c.Respond()
+ return c.EditOrSend("✏️ Обновление учетных данных\n\nВведите новый логин для сервера "+server.Name+".\n\n(Напишите 'отмена' для выхода)", tele.ModeHTML)
+ }
+
+ if strings.HasPrefix(data, "srv_delete_") {
+ serverIDStr := strings.TrimPrefix(data, "srv_delete_")
+ serverIDStr = strings.TrimSpace(serverIDStr)
+ if idx := strings.Index(serverIDStr, "|"); idx != -1 {
+ serverIDStr = serverIDStr[:idx]
+ }
+ targetID := parseUUID(serverIDStr)
+ role, err := bot.accountRepo.GetUserRole(userDB.ID, targetID)
+ if err != nil {
+ return c.Respond(&tele.CallbackResponse{Text: "Ошибка прав доступа"})
+ }
+ if role != account.RoleOwner {
+ return c.Respond(&tele.CallbackResponse{Text: "Только владелец может удалить сервер"})
+ }
+ // Подтверждение удаления
+ menu := &tele.ReplyMarkup{}
+ btnYes := menu.Data("✅ Да, удалить", "srv_delete_confirm_"+targetID.String())
+ btnNo := menu.Data("❌ Отмена", "srv_menu_"+targetID.String())
+ menu.Inline(menu.Row(btnYes), menu.Row(btnNo))
+ server, _ := bot.accountRepo.GetServerByID(targetID)
+ return c.EditOrSend("⚠️ Подтверждение удаления\n\nВы уверены, что хотите удалить сервер "+server.Name+"?\n\nЭто действие необратимо!", menu, tele.ModeHTML)
+ }
+
+ if strings.HasPrefix(data, "srv_delete_confirm_") {
+ serverIDStr := strings.TrimPrefix(data, "srv_delete_confirm_")
+ serverIDStr = strings.TrimSpace(serverIDStr)
+ if idx := strings.Index(serverIDStr, "|"); idx != -1 {
+ serverIDStr = serverIDStr[:idx]
+ }
+ targetID := parseUUID(serverIDStr)
+ if err := bot.accountRepo.DeleteServer(targetID); err != nil {
+ return c.Respond(&tele.CallbackResponse{Text: "Ошибка удаления"})
+ }
+ bot.rmsFactory.ClearCacheForUser(userDB.ID)
+ c.Respond(&tele.CallbackResponse{Text: "Сервер удален"})
+ return bot.renderServersMenu(c)
+ }
+
if strings.HasPrefix(data, "set_server_") {
serverIDStr := strings.TrimPrefix(data, "set_server_")
serverIDStr = strings.TrimSpace(serverIDStr)
@@ -799,6 +975,7 @@ func (bot *Bot) handleText(c tele.Context) error {
userID := c.Sender().ID
state := bot.fsm.GetState(userID)
text := strings.TrimSpace(c.Text())
+ userDB, _ := bot.accountRepo.GetUserByTelegramID(userID)
if bot.maintenanceMode && !bot.isDev(userID) {
return c.Send("Сервис на обслуживании", tele.ModeHTML)
@@ -888,6 +1065,52 @@ func (bot *Bot) handleText(c tele.Context) error {
ctx.BillingTargetURL = text
})
return bot.renderTariffShowcase(c, text)
+
+ case StateUpdateServerLogin:
+ ctx := bot.fsm.GetContext(userID)
+ if ctx.EditingServerID == uuid.Nil {
+ bot.fsm.Reset(userID)
+ return bot.renderMainMenu(c)
+ }
+ bot.fsm.UpdateContext(userID, func(uCtx *UserContext) {
+ uCtx.TempLogin = text
+ uCtx.State = StateUpdateServerPassword
+ })
+ return c.Send("🔑 Введите новый пароль:")
+
+ case StateUpdateServerPassword:
+ password := text
+ ctx := bot.fsm.GetContext(userID)
+ if ctx.EditingServerID == uuid.Nil {
+ bot.fsm.Reset(userID)
+ return bot.renderMainMenu(c)
+ }
+ server, err := bot.accountRepo.GetServerByID(ctx.EditingServerID)
+ if err != nil {
+ bot.fsm.Reset(userID)
+ return c.Send("❌ Ошибка: сервер не найден")
+ }
+ // Проверяем новые креды
+ msg, _ := bot.b.Send(c.Sender(), "⏳ Проверяю подключение...")
+ tempClient := bot.rmsFactory.CreateClientFromRawCredentials(ctx.TempURL, ctx.TempLogin, password)
+ if err := tempClient.Auth(); err != nil {
+ bot.b.Delete(msg)
+ bot.fsm.Reset(userID)
+ return c.Send(fmt.Sprintf("❌ Ошибка авторизации: %v\nПроверьте логин/пароль.", err))
+ }
+ // Шифруем пароль и сохраняем
+ encPass, _ := bot.cryptoManager.Encrypt(password)
+ // Обновляем креды через ConnectServer (он обновит существующую связь)
+ _, err = bot.accountRepo.ConnectServer(userDB.ID, ctx.TempURL, ctx.TempLogin, encPass, server.Name)
+ bot.b.Delete(msg)
+ if err != nil {
+ bot.fsm.Reset(userID)
+ return c.Send("❌ Ошибка обновления данных")
+ }
+ bot.fsm.Reset(userID)
+ bot.rmsFactory.ClearCacheForUser(userDB.ID)
+ c.Send("✅ Учетные данные обновлены!\n\nТеперь вы можете использовать новые логин и пароль для подключения к серверу.", tele.ModeHTML)
+ return bot.renderServerMenu(c, server.ID)
}
return nil
diff --git a/internal/transport/telegram/fsm.go b/internal/transport/telegram/fsm.go
index 10a20aa..ce28d9e 100644
--- a/internal/transport/telegram/fsm.go
+++ b/internal/transport/telegram/fsm.go
@@ -17,10 +17,12 @@ const (
StateAddServerConfirmName
StateAddServerInputName
StateBillingGiftURL
+ StateUpdateServerLogin // Обновление логина для существующего сервера
+ StateUpdateServerPassword // Обновление пароля для существующего сервера
// Состояния редактора черновиков (начиная с 100)
- StateDraftEditItemName State = 100 // Ожидание ввода нового названия позиции
- StateDraftEditItemQty State = 101 // Ожидание ввода количества
+ StateDraftEditItemName State = 100 // Ожидание ввода нового названия позиции
+ StateDraftEditItemQty State = 101 // Ожидание ввода количества
StateDraftEditItemPrice State = 102 // Ожидание ввода цены
)
@@ -34,6 +36,9 @@ type UserContext struct {
TempPassword string
TempServerName string
+ // Поля для обновления сервера
+ EditingServerID uuid.UUID // ID редактируемого сервера
+
// Поля для биллинга
BillingTargetURL string
diff --git a/migrations/20250202040746_add_sync_fields_to_rms_servers.sql b/migrations/20250202040746_add_sync_fields_to_rms_servers.sql
new file mode 100644
index 0000000..99db25c
--- /dev/null
+++ b/migrations/20250202040746_add_sync_fields_to_rms_servers.sql
@@ -0,0 +1,17 @@
+-- Добавляем поля для синхронизации в таблицу rms_servers
+-- Миграция для отслеживания активности и времени синхронизации
+
+-- Добавляем колонку sync_interval со значением по умолчанию 360 (6 часов)
+ALTER TABLE rms_servers ADD COLUMN sync_interval INTEGER NOT NULL DEFAULT 360;
+
+-- Добавляем колонку last_sync_at (время последней успешной синхронизации)
+ALTER TABLE rms_servers ADD COLUMN last_sync_at TIMESTAMP WITH TIME ZONE;
+
+-- Добавляем колонку last_activity_at (время последнего действия пользователя)
+ALTER TABLE rms_servers ADD COLUMN last_activity_at TIMESTAMP WITH TIME ZONE;
+
+-- Создаем индекс для оптимизации запросов на синхронизацию
+CREATE INDEX idx_rms_servers_sync ON rms_servers(deleted_at, last_sync_at, sync_interval);
+
+-- Создаем индекс для оптимизации запросов по активности
+CREATE INDEX idx_rms_servers_activity ON rms_servers(deleted_at, last_activity_at);
diff --git a/migrations/20250202051336_add_server_id_to_recommendations.sql b/migrations/20250202051336_add_server_id_to_recommendations.sql
new file mode 100644
index 0000000..c1fe5b6
--- /dev/null
+++ b/migrations/20250202051336_add_server_id_to_recommendations.sql
@@ -0,0 +1,12 @@
+-- +goose Up
+ALTER TABLE recommendations ADD COLUMN IF NOT EXISTS rms_server_id UUID;
+CREATE INDEX IF NOT EXISTS idx_recommendations_server_id ON recommendations(rms_server_id);
+
+-- Удаляем старые записи без server_id (они невалидны)
+DELETE FROM recommendations WHERE rms_server_id IS NULL;
+
+-- Делаем поле NOT NULL после очистки
+ALTER TABLE recommendations ALTER COLUMN rms_server_id SET NOT NULL;
+
+-- +goose Down
+ALTER TABLE recommendations DROP COLUMN IF EXISTS rms_server_id;
diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go
index df788a2..3078acf 100644
--- a/pkg/logger/logger.go
+++ b/pkg/logger/logger.go
@@ -1,25 +1,25 @@
-package logger
-
-import (
- "go.uber.org/zap"
- "go.uber.org/zap/zapcore"
-)
-
-var Log *zap.Logger
-
-func Init(mode string) {
- var config zap.Config
-
- if mode == "release" {
- config = zap.NewProductionConfig()
- } else {
- config = zap.NewDevelopmentConfig()
- config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
- }
-
- var err error
- Log, err = config.Build()
- if err != nil {
- panic("не удалось инициализировать логгер: " + err.Error())
- }
-}
\ No newline at end of file
+package logger
+
+import (
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+)
+
+var Log *zap.Logger
+
+func Init(mode string) {
+ var config zap.Config
+
+ if mode == "release" {
+ config = zap.NewProductionConfig()
+ } else {
+ config = zap.NewDevelopmentConfig()
+ config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
+ }
+
+ var err error
+ Log, err = config.Build()
+ if err != nil {
+ panic("не удалось инициализировать логгер: " + err.Error())
+ }
+}
diff --git a/rmser-view/project_context.md b/rmser-view/project_context.md
deleted file mode 100644
index a00433a..0000000
--- a/rmser-view/project_context.md
+++ /dev/null
@@ -1,10659 +0,0 @@
-# ===================================================================
-# Полный контекст React Typescript проекта
-# Сгенерировано: 2026-01-28 08:00:40
-# ===================================================================
-
-Это полный дамп исходного кода React Typescript (Vite) проекта.
-Каждый файл предваряется заголовком с путём к нему.
-
-
-# ===================================================================
-# Файл: Dockerfile
-# ===================================================================
-
-```
-# Этап 1: Сборка (Build)
-FROM node:24-alpine as builder
-
-WORKDIR /app
-
-# Копируем файлы зависимостей
-COPY package*.json ./
-
-# Устанавливаем зависимости
-RUN npm install
-
-# Копируем исходный код
-COPY . .
-
-# Собираем проект (результат будет в папке dist)
-# Важно: Vite подставит VITE_API_URL во время сборки.
-# Мы будем использовать относительный путь /api, чтобы работал прокси Nginx.
-ENV VITE_API_URL=/api
-RUN npm run build
-
-# Этап 2: Запуск (Serve via Nginx)
-FROM nginx:alpine
-
-# Копируем конфиг nginx (создадим его на след. шаге)
-COPY nginx.conf /etc/nginx/conf.d/default.conf
-
-# Копируем собранные файлы из этапа сборки
-COPY --from=builder /app/dist /usr/share/nginx/html
-
-EXPOSE 80
-
-CMD ["nginx", "-g", "daemon off;"]
-```
-
-# ===================================================================
-# Файл: eslint.config.js
-# ===================================================================
-
-```
-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,
- },
- },
-])
-
-```
-
-# ===================================================================
-# Файл: index.html
-# ===================================================================
-
-```
-
-
-
-
-
-
- RMSer App
-
-
-
-
-
-
-
-```
-
-# ===================================================================
-# Файл: package-lock.json
-# ===================================================================
-
-```
-{
- "name": "rmser-view",
- "version": "0.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "rmser-view",
- "version": "0.0.0",
- "dependencies": {
- "@hello-pangea/dnd": "^18.0.1",
- "@tanstack/react-query": "^5.90.12",
- "@twa-dev/sdk": "^8.0.2",
- "antd": "^6.1.0",
- "axios": "^1.13.2",
- "clsx": "^2.1.1",
- "lucide-react": "^0.563.0",
- "qrcode.react": "^4.2.0",
- "react": "^19.2.0",
- "react-dom": "^19.2.0",
- "react-dropzone": "^14.3.8",
- "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/@hello-pangea/dnd": {
- "version": "18.0.1",
- "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz",
- "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@babel/runtime": "^7.26.7",
- "css-box-model": "^1.2.1",
- "raf-schd": "^4.0.3",
- "react-redux": "^9.2.0",
- "redux": "^5.0.1"
- },
- "peerDependencies": {
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.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/@types/use-sync-external-store": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
- "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
- "license": "MIT"
- },
- "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/attr-accept": {
- "version": "2.2.5",
- "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
- "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "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/css-box-model": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
- "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
- "license": "MIT",
- "dependencies": {
- "tiny-invariant": "^1.0.6"
- }
- },
- "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/file-selector": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
- "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.7.0"
- },
- "engines": {
- "node": ">= 12"
- }
- },
- "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==",
- "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/loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- },
- "bin": {
- "loose-envify": "cli.js"
- }
- },
- "node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^3.0.2"
- }
- },
- "node_modules/lucide-react": {
- "version": "0.563.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz",
- "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==",
- "license": "ISC",
- "peerDependencies": {
- "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "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/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/optionator": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
- "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "deep-is": "^0.1.3",
- "fast-levenshtein": "^2.0.6",
- "levn": "^0.4.1",
- "prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.5"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/p-limit": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "yocto-queue": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-locate": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
- "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-limit": "^3.0.2"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/parent-module": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
- "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "callsites": "^3.0.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/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/prop-types": {
- "version": "15.8.1",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
- "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.4.0",
- "object-assign": "^4.1.1",
- "react-is": "^16.13.1"
- }
- },
- "node_modules/prop-types/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
- },
- "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/qrcode.react": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
- "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
- "license": "ISC",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/raf-schd": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
- "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
- "license": "MIT"
- },
- "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",
- "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-dropzone": {
- "version": "14.3.8",
- "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
- "integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
- "license": "MIT",
- "dependencies": {
- "attr-accept": "^2.2.4",
- "file-selector": "^2.1.0",
- "prop-types": "^15.8.1"
- },
- "engines": {
- "node": ">= 10.13"
- },
- "peerDependencies": {
- "react": ">= 16.8 || 18.0.0"
- }
- },
- "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-redux": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
- "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
- "license": "MIT",
- "dependencies": {
- "@types/use-sync-external-store": "^0.0.6",
- "use-sync-external-store": "^1.4.0"
- },
- "peerDependencies": {
- "@types/react": "^18.2.25 || ^19",
- "react": "^18.0 || ^19",
- "redux": "^5.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "redux": {
- "optional": true
- }
- }
- },
- "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/redux": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
- "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
- "license": "MIT",
- "peer": true
- },
- "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/tiny-invariant": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
- "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
- "license": "MIT"
- },
- "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/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
- },
- "node_modules/type-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "prelude-ls": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/typescript": {
- "version": "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/use-sync-external-store": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
- "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.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
- }
- }
- }
- }
-}
-
-```
-
-# ===================================================================
-# Файл: package.json
-# ===================================================================
-
-```
-{
- "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": {
- "@hello-pangea/dnd": "^18.0.1",
- "@tanstack/react-query": "^5.90.12",
- "@twa-dev/sdk": "^8.0.2",
- "antd": "^6.1.0",
- "axios": "^1.13.2",
- "clsx": "^2.1.1",
- "lucide-react": "^0.563.0",
- "qrcode.react": "^4.2.0",
- "react": "^19.2.0",
- "react-dom": "^19.2.0",
- "react-dropzone": "^14.3.8",
- "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"
- }
-}
-
-```
-
-# ===================================================================
-# Файл: src/App.tsx
-# ===================================================================
-
-```
-import { useEffect, useState } from "react";
-import {
- BrowserRouter,
- Routes,
- Route,
- Navigate,
- useLocation,
-} from "react-router-dom";
-import { Result, Button } from "antd";
-import { Providers } from "./components/layout/Providers";
-import { AppLayout } from "./components/layout/AppLayout";
-import { OcrLearning } from "./pages/OcrLearning";
-import { InvoiceDraftPage } from "./pages/InvoiceDraftPage";
-import { InvoiceViewPage } from "./pages/InvoiceViewPage";
-import { DraftsList } from "./pages/DraftsList";
-import { SettingsPage } from "./pages/SettingsPage";
-import { UNAUTHORIZED_EVENT, MAINTENANCE_EVENT } from "./services/api";
-import MaintenancePage from "./pages/MaintenancePage";
-import { usePlatform } from "./hooks/usePlatform";
-import { useAuthStore } from "./stores/authStore";
-import { DesktopAuthScreen } from "./pages/desktop/auth/DesktopAuthScreen";
-import { MobileBrowserStub } from "./pages/desktop/auth/MobileBrowserStub";
-import { DesktopLayout } from "./layouts/DesktopLayout/DesktopLayout";
-import { InvoicesDashboard } from "./pages/desktop/dashboard/InvoicesDashboard";
-
-// Компонент-заглушка для внешних браузеров
-const NotInTelegramScreen = () => (
-
-
- Перейти в бота
-
- }
- />
-
-);
-
-// Protected Route для десктопной версии
-const ProtectedDesktopRoute = ({ children }: { children: React.ReactNode }) => {
- const { isAuthenticated } = useAuthStore();
- const location = useLocation();
-
- if (!isAuthenticated) {
- return ;
- }
-
- return <>{children}>;
-};
-
-// Внутренний компонент с логикой, которая требует контекста роутера
-const AppContent = () => {
- const [isUnauthorized, setIsUnauthorized] = useState(false);
- const [isMaintenance, setIsMaintenance] = useState(false);
- const tg = window.Telegram?.WebApp;
- const platform = usePlatform();
- const location = useLocation(); // Теперь это безопасно, т.к. мы внутри BrowserRouter
-
- // Проверяем, есть ли данные от Telegram
- const isInTelegram = !!tg?.initData;
-
- // Проверяем, находимся ли мы на десктопном роуте
- const isDesktopRoute = location.pathname.startsWith("/web");
-
- useEffect(() => {
- const handleUnauthorized = () => setIsUnauthorized(true);
- const handleMaintenance = () => setIsMaintenance(true);
- window.addEventListener(UNAUTHORIZED_EVENT, handleUnauthorized);
- window.addEventListener(MAINTENANCE_EVENT, handleMaintenance);
-
- if (tg) {
- tg.expand();
- }
-
- return () => {
- window.removeEventListener(UNAUTHORIZED_EVENT, handleUnauthorized);
- window.removeEventListener(MAINTENANCE_EVENT, handleMaintenance);
- };
- }, [tg]);
-
- // Если открыто не в Telegram и это не десктопный роут — блокируем всё
- if (!isInTelegram && !isDesktopRoute) {
- return ;
- }
-
- // Если это десктопный роут и платформа - мобильный браузер
- if (isDesktopRoute && platform === "MobileBrowser") {
- return ;
- }
-
- // Если бэкенд вернул 401
- if (isUnauthorized) {
- return (
-
-
-
- );
- }
-
- // Если бэкенд вернул 503 (режим технического обслуживания)
- if (isMaintenance) {
- return ;
- }
-
- return (
-
- {/* Мобильные роуты (существующие) */}
- }>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
- {/* Десктопные роуты */}
- } />
- }>
-
-
-
- }
- />
-
-
- );
-};
-
-// Главный компонент-обертка
-function App() {
- return (
-
-
-
-
-
- );
-}
-
-export default App;
-
-```
-
-# ===================================================================
-# Файл: src/components/DragDropZone.tsx
-# ===================================================================
-
-```
-import React from 'react';
-import { useDropzone } from 'react-dropzone';
-import { InboxOutlined } from '@ant-design/icons';
-import { Typography } from 'antd';
-
-const { Text } = Typography;
-
-interface DragDropZoneProps {
- onDrop: (files: File[]) => void;
- accept?: Record;
- maxSize?: number;
- maxFiles?: number;
- disabled?: boolean;
- className?: string;
- children?: React.ReactNode;
-}
-
-/**
- * Компонент зоны перетаскивания файлов
- * Обертка над react-dropzone с Ant Design стилизацией
- */
-export const DragDropZone: React.FC = ({
- onDrop,
- accept = {
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
- 'application/vnd.ms-excel': ['.xls'],
- 'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp'],
- },
- maxSize = 10 * 1024 * 1024, // 10MB по умолчанию
- maxFiles = 10,
- disabled = false,
- className = '',
- children,
-}) => {
- const { getRootProps, getInputProps, isDragActive, isDragReject } = useDropzone({
- onDrop,
- accept,
- maxSize,
- maxFiles,
- disabled,
- });
-
- const getBorderColor = () => {
- if (isDragReject) return '#ff4d4f';
- if (isDragActive) return '#1890ff';
- return '#d9d9d9';
- };
-
- const getBackgroundColor = () => {
- if (isDragActive) return '#e6f7ff';
- if (disabled) return '#f5f5f5';
- return '#fafafa';
- };
-
- return (
-
-
- {children || (
-
-
-
- {isDragActive ? (
-
Отпустите файлы здесь
- ) : (
-
- Перетащите файлы сюда или нажмите для выбора
-
-
- Поддерживаются: .xlsx, .xls, изображения (макс. {maxSize / 1024 / 1024}MB)
-
-
- )}
-
-
- )}
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/components/invoices/CreateContainerModal.tsx
-# ===================================================================
-
-```
-import React, { useState } from 'react';
-import { Modal, Form, Input, InputNumber, Button, message } from 'antd';
-import { api } from '../../services/api';
-import type { ProductContainer } from '../../services/types';
-
-interface Props {
- visible: boolean;
- onCancel: () => void;
- productId: string;
- productBaseUnit: string;
- // Callback возвращает уже полный объект с ID от сервера
- onSuccess: (container: ProductContainer) => void;
-}
-
-export const CreateContainerModal: React.FC = ({
- visible, onCancel, productId, productBaseUnit, onSuccess
-}) => {
- const [loading, setLoading] = useState(false);
- const [form] = Form.useForm();
-
- const handleOk = async () => {
- try {
- const values = await form.validateFields();
- setLoading(true);
-
- // 1. Отправляем запрос на БЭКЕНД
- const res = await api.createContainer({
- product_id: productId,
- name: values.name,
- count: values.count
- });
-
- message.success('Фасовка создана');
-
- // 2. БЭКЕНД вернул ID. Теперь мы собираем объект для UI
- // Мы не придумываем ID сами, мы берем res.container_id
- const newContainer: ProductContainer = {
- id: res.container_id, // <--- ID от сервера
- name: values.name,
- count: values.count
- };
-
- // 3. Возвращаем полный объект родителю
- onSuccess(newContainer);
-
- form.resetFields();
- } catch {
- message.error('Ошибка создания фасовки');
- } finally {
- setLoading(false);
- }
- };
-
- return (
- Отмена,
- ,
- ]}
- >
-
-
-
-
-
-
-
-
- );
-};
-```
-
-# ===================================================================
-# Файл: src/components/invoices/DraftItemRow.tsx
-# ===================================================================
-
-```
-import React, { useMemo, useState, useEffect, useRef } from "react";
-import { Draggable } from "@hello-pangea/dnd";
-import {
- Card,
- Flex,
- InputNumber,
- Typography,
- Select,
- Tag,
- Button,
- Divider,
- Modal,
- Popconfirm,
-} from "antd";
-import {
- SyncOutlined,
- PlusOutlined,
- WarningFilled,
- DeleteOutlined,
-} from "@ant-design/icons";
-import { GripVertical } from "lucide-react";
-import { CatalogSelect } from "../ocr/CatalogSelect";
-import { CreateContainerModal } from "./CreateContainerModal";
-import type {
- DraftItem,
- UpdateDraftItemRequest,
- ProductSearchResult,
- ProductContainer,
- Recommendation,
-} from "../../services/types";
-
-const { Text } = Typography;
-
-interface Props {
- item: DraftItem;
- index: number;
- onUpdate: (itemId: string, changes: UpdateDraftItemRequest) => void;
- onDelete: (itemId: string) => void;
- isUpdating: boolean;
- recommendations?: Recommendation[];
- isReordering: boolean;
-}
-
-type FieldType = "quantity" | "price" | "sum";
-
-export const DraftItemRow: React.FC = ({
- item,
- index,
- onUpdate,
- onDelete,
- isUpdating,
- recommendations = [],
- isReordering,
-}) => {
- const [isModalOpen, setIsModalOpen] = useState(false);
-
- // --- Локальное состояние значений (строки для удобства ввода) ---
- const [localQty, setLocalQty] = useState(item.quantity);
- const [localPrice, setLocalPrice] = useState(item.price);
- const [localSum, setLocalSum] = useState(item.sum);
-
- // --- История редактирования (Stack) ---
- // Храним 2 последних отредактированных поля.
- // Инициализируем из пропсов или дефолтно ['quantity', 'price'], чтобы пересчитывалась сумма.
- const editStack = useRef([
- (item.last_edited_field_1 as FieldType) || "quantity",
- (item.last_edited_field_2 as FieldType) || "price",
- ]);
-
- // Храним ссылку на предыдущую версию item, чтобы сравнивать изменения
-
- // --- Синхронизация с сервером ---
- useEffect(() => {
- // Если мы ждем ответа от сервера, не сбиваем локальный ввод
- if (isUpdating) return;
-
- // Обновляем локальные стейты только когда меняются конкретные поля в item
- setLocalQty(item.quantity);
- setLocalPrice(item.price);
- setLocalSum(item.sum);
-
- // Обновляем стек редактирования
- if (item.last_edited_field_1 && item.last_edited_field_2) {
- editStack.current = [
- item.last_edited_field_1 as FieldType,
- item.last_edited_field_2 as FieldType,
- ];
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- // Зависим ТОЛЬКО от примитивов. Если объект item изменится, но цифры те же - эффект не сработает.
- item.quantity,
- item.price,
- item.sum,
- item.last_edited_field_1,
- item.last_edited_field_2,
- isUpdating,
- ]);
-
- // --- Логика пересчета (Треугольник) ---
- const recalculateLocally = (changedField: FieldType, newVal: number) => {
- // 1. Обновляем стек истории
- // Удаляем поле, если оно уже было в стеке, и добавляем в начало (LIFO для важности)
- const currentStack = editStack.current.filter((f) => f !== changedField);
- currentStack.unshift(changedField);
- // Оставляем только 2 последних
- if (currentStack.length > 2) currentStack.pop();
- editStack.current = currentStack;
-
- // 2. Определяем, какое поле нужно пересчитать (то, которого НЕТ в стеке)
- const allFields: FieldType[] = ["quantity", "price", "sum"];
- const fieldToRecalc = allFields.find((f) => !currentStack.includes(f));
-
- // 3. Выполняем расчет
- let q = changedField === "quantity" ? newVal : localQty || 0;
- let p = changedField === "price" ? newVal : localPrice || 0;
- let s = changedField === "sum" ? newVal : localSum || 0;
-
- switch (fieldToRecalc) {
- case "sum":
- s = q * p;
- setLocalSum(s);
- break;
- case "quantity":
- if (p !== 0) {
- q = s / p;
- setLocalQty(q);
- } else {
- setLocalQty(0);
- }
- break;
- case "price":
- if (q !== 0) {
- p = s / q;
- setLocalPrice(p);
- } else {
- setLocalPrice(0);
- }
- break;
- }
- };
-
- // --- Обработчики ввода ---
-
- const handleValueChange = (field: FieldType, val: number | null) => {
- // Обновляем само поле
- if (field === "quantity") setLocalQty(val);
- if (field === "price") setLocalPrice(val);
- if (field === "sum") setLocalSum(val);
-
- if (val !== null) {
- recalculateLocally(field, val);
- }
- };
-
- const handleBlur = (field: FieldType) => {
- // Отправляем на сервер только измененное поле + маркер edited_field.
- // Сервер сам проведет пересчет и вернет точные данные.
- // Важно: отправляем текущее локальное значение.
-
- let val: number | null = null;
- if (field === "quantity") val = localQty;
- if (field === "price") val = localPrice;
- if (field === "sum") val = localSum;
-
- if (val === null) return;
-
- // Сравниваем с текущим item, чтобы не спамить запросами, если число не поменялось
- const serverVal = item[field];
- // Используем эпсилон для сравнения float
- if (Math.abs(val - serverVal) > 0.0001) {
- onUpdate(item.id, {
- [field]: val,
- edited_field: field,
- });
- }
- };
-
- // --- Product & Container Logic (как было) ---
- const [searchedProduct, setSearchedProduct] =
- useState(null);
- const [addedContainers, setAddedContainers] = useState<
- Record
- >({});
-
- const activeProduct = useMemo(() => {
- if (searchedProduct && searchedProduct.id === item.product_id)
- return searchedProduct;
- return item.product as unknown as ProductSearchResult | undefined;
- }, [searchedProduct, item.product, item.product_id]);
-
- const containers = useMemo(() => {
- if (!activeProduct) return [];
- const baseContainers = activeProduct.containers || [];
- const manuallyAdded = addedContainers[activeProduct.id] || [];
- const combined = [...baseContainers];
- manuallyAdded.forEach((c) => {
- if (!combined.find((existing) => existing.id === c.id)) combined.push(c);
- });
- return combined;
- }, [activeProduct, addedContainers]);
-
- const baseUom =
- activeProduct?.main_unit?.name || activeProduct?.measure_unit || "ед.";
-
- const containerOptions = useMemo(() => {
- if (!activeProduct) return [];
- const opts = [
- { value: "BASE_UNIT", label: `Базовая (${baseUom})` },
- ...containers.map((c) => ({
- value: c.id,
- label: `${c.name} (=${Number(c.count)} ${baseUom})`,
- })),
- ];
- if (
- item.container_id &&
- item.container &&
- !containers.find((c) => c.id === item.container_id)
- ) {
- opts.push({
- value: item.container.id,
- label: `${item.container.name} (=${Number(
- item.container.count
- )} ${baseUom})`,
- });
- }
- return opts;
- }, [activeProduct, containers, baseUom, item.container_id, item.container]);
-
- // --- WARNING LOGIC ---
- const activeWarning = useMemo(() => {
- if (!item.product_id) return null;
- return recommendations.find((r) => r.ProductID === item.product_id);
- }, [item.product_id, recommendations]);
-
- const showWarningModal = () => {
- if (!activeWarning) return;
- Modal.warning({
- title: "Внимание: проблемный товар",
- content: (
-
-
- {activeWarning.ProductName}
-
-
{activeWarning.Reason}
-
- {activeWarning.Type}
-
-
- ),
- okText: "Понятно",
- maskClosable: true,
- });
- };
-
- // --- Handlers ---
- const handleProductChange = (
- prodId: string,
- productObj?: ProductSearchResult
- ) => {
- if (productObj) setSearchedProduct(productObj);
- onUpdate(item.id, {
- product_id: prodId,
- container_id: null, // Сбрасываем фасовку
- // При смене товара логично оставить Qty и Sum, пересчитав Price?
- // Или оставить Qty и Price? Обычно цена меняется.
- // Пока не трогаем числа, пусть остаются как были.
- });
- };
-
- const handleContainerChange = (val: string) => {
- // "" пустая строка приходит при выборе "Базовая" (мы так настроим value)
- const newVal = val === "BASE_UNIT" ? "" : val;
- onUpdate(item.id, { container_id: newVal });
- };
-
- const handleContainerCreated = (newContainer: ProductContainer) => {
- setIsModalOpen(false);
- if (activeProduct) {
- setAddedContainers((prev) => ({
- ...prev,
- [activeProduct.id]: [...(prev[activeProduct.id] || []), newContainer],
- }));
- }
- onUpdate(item.id, { container_id: newContainer.id });
- };
-
- const cardBorderColor = !item.product_id
- ? "#ffa39e"
- : item.is_matched
- ? "#b7eb8f"
- : "#d9d9d9";
-
- return (
- <>
-
- {(provided, snapshot) => {
- const style = {
- marginBottom: "8px",
- backgroundColor: snapshot.isDragging ? "#e6f7ff" : "transparent",
- boxShadow: snapshot.isDragging
- ? "0 4px 12px rgba(0, 0, 0, 0.15)"
- : "none",
- borderRadius: "4px",
- transition: "background-color 0.2s ease, box-shadow 0.2s ease",
- ...provided.draggableProps.style,
- };
-
- return (
-
-
- {/* Drag handle - иконка для перетаскивания (показываем только в режиме перетаскивания) */}
- {isReordering && (
- {
- e.currentTarget.style.color = "#1890ff";
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = "#8c8c8c";
- }}
- >
-
-
- )}
-
-
-
-
-
- {item.raw_name || "Новая позиция"}
-
- {item.raw_amount > 0 && (
-
- (чек: {item.raw_amount} x {item.raw_price})
-
- )}
-
-
- {isUpdating && (
-
- )}
-
- {activeWarning && (
-
- )}
-
- {!item.product_id && (
-
- ?
-
- )}
-
-
onDelete(item.id)}
- okText="Да"
- cancelText="Нет"
- placement="left"
- >
- }
- danger
- style={{ marginLeft: 4 }}
- />
-
-
-
-
-
-
- {activeProduct && (
-
-
-
- );
- }}
-
- {activeProduct && (
- setIsModalOpen(false)}
- productId={activeProduct.id}
- productBaseUnit={baseUom}
- onSuccess={handleContainerCreated}
- />
- )}
- >
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/components/layout/AppLayout.tsx
-# ===================================================================
-
-```
-import React from "react";
-import { Layout, theme } from "antd";
-import { Outlet, useNavigate, useLocation } from "react-router-dom";
-import {
- ScanOutlined,
- FileTextOutlined,
- SettingOutlined,
-} from "@ant-design/icons";
-
-const { Content } = Layout;
-
-export const AppLayout: React.FC = () => {
- const navigate = useNavigate();
- const location = useLocation();
-
- const {
- token: { colorBgContainer, colorPrimary, colorTextSecondary },
- } = theme.useToken();
-
- const path = location.pathname;
- let activeKey = "invoices";
- if (path.startsWith("/ocr")) activeKey = "ocr";
- else if (path.startsWith("/settings")) activeKey = "settings";
-
- const menuItems = [
- {
- key: "invoices",
- icon: ,
- label: "Накладные",
- path: "/invoices",
- },
- {
- key: "ocr",
- icon: ,
- label: "Обучение",
- path: "/ocr",
- },
- {
- key: "settings",
- icon: ,
- label: "Настройки",
- path: "/settings",
- },
- ];
-
- return (
-
- {/* Верхнюю шапку (Header) удалили для экономии места */}
-
-
- {/* Убрали лишние паддинги вокруг контента для мобилок */}
-
-
-
-
-
- {/* Нижний Таб-бар */}
-
- {menuItems.map((item) => {
- const isActive = activeKey === item.key;
- return (
-
navigate(item.path)}
- style={{
- display: "flex",
- flexDirection: "column",
- alignItems: "center",
- justifyContent: "center",
- width: "33%",
- cursor: "pointer",
- color: isActive ? colorPrimary : colorTextSecondary,
- }}
- >
- {item.icon}
-
- {item.label}
-
-
- );
- })}
-
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/components/layout/Providers.tsx
-# ===================================================================
-
-```
-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}
-
- );
-};
-```
-
-# ===================================================================
-# Файл: src/components/ocr/AddMatchForm.tsx
-# ===================================================================
-
-```
-import React, { useState, useMemo, useEffect } from "react";
-import {
- Card,
- Button,
- Flex,
- AutoComplete,
- Input,
- InputNumber,
- Typography,
- Select,
- Divider,
- Popconfirm,
-} from "antd";
-import {
- PlusOutlined,
- DeleteOutlined,
- EditOutlined,
- CloseOutlined,
-} from "@ant-design/icons";
-import { CatalogSelect } from "./CatalogSelect";
-import { CreateContainerModal } from "../invoices/CreateContainerModal";
-import type {
- CatalogItem,
- UnmatchedItem,
- ProductSearchResult,
- ProductContainer,
- ProductMatch,
-} from "../../services/types";
-
-const { Text } = Typography;
-
-interface Props {
- catalog: CatalogItem[];
- unmatched?: UnmatchedItem[];
- onSave: (
- rawName: string,
- productId: string,
- quantity: number,
- containerId?: string
- ) => void;
- onDeleteUnmatched?: (rawName: string) => void;
- isLoading: boolean;
- initialValues?: ProductMatch; // Для редактирования
- onCancelEdit?: () => void; // Для сброса режима редактирования
-}
-
-export const AddMatchForm: React.FC = ({
- catalog,
- unmatched = [],
- onSave,
- onDeleteUnmatched,
- isLoading,
- initialValues,
- onCancelEdit,
-}) => {
- // --- Состояния ---
- const [rawName, setRawName] = useState("");
- const [selectedProduct, setSelectedProduct] = useState(
- undefined
- );
- // Храним полный объект товара, чтобы достать из него фасовки и имя для отображения
- const [selectedProductData, setSelectedProductData] = useState<
- ProductSearchResult | undefined
- >(undefined);
- const [quantity, setQuantity] = useState(1);
- const [selectedContainer, setSelectedContainer] = useState(
- null
- );
-
- const [isModalOpen, setIsModalOpen] = useState(false);
-
- // --- Эффект для инициализации полей при редактировании ---
- useEffect(() => {
- if (initialValues) {
- // eslint-disable-next-line
- setRawName(initialValues.raw_name || "");
-
- const prodId = initialValues.product?.id;
- setSelectedProduct(prodId);
-
- // Важно: восстанавливаем объект продукта из initialValues
- // Приводим тип, так как DTO могут немного отличаться, но нам нужны containers и name
- const prodData = initialValues.product as unknown as ProductSearchResult;
- setSelectedProductData(prodData);
-
- setQuantity(Number(initialValues.quantity) || 1);
- setSelectedContainer(initialValues.container?.id || null);
- } else {
- // РЕЖИМ СОЗДАНИЯ (Сброс)
- setRawName("");
- setSelectedProduct(undefined);
- setSelectedProductData(undefined);
- setQuantity(1);
- setSelectedContainer(null);
- }
- }, [initialValues]);
-
- // --- Вычисляемые значения ---
-
- const unmatchedOptions = useMemo(() => {
- return unmatched.map((item) => ({
- value: item.raw_name,
- label: item.count ? `${item.raw_name} (${item.count} шт)` : item.raw_name,
- }));
- }, [unmatched]);
-
- // Активный продукт: либо то, что выбрали в поиске, либо то, что пришло из редактирования
- const activeProduct = useMemo(() => {
- if (selectedProductData) return selectedProductData;
- // Фоллбэк: пытаемся найти в общем каталоге (если он загружен полностью, что редко)
- if (selectedProduct && catalog.length > 0) {
- return catalog.find(
- (item) => item.id === selectedProduct
- ) as unknown as ProductSearchResult;
- }
- return undefined;
- }, [selectedProduct, selectedProductData, catalog]);
-
- // Список контейнеров текущего товара
- const containers = useMemo(() => {
- return activeProduct?.containers || [];
- }, [activeProduct]);
-
- // Базовая единица
- const baseUom =
- activeProduct?.main_unit?.name || activeProduct?.measure_unit || "ед.";
-
- // Текстовое отображение текущей единицы (для инпута количества)
- const currentUomName = useMemo(() => {
- if (selectedContainer) {
- const cont = containers.find((c) => c.id === selectedContainer);
- return cont ? cont.name : baseUom;
- }
- return baseUom;
- }, [selectedContainer, containers, baseUom]);
-
- const isButtonDisabled =
- !rawName.trim() ||
- !selectedProduct ||
- quantity === null ||
- quantity <= 0 ||
- isLoading;
-
- // --- Хендлеры ---
-
- const handleProductChange = (
- val: string,
- productObj?: ProductSearchResult
- ) => {
- setSelectedProduct(val);
- if (productObj) {
- setSelectedProductData(productObj);
- }
- // При смене товара сбрасываем фасовку
- setSelectedContainer(null);
- };
-
- const handleSubmit = () => {
- let quantityValue = quantity;
-
- // Защита от null/строк
- if (quantityValue === null || quantityValue === undefined) {
- quantityValue = 1;
- } else if (typeof quantityValue === "string") {
- quantityValue = parseFloat(quantityValue);
- }
- if (isNaN(quantityValue) || quantityValue <= 0) {
- quantityValue = 1;
- }
-
- if (rawName.trim() && selectedProduct) {
- onSave(
- rawName,
- selectedProduct,
- quantityValue,
- selectedContainer || undefined
- );
-
- // Если это не редактирование, очищаем форму
- if (!initialValues) {
- setRawName("");
- setSelectedProduct(undefined);
- setSelectedProductData(undefined);
- setQuantity(1);
- setSelectedContainer(null);
- }
- }
- };
-
- const handleContainerCreated = (newContainer: ProductContainer) => {
- setIsModalOpen(false);
-
- // Добавляем созданную фасовку в локальный стейт продукта
- if (selectedProductData) {
- setSelectedProductData({
- ...selectedProductData,
- containers: [...(selectedProductData.containers || []), newContainer],
- });
- } else if (activeProduct) {
- setSelectedProductData({
- ...activeProduct,
- containers: [...(activeProduct.containers || []), newContainer],
- });
- }
-
- // Выбираем новую фасовку
- setSelectedContainer(newContainer.id);
- };
-
- const handleDeleteUnmatched = () => {
- if (onDeleteUnmatched && rawName.trim()) {
- onDeleteUnmatched(rawName);
- setRawName("");
- }
- };
-
- // Кнопка "Сбросить" вызывает внешний обработчик отмены редактирования
- const handleCancel = () => {
- if (onCancelEdit) {
- onCancelEdit();
- }
- };
-
- return (
-
-
- {/* Поле: Текст из чека */}
-
-
- Текст из чека (Raw Name):
-
-
-
- !inputValue ||
- (option?.value as string)
- .toLowerCase()
- .includes(inputValue.toLowerCase())
- }
- style={{ flex: 1 }}
- >
-
-
- {onDeleteUnmatched && !initialValues && (
-
- }
- disabled={!rawName.trim()}
- title="Удалить мусорную строку"
- />
-
- )}
-
-
-
- {/* Поле: Товар */}
-
-
- {/* Поле: Фасовка */}
- {activeProduct && (
-
-
- Единица измерения / Фасовка:
-
-
- )}
-
- {/* Поле: Количество */}
-
-
- Коэффициент (сколько товара в одной позиции чека):
-
-
- setQuantity(Number(val))}
- style={{ flex: 1 }}
- placeholder="1"
- />
- {currentUomName}
-
-
-
- {/* Кнопки действий */}
-
-
:
}
- onClick={handleSubmit}
- loading={isLoading}
- disabled={isButtonDisabled}
- block
- >
- {initialValues ? "Сохранить изменения" : "Добавить связь"}
-
-
- {initialValues && (
-
}
- title="Отменить редактирование"
- >
- Отмена
-
- )}
-
-
-
- {/* Модалка создания фасовки */}
- {activeProduct && (
- setIsModalOpen(false)}
- productId={activeProduct.id}
- productBaseUnit={baseUom}
- onSuccess={handleContainerCreated}
- />
- )}
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/components/ocr/CatalogSelect.tsx
-# ===================================================================
-
-```
-import React, { useState, useEffect, useRef } from "react";
-import { Select, Spin } from "antd";
-import { api } from "../../services/api";
-import type { CatalogItem, ProductSearchResult } from "../../services/types";
-
-interface Props {
- value?: string;
- onChange?: (value: string, productObj?: ProductSearchResult) => void;
- disabled?: boolean;
- initialProduct?: CatalogItem | ProductSearchResult;
-}
-
-// Интерфейс для элемента выпадающего списка
-interface SelectOption {
- label: string;
- value: string;
- data: ProductSearchResult;
-}
-
-export const CatalogSelect: React.FC = ({
- value,
- onChange,
- disabled,
- initialProduct,
-}) => {
- const [options, setOptions] = useState([]);
- const [fetching, setFetching] = useState(false);
- const [notFound, setNotFound] = useState(false);
-
- const fetchRef = useRef(null);
-
- useEffect(() => {
- if (initialProduct && initialProduct.id === value) {
- const name = initialProduct.name;
- const code = initialProduct.code;
- setOptions([
- {
- label: code ? `${name} [${code}]` : name,
- value: initialProduct.id,
- data: initialProduct as ProductSearchResult,
- },
- ]);
- }
- }, [initialProduct, value]);
-
- const fetchProducts = async (search: string) => {
- if (!search) {
- setOptions([]);
- setNotFound(false);
- return;
- }
- setFetching(true);
- // Не сбрасываем options сразу, чтобы не моргало
- try {
- const results = await api.searchProducts(search);
- const newOptions = results.map((item) => ({
- label: item.code ? `${item.name} [${item.code}]` : item.name,
- value: item.id,
- data: item,
- }));
- setOptions(newOptions);
- // Показываем "Не найдено" если результатов нет
- setNotFound(results.length === 0);
- } catch (e) {
- console.error(e);
- setNotFound(true);
- } finally {
- setFetching(false);
- }
- };
-
- const handleSearch = (val: string) => {
- if (fetchRef.current !== null) {
- window.clearTimeout(fetchRef.current);
- }
- // Сбрасываем notFound при новом поиске
- setNotFound(false);
- // Запускаем поиск только если введено хотя бы 2 символа
- if (val.length < 2) {
- return;
- }
- fetchRef.current = window.setTimeout(() => {
- fetchProducts(val);
- }, 500);
- };
-
- const handleChange = (
- val: string,
- option: SelectOption | SelectOption[] | undefined
- ) => {
- if (onChange) {
- const opt = Array.isArray(option) ? option[0] : option;
- onChange(val, opt?.data);
- }
- };
-
- return (
-
- ) : notFound ? (
- Товар не найден
- ) : null
- }
- options={options}
- value={value}
- onChange={handleChange}
- disabled={disabled}
- style={{ width: "100%" }}
- listHeight={256}
- allowClear
- // При очистке сбрасываем опции и notFound, чтобы при следующем клике не вылезал старый товар
- onClear={() => {
- setOptions([]);
- setNotFound(false);
- }}
- // При клике (фокусе), если поле пустое - можно показать дефолтные опции или оставить пустым
- onFocus={() => {
- if (!value) setOptions([]);
- }}
- />
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/components/ocr/MatchList.tsx
-# ===================================================================
-
-```
-import React from "react";
-import { List, Typography, Tag, Input, Empty, Button, Popconfirm } from "antd";
-import {
- ArrowRightOutlined,
- SearchOutlined,
- DeleteOutlined,
- EditOutlined,
-} from "@ant-design/icons";
-import type { ProductMatch } from "../../services/types";
-
-const { Text } = Typography;
-
-interface Props {
- matches: ProductMatch[];
- onDeleteMatch?: (rawName: string) => void;
- onEditMatch?: (match: ProductMatch) => void;
- isDeleting?: boolean;
-}
-
-export const MatchList: React.FC = ({
- matches,
- onDeleteMatch,
- onEditMatch,
- isDeleting = false,
-}) => {
- const [searchText, setSearchText] = React.useState("");
-
- const filteredData = matches.filter((item) => {
- const raw = (item.raw_name || "").toLowerCase();
- const prod = item.product;
- const prodName = (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) => {
- // Унификация полей (только snake_case)
- const rawName = item.raw_name || "Без названия";
- const product = item.product;
- const productName = product?.name || "Товар не найден";
- const qty = item.quantity || 1;
-
- // Логика отображения Единицы или Фасовки
- const container = item.container;
- let displayUnit = "";
-
- if (container) {
- // Если есть фасовка: "Пачка 180г"
- displayUnit = container.name;
- } else {
- // Иначе базовая ед.: "кг"
- displayUnit = product?.measure_unit || "ед.";
- }
-
- return (
-
-
-
- Чек
- {rawName}
-
-
-
-
- {productName}
-
- x {qty} {displayUnit}
-
-
-
-
- {(onDeleteMatch || onEditMatch) && (
-
- {onEditMatch && (
-
}
- onClick={() => onEditMatch(item)}
- size="small"
- >
- Редактировать
-
- )}
- {onDeleteMatch && (
-
onDeleteMatch(rawName)}
- okText="Да"
- cancelText="Нет"
- >
- }
- loading={isDeleting}
- size="small"
- >
- Удалить
-
-
- )}
-
- )}
-
- );
- }}
- />
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/components/recommendations/RecommendationCard.tsx
-# ===================================================================
-
-```
-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()}
-
- {/* Кнопка действия (заглушка на будущее) */}
-
-
-
- );
-};
-```
-
-# ===================================================================
-# Файл: src/components/settings/PhotoStorageTab.tsx
-# ===================================================================
-
-```
-import React, { useState } from "react";
-import {
- Card,
- Image,
- Button,
- Popconfirm,
- Tag,
- Pagination,
- Empty,
- Spin,
- message,
- Tooltip,
-} from "antd";
-import {
- DeleteOutlined,
- ReloadOutlined,
- FileImageOutlined,
- CheckCircleOutlined,
- FileTextOutlined,
-} from "@ant-design/icons";
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
-import { api, getStaticUrl } from "../../services/api";
-import { AxiosError } from "axios";
-import type { ReceiptPhoto, PhotoStatus } from "../../services/types";
-
-export const PhotoStorageTab: React.FC = () => {
- const [page, setPage] = useState(1);
- const queryClient = useQueryClient();
-
- const { data, isLoading, isError } = useQuery({
- queryKey: ["photos", page],
- queryFn: () => api.getPhotos(page, 18), // 18 - удобно делится на 2, 3, 6 колонок
- });
-
- const deleteMutation = useMutation({
- mutationFn: ({ id, force }: { id: string; force: boolean }) =>
- api.deletePhoto(id, force),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["photos"] });
- message.success("Фото удалено");
- },
- // Исправленная типизация:
- onError: (error: AxiosError<{ error: string }>) => {
- if (error.response?.status === 409) {
- message.warning(
- "Это фото связано с черновиком. Используйте кнопку 'Удалить' с подтверждением."
- );
- } else {
- message.error(error.response?.data?.error || "Ошибка удаления");
- }
- },
- });
-
- const regenerateMutation = useMutation({
- mutationFn: (id: string) => api.regenerateDraftFromPhoto(id),
- onSuccess: () => {
- message.success("Черновик восстановлен");
- // Можно редиректить, но пока просто обновим список
- },
- onError: () => {
- message.error("Ошибка восстановления");
- },
- });
-
- const getStatusTag = (status: PhotoStatus) => {
- switch (status) {
- case "ORPHAN":
- return Без привязки;
- case "HAS_DRAFT":
- return (
- } color="processing">
- Черновик
-
- );
- case "HAS_INVOICE":
- return (
- } color="success">
- В iiko
-
- );
- default:
- return {status};
- }
- };
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- if (isError) {
- return ;
- }
-
- if (!data?.photos?.length) {
- return ;
- }
-
- return (
-
-
- {data.photos.map((photo: ReceiptPhoto) => (
-
- }}
- />
-
- }
- actions={[
- photo.can_regenerate ? (
-
- }
- onClick={() => regenerateMutation.mutate(photo.id)}
- loading={regenerateMutation.isPending}
- size="small"
- />
-
- ) : (
-
- ), // Placeholder для выравнивания
-
- photo.can_delete ? (
-
- deleteMutation.mutate({
- id: photo.id,
- force: photo.status === "HAS_DRAFT",
- })
- }
- okText="Удалить"
- cancelText="Отмена"
- okButtonProps={{ danger: true }}
- >
- }
- size="small"
- />
-
- ) : (
-
- }
- size="small"
- />
-
- ),
- ]}
- >
-
- {new Date(photo.created_at).toLocaleDateString()}
-
- }
- description={getStatusTag(photo.status)}
- />
-
- ))}
-
-
-
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/components/settings/TeamList.tsx
-# ===================================================================
-
-```
-import React from "react";
-import {
- List,
- Avatar,
- Tag,
- Button,
- Select,
- Popconfirm,
- message,
- Spin,
- Alert,
- Typography,
-} from "antd";
-import { DeleteOutlined, UserOutlined } from "@ant-design/icons";
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
-import { api } from "../../services/api";
-import type { ServerUser, UserRole } from "../../services/types";
-
-const { Text } = Typography;
-
-interface Props {
- currentUserRole: UserRole;
-}
-
-export const TeamList: React.FC = ({ currentUserRole }) => {
- const queryClient = useQueryClient();
-
- // Запрос списка пользователей
- const {
- data: users,
- isLoading,
- isError,
- } = useQuery({
- queryKey: ["serverUsers"],
- queryFn: api.getUsers,
- });
-
- // Мутация изменения роли
- const updateRoleMutation = useMutation({
- mutationFn: ({ userId, newRole }: { userId: string; newRole: UserRole }) =>
- api.updateUserRole(userId, newRole),
- onSuccess: () => {
- message.success("Роль пользователя обновлена");
- queryClient.invalidateQueries({ queryKey: ["serverUsers"] });
- },
- onError: () => {
- message.error("Не удалось изменить роль");
- },
- });
-
- // Мутация удаления пользователя
- const removeUserMutation = useMutation({
- mutationFn: (userId: string) => api.removeUser(userId),
- onSuccess: () => {
- message.success("Пользователь удален из команды");
- queryClient.invalidateQueries({ queryKey: ["serverUsers"] });
- },
- onError: () => {
- message.error("Не удалось удалить пользователя");
- },
- });
-
- // Хелперы для UI
- const getRoleColor = (role: UserRole) => {
- switch (role) {
- case "OWNER":
- return "gold";
- case "ADMIN":
- return "blue";
- case "OPERATOR":
- return "default";
- default:
- return "default";
- }
- };
-
- const getRoleName = (role: UserRole) => {
- switch (role) {
- case "OWNER":
- return "Владелец";
- case "ADMIN":
- return "Админ";
- case "OPERATOR":
- return "Оператор";
- default:
- return role;
- }
- };
-
- // Проверка прав на удаление
- const canDelete = (targetUser: ServerUser) => {
- if (targetUser.is_me) return false; // Себя удалить нельзя
- if (targetUser.role === "OWNER") return false; // Владельца удалить нельзя
- if (currentUserRole === "ADMIN" && targetUser.role === "ADMIN")
- return false; // Админ не может удалить админа
- return true;
- };
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- if (isError) {
- return ;
- }
-
- return (
- <>
-
-
- (
-
- updateRoleMutation.mutate({
- userId: user.user_id,
- newRole: val,
- })
- }
- options={[
- { value: "ADMIN", label: "Админ" },
- { value: "OPERATOR", label: "Оператор" },
- ]}
- />
- ) : (
-
- {getRoleName(user.role)}
-
- ),
-
- // Кнопка удаления
- removeUserMutation.mutate(user.user_id)}
- disabled={!canDelete(user)}
- okText="Да"
- cancelText="Нет"
- >
- }
- disabled={!canDelete(user) || removeUserMutation.isPending}
- />
- ,
- ]}
- >
- }>
- {user.first_name?.[0]}
-
- }
- title={
-
- {user.first_name} {user.last_name}{" "}
- {user.is_me && (Вы)}
-
- }
- description={
- user.username ? (
-
- @{user.username}
-
- ) : (
-
- Нет username
-
- )
- }
- />
-
- )}
- />
- >
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/hooks/useOcr.ts
-# ===================================================================
-
-```
-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('Ошибка при сохранении');
- },
- });
-
- const deleteMatchMutation = useMutation({
- mutationFn: (rawName: string) => api.deleteMatch(rawName),
- onSuccess: () => {
- message.success('Связь удалена');
- queryClient.invalidateQueries({ queryKey: ['matches'] });
- queryClient.invalidateQueries({ queryKey: ['unmatched'] });
- },
- onError: () => {
- message.error('Ошибка при удалении связи');
- },
- });
-
- const deleteUnmatchedMutation = useMutation({
- mutationFn: (rawName: string) => api.deleteUnmatched(rawName),
- onSuccess: () => {
- message.success('Нераспознанная строка удалена');
- 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,
- deleteMatch: deleteMatchMutation.mutate,
- isDeletingMatch: deleteMatchMutation.isPending,
- deleteUnmatched: deleteUnmatchedMutation.mutate,
- isDeletingUnmatched: deleteUnmatchedMutation.isPending,
- };
-};
-```
-
-# ===================================================================
-# Файл: src/hooks/usePlatform.ts
-# ===================================================================
-
-```
-import { useMemo } from 'react';
-
-export type Platform = 'MobileApp' | 'Desktop' | 'MobileBrowser';
-
-/**
- * Хук для определения текущей платформы
- * MobileApp - если есть специфические признаки мобильного приложения
- * Desktop - если это десктопный браузер
- * MobileBrowser - если это мобильный браузер
- */
-export const usePlatform = (): Platform => {
- return useMemo(() => {
- const userAgent = navigator.userAgent;
-
- // Проверка на мобильное приложение (специфические признаки)
- // Можно добавить дополнительные проверки для конкретных приложений
- const isMobileApp = /rmser-app|mobile-app|cordova|phonegap/i.test(userAgent);
-
- if (isMobileApp) {
- return 'MobileApp';
- }
-
- // Проверка на мобильный браузер
- const isMobileBrowser = /android|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent);
-
- if (isMobileBrowser) {
- return 'MobileBrowser';
- }
-
- // По умолчанию - десктоп
- return 'Desktop';
- }, []);
-};
-
-```
-
-# ===================================================================
-# Файл: src/hooks/useRecommendations.ts
-# ===================================================================
-
-```
-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,
- });
-};
-```
-
-# ===================================================================
-# Файл: src/hooks/useWebSocket.ts
-# ===================================================================
-
-```
-import { useEffect, useState, useRef } from 'react';
-
-const apiUrl = import.meta.env.VITE_API_URL || '';
-
-// Определяем базовый URL для WS (меняем http->ws, https->wss)
-const getWsUrl = () => {
- let baseUrl = apiUrl;
- if (baseUrl.startsWith('/')) {
- baseUrl = window.location.origin;
- } else if (!baseUrl) {
- baseUrl = 'http://localhost:8080';
- }
-
- // Заменяем протокол
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
- const host = baseUrl.replace(/^http(s)?:\/\//, '');
- // Важно: путь /socket.io/ оставлен для совместимости с Nginx конфигом
- return `${protocol}//${host}/socket.io/`;
-};
-
-interface WsEvent {
- event: string;
- data: unknown;
-}
-
-export const useWebSocket = (sessionId: string | null) => {
- const [isConnected, setIsConnected] = useState(false);
- const [lastError, setLastError] = useState(null);
- const [lastMessage, setLastMessage] = useState(null);
-
- const wsRef = useRef(null);
-
- useEffect(() => {
- if (!sessionId) return;
-
- const url = `${getWsUrl()}?session_id=${sessionId}`;
- console.log('🔌 Connecting Native WS:', url);
-
- const ws = new WebSocket(url);
- wsRef.current = ws;
-
- ws.onopen = () => {
- console.log('✅ WS Connected');
- setIsConnected(true);
- setLastError(null);
- };
-
- ws.onclose = (event) => {
- console.log('⚠️ WS Closed', event.code, event.reason);
- setIsConnected(false);
- };
-
- ws.onerror = (error) => {
- console.error('❌ WS Error', error);
- setLastError('Connection error');
- setIsConnected(false);
- };
-
- ws.onmessage = (event) => {
- try {
- const parsed = JSON.parse(event.data);
- console.log('📨 WS Message:', parsed);
- setLastMessage(parsed);
- } catch {
- console.error('Failed to parse WS message', event.data);
- }
- };
-
- return () => {
- console.log('🧹 WS Cleanup');
- ws.close();
- };
- }, [sessionId]);
-
- return { isConnected, lastError, lastMessage };
-};
-
-```
-
-# ===================================================================
-# Файл: src/layouts/DesktopLayout/DesktopHeader.tsx
-# ===================================================================
-
-```
-import React from 'react';
-import { Layout, Space, Avatar, Dropdown, Button } from 'antd';
-import { UserOutlined, LogoutOutlined } from '@ant-design/icons';
-import { useAuthStore } from '../../stores/authStore';
-
-const { Header } = Layout;
-
-/**
- * Header для десктопной версии
- * Содержит логотип, заглушку выбора сервера и аватар пользователя
- */
-export const DesktopHeader: React.FC = () => {
- const { user, logout } = useAuthStore();
-
- const handleLogout = () => {
- logout();
- window.location.href = '/web';
- };
-
- const userMenuItems = [
- {
- key: 'logout',
- label: 'Выйти',
- icon: ,
- onClick: handleLogout,
- },
- ];
-
- return (
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/layouts/DesktopLayout/DesktopLayout.tsx
-# ===================================================================
-
-```
-import React from 'react';
-import { Layout } from 'antd';
-import { Outlet } from 'react-router-dom';
-import { DesktopHeader } from './DesktopHeader.tsx';
-
-const { Content } = Layout;
-
-/**
- * Основной layout для десктопной версии
- * Использует Ant Design Layout с фиксированным Header
- */
-export const DesktopLayout: React.FC = () => {
- return (
-
-
-
-
-
-
-
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/main.tsx
-# ===================================================================
-
-```
-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(
-
-
- ,
-)
-```
-
-# ===================================================================
-# Файл: src/pages/Dashboard.tsx
-# ===================================================================
-
-```
-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) => (
-
- ))
- ) : (
-
- )}
-
- );
-};
-```
-
-# ===================================================================
-# Файл: src/pages/DraftsList.tsx
-# ===================================================================
-
-```
-// src/pages/DraftsList.tsx
-
-import React, { useState, useMemo } from "react";
-import { useQuery } from "@tanstack/react-query";
-import {
- List,
- Typography,
- Tag,
- Spin,
- Empty,
- Flex,
- Button,
- Select,
- DatePicker,
-} from "antd";
-import { useNavigate } from "react-router-dom";
-import {
- CheckCircleOutlined,
- DeleteOutlined,
- PlusOutlined,
- ExclamationCircleOutlined,
- LoadingOutlined,
- CloseCircleOutlined,
- StopOutlined,
- SyncOutlined,
- CloudServerOutlined,
-} from "@ant-design/icons";
-import dayjs from "dayjs";
-import "dayjs/locale/ru";
-import { api } from "../services/api";
-import type { UnifiedInvoice } from "../services/types";
-
-const { Title, Text } = Typography;
-
-type FilterType = "ALL" | "DRAFT" | "SYNCED";
-
-dayjs.locale("ru");
-
-const DayDivider: React.FC<{ date: string }> = ({ date }) => {
- const d = dayjs(date);
- const dayOfWeek = d.format("dddd");
- const formattedDate = d.format("D MMMM YYYY");
-
- return (
-
-
- {formattedDate}
-
-
- {dayOfWeek}
-
-
- );
-};
-
-export const DraftsList: React.FC = () => {
- const navigate = useNavigate();
-
- const [syncLoading, setSyncLoading] = useState(false);
- const [filterType, setFilterType] = useState("ALL");
- const [currentPage, setCurrentPage] = useState(1);
- const [pageSize, setPageSize] = useState(20);
- const [startDate, setStartDate] = useState(
- dayjs().subtract(30, "day")
- );
- const [endDate, setEndDate] = useState(dayjs());
-
- const {
- data: invoices,
- isLoading,
- isError,
- refetch,
- } = useQuery({
- queryKey: [
- "drafts",
- startDate?.format("YYYY-MM-DD"),
- endDate?.format("YYYY-MM-DD"),
- ],
- queryFn: () =>
- api.getDrafts(
- startDate?.format("YYYY-MM-DD"),
- endDate?.format("YYYY-MM-DD")
- ),
- staleTime: 0,
- refetchOnMount: true,
- refetchOnWindowFocus: true,
- });
-
- const handleSync = async () => {
- setSyncLoading(true);
- try {
- await api.syncInvoices();
- refetch();
- } finally {
- setSyncLoading(false);
- }
- };
-
- const getStatusTag = (item: UnifiedInvoice) => {
- switch (item.status) {
- case "PROCESSING":
- return (
- } color="blue">
- Обработка
-
- );
- case "READY_TO_VERIFY":
- return (
- } color="orange">
- Проверка
-
- );
- case "COMPLETED":
- return (
- } color="green">
- Готово
-
- );
- case "ERROR":
- return (
- } color="red">
- Ошибка
-
- );
- case "CANCELED":
- return (
- } color="default">
- Отменен
-
- );
- case "NEW":
- return (
- } color="blue">
- Новая
-
- );
- case "PROCESSED":
- return (
- } color="green">
- Проведена
-
- );
- case "DELETED":
- return (
- } color="red">
- Удалена
-
- );
- default:
- return {item.status};
- }
- };
-
- const handleInvoiceClick = (item: UnifiedInvoice) => {
- if (item.type === "DRAFT") {
- navigate("/invoice/draft/" + item.id);
- } else if (item.type === "SYNCED") {
- navigate("/invoice/view/" + item.id);
- }
- };
-
- const handleFilterChange = (value: FilterType) => {
- setFilterType(value);
- setCurrentPage(1);
- };
-
- const handlePageSizeChange = (value: number) => {
- setPageSize(value);
- setCurrentPage(1);
- };
-
- const getItemDate = (item: UnifiedInvoice) =>
- item.type === "DRAFT" ? item.created_at : item.date_incoming;
-
- const filteredAndSortedInvoices = useMemo(() => {
- if (!invoices || invoices.length === 0) return [];
-
- let result = [...invoices];
-
- if (filterType !== "ALL") {
- result = result.filter((item) => item.type === filterType);
- }
-
- result.sort((a, b) => {
- const dateA = dayjs(getItemDate(a)).startOf("day");
- const dateB = dayjs(getItemDate(b)).startOf("day");
-
- // Сначала по дате DESC
- if (!dateA.isSame(dateB)) {
- return dateB.valueOf() - dateA.valueOf();
- }
-
- // Внутри дня: DRAFT < SYNCED
- if (a.type !== b.type) {
- return a.type === "DRAFT" ? -1 : 1;
- }
-
- // Внутри типа: по номеру DESC
- return (b.document_number || "").localeCompare(
- a.document_number || "",
- "ru",
- { numeric: true }
- );
- });
-
- return result;
- }, [invoices, filterType]);
-
- const paginatedInvoices = useMemo(() => {
- const startIndex = (currentPage - 1) * pageSize;
- return filteredAndSortedInvoices.slice(startIndex, startIndex + pageSize);
- }, [filteredAndSortedInvoices, currentPage, pageSize]);
-
- const groupedInvoices = useMemo(() => {
- const groups: { [key: string]: UnifiedInvoice[] } = {};
- paginatedInvoices.forEach((item) => {
- const dateKey = dayjs(getItemDate(item)).format("YYYY-MM-DD");
- if (!groups[dateKey]) {
- groups[dateKey] = [];
- }
- groups[dateKey].push(item);
- });
- return groups;
- }, [paginatedInvoices]);
-
- const filterCounts = useMemo(() => {
- if (!invoices) return { all: 0, draft: 0, synced: 0 };
- return {
- all: invoices.length,
- draft: invoices.filter((item) => item.type === "DRAFT").length,
- synced: invoices.filter((item) => item.type === "SYNCED").length,
- };
- }, [invoices]);
-
- const totalPages = Math.ceil(
- (filteredAndSortedInvoices.length || 0) / pageSize
- );
-
- if (isError) {
- return (
-
- Ошибка загрузки списка накладных
-
- );
- }
-
- return (
-
-
-
-
- Накладные
-
- }
- loading={syncLoading}
- onClick={handleSync}
- />
-
-
-
-
-
-
- Период:
-
- —
-
-
-
-
- {isLoading ? (
-
-
-
- ) : !invoices || invoices.length === 0 ? (
-
- ) : (
- <>
-
- {Object.entries(groupedInvoices).map(([dateKey, items]) => (
-
-
- {items.map((item) => {
- const isSynced = item.type === "SYNCED";
- const displayDate =
- item.type === "DRAFT"
- ? item.created_at
- : item.date_incoming;
-
- return (
-
handleInvoiceClick(item)}
- >
-
- {getStatusTag(item)}
-
-
-
-
-
- {item.document_number || "Без номера"}
-
- {item.type === "SYNCED" && (
-
- )}
- {item.is_app_created && (
- 📱
- )}
-
-
-
- {item.items_count} поз.
-
-
- {dayjs(displayDate).format("DD.MM.YYYY")}
-
-
- {item.incoming_number && (
-
- Вх. № {item.incoming_number}
-
- )}
-
-
-
- {item.store_name && (
-
- {item.store_name}
-
- )}
-
-
-
-
- {item.total_sum.toLocaleString("ru-RU", {
- style: "currency",
- currency: "RUB",
- maximumFractionDigits: 0,
- })}
-
- {item.items_preview && (
-
- {item.items_preview
- .split(", ")
- .slice(0, 3)
- .map((previewItem, idx) => (
-
- {previewItem}
-
- ))}
-
- )}
-
-
-
- );
- })}
-
- ))}
-
-
- {totalPages > 1 && (
-
-
- На странице:
-
-
-
-
- Стр. {currentPage} из {totalPages}
-
-
-
-
-
- )}
- >
- )}
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/pages/InvoiceDraftPage.tsx
-# ===================================================================
-
-```
-import React, { useEffect, useMemo, useState } from "react";
-import { useParams, useNavigate } from "react-router-dom";
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
-import {
- Spin,
- Alert,
- Button,
- Form,
- Select,
- DatePicker,
- Input,
- Typography,
- message,
- Row,
- Col,
- Affix,
- Modal,
- Tag,
- Image,
-} from "antd";
-import {
- ArrowLeftOutlined,
- CheckOutlined,
- DeleteOutlined,
- ExclamationCircleFilled,
- RestOutlined,
- PlusOutlined,
- FileImageOutlined,
- SwapOutlined,
-} from "@ant-design/icons";
-import dayjs from "dayjs";
-import { api, getStaticUrl } from "../services/api";
-import { DraftItemRow } from "../components/invoices/DraftItemRow";
-import type {
- UpdateDraftItemRequest,
- CommitDraftRequest,
- ReorderDraftItemsRequest,
-} from "../services/types";
-import { DragDropContext, Droppable, type DropResult } from "@hello-pangea/dnd";
-
-const { Text } = Typography;
-const { TextArea } = Input;
-const { confirm } = Modal;
-
-export const InvoiceDraftPage: React.FC = () => {
- const { id } = useParams<{ id: string }>();
- const navigate = useNavigate();
- const queryClient = useQueryClient();
- const [form] = Form.useForm();
-
- const [updatingItems, setUpdatingItems] = useState>(new Set());
- const [itemsOrder, setItemsOrder] = useState>({});
- const [isDragging, setIsDragging] = useState(false);
- const [isReordering, setIsReordering] = useState(false);
-
- // Состояние для просмотра фото чека
- const [previewVisible, setPreviewVisible] = useState(false);
-
- // --- ЗАПРОСЫ ---
-
- const dictQuery = useQuery({
- queryKey: ["dictionaries"],
- queryFn: api.getDictionaries,
- staleTime: 1000 * 60 * 5,
- });
-
- const recommendationsQuery = useQuery({
- queryKey: ["recommendations"],
- queryFn: api.getRecommendations,
- });
-
- const draftQuery = useQuery({
- queryKey: ["draft", id],
- queryFn: () => api.getDraft(id!),
- enabled: !!id,
- refetchInterval: (query) => {
- if (isDragging) return false;
- const status = query.state.data?.status;
- return status === "PROCESSING" ? 3000 : false;
- },
- });
-
- const draft = draftQuery.data;
- const stores = dictQuery.data?.stores || [];
- const suppliers = dictQuery.data?.suppliers || [];
-
- // --- МУТАЦИИ ---
-
- const updateItemMutation = useMutation({
- mutationFn: (vars: { itemId: string; payload: UpdateDraftItemRequest }) =>
- api.updateDraftItem(id!, vars.itemId, vars.payload),
- onMutate: async ({ itemId }) => {
- setUpdatingItems((prev) => new Set(prev).add(itemId));
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["draft", id] });
- },
- onError: () => {
- message.error("Не удалось сохранить строку");
- },
- onSettled: (_data, _err, vars) => {
- setUpdatingItems((prev) => {
- const next = new Set(prev);
- next.delete(vars.itemId);
- return next;
- });
- },
- });
-
- const addItemMutation = useMutation({
- mutationFn: () => api.addDraftItem(id!),
- onSuccess: () => {
- message.success("Строка добавлена");
- queryClient.invalidateQueries({ queryKey: ["draft", id] });
- },
- onError: () => {
- message.error("Ошибка создания строки");
- },
- });
-
- const deleteItemMutation = useMutation({
- mutationFn: (itemId: string) => api.deleteDraftItem(id!, itemId),
- onSuccess: () => {
- message.success("Строка удалена");
- queryClient.invalidateQueries({ queryKey: ["draft", id] });
- },
- onError: () => {
- message.error("Ошибка удаления строки");
- },
- });
-
- const commitMutation = useMutation({
- mutationFn: (payload: CommitDraftRequest) => api.commitDraft(id!, payload),
- onSuccess: (data) => {
- message.success(`Накладная ${data.document_number} создана!`);
- navigate("/invoices");
- queryClient.invalidateQueries({ queryKey: ["drafts"] });
- },
- onError: () => {
- message.error("Ошибка при создании накладной");
- },
- });
-
- const deleteDraftMutation = useMutation({
- mutationFn: () => api.deleteDraft(id!),
- onSuccess: () => {
- if (draft?.status === "CANCELED") {
- message.info("Черновик удален окончательно");
- navigate("/invoices");
- } else {
- message.warning("Черновик отменен");
- queryClient.invalidateQueries({ queryKey: ["draft", id] });
- }
- },
- onError: () => {
- message.error("Ошибка при удалении");
- },
- });
-
- const reorderItemsMutation = useMutation({
- mutationFn: ({
- draftId,
- payload,
- }: {
- draftId: string;
- payload: ReorderDraftItemsRequest;
- }) => api.reorderDraftItems(draftId, payload),
- onError: (error) => {
- message.error("Не удалось изменить порядок элементов");
- console.error("Reorder error:", error);
- },
- });
-
- // --- ЭФФЕКТЫ ---
-
- useEffect(() => {
- if (draft) {
- const currentValues = form.getFieldsValue();
- if (!currentValues.store_id && draft.store_id)
- form.setFieldValue("store_id", draft.store_id);
- if (!currentValues.supplier_id && draft.supplier_id)
- form.setFieldValue("supplier_id", draft.supplier_id);
- if (!currentValues.comment && draft.comment)
- form.setFieldValue("comment", draft.comment);
-
- // Инициализация входящего номера
- if (
- !currentValues.incoming_document_number &&
- draft.incoming_document_number
- )
- form.setFieldValue(
- "incoming_document_number",
- draft.incoming_document_number
- );
-
- if (!currentValues.date_incoming)
- form.setFieldValue(
- "date_incoming",
- draft.date_incoming ? dayjs(draft.date_incoming) : dayjs()
- );
- }
- }, [draft, form]);
-
- // --- ХЕЛПЕРЫ ---
- const totalSum = useMemo(() => {
- return (
- draft?.items.reduce(
- (acc, item) => acc + Number(item.quantity) * Number(item.price),
- 0
- ) || 0
- );
- }, [draft?.items]);
-
- const invalidItemsCount = useMemo(() => {
- return draft?.items.filter((i) => !i.product_id).length || 0;
- }, [draft?.items]);
-
- const handleItemUpdate = (
- itemId: string,
- changes: UpdateDraftItemRequest
- ) => {
- updateItemMutation.mutate({ itemId, payload: changes });
- };
-
- const handleCommit = async () => {
- try {
- const values = await form.validateFields();
-
- if (invalidItemsCount > 0) {
- message.warning(
- `Осталось ${invalidItemsCount} нераспознанных товаров!`
- );
- return;
- }
-
- commitMutation.mutate({
- date_incoming: values.date_incoming.format("YYYY-MM-DD"),
- store_id: values.store_id,
- supplier_id: values.supplier_id,
- comment: values.comment || "",
- incoming_document_number: values.incoming_document_number || "",
- });
- } catch {
- message.error("Заполните обязательные поля (Склад, Поставщик)");
- }
- };
-
- const isCanceled = draft?.status === "CANCELED";
-
- const handleDelete = () => {
- confirm({
- title: isCanceled ? "Удалить окончательно?" : "Отменить черновик?",
- icon: ,
- content: isCanceled
- ? "Черновик пропадет из списка навсегда."
- : 'Черновик получит статус "Отменен", но останется в списке.',
- okText: isCanceled ? "Удалить навсегда" : "Отменить",
- okType: "danger",
- cancelText: "Назад",
- onOk() {
- deleteDraftMutation.mutate();
- },
- });
- };
-
- const handleDragStart = () => {
- setIsDragging(true);
- };
-
- const handleDragEnd = async (result: DropResult) => {
- setIsDragging(false);
- const { source, destination } = result;
-
- // Если нет назначения или позиция не изменилась
- if (
- !destination ||
- (source.droppableId === destination.droppableId &&
- source.index === destination.index)
- ) {
- return;
- }
-
- if (!draft) return;
-
- // Сохраняем предыдущее состояние для отката
- const previousItems = [...draft.items];
- const previousOrder = { ...itemsOrder };
-
- // Создаём новый массив с изменённым порядком
- const newItems = [...draft.items];
- const [removed] = newItems.splice(source.index, 1);
- newItems.splice(destination.index, 0, removed);
-
- // Обновляем локальное состояние немедленно для быстрого UI
- queryClient.setQueryData(["draft", id], {
- ...draft,
- items: newItems,
- });
-
- // Подготавливаем payload для API
- const reorderPayload: ReorderDraftItemsRequest = {
- items: newItems.map((item, index) => ({
- id: item.id,
- order: index,
- })),
- };
-
- // Отправляем запрос на сервер
- try {
- await reorderItemsMutation.mutateAsync({
- draftId: draft.id,
- payload: reorderPayload,
- });
- } catch {
- // При ошибке откатываем локальное состояние
- queryClient.setQueryData(["draft", id], {
- ...draft,
- items: previousItems,
- });
- setItemsOrder(previousOrder);
- }
- };
-
- // --- RENDER ---
- const showSpinner =
- draftQuery.isLoading ||
- (draft?.status === "PROCESSING" &&
- (!draft?.items || draft.items.length === 0));
-
- if (showSpinner) {
- return (
-
-
-
- );
- }
-
- if (draftQuery.isError || !draft) {
- return ;
- }
-
- return (
-
- {/* Header */}
-
-
-
}
- onClick={() => navigate("/invoices")}
- size="small"
- />
-
-
-
- {draft.document_number ? `№${draft.document_number}` : "Черновик"}
-
- {draft.status === "PROCESSING" && }
- {isCanceled && (
-
- ОТМЕНЕН
-
- )}
-
-
-
- {/* Правая часть хедера: Кнопка чека, Кнопка перетаскивания и Кнопка удаления */}
-
- {/* Кнопка просмотра чека (только если есть URL) */}
- {draft.photo_url && (
- }
- onClick={() => setPreviewVisible(true)}
- size="small"
- >
- Чек
-
- )}
-
- {/* Кнопка переключения режима перетаскивания */}
- }
- onClick={() => setIsReordering(!isReordering)}
- size="small"
- >
- {isReordering ? "Ок" : ""}
-
-
- : }
- onClick={handleDelete}
- loading={deleteDraftMutation.isPending}
- size="small"
- >
- {isCanceled ? "Удалить" : "Отмена"}
-
-
-
-
- {/* Form: Склады и Поставщики */}
-
-
-
-
-
-
-
-
-
- {/* Items Header */}
-
- Позиции ({draft.items.length})
- {invalidItemsCount > 0 && (
-
- {invalidItemsCount} нераспознано
-
- )}
-
-
- {/* Items List */}
-
-
- {(provided, snapshot) => (
-
- {draft.items.map((item, index) => (
- deleteItemMutation.mutate(itemId)}
- isUpdating={updatingItems.has(item.id)}
- recommendations={recommendationsQuery.data || []}
- isReordering={isReordering}
- />
- ))}
- {provided.placeholder}
-
- )}
-
-
-
- {/* Кнопка добавления позиции */}
-
}
- style={{ marginTop: 12, marginBottom: 80, height: 48 }}
- onClick={() => addItemMutation.mutate()}
- loading={addItemMutation.isPending}
- disabled={isCanceled}
- >
- Добавить товар
-
-
- {/* Footer Actions */}
-
-
-
-
- Итого:
-
-
- {totalSum.toLocaleString("ru-RU", {
- style: "currency",
- currency: "RUB",
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- })}
-
-
-
-
}
- onClick={handleCommit}
- loading={commitMutation.isPending}
- disabled={invalidItemsCount > 0 || isCanceled}
- style={{ height: 40, padding: "0 24px" }}
- >
- {isCanceled ? "Восстановить" : "Отправить"}
-
-
-
-
- {/* Скрытый компонент для просмотра изображения */}
- {draft.photo_url && (
-
- setPreviewVisible(vis),
- movable: true,
- scaleStep: 0.5,
- }}
- >
-
-
-
- )}
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/pages/InvoiceViewPage.tsx
-# ===================================================================
-
-```
-// src/pages/InvoiceViewPage.tsx
-
-import React, { useState } from "react";
-import { useParams } from "react-router-dom";
-import { useQuery } from "@tanstack/react-query";
-import { Spin, Alert, Button, Table, Typography, Tag, Image } from "antd";
-import {
- ArrowLeftOutlined,
- FileImageOutlined,
- HistoryOutlined,
-} from "@ant-design/icons";
-import { useNavigate } from "react-router-dom";
-import { api, getStaticUrl } from "../services/api";
-import type { DraftStatus } from "../services/types";
-
-const { Title, Text } = Typography;
-
-export const InvoiceViewPage: React.FC = () => {
- const { id } = useParams<{ id: string }>();
- const navigate = useNavigate();
-
- // Состояние для просмотра фото чека
- const [previewVisible, setPreviewVisible] = useState(false);
-
- // Запрос данных накладной
- const {
- data: invoice,
- isLoading,
- isError,
- } = useQuery({
- queryKey: ["invoice", id],
- queryFn: () => api.getInvoice(id!),
- enabled: !!id,
- });
-
- const getStatusTag = (status: DraftStatus) => {
- switch (status) {
- case "PROCESSING":
- return Обработка;
- case "READY_TO_VERIFY":
- return Проверка;
- case "COMPLETED":
- return (
- } color="success">
- Синхронизировано
-
- );
- case "ERROR":
- return Ошибка;
- case "CANCELED":
- return Отменен;
- default:
- return {status};
- }
- };
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- if (isError || !invoice) {
- return ;
- }
-
- const columns = [
- {
- title: "Товар",
- dataIndex: "name",
- key: "name",
- },
- {
- title: "Кол-во",
- dataIndex: "quantity",
- key: "quantity",
- align: "right" as const,
- },
- {
- title: "Сумма",
- dataIndex: "total",
- key: "total",
- align: "right" as const,
- render: (total: number) =>
- total.toLocaleString("ru-RU", {
- style: "currency",
- currency: "RUB",
- }),
- },
- ];
-
- const totalSum = (invoice.items || []).reduce(
- (acc, item) => acc + item.total,
- 0
- );
-
- return (
-
- {/* Header */}
-
-
-
}
- onClick={() => navigate("/invoices")}
- size="small"
- />
-
-
-
- №{invoice.number}
-
-
- {invoice.date} • {invoice.supplier.name}
-
-
-
-
-
- {getStatusTag(invoice.status)}
-
- {/* Кнопка просмотра чека (только если есть URL) */}
- {invoice.photo_url && (
- }
- onClick={() => setPreviewVisible(true)}
- size="small"
- >
- Чек
-
- )}
-
-
-
- {/* Таблица товаров */}
-
-
- Товары ({(invoice.items || []).length} поз.)
-
-
-
(
-
-
- Итого:
-
-
-
- {totalSum.toLocaleString("ru-RU", {
- style: "currency",
- currency: "RUB",
- })}
-
-
-
- )}
- />
-
-
- {/* Скрытый компонент для просмотра изображения */}
- {invoice.photo_url && (
-
- setPreviewVisible(vis),
- movable: true,
- scaleStep: 0.5,
- }}
- >
-
-
-
- )}
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/pages/MaintenancePage.tsx
-# ===================================================================
-
-```
-import { Result, Button } from "antd";
-
-// Страница-заглушка для режима технического обслуживания
-const MaintenancePage = () => (
-
- window.location.reload()}>
- Попробовать снова
-
- }
- />
-
-);
-
-export default MaintenancePage;
-
-```
-
-# ===================================================================
-# Файл: src/pages/OcrLearning.tsx
-# ===================================================================
-
-```
-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,
- deleteMatch,
- isDeletingMatch,
- deleteUnmatched,
- } = useOcr();
-
- // Состояние для редактирования
- const [editingMatch, setEditingMatch] = React.useState(null);
-
- // Найти редактируемую связь
- const currentEditingMatch = React.useMemo(() => {
- if (!editingMatch) return undefined;
- return matches.find((match) => match.raw_name === editingMatch);
- }, [editingMatch, matches]);
-
- if (isLoading && matches.length === 0) {
- return (
-
-
- Загрузка справочников...
-
- );
- }
-
- if (isError) {
- return (
-
- );
- }
-
- return (
-
-
{
- if (currentEditingMatch) {
- // Обновление существующей связи
- createMatch({
- raw_name: raw,
- product_id: prodId,
- quantity: qty,
- container_id: contId,
- });
- setEditingMatch(null);
- } else {
- // Создание новой связи
- createMatch({
- raw_name: raw,
- product_id: prodId,
- quantity: qty,
- container_id: contId,
- });
- }
- }}
- onDeleteUnmatched={deleteUnmatched}
- isLoading={isCreating}
- initialValues={currentEditingMatch}
- onCancelEdit={() => setEditingMatch(null)}
- />
-
- Обученные позиции ({matches.length})
-
- {
- setEditingMatch(match.raw_name);
- }}
- isDeleting={isDeletingMatch}
- />
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/pages/SettingsPage.tsx
-# ===================================================================
-
-```
-import React, { useEffect } from "react";
-import {
- Typography,
- Card,
- Form,
- Select,
- Switch,
- Button,
- Row,
- Col,
- Statistic,
- TreeSelect,
- Spin,
- message,
- Tabs,
- Popconfirm,
-} from "antd";
-import {
- SaveOutlined,
- BarChartOutlined,
- SettingOutlined,
- FolderOpenOutlined,
- TeamOutlined,
- CameraOutlined,
-} from "@ant-design/icons";
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
-import { api } from "../services/api";
-import type { UserSettings } from "../services/types";
-import { TeamList } from "../components/settings/TeamList";
-import { PhotoStorageTab } from "../components/settings/PhotoStorageTab";
-
-const { Title, Text } = Typography;
-
-export const SettingsPage: React.FC = () => {
- const queryClient = useQueryClient();
- const [form] = Form.useForm();
-
- // --- Запросы ---
-
- const settingsQuery = useQuery({
- queryKey: ["settings"],
- queryFn: api.getSettings,
- });
-
- const statsQuery = useQuery({
- queryKey: ["stats"],
- queryFn: api.getStats,
- });
-
- const dictQuery = useQuery({
- queryKey: ["dictionaries"],
- queryFn: api.getDictionaries,
- staleTime: 1000 * 60 * 5,
- });
-
- const groupsQuery = useQuery({
- queryKey: ["productGroups"],
- queryFn: api.getProductGroups,
- staleTime: 1000 * 60 * 10,
- });
-
- // --- Мутации ---
-
- const saveMutation = useMutation({
- mutationFn: (vals: UserSettings) => api.updateSettings(vals),
- onSuccess: () => {
- message.success("Настройки сохранены");
- queryClient.invalidateQueries({ queryKey: ["settings"] });
- },
- onError: () => {
- message.error("Не удалось сохранить настройки");
- },
- });
-
- const deleteAllDraftsMutation = useMutation({
- mutationFn: () => api.deleteAllDrafts(),
- onSuccess: (data) => {
- message.success(`Удалено черновиков: ${data.count}`);
- queryClient.invalidateQueries({ queryKey: ["stats"] });
- },
- onError: () => {
- message.error("Не удалось удалить черновики");
- },
- });
-
- // --- Эффекты ---
-
- useEffect(() => {
- if (settingsQuery.data) {
- form.setFieldsValue(settingsQuery.data);
- }
- }, [settingsQuery.data, form]);
-
- // --- Хендлеры ---
-
- const handleSave = async () => {
- try {
- const values = await form.validateFields();
- saveMutation.mutate(values);
- } catch {
- // Ошибки валидации
- }
- };
-
- // --- Рендер ---
-
- if (settingsQuery.isLoading) {
- return (
-
-
-
- );
- }
-
- // Определяем роль текущего пользователя
- const currentUserRole = settingsQuery.data?.role || "OPERATOR";
- const showTeamSettings =
- currentUserRole === "ADMIN" || currentUserRole === "OWNER";
-
- // Сохраняем JSX в переменную вместо создания вложенного компонента
- const generalSettingsContent = (
-
- );
-
- const tabsItems = [
- {
- key: "general",
- label: "Общие",
- icon: ,
- children: generalSettingsContent,
- },
- ];
-
- if (showTeamSettings) {
- tabsItems.push({
- key: "team",
- label: "Команда",
- icon: ,
- children: (
-
-
-
- ),
- });
- }
-
- // Добавляем вкладку с фото (доступна для OWNER)
- if (currentUserRole === "OWNER") {
- tabsItems.push({
- key: "photos",
- label: "Архив фото",
- icon: ,
- children: ,
- });
- }
-
- return (
-
-
- Настройки
-
-
- {/* Статистика */}
-
-
-
- Статистика накладных
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Табы настроек */}
-
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/pages/desktop/auth/DesktopAuthScreen.tsx
-# ===================================================================
-
-```
-import React, { useEffect, useState } from "react";
-import { Card, Typography, Spin, Alert, message } from "antd";
-import { QRCodeSVG } from "qrcode.react";
-import { useNavigate } from "react-router-dom";
-import { useWebSocket } from "../../../hooks/useWebSocket";
-import { useAuthStore } from "../../../stores/authStore";
-import { api } from "../../../services/api";
-
-const { Title, Paragraph } = Typography;
-
-interface AuthSuccessData {
- token: string;
- user: {
- id: string;
- username: string;
- email?: string;
- role?: string;
- };
-}
-
-/**
- * Экран авторизации для десктопной версии
- * Отображает QR код для авторизации через мобильное приложение
- */
-export const DesktopAuthScreen: React.FC = () => {
- const navigate = useNavigate();
- const { setToken, setUser } = useAuthStore();
- const [sessionId, setSessionId] = useState(null);
- const [qrLink, setQrLink] = useState(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
-
- const { isConnected, lastError, lastMessage } = useWebSocket(sessionId);
-
- // Инициализация сессии авторизации при маунте
- useEffect(() => {
- const initAuth = async () => {
- try {
- setLoading(true);
- setError(null);
-
- const data = await api.initDesktopAuth();
- setSessionId(data.session_id);
- setQrLink(data.qr_url);
- } catch (err) {
- setError(err instanceof Error ? err.message : "Неизвестная ошибка");
- } finally {
- setLoading(false);
- }
- };
-
- initAuth();
- }, []);
-
- // Обработка события успешной авторизации через WebSocket
- useEffect(() => {
- if (lastMessage && lastMessage.event === "auth_success") {
- const data = lastMessage.data as AuthSuccessData;
- const { token, user } = data;
- console.log("🎉 Auth Success:", user);
- setToken(token);
- setUser(user);
- message.success("Вход выполнен!");
- navigate("/web/dashboard");
- }
- }, [lastMessage, setToken, setUser, navigate]);
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- if (error || lastError) {
- return (
-
- );
- }
-
- return (
-
-
- Авторизация
-
- Отсканируйте QR код в мобильном приложении для входа
-
-
- {qrLink && (
-
-
-
- )}
-
-
-
- Status:{" "}
- {isConnected ? (
- Connected
- ) : (
- Disconnected
- )}
-
-
Session: {sessionId}
- {lastError &&
Error: {lastError}
}
-
-
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/pages/desktop/auth/MobileBrowserStub.tsx
-# ===================================================================
-
-```
-import React from 'react';
-import { Result, Button } from 'antd';
-import { MobileOutlined } from '@ant-design/icons';
-
-/**
- * Заглушка для мобильных браузеров
- * Отображается когда пользователь пытается открыть десктопную версию на мобильном устройстве
- */
-export const MobileBrowserStub: React.FC = () => {
- const handleRedirect = () => {
- window.location.href = '/';
- };
-
- return (
-
- }
- title="Десктопная версия недоступна"
- subTitle="Пожалуйста, используйте мобильное приложение или откройте сайт на десктопном устройстве"
- extra={[
- ,
- ]}
- />
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/pages/desktop/dashboard/InvoicesDashboard.tsx
-# ===================================================================
-
-```
-import React from "react";
-import { Typography, Card, List, Empty } from "antd";
-import { DragDropZone } from "../../../components/DragDropZone";
-
-const { Title } = Typography;
-
-/**
- * Дашборд черновиков для десктопной версии
- * Содержит зону для загрузки файлов и список черновиков
- */
-export const InvoicesDashboard: React.FC = () => {
- const handleDrop = (files: File[]) => {
- console.log("Файлы загружены:", files);
- // TODO: Добавить логику обработки файлов
- };
-
- // Заглушка списка черновиков
- const mockDrafts = [
- {
- id: "1",
- title: "Черновик #1",
- date: "2024-01-15",
- status: "В работе",
- },
- {
- id: "2",
- title: "Черновик #2",
- date: "2024-01-14",
- status: "Черновик",
- },
- ];
-
- return (
-
-
Черновики
-
- {/* Зона для загрузки файлов */}
-
-
-
-
- {/* Список черновиков (заглушка) */}
-
- (
-
-
-
- )}
- locale={{
- emptyText: (
-
- ),
- }}
- />
-
-
- );
-};
-
-```
-
-# ===================================================================
-# Файл: src/services/api.ts
-# ===================================================================
-
-```
-import axios from 'axios';
-import { notification } from 'antd';
-import { useAuthStore } from '../stores/authStore';
-import type {
- CatalogItem,
- CreateInvoiceRequest,
- MatchRequest,
- HealthResponse,
- InvoiceResponse,
- ProductMatch,
- Recommendation,
- UnmatchedItem,
- UserSettings,
- InvoiceStats,
- ProductGroup,
- Store,
- Supplier,
- DraftInvoice,
- DraftItem,
- UpdateDraftItemRequest,
- CommitDraftRequest,
- ReorderDraftItemsRequest,
- ProductSearchResult,
- AddContainerRequest,
- AddContainerResponse,
- DictionariesResponse,
- UnifiedInvoice,
- ServerUser,
- UserRole,
- InvoiceDetails,
- GetPhotosResponse
-} from './types';
-
-// Интерфейс для ответа метода инициализации десктопной авторизации
-export interface InitDesktopAuthResponse {
- session_id: string;
- qr_url: string;
-}
-
-// Базовый URL
-export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api';
-
-// Хелпер для получения полного URL картинки (убирает /api если путь статики идет от корня, или добавляет как есть)
-// В данном ТЗ сказано просто склеивать.
-export const getStaticUrl = (path: string | null | undefined): string => {
- if (!path) return '';
- if (path.startsWith('http')) return path;
- return `${API_BASE_URL}${path}`;
-};
-
-// Телеграм объект
-const tg = window.Telegram?.WebApp;
-
-// Событие для глобальной обработки 401
-export const UNAUTHORIZED_EVENT = 'rms_unauthorized';
-
-// Событие для режима технического обслуживания (503)
-export const MAINTENANCE_EVENT = 'rms_maintenance';
-
-const apiClient = axios.create({
- baseURL: API_BASE_URL,
- headers: {
- 'Content-Type': 'application/json',
- },
-});
-
-// --- Request Interceptor (Авторизация через JWT или initData) ---
-apiClient.interceptors.request.use((config) => {
- // Шаг 1: Whitelist - пропускаем запросы к инициализации десктопной авторизации
- if (config.url?.endsWith('/auth/init-desktop')) {
- return config;
- }
-
- // Шаг 2: Desktop Auth - проверяем JWT токен из authStore
- const jwtToken = useAuthStore.getState().token;
- if (jwtToken) {
- config.headers['Authorization'] = `Bearer ${jwtToken}`;
- return config;
- }
-
- // Шаг 3: Mobile Auth - проверяем Telegram initData
- const initData = tg?.initData;
- if (initData) {
- config.headers['Authorization'] = `Bearer ${initData}`;
- return config;
- }
-
- // Шаг 4: Block - если нет ни JWT, ни initData, отклоняем запрос
- console.error('Запрос заблокирован: отсутствуют данные авторизации.');
- return Promise.reject(new Error('MISSING_AUTH'));
-});
-
-// --- Response Interceptor (Обработка ошибок и уведомления) ---
-apiClient.interceptors.response.use(
- (response) => response,
- (error) => {
- if (error.response && error.response.status === 401) {
- // Глобальное уведомление об ошибке авторизации
- notification.error({
- message: 'Ошибка авторизации',
- description: 'Ваша сессия в Telegram истекла или данные неверны. Попробуйте перезапустить бота.',
- placement: 'top',
- });
-
- window.dispatchEvent(new Event(UNAUTHORIZED_EVENT));
- }
-
- if (error.response && error.response.status === 503) {
- // Режим технического обслуживания
- window.dispatchEvent(new Event(MAINTENANCE_EVENT));
- }
-
- // Если запрос был отменен нами (нет авторизации), не выводим стандартную ошибку API
- if (error.message === 'MISSING_AUTH') {
- return Promise.reject(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;
- },
-
- searchProducts: async (query: string): Promise => {
- const { data } = await apiClient.get('/ocr/search', {
- params: { q: query }
- });
- return data;
- },
-
- createContainer: async (payload: AddContainerRequest): Promise => {
- const { data } = await apiClient.post('/drafts/container', payload);
- 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;
- },
-
- createMatch: async (payload: MatchRequest): Promise<{ status: string }> => {
- const { data } = await apiClient.post<{ status: string }>('/ocr/match', payload);
- return data;
- },
- deleteMatch: async (rawName: string): Promise<{ status: string }> => {
- const { data } = await apiClient.delete<{ status: string }>('/ocr/match', {
- params: { raw_name: rawName }
- });
- return data;
- },
- deleteUnmatched: async (rawName: string): Promise<{ status: string }> => {
- const { data } = await apiClient.delete<{ status: string }>('/ocr/unmatched', {
- params: { raw_name: rawName }
- });
- return data;
- },
-
- createInvoice: async (payload: CreateInvoiceRequest): Promise => {
- const { data } = await apiClient.post('/invoices/send', payload);
- return data;
- },
-
- // --- НОВЫЙ МЕТОД: Получение всех справочников ---
- getDictionaries: async (): Promise => {
- const { data } = await apiClient.get('/dictionaries');
- return data;
- },
-
- // Старые методы оставляем для совместимости, но они могут вызывать getDictionaries внутри или deprecated endpoint
- getStores: async (): Promise => {
- // Можно использовать новый эндпоинт и возвращать часть данных
- const { data } = await apiClient.get('/dictionaries');
- return data.stores;
- },
-
- getSuppliers: async (): Promise => {
- // Реальный запрос вместо мока
- const { data } = await apiClient.get('/dictionaries');
- return data.suppliers;
- },
-
- // Обновленный метод получения списка накладных с фильтрацией
- getDrafts: async (from?: string, to?: string): Promise => {
- const { data } = await apiClient.get('/drafts', {
- params: { from, to }
- });
- return data;
- },
-
- getDraft: async (id: string): Promise => {
- const { data } = await apiClient.get(`/drafts/${id}`);
- return data;
- },
-
- updateDraftItem: async (draftId: string, itemId: string, payload: UpdateDraftItemRequest): Promise => {
- const { data } = await apiClient.patch(`/drafts/${draftId}/items/${itemId}`, payload);
- return data;
- },
-
- addDraftItem: async (draftId: string): Promise => {
- const { data } = await apiClient.post(`/drafts/${draftId}/items`, {});
- return data;
- },
-
- deleteDraftItem: async (draftId: string, itemId: string): Promise => {
- await apiClient.delete(`/drafts/${draftId}/items/${itemId}`);
- },
-
- reorderDraftItems: async (draftId: string, payload: ReorderDraftItemsRequest): Promise => {
- await apiClient.post(`/drafts/${draftId}/reorder`, payload);
- },
-
- commitDraft: async (draftId: string, payload: CommitDraftRequest): Promise<{ document_number: string }> => {
- const { data } = await apiClient.post<{ document_number: string }>(`/drafts/${draftId}/commit`, payload);
- return data;
- },
-
- deleteDraft: async (id: string): Promise => {
- await apiClient.delete(`/drafts/${id}`);
- },
-
- deleteAllDrafts: async (): Promise<{ count: number }> => {
- const { data } = await apiClient.delete<{ count: number }>('/drafts');
- return data;
- },
-
- // --- Настройки и Статистика ---
-
- getSettings: async (): Promise => {
- const { data } = await apiClient.get('/settings');
- return data;
- },
-
- updateSettings: async (payload: UserSettings): Promise => {
- const { data } = await apiClient.post('/settings', payload);
- return data;
- },
-
- getStats: async (): Promise => {
- const { data } = await apiClient.get('/stats/invoices');
- return data;
- },
-
- getProductGroups: async (): Promise => {
- const { data } = await apiClient.get('/dictionaries/groups');
- return data;
- },
-
- // --- Управление командой ---
-
- getUsers: async (): Promise => {
- const { data } = await apiClient.get('/settings/users');
- return data;
- },
-
- updateUserRole: async (userId: string, newRole: UserRole): Promise<{ status: string }> => {
- const { data } = await apiClient.patch<{ status: string }>(`/settings/users/${userId}`, { new_role: newRole });
- return data;
- },
-
- removeUser: async (userId: string): Promise<{ status: string }> => {
- const { data } = await apiClient.delete<{ status: string }>(`/settings/users/${userId}`);
- return data;
- },
-
- getInvoice: async (id: string): Promise => {
- const { data } = await apiClient.get(`/invoices/${id}`);
- return data;
- },
-
- syncInvoices: async (): Promise => {
- await apiClient.post('/invoices/sync');
- },
- getPhotos: async (page = 1, limit = 20): Promise => {
- const { data } = await apiClient.get('/photos', {
- params: { page, limit }
- });
- return data;
- },
-
- deletePhoto: async (id: string, force = false): Promise => {
- await apiClient.delete(`/photos/${id}`, {
- params: { force }
- });
- },
-
- regenerateDraftFromPhoto: async (id: string): Promise => {
- await apiClient.post(`/photos/${id}/regenerate`);
- },
-
- // --- Десктопная авторизация ---
-
- initDesktopAuth: async (): Promise => {
- const { data } = await apiClient.post('/auth/init-desktop');
- return data;
- },
-};
-
-
-```
-
-# ===================================================================
-# Файл: src/services/types.ts
-# ===================================================================
-
-```
-// --- Общие типы ---
-
-export type UUID = string;
-
-// Добавляем типы ролей
-export type UserRole = 'OWNER' | 'ADMIN' | 'OPERATOR';
-
-// Интерфейс пользователя сервера
-export interface ServerUser {
- user_id: string;
- username: string; // @username или пустая строка
- first_name: string;
- last_name: string;
- photo_url: string; // URL картинки или пустая строка
- role: UserRole;
- is_me: boolean; // Флаг, является ли этот юзер текущим пользователем
-}
-
-// --- Каталог и Фасовки (API v2.0) ---
-
-export interface ProductContainer {
- id: UUID;
- name: string; // "Пачка 180г"
- count: number; // 0.180
-}
-
-// Запрос на создание фасовки
-export interface AddContainerRequest {
- product_id: UUID;
- name: string; // "Бутылка 0.75"
- count: number; // 0.75
-}
-
-// Ответ на создание фасовки
-export interface AddContainerResponse {
- status: string;
- container_id: UUID;
-}
-
-// Результат поиска товара
-export interface ProductSearchResult {
- id: UUID;
- name: string;
- code: string;
- num?: string;
-
- // Обновляем структуру единицы измерения
- main_unit?: MainUnit;
- measure_unit?: string; // Оставим для совместимости, но брать будем из main_unit.name
-
- containers: ProductContainer[];
-}
-
-// Совместимость с CatalogItem (чтобы не ломать старый код, если он где-то используется)
-export interface CatalogItem extends ProductSearchResult {
- // 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 {
- raw_name: string;
- product_id: UUID;
- product?: CatalogItem;
- quantity: number;
- container_id?: UUID;
- container?: ProductContainer;
- updated_at: string;
-}
-
-// --- Нераспознанное ---
-
-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;
-}
-
-// --- Справочники ---
-
-export interface Store {
- id: UUID;
- name: string;
-}
-
-export interface Supplier {
- id: UUID;
- name: string;
-}
-
-export interface DictionariesResponse {
- stores: Store[];
- suppliers: Supplier[];
- // product_groups?: ProductGroup[]; // пока не реализовано
-}
-
-// --- Настройки и Статистика ---
-
-export interface UserSettings {
- root_group_id: UUID | null;
- default_store_id: UUID | null;
- auto_conduct: boolean;
- role: UserRole; // Добавляем поле роли в настройки текущего пользователя
-}
-
-export interface InvoiceStats {
- last_month: number;
- last_24h: number;
- total: number;
-}
-
-// Интерфейс группы товаров (рекурсивный)
-export interface ProductGroup {
- key: string;
- value: string;
- title: string;
- children?: ProductGroup[];
-}
-
-// --- Черновик Накладной (Draft) ---
-
-export type DraftStatus = 'PROCESSING' | 'READY_TO_VERIFY' | 'COMPLETED' | 'ERROR' | 'CANCELED' | 'NEW' | 'PROCESSED' | 'DELETED';
-
-export interface DraftItem {
- id: UUID;
-
- // Данные из OCR (Read-only)
- raw_name: string;
- raw_amount: number;
- raw_price: number;
-
- // Редактируемые данные
- product_id: UUID | null;
- container_id: UUID | null; // Фасовка
- quantity: number;
- price: number;
- sum: number;
-
- // Мета-данные
- is_matched: boolean;
- product?: CatalogItem;
- container?: ProductContainer;
-
- // Поля для синхронизации состояния (опционально, если бэкенд их отдает)
- last_edited_field_1?: string;
- last_edited_field_2?: string;
-}
-
-// --- Список Черновиков (Summary) ---
-export interface DraftSummary {
- id: UUID;
- document_number: string;
- date_incoming: string;
- status: DraftStatus; // Используем существующий тип статуса
- items_count: number;
- total_sum: number;
- store_name?: string;
- created_at: string;
-}
-
-export interface DraftInvoice {
- id: UUID;
- status: DraftStatus;
- document_number: string;
- incoming_document_number?: string
- date_incoming: string | null; // YYYY-MM-DD
- store_id: UUID | null;
- supplier_id: UUID | null;
- comment: string;
- items: DraftItem[];
- created_at?: string;
- photo_url?: string; // Добавлено поле фото чека
-}
-
-// DTO для обновления строки
-export interface UpdateDraftItemRequest {
- product_id?: UUID;
- container_id?: UUID | null;
- quantity?: number;
- price?: number;
- sum?: number;
- edited_field?: string; // ('quantity' | 'price' | 'sum')
-}
-
-// DTO для коммита
-export interface CommitDraftRequest {
- date_incoming: string;
- store_id: UUID;
- supplier_id: UUID;
- comment: string;
- incoming_document_number?: string;
-}
-
-export interface ReorderDraftItemsRequest {
- items: Array<{
- id: UUID;
- order: number;
- }>;
-}
-
-export interface MainUnit {
- id: UUID;
- name: string; // "кг"
- code: string;
-}
-
-export type InvoiceType = 'DRAFT' | 'SYNCED'; // Тип записи: Черновик или Синхронизировано из iiko
-
-export interface UnifiedInvoice {
- id: UUID;
- type: InvoiceType; // Новый признак типа
- document_number: string; // Внутренний номер iiko или ID черновика
- incoming_number: string; // Входящий номер накладной от поставщика
- date_incoming: string;
- status: DraftStatus;
- items_count: number;
- total_sum: number;
- store_name?: string;
- created_at: string;
- is_app_created: boolean; // Создано ли через наше приложение
- items_preview: string; // Краткое содержание товаров
- photo_url: string | null; // Ссылка на фото чека
-}
-
-export interface InvoiceDetails {
- id: UUID;
- number: string;
- date: string;
- status: DraftStatus;
- supplier: Supplier;
- items: {
- name: string;
- quantity: number;
- price: number;
- total: number;
- }[];
- photo_url: string | null;
-}
-
-export type PhotoStatus = 'ORPHAN' | 'HAS_DRAFT' | 'HAS_INVOICE';
-
-export interface ReceiptPhoto {
- id: string;
- rms_server_id: string;
- uploaded_by: string;
- file_url: string;
- file_name: string;
- file_size: number;
- draft_id?: string;
- invoice_id?: string;
- created_at: string;
- status: PhotoStatus;
- can_delete: boolean;
- can_regenerate: boolean;
-}
-
-export interface GetPhotosResponse {
- photos: ReceiptPhoto[];
- total: number;
- page: number;
- limit: number;
-}
-```
-
-# ===================================================================
-# Файл: src/stores/authStore.ts
-# ===================================================================
-
-```
-import { create } from 'zustand';
-import { persist } from 'zustand/middleware';
-
-interface User {
- id: string;
- username: string;
- email?: string;
- role?: string;
-}
-
-interface AuthState {
- token: string | null;
- isAuthenticated: boolean;
- user: User | null;
- setToken: (token: string) => void;
- setUser: (user: User) => void;
- logout: () => void;
-}
-
-/**
- * Хранилище состояния авторизации
- * Сохраняет токен и данные пользователя в localStorage
- */
-export const useAuthStore = create()(
- persist(
- (set) => ({
- token: null,
- isAuthenticated: false,
- user: null,
-
- setToken: (token: string) => {
- set({ token, isAuthenticated: true });
- },
-
- setUser: (user: User) => {
- set({ user });
- },
-
- logout: () => {
- set({ token: null, isAuthenticated: false, user: null });
- },
- }),
- {
- name: 'auth-storage',
- partialize: (state) => ({
- token: state.token,
- isAuthenticated: state.isAuthenticated,
- user: state.user,
- }),
- }
- )
-);
-
-```
-
-# ===================================================================
-# Файл: src/stores/uiStore.ts
-# ===================================================================
-
-```
-import { create } from 'zustand';
-
-interface UIState {
- // Выбранный сервер (заглушка для будущего функционала)
- selectedServer: string | null;
- sidebarCollapsed: boolean;
- setSelectedServer: (server: string | null) => void;
- toggleSidebar: () => void;
- setSidebarCollapsed: (collapsed: boolean) => void;
-}
-
-/**
- * Хранилище UI состояния десктопной версии
- */
-export const useUIStore = create((set) => ({
- selectedServer: null,
- sidebarCollapsed: false,
-
- setSelectedServer: (server: string | null) => {
- set({ selectedServer: server });
- },
-
- toggleSidebar: () => {
- set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed }));
- },
-
- setSidebarCollapsed: (collapsed: boolean) => {
- set({ sidebarCollapsed: collapsed });
- },
-}));
-
-```
-
-# ===================================================================
-# Файл: src/vite-env.d.ts
-# ===================================================================
-
-```
-///
-
-interface TelegramWebApp {
- initData: string; // Сырая строка с параметрами и хешем
- initDataUnsafe: {
- user?: {
- id: number;
- first_name: string;
- last_name?: string;
- username?: string;
- language_code?: string;
- };
- };
- close: () => void;
- expand: () => void;
-}
-
-interface Window {
- Telegram?: {
- WebApp: TelegramWebApp;
- };
-}
-```
-
-# ===================================================================
-# Файл: tsconfig.app.json
-# ===================================================================
-
-```
-{
- "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"]
-}
-
-```
-
-# ===================================================================
-# Файл: tsconfig.json
-# ===================================================================
-
-```
-{
- "files": [],
- "references": [
- { "path": "./tsconfig.app.json" },
- { "path": "./tsconfig.node.json" }
- ]
-}
-
-```
-
-# ===================================================================
-# Файл: tsconfig.node.json
-# ===================================================================
-
-```
-{
- "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/src/App.tsx b/rmser-view/src/App.tsx
index da8cf17..2a26e7c 100644
--- a/rmser-view/src/App.tsx
+++ b/rmser-view/src/App.tsx
@@ -6,7 +6,7 @@ import {
Navigate,
useLocation,
} from "react-router-dom";
-import { Result, Button } from "antd";
+import { Result, Button, Spin } from "antd";
import { Providers } from "./components/layout/Providers";
import { AppLayout } from "./components/layout/AppLayout";
import { OcrLearning } from "./pages/OcrLearning";
@@ -18,10 +18,13 @@ import { UNAUTHORIZED_EVENT, MAINTENANCE_EVENT } from "./services/api";
import MaintenancePage from "./pages/MaintenancePage";
import { usePlatform } from "./hooks/usePlatform";
import { useAuthStore } from "./stores/authStore";
+import { useServerStore } from "./stores/serverStore";
import { DesktopAuthScreen } from "./pages/desktop/auth/DesktopAuthScreen";
import { MobileBrowserStub } from "./pages/desktop/auth/MobileBrowserStub";
import { DesktopLayout } from "./layouts/DesktopLayout/DesktopLayout";
import { InvoicesDashboard } from "./pages/desktop/dashboard/InvoicesDashboard";
+import OperatorRestricted from "./components/OperatorRestricted";
+import type { UserRole } from "./services/types";
// Компонент-заглушка для внешних браузеров
const NotInTelegramScreen = () => (
@@ -64,9 +67,12 @@ const ProtectedDesktopRoute = ({ children }: { children: React.ReactNode }) => {
const AppContent = () => {
const [isUnauthorized, setIsUnauthorized] = useState(false);
const [isMaintenance, setIsMaintenance] = useState(false);
+ const [userRole, setUserRole] = useState(null);
+ const [isLoadingRole, setIsLoadingRole] = useState(true);
const tg = window.Telegram?.WebApp;
const platform = usePlatform();
- const location = useLocation(); // Теперь это безопасно, т.к. мы внутри BrowserRouter
+ const location = useLocation();
+ const { activeServer, fetchServers } = useServerStore();
// Проверяем, есть ли данные от Telegram
const isInTelegram = !!tg?.initData;
@@ -74,6 +80,28 @@ const AppContent = () => {
// Проверяем, находимся ли мы на десктопном роуте
const isDesktopRoute = location.pathname.startsWith("/web");
+ // Загружаем роль пользователя и список серверов при монтировании
+ useEffect(() => {
+ const loadUserData = async () => {
+ try {
+ // Загружаем список серверов (там есть информация о роли)
+ await fetchServers();
+
+ // Если есть активный сервер, получаем роль из него
+ const currentServer = useServerStore.getState().activeServer;
+ if (currentServer) {
+ setUserRole(currentServer.role);
+ }
+ } catch (error) {
+ console.error('Ошибка при загрузке данных пользователя:', error);
+ } finally {
+ setIsLoadingRole(false);
+ }
+ };
+
+ loadUserData();
+ }, [fetchServers]);
+
useEffect(() => {
const handleUnauthorized = () => setIsUnauthorized(true);
const handleMaintenance = () => setIsMaintenance(true);
@@ -90,6 +118,23 @@ const AppContent = () => {
};
}, [tg]);
+ // Показываем лоадер пока загружается роль
+ if (isLoadingRole) {
+ return (
+
+
+
+ );
+ }
+
// Если открыто не в Telegram и это не десктопный роут — блокируем всё
if (!isInTelegram && !isDesktopRoute) {
return ;
@@ -100,6 +145,11 @@ const AppContent = () => {
return ;
}
+ // Заглушка для операторов (только для мобильной версии в Telegram)
+ if (userRole === 'OPERATOR' && isInTelegram) {
+ return ;
+ }
+
// Если бэкенд вернул 401
if (isUnauthorized) {
return (
diff --git a/rmser-view/src/components/OperatorRestricted.tsx b/rmser-view/src/components/OperatorRestricted.tsx
new file mode 100644
index 0000000..58fe9e4
--- /dev/null
+++ b/rmser-view/src/components/OperatorRestricted.tsx
@@ -0,0 +1,90 @@
+import React from "react";
+import { Result, Button, Spin } from "antd";
+import { StopOutlined, CameraOutlined } from "@ant-design/icons";
+
+interface Props {
+ serverName?: string;
+ loading?: boolean;
+}
+
+/**
+ * Компонент заглушки для операторов.
+ * Отображается вместо основного интерфейса приложения для пользователей с ролью OPERATOR.
+ * Операторы могут загружать фото накладных только через Telegram-бота.
+ */
+const OperatorRestricted: React.FC = ({
+ serverName,
+ loading = false,
+}) => {
+ // Показываем лоадер пока идёт загрузка настроек
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
}
+ title="Доступ ограничен"
+ subTitle={
+
+
+ Вы вошли как Оператор
+ {serverName && (
+ <>
+ {" "}
+ на сервере {serverName}
+ >
+ )}
+ .
+
+
+ Операторы могут загружать фото накладных только через
+ Telegram-бота.
+
+
+ Для доступа к полному интерфейсу обратитесь к администратору
+ сервера.
+
+
+ }
+ extra={
+
}
+ size="large"
+ onClick={() => {
+ // Открываем Telegram-бота
+ window.location.href = "https://t.me/RmserBot";
+ }}
+ >
+ Открыть бота в Telegram
+
+ }
+ />
+
+ );
+};
+
+export default OperatorRestricted;
diff --git a/rmser-view/src/components/common/ExcelPreviewModal.tsx b/rmser-view/src/components/common/ExcelPreviewModal.tsx
index 1a6f8d7..7851190 100644
--- a/rmser-view/src/components/common/ExcelPreviewModal.tsx
+++ b/rmser-view/src/components/common/ExcelPreviewModal.tsx
@@ -6,6 +6,7 @@ import {
UndoOutlined,
} from "@ant-design/icons";
import * as XLSX from "xlsx";
+import { apiClient } from "../../services/api";
interface ExcelPreviewModalProps {
visible: boolean;
@@ -38,18 +39,23 @@ const ExcelPreviewModal: React.FC = ({
return;
}
+ console.log("ExcelPreviewModal: Start loading", fileUrl);
+
try {
- // Загрузка файла как arrayBuffer
- const response = await fetch(fileUrl);
-
- if (!response.ok) {
- throw new Error(`Ошибка загрузки файла: ${response.status}`);
- }
-
- const arrayBuffer = await response.arrayBuffer();
+ // Загрузка файла через apiClient с авторизацией
+ const response = await apiClient.get(fileUrl, {
+ responseType: "arraybuffer",
+ });
+ console.log(
+ "ExcelPreviewModal: Got response",
+ response.status,
+ response.data.byteLength
+ );
+ const arrayBuffer = response.data;
// Чтение Excel файла
const workbook = XLSX.read(arrayBuffer, { type: "array" });
+ console.log("ExcelPreviewModal: Workbook parsed", workbook.SheetNames);
// Получение первого листа
const firstSheetName = workbook.SheetNames[0];
@@ -61,11 +67,26 @@ const ExcelPreviewModal: React.FC = ({
}) as (string | number | boolean | null | undefined)[][];
setData(jsonData);
+ console.log("ExcelPreviewModal: Data set, rows:", jsonData.length);
// Сброс масштаба при загрузке нового файла
setScale(1);
} catch (error) {
- console.error("Ошибка при загрузке Excel файла:", error);
- message.error("Не удалось загрузить Excel файл");
+ console.error("ExcelPreviewModal Error:", error);
+
+ // Обработка ошибок авторизации (401) обрабатывается в интерсепторе apiClient
+ if (error && typeof error === "object" && "response" in error) {
+ const axiosError = error as { response?: { status?: number } };
+ if (axiosError.response?.status === 401) {
+ message.error(
+ "Ошибка авторизации. Необходима повторная авторизация."
+ );
+ } else {
+ message.error("Не удалось загрузить Excel файл");
+ }
+ } else {
+ message.error("Не удалось загрузить Excel файл");
+ }
+
setData([]);
}
};
@@ -94,10 +115,18 @@ const ExcelPreviewModal: React.FC = ({
setScale(1);
};
+ /**
+ * Обработчик закрытия модалки
+ */
+ const handleCancel = () => {
+ setData([]);
+ onCancel();
+ };
+
return (
= ({
// Состояние для просмотра Excel файла
const [excelPreviewVisible, setExcelPreviewVisible] = useState(false);
+ // Состояние для чекбокса "Проведено"
+ const [isProcessed, setIsProcessed] = useState(true); // По умолчанию true для MVP
+
// --- ЗАПРОСЫ ---
const dictQuery = useQuery({
@@ -282,6 +286,7 @@ export const DraftEditor: React.FC = ({
supplier_id: values.supplier_id,
comment: values.comment || "",
incoming_document_number: values.incoming_document_number || "",
+ is_processed: isProcessed,
});
} catch {
message.error("Заполните обязательные поля (Склад, Поставщик)");
@@ -289,6 +294,7 @@ export const DraftEditor: React.FC = ({
};
const isCanceled = draft?.status === "CANCELED";
+ const isCompleted = draft?.status === "COMPLETED";
const handleBack = () => {
if (isDirty) {
@@ -512,7 +518,7 @@ export const DraftEditor: React.FC = ({
= ({
layout="vertical"
onValuesChange={() => markAsDirty()}
>
-
+
@@ -541,13 +548,14 @@ export const DraftEditor: React.FC = ({
-
+
-
+
= ({
}
+ // icon={}
onClick={handleCommit}
loading={commitMutation.isPending}
disabled={invalidItemsCount > 0 || isCanceled}
- style={{ height: 36, padding: "0 20px" }}
+ style={{
+ position: "relative",
+ paddingLeft: 40,
+ height: 36,
+ }}
size="small"
>
- {isCanceled ? "Восстановить" : "Отправить"}
+ setIsProcessed(e.target.checked)}
+ onClick={(e) => e.stopPropagation()}
+ disabled={invalidItemsCount > 0 || isCanceled}
+ style={{
+ position: "absolute",
+ left: 10,
+ top: "50%",
+ transform: "translateY(-50%)",
+ pointerEvents: "auto",
+ }}
+ />
+
+ {isCanceled
+ ? "Восстановить"
+ : isCompleted
+ ? "Обновить в iiko"
+ : isProcessed
+ ? "Провести и отправить"
+ : "Сохранить (без проведения)"}
+
@@ -736,7 +769,7 @@ export const DraftEditor: React.FC = ({
setExcelPreviewVisible(false)}
- fileUrl={draft.photo_url ? getStaticUrl(draft.photo_url) : ""}
+ fileUrl={draft.photo_url || ""}
/>
);
diff --git a/rmser-view/src/components/invoices/InvoiceViewer.tsx b/rmser-view/src/components/invoices/InvoiceViewer.tsx
index 61d9b34..192c64e 100644
--- a/rmser-view/src/components/invoices/InvoiceViewer.tsx
+++ b/rmser-view/src/components/invoices/InvoiceViewer.tsx
@@ -5,7 +5,7 @@ import {
FileImageOutlined,
FileExcelOutlined,
HistoryOutlined,
- RestOutlined,
+ ArrowLeftOutlined,
} from "@ant-design/icons";
import { api, getStaticUrl } from "../../services/api";
import type { DraftStatus } from "../../services/types";
@@ -140,7 +140,7 @@ export const InvoiceViewer: React.FC = ({
{onBack && (
}
+ icon={}
onClick={onBack}
size="small"
style={{ flexShrink: 0 }}
@@ -256,7 +256,7 @@ export const InvoiceViewer: React.FC = ({
setExcelPreviewVisible(false)}
- fileUrl={invoice.photo_url ? getStaticUrl(invoice.photo_url) : ""}
+ fileUrl={invoice.photo_url || ""}
/>
);
diff --git a/rmser-view/src/components/settings/SyncBlock.tsx b/rmser-view/src/components/settings/SyncBlock.tsx
new file mode 100644
index 0000000..aab3edf
--- /dev/null
+++ b/rmser-view/src/components/settings/SyncBlock.tsx
@@ -0,0 +1,76 @@
+import React from "react";
+import { Card, Button, Typography, Space, Tooltip } from "antd";
+import { SyncOutlined } from "@ant-design/icons";
+import dayjs from "dayjs";
+import relativeTime from "dayjs/plugin/relativeTime";
+import "dayjs/locale/ru";
+import type { UserRole } from "../../services/types";
+
+// Настройка dayjs для русской локали и относительного времени
+dayjs.extend(relativeTime);
+dayjs.locale("ru");
+
+const { Text } = Typography;
+
+interface SyncBlockProps {
+ lastSyncAt: string | null;
+ userRole: UserRole;
+ onSync: () => void;
+ isLoading?: boolean;
+}
+
+export const SyncBlock: React.FC = ({
+ lastSyncAt,
+ userRole,
+ onSync,
+ isLoading = false,
+}) => {
+ // Проверяем, есть ли права на синхронизацию
+ const canSync = userRole === "OWNER" || userRole === "ADMIN";
+
+ // Форматируем дату последней синхронизации
+ const formatLastSync = (dateStr: string | null): string => {
+ if (!dateStr) {
+ return "Никогда";
+ }
+ const date = dayjs(dateStr);
+ const formatted = date.format("DD.MM.YYYY HH:mm");
+ const relative = date.fromNow();
+ return `${formatted} (${relative})`;
+ };
+
+ return (
+
+
+
+
+ Синхронизация данных
+
+
+ Последняя синхронизация: {formatLastSync(lastSyncAt)}
+
+
+
+
+ }
+ onClick={onSync}
+ loading={isLoading}
+ disabled={!canSync}
+ block
+ >
+ Синхронизировать
+
+
+
+
+ Загружает справочники, накладные и пересчитывает рекомендации
+
+
+
+ );
+};
diff --git a/rmser-view/src/hooks/useUserRole.ts b/rmser-view/src/hooks/useUserRole.ts
new file mode 100644
index 0000000..50b1379
--- /dev/null
+++ b/rmser-view/src/hooks/useUserRole.ts
@@ -0,0 +1,63 @@
+import { useEffect, useState, useCallback } from 'react';
+import { api } from '../services/api';
+import type { UserSettings, UserRole } from '../services/types';
+
+interface UseUserRoleResult {
+ /** Роль текущего пользователя или null если не загружено */
+ role: UserRole | null;
+ /** Полные настройки пользователя */
+ settings: UserSettings | null;
+ /** Состояние загрузки */
+ loading: boolean;
+ /** Ошибка загрузки */
+ error: string | null;
+ /** Функция для повторной загрузки настроек */
+ refetch: () => Promise;
+ /** Является ли пользователь оператором */
+ isOperator: boolean;
+ /** Является ли пользователь админом или владельцем */
+ isAdminOrOwner: boolean;
+}
+
+/**
+ * Хук для получения роли пользователя и настроек.
+ * Автоматически загружает настройки при монтировании компонента.
+ */
+export const useUserRole = (): UseUserRoleResult => {
+ const [settings, setSettings] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const fetchSettings = useCallback(async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const data = await api.getSettings();
+ setSettings(data);
+ } catch (err) {
+ console.error('Ошибка при загрузке настроек:', err);
+ setError(err instanceof Error ? err.message : 'Не удалось загрузить настройки');
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ // Загружаем настройки при монтировании
+ useEffect(() => {
+ fetchSettings();
+ }, [fetchSettings]);
+
+ const role = settings?.role ?? null;
+ const isOperator = role === 'OPERATOR';
+ const isAdminOrOwner = role === 'ADMIN' || role === 'OWNER';
+
+ return {
+ role,
+ settings,
+ loading,
+ error,
+ refetch: fetchSettings,
+ isOperator,
+ isAdminOrOwner,
+ };
+};
diff --git a/rmser-view/src/pages/DraftsList.tsx b/rmser-view/src/pages/DraftsList.tsx
index cf22923..02475ca 100644
--- a/rmser-view/src/pages/DraftsList.tsx
+++ b/rmser-view/src/pages/DraftsList.tsx
@@ -161,10 +161,21 @@ export const DraftsList: React.FC = () => {
};
const handleInvoiceClick = (item: UnifiedInvoice) => {
+ // Если это черновик - используем его ID
if (item.type === "DRAFT") {
navigate("/invoice/draft/" + item.id);
- } else if (item.type === "SYNCED") {
- navigate("/invoice/view/" + item.id);
+ return;
+ }
+
+ // Если это синхронизированная накладная
+ if (item.type === "SYNCED") {
+ // Если у нее есть ссылка на черновик (пришла с бэка) - открываем редактор черновика
+ if (item.draft_id) {
+ navigate("/invoice/draft/" + item.draft_id);
+ } else {
+ // Иначе просто просмотр
+ navigate("/invoice/view/" + item.id);
+ }
}
};
@@ -285,31 +296,33 @@ export const DraftsList: React.FC = () => {
-
+
Период:
-
- —
-
+
+
+ —
+
+
diff --git a/rmser-view/src/pages/SettingsPage.tsx b/rmser-view/src/pages/SettingsPage.tsx
index 260456e..ddf966e 100644
--- a/rmser-view/src/pages/SettingsPage.tsx
+++ b/rmser-view/src/pages/SettingsPage.tsx
@@ -28,6 +28,7 @@ import { api } from "../services/api";
import type { UserSettings } from "../services/types";
import { TeamList } from "../components/settings/TeamList";
import { PhotoStorageTab } from "../components/settings/PhotoStorageTab";
+import { SyncBlock } from "../components/settings/SyncBlock";
const { Title, Text } = Typography;
@@ -83,6 +84,17 @@ export const SettingsPage: React.FC = () => {
},
});
+ const syncMutation = useMutation({
+ mutationFn: () => api.syncAll(true),
+ onSuccess: () => {
+ message.success("Синхронизация запущена в фоне");
+ queryClient.invalidateQueries({ queryKey: ["settings"] });
+ },
+ onError: () => {
+ message.error("Ошибка запуска синхронизации");
+ },
+ });
+
// --- Эффекты ---
useEffect(() => {
@@ -96,7 +108,11 @@ export const SettingsPage: React.FC = () => {
const handleSave = async () => {
try {
const values = await form.validateFields();
- saveMutation.mutate(values);
+ console.log("Settings Form Values:", values);
+ saveMutation.mutate({
+ ...values,
+ auto_conduct: !!values.auto_conduct,
+ });
} catch {
// Ошибки валидации
}
@@ -117,121 +133,126 @@ export const SettingsPage: React.FC = () => {
const showTeamSettings =
currentUserRole === "ADMIN" || currentUserRole === "OWNER";
- // Сохраняем JSX в переменную вместо создания вложенного компонента
- const generalSettingsContent = (
-
- );
-
const tabsItems = [
{
key: "general",
label: "Общие",
icon: ,
- children: generalSettingsContent,
+ children: (
+
+ ),
},
];
diff --git a/rmser-view/src/services/api.ts b/rmser-view/src/services/api.ts
index 47155ce..23d865e 100644
--- a/rmser-view/src/services/api.ts
+++ b/rmser-view/src/services/api.ts
@@ -59,7 +59,7 @@ export const UNAUTHORIZED_EVENT = 'rms_unauthorized';
// Событие для режима технического обслуживания (503)
export const MAINTENANCE_EVENT = 'rms_maintenance';
-const apiClient = axios.create({
+export const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
@@ -264,7 +264,7 @@ export const api = {
},
getStats: async (): Promise => {
- const { data } = await apiClient.get('/stats/invoices');
+ const { data } = await apiClient.get('/invoices/stats');
return data;
},
@@ -298,6 +298,12 @@ export const api = {
syncInvoices: async (): Promise => {
await apiClient.post('/invoices/sync');
},
+
+ syncAll: async (force = true): Promise => {
+ await apiClient.post('/sync/all', null, {
+ params: { force }
+ });
+ },
getPhotos: async (page = 1, limit = 20): Promise => {
const { data } = await apiClient.get('/photos', {
params: { page, limit }
diff --git a/rmser-view/src/services/types.ts b/rmser-view/src/services/types.ts
index a044095..99457c5 100644
--- a/rmser-view/src/services/types.ts
+++ b/rmser-view/src/services/types.ts
@@ -156,6 +156,9 @@ export interface UserSettings {
default_store_id: UUID | null;
auto_conduct: boolean;
role: UserRole; // Добавляем поле роли в настройки текущего пользователя
+ last_sync_at: string | null; // Время последней синхронизации с iiko
+ last_activity_at: string | null; // Время последней активности
+ sync_interval: number; // Интервал синхронизации в минутах
}
export interface InvoiceStats {
@@ -255,6 +258,7 @@ export interface CommitDraftRequest {
supplier_id: UUID;
comment: string;
incoming_document_number?: string;
+ is_processed: boolean;
}
export interface ReorderDraftItemsRequest {
@@ -286,6 +290,7 @@ export interface UnifiedInvoice {
is_app_created: boolean; // Создано ли через наше приложение
items_preview: string; // Краткое содержание товаров
photo_url: string | null; // Ссылка на фото чека
+ draft_id?: string; // ID черновика для SYNCED накладных, созданных в приложении
}
export interface InvoiceDetails {
diff --git a/rmser-view/src/utils/calculations.ts b/rmser-view/src/utils/calculations.ts
index 27e9955..8959534 100644
--- a/rmser-view/src/utils/calculations.ts
+++ b/rmser-view/src/utils/calculations.ts
@@ -3,17 +3,25 @@ import type { DraftItem } from '../services/types';
/**
* Пересчитывает значения полей элемента черновика на основе измененного поля.
*
- * @param item - Исходный элемент черновика
+ * Логика "Треугольник": Q (Quantity) -> P (Price) -> S (Sum) -> Q...
+ * Правило: "Пересчитываем значение, следующее за редактируемым.
+ * Оставляем значение, предшествующее редактируемому."
+ *
+ * - Если меняем Quantity (Q): Previous=Sum (Keep), Next=Price (Recalc). Price = Sum / Quantity
+ * - Если меняем Price (P): Previous=Quantity (Keep), Next=Sum (Recalc). Sum = Quantity * Price
+ * - Если меняем Sum (S): Previous=Price (Keep), Next=Quantity (Recalc). Quantity = Sum / Price
+ *
+ * @param item - Исходный элемент черновика (содержит "предыдущие/сохраняемые" значения)
* @param changedField - Измененное поле ('quantity' | 'price' | 'sum')
* @param newValue - Новое значение измененного поля
* @returns Новый объект DraftItem с пересчитанными значениями
*
* @example
- * // При изменении количества
+ * // При изменении количества (пересчитываем цену, сумма сохраняется)
* const updated = recalculateItem(item, 'quantity', 5);
*
* @example
- * // При изменении суммы
+ * // При изменении суммы (пересчитываем количество, цена сохраняется)
* const updated = recalculateItem(item, 'sum', 100);
*/
export function recalculateItem(
@@ -23,38 +31,53 @@ export function recalculateItem(
): DraftItem {
switch (changedField) {
case 'quantity': {
- // При изменении количества пересчитываем сумму: sum = qty * price
- return {
- ...item,
- quantity: newValue,
- sum: newValue * item.price,
- };
- }
-
- case 'price': {
- // При изменении цены пересчитываем сумму: sum = qty * price
- return {
- ...item,
- price: newValue,
- sum: item.quantity * newValue,
- };
- }
-
- case 'sum': {
- // При изменении суммы пересчитываем цену: price = sum / qty
- // Обрабатываем случай деления на ноль
- if (item.quantity === 0) {
+ // Меняем Quantity (Q): Previous=Sum (Keep), Next=Price (Recalc)
+ // Price = Sum / Quantity
+ // Обрабатываем деление на ноль
+ if (newValue === 0) {
return {
...item,
- sum: newValue,
+ quantity: newValue,
price: 0,
};
}
+ const newPrice = item.sum / newValue;
+ return {
+ ...item,
+ quantity: newValue,
+ price: newPrice,
+ };
+ }
+
+ case 'price': {
+ // Меняем Price (P): Previous=Quantity (Keep), Next=Sum (Recalc)
+ // Sum = Quantity * Price
+ const newSum = item.quantity * newValue;
+ return {
+ ...item,
+ price: newValue,
+ sum: newSum,
+ };
+ }
+
+ case 'sum': {
+ // Меняем Sum (S): Previous=Price (Keep), Next=Quantity (Recalc)
+ // Quantity = Sum / Price
+ // Обрабатываем деление на ноль
+ if (item.price === 0) {
+ return {
+ ...item,
+ sum: newValue,
+ quantity: 0,
+ };
+ }
+
+ const newQuantity = newValue / item.price;
return {
...item,
sum: newValue,
- price: newValue / item.quantity,
+ quantity: newQuantity,
};
}