Files
QuoteForge/internal/handlers/pricelist.go
Michael Chus 143d217397 Add Phase 2: Local SQLite database with sync functionality
Implements complete offline-first architecture with SQLite caching and MariaDB synchronization.

Key features:
- Local SQLite database for offline operation (data/quoteforge.db)
- Connection settings with encrypted credentials
- Component and pricelist caching with auto-sync
- Sync API endpoints (/api/sync/status, /components, /pricelists, /all)
- Real-time sync status indicator in UI with auto-refresh
- Offline mode detection middleware
- Migration tool for database initialization
- Setup wizard for initial configuration

New components:
- internal/localdb: SQLite repository layer (components, pricelists, sync)
- internal/services/sync: Synchronization service
- internal/handlers/sync: Sync API handlers
- internal/handlers/setup: Setup wizard handlers
- internal/middleware/offline: Offline detection
- cmd/migrate: Database migration tool

UI improvements:
- Setup page for database configuration
- Sync status indicator with online/offline detection
- Warning icons for pending synchronization
- Auto-refresh every 30 seconds

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-01 11:00:32 +03:00

135 lines
3.4 KiB
Go

package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"git.mchus.pro/mchus/quoteforge/internal/localdb"
"git.mchus.pro/mchus/quoteforge/internal/services/pricelist"
)
type PricelistHandler struct {
service *pricelist.Service
localDB *localdb.LocalDB
}
func NewPricelistHandler(service *pricelist.Service, localDB *localdb.LocalDB) *PricelistHandler {
return &PricelistHandler{service: service, localDB: localDB}
}
// List returns all pricelists with pagination
func (h *PricelistHandler) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
pricelists, total, err := h.service.List(page, perPage)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"pricelists": pricelists,
"total": total,
"page": page,
"per_page": perPage,
})
}
// Get returns a single pricelist by ID
func (h *PricelistHandler) Get(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid pricelist ID"})
return
}
pl, err := h.service.GetByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "pricelist not found"})
return
}
c.JSON(http.StatusOK, pl)
}
// Create creates a new pricelist from current prices
func (h *PricelistHandler) Create(c *gin.Context) {
// Get the database username as the creator
createdBy := h.localDB.GetDBUser()
if createdBy == "" {
createdBy = "unknown"
}
pl, err := h.service.CreateFromCurrentPrices(createdBy)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, pl)
}
// Delete deletes a pricelist by ID
func (h *PricelistHandler) Delete(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid pricelist ID"})
return
}
if err := h.service.Delete(uint(id)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "pricelist deleted"})
}
// GetItems returns items for a pricelist with pagination
func (h *PricelistHandler) GetItems(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid pricelist ID"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "50"))
search := c.Query("search")
items, total, err := h.service.GetItems(uint(id), page, perPage, search)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"items": items,
"total": total,
"page": page,
"per_page": perPage,
})
}
// CanWrite returns whether the current user can create pricelists
func (h *PricelistHandler) CanWrite(c *gin.Context) {
canWrite, debugInfo := h.service.CanWriteDebug()
c.JSON(http.StatusOK, gin.H{"can_write": canWrite, "debug": debugInfo})
}
// GetLatest returns the most recent active pricelist
func (h *PricelistHandler) GetLatest(c *gin.Context) {
pl, err := h.service.GetLatestActive()
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "no active pricelists found"})
return
}
c.JSON(http.StatusOK, pl)
}