package repository import ( "git.mchus.pro/mchus/quoteforge/internal/localdb" "gorm.io/gorm" "gorm.io/gorm/clause" ) // PartnumberBookRepository provides read-only access to local partnumber book snapshots. type PartnumberBookRepository struct { db *gorm.DB } func NewPartnumberBookRepository(db *gorm.DB) *PartnumberBookRepository { return &PartnumberBookRepository{db: db} } // GetActiveBook returns the most recently active local partnumber book. func (r *PartnumberBookRepository) GetActiveBook() (*localdb.LocalPartnumberBook, error) { var book localdb.LocalPartnumberBook err := r.db.Where("is_active = 1").Order("created_at DESC, id DESC").First(&book).Error if err != nil { return nil, err } return &book, nil } // GetBookItems returns all items for the given local book ID. func (r *PartnumberBookRepository) GetBookItems(bookID uint) ([]localdb.LocalPartnumberBookItem, error) { book, err := r.getBook(bookID) if err != nil { return nil, err } items, _, err := r.listCatalogItems(book.PartnumbersJSON, "", 0, 0) return items, err } // GetBookItemsPage returns items for the given local book ID with optional search and pagination. func (r *PartnumberBookRepository) GetBookItemsPage(bookID uint, search string, page, perPage int) ([]localdb.LocalPartnumberBookItem, int64, error) { if page < 1 { page = 1 } if perPage < 1 { perPage = 100 } book, err := r.getBook(bookID) if err != nil { return nil, 0, err } return r.listCatalogItems(book.PartnumbersJSON, search, page, perPage) } // FindLotByPartnumber looks up a partnumber in the active book and returns the matching items. func (r *PartnumberBookRepository) FindLotByPartnumber(bookID uint, partnumber string) ([]localdb.LocalPartnumberBookItem, error) { book, err := r.getBook(bookID) if err != nil { return nil, err } found := false for _, pn := range book.PartnumbersJSON { if pn == partnumber { found = true break } } if !found { return nil, nil } var items []localdb.LocalPartnumberBookItem err = r.db.Where("partnumber = ?", partnumber).Find(&items).Error return items, err } // ListBooks returns all local partnumber books ordered newest first. func (r *PartnumberBookRepository) ListBooks() ([]localdb.LocalPartnumberBook, error) { var books []localdb.LocalPartnumberBook err := r.db.Order("created_at DESC, id DESC").Find(&books).Error return books, err } // SaveBook saves a new partnumber book snapshot. func (r *PartnumberBookRepository) SaveBook(book *localdb.LocalPartnumberBook) error { return r.db.Save(book).Error } // SaveBookItems upserts canonical PN catalog rows. func (r *PartnumberBookRepository) SaveBookItems(items []localdb.LocalPartnumberBookItem) error { if len(items) == 0 { return nil } return r.db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "partnumber"}}, DoUpdates: clause.AssignmentColumns([]string{ "lots_json", "description", }), }).CreateInBatches(items, 500).Error } // CountBookItems returns the number of items for a given local book ID. func (r *PartnumberBookRepository) CountBookItems(bookID uint) int64 { book, err := r.getBook(bookID) if err != nil { return 0 } return int64(len(book.PartnumbersJSON)) } func (r *PartnumberBookRepository) CountDistinctLots(bookID uint) int64 { items, err := r.GetBookItems(bookID) if err != nil { return 0 } seen := make(map[string]struct{}) for _, item := range items { for _, lot := range item.LotsJSON { if lot.LotName == "" { continue } seen[lot.LotName] = struct{}{} } } return int64(len(seen)) } func (r *PartnumberBookRepository) HasAllBookItems(bookID uint) bool { book, err := r.getBook(bookID) if err != nil { return false } if len(book.PartnumbersJSON) == 0 { return true } var count int64 if err := r.db.Model(&localdb.LocalPartnumberBookItem{}). Where("partnumber IN ?", []string(book.PartnumbersJSON)). Count(&count).Error; err != nil { return false } return count == int64(len(book.PartnumbersJSON)) } func (r *PartnumberBookRepository) getBook(bookID uint) (*localdb.LocalPartnumberBook, error) { var book localdb.LocalPartnumberBook if err := r.db.First(&book, bookID).Error; err != nil { return nil, err } return &book, nil } func (r *PartnumberBookRepository) listCatalogItems(partnumbers localdb.LocalStringList, search string, page, perPage int) ([]localdb.LocalPartnumberBookItem, int64, error) { if len(partnumbers) == 0 { return []localdb.LocalPartnumberBookItem{}, 0, nil } query := r.db.Model(&localdb.LocalPartnumberBookItem{}).Where("partnumber IN ?", []string(partnumbers)) if search != "" { trimmedSearch := "%" + search + "%" query = query.Where("partnumber LIKE ? OR lots_json LIKE ? OR description LIKE ?", trimmedSearch, trimmedSearch, trimmedSearch) } var total int64 if err := query.Count(&total).Error; err != nil { return nil, 0, err } var items []localdb.LocalPartnumberBookItem if page > 0 && perPage > 0 { query = query.Offset((page - 1) * perPage).Limit(perPage) } err := query.Order("partnumber ASC, id ASC").Find(&items).Error return items, total, err }