diff --git a/rrbec_server/ELECTRON_INTEGRATION.md b/rrbec_server/ELECTRON_INTEGRATION.md deleted file mode 100644 index 93945f4..0000000 --- a/rrbec_server/ELECTRON_INTEGRATION.md +++ /dev/null @@ -1,86 +0,0 @@ -# RRBEC Middleware - Guia de Integração (Electron/Desktop) - -Este guia documenta como a aplicação Electron deve se comunicar com o servidor middleware local em Go. - -## 1. Configurações Base -- **URL Base**: `http://localhost:8080/api/v1` -- **Porta Default**: `8080` (configurável no arquivo `.env` do servidor). -- **Formato**: Todas as requisições e respostas utilizam `application/json`. - -## 2. Autenticação (SimpleAuth) -O servidor não utiliza tokens JWT complexos localmente. A autenticação funciona assim: -1. Faça login em `/login` enviando `username` e `password`. -2. O servidor retornará um objeto de usuário. Capture o valor do campo `id` (inteiro). -3. Envie esse valor no cabeçalho HTTP `X-User-ID` em todas as rotas marcadas como **[PROTEGIDO]**. - ---- - -## 3. Endpoints da API - -### [PÚBLICO] Login -**POST** `/login` -- **Body**: `{ "username": "seu_usuario", "password": "sua_senha" }` -- **Retorno**: Objeto User completo. - -### [PÚBLICO] Listar Mesas -**GET** `/mesas` -- **Retorno**: Array de objetos mesas com `id`, `uuid`, `name`, `active`. - -### [PÚBLICO] Listar Produtos/Estoque -**GET** `/products` -- **Retorno**: Array de produtos com preços e quantidade em estoque. - -### [PÚBLICO] Listar Categorias -**GET** `/categories` - -### [PÚBLICO] Listar Clientes -**GET** `/clients` - -### [PÚBLICO] Listar Pedidos (Cozinha/Orders) -**GET** `/orders` - -### [PÚBLICO] Listar Tipos de Pagamento -**GET** `/payment-types` - -### [PÚBLICO] Listar Pagamentos Realizados -**GET** `/payments` - -### [PÚBLICO] Ver Comanda por ID -**GET** `/comandas/:id` (Ex: `/api/v1/comandas/9`) -- **Retorno**: Detalhes da comanda. - ---- - -## 4. Comandas e Itens (Ações) - -### [PROTEGIDO] Abrir Nova Comanda -**POST** `/comandas` -- **Headers**: `X-User-ID: ` -- **Body**: `{ "mesa_id": 1, "client_id": null }` - -### [PROTEGIDO] Lançar Pedido (Adicionar Item) -**POST** `/items-comanda` -- **Headers**: `X-User-ID: ` -- **Body**: `{ "comanda_id": 9, "product_id": 50, "applicant": "Nome do Garçom" }` - -### [PROTEGIDO] Deletar Item Individual -**DELETE** `/items-comanda/:id` -- **Headers**: `X-User-ID: ` - -### [PROTEGIDO] Limpar e Fechar Comanda (Apagar Inteira) -**POST** `/comandas/:id/apagar` -- **Headers**: `X-User-ID: ` -- **Efeito**: Remove todos os itens da comanda e muda o status para `CLOSED`. - -### [PROTEGIDO] Pagar e Fechar Comanda -**POST** `/comandas/:id/pagar` -- **Headers**: `X-User-ID: ` -- **Body**: - ```json - { - "value": 50.00, - "type_pay_id": 1, - "client_id": null - } - ``` -- **Efeito**: Registra o pagamento localmente e fecha a comanda. diff --git a/rrbec_server/cmd/server/main.go b/rrbec_server/cmd/server/main.go index 3b17cb5..576f238 100644 --- a/rrbec_server/cmd/server/main.go +++ b/rrbec_server/cmd/server/main.go @@ -26,6 +26,7 @@ func main() { // 4. Start Sync Worker syncer := sync.NewSyncer(repo, config.AppConfig.DjangoBaseURL, config.AppConfig.DjangoMasterUser, config.AppConfig.DjangoMasterPassword) + handler.SetSyncer(syncer) syncer.Start() // 4. Setup Gin Router @@ -52,6 +53,7 @@ func main() { { protected.POST("/products", handler.CreateProduct) protected.PATCH("/products/:id", handler.UpdateProduct) + protected.POST("/admin/resync-products", handler.ResyncProducts) protected.POST("/categories", handler.CreateCategory) protected.PATCH("/categories/:id", handler.UpdateCategory) protected.POST("/comandas", handler.CreateComanda) diff --git a/rrbec_server/internal/api/handlers.go b/rrbec_server/internal/api/handlers.go index 2a92c8a..17694e9 100644 --- a/rrbec_server/internal/api/handlers.go +++ b/rrbec_server/internal/api/handlers.go @@ -4,27 +4,97 @@ import ( "log" "net/http" "rrbec_server/internal/models" + "rrbec_server/internal/repository" "rrbec_server/internal/service" "strconv" + "time" "github.com/gin-gonic/gin" ) type Handler struct { - svc *service.Service + svc *service.Service + syncer interface{ SyncProductsOnly() } } func NewHandler(svc *service.Service) *Handler { return &Handler{svc: svc} } +func (h *Handler) SetSyncer(syncer interface{ SyncProductsOnly() }) { + h.syncer = syncer +} + +func parsePagination(c *gin.Context) *repository.PaginationParams { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) + sort := c.DefaultQuery("sort", "id ASC") + + params := &repository.PaginationParams{ + Page: page, + Limit: limit, + Sort: sort, + Filters: make(map[string]interface{}), + } + + if name := c.Query("name"); name != "" { + params.Filters["name"] = name + } + if active := c.Query("active"); active != "" { + if b, err := strconv.ParseBool(active); err == nil { + params.Filters["active"] = b + } + } + if status := c.Query("status"); status != "" { + params.Filters["status"] = status + } + if category := c.Query("category"); category != "" { + if c, err := strconv.ParseUint(category, 10, 64); err == nil { + params.Filters["category"] = uint(c) + } + } + if cuisine := c.Query("cuisine"); cuisine != "" { + if b, err := strconv.ParseBool(cuisine); err == nil { + params.Filters["cuisine"] = b + } + } + if dateFrom := c.Query("date_from"); dateFrom != "" { + if t, err := time.Parse("2006-01-02", dateFrom); err == nil { + params.Filters["date_from"] = t + } + } + if dateTo := c.Query("date_to"); dateTo != "" { + if t, err := time.Parse("2006-01-02", dateTo); err == nil { + params.Filters["date_to"] = t + } + } + if comandaID := c.Query("comanda_id"); comandaID != "" { + if c, err := strconv.ParseUint(comandaID, 10, 64); err == nil { + params.Filters["comanda_id"] = uint(c) + } + } + if canceled := c.Query("canceled"); canceled != "" { + if b, err := strconv.ParseBool(canceled); err == nil { + params.Filters["canceled"] = b + } + } + if delivered := c.Query("delivered"); delivered != "" { + if b, err := strconv.ParseBool(delivered); err == nil { + params.Filters["delivered"] = b + } + } + + return params +} + func (h *Handler) GetProducts(c *gin.Context) { - products, err := h.svc.GetProducts() + params := parsePagination(c) + result, err := h.svc.GetProducts(params) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, products) + c.JSON(http.StatusOK, result) } func (h *Handler) CreateProduct(c *gin.Context) { @@ -65,21 +135,23 @@ func (h *Handler) UpdateProduct(c *gin.Context) { } func (h *Handler) GetMesas(c *gin.Context) { - mesas, err := h.svc.GetMesas() + params := parsePagination(c) + result, err := h.svc.GetMesas(params) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, mesas) + c.JSON(http.StatusOK, result) } func (h *Handler) GetCategories(c *gin.Context) { - categories, err := h.svc.GetCategories() + params := parsePagination(c) + result, err := h.svc.GetCategories(params) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, categories) + c.JSON(http.StatusOK, result) } func (h *Handler) CreateCategory(c *gin.Context) { @@ -120,30 +192,33 @@ func (h *Handler) UpdateCategory(c *gin.Context) { } func (h *Handler) GetTypePayments(c *gin.Context) { - types, err := h.svc.GetTypePayments() + params := parsePagination(c) + result, err := h.svc.GetTypePayments(params) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, types) + c.JSON(http.StatusOK, result) } func (h *Handler) GetClients(c *gin.Context) { - clients, err := h.svc.GetClients() + params := parsePagination(c) + result, err := h.svc.GetClients(params) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, clients) + c.JSON(http.StatusOK, result) } func (h *Handler) GetOrders(c *gin.Context) { - orders, err := h.svc.GetOrders() + params := parsePagination(c) + result, err := h.svc.GetOrders(params) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, orders) + c.JSON(http.StatusOK, result) } func (h *Handler) CreateOrder(c *gin.Context) { @@ -244,12 +319,16 @@ func (h *Handler) SetOrderCanceled(c *gin.Context) { } func (h *Handler) GetPayments(c *gin.Context) { - payments, err := h.svc.GetPayments() + params := parsePagination(c) + if c.Query("sort") == "" { + params.Sort = "id DESC" + } + result, err := h.svc.GetPayments(params) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, payments) + c.JSON(http.StatusOK, result) } func (h *Handler) CreateComanda(c *gin.Context) { @@ -400,12 +479,16 @@ func (h *Handler) PagarComanda(c *gin.Context) { } func (h *Handler) GetComandas(c *gin.Context) { - comandas, err := h.svc.GetComandas() + params := parsePagination(c) + if c.Query("sort") == "" { + params.Sort = "id DESC" + } + result, err := h.svc.GetComandas(params) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, comandas) + c.JSON(http.StatusOK, result) } func (h *Handler) GetComandaByID(c *gin.Context) { @@ -455,3 +538,12 @@ func (h *Handler) GetCurrentUser(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"user_id": userID}) } + +func (h *Handler) ResyncProducts(c *gin.Context) { + if h.syncer == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "syncer not configured"}) + return + } + go h.syncer.SyncProductsOnly() + c.JSON(http.StatusOK, gin.H{"message": "Product resync started in background"}) +} diff --git a/rrbec_server/internal/models/models.go b/rrbec_server/internal/models/models.go index cf21ebd..1a66386 100644 --- a/rrbec_server/internal/models/models.go +++ b/rrbec_server/internal/models/models.go @@ -97,8 +97,8 @@ type Comanda struct { DtOpen time.Time `json:"dt_open"` DtClose *time.Time `json:"dt_close"` - // Nested items from Django (not stored in coma nda table) - Items []ProductComanda `gorm:"-" json:"items"` + // Nested items from Django (not stored in comanda table) + Items []ProductComanda `gorm:"foreignKey:ComandaID" json:"items"` } type ProductComanda struct { diff --git a/rrbec_server/internal/repository/repository.go b/rrbec_server/internal/repository/repository.go index a953b64..33543a1 100644 --- a/rrbec_server/internal/repository/repository.go +++ b/rrbec_server/internal/repository/repository.go @@ -4,9 +4,131 @@ import ( "fmt" "gorm.io/gorm" "log" + "math" "rrbec_server/internal/models" + "time" ) +type PaginationParams struct { + Page int + Limit int + Sort string + Filters map[string]interface{} +} + +type PaginatedResponse struct { + Data interface{} `json:"data"` + Total int64 `json:"total"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int `json:"total_pages"` +} + +func (p *PaginationParams) Apply(db *gorm.DB, model interface{}) (*gorm.DB, int64, error) { + if p.Page <= 0 { + p.Page = 1 + } + if p.Limit <= 0 { + p.Limit = 50 + } + if p.Limit > 200 { + p.Limit = 200 + } + + query := db.Model(model) + + for key, value := range p.Filters { + if key == "status" { + if s, ok := value.(string); ok && s != "" { + query = query.Where("status = ?", s) + } + } else if key == "active" { + if b, ok := value.(bool); ok { + query = query.Where("active = ?", b) + } + } else if key == "category" { + if c, ok := value.(uint); ok && c > 0 { + query = query.Where("category = ?", c) + } + } else if key == "cuisine" { + if b, ok := value.(bool); ok { + query = query.Where("cuisine = ?", b) + } + } else if key == "date_from" { + if t, ok := value.(time.Time); ok { + query = query.Where("date_time >= ? OR dt_open >= ? OR created_at >= ?", t, t, t) + } + } else if key == "date_to" { + if t, ok := value.(time.Time); ok { + query = query.Where("date_time <= ? OR dt_open <= ? OR created_at <= ?", t, t, t) + } + } else if key == "comanda_id" { + if c, ok := value.(uint); ok && c > 0 { + query = query.Where("comanda_id = ?", c) + } + } else if key == "name" { + if n, ok := value.(string); ok && n != "" { + query = query.Where("name LIKE ?", "%"+n+"%") + } + } else if key == "canceled" { + if v, ok := value.(bool); ok { + if v { + query = query.Where("canceled IS NOT NULL") + } else { + query = query.Where("canceled IS NULL") + } + } + } else if key == "delivered" { + if v, ok := value.(bool); ok { + if v { + query = query.Where("delivered IS NOT NULL") + } else { + query = query.Where("delivered IS NULL") + } + } + } + } + + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + sort := "id ASC" + if p.Sort != "" { + sort = p.Sort + } + + offset := (p.Page - 1) * p.Limit + query = query.Order(sort).Offset(offset).Limit(p.Limit) + + return query, total, nil +} + +func (p *PaginationParams) Paginate(db *gorm.DB, model interface{}, result interface{}) (*PaginatedResponse, error) { + query, total, err := p.Apply(db, model) + if err != nil { + return nil, err + } + + if err := query.Find(result).Error; err != nil { + return nil, err + } + + totalPages := int(math.Ceil(float64(total) / float64(p.Limit))) + if totalPages == 0 { + totalPages = 1 + } + + return &PaginatedResponse{ + Data: result, + Total: total, + Page: p.Page, + Limit: p.Limit, + TotalPages: totalPages, + }, nil +} + type Repository struct { db *gorm.DB } @@ -16,10 +138,9 @@ func NewRepository(db *gorm.DB) *Repository { } // Mesa -func (r *Repository) GetMesas() ([]models.Mesa, error) { +func (r *Repository) GetMesas(params *PaginationParams) (*PaginatedResponse, error) { var mesas []models.Mesa - err := r.db.Find(&mesas).Error - return mesas, err + return params.Paginate(r.db, &models.Mesa{}, &mesas) } func (r *Repository) CreateMesa(mesa *models.Mesa) error { @@ -31,10 +152,9 @@ func (r *Repository) SaveMesa(mesa *models.Mesa) error { } // Client -func (r *Repository) GetClients() ([]models.Client, error) { +func (r *Repository) GetClients(params *PaginationParams) (*PaginatedResponse, error) { var clients []models.Client - err := r.db.Find(&clients).Error - return clients, err + return params.Paginate(r.db, &models.Client{}, &clients) } func (r *Repository) CreateClient(client *models.Client) error { @@ -46,10 +166,13 @@ func (r *Repository) SaveClient(client *models.Client) error { } // Product -func (r *Repository) GetProducts() ([]models.Product, error) { +func (r *Repository) GetProducts(params *PaginationParams) (*PaginatedResponse, error) { var products []models.Product - err := r.db.Find(&products).Error - return products, err + return params.Paginate(r.db, &models.Product{}, &products) +} + +func (r *Repository) GetProductsAll(products *[]models.Product) error { + return r.db.Find(products).Error } func (r *Repository) SaveProduct(product *models.Product) error { @@ -88,20 +211,33 @@ func (r *Repository) CreateProduct(product *models.Product) error { } // Comanda -func (r *Repository) GetComandas() ([]models.Comanda, error) { +func (r *Repository) GetComandas(params *PaginationParams) (*PaginatedResponse, error) { var comandas []models.Comanda - if err := r.db.Find(&comandas).Error; err != nil { + if params == nil { + params = &PaginationParams{Page: 1, Limit: 50} + } + + query, total, err := params.Apply(r.db, &models.Comanda{}) + if err != nil { return nil, err } - // For each comanda, load its items - for i := range comandas { - var items []models.ProductComanda - r.db.Where("comanda_id = ?", comandas[i].ID).Find(&items) - comandas[i].Items = items + if err := query.Preload("Items").Find(&comandas).Error; err != nil { + return nil, err } - return comandas, nil + totalPages := int(math.Ceil(float64(total) / float64(params.Limit))) + if totalPages == 0 { + totalPages = 1 + } + + return &PaginatedResponse{ + Data: comandas, + Total: total, + Page: params.Page, + Limit: params.Limit, + TotalPages: totalPages, + }, nil } func (r *Repository) CreateComanda(comanda *models.Comanda) error { @@ -128,16 +264,14 @@ func (r *Repository) SaveTypePay(tp *models.TypePay) error { return r.db.Save(tp).Error } -func (r *Repository) GetCategories() ([]models.Category, error) { +func (r *Repository) GetCategories(params *PaginationParams) (*PaginatedResponse, error) { var categories []models.Category - err := r.db.Find(&categories).Error - return categories, err + return params.Paginate(r.db, &models.Category{}, &categories) } -func (r *Repository) GetTypePayments() ([]models.TypePay, error) { +func (r *Repository) GetTypePayments(params *PaginationParams) (*PaginatedResponse, error) { var types []models.TypePay - err := r.db.Find(&types).Error - return types, err + return params.Paginate(r.db, &models.TypePay{}, &types) } func (r *Repository) GetComandaByID(id uint) (*models.Comanda, error) { @@ -172,10 +306,9 @@ func (r *Repository) DeleteItem(itemID uint) error { return r.db.Delete(&models.ProductComanda{}, itemID).Error } -func (r *Repository) GetOrders() ([]models.Order, error) { +func (r *Repository) GetOrders(params *PaginationParams) (*PaginatedResponse, error) { var orders []models.Order - err := r.db.Find(&orders).Error - return orders, err + return params.Paginate(r.db, &models.Order{}, &orders) } func (r *Repository) SaveOrder(order *models.Order) error { @@ -216,10 +349,9 @@ func (r *Repository) UpdateOrderFields(id uint, updates map[string]interface{}) return r.db.Model(&models.Order{}).Where("id = ?", id).Updates(mappedUpdates).Error } -func (r *Repository) GetPayments() ([]models.Payment, error) { +func (r *Repository) GetPayments(params *PaginationParams) (*PaginatedResponse, error) { var payments []models.Payment - err := r.db.Find(&payments).Error - return payments, err + return params.Paginate(r.db, &models.Payment{}, &payments) } func (r *Repository) CreatePayment(payment *models.Payment) error { diff --git a/rrbec_server/internal/service/service.go b/rrbec_server/internal/service/service.go index e53fde4..90a0cfe 100644 --- a/rrbec_server/internal/service/service.go +++ b/rrbec_server/internal/service/service.go @@ -122,8 +122,8 @@ func (s *Service) SyncWithCloud() error { return nil } -func (s *Service) GetProducts() ([]models.Product, error) { - return s.repo.GetProducts() +func (s *Service) GetProducts(params *repository.PaginationParams) (*repository.PaginatedResponse, error) { + return s.repo.GetProducts(params) } func (s *Service) CreateProduct(product *models.Product) error { @@ -134,20 +134,20 @@ func (s *Service) UpdateProduct(id uint, updates map[string]interface{}) error { return s.repo.UpdateProductFields(id, updates) } -func (s *Service) GetMesas() ([]models.Mesa, error) { - return s.repo.GetMesas() +func (s *Service) GetMesas(params *repository.PaginationParams) (*repository.PaginatedResponse, error) { + return s.repo.GetMesas(params) } -func (s *Service) GetComandas() ([]models.Comanda, error) { - return s.repo.GetComandas() +func (s *Service) GetComandas(params *repository.PaginationParams) (*repository.PaginatedResponse, error) { + return s.repo.GetComandas(params) } func (s *Service) GetComandaByID(id uint) (*models.Comanda, error) { return s.repo.GetComandaByID(id) } -func (s *Service) GetCategories() ([]models.Category, error) { - return s.repo.GetCategories() +func (s *Service) GetCategories(params *repository.PaginationParams) (*repository.PaginatedResponse, error) { + return s.repo.GetCategories(params) } func (s *Service) CreateCategory(cat *models.Category) error { @@ -158,16 +158,16 @@ func (s *Service) UpdateCategory(id uint, updates map[string]interface{}) error return s.repo.UpdateCategoryFields(id, updates) } -func (s *Service) GetTypePayments() ([]models.TypePay, error) { - return s.repo.GetTypePayments() +func (s *Service) GetTypePayments(params *repository.PaginationParams) (*repository.PaginatedResponse, error) { + return s.repo.GetTypePayments(params) } -func (s *Service) GetClients() ([]models.Client, error) { - return s.repo.GetClients() +func (s *Service) GetClients(params *repository.PaginationParams) (*repository.PaginatedResponse, error) { + return s.repo.GetClients(params) } -func (s *Service) GetOrders() ([]models.Order, error) { - return s.repo.GetOrders() +func (s *Service) GetOrders(params *repository.PaginationParams) (*repository.PaginatedResponse, error) { + return s.repo.GetOrders(params) } func (s *Service) CreateOrder(order *models.Order) error { @@ -199,8 +199,8 @@ func (s *Service) SetOrderCanceled(id uint) error { return s.repo.UpdateOrderFields(id, map[string]interface{}{"canceled": &now}) } -func (s *Service) GetPayments() ([]models.Payment, error) { - return s.repo.GetPayments() +func (s *Service) GetPayments(params *repository.PaginationParams) (*repository.PaginatedResponse, error) { + return s.repo.GetPayments(params) } func (s *Service) DeleteItem(itemID uint) error { diff --git a/rrbec_server/internal/sync/syncer.go b/rrbec_server/internal/sync/syncer.go index bd56be0..dc40e62 100644 --- a/rrbec_server/internal/sync/syncer.go +++ b/rrbec_server/internal/sync/syncer.go @@ -288,48 +288,76 @@ func (s *Syncer) InitialSync() { log.Println("Initial sync completed successfully.") } -func (s *Syncer) fetchFromDjango(endpoint string, target interface{}) { +func (s *Syncer) fetchAllFromDjango(endpoint string, target interface{}) { endpoint = strings.Trim(endpoint, "/") - - url := "" - if strings.Contains(endpoint, "?") { - url = fmt.Sprintf("%s/api/v1/%s", s.djangoURL, endpoint) - } else { - url = fmt.Sprintf("%s/api/v1/%s/", s.djangoURL, endpoint) - } + url := fmt.Sprintf("%s/api/v1/%s/", s.djangoURL, endpoint) token := s.accessToken if token == "" { token = s.masterPass } - req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - client := &http.Client{Timeout: 30 * time.Second} - resp, err := client.Do(req) - if err != nil { - log.Printf("Failed to fetch %s: %v", endpoint, err) - return - } - defer resp.Body.Close() + allData := []json.RawMessage{} - bodyBytes, _ := io.ReadAll(resp.Body) + for url != "" { + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - if err := json.Unmarshal(bodyBytes, target); err != nil { - var paginated struct { - Results json.RawMessage `json:"results"` + resp, err := client.Do(req) + if err != nil { + log.Printf("Failed to fetch %s: %v", endpoint, err) + return } - if err2 := json.Unmarshal(bodyBytes, &paginated); err2 == nil && paginated.Results != nil { - if err3 := json.Unmarshal(paginated.Results, target); err3 != nil { - log.Printf("Failed to decode paginated %s: %v", endpoint, err3) + + bodyBytes, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + if s.IsTokenExpired(bodyBytes) { + log.Printf("Token expired, refreshing...") + if err := s.RefreshAccessToken(); err == nil { + token = s.accessToken + continue + } + return + } + + var paginated struct { + Results json.RawMessage `json:"results"` + Next *string `json:"next"` + Previous *string `json:"previous"` + Count int `json:"count"` + } + + if err := json.Unmarshal(bodyBytes, &paginated); err == nil && paginated.Results != nil { + var items []json.RawMessage + if err := json.Unmarshal(paginated.Results, &items); err == nil { + allData = append(allData, items...) + } + if paginated.Next != nil && *paginated.Next != "" { + url = *paginated.Next + } else { + url = "" } } else { - log.Printf("Failed to decode %s: %v", endpoint, err) + var items []json.RawMessage + if err := json.Unmarshal(bodyBytes, &items); err == nil { + allData = append(allData, items...) + } + url = "" } } - log.Printf("Synchronized %s from cloud", endpoint) + if len(allData) > 0 { + combined, _ := json.Marshal(allData) + json.Unmarshal(combined, target) + } + + log.Printf("Synchronized %d %s from cloud", len(allData), endpoint) +} + +func (s *Syncer) fetchFromDjango(endpoint string, target interface{}) { + s.fetchAllFromDjango(endpoint, target) } func (s *Syncer) fetchFromDjangoRaw(endpoint string) []byte { @@ -866,6 +894,75 @@ func (s *Syncer) sendMultipartRequest(method, endpoint string, data map[string]i return nil, fmt.Errorf("django returned status: %d | Response: %s", resp.StatusCode, string(respBody)) } +func (s *Syncer) SyncProductsOnly() { + log.Println("Starting products-only sync...") + + var products []models.Product + s.fetchAllFromDjango("products", &products) + log.Printf("Fetched %d products from Django", len(products)) + + // Build a map of cloud products by ID + cloudMap := make(map[uint]models.Product) + for _, p := range products { + cloudMap[p.ID] = p + } + + // Get all local products + var localProducts []models.Product + s.repo.GetProductsAll(&localProducts) + + localMap := make(map[uint]models.Product) + for _, p := range localProducts { + localMap[p.ID] = p + } + + created := 0 + updated := 0 + failed := 0 + for _, cp := range products { + cp.Sincronizado = true + if cp.UUID == "" { + if lp, exists := localMap[cp.ID]; exists && lp.UUID != "" { + cp.UUID = lp.UUID + } else { + cp.UUID = uuid.New().String() + } + } + + var err error + if _, exists := localMap[cp.ID]; exists { + err = s.repo.SaveProduct(&cp) + if err == nil { + updated++ + } + } else { + err = s.repo.CreateProduct(&cp) + if err == nil { + created++ + } + } + if err != nil { + failed++ + log.Printf("ERROR syncing product %d (%s): %v", cp.ID, cp.Name, err) + } + } + + // Delete local products that no longer exist in cloud + deleted := 0 + for _, lp := range localProducts { + if _, exists := cloudMap[lp.ID]; !exists { + if err := s.repo.DeleteByID("Product", lp.ID); err != nil { + log.Printf("ERROR deleting product %d: %v", lp.ID, err) + } else { + deleted++ + log.Printf("Deleted local product %d (no longer in cloud)", lp.ID) + } + } + } + + log.Printf("Products sync complete: %d created, %d updated, %d deleted, %d failed", created, updated, deleted, failed) +} + func (s *Syncer) syncUsers() { // Optional: logic for background sync of users if needed } diff --git a/rrbec_server/main b/rrbec_server/main deleted file mode 100755 index 9775b0e..0000000 Binary files a/rrbec_server/main and /dev/null differ diff --git a/rrbec_server/rrbec.db b/rrbec_server/rrbec.db index e799f14..a554a62 100644 Binary files a/rrbec_server/rrbec.db and b/rrbec_server/rrbec.db differ diff --git a/rrbec_server/server b/rrbec_server/server deleted file mode 100755 index 4ae3949..0000000 Binary files a/rrbec_server/server and /dev/null differ diff --git a/rrbec_server/server-linux b/rrbec_server/server-linux deleted file mode 100755 index 1b25d1f..0000000 Binary files a/rrbec_server/server-linux and /dev/null differ diff --git a/rrbec_server/server-windows.exe b/rrbec_server/server-windows.exe deleted file mode 100755 index 33b579c..0000000 Binary files a/rrbec_server/server-windows.exe and /dev/null differ