package repository import ( "encoding/json" "fmt" "time" "git.mchus.pro/mchus/quoteforge/internal/localdb" "git.mchus.pro/mchus/quoteforge/internal/models" "gorm.io/gorm" ) // DataSource defines the unified interface for data access // It abstracts whether data comes from MariaDB (online) or SQLite (offline) type DataSource interface { // Components GetComponents(filter ComponentFilter, offset, limit int) ([]models.LotMetadata, int64, error) GetComponent(lotName string) (*models.LotMetadata, error) // Configurations SaveConfiguration(cfg *models.Configuration) error GetConfigurations(ownerUsername string) ([]models.Configuration, error) GetConfigurationByUUID(uuid string) (*models.Configuration, error) DeleteConfiguration(uuid string) error // Pricelists (read-only in offline mode) GetPricelists() ([]models.PricelistSummary, error) GetPricelistByID(id uint) (*models.Pricelist, error) GetPricelistItems(pricelistID uint) ([]models.PricelistItem, error) GetLatestPricelist() (*models.Pricelist, error) } // UnifiedRepo implements DataSource with automatic online/offline switching type UnifiedRepo struct { mariaDB *gorm.DB localDB *localdb.LocalDB isOnline bool } // NewUnifiedRepo creates a new unified repository func NewUnifiedRepo(mariaDB *gorm.DB, localDB *localdb.LocalDB, isOnline bool) *UnifiedRepo { return &UnifiedRepo{ mariaDB: mariaDB, localDB: localDB, isOnline: isOnline, } } // SetOnlineStatus updates the online/offline status func (r *UnifiedRepo) SetOnlineStatus(online bool) { r.isOnline = online } // IsOnline returns the current online/offline status func (r *UnifiedRepo) IsOnline() bool { return r.isOnline } // Component methods // GetComponents returns components from MariaDB (online) or local cache (offline) func (r *UnifiedRepo) GetComponents(filter ComponentFilter, offset, limit int) ([]models.LotMetadata, int64, error) { if r.isOnline { return r.getComponentsOnline(filter, offset, limit) } return r.getComponentsOffline(filter, offset, limit) } func (r *UnifiedRepo) getComponentsOnline(filter ComponentFilter, offset, limit int) ([]models.LotMetadata, int64, error) { repo := NewComponentRepository(r.mariaDB) return repo.List(filter, offset, limit) } func (r *UnifiedRepo) getComponentsOffline(filter ComponentFilter, offset, limit int) ([]models.LotMetadata, int64, error) { var components []localdb.LocalComponent query := r.localDB.DB().Model(&localdb.LocalComponent{}) // Apply filters if filter.Category != "" { query = query.Where("category = ?", filter.Category) } if filter.Search != "" { search := "%" + filter.Search + "%" query = query.Where("lot_name LIKE ? OR lot_description LIKE ? OR model LIKE ?", search, search, search) } var total int64 query.Count(&total) // Apply sorting sortDir := "ASC" if filter.SortDir == "desc" { sortDir = "DESC" } switch filter.SortField { case "lot_name": query = query.Order("lot_name " + sortDir) default: query = query.Order("lot_name ASC") } if err := query.Offset(offset).Limit(limit).Find(&components).Error; err != nil { return nil, 0, fmt.Errorf("fetching offline components: %w", err) } // Convert to models.LotMetadata result := make([]models.LotMetadata, len(components)) for i, comp := range components { result[i] = models.LotMetadata{ LotName: comp.LotName, Model: comp.Model, Lot: &models.Lot{ LotName: comp.LotName, LotDescription: comp.LotDescription, }, } } return result, total, nil } // GetComponent returns a single component by lot name func (r *UnifiedRepo) GetComponent(lotName string) (*models.LotMetadata, error) { if r.isOnline { repo := NewComponentRepository(r.mariaDB) return repo.GetByLotName(lotName) } var comp localdb.LocalComponent if err := r.localDB.DB().Where("lot_name = ?", lotName).First(&comp).Error; err != nil { return nil, fmt.Errorf("fetching offline component: %w", err) } return &models.LotMetadata{ LotName: comp.LotName, Model: comp.Model, Lot: &models.Lot{ LotName: comp.LotName, LotDescription: comp.LotDescription, }, }, nil } // Configuration methods // SaveConfiguration saves a configuration (online: MariaDB, offline: SQLite + pending_changes) func (r *UnifiedRepo) SaveConfiguration(cfg *models.Configuration) error { if r.isOnline { repo := NewConfigurationRepository(r.mariaDB) return repo.Create(cfg) } // Offline: save to local SQLite and queue for sync localCfg := &localdb.LocalConfiguration{ UUID: cfg.UUID, Name: cfg.Name, TotalPrice: cfg.TotalPrice, CustomPrice: cfg.CustomPrice, Notes: cfg.Notes, IsTemplate: cfg.IsTemplate, ServerCount: cfg.ServerCount, CreatedAt: cfg.CreatedAt, UpdatedAt: time.Now(), SyncStatus: "pending", OriginalUsername: cfg.OwnerUsername, } // Convert items localItems := make(localdb.LocalConfigItems, len(cfg.Items)) for i, item := range cfg.Items { localItems[i] = localdb.LocalConfigItem{ LotName: item.LotName, Quantity: item.Quantity, UnitPrice: item.UnitPrice, } } localCfg.Items = localItems if err := r.localDB.SaveConfiguration(localCfg); err != nil { return fmt.Errorf("saving local configuration: %w", err) } // Add to pending changes queue payload, err := json.Marshal(cfg) if err != nil { return fmt.Errorf("marshaling configuration for sync: %w", err) } return r.localDB.AddPendingChange("configuration", cfg.UUID, "create", string(payload)) } // GetConfigurations returns all configurations for a user func (r *UnifiedRepo) GetConfigurations(ownerUsername string) ([]models.Configuration, error) { if r.isOnline { repo := NewConfigurationRepository(r.mariaDB) configs, _, err := repo.ListByUser(ownerUsername, 0, 1000) return configs, err } // Offline: get from local SQLite localConfigs, err := r.localDB.GetConfigurations() if err != nil { return nil, fmt.Errorf("fetching local configurations: %w", err) } // Convert to models.Configuration result := make([]models.Configuration, len(localConfigs)) for i, lc := range localConfigs { items := make(models.ConfigItems, len(lc.Items)) for j, item := range lc.Items { items[j] = models.ConfigItem{ LotName: item.LotName, Quantity: item.Quantity, UnitPrice: item.UnitPrice, } } result[i] = models.Configuration{ UUID: lc.UUID, OwnerUsername: lc.OriginalUsername, Name: lc.Name, Items: items, TotalPrice: lc.TotalPrice, CustomPrice: lc.CustomPrice, Notes: lc.Notes, IsTemplate: lc.IsTemplate, ServerCount: lc.ServerCount, CreatedAt: lc.CreatedAt, } } return result, nil } // GetConfigurationByUUID returns a configuration by UUID func (r *UnifiedRepo) GetConfigurationByUUID(uuid string) (*models.Configuration, error) { if r.isOnline { repo := NewConfigurationRepository(r.mariaDB) return repo.GetByUUID(uuid) } localCfg, err := r.localDB.GetConfigurationByUUID(uuid) if err != nil { return nil, fmt.Errorf("fetching local configuration: %w", err) } items := make(models.ConfigItems, len(localCfg.Items)) for i, item := range localCfg.Items { items[i] = models.ConfigItem{ LotName: item.LotName, Quantity: item.Quantity, UnitPrice: item.UnitPrice, } } return &models.Configuration{ UUID: localCfg.UUID, Name: localCfg.Name, Items: items, TotalPrice: localCfg.TotalPrice, CustomPrice: localCfg.CustomPrice, Notes: localCfg.Notes, IsTemplate: localCfg.IsTemplate, ServerCount: localCfg.ServerCount, CreatedAt: localCfg.CreatedAt, }, nil } // DeleteConfiguration deletes a configuration func (r *UnifiedRepo) DeleteConfiguration(uuid string) error { if r.isOnline { // Get ID first cfg, err := r.GetConfigurationByUUID(uuid) if err != nil { return err } repo := NewConfigurationRepository(r.mariaDB) return repo.Delete(cfg.ID) } // Offline: delete from local and queue sync if err := r.localDB.DeleteConfiguration(uuid); err != nil { return fmt.Errorf("deleting local configuration: %w", err) } return r.localDB.AddPendingChange("configuration", uuid, "delete", "") } // Pricelist methods // GetPricelists returns all pricelists func (r *UnifiedRepo) GetPricelists() ([]models.PricelistSummary, error) { if r.isOnline { repo := NewPricelistRepository(r.mariaDB) summaries, _, err := repo.List(0, 1000) return summaries, err } // Offline: get from local cache localPLs, err := r.localDB.GetLocalPricelists() if err != nil { return nil, fmt.Errorf("fetching local pricelists: %w", err) } summaries := make([]models.PricelistSummary, len(localPLs)) for i, pl := range localPLs { itemCount := r.localDB.CountLocalPricelistItems(pl.ID) summaries[i] = models.PricelistSummary{ ID: pl.ServerID, Version: pl.Version, CreatedAt: pl.CreatedAt, ItemCount: itemCount, } } return summaries, nil } // GetPricelistByID returns a pricelist by ID func (r *UnifiedRepo) GetPricelistByID(id uint) (*models.Pricelist, error) { if r.isOnline { repo := NewPricelistRepository(r.mariaDB) return repo.GetByID(id) } // Offline: get from local cache localPL, err := r.localDB.GetLocalPricelistByServerID(id) if err != nil { return nil, fmt.Errorf("fetching local pricelist: %w", err) } itemCount := r.localDB.CountLocalPricelistItems(localPL.ID) return &models.Pricelist{ ID: localPL.ServerID, Version: localPL.Version, CreatedAt: localPL.CreatedAt, ItemCount: int(itemCount), }, nil } // GetPricelistItems returns items for a pricelist func (r *UnifiedRepo) GetPricelistItems(pricelistID uint) ([]models.PricelistItem, error) { if r.isOnline { repo := NewPricelistRepository(r.mariaDB) items, _, err := repo.GetItems(pricelistID, 0, 100000, "") return items, err } // Offline: get from local cache // First find the local pricelist by server ID localPL, err := r.localDB.GetLocalPricelistByServerID(pricelistID) if err != nil { return nil, fmt.Errorf("fetching local pricelist: %w", err) } localItems, err := r.localDB.GetLocalPricelistItems(localPL.ID) if err != nil { return nil, fmt.Errorf("fetching local pricelist items: %w", err) } items := make([]models.PricelistItem, len(localItems)) for i, item := range localItems { items[i] = models.PricelistItem{ ID: item.ID, PricelistID: pricelistID, LotName: item.LotName, Price: item.Price, } } return items, nil } // GetLatestPricelist returns the latest pricelist func (r *UnifiedRepo) GetLatestPricelist() (*models.Pricelist, error) { if r.isOnline { repo := NewPricelistRepository(r.mariaDB) return repo.GetLatestActive() } // Offline: get from local cache localPL, err := r.localDB.GetLatestLocalPricelist() if err != nil { return nil, fmt.Errorf("fetching latest local pricelist: %w", err) } itemCount := r.localDB.CountLocalPricelistItems(localPL.ID) return &models.Pricelist{ ID: localPL.ServerID, Version: localPL.Version, CreatedAt: localPL.CreatedAt, ItemCount: int(itemCount), }, nil }