- Go module with Gin, GORM, JWT, excelize dependencies - Configuration loading from YAML with all settings - GORM models for users, categories, components, configurations, alerts - Repository layer for all entities - Services: auth (JWT), pricing (median/average/weighted), components, quotes, configurations, export (CSV/XLSX), alerts - Middleware: JWT auth, role-based access, CORS - HTTP handlers for all API endpoints - Main server with dependency injection and graceful shutdown Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
176 lines
4.6 KiB
Go
176 lines
4.6 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/csv"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/mchus/quoteforge/internal/config"
|
|
"github.com/mchus/quoteforge/internal/models"
|
|
"github.com/xuri/excelize/v2"
|
|
)
|
|
|
|
type ExportService struct {
|
|
config config.ExportConfig
|
|
}
|
|
|
|
func NewExportService(cfg config.ExportConfig) *ExportService {
|
|
return &ExportService{config: cfg}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Items
|
|
for _, item := range data.Items {
|
|
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) ToXLSX(data *ExportData) ([]byte, error) {
|
|
f := excelize.NewFile()
|
|
sheet := "Конфигурация"
|
|
f.SetSheetName("Sheet1", sheet)
|
|
|
|
// Styles
|
|
headerStyle, _ := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Size: 12, Color: "#FFFFFF"},
|
|
Fill: excelize.Fill{Type: "pattern", Color: []string{"#4472C4"}, Pattern: 1},
|
|
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
|
|
Border: []excelize.Border{
|
|
{Type: "left", Color: "#000000", Style: 1},
|
|
{Type: "top", Color: "#000000", Style: 1},
|
|
{Type: "bottom", Color: "#000000", Style: 1},
|
|
{Type: "right", Color: "#000000", Style: 1},
|
|
},
|
|
})
|
|
|
|
totalStyle, _ := f.NewStyle(&excelize.Style{
|
|
Font: &excelize.Font{Bold: true, Size: 12},
|
|
Fill: excelize.Fill{Type: "pattern", Color: []string{"#E2EFDA"}, Pattern: 1},
|
|
})
|
|
|
|
priceStyle, _ := f.NewStyle(&excelize.Style{
|
|
NumFmt: 4, // #,##0.00
|
|
})
|
|
|
|
// Title
|
|
f.SetCellValue(sheet, "A1", s.config.CompanyName)
|
|
f.SetCellValue(sheet, "A2", "Коммерческое предложение: "+data.Name)
|
|
f.SetCellValue(sheet, "A3", "Дата: "+data.CreatedAt.Format("02.01.2006"))
|
|
|
|
// Headers
|
|
headers := []string{"Артикул", "Описание", "Категория", "Кол-во", "Цена", "Сумма"}
|
|
for i, h := range headers {
|
|
cell := fmt.Sprintf("%c5", 'A'+i)
|
|
f.SetCellValue(sheet, cell, h)
|
|
f.SetCellStyle(sheet, cell, cell, headerStyle)
|
|
}
|
|
|
|
// Data rows
|
|
row := 6
|
|
for _, item := range data.Items {
|
|
f.SetCellValue(sheet, fmt.Sprintf("A%d", row), item.LotName)
|
|
f.SetCellValue(sheet, fmt.Sprintf("B%d", row), item.Description)
|
|
f.SetCellValue(sheet, fmt.Sprintf("C%d", row), item.Category)
|
|
f.SetCellValue(sheet, fmt.Sprintf("D%d", row), item.Quantity)
|
|
f.SetCellValue(sheet, fmt.Sprintf("E%d", row), item.UnitPrice)
|
|
f.SetCellValue(sheet, fmt.Sprintf("F%d", row), item.TotalPrice)
|
|
f.SetCellStyle(sheet, fmt.Sprintf("E%d", row), fmt.Sprintf("F%d", row), priceStyle)
|
|
row++
|
|
}
|
|
|
|
// Total row
|
|
f.SetCellValue(sheet, fmt.Sprintf("E%d", row), "ИТОГО:")
|
|
f.SetCellValue(sheet, fmt.Sprintf("F%d", row), data.Total)
|
|
f.SetCellStyle(sheet, fmt.Sprintf("E%d", row), fmt.Sprintf("F%d", row), totalStyle)
|
|
|
|
// Notes
|
|
if data.Notes != "" {
|
|
row += 2
|
|
f.SetCellValue(sheet, fmt.Sprintf("A%d", row), "Примечания: "+data.Notes)
|
|
}
|
|
|
|
// Column widths
|
|
f.SetColWidth(sheet, "A", "A", 25)
|
|
f.SetColWidth(sheet, "B", "B", 50)
|
|
f.SetColWidth(sheet, "C", "C", 15)
|
|
f.SetColWidth(sheet, "D", "D", 10)
|
|
f.SetColWidth(sheet, "E", "E", 15)
|
|
f.SetColWidth(sheet, "F", "F", 15)
|
|
|
|
var buf bytes.Buffer
|
|
if err := f.Write(&buf); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func (s *ExportService) ConfigToExportData(config *models.Configuration) *ExportData {
|
|
items := make([]ExportItem, len(config.Items))
|
|
var total float64
|
|
|
|
for i, item := range config.Items {
|
|
itemTotal := item.UnitPrice * float64(item.Quantity)
|
|
items[i] = ExportItem{
|
|
LotName: item.LotName,
|
|
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,
|
|
}
|
|
}
|