добавил пользователей для сервера и роли

добавил инвайт-ссылки с ролью оператор для сервера
добавил супер-админку для смены владельцев
добавил уведомления о смене ролей на серверах
добавил модалку для фото прям в черновике
добавил UI для редактирования прав
This commit is contained in:
2025-12-23 13:06:06 +03:00
parent 9441579a34
commit b4ce819931
21 changed files with 9244 additions and 418 deletions

View File

@@ -2,7 +2,10 @@ package ocr
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/google/uuid"
"github.com/shopspring/decimal"
@@ -18,16 +21,18 @@ type Service struct {
ocrRepo ocr.Repository
catalogRepo catalog.Repository
draftRepo drafts.Repository
accountRepo account.Repository // <-- NEW
accountRepo account.Repository
pyClient *ocr_client.Client
storagePath string
}
func NewService(
ocrRepo ocr.Repository,
catalogRepo catalog.Repository,
draftRepo drafts.Repository,
accountRepo account.Repository, // <-- NEW
accountRepo account.Repository,
pyClient *ocr_client.Client,
storagePath string,
) *Service {
return &Service{
ocrRepo: ocrRepo,
@@ -35,10 +40,23 @@ func NewService(
draftRepo: draftRepo,
accountRepo: accountRepo,
pyClient: pyClient,
storagePath: storagePath,
}
}
// ProcessReceiptImage
// checkWriteAccess - вспомогательный метод проверки прав
func (s *Service) checkWriteAccess(userID, serverID uuid.UUID) error {
role, err := s.accountRepo.GetUserRole(userID, serverID)
if err != nil {
return err
}
if role == account.RoleOperator {
return errors.New("access denied: operators cannot modify data")
}
return nil
}
// ProcessReceiptImage - Доступно всем (включая Операторов)
func (s *Service) ProcessReceiptImage(ctx context.Context, userID uuid.UUID, imgData []byte) (*drafts.DraftInvoice, error) {
// 1. Получаем активный сервер для UserID
server, err := s.accountRepo.GetActiveServer(userID)
@@ -54,6 +72,18 @@ func (s *Service) ProcessReceiptImage(ctx context.Context, userID uuid.UUID, img
Status: drafts.StatusProcessing,
StoreID: server.DefaultStoreID,
}
draft.ID = uuid.New()
fileName := fmt.Sprintf("receipt_%s.jpg", draft.ID.String())
filePath := filepath.Join(s.storagePath, fileName)
if err := os.WriteFile(filePath, imgData, 0644); err != nil {
return nil, fmt.Errorf("failed to save image: %w", err)
}
draft.SenderPhotoURL = "/uploads/" + fileName
if err := s.draftRepo.Create(draft); err != nil {
return nil, fmt.Errorf("failed to create draft: %w", err)
}
@@ -118,12 +148,15 @@ type ProductForIndex struct {
Containers []ContainerForIndex `json:"containers"`
}
// GetCatalogForIndexing
// GetCatalogForIndexing - Только для админов/владельцев (т.к. используется для ручного матчинга)
func (s *Service) GetCatalogForIndexing(userID uuid.UUID) ([]ProductForIndex, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("no server")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
products, err := s.catalogRepo.GetActiveGoods(server.ID, server.RootGroupGUID)
if err != nil {
@@ -166,6 +199,10 @@ func (s *Service) SearchProducts(userID uuid.UUID, query string) ([]catalog.Prod
if err != nil || server == nil {
return nil, fmt.Errorf("no server")
}
// Поиск нужен для матчинга, значит тоже защищаем
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
return s.catalogRepo.Search(server.ID, query, server.RootGroupGUID)
}
@@ -174,6 +211,9 @@ func (s *Service) SaveMapping(userID uuid.UUID, rawName string, productID uuid.U
if err != nil || server == nil {
return fmt.Errorf("no server")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return err
}
return s.ocrRepo.SaveMatch(server.ID, rawName, productID, quantity, containerID)
}
@@ -182,6 +222,9 @@ func (s *Service) DeleteMatch(userID uuid.UUID, rawName string) error {
if err != nil || server == nil {
return fmt.Errorf("no server")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return err
}
return s.ocrRepo.DeleteMatch(server.ID, rawName)
}
@@ -190,6 +233,9 @@ func (s *Service) GetKnownMatches(userID uuid.UUID) ([]ocr.ProductMatch, error)
if err != nil || server == nil {
return nil, fmt.Errorf("no server")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
return s.ocrRepo.GetAllMatches(server.ID)
}
@@ -198,5 +244,8 @@ func (s *Service) GetUnmatchedItems(userID uuid.UUID) ([]ocr.UnmatchedItem, erro
if err != nil || server == nil {
return nil, fmt.Errorf("no server")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
return s.ocrRepo.GetTopUnmatched(server.ID, 50)
}