629 lines
16 KiB
Go
629 lines
16 KiB
Go
package services
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
|
"git.mchus.pro/mchus/quoteforge/internal/models"
|
|
"git.mchus.pro/mchus/quoteforge/internal/services/sync"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// LocalConfigurationService handles configurations in local-first mode
|
|
// All operations go through SQLite, MariaDB is used only for sync
|
|
type LocalConfigurationService struct {
|
|
localDB *localdb.LocalDB
|
|
syncService *sync.Service
|
|
quoteService *QuoteService
|
|
isOnline func() bool // Function to check if we're online
|
|
}
|
|
|
|
// NewLocalConfigurationService creates a new local-first configuration service
|
|
func NewLocalConfigurationService(
|
|
localDB *localdb.LocalDB,
|
|
syncService *sync.Service,
|
|
quoteService *QuoteService,
|
|
isOnline func() bool,
|
|
) *LocalConfigurationService {
|
|
return &LocalConfigurationService{
|
|
localDB: localDB,
|
|
syncService: syncService,
|
|
quoteService: quoteService,
|
|
isOnline: isOnline,
|
|
}
|
|
}
|
|
|
|
// Create creates a new configuration in local SQLite and queues it for sync
|
|
func (s *LocalConfigurationService) Create(userID uint, req *CreateConfigRequest) (*models.Configuration, error) {
|
|
// If online, check for new pricelists first
|
|
if s.isOnline() {
|
|
if err := s.syncService.SyncPricelistsIfNeeded(); err != nil {
|
|
// Log but don't fail - we can still use local pricelists
|
|
}
|
|
}
|
|
|
|
total := req.Items.Total()
|
|
if req.ServerCount > 1 {
|
|
total *= float64(req.ServerCount)
|
|
}
|
|
|
|
cfg := &models.Configuration{
|
|
UUID: uuid.New().String(),
|
|
UserID: userID,
|
|
Name: req.Name,
|
|
Items: req.Items,
|
|
TotalPrice: &total,
|
|
CustomPrice: req.CustomPrice,
|
|
Notes: req.Notes,
|
|
IsTemplate: req.IsTemplate,
|
|
ServerCount: req.ServerCount,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
// Convert to local model
|
|
localCfg := localdb.ConfigurationToLocal(cfg)
|
|
|
|
// Save to local SQLite
|
|
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add to pending sync queue
|
|
payload, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.localDB.AddPendingChange("configuration", cfg.UUID, "create", string(payload)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Record usage stats
|
|
_ = s.quoteService.RecordUsage(req.Items)
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// GetByUUID returns a configuration from local SQLite
|
|
func (s *LocalConfigurationService) GetByUUID(uuid string, userID uint) (*models.Configuration, error) {
|
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
|
if err != nil {
|
|
return nil, ErrConfigNotFound
|
|
}
|
|
|
|
// Convert to models.Configuration
|
|
cfg := localdb.LocalToConfiguration(localCfg)
|
|
|
|
// Allow access if user owns config or it's a template
|
|
if cfg.UserID != userID && !cfg.IsTemplate {
|
|
return nil, ErrConfigForbidden
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// Update updates a configuration in local SQLite and queues it for sync
|
|
func (s *LocalConfigurationService) Update(uuid string, userID uint, req *CreateConfigRequest) (*models.Configuration, error) {
|
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
|
if err != nil {
|
|
return nil, ErrConfigNotFound
|
|
}
|
|
|
|
if localCfg.OriginalUserID != userID {
|
|
return nil, ErrConfigForbidden
|
|
}
|
|
|
|
total := req.Items.Total()
|
|
if req.ServerCount > 1 {
|
|
total *= float64(req.ServerCount)
|
|
}
|
|
|
|
// Update fields
|
|
localCfg.Name = req.Name
|
|
localCfg.Items = localdb.LocalConfigItems{}
|
|
for _, item := range req.Items {
|
|
localCfg.Items = append(localCfg.Items, localdb.LocalConfigItem{
|
|
LotName: item.LotName,
|
|
Quantity: item.Quantity,
|
|
UnitPrice: item.UnitPrice,
|
|
})
|
|
}
|
|
localCfg.TotalPrice = &total
|
|
localCfg.CustomPrice = req.CustomPrice
|
|
localCfg.Notes = req.Notes
|
|
localCfg.IsTemplate = req.IsTemplate
|
|
localCfg.ServerCount = req.ServerCount
|
|
localCfg.UpdatedAt = time.Now()
|
|
localCfg.SyncStatus = "pending"
|
|
|
|
// Save to local SQLite
|
|
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add to pending sync queue
|
|
cfg := localdb.LocalToConfiguration(localCfg)
|
|
payload, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// Delete deletes a configuration from local SQLite and queues it for sync
|
|
func (s *LocalConfigurationService) Delete(uuid string, userID uint) error {
|
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
|
if err != nil {
|
|
return ErrConfigNotFound
|
|
}
|
|
|
|
if localCfg.OriginalUserID != userID {
|
|
return ErrConfigForbidden
|
|
}
|
|
|
|
// Delete from local SQLite
|
|
if err := s.localDB.DeleteConfiguration(uuid); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add to pending sync queue
|
|
if err := s.localDB.AddPendingChange("configuration", uuid, "delete", ""); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Rename renames a configuration
|
|
func (s *LocalConfigurationService) Rename(uuid string, userID uint, newName string) (*models.Configuration, error) {
|
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
|
if err != nil {
|
|
return nil, ErrConfigNotFound
|
|
}
|
|
|
|
if localCfg.OriginalUserID != userID {
|
|
return nil, ErrConfigForbidden
|
|
}
|
|
|
|
localCfg.Name = newName
|
|
localCfg.UpdatedAt = time.Now()
|
|
localCfg.SyncStatus = "pending"
|
|
|
|
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add to pending sync queue
|
|
cfg := localdb.LocalToConfiguration(localCfg)
|
|
payload, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// Clone clones a configuration
|
|
func (s *LocalConfigurationService) Clone(configUUID string, userID uint, newName string) (*models.Configuration, error) {
|
|
original, err := s.GetByUUID(configUUID, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
total := original.Items.Total()
|
|
if original.ServerCount > 1 {
|
|
total *= float64(original.ServerCount)
|
|
}
|
|
|
|
clone := &models.Configuration{
|
|
UUID: uuid.New().String(),
|
|
UserID: userID,
|
|
Name: newName,
|
|
Items: original.Items,
|
|
TotalPrice: &total,
|
|
CustomPrice: original.CustomPrice,
|
|
Notes: original.Notes,
|
|
IsTemplate: false,
|
|
ServerCount: original.ServerCount,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
localCfg := localdb.ConfigurationToLocal(clone)
|
|
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add to pending sync queue
|
|
payload, err := json.Marshal(clone)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.localDB.AddPendingChange("configuration", clone.UUID, "create", string(payload)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return clone, nil
|
|
}
|
|
|
|
// ListByUser returns all configurations for a user from local SQLite
|
|
func (s *LocalConfigurationService) ListByUser(userID uint, page, perPage int) ([]models.Configuration, int64, error) {
|
|
// Get all local configurations
|
|
localConfigs, err := s.localDB.GetConfigurations()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Filter by user
|
|
var userConfigs []models.Configuration
|
|
for _, lc := range localConfigs {
|
|
if lc.OriginalUserID == userID || lc.IsTemplate {
|
|
userConfigs = append(userConfigs, *localdb.LocalToConfiguration(&lc))
|
|
}
|
|
}
|
|
|
|
total := int64(len(userConfigs))
|
|
|
|
// Apply pagination
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if perPage < 1 || perPage > 100 {
|
|
perPage = 20
|
|
}
|
|
offset := (page - 1) * perPage
|
|
|
|
start := offset
|
|
if start > len(userConfigs) {
|
|
start = len(userConfigs)
|
|
}
|
|
end := start + perPage
|
|
if end > len(userConfigs) {
|
|
end = len(userConfigs)
|
|
}
|
|
|
|
return userConfigs[start:end], total, nil
|
|
}
|
|
|
|
// RefreshPrices updates all component prices in the configuration from local cache
|
|
func (s *LocalConfigurationService) RefreshPrices(uuid string, userID uint) (*models.Configuration, error) {
|
|
// Get configuration from local SQLite
|
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
|
if err != nil {
|
|
return nil, ErrConfigNotFound
|
|
}
|
|
|
|
// Check ownership
|
|
if localCfg.OriginalUserID != userID {
|
|
return nil, ErrConfigForbidden
|
|
}
|
|
|
|
// Update prices for all items
|
|
updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
|
|
for i, item := range localCfg.Items {
|
|
// Get current component price from local cache
|
|
component, err := s.localDB.GetLocalComponent(item.LotName)
|
|
if err != nil || component.CurrentPrice == nil {
|
|
// Keep original item if component not found or no price available
|
|
updatedItems[i] = item
|
|
continue
|
|
}
|
|
|
|
// Update item with current price from local cache
|
|
updatedItems[i] = localdb.LocalConfigItem{
|
|
LotName: item.LotName,
|
|
Quantity: item.Quantity,
|
|
UnitPrice: *component.CurrentPrice,
|
|
}
|
|
}
|
|
|
|
// Update configuration
|
|
localCfg.Items = updatedItems
|
|
total := updatedItems.Total()
|
|
|
|
// If server count is greater than 1, multiply the total by server count
|
|
if localCfg.ServerCount > 1 {
|
|
total *= float64(localCfg.ServerCount)
|
|
}
|
|
|
|
localCfg.TotalPrice = &total
|
|
|
|
// Set price update timestamp and mark for sync
|
|
now := time.Now()
|
|
localCfg.PriceUpdatedAt = &now
|
|
localCfg.UpdatedAt = now
|
|
localCfg.SyncStatus = "pending"
|
|
|
|
// Save to local SQLite
|
|
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add to pending sync queue
|
|
cfg := localdb.LocalToConfiguration(localCfg)
|
|
payload, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// GetByUUIDNoAuth returns configuration without ownership check
|
|
func (s *LocalConfigurationService) GetByUUIDNoAuth(uuid string) (*models.Configuration, error) {
|
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
|
if err != nil {
|
|
return nil, ErrConfigNotFound
|
|
}
|
|
return localdb.LocalToConfiguration(localCfg), nil
|
|
}
|
|
|
|
// UpdateNoAuth updates configuration without ownership check
|
|
func (s *LocalConfigurationService) UpdateNoAuth(uuid string, req *CreateConfigRequest) (*models.Configuration, error) {
|
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
|
if err != nil {
|
|
return nil, ErrConfigNotFound
|
|
}
|
|
|
|
total := req.Items.Total()
|
|
if req.ServerCount > 1 {
|
|
total *= float64(req.ServerCount)
|
|
}
|
|
|
|
localCfg.Name = req.Name
|
|
localCfg.Items = localdb.LocalConfigItems{}
|
|
for _, item := range req.Items {
|
|
localCfg.Items = append(localCfg.Items, localdb.LocalConfigItem{
|
|
LotName: item.LotName,
|
|
Quantity: item.Quantity,
|
|
UnitPrice: item.UnitPrice,
|
|
})
|
|
}
|
|
localCfg.TotalPrice = &total
|
|
localCfg.CustomPrice = req.CustomPrice
|
|
localCfg.Notes = req.Notes
|
|
localCfg.IsTemplate = req.IsTemplate
|
|
localCfg.ServerCount = req.ServerCount
|
|
localCfg.UpdatedAt = time.Now()
|
|
localCfg.SyncStatus = "pending"
|
|
|
|
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg := localdb.LocalToConfiguration(localCfg)
|
|
payload, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// DeleteNoAuth deletes configuration without ownership check
|
|
func (s *LocalConfigurationService) DeleteNoAuth(uuid string) error {
|
|
if err := s.localDB.DeleteConfiguration(uuid); err != nil {
|
|
return err
|
|
}
|
|
return s.localDB.AddPendingChange("configuration", uuid, "delete", "")
|
|
}
|
|
|
|
// RenameNoAuth renames configuration without ownership check
|
|
func (s *LocalConfigurationService) RenameNoAuth(uuid string, newName string) (*models.Configuration, error) {
|
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
|
if err != nil {
|
|
return nil, ErrConfigNotFound
|
|
}
|
|
|
|
localCfg.Name = newName
|
|
localCfg.UpdatedAt = time.Now()
|
|
localCfg.SyncStatus = "pending"
|
|
|
|
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg := localdb.LocalToConfiguration(localCfg)
|
|
payload, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// CloneNoAuth clones configuration without ownership check
|
|
func (s *LocalConfigurationService) CloneNoAuth(configUUID string, newName string, userID uint) (*models.Configuration, error) {
|
|
original, err := s.GetByUUIDNoAuth(configUUID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
total := original.Items.Total()
|
|
if original.ServerCount > 1 {
|
|
total *= float64(original.ServerCount)
|
|
}
|
|
|
|
clone := &models.Configuration{
|
|
UUID: uuid.New().String(),
|
|
UserID: userID,
|
|
Name: newName,
|
|
Items: original.Items,
|
|
TotalPrice: &total,
|
|
CustomPrice: original.CustomPrice,
|
|
Notes: original.Notes,
|
|
IsTemplate: false,
|
|
ServerCount: original.ServerCount,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
localCfg := localdb.ConfigurationToLocal(clone)
|
|
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payload, err := json.Marshal(clone)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.localDB.AddPendingChange("configuration", clone.UUID, "create", string(payload)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return clone, nil
|
|
}
|
|
|
|
// ListAll returns all configurations without user filter
|
|
func (s *LocalConfigurationService) ListAll(page, perPage int) ([]models.Configuration, int64, error) {
|
|
localConfigs, err := s.localDB.GetConfigurations()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
configs := make([]models.Configuration, len(localConfigs))
|
|
for i, lc := range localConfigs {
|
|
configs[i] = *localdb.LocalToConfiguration(&lc)
|
|
}
|
|
|
|
total := int64(len(configs))
|
|
|
|
// Apply pagination
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if perPage < 1 || perPage > 100 {
|
|
perPage = 20
|
|
}
|
|
offset := (page - 1) * perPage
|
|
|
|
start := offset
|
|
if start > len(configs) {
|
|
start = len(configs)
|
|
}
|
|
end := start + perPage
|
|
if end > len(configs) {
|
|
end = len(configs)
|
|
}
|
|
|
|
return configs[start:end], total, nil
|
|
}
|
|
|
|
// ListTemplates returns all template configurations
|
|
func (s *LocalConfigurationService) ListTemplates(page, perPage int) ([]models.Configuration, int64, error) {
|
|
localConfigs, err := s.localDB.GetConfigurations()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
var templates []models.Configuration
|
|
for _, lc := range localConfigs {
|
|
if lc.IsTemplate {
|
|
templates = append(templates, *localdb.LocalToConfiguration(&lc))
|
|
}
|
|
}
|
|
|
|
total := int64(len(templates))
|
|
|
|
// Apply pagination
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if perPage < 1 || perPage > 100 {
|
|
perPage = 20
|
|
}
|
|
offset := (page - 1) * perPage
|
|
|
|
start := offset
|
|
if start > len(templates) {
|
|
start = len(templates)
|
|
}
|
|
end := start + perPage
|
|
if end > len(templates) {
|
|
end = len(templates)
|
|
}
|
|
|
|
return templates[start:end], total, nil
|
|
}
|
|
|
|
// RefreshPricesNoAuth updates all component prices in the configuration without ownership check
|
|
func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Configuration, error) {
|
|
// Get configuration from local SQLite
|
|
localCfg, err := s.localDB.GetConfigurationByUUID(uuid)
|
|
if err != nil {
|
|
return nil, ErrConfigNotFound
|
|
}
|
|
|
|
// Update prices for all items
|
|
updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
|
|
for i, item := range localCfg.Items {
|
|
// Get current component price from local cache
|
|
component, err := s.localDB.GetLocalComponent(item.LotName)
|
|
if err != nil || component.CurrentPrice == nil {
|
|
// Keep original item if component not found or no price available
|
|
updatedItems[i] = item
|
|
continue
|
|
}
|
|
|
|
// Update item with current price from local cache
|
|
updatedItems[i] = localdb.LocalConfigItem{
|
|
LotName: item.LotName,
|
|
Quantity: item.Quantity,
|
|
UnitPrice: *component.CurrentPrice,
|
|
}
|
|
}
|
|
|
|
// Update configuration
|
|
localCfg.Items = updatedItems
|
|
total := updatedItems.Total()
|
|
|
|
// If server count is greater than 1, multiply the total by server count
|
|
if localCfg.ServerCount > 1 {
|
|
total *= float64(localCfg.ServerCount)
|
|
}
|
|
|
|
localCfg.TotalPrice = &total
|
|
|
|
// Set price update timestamp and mark for sync
|
|
now := time.Now()
|
|
localCfg.PriceUpdatedAt = &now
|
|
localCfg.UpdatedAt = now
|
|
localCfg.SyncStatus = "pending"
|
|
|
|
// Save to local SQLite
|
|
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add to pending sync queue
|
|
cfg := localdb.LocalToConfiguration(localCfg)
|
|
payload, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.localDB.AddPendingChange("configuration", uuid, "update", string(payload)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// ImportFromServer imports configurations from MariaDB to local SQLite cache.
|
|
func (s *LocalConfigurationService) ImportFromServer() (*sync.ConfigImportResult, error) {
|
|
return s.syncService.ImportConfigurationsToLocal()
|
|
}
|