Основные изменения: 1. CSV экспорт и веб-интерфейс: - Компоненты теперь сортируются по иерархии категорий (display_order) - Категории отображаются в правильном порядке: BB, CPU, MEM, GPU и т.д. - Компоненты без категории отображаются в конце 2. Раздел PCI в конфигураторе: - Разделен на секции: GPU/DPU, NIC/HCA, HBA - Улучшена навигация и выбор компонентов 3. Сохранение "своей цены": - Добавлено поле custom_price в модель Configuration - Создана миграция 002_add_custom_price.sql - "Своя цена" сохраняется при сохранении конфигурации - При загрузке конфигурации восстанавливается сохраненная цена 4. Автосохранение: - Конфигурация автоматически сохраняется через 1 секунду после изменений - Debounce предотвращает избыточные запросы - Автосохранение работает для всех изменений (компоненты, количество, цена) 5. Дополнительно: - Добавлен cmd/importer для импорта метаданных из таблицы lot - Создан скрипт apply_migration.sh для применения миграций - Оптимизирована работа с категориями в ExportService Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
148 lines
3.8 KiB
Go
148 lines
3.8 KiB
Go
package services
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/csv"
|
||
"fmt"
|
||
"time"
|
||
|
||
"git.mchus.pro/mchus/quoteforge/internal/config"
|
||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||
"git.mchus.pro/mchus/quoteforge/internal/repository"
|
||
)
|
||
|
||
type ExportService struct {
|
||
config config.ExportConfig
|
||
categoryRepo *repository.CategoryRepository
|
||
}
|
||
|
||
func NewExportService(cfg config.ExportConfig, categoryRepo *repository.CategoryRepository) *ExportService {
|
||
return &ExportService{
|
||
config: cfg,
|
||
categoryRepo: categoryRepo,
|
||
}
|
||
}
|
||
|
||
type ExportData struct {
|
||
Name string
|
||
Items []ExportItem
|
||
Total float64
|
||
Notes string
|
||
CreatedAt time.Time
|
||
}
|
||
|
||
type ExportItem struct {
|
||
LotName string
|
||
Description string
|
||
Category string
|
||
Quantity int
|
||
UnitPrice float64
|
||
TotalPrice float64
|
||
}
|
||
|
||
func (s *ExportService) ToCSV(data *ExportData) ([]byte, error) {
|
||
var buf bytes.Buffer
|
||
w := csv.NewWriter(&buf)
|
||
|
||
// Header
|
||
headers := []string{"Артикул", "Описание", "Категория", "Количество", "Цена за единицу", "Сумма"}
|
||
if err := w.Write(headers); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Get category hierarchy for sorting
|
||
categoryOrder := make(map[string]int)
|
||
if s.categoryRepo != nil {
|
||
categories, err := s.categoryRepo.GetAll()
|
||
if err == nil {
|
||
for _, cat := range categories {
|
||
categoryOrder[cat.Code] = cat.DisplayOrder
|
||
}
|
||
}
|
||
}
|
||
|
||
// Sort items by category display order
|
||
sortedItems := make([]ExportItem, len(data.Items))
|
||
copy(sortedItems, data.Items)
|
||
|
||
// Sort using category display order (items without category go to the end)
|
||
for i := 0; i < len(sortedItems)-1; i++ {
|
||
for j := i + 1; j < len(sortedItems); j++ {
|
||
orderI, hasI := categoryOrder[sortedItems[i].Category]
|
||
orderJ, hasJ := categoryOrder[sortedItems[j].Category]
|
||
|
||
// Items without category go to the end
|
||
if !hasI && hasJ {
|
||
sortedItems[i], sortedItems[j] = sortedItems[j], sortedItems[i]
|
||
} else if hasI && hasJ {
|
||
// Both have categories, sort by display order
|
||
if orderI > orderJ {
|
||
sortedItems[i], sortedItems[j] = sortedItems[j], sortedItems[i]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Items
|
||
for _, item := range sortedItems {
|
||
row := []string{
|
||
item.LotName,
|
||
item.Description,
|
||
item.Category,
|
||
fmt.Sprintf("%d", item.Quantity),
|
||
fmt.Sprintf("%.2f", item.UnitPrice),
|
||
fmt.Sprintf("%.2f", item.TotalPrice),
|
||
}
|
||
if err := w.Write(row); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
// Total row
|
||
if err := w.Write([]string{"", "", "", "", "ИТОГО:", fmt.Sprintf("%.2f", data.Total)}); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
w.Flush()
|
||
return buf.Bytes(), w.Error()
|
||
}
|
||
|
||
func (s *ExportService) ConfigToExportData(config *models.Configuration, componentService *ComponentService) *ExportData {
|
||
items := make([]ExportItem, len(config.Items))
|
||
var total float64
|
||
|
||
for i, item := range config.Items {
|
||
itemTotal := item.UnitPrice * float64(item.Quantity)
|
||
|
||
// Получаем информацию о компоненте для заполнения категории
|
||
componentView, err := componentService.GetByLotName(item.LotName)
|
||
if err != nil {
|
||
// Если не удалось получить информацию о компоненте, используем только основные данные
|
||
items[i] = ExportItem{
|
||
LotName: item.LotName,
|
||
Quantity: item.Quantity,
|
||
UnitPrice: item.UnitPrice,
|
||
TotalPrice: itemTotal,
|
||
}
|
||
} else {
|
||
items[i] = ExportItem{
|
||
LotName: item.LotName,
|
||
Description: componentView.Description,
|
||
Category: componentView.Category,
|
||
Quantity: item.Quantity,
|
||
UnitPrice: item.UnitPrice,
|
||
TotalPrice: itemTotal,
|
||
}
|
||
}
|
||
total += itemTotal
|
||
}
|
||
|
||
return &ExportData{
|
||
Name: config.Name,
|
||
Items: items,
|
||
Total: total,
|
||
Notes: config.Notes,
|
||
CreatedAt: config.CreatedAt,
|
||
}
|
||
}
|