diff --git a/cmd/main.go b/cmd/main.go index 0c2688f..bc085dd 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -92,7 +92,7 @@ func main() { syncService := sync.NewService(rmsFactory, accountRepo, catalogRepo, recipesRepo, invoicesRepo, opsRepo, supplierRepo) recService := recServicePkg.NewService(recRepo) ocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, draftsRepo, accountRepo, pyClient, cfg.App.StoragePath) - draftsService := draftsServicePkg.NewService(draftsRepo, ocrRepo, catalogRepo, accountRepo, supplierRepo, rmsFactory, billingService) + draftsService := draftsServicePkg.NewService(draftsRepo, ocrRepo, catalogRepo, accountRepo, supplierRepo, invoicesRepo, rmsFactory, billingService) // 7. Handlers draftsHandler := handlers.NewDraftsHandler(draftsService) @@ -173,9 +173,10 @@ func main() { // Manual Sync Trigger api.POST("/sync/all", func(c *gin.Context) { userID := c.MustGet("userID").(uuid.UUID) + force := c.Query("force") == "true" // Запускаем в горутине, чтобы не держать соединение go func() { - if err := syncService.SyncAllData(userID); err != nil { + if err := syncService.SyncAllData(userID, force); err != nil { logger.Log.Error("Manual sync failed", zap.Error(err)) } }() diff --git a/internal/domain/invoices/entity.go b/internal/domain/invoices/entity.go index 361b8fe..6947aba 100644 --- a/internal/domain/invoices/entity.go +++ b/internal/domain/invoices/entity.go @@ -11,14 +11,15 @@ import ( // Invoice - Приходная накладная type Invoice struct { - ID uuid.UUID `gorm:"type:uuid;primary_key;"` - RMSServerID uuid.UUID `gorm:"type:uuid;not null;index"` - DocumentNumber string `gorm:"type:varchar(100);index"` - DateIncoming time.Time `gorm:"index"` - SupplierID uuid.UUID `gorm:"type:uuid;index"` - DefaultStoreID uuid.UUID `gorm:"type:uuid;index"` - Status string `gorm:"type:varchar(50)"` - Comment string `gorm:"type:text"` + ID uuid.UUID `gorm:"type:uuid;primary_key;"` + RMSServerID uuid.UUID `gorm:"type:uuid;not null;index"` + DocumentNumber string `gorm:"type:varchar(100);index"` + IncomingDocumentNumber string `gorm:"type:varchar(100)"` + DateIncoming time.Time `gorm:"index"` + SupplierID uuid.UUID `gorm:"type:uuid;index"` + DefaultStoreID uuid.UUID `gorm:"type:uuid;index"` + Status string `gorm:"type:varchar(50)"` + Comment string `gorm:"type:text"` Items []InvoiceItem `gorm:"foreignKey:InvoiceID;constraint:OnDelete:CASCADE"` @@ -42,6 +43,7 @@ type InvoiceItem struct { type Repository interface { GetLastInvoiceDate(serverID uuid.UUID) (*time.Time, error) + GetByPeriod(serverID uuid.UUID, from, to time.Time) ([]Invoice, error) SaveInvoices(invoices []Invoice) error CountRecent(serverID uuid.UUID, days int) (int64, error) } diff --git a/internal/infrastructure/repository/invoices/postgres.go b/internal/infrastructure/repository/invoices/postgres.go index 8fb870e..aff9cb5 100644 --- a/internal/infrastructure/repository/invoices/postgres.go +++ b/internal/infrastructure/repository/invoices/postgres.go @@ -31,6 +31,16 @@ func (r *pgRepository) GetLastInvoiceDate(serverID uuid.UUID) (*time.Time, error return &inv.DateIncoming, nil } +func (r *pgRepository) GetByPeriod(serverID uuid.UUID, from, to time.Time) ([]invoices.Invoice, error) { + var list []invoices.Invoice + err := r.db. + Preload("Items"). + Where("rms_server_id = ? AND date_incoming BETWEEN ? AND ?", serverID, from, to). + Order("date_incoming DESC"). + Find(&list).Error + return list, err +} + func (r *pgRepository) SaveInvoices(list []invoices.Invoice) error { return r.db.Transaction(func(tx *gorm.DB) error { for _, inv := range list { diff --git a/internal/infrastructure/rms/client.go b/internal/infrastructure/rms/client.go index 7014a91..768167c 100644 --- a/internal/infrastructure/rms/client.go +++ b/internal/infrastructure/rms/client.go @@ -518,13 +518,14 @@ func (c *Client) FetchInvoices(from, to time.Time) ([]invoices.Invoice, error) { } allinvoices = append(allinvoices, invoices.Invoice{ - ID: docID, - DocumentNumber: doc.DocumentNumber, - DateIncoming: dateInc, - SupplierID: supID, - DefaultStoreID: storeID, - Status: doc.Status, - Items: items, + ID: docID, + DocumentNumber: doc.DocumentNumber, + IncomingDocumentNumber: doc.IncomingDocumentNumber, + DateIncoming: dateInc, + SupplierID: supID, + DefaultStoreID: storeID, + Status: doc.Status, + Items: items, }) } diff --git a/internal/infrastructure/rms/dto.go b/internal/infrastructure/rms/dto.go index 7d0cbfc..4850c62 100644 --- a/internal/infrastructure/rms/dto.go +++ b/internal/infrastructure/rms/dto.go @@ -142,13 +142,14 @@ type IncomingInvoiceListXML struct { } type IncomingInvoiceXML struct { - ID string `xml:"id"` - DocumentNumber string `xml:"documentNumber"` - DateIncoming string `xml:"dateIncoming"` // Format: yyyy-MM-ddTHH:mm:ss - Status string `xml:"status"` // PROCESSED, NEW, DELETED - Supplier string `xml:"supplier"` // GUID - DefaultStore string `xml:"defaultStore"` // GUID - Items []InvoiceItemXML `xml:"items>item"` + ID string `xml:"id"` + DocumentNumber string `xml:"documentNumber"` + IncomingDocumentNumber string `xml:"incomingDocumentNumber"` + DateIncoming string `xml:"dateIncoming"` // Format: yyyy-MM-ddTHH:mm:ss + Status string `xml:"status"` // PROCESSED, NEW, DELETED + Supplier string `xml:"supplier"` // GUID + DefaultStore string `xml:"defaultStore"` // GUID + Items []InvoiceItemXML `xml:"items>item"` } type InvoiceItemXML struct { diff --git a/internal/services/drafts/service.go b/internal/services/drafts/service.go index f01e779..f7fdc5f 100644 --- a/internal/services/drafts/service.go +++ b/internal/services/drafts/service.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "strconv" + "strings" "time" "github.com/google/uuid" @@ -27,6 +28,7 @@ type Service struct { catalogRepo catalog.Repository accountRepo account.Repository supplierRepo suppliers.Repository + invoiceRepo invoices.Repository rmsFactory *rms.Factory billingService *billing.Service } @@ -37,6 +39,7 @@ func NewService( catalogRepo catalog.Repository, accountRepo account.Repository, supplierRepo suppliers.Repository, + invoiceRepo invoices.Repository, rmsFactory *rms.Factory, billingService *billing.Service, ) *Service { @@ -46,6 +49,7 @@ func NewService( catalogRepo: catalogRepo, accountRepo: accountRepo, supplierRepo: supplierRepo, + invoiceRepo: invoiceRepo, rmsFactory: rmsFactory, billingService: billingService, } @@ -220,6 +224,7 @@ func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, co return s.draftRepo.UpdateItem(itemID, productID, containerID, qty, price) } +// CommitDraft отправляет накладную // CommitDraft отправляет накладную func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) { // 1. Получаем сервер и права @@ -285,11 +290,39 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) { sum = dItem.Quantity.Mul(dItem.Price) } + // Инициализируем значениями из черновика (по умолчанию для базовых единиц) + amountToSend := dItem.Quantity + priceToSend := dItem.Price + + // ЛОГИКА ПЕРЕСЧЕТА ДЛЯ ФАСОВОК (СОГЛАСНО ДОКУМЕНТАЦИИ IIKO) + // Если указан ContainerID, iiko требует: + // = кол-во упаковок * вес упаковки (итоговое кол-во в базовых единицах) + // = цена за упаковку / вес упаковки (цена за базовую единицу) + // = ID фасовки + if dItem.ContainerID != nil && *dItem.ContainerID != uuid.Nil { + // Проверяем, что Container загружен (Preload в репозитории) + if dItem.Container != nil { + if !dItem.Container.Count.IsZero() { + // 1. Пересчитываем кол-во: 5 ящиков * 10 кг = 50 кг + amountToSend = dItem.Quantity.Mul(dItem.Container.Count) + + // 2. Пересчитываем цену: 1000 руб/ящ / 10 кг = 100 руб/кг + priceToSend = dItem.Price.Div(dItem.Container.Count) + } + } else { + // Если фасовка есть в ID, но не подгрузилась структура - это ошибка данных. + // Логируем варнинг, но пробуем отправить как есть (iiko может отвергнуть или посчитать криво) + logger.Log.Warn("Container struct is nil for item with ContainerID", + zap.String("item_id", dItem.ID.String()), + zap.String("container_id", dItem.ContainerID.String())) + } + } + invItem := invoices.InvoiceItem{ ProductID: *dItem.ProductID, - Amount: dItem.Quantity, - Price: dItem.Price, - Sum: sum, + Amount: amountToSend, // Отправляем ПЕРЕСЧИТАННЫЙ вес/объем + Price: priceToSend, // Отправляем ПЕРЕСЧИТАННУЮ цену за базовую ед. + Sum: sum, // Сумма остается неизменной (Total) ContainerID: dItem.ContainerID, } inv.Items = append(inv.Items, invItem) @@ -424,3 +457,99 @@ func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID, return createdID, nil } + +// Добавим новый DTO для единого списка (Frontend Contract) +type UnifiedInvoiceDTO struct { + ID uuid.UUID `json:"id"` + Type string `json:"type"` // "DRAFT" или "SYNCED" + 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"` +} + +func (s *Service) GetUnifiedList(userID uuid.UUID, from, to time.Time) ([]UnifiedInvoiceDTO, error) { + server, err := s.accountRepo.GetActiveServer(userID) + if err != nil || server == nil { + return nil, errors.New("активный сервер не выбран") + } + + // 1. Получаем черновики (их обычно немного, берем все активные) + draftsList, err := s.draftRepo.GetActive(server.ID) + if err != nil { + return nil, err + } + + // 2. Получаем синхронизированные накладные за период + invoicesList, err := s.invoiceRepo.GetByPeriod(server.ID, from, to) + if err != nil { + return nil, err + } + + result := make([]UnifiedInvoiceDTO, 0, len(draftsList)+len(invoicesList)) + + // Маппим черновики + for _, d := range draftsList { + var sum decimal.Decimal + for _, it := range d.Items { + if !it.Sum.IsZero() { + sum = sum.Add(it.Sum) + } else { + sum = sum.Add(it.Quantity.Mul(it.Price)) + } + } + + val, _ := sum.Float64() + date := time.Now() + if d.DateIncoming != nil { + date = *d.DateIncoming + } + + result = append(result, UnifiedInvoiceDTO{ + ID: d.ID, + Type: "DRAFT", + DocumentNumber: d.DocumentNumber, + IncomingNumber: "", // В черновиках пока не разделяем + DateIncoming: date, + Status: d.Status, + TotalSum: val, + StoreName: "", // Можно подгрузить из d.Store.Name если сделан Preload + ItemsCount: len(d.Items), + CreatedAt: d.CreatedAt, + IsAppCreated: true, + }) + } + + // Маппим проведенные + for _, inv := range invoicesList { + var sum decimal.Decimal + for _, it := range inv.Items { + sum = sum.Add(it.Sum) + } + val, _ := sum.Float64() + + isOurs := strings.Contains(strings.ToUpper(inv.Comment), "RMSER") + + result = append(result, UnifiedInvoiceDTO{ + ID: inv.ID, + Type: "SYNCED", + DocumentNumber: inv.DocumentNumber, + IncomingNumber: inv.IncomingDocumentNumber, + DateIncoming: inv.DateIncoming, + Status: inv.Status, + TotalSum: val, + ItemsCount: len(inv.Items), + CreatedAt: inv.CreatedAt, + IsAppCreated: isOurs, + }) + } + + // Сортировка по дате накладной (desc) + // (Здесь можно добавить библиотеку sort или оставить как есть, если БД уже отсортировала части) + return result, nil +} diff --git a/internal/services/sync/service.go b/internal/services/sync/service.go index c480bf0..38d13b5 100644 --- a/internal/services/sync/service.go +++ b/internal/services/sync/service.go @@ -54,8 +54,8 @@ func NewService( } // SyncAllData запускает полную синхронизацию для конкретного пользователя -func (s *Service) SyncAllData(userID uuid.UUID) error { - logger.Log.Info("Запуск полной синхронизации", zap.String("user_id", userID.String())) +func (s *Service) SyncAllData(userID uuid.UUID, force bool) error { + logger.Log.Info("Запуск синхронизации", zap.String("user_id", userID.String()), zap.Bool("force", force)) // 1. Получаем клиент и инфо о сервере client, err := s.rmsFactory.GetClientForUser(userID) @@ -92,7 +92,7 @@ func (s *Service) SyncAllData(userID uuid.UUID) error { } // 6. Накладные (история) - if err := s.syncInvoices(client, serverID); err != nil { + if err := s.syncInvoices(client, serverID, force); err != nil { logger.Log.Error("Sync Invoices failed", zap.Error(err)) } @@ -172,19 +172,24 @@ func (s *Service) syncRecipes(c rms.ClientI, serverID uuid.UUID) error { return s.recipeRepo.SaveRecipes(recipesList) } -func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID) error { - lastDate, err := s.invoiceRepo.GetLastInvoiceDate(serverID) - if err != nil { - return err - } - +func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID, force bool) error { var from time.Time to := time.Now() - if lastDate != nil { - from = *lastDate + if force { + // Принудительная перезагрузка за последние 40 дней + from = time.Now().AddDate(0, 0, -40) + logger.Log.Info("Force sync invoices", zap.String("from", from.Format("2006-01-02"))) } else { - from = time.Now().AddDate(0, 0, -45) // 45 дней по дефолту + lastDate, err := s.invoiceRepo.GetLastInvoiceDate(serverID) + if err != nil { + return err + } + if lastDate != nil { + from = *lastDate + } else { + from = time.Now().AddDate(0, 0, -45) + } } invs, err := c.FetchInvoices(from, to) @@ -194,10 +199,10 @@ func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID) error { for i := range invs { invs[i].RMSServerID = serverID - // В Items пока не добавляли ServerID } if len(invs) > 0 { + // Репозиторий использует OnConflict(UpdateAll), поэтому существующие записи обновятся return s.invoiceRepo.SaveInvoices(invs) } return nil diff --git a/internal/transport/http/handlers/drafts.go b/internal/transport/http/handlers/drafts.go index 72eb698..5ade21b 100644 --- a/internal/transport/http/handlers/drafts.go +++ b/internal/transport/http/handlers/drafts.go @@ -253,49 +253,28 @@ type DraftListItemDTO struct { TotalSum float64 `json:"total_sum"` StoreName string `json:"store_name"` CreatedAt string `json:"created_at"` + IsAppCreated bool `json:"is_app_created"` } func (h *DraftsHandler) GetDrafts(c *gin.Context) { userID := c.MustGet("userID").(uuid.UUID) - list, err := h.service.GetActiveDrafts(userID) + + // Читаем параметры периода из Query (default: 30 days) + fromStr := c.DefaultQuery("from", time.Now().AddDate(0, 0, -30).Format("2006-01-02")) + toStr := c.DefaultQuery("to", time.Now().Format("2006-01-02")) + + from, _ := time.Parse("2006-01-02", fromStr) + to, _ := time.Parse("2006-01-02", toStr) + // Устанавливаем конец дня для 'to' + to = to.Add(23*time.Hour + 59*time.Minute + 59*time.Second) + + list, err := h.service.GetUnifiedList(userID, from, to) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - response := make([]DraftListItemDTO, 0, len(list)) - for _, d := range list { - var totalSum decimal.Decimal - for _, item := range d.Items { - if !item.Sum.IsZero() { - totalSum = totalSum.Add(item.Sum) - } else { - totalSum = totalSum.Add(item.Quantity.Mul(item.Price)) - } - } - sumFloat, _ := totalSum.Float64() - - dateStr := "" - if d.DateIncoming != nil { - dateStr = d.DateIncoming.Format("2006-01-02") - } - storeName := "" - if d.Store != nil { - storeName = d.Store.Name - } - - response = append(response, DraftListItemDTO{ - ID: d.ID.String(), - DocumentNumber: d.DocumentNumber, - DateIncoming: dateStr, - Status: d.Status, - ItemsCount: len(d.Items), - TotalSum: sumFloat, - StoreName: storeName, - CreatedAt: d.CreatedAt.Format(time.RFC3339), - }) - } - c.JSON(http.StatusOK, response) + c.JSON(http.StatusOK, list) } func (h *DraftsHandler) DeleteDraft(c *gin.Context) { diff --git a/internal/transport/telegram/bot.go b/internal/transport/telegram/bot.go index 8dd91b9..7145908 100644 --- a/internal/transport/telegram/bot.go +++ b/internal/transport/telegram/bot.go @@ -97,10 +97,12 @@ func (bot *Bot) initMenus() { bot.menuServers = &tele.ReplyMarkup{} bot.menuDicts = &tele.ReplyMarkup{} - btnSync := bot.menuDicts.Data("⚡️ Обновить данные", "act_sync") + btnSync := bot.menuDicts.Data("⚡️ Быстрое обновление", "act_sync") + btnFullSync := bot.menuDicts.Data("♻️ Полная перезагрузка", "act_full_sync") btnBack := bot.menuDicts.Data("🔙 Назад", "nav_main") bot.menuDicts.Inline( bot.menuDicts.Row(btnSync), + bot.menuDicts.Row(btnFullSync), bot.menuDicts.Row(btnBack), ) @@ -122,6 +124,7 @@ func (bot *Bot) initHandlers() { bot.b.Handle(&tele.Btn{Unique: "act_add_server"}, bot.startAddServerFlow) bot.b.Handle(&tele.Btn{Unique: "act_sync"}, bot.triggerSync) + bot.b.Handle(&tele.Btn{Unique: "act_full_sync"}, bot.triggerFullSync) bot.b.Handle(&tele.Btn{Unique: "act_del_server_menu"}, bot.renderDeleteServerMenu) bot.b.Handle(&tele.Btn{Unique: "confirm_name_yes"}, bot.handleConfirmNameYes) bot.b.Handle(&tele.Btn{Unique: "confirm_name_no"}, bot.handleConfirmNameNo) @@ -571,6 +574,26 @@ func (bot *Bot) NotifySuccess(userID uuid.UUID, amount float64, newBalance int, bot.b.Send(&tele.User{ID: user.TelegramID}, msg, tele.ModeHTML) } +func (bot *Bot) triggerFullSync(c tele.Context) error { + userDB, _ := bot.accountRepo.GetUserByTelegramID(c.Sender().ID) + server, _ := bot.accountRepo.GetActiveServer(userDB.ID) + if server == nil { + return c.Respond(&tele.CallbackResponse{Text: "Нет активного сервера"}) + } + + c.Respond(&tele.CallbackResponse{Text: "Запущена полная перезагрузка данных...", ShowAlert: false}) + c.Send("⏳ Полная синхронизация\\nОбновляю историю накладных за 60 дней и справочники. Это может занять до 1 минуты.", tele.ModeHTML) + + go func() { + if err := bot.syncService.SyncAllData(userDB.ID, true); err != nil { + bot.b.Send(c.Sender(), "❌ Ошибка при полной синхронизации: "+err.Error()) + } else { + bot.b.Send(c.Sender(), "✅ Все данные успешно обновлены!") + } + }() + return nil +} + func (bot *Bot) handleBillingCallbacks(c tele.Context, data string, userDB *account.User) error { if data == "bill_topup" { return bot.renderTariffShowcase(c, "") @@ -670,7 +693,7 @@ func (bot *Bot) triggerSync(c tele.Context) error { } c.Respond(&tele.CallbackResponse{Text: "Запускаю синхронизацию..."}) go func() { - if err := bot.syncService.SyncAllData(userDB.ID); err != nil { + if err := bot.syncService.SyncAllData(userDB.ID, false); err != nil { logger.Log.Error("Manual sync failed", zap.Error(err)) bot.b.Send(c.Sender(), "❌ Ошибка синхронизации. Проверьте настройки сервера.") } else { @@ -867,7 +890,7 @@ func (bot *Bot) saveServerFinal(c tele.Context, userID int64, serverName string) successMsg += "Начинаю первичную синхронизацию данных..." c.Send(successMsg, tele.ModeHTML) - go bot.syncService.SyncAllData(userDB.ID) + go bot.syncService.SyncAllData(userDB.ID, false) return bot.renderMainMenu(c) } diff --git a/rmser-view/src/pages/DraftsList.tsx b/rmser-view/src/pages/DraftsList.tsx index 413956a..a5b0337 100644 --- a/rmser-view/src/pages/DraftsList.tsx +++ b/rmser-view/src/pages/DraftsList.tsx @@ -1,86 +1,233 @@ -import React from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { List, Typography, Tag, Spin, Empty } from 'antd'; -import { useNavigate } from 'react-router-dom'; -import { ArrowRightOutlined } from '@ant-design/icons'; -import { api } from '../services/api'; +// src/pages/DraftsList.tsx + +import React, { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { + List, + Typography, + Tag, + Spin, + Empty, + DatePicker, + Flex, + message, +} from "antd"; +import { useNavigate } from "react-router-dom"; +import { + ArrowRightOutlined, + ThunderboltFilled, + HistoryOutlined, + FileTextOutlined, +} from "@ant-design/icons"; +import dayjs, { Dayjs } from "dayjs"; +import { api } from "../services/api"; +import type { UnifiedInvoice } from "../services/types"; const { Title, Text } = Typography; +const { RangePicker } = DatePicker; export const DraftsList: React.FC = () => { const navigate = useNavigate(); - - const { data: drafts, isLoading, isError } = useQuery({ - queryKey: ['drafts'], - queryFn: api.getDrafts, - refetchOnWindowFocus: true + + // Состояние фильтра дат: по умолчанию последние 30 дней + const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([ + dayjs().subtract(30, "day"), + dayjs(), + ]); + + // Запрос данных с учетом дат (даты в ключе обеспечивают авто-перезапрос) + const { + data: invoices, + isLoading, + isError, + } = useQuery({ + queryKey: [ + "drafts", + dateRange[0].format("YYYY-MM-DD"), + dateRange[1].format("YYYY-MM-DD"), + ], + queryFn: () => + api.getDrafts( + dateRange[0].format("YYYY-MM-DD"), + dateRange[1].format("YYYY-MM-DD") + ), }); - const getStatusTag = (status: string) => { - switch (status) { - case 'PROCESSING': return Обработка; - case 'READY_TO_VERIFY': return Проверка; - case 'COMPLETED': return Готово; - case 'ERROR': return Ошибка; - case 'CANCELED': return Отменен; - default: return {status}; + const getStatusTag = (item: UnifiedInvoice) => { + if (item.type === "SYNCED") { + return ( + } color="success"> + Синхронизировано + + ); + } + + switch (item.status) { + case "PROCESSING": + return Обработка; + case "READY_TO_VERIFY": + return Проверка; + case "COMPLETED": + return Готово; + case "ERROR": + return Ошибка; + case "CANCELED": + return Отменен; + default: + return {item.status}; } }; - if (isLoading) { - return
; - } + const handleInvoiceClick = (item: UnifiedInvoice) => { + if (item.type === "SYNCED") { + message.info("История доступна только для просмотра"); + return; + } + navigate(`/invoice/${item.id}`); + }; if (isError) { - return
Ошибка загрузки списка
; + return ( +
+ Ошибка загрузки списка накладных +
+ ); } return ( -
- Черновики накладных - - {(!drafts || drafts.length === 0) ? ( - +
+ + Накладные + + + {/* Фильтр дат */} +
+ + Период загрузки: + + dates && setDateRange([dates[0]!, dates[1]!])} + style={{ width: "100%" }} + allowClear={false} + format="DD.MM.YYYY" + /> +
+ + {isLoading ? ( +
+ +
+ ) : !invoices || invoices.length === 0 ? ( + ) : ( ( - navigate(`/invoice/${item.id}`)} - > -
-
- - {item.document_number || 'Без номера'} + dataSource={invoices} + renderItem={(item) => { + const isSynced = item.type === "SYNCED"; + + return ( + handleInvoiceClick(item)} + > + + + + + + {item.document_number || "Без номера"} + + {item.is_app_created && ( + + )} + + {item.incoming_number && ( + + Вх. № {item.incoming_number} + + )} + + {getStatusTag(item)} + + + + + + + {item.items_count} поз. + + + • + + + {dayjs(item.date_incoming).format("DD.MM.YYYY")} + + + {item.store_name && ( + + {item.store_name} + + )} + + + + + {item.total_sum.toLocaleString("ru-RU", { + style: "currency", + currency: "RUB", + maximumFractionDigits: 0, + })} - {getStatusTag(item.status)} -
- -
- {new Date(item.date_incoming).toLocaleDateString()} - {item.items_count} поз. -
- -
- - {item.total_sum.toLocaleString('ru-RU', { style: 'currency', currency: 'RUB' })} - - -
-
-
- )} + {!isSynced && ( + + )} + + + + ); + }} /> )}
); -}; \ No newline at end of file +}; diff --git a/rmser-view/src/services/api.ts b/rmser-view/src/services/api.ts index f48453a..86e0525 100644 --- a/rmser-view/src/services/api.ts +++ b/rmser-view/src/services/api.ts @@ -22,7 +22,7 @@ import type { AddContainerRequest, AddContainerResponse, DictionariesResponse, - DraftSummary, + UnifiedInvoice, ServerUser, UserRole } from './types'; @@ -159,8 +159,11 @@ export const api = { return data.suppliers; }, - getDrafts: async (): Promise => { - const { data } = await apiClient.get('/drafts'); + // Обновленный метод получения списка накладных с фильтрацией + getDrafts: async (from?: string, to?: string): Promise => { + const { data } = await apiClient.get('/drafts', { + params: { from, to } + }); return data; }, diff --git a/rmser-view/src/services/types.ts b/rmser-view/src/services/types.ts index 6261b0f..fa622e3 100644 --- a/rmser-view/src/services/types.ts +++ b/rmser-view/src/services/types.ts @@ -233,4 +233,20 @@ 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; // Создано ли через наше приложение } \ No newline at end of file