Update CLAUDE.md TODO list and add local-first documentation
- Consolidate UI TODO items into single sync status partial task - Move conflict resolution to Phase 4 - Add LOCAL_FIRST_INTEGRATION.md with architecture guide - Add unified repository interface for future use Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,11 +31,9 @@
|
||||
- ✅ Background sync worker: auto-sync every 5min (push + pull) - `internal/services/sync/worker.go`
|
||||
|
||||
**TODO:**
|
||||
- ❌ Conflict resolution (last-write-wins or manual)
|
||||
- ❌ UI: pending counter in header
|
||||
- ❌ UI: manual sync button
|
||||
- ❌ UI: offline indicator (middleware already exists)
|
||||
- ❌ UI: sync status partial (pending badge + sync button + offline indicator)
|
||||
- ❌ RefreshPrices for local mode (via local_components)
|
||||
- ❌ Conflict resolution (Phase 4, last-write-wins default)
|
||||
|
||||
### Phase 3: Projects and Specifications
|
||||
- qt_projects, qt_specifications tables (MariaDB)
|
||||
|
||||
178
LOCAL_FIRST_INTEGRATION.md
Normal file
178
LOCAL_FIRST_INTEGRATION.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# Local-First Architecture Integration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
QuoteForge теперь поддерживает local-first архитектуру: приложение ВСЕГДА работает с SQLite (localdb), MariaDB используется только для синхронизации.
|
||||
|
||||
## Реализованные компоненты
|
||||
|
||||
### 1. Конвертеры моделей (`internal/localdb/converters.go`)
|
||||
|
||||
Конвертеры между MariaDB и SQLite моделями:
|
||||
- `ConfigurationToLocal()` / `LocalToConfiguration()`
|
||||
- `PricelistToLocal()` / `LocalToPricelist()`
|
||||
- `ComponentToLocal()` / `LocalToComponent()`
|
||||
|
||||
### 2. LocalDB методы (`internal/localdb/localdb.go`)
|
||||
|
||||
Добавлены методы для работы с pending changes:
|
||||
- `MarkChangesSynced(ids []int64)` - помечает изменения как синхронизированные
|
||||
- `GetPendingCount()` - возвращает количество несинхронизированных изменений
|
||||
|
||||
### 3. Sync Service расширения (`internal/services/sync/service.go`)
|
||||
|
||||
Новые методы:
|
||||
- `SyncPricelistsIfNeeded()` - проверяет и скачивает новые прайслисты при необходимости
|
||||
- `PushPendingChanges()` - отправляет все pending changes на сервер
|
||||
- `pushSingleChange()` - обрабатывает один pending change
|
||||
- `pushConfigurationCreate/Update/Delete()` - специфичные методы для конфигураций
|
||||
|
||||
**ВАЖНО**: Конструктор изменен - теперь требует `ConfigurationRepository`:
|
||||
```go
|
||||
syncService := sync.NewService(pricelistRepo, configRepo, local)
|
||||
```
|
||||
|
||||
### 4. LocalConfigurationService (`internal/services/local_configuration.go`)
|
||||
|
||||
Новый сервис для работы с конфигурациями в local-first режиме:
|
||||
- Все операции CRUD работают через SQLite
|
||||
- Автоматически добавляет изменения в pending_changes
|
||||
- При создании конфигурации (если online) проверяет новые прайслисты
|
||||
|
||||
```go
|
||||
localConfigService := services.NewLocalConfigurationService(
|
||||
localDB,
|
||||
syncService,
|
||||
quoteService,
|
||||
isOnlineFunc,
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Sync Handler расширения (`internal/handlers/sync.go`)
|
||||
|
||||
Новые endpoints:
|
||||
- `POST /api/sync/push` - отправить pending changes на сервер
|
||||
- `GET /api/sync/pending/count` - получить количество pending changes
|
||||
- `GET /api/sync/pending` - получить список pending changes
|
||||
|
||||
## Интеграция
|
||||
|
||||
### Шаг 1: Обновить main.go
|
||||
|
||||
```go
|
||||
// В cmd/server/main.go
|
||||
syncService := sync.NewService(pricelistRepo, configRepo, local)
|
||||
|
||||
// Создать isOnline функцию
|
||||
isOnlineFunc := func() bool {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return sqlDB.Ping() == nil
|
||||
}
|
||||
|
||||
// Создать LocalConfigurationService
|
||||
localConfigService := services.NewLocalConfigurationService(
|
||||
local,
|
||||
syncService,
|
||||
quoteService,
|
||||
isOnlineFunc,
|
||||
)
|
||||
```
|
||||
|
||||
### Шаг 2: Обновить ConfigurationHandler
|
||||
|
||||
Заменить `ConfigurationService` на `LocalConfigurationService` в handlers:
|
||||
|
||||
```go
|
||||
// Было:
|
||||
configHandler := handlers.NewConfigurationHandler(configService, exportService)
|
||||
|
||||
// Стало:
|
||||
configHandler := handlers.NewConfigurationHandler(localConfigService, exportService)
|
||||
```
|
||||
|
||||
### Шаг 3: Добавить endpoints для sync
|
||||
|
||||
В роутере добавить:
|
||||
```go
|
||||
syncGroup := router.Group("/api/sync")
|
||||
{
|
||||
syncGroup.POST("/push", syncHandler.PushPendingChanges)
|
||||
syncGroup.GET("/pending/count", syncHandler.GetPendingCount)
|
||||
syncGroup.GET("/pending", syncHandler.GetPendingChanges)
|
||||
}
|
||||
```
|
||||
|
||||
## Как это работает
|
||||
|
||||
### Создание конфигурации
|
||||
|
||||
1. Пользователь создает конфигурацию
|
||||
2. `LocalConfigurationService.Create()`:
|
||||
- Если online → `SyncPricelistsIfNeeded()` проверяет новые прайслисты
|
||||
- Сохраняет конфигурацию в SQLite
|
||||
- Добавляет в `pending_changes` с operation="create"
|
||||
3. Конфигурация доступна локально сразу
|
||||
|
||||
### Синхронизация с сервером
|
||||
|
||||
**Manual sync:**
|
||||
```bash
|
||||
POST /api/sync/push
|
||||
```
|
||||
|
||||
**Background sync (TODO):**
|
||||
- Периодический worker вызывает `syncService.PushPendingChanges()`
|
||||
- Проверяет online статус
|
||||
- Отправляет все pending changes на сервер
|
||||
- Удаляет успешно синхронизированные записи
|
||||
|
||||
### Offline режим
|
||||
|
||||
1. Все операции работают нормально через SQLite
|
||||
2. Изменения копятся в `pending_changes`
|
||||
3. При восстановлении соединения автоматически синхронизируются
|
||||
|
||||
## Pending Changes Queue
|
||||
|
||||
Таблица `pending_changes`:
|
||||
```go
|
||||
type PendingChange struct {
|
||||
ID int64 // Auto-increment
|
||||
EntityType string // "configuration", "project", "specification"
|
||||
EntityUUID string // UUID сущности
|
||||
Operation string // "create", "update", "delete"
|
||||
Payload string // JSON snapshot сущности
|
||||
CreatedAt time.Time
|
||||
Attempts int // Счетчик попыток синхронизации
|
||||
LastError string // Последняя ошибка синхронизации
|
||||
}
|
||||
```
|
||||
|
||||
## TODO для Phase 2.5
|
||||
|
||||
- [ ] Background sync worker (автоматическая синхронизация каждые N минут)
|
||||
- [ ] Conflict resolution (при конфликтах обновления)
|
||||
- [ ] UI: pending counter в header
|
||||
- [ ] UI: manual sync button
|
||||
- [ ] UI: conflict alerts
|
||||
- [ ] Retry logic для failed pending changes
|
||||
- [ ] RefreshPrices для local mode (через local_components)
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Compile
|
||||
go build ./cmd/server
|
||||
|
||||
# Run
|
||||
./quoteforge
|
||||
|
||||
# Check pending changes
|
||||
curl http://localhost:8080/api/sync/pending/count
|
||||
|
||||
# Manual sync
|
||||
curl -X POST http://localhost:8080/api/sync/push
|
||||
```
|
||||
399
internal/repository/unified.go
Normal file
399
internal/repository/unified.go
Normal file
@@ -0,0 +1,399 @@
|
||||
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(userID uint) ([]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)
|
||||
}
|
||||
if filter.HasPrice {
|
||||
query = query.Where("current_price IS NOT NULL AND current_price > 0")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
// Apply sorting
|
||||
sortDir := "ASC"
|
||||
if filter.SortDir == "desc" {
|
||||
sortDir = "DESC"
|
||||
}
|
||||
switch filter.SortField {
|
||||
case "current_price":
|
||||
query = query.Order("current_price " + sortDir)
|
||||
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,
|
||||
CurrentPrice: comp.CurrentPrice,
|
||||
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,
|
||||
CurrentPrice: comp.CurrentPrice,
|
||||
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",
|
||||
}
|
||||
|
||||
// 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(userID uint) ([]models.Configuration, error) {
|
||||
if r.isOnline {
|
||||
repo := NewConfigurationRepository(r.mariaDB)
|
||||
configs, _, err := repo.ListByUser(userID, 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,
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user