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

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