Go refactoring: - Split handlers/pricing.go (2446→291 lines) into 5 focused files - Split services/stock_import.go (1334→~400 lines) into stock_mappings.go + stock_parse.go - Split services/sync/service.go (1290→~250 lines) into 3 files JS extraction: - Move all inline <script> blocks to web/static/js/ (6 files) - Templates reduced: admin_pricing 2873→521, lot 1531→304, vendor_mappings 1063→169, etc. Competitor pricelists (migrations 033-039): - qt_competitors + partnumber_log_competitors tables - Excel import with column mapping, dedup, bulk insert - p/n→lot resolution via weighted_median, discount applied - Unmapped p/ns written to qt_vendor_partnumber_seen - Quote counts (unique/total) shown on /admin/competitors - price_method="weighted_median", price_period_days=0 stored explicitly Fix price_method/price_period_days for warehouse items: - warehouse: weighted_avg, period=0 - competitor: weighted_median, period=0 - Removes misleading DB defaults (was: median/90) Update bible: architecture.md, pricelist.md, history.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
147 lines
3.9 KiB
Go
147 lines
3.9 KiB
Go
package sync
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/priceforge/internal/localdb"
|
|
"git.mchus.pro/mchus/priceforge/internal/repository"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ImportConfigurationsToLocal imports configurations from MariaDB into local SQLite.
|
|
// Existing local configs with pending local changes are skipped to avoid data loss.
|
|
func (s *Service) ImportConfigurationsToLocal() (*ConfigImportResult, error) {
|
|
mariaDB, err := s.getDB()
|
|
if err != nil {
|
|
return nil, ErrOffline
|
|
}
|
|
|
|
configRepo := repository.NewConfigurationRepository(mariaDB)
|
|
result := &ConfigImportResult{}
|
|
|
|
offset := 0
|
|
const limit = 200
|
|
for {
|
|
serverConfigs, _, err := configRepo.ListAll(offset, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing server configurations: %w", err)
|
|
}
|
|
if len(serverConfigs) == 0 {
|
|
break
|
|
}
|
|
|
|
for i := range serverConfigs {
|
|
cfg := serverConfigs[i]
|
|
existing, err := s.localDB.GetConfigurationByUUID(cfg.UUID)
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, fmt.Errorf("getting local configuration %s: %w", cfg.UUID, err)
|
|
}
|
|
|
|
if existing != nil && err == nil && existing.SyncStatus == "pending" {
|
|
result.Skipped++
|
|
continue
|
|
}
|
|
if existing != nil && err == nil && !existing.IsActive {
|
|
// Keep local deactivation sticky: do not resurrect hidden entries from server pull.
|
|
result.Skipped++
|
|
continue
|
|
}
|
|
|
|
localCfg := localdb.ConfigurationToLocal(&cfg)
|
|
now := time.Now()
|
|
localCfg.SyncedAt = &now
|
|
localCfg.SyncStatus = "synced"
|
|
localCfg.UpdatedAt = now
|
|
|
|
if existing != nil && err == nil {
|
|
localCfg.ID = existing.ID
|
|
result.Updated++
|
|
} else {
|
|
result.Imported++
|
|
}
|
|
|
|
if err := s.localDB.SaveConfiguration(localCfg); err != nil {
|
|
return nil, fmt.Errorf("saving local configuration %s: %w", cfg.UUID, err)
|
|
}
|
|
}
|
|
|
|
offset += len(serverConfigs)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ImportProjectsToLocal imports projects from MariaDB into local SQLite.
|
|
// Existing local projects with pending local changes are skipped to avoid data loss.
|
|
func (s *Service) ImportProjectsToLocal() (*ProjectImportResult, error) {
|
|
mariaDB, err := s.getDB()
|
|
if err != nil {
|
|
return nil, ErrOffline
|
|
}
|
|
|
|
projectRepo := repository.NewProjectRepository(mariaDB)
|
|
result := &ProjectImportResult{}
|
|
|
|
offset := 0
|
|
const limit = 200
|
|
for {
|
|
serverProjects, _, err := projectRepo.List(offset, limit, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing server projects: %w", err)
|
|
}
|
|
if len(serverProjects) == 0 {
|
|
break
|
|
}
|
|
|
|
now := time.Now()
|
|
for i := range serverProjects {
|
|
project := serverProjects[i]
|
|
|
|
existing, getErr := s.localDB.GetProjectByUUID(project.UUID)
|
|
if getErr != nil && !errors.Is(getErr, gorm.ErrRecordNotFound) {
|
|
return nil, fmt.Errorf("getting local project %s: %w", project.UUID, getErr)
|
|
}
|
|
|
|
if existing != nil && getErr == nil {
|
|
// Keep unsynced local changes intact.
|
|
if existing.SyncStatus == "pending" {
|
|
result.Skipped++
|
|
continue
|
|
}
|
|
|
|
existing.OwnerUsername = project.OwnerUsername
|
|
existing.Name = project.Name
|
|
existing.TrackerURL = project.TrackerURL
|
|
existing.IsActive = project.IsActive
|
|
existing.IsSystem = project.IsSystem
|
|
existing.CreatedAt = project.CreatedAt
|
|
existing.UpdatedAt = project.UpdatedAt
|
|
serverID := project.ID
|
|
existing.ServerID = &serverID
|
|
existing.SyncStatus = "synced"
|
|
existing.SyncedAt = &now
|
|
|
|
if err := s.localDB.SaveProject(existing); err != nil {
|
|
return nil, fmt.Errorf("saving local project %s: %w", project.UUID, err)
|
|
}
|
|
result.Updated++
|
|
continue
|
|
}
|
|
|
|
localProject := localdb.ProjectToLocal(&project)
|
|
localProject.SyncStatus = "synced"
|
|
localProject.SyncedAt = &now
|
|
if err := s.localDB.SaveProject(localProject); err != nil {
|
|
return nil, fmt.Errorf("saving local project %s: %w", project.UUID, err)
|
|
}
|
|
result.Imported++
|
|
}
|
|
|
|
offset += len(serverProjects)
|
|
}
|
|
|
|
return result, nil
|
|
}
|