package localdb import ( "fmt" "strings" "time" ) // ComponentFilter for searching with filters type ComponentFilter struct { Category string Search string HasPrice bool } // ComponentSyncResult contains statistics from component sync type ComponentSyncResult struct { TotalSynced int NewCount int UpdateCount int Duration time.Duration } // latestActivePricelistID returns the local DB id of the most recently created // active pricelist for the given source ("estimate", "warehouse", etc.). func (l *LocalDB) latestActivePricelistID(source string) (uint, error) { var id uint err := l.db.Table("local_pricelists"). Select("id"). Where("is_active = ? AND source = ?", true, source). Order("created_at DESC, id DESC"). Limit(1). Scan(&id).Error if err != nil { return 0, err } if id == 0 { return 0, fmt.Errorf("no active %s pricelist", source) } return id, nil } // pricelistItemRow is used for scanning rows from local_pricelist_items. type pricelistItemRow struct { LotName string `gorm:"column:lot_name"` Category string `gorm:"column:lot_category"` } func (r pricelistItemRow) toLocalComponent() LocalComponent { return LocalComponent{ LotName: r.LotName, Category: r.Category, } } // SearchLocalComponents searches components in the latest active estimate // pricelist by lot_name. func (l *LocalDB) SearchLocalComponents(query string, limit int) ([]LocalComponent, error) { if limit <= 0 { limit = 50 } pricelistID, err := l.latestActivePricelistID("estimate") if err != nil { return nil, err } db := l.db.Table("local_pricelist_items"). Where("pricelist_id = ?", pricelistID) if query != "" { db = db.Where("LOWER(lot_name) LIKE ?", "%"+strings.ToLower(query)+"%") } var rows []pricelistItemRow if err := db.Select("lot_name, lot_category").Order("lot_name").Limit(limit).Scan(&rows).Error; err != nil { return nil, err } components := make([]LocalComponent, len(rows)) for i, r := range rows { components[i] = r.toLocalComponent() } return components, nil } // SearchLocalComponentsByCategory searches components in the latest active // estimate pricelist filtered by category. func (l *LocalDB) SearchLocalComponentsByCategory(category, query string, limit int) ([]LocalComponent, error) { if limit <= 0 { limit = 50 } pricelistID, err := l.latestActivePricelistID("estimate") if err != nil { return nil, err } db := l.db.Table("local_pricelist_items"). Where("pricelist_id = ? AND UPPER(lot_category) = ?", pricelistID, strings.ToUpper(category)) if query != "" { db = db.Where("LOWER(lot_name) LIKE ?", "%"+strings.ToLower(query)+"%") } var rows []pricelistItemRow if err := db.Select("lot_name, lot_category").Order("lot_name").Limit(limit).Scan(&rows).Error; err != nil { return nil, err } components := make([]LocalComponent, len(rows)) for i, r := range rows { components[i] = r.toLocalComponent() } return components, nil } // ListComponents returns components from the latest active estimate pricelist // with optional category/search filtering and pagination. func (l *LocalDB) ListComponents(filter ComponentFilter, offset, limit int) ([]LocalComponent, int64, error) { pricelistID, err := l.latestActivePricelistID("estimate") if err != nil { return nil, 0, err } db := l.db.Table("local_pricelist_items"). Where("pricelist_id = ?", pricelistID) if filter.Category != "" { db = db.Where("UPPER(lot_category) = ?", strings.ToUpper(filter.Category)) } if filter.Search != "" { db = db.Where("LOWER(lot_name) LIKE ?", "%"+strings.ToLower(filter.Search)+"%") } var total int64 if err := db.Count(&total).Error; err != nil { return nil, 0, err } var rows []pricelistItemRow if err := db.Select("lot_name, lot_category").Order("lot_name").Offset(offset).Limit(limit).Scan(&rows).Error; err != nil { return nil, 0, err } components := make([]LocalComponent, len(rows)) for i, r := range rows { components[i] = r.toLocalComponent() } return components, total, nil } // GetLocalComponent returns a single component by lot_name from the latest // active estimate pricelist. func (l *LocalDB) GetLocalComponent(lotName string) (*LocalComponent, error) { pricelistID, err := l.latestActivePricelistID("estimate") if err != nil { return nil, err } var row pricelistItemRow if err := l.db.Table("local_pricelist_items"). Select("lot_name, lot_category"). Where("pricelist_id = ? AND UPPER(lot_name) = ?", pricelistID, strings.ToUpper(lotName)). First(&row).Error; err != nil { return nil, err } c := row.toLocalComponent() return &c, nil } // GetLocalComponentCategoriesByLotNames returns category for each lot_name // from the latest active estimate pricelist. func (l *LocalDB) GetLocalComponentCategoriesByLotNames(lotNames []string) (map[string]string, error) { result := make(map[string]string, len(lotNames)) if len(lotNames) == 0 { return result, nil } pricelistID, err := l.latestActivePricelistID("estimate") if err != nil { return result, nil } // Build uppercase → original mapping so result keys match what the caller passed. upperToOrig := make(map[string]string, len(lotNames)) upper := make([]string, len(lotNames)) for i, n := range lotNames { u := strings.ToUpper(n) upper[i] = u upperToOrig[u] = n } var rows []pricelistItemRow if err := l.db.Table("local_pricelist_items"). Select("lot_name, lot_category"). Where("pricelist_id = ? AND UPPER(lot_name) IN ?", pricelistID, upper). Scan(&rows).Error; err != nil { return nil, err } for _, r := range rows { orig := upperToOrig[strings.ToUpper(r.LotName)] if orig == "" { orig = r.LotName } result[orig] = r.Category } return result, nil } // GetLocalComponentCategories returns distinct categories from the latest // active estimate pricelist. func (l *LocalDB) GetLocalComponentCategories() ([]string, error) { pricelistID, err := l.latestActivePricelistID("estimate") if err != nil { return nil, err } var categories []string if err := l.db.Table("local_pricelist_items"). Where("pricelist_id = ? AND lot_category != ''", pricelistID). Distinct("lot_category"). Order("lot_category"). Pluck("lot_category", &categories).Error; err != nil { return nil, err } return categories, nil } // CountComponents returns the number of distinct lot names in the latest // active estimate pricelist (used to check if data is available). func (l *LocalDB) CountComponents() int64 { pricelistID, err := l.latestActivePricelistID("estimate") if err != nil { return 0 } var count int64 l.db.Table("local_pricelist_items").Where("pricelist_id = ?", pricelistID).Count(&count) return count }