From d5216e7994d81a375b3e4b6d225d9a784e6d667b Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 11 Jun 2026 15:57:19 +0700 Subject: [PATCH 1/2] Fix purchase analytic zero for group by today --- internal/repository/analytics_repository.go | 120 ++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/internal/repository/analytics_repository.go b/internal/repository/analytics_repository.go index 5f90e0f..c03ee0f 100644 --- a/internal/repository/analytics_repository.go +++ b/internal/repository/analytics_repository.go @@ -144,6 +144,9 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or outletName = &outlet.Name } } + if outletID == nil { + return r.getPurchaseOrderPurchasingAnalytics(ctx, organizationID, dateFrom, dateTo, groupBy) + } summaryQuery := r.db.WithContext(ctx). Table("inventory_movements im"). @@ -276,6 +279,123 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or }, nil } +func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx context.Context, organizationID uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.PurchasingAnalytics, error) { + var summary entities.PurchasingSummary + summaryQuery := r.db.WithContext(ctx). + Table("purchase_orders po"). + Select(` + COALESCE(SUM(poi.amount), 0) as total_purchases, + COUNT(DISTINCT po.id) as total_purchase_orders, + COALESCE(SUM(poi.quantity), 0) as total_quantity, + CASE + WHEN COUNT(DISTINCT po.id) > 0 + THEN COALESCE(SUM(poi.amount), 0) / COUNT(DISTINCT po.id) + ELSE 0 + END as average_purchase_order_value, + COUNT(DISTINCT poi.ingredient_id) as total_ingredients, + COUNT(DISTINCT po.vendor_id) as total_vendors + `). + Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id"). + Where("po.organization_id = ?", organizationID). + Where("po.status != ?", "cancelled"). + Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo) + + if err := summaryQuery.Scan(&summary).Error; err != nil { + return nil, err + } + + var dateFormat string + switch groupBy { + case "hour": + dateFormat = "DATE_TRUNC('hour', po.created_at)" + case "week": + dateFormat = "DATE_TRUNC('week', po.transaction_date::timestamp)" + case "month": + dateFormat = "DATE_TRUNC('month', po.transaction_date::timestamp)" + default: + dateFormat = "DATE_TRUNC('day', po.transaction_date::timestamp)" + } + + var data []entities.PurchasingAnalyticsData + dataQuery := r.db.WithContext(ctx). + Table("purchase_orders po"). + Select(` + `+dateFormat+` as date, + COALESCE(SUM(poi.amount), 0) as purchases, + COUNT(DISTINCT po.id) as purchase_orders, + COALESCE(SUM(poi.quantity), 0) as quantity, + COUNT(DISTINCT poi.ingredient_id) as ingredients, + COUNT(DISTINCT po.vendor_id) as vendors + `). + Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id"). + Where("po.organization_id = ?", organizationID). + Where("po.status != ?", "cancelled"). + Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). + Group(dateFormat). + Order(dateFormat) + + if err := dataQuery.Scan(&data).Error; err != nil { + return nil, err + } + + var ingredientData []entities.PurchasingIngredientData + ingredientQuery := r.db.WithContext(ctx). + Table("purchase_order_items poi"). + Select(` + i.id as ingredient_id, + i.name as ingredient_name, + COALESCE(SUM(poi.quantity), 0) as quantity, + COALESCE(SUM(poi.amount), 0) as total_cost, + CASE + WHEN SUM(poi.quantity) > 0 + THEN COALESCE(SUM(poi.amount), 0) / SUM(poi.quantity) + ELSE 0 + END as average_unit_cost, + COUNT(DISTINCT po.id) as purchase_order_count + `). + Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id"). + Joins("JOIN ingredients i ON poi.ingredient_id = i.id"). + Where("po.organization_id = ?", organizationID). + Where("po.status != ?", "cancelled"). + Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). + Group("i.id, i.name"). + Order("total_cost DESC") + + if err := ingredientQuery.Scan(&ingredientData).Error; err != nil { + return nil, err + } + + var vendorData []entities.PurchasingVendorData + vendorQuery := r.db.WithContext(ctx). + Table("purchase_orders po"). + Select(` + v.id as vendor_id, + v.name as vendor_name, + COALESCE(SUM(poi.amount), 0) as total_cost, + COUNT(DISTINCT po.id) as purchase_order_count, + COUNT(DISTINCT poi.ingredient_id) as ingredient_count, + COALESCE(SUM(poi.quantity), 0) as quantity + `). + Joins("JOIN vendors v ON po.vendor_id = v.id"). + Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id"). + Where("po.organization_id = ?", organizationID). + Where("po.status != ?", "cancelled"). + Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). + Group("v.id, v.name"). + Order("total_cost DESC") + + if err := vendorQuery.Scan(&vendorData).Error; err != nil { + return nil, err + } + + return &entities.PurchasingAnalytics{ + Summary: summary, + Data: data, + IngredientData: ingredientData, + VendorData: vendorData, + }, nil +} + func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) { var results []*entities.ProductAnalytics From f4172fcea71003b577365e2c6c02ea8ec42bb72b Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 11 Jun 2026 16:27:50 +0700 Subject: [PATCH 2/2] Fix purchase analytic by outlet --- internal/repository/analytics_repository.go | 162 +++----------------- 1 file changed, 24 insertions(+), 138 deletions(-) diff --git a/internal/repository/analytics_repository.go b/internal/repository/analytics_repository.go index c03ee0f..9667788 100644 --- a/internal/repository/analytics_repository.go +++ b/internal/repository/analytics_repository.go @@ -124,7 +124,6 @@ func (r *AnalyticsRepositoryImpl) GetSalesAnalytics(ctx context.Context, organiz } func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.PurchasingAnalytics, error) { - var summary entities.PurchasingSummary var outletName *string if outletID != nil { @@ -144,142 +143,10 @@ func (r *AnalyticsRepositoryImpl) GetPurchasingAnalytics(ctx context.Context, or outletName = &outlet.Name } } - if outletID == nil { - return r.getPurchaseOrderPurchasingAnalytics(ctx, organizationID, dateFrom, dateTo, groupBy) - } - - summaryQuery := r.db.WithContext(ctx). - Table("inventory_movements im"). - Select(` - COALESCE(SUM(im.total_cost), 0) as total_purchases, - COUNT(DISTINCT im.reference_id) as total_purchase_orders, - COALESCE(SUM(im.quantity), 0) as total_quantity, - CASE - WHEN COUNT(DISTINCT im.reference_id) > 0 - THEN COALESCE(SUM(im.total_cost), 0) / COUNT(DISTINCT im.reference_id) - ELSE 0 - END as average_purchase_order_value, - COUNT(DISTINCT im.item_id) as total_ingredients, - COUNT(DISTINCT po.vendor_id) as total_vendors - `). - Joins("LEFT JOIN purchase_orders po ON im.reference_id = po.id"). - Where("im.organization_id = ?", organizationID). - Where("im.movement_type = ?", entities.InventoryMovementTypePurchase). - Where("im.item_type = ?", "INGREDIENT"). - Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder). - Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo) - - summaryQuery = r.resolveOutletID(summaryQuery, outletID, "im.outlet_id") - - if err := summaryQuery.Scan(&summary).Error; err != nil { - return nil, err - } - - var dateFormat string - switch groupBy { - case "hour": - dateFormat = "DATE_TRUNC('hour', im.created_at)" - case "week": - dateFormat = "DATE_TRUNC('week', im.created_at)" - case "month": - dateFormat = "DATE_TRUNC('month', im.created_at)" - default: - dateFormat = "DATE_TRUNC('day', im.created_at)" - } - - var data []entities.PurchasingAnalyticsData - dataQuery := r.db.WithContext(ctx). - Table("inventory_movements im"). - Select(` - `+dateFormat+` as date, - COALESCE(SUM(im.total_cost), 0) as purchases, - COUNT(DISTINCT im.reference_id) as purchase_orders, - COALESCE(SUM(im.quantity), 0) as quantity, - COUNT(DISTINCT im.item_id) as ingredients, - COUNT(DISTINCT po.vendor_id) as vendors - `). - Joins("LEFT JOIN purchase_orders po ON im.reference_id = po.id"). - Where("im.organization_id = ?", organizationID). - Where("im.movement_type = ?", entities.InventoryMovementTypePurchase). - Where("im.item_type = ?", "INGREDIENT"). - Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder). - Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo). - Group(dateFormat). - Order(dateFormat) - - dataQuery = r.resolveOutletID(dataQuery, outletID, "im.outlet_id") - - if err := dataQuery.Scan(&data).Error; err != nil { - return nil, err - } - - var ingredientData []entities.PurchasingIngredientData - ingredientQuery := r.db.WithContext(ctx). - Table("inventory_movements im"). - Select(` - i.id as ingredient_id, - i.name as ingredient_name, - COALESCE(SUM(im.quantity), 0) as quantity, - COALESCE(SUM(im.total_cost), 0) as total_cost, - CASE - WHEN SUM(im.quantity) > 0 - THEN COALESCE(SUM(im.total_cost), 0) / SUM(im.quantity) - ELSE 0 - END as average_unit_cost, - COUNT(DISTINCT im.reference_id) as purchase_order_count - `). - Joins("JOIN ingredients i ON im.item_id = i.id"). - Where("im.organization_id = ?", organizationID). - Where("im.movement_type = ?", entities.InventoryMovementTypePurchase). - Where("im.item_type = ?", "INGREDIENT"). - Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder). - Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo). - Group("i.id, i.name"). - Order("total_cost DESC") - - ingredientQuery = r.resolveOutletID(ingredientQuery, outletID, "im.outlet_id") - - if err := ingredientQuery.Scan(&ingredientData).Error; err != nil { - return nil, err - } - - var vendorData []entities.PurchasingVendorData - vendorQuery := r.db.WithContext(ctx). - Table("inventory_movements im"). - Select(` - v.id as vendor_id, - v.name as vendor_name, - COALESCE(SUM(im.total_cost), 0) as total_cost, - COUNT(DISTINCT im.reference_id) as purchase_order_count, - COUNT(DISTINCT im.item_id) as ingredient_count, - COALESCE(SUM(im.quantity), 0) as quantity - `). - Joins("JOIN purchase_orders po ON im.reference_id = po.id"). - Joins("JOIN vendors v ON po.vendor_id = v.id"). - Where("im.organization_id = ?", organizationID). - Where("im.movement_type = ?", entities.InventoryMovementTypePurchase). - Where("im.item_type = ?", "INGREDIENT"). - Where("im.reference_type = ?", entities.InventoryMovementReferenceTypePurchaseOrder). - Where("im.created_at >= ? AND im.created_at <= ?", dateFrom, dateTo). - Group("v.id, v.name"). - Order("total_cost DESC") - - vendorQuery = r.resolveOutletID(vendorQuery, outletID, "im.outlet_id") - - if err := vendorQuery.Scan(&vendorData).Error; err != nil { - return nil, err - } - - return &entities.PurchasingAnalytics{ - OutletName: outletName, - Summary: summary, - Data: data, - IngredientData: ingredientData, - VendorData: vendorData, - }, nil + return r.getPurchaseOrderPurchasingAnalytics(ctx, organizationID, outletID, outletName, dateFrom, dateTo, groupBy) } -func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx context.Context, organizationID uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.PurchasingAnalytics, error) { +func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, outletName *string, dateFrom, dateTo time.Time, groupBy string) (*entities.PurchasingAnalytics, error) { var summary entities.PurchasingSummary summaryQuery := r.db.WithContext(ctx). Table("purchase_orders po"). @@ -292,13 +159,16 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex THEN COALESCE(SUM(poi.amount), 0) / COUNT(DISTINCT po.id) ELSE 0 END as average_purchase_order_value, - COUNT(DISTINCT poi.ingredient_id) as total_ingredients, + COUNT(DISTINCT i.id) as total_ingredients, COUNT(DISTINCT po.vendor_id) as total_vendors `). Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id"). + Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id"). + Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Where("po.organization_id = ?", organizationID). Where("po.status != ?", "cancelled"). Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo) + summaryQuery = r.applyPurchaseOrderItemOutletFilter(summaryQuery, outletID) if err := summaryQuery.Scan(&summary).Error; err != nil { return nil, err @@ -324,15 +194,18 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex COALESCE(SUM(poi.amount), 0) as purchases, COUNT(DISTINCT po.id) as purchase_orders, COALESCE(SUM(poi.quantity), 0) as quantity, - COUNT(DISTINCT poi.ingredient_id) as ingredients, + COUNT(DISTINCT i.id) as ingredients, COUNT(DISTINCT po.vendor_id) as vendors `). Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id"). + Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id"). + Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Where("po.organization_id = ?", organizationID). Where("po.status != ?", "cancelled"). Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). Group(dateFormat). Order(dateFormat) + dataQuery = r.applyPurchaseOrderItemOutletFilter(dataQuery, outletID) if err := dataQuery.Scan(&data).Error; err != nil { return nil, err @@ -355,11 +228,13 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex `). Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id"). Joins("JOIN ingredients i ON poi.ingredient_id = i.id"). + Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Where("po.organization_id = ?", organizationID). Where("po.status != ?", "cancelled"). Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). Group("i.id, i.name"). Order("total_cost DESC") + ingredientQuery = r.applyPurchaseOrderItemOutletFilter(ingredientQuery, outletID) if err := ingredientQuery.Scan(&ingredientData).Error; err != nil { return nil, err @@ -373,22 +248,26 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex v.name as vendor_name, COALESCE(SUM(poi.amount), 0) as total_cost, COUNT(DISTINCT po.id) as purchase_order_count, - COUNT(DISTINCT poi.ingredient_id) as ingredient_count, + COUNT(DISTINCT i.id) as ingredient_count, COALESCE(SUM(poi.quantity), 0) as quantity `). Joins("JOIN vendors v ON po.vendor_id = v.id"). Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id"). + Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id"). + Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Where("po.organization_id = ?", organizationID). Where("po.status != ?", "cancelled"). Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). Group("v.id, v.name"). Order("total_cost DESC") + vendorQuery = r.applyPurchaseOrderItemOutletFilter(vendorQuery, outletID) if err := vendorQuery.Scan(&vendorData).Error; err != nil { return nil, err } return &entities.PurchasingAnalytics{ + OutletName: outletName, Summary: summary, Data: data, IngredientData: ingredientData, @@ -396,6 +275,13 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex }, nil } +func (r *AnalyticsRepositoryImpl) applyPurchaseOrderItemOutletFilter(query *gorm.DB, outletID *uuid.UUID) *gorm.DB { + if outletID == nil { + return query + } + return query.Where("(i.outlet_id = ? OR u.outlet_id = ?)", *outletID, *outletID) +} + func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) { var results []*entities.ProductAnalytics