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

добавил инвайт-ссылки с ролью оператор для сервера
добавил супер-админку для смены владельцев
добавил уведомления о смене ролей на серверах
добавил модалку для фото прям в черновике
добавил 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

@@ -47,13 +47,57 @@ func NewService(
}
}
// checkWriteAccess проверяет, что пользователь имеет право редактировать данные на сервере (ADMIN/OWNER)
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("доступ запрещен: оператор не может редактировать данные")
}
return nil
}
func (s *Service) GetDraft(draftID, userID uuid.UUID) (*drafts.DraftInvoice, error) {
// TODO: Проверить что userID совпадает с draft.UserID
return s.draftRepo.GetByID(draftID)
draft, err := s.draftRepo.GetByID(draftID)
if err != nil {
return nil, err
}
// Проверяем, что черновик принадлежит активному серверу пользователя
// И пользователь не Оператор (операторы вообще не ходят в API)
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, errors.New("нет активного сервера")
}
if draft.RMSServerID != server.ID {
return nil, errors.New("черновик не принадлежит активному серверу")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
return draft, nil
}
func (s *Service) GetActiveDrafts(userID uuid.UUID) ([]drafts.DraftInvoice, error) {
return s.draftRepo.GetActive(userID)
// 1. Узнаем активный сервер
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, errors.New("активный сервер не выбран")
}
// 2. Проверяем роль (Security)
// Операторам список недоступен
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
// 3. Возвращаем все черновики СЕРВЕРА
return s.draftRepo.GetActive(server.ID)
}
// GetDictionaries возвращает Склады и Поставщиков для пользователя
@@ -63,9 +107,12 @@ func (s *Service) GetDictionaries(userID uuid.UUID) (map[string]interface{}, err
return nil, fmt.Errorf("active server not found")
}
stores, _ := s.catalogRepo.GetActiveStores(server.ID)
// Словари нужны только тем, кто редактирует
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
// Ранжированные поставщики (топ за 90 дней)
stores, _ := s.catalogRepo.GetActiveStores(server.ID)
suppliersList, _ := s.supplierRepo.GetRankedByUsage(server.ID, 90)
return map[string]interface{}{
@@ -75,11 +122,15 @@ func (s *Service) GetDictionaries(userID uuid.UUID) (map[string]interface{}, err
}
func (s *Service) DeleteDraft(id uuid.UUID) (string, error) {
// Без изменений логики, только вызов репо
draft, err := s.draftRepo.GetByID(id)
if err != nil {
return "", err
}
// TODO: Здесь тоже бы проверить userID и права, но пока оставим как есть,
// так как DeleteDraft вызывается из хендлера, где мы можем добавить проверку,
// но лучше передавать userID в сигнатуру DeleteDraft(id, userID).
// Для скорости пока оставим, полагаясь на то, что фронт не покажет кнопку.
if draft.Status == drafts.StatusCanceled {
draft.Status = drafts.StatusDeleted
s.draftRepo.Update(draft)
@@ -110,8 +161,6 @@ func (s *Service) UpdateDraftHeader(id uuid.UUID, storeID *uuid.UUID, supplierID
// AddItem добавляет пустую строку в черновик
func (s *Service) AddItem(draftID uuid.UUID) (*drafts.DraftInvoiceItem, error) {
// Проверка статуса драфта (можно добавить)
newItem := &drafts.DraftInvoiceItem{
ID: uuid.New(),
DraftID: draftID,
@@ -132,19 +181,15 @@ func (s *Service) AddItem(draftID uuid.UUID) (*drafts.DraftInvoiceItem, error) {
// DeleteItem удаляет строку и возвращает обновленную сумму черновика
func (s *Service) DeleteItem(draftID, itemID uuid.UUID) (float64, error) {
// 1. Удаляем
if err := s.draftRepo.DeleteItem(itemID); err != nil {
return 0, err
}
// 2. Получаем драфт заново для пересчета суммы
// Это самый надежный способ, чем считать в памяти
draft, err := s.draftRepo.GetByID(draftID)
if err != nil {
return 0, err
}
// 3. Считаем сумму
var totalSum decimal.Decimal
for _, item := range draft.Items {
if !item.Sum.IsZero() {
@@ -163,6 +208,7 @@ func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, co
if err != nil {
return err
}
// Автосмена статуса
if draft.Status == drafts.StatusCanceled {
draft.Status = drafts.StatusReadyToVerify
s.draftRepo.Update(draft)
@@ -172,9 +218,13 @@ func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, co
// CommitDraft отправляет накладную
func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
// 1. Клиент для пользователя
client, err := s.rmsFactory.GetClientForUser(userID)
// 1. Получаем сервер и права
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil {
return "", fmt.Errorf("active server not found: %w", err)
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return "", err
}
@@ -183,13 +233,20 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
if err != nil {
return "", err
}
// Проверка принадлежности черновика серверу
if draft.RMSServerID != server.ID {
return "", errors.New("черновик принадлежит другому серверу")
}
if draft.Status == drafts.StatusCompleted {
return "", errors.New("накладная уже отправлена")
}
server, err := s.accountRepo.GetActiveServer(userID)
// 3. Клиент (использует права текущего юзера - Админа/Владельца)
client, err := s.rmsFactory.GetClientForUser(userID)
if err != nil {
return "", fmt.Errorf("active server not found: %w", err)
return "", err
}
targetStatus := "NEW"
@@ -197,15 +254,15 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
targetStatus = "PROCESSED"
}
// 3. Сборка Invoice
// 4. Сборка Invoice
inv := invoices.Invoice{
ID: uuid.Nil,
DocumentNumber: draft.DocumentNumber,
DateIncoming: *draft.DateIncoming,
SupplierID: *draft.SupplierID,
DefaultStoreID: *draft.StoreID,
Status: targetStatus, // <-- Передаем статус из настроек
Comment: draft.Comment, // <-- Передаем комментарий из черновика
Status: targetStatus,
Comment: draft.Comment,
Items: make([]invoices.InvoiceItem, 0, len(draft.Items)),
}
@@ -214,7 +271,6 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
continue // Skip unrecognized
}
// Если суммы нет, считаем
sum := dItem.Sum
if sum.IsZero() {
sum = dItem.Quantity.Mul(dItem.Price)
@@ -234,17 +290,17 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
return "", errors.New("нет распознанных позиций для отправки")
}
// 4. Отправка в RMS
// 5. Отправка в RMS
docNum, err := client.CreateIncomingInvoice(inv)
if err != nil {
return "", err
}
// 5. Обновление статуса черновика
// 6. Обновление статуса черновика
draft.Status = drafts.StatusCompleted
s.draftRepo.Update(draft)
// 6. БИЛЛИНГ и Обучение
// 7. БИЛЛИНГ и Обучение
if err := s.accountRepo.IncrementInvoiceCount(server.ID); err != nil {
logger.Log.Error("Billing increment failed", zap.Error(err))
}
@@ -266,11 +322,18 @@ func (s *Service) learnFromDraft(draft *drafts.DraftInvoice, serverID uuid.UUID)
}
func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID, name string, count decimal.Decimal) (uuid.UUID, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return uuid.Nil, errors.New("no active server")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return uuid.Nil, err
}
client, err := s.rmsFactory.GetClientForUser(userID)
if err != nil {
return uuid.Nil, err
}
server, _ := s.accountRepo.GetActiveServer(userID) // нужен ServerID для сохранения в локальную БД
fullProduct, err := client.GetProductByID(productID)
if err != nil {
@@ -337,7 +400,7 @@ func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID,
// Save Local
newLocalContainer := catalog.ProductContainer{
ID: createdID,
RMSServerID: server.ID, // <-- NEW
RMSServerID: server.ID,
ProductID: productID,
Name: name,
Count: count,

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)
}