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`
|
- ✅ Background sync worker: auto-sync every 5min (push + pull) - `internal/services/sync/worker.go`
|
||||||
|
|
||||||
**TODO:**
|
**TODO:**
|
||||||
- ❌ Conflict resolution (last-write-wins or manual)
|
- ❌ UI: sync status partial (pending badge + sync button + offline indicator)
|
||||||
- ❌ UI: pending counter in header
|
|
||||||
- ❌ UI: manual sync button
|
|
||||||
- ❌ UI: offline indicator (middleware already exists)
|
|
||||||
- ❌ RefreshPrices for local mode (via local_components)
|
- ❌ RefreshPrices for local mode (via local_components)
|
||||||
|
- ❌ Conflict resolution (Phase 4, last-write-wins default)
|
||||||
|
|
||||||
### Phase 3: Projects and Specifications
|
### Phase 3: Projects and Specifications
|
||||||
- qt_projects, qt_specifications tables (MariaDB)
|
- 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