Implement persistent Line ordering for project specs and update bible
This commit is contained in:
@@ -107,6 +107,7 @@ func (s *LocalConfigurationService) Create(ownerUsername string, req *CreateConf
|
||||
if err := s.createWithVersion(localCfg, ownerUsername); err != nil {
|
||||
return nil, fmt.Errorf("create configuration with version: %w", err)
|
||||
}
|
||||
cfg.Line = localCfg.Line
|
||||
|
||||
// Record usage stats
|
||||
_ = s.quoteService.RecordUsage(req.Items)
|
||||
@@ -325,6 +326,7 @@ func (s *LocalConfigurationService) CloneToProject(configUUID string, ownerUsern
|
||||
if err := s.createWithVersion(localCfg, ownerUsername); err != nil {
|
||||
return nil, fmt.Errorf("clone configuration with version: %w", err)
|
||||
}
|
||||
clone.Line = localCfg.Line
|
||||
|
||||
return clone, nil
|
||||
}
|
||||
@@ -640,6 +642,7 @@ func (s *LocalConfigurationService) CloneNoAuthToProjectFromVersion(configUUID s
|
||||
if err := s.createWithVersion(localCfg, ownerUsername); err != nil {
|
||||
return nil, fmt.Errorf("clone configuration without auth with version: %w", err)
|
||||
}
|
||||
clone.Line = localCfg.Line
|
||||
|
||||
return clone, nil
|
||||
}
|
||||
@@ -826,21 +829,13 @@ func (s *LocalConfigurationService) UpdateServerCount(configUUID string, serverC
|
||||
return fmt.Errorf("save local configuration: %w", err)
|
||||
}
|
||||
|
||||
// Use existing current version for the pending change
|
||||
var version localdb.LocalConfigurationVersion
|
||||
if localCfg.CurrentVersionID != nil && *localCfg.CurrentVersionID != "" {
|
||||
if err := tx.Where("id = ?", *localCfg.CurrentVersionID).First(&version).Error; err != nil {
|
||||
return fmt.Errorf("load current version: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := tx.Where("configuration_uuid = ?", localCfg.UUID).
|
||||
Order("version_no DESC").First(&version).Error; err != nil {
|
||||
return fmt.Errorf("load latest version: %w", err)
|
||||
}
|
||||
version, err := s.loadVersionForPendingTx(tx, localCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg = localdb.LocalToConfiguration(localCfg)
|
||||
if err := s.enqueueConfigurationPendingChangeTx(tx, localCfg, "update", &version, ""); err != nil {
|
||||
if err := s.enqueueConfigurationPendingChangeTx(tx, localCfg, "update", version, ""); err != nil {
|
||||
return fmt.Errorf("enqueue server-count pending change: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -852,6 +847,99 @@ func (s *LocalConfigurationService) UpdateServerCount(configUUID string, serverC
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (s *LocalConfigurationService) ReorderProjectConfigurationsNoAuth(projectUUID string, orderedUUIDs []string) ([]models.Configuration, error) {
|
||||
projectUUID = strings.TrimSpace(projectUUID)
|
||||
if projectUUID == "" {
|
||||
return nil, ErrProjectNotFound
|
||||
}
|
||||
if _, err := s.localDB.GetProjectByUUID(projectUUID); err != nil {
|
||||
return nil, ErrProjectNotFound
|
||||
}
|
||||
if len(orderedUUIDs) == 0 {
|
||||
return []models.Configuration{}, nil
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(orderedUUIDs))
|
||||
normalized := make([]string, 0, len(orderedUUIDs))
|
||||
for _, raw := range orderedUUIDs {
|
||||
u := strings.TrimSpace(raw)
|
||||
if u == "" {
|
||||
return nil, fmt.Errorf("ordered_uuids contains empty uuid")
|
||||
}
|
||||
if _, exists := seen[u]; exists {
|
||||
return nil, fmt.Errorf("ordered_uuids contains duplicate uuid: %s", u)
|
||||
}
|
||||
seen[u] = struct{}{}
|
||||
normalized = append(normalized, u)
|
||||
}
|
||||
|
||||
err := s.localDB.DB().Transaction(func(tx *gorm.DB) error {
|
||||
var active []localdb.LocalConfiguration
|
||||
if err := tx.Where("project_uuid = ? AND is_active = ?", projectUUID, true).
|
||||
Find(&active).Error; err != nil {
|
||||
return fmt.Errorf("load project active configurations: %w", err)
|
||||
}
|
||||
if len(active) != len(normalized) {
|
||||
return fmt.Errorf("ordered_uuids count mismatch: expected %d got %d", len(active), len(normalized))
|
||||
}
|
||||
|
||||
byUUID := make(map[string]*localdb.LocalConfiguration, len(active))
|
||||
for i := range active {
|
||||
cfg := active[i]
|
||||
byUUID[cfg.UUID] = &cfg
|
||||
}
|
||||
for _, id := range normalized {
|
||||
if _, ok := byUUID[id]; !ok {
|
||||
return fmt.Errorf("configuration %s not found in project %s", id, projectUUID)
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for idx, id := range normalized {
|
||||
cfg := byUUID[id]
|
||||
newLine := (idx + 1) * 10
|
||||
if cfg.Line == newLine {
|
||||
continue
|
||||
}
|
||||
|
||||
cfg.Line = newLine
|
||||
cfg.UpdatedAt = now
|
||||
cfg.SyncStatus = "pending"
|
||||
|
||||
if err := tx.Save(cfg).Error; err != nil {
|
||||
return fmt.Errorf("save reordered configuration %s: %w", cfg.UUID, err)
|
||||
}
|
||||
|
||||
version, err := s.loadVersionForPendingTx(tx, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.enqueueConfigurationPendingChangeTx(tx, cfg, "update", version, ""); err != nil {
|
||||
return fmt.Errorf("enqueue reorder pending change for %s: %w", cfg.UUID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var localConfigs []localdb.LocalConfiguration
|
||||
if err := s.localDB.DB().
|
||||
Preload("CurrentVersion").
|
||||
Where("project_uuid = ? AND is_active = ?", projectUUID, true).
|
||||
Order("CASE WHEN COALESCE(line_no, 0) <= 0 THEN 2147483647 ELSE line_no END ASC, created_at DESC, id DESC").
|
||||
Find(&localConfigs).Error; err != nil {
|
||||
return nil, fmt.Errorf("load reordered configurations: %w", err)
|
||||
}
|
||||
|
||||
result := make([]models.Configuration, 0, len(localConfigs))
|
||||
for i := range localConfigs {
|
||||
result = append(result, *localdb.LocalToConfiguration(&localConfigs[i]))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ImportFromServer imports configurations from MariaDB to local SQLite cache.
|
||||
func (s *LocalConfigurationService) ImportFromServer() (*sync.ConfigImportResult, error) {
|
||||
return s.syncService.ImportConfigurationsToLocal()
|
||||
@@ -965,6 +1053,11 @@ func (s *LocalConfigurationService) isOwner(cfg *localdb.LocalConfiguration, own
|
||||
|
||||
func (s *LocalConfigurationService) createWithVersion(localCfg *localdb.LocalConfiguration, createdBy string) error {
|
||||
return s.localDB.DB().Transaction(func(tx *gorm.DB) error {
|
||||
if localCfg.IsActive && localCfg.Line <= 0 {
|
||||
if err := s.ensureConfigurationLineTx(tx, localCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Create(localCfg).Error; err != nil {
|
||||
return fmt.Errorf("create local configuration: %w", err)
|
||||
}
|
||||
@@ -1021,12 +1114,31 @@ func (s *LocalConfigurationService) saveWithVersionAndPending(localCfg *localdb.
|
||||
return fmt.Errorf("compare revision content: %w", err)
|
||||
}
|
||||
if sameRevisionContent {
|
||||
cfg = localdb.LocalToConfiguration(&locked)
|
||||
if !hasNonRevisionConfigurationChanges(&locked, localCfg) {
|
||||
cfg = localdb.LocalToConfiguration(&locked)
|
||||
return nil
|
||||
}
|
||||
if err := tx.Save(localCfg).Error; err != nil {
|
||||
return fmt.Errorf("save local configuration (no new revision): %w", err)
|
||||
}
|
||||
cfg = localdb.LocalToConfiguration(localCfg)
|
||||
if err := s.enqueueConfigurationPendingChangeTx(tx, localCfg, operation, currentVersion, createdBy); err != nil {
|
||||
return fmt.Errorf("enqueue %s pending change without revision: %w", operation, err)
|
||||
}
|
||||
if err := s.recalculateLocalPricelistUsageTx(tx); err != nil {
|
||||
return fmt.Errorf("recalculate local pricelist usage: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if localCfg.IsActive && localCfg.Line <= 0 {
|
||||
if err := s.ensureConfigurationLineTx(tx, localCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Save(localCfg).Error; err != nil {
|
||||
return fmt.Errorf("save local configuration: %w", err)
|
||||
}
|
||||
@@ -1061,6 +1173,50 @@ func (s *LocalConfigurationService) saveWithVersionAndPending(localCfg *localdb.
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func hasNonRevisionConfigurationChanges(current *localdb.LocalConfiguration, next *localdb.LocalConfiguration) bool {
|
||||
if current == nil || next == nil {
|
||||
return true
|
||||
}
|
||||
if current.Name != next.Name ||
|
||||
current.Notes != next.Notes ||
|
||||
current.IsTemplate != next.IsTemplate ||
|
||||
current.ServerModel != next.ServerModel ||
|
||||
current.SupportCode != next.SupportCode ||
|
||||
current.Article != next.Article ||
|
||||
current.OnlyInStock != next.OnlyInStock ||
|
||||
current.IsActive != next.IsActive ||
|
||||
current.Line != next.Line {
|
||||
return true
|
||||
}
|
||||
if !equalUintPtr(current.PricelistID, next.PricelistID) ||
|
||||
!equalUintPtr(current.WarehousePricelistID, next.WarehousePricelistID) ||
|
||||
!equalUintPtr(current.CompetitorPricelistID, next.CompetitorPricelistID) ||
|
||||
!equalStringPtr(current.ProjectUUID, next.ProjectUUID) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func equalStringPtr(a, b *string) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(*a) == strings.TrimSpace(*b)
|
||||
}
|
||||
|
||||
func equalUintPtr(a, b *uint) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
return *a == *b
|
||||
}
|
||||
|
||||
func (s *LocalConfigurationService) loadCurrentVersionTx(tx *gorm.DB, localCfg *localdb.LocalConfiguration) (*localdb.LocalConfigurationVersion, error) {
|
||||
var version localdb.LocalConfigurationVersion
|
||||
if localCfg.CurrentVersionID != nil && *localCfg.CurrentVersionID != "" {
|
||||
@@ -1082,6 +1238,32 @@ func (s *LocalConfigurationService) loadCurrentVersionTx(tx *gorm.DB, localCfg *
|
||||
return &version, nil
|
||||
}
|
||||
|
||||
func (s *LocalConfigurationService) loadVersionForPendingTx(tx *gorm.DB, localCfg *localdb.LocalConfiguration) (*localdb.LocalConfigurationVersion, error) {
|
||||
if localCfg.CurrentVersionID != nil && *localCfg.CurrentVersionID != "" {
|
||||
var current localdb.LocalConfigurationVersion
|
||||
if err := tx.Where("id = ?", *localCfg.CurrentVersionID).First(¤t).Error; err == nil {
|
||||
return ¤t, nil
|
||||
}
|
||||
}
|
||||
|
||||
var latest localdb.LocalConfigurationVersion
|
||||
if err := tx.Where("configuration_uuid = ?", localCfg.UUID).
|
||||
Order("version_no DESC").
|
||||
First(&latest).Error; err != nil {
|
||||
return nil, fmt.Errorf("load version for pending change: %w", err)
|
||||
}
|
||||
return &latest, nil
|
||||
}
|
||||
|
||||
func (s *LocalConfigurationService) ensureConfigurationLineTx(tx *gorm.DB, localCfg *localdb.LocalConfiguration) error {
|
||||
line, err := localdb.NextConfigurationLineTx(tx, localCfg.ProjectUUID, localCfg.UUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("assign line_no for configuration %s: %w", localCfg.UUID, err)
|
||||
}
|
||||
localCfg.Line = line
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalConfigurationService) hasSameRevisionContent(localCfg *localdb.LocalConfiguration, currentVersion *localdb.LocalConfigurationVersion) (bool, error) {
|
||||
currentSnapshotCfg, err := s.decodeConfigurationSnapshot(currentVersion.Data)
|
||||
if err != nil {
|
||||
@@ -1195,6 +1377,9 @@ func (s *LocalConfigurationService) rollbackToVersion(configurationUUID string,
|
||||
current.ServerCount = rollbackData.ServerCount
|
||||
current.PricelistID = rollbackData.PricelistID
|
||||
current.OnlyInStock = rollbackData.OnlyInStock
|
||||
if rollbackData.Line > 0 {
|
||||
current.Line = rollbackData.Line
|
||||
}
|
||||
current.PriceUpdatedAt = rollbackData.PriceUpdatedAt
|
||||
current.UpdatedAt = time.Now()
|
||||
current.SyncStatus = "pending"
|
||||
|
||||
Reference in New Issue
Block a user