From 793919cf101b2ece2d16f0b77ce1281dbeb59375 Mon Sep 17 00:00:00 2001 From: Efril Date: Tue, 23 Jun 2026 22:18:16 +0700 Subject: [PATCH] feat: add outlet name at analytic response and new overview dashboard --- internal/contract/analytics_contract.go | 23 +++++--- internal/entities/analytics.go | 3 ++ internal/models/analytics.go | 23 +++++--- internal/processor/analytics_processor.go | 37 ++++++++++--- internal/repository/analytics_repository.go | 52 +++++++++++++++++++ internal/transformer/analytics_transformer.go | 23 +++++--- 6 files changed, 137 insertions(+), 24 deletions(-) diff --git a/internal/contract/analytics_contract.go b/internal/contract/analytics_contract.go index 04e2d61..1da3659 100644 --- a/internal/contract/analytics_contract.go +++ b/internal/contract/analytics_contract.go @@ -18,6 +18,7 @@ type PaymentMethodAnalyticsRequest struct { type PaymentMethodAnalyticsResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` DateFrom time.Time `json:"date_from"` DateTo time.Time `json:"date_to"` GroupBy string `json:"group_by"` @@ -54,6 +55,7 @@ type SalesAnalyticsRequest struct { type SalesAnalyticsResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` DateFrom time.Time `json:"date_from"` DateTo time.Time `json:"date_to"` GroupBy string `json:"group_by"` @@ -161,6 +163,7 @@ type ProductAnalyticsRequest struct { type ProductAnalyticsResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` DateFrom time.Time `json:"date_from"` DateTo time.Time `json:"date_to"` Data []ProductAnalyticsData `json:"data"` @@ -198,6 +201,7 @@ type ProductAnalyticsPerCategoryRequest struct { type ProductAnalyticsPerCategoryResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` DateFrom time.Time `json:"date_from"` DateTo time.Time `json:"date_to"` Data []ProductAnalyticsPerCategoryData `json:"data"` @@ -227,6 +231,7 @@ type DashboardAnalyticsRequest struct { type DashboardAnalyticsResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` DateFrom time.Time `json:"date_from"` DateTo time.Time `json:"date_to"` Overview DashboardOverview `json:"overview"` @@ -237,12 +242,15 @@ type DashboardAnalyticsResponse struct { // DashboardOverview represents the overview data for dashboard type DashboardOverview struct { - TotalSales float64 `json:"total_sales"` - TotalOrders int64 `json:"total_orders"` - AverageOrderValue float64 `json:"average_order_value"` - TotalCustomers int64 `json:"total_customers"` - VoidedOrders int64 `json:"voided_orders"` - RefundedOrders int64 `json:"refunded_orders"` + TotalSales float64 `json:"total_sales"` + TotalOrders int64 `json:"total_orders"` + AverageOrderValue float64 `json:"average_order_value"` + TotalCustomers int64 `json:"total_customers"` + VoidedOrders int64 `json:"voided_orders"` + RefundedOrders int64 `json:"refunded_orders"` + TotalItemSold int64 `json:"total_item_sold"` + TotalLowStock int64 `json:"total_low_stock"` + TotalProductActive int64 `json:"total_product_active"` } type ProfitLossAnalyticsRequest struct { @@ -256,6 +264,7 @@ type ProfitLossAnalyticsRequest struct { type ProfitLossAnalyticsResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` DateFrom time.Time `json:"date_from"` DateTo time.Time `json:"date_to"` GroupBy string `json:"group_by"` @@ -349,6 +358,7 @@ type ExclusiveSummaryMTDRequest struct { type ExclusiveSummaryPeriodResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` Period ExclusiveSummaryPeriodRange `json:"period"` Summary ExclusiveSummaryPeriodSummary `json:"summary"` Reimburse ExclusiveSummaryReimburse `json:"reimburse"` @@ -408,6 +418,7 @@ type ExclusiveSummaryDailyTransaction struct { type ExclusiveSummaryMonthlyResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` Month string `json:"month"` Summary ExclusiveSummaryMonthlySummary `json:"summary"` Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"` diff --git a/internal/entities/analytics.go b/internal/entities/analytics.go index 5155cf2..bfcf8b7 100644 --- a/internal/entities/analytics.go +++ b/internal/entities/analytics.go @@ -120,6 +120,9 @@ type DashboardOverview struct { TotalCustomers int64 `json:"total_customers"` VoidedOrders int64 `json:"voided_orders"` RefundedOrders int64 `json:"refunded_orders"` + TotalItemSold int64 `json:"total_item_sold"` + TotalLowStock int64 `json:"total_low_stock"` + TotalProductActive int64 `json:"total_product_active"` } type ProfitLossAnalytics struct { diff --git a/internal/models/analytics.go b/internal/models/analytics.go index fe51e64..446ec92 100644 --- a/internal/models/analytics.go +++ b/internal/models/analytics.go @@ -19,6 +19,7 @@ type PaymentMethodAnalyticsRequest struct { type PaymentMethodAnalyticsResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` DateFrom time.Time `json:"date_from"` DateTo time.Time `json:"date_to"` GroupBy string `json:"group_by"` @@ -58,6 +59,7 @@ type SalesAnalyticsRequest struct { type SalesAnalyticsResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` DateFrom time.Time `json:"date_from"` DateTo time.Time `json:"date_to"` GroupBy string `json:"group_by"` @@ -171,6 +173,7 @@ type ProductAnalyticsRequest struct { type ProductAnalyticsResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` DateFrom time.Time `json:"date_from"` DateTo time.Time `json:"date_to"` Data []ProductAnalyticsData `json:"data"` @@ -208,6 +211,7 @@ type ProductAnalyticsPerCategoryRequest struct { type ProductAnalyticsPerCategoryResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` DateFrom time.Time `json:"date_from"` DateTo time.Time `json:"date_to"` Data []ProductAnalyticsPerCategoryData `json:"data"` @@ -237,6 +241,7 @@ type DashboardAnalyticsRequest struct { type DashboardAnalyticsResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` DateFrom time.Time `json:"date_from"` DateTo time.Time `json:"date_to"` Overview DashboardOverview `json:"overview"` @@ -247,12 +252,15 @@ type DashboardAnalyticsResponse struct { // DashboardOverview represents the overview data for dashboard type DashboardOverview struct { - TotalSales float64 `json:"total_sales"` - TotalOrders int64 `json:"total_orders"` - AverageOrderValue float64 `json:"average_order_value"` - TotalCustomers int64 `json:"total_customers"` - VoidedOrders int64 `json:"voided_orders"` - RefundedOrders int64 `json:"refunded_orders"` + TotalSales float64 `json:"total_sales"` + TotalOrders int64 `json:"total_orders"` + AverageOrderValue float64 `json:"average_order_value"` + TotalCustomers int64 `json:"total_customers"` + VoidedOrders int64 `json:"voided_orders"` + RefundedOrders int64 `json:"refunded_orders"` + TotalItemSold int64 `json:"total_item_sold"` + TotalLowStock int64 `json:"total_low_stock"` + TotalProductActive int64 `json:"total_product_active"` } type ProfitLossAnalyticsRequest struct { @@ -266,6 +274,7 @@ type ProfitLossAnalyticsRequest struct { type ProfitLossAnalyticsResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` DateFrom time.Time `json:"date_from"` DateTo time.Time `json:"date_to"` GroupBy string `json:"group_by"` @@ -359,6 +368,7 @@ type ExclusiveSummaryMTDRequest struct { type ExclusiveSummaryPeriodResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` Period ExclusiveSummaryPeriodRange `json:"period"` Summary ExclusiveSummaryPeriodSummary `json:"summary"` Reimburse ExclusiveSummaryReimburse `json:"reimburse"` @@ -418,6 +428,7 @@ type ExclusiveSummaryDailyTransaction struct { type ExclusiveSummaryMonthlyResponse struct { OrganizationID uuid.UUID `json:"organization_id"` OutletID *uuid.UUID `json:"outlet_id,omitempty"` + OutletName *string `json:"outlet_name,omitempty"` Month string `json:"month"` Summary ExclusiveSummaryMonthlySummary `json:"summary"` Periods []ExclusiveSummaryMonthlyPeriod `json:"periods"` diff --git a/internal/processor/analytics_processor.go b/internal/processor/analytics_processor.go index 728f06a..20e758c 100644 --- a/internal/processor/analytics_processor.go +++ b/internal/processor/analytics_processor.go @@ -9,6 +9,8 @@ import ( "apskel-pos-be/internal/entities" "apskel-pos-be/internal/models" "apskel-pos-be/internal/repository" + + "github.com/google/uuid" ) type AnalyticsProcessor interface { @@ -36,6 +38,18 @@ func NewAnalyticsProcessorImpl(analyticsRepo repository.AnalyticsRepository, exp } } +// resolveOutletName fetches the outlet name from the database if outletID is provided +func (p *AnalyticsProcessorImpl) resolveOutletName(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) *string { + if outletID == nil { + return nil + } + name, err := p.analyticsRepo.GetOutletName(ctx, organizationID, *outletID) + if err != nil || name == "" { + return nil + } + return &name +} + func (p *AnalyticsProcessorImpl) GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error) { if req.DateFrom.After(req.DateTo) { return nil, fmt.Errorf("date_from cannot be after date_to") @@ -90,6 +104,7 @@ func (p *AnalyticsProcessorImpl) GetPaymentMethodAnalytics(ctx context.Context, return &models.PaymentMethodAnalyticsResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, + OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID), DateFrom: req.DateFrom, DateTo: req.DateTo, GroupBy: req.GroupBy, @@ -164,6 +179,7 @@ func (p *AnalyticsProcessorImpl) GetSalesAnalytics(ctx context.Context, req *mod return &models.SalesAnalyticsResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, + OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID), DateFrom: req.DateFrom, DateTo: req.DateTo, GroupBy: req.GroupBy, @@ -295,6 +311,7 @@ func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *m return &models.ProductAnalyticsResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, + OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID), DateFrom: req.DateFrom, DateTo: req.DateTo, Data: resultData, @@ -332,6 +349,7 @@ func (p *AnalyticsProcessorImpl) GetProductAnalyticsPerCategory(ctx context.Cont return &models.ProductAnalyticsPerCategoryResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, + OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID), DateFrom: req.DateFrom, DateTo: req.DateTo, Data: resultData, @@ -393,15 +411,19 @@ func (p *AnalyticsProcessorImpl) GetDashboardAnalytics(ctx context.Context, req return &models.DashboardAnalyticsResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, + OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID), DateFrom: req.DateFrom, DateTo: req.DateTo, Overview: models.DashboardOverview{ - TotalSales: overview.TotalSales, - TotalOrders: overview.TotalOrders, - AverageOrderValue: overview.AverageOrderValue, - TotalCustomers: overview.TotalCustomers, - VoidedOrders: overview.VoidedOrders, - RefundedOrders: overview.RefundedOrders, + TotalSales: overview.TotalSales, + TotalOrders: overview.TotalOrders, + AverageOrderValue: overview.AverageOrderValue, + TotalCustomers: overview.TotalCustomers, + VoidedOrders: overview.VoidedOrders, + RefundedOrders: overview.RefundedOrders, + TotalItemSold: overview.TotalItemSold, + TotalLowStock: overview.TotalLowStock, + TotalProductActive: overview.TotalProductActive, }, TopProducts: topProducts.Data, PaymentMethods: paymentMethods.Data, @@ -607,6 +629,7 @@ func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req return &models.ProfitLossAnalyticsResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, + OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID), DateFrom: req.DateFrom, DateTo: req.DateTo, GroupBy: req.GroupBy, @@ -721,6 +744,7 @@ func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context, return &models.ExclusiveSummaryMonthlyResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, + OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID), Month: monthStart.Format("2006-01"), Summary: models.ExclusiveSummaryMonthlySummary{ TotalSales: fullPeriod.Summary.Sales, @@ -795,6 +819,7 @@ func (p *AnalyticsProcessorImpl) buildExclusiveSummaryPeriod(ctx context.Context return &models.ExclusiveSummaryPeriodResponse{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, + OutletName: p.resolveOutletName(ctx, req.OrganizationID, req.OutletID), Period: models.ExclusiveSummaryPeriodRange{ DateFrom: req.DateFrom, DateTo: req.DateTo, diff --git a/internal/repository/analytics_repository.go b/internal/repository/analytics_repository.go index f2b37b7..66deaa2 100644 --- a/internal/repository/analytics_repository.go +++ b/internal/repository/analytics_repository.go @@ -21,6 +21,7 @@ type AnalyticsRepository interface { GetProfitLossAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.ProfitLossAnalytics, error) GetExclusiveSummaryAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (*entities.ExclusiveSummaryAnalytics, error) GetExclusiveSummaryBankBalances(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error) + GetOutletName(ctx context.Context, organizationID uuid.UUID, outletID uuid.UUID) (string, error) } type AnalyticsRepositoryImpl struct { @@ -40,6 +41,22 @@ func (r *AnalyticsRepositoryImpl) resolveOutletID(query *gorm.DB, outletID *uuid return query } +func (r *AnalyticsRepositoryImpl) GetOutletName(ctx context.Context, organizationID uuid.UUID, outletID uuid.UUID) (string, error) { + var outlet struct { + Name string + } + result := r.db.WithContext(ctx). + Table("outlets"). + Select("name"). + Where("id = ? AND organization_id = ?", outletID, organizationID). + Limit(1). + Scan(&outlet) + if result.Error != nil { + return "", result.Error + } + return outlet.Name, nil +} + func purchaseOrderItemTotalAmountSQL() string { return "CASE WHEN pc.type = '" + string(entities.PurchaseCategoryTypeRawMaterial) + "' THEN COALESCE(poi.quantity, 0) * poi.amount ELSE poi.amount END" } @@ -471,6 +488,41 @@ func (r *AnalyticsRepositoryImpl) GetDashboardOverview(ctx context.Context, orga return nil, err } + // Total item sold (sum of order_items quantity for completed orders in date range) + var totalItemSold int64 + itemQuery := r.db.WithContext(ctx). + Table("order_items oi"). + Select("COALESCE(SUM(oi.quantity), 0)"). + Joins("JOIN orders o ON o.id = oi.order_id"). + Where("o.organization_id = ?", organizationID). + Where("o.is_void = false AND o.is_refund = false AND o.payment_status = 'completed'"). + Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo) + itemQuery = r.resolveOutletID(itemQuery, outletID, "o.outlet_id") + itemQuery.Scan(&totalItemSold) + result.TotalItemSold = totalItemSold + + // Total low stock (inventory where quantity <= reorder_level) + var totalLowStock int64 + lowStockQuery := r.db.WithContext(ctx). + Table("inventory i"). + Select("COUNT(i.id)"). + Joins("JOIN products p ON p.id = i.product_id"). + Where("p.organization_id = ?", organizationID). + Where("i.quantity <= i.reorder_level") + lowStockQuery = r.resolveOutletID(lowStockQuery, outletID, "i.outlet_id") + lowStockQuery.Scan(&totalLowStock) + result.TotalLowStock = totalLowStock + + // Total active products + var totalProductActive int64 + productQuery := r.db.WithContext(ctx). + Table("products p"). + Select("COUNT(p.id)"). + Where("p.organization_id = ?", organizationID). + Where("p.is_active = true") + productQuery.Scan(&totalProductActive) + result.TotalProductActive = totalProductActive + return &result, nil } diff --git a/internal/transformer/analytics_transformer.go b/internal/transformer/analytics_transformer.go index 03deab4..f4b033e 100644 --- a/internal/transformer/analytics_transformer.go +++ b/internal/transformer/analytics_transformer.go @@ -66,6 +66,7 @@ func PaymentMethodAnalyticsModelToContract(resp *models.PaymentMethodAnalyticsRe return &contract.PaymentMethodAnalyticsResponse{ OrganizationID: resp.OrganizationID, OutletID: resp.OutletID, + OutletName: resp.OutletName, DateFrom: resp.DateFrom, DateTo: resp.DateTo, GroupBy: resp.GroupBy, @@ -122,6 +123,7 @@ func SalesAnalyticsModelToContract(resp *models.SalesAnalyticsResponse) *contrac return &contract.SalesAnalyticsResponse{ OrganizationID: resp.OrganizationID, OutletID: resp.OutletID, + OutletName: resp.OutletName, DateFrom: resp.DateFrom, DateTo: resp.DateTo, GroupBy: resp.GroupBy, @@ -285,6 +287,7 @@ func ProductAnalyticsModelToContract(resp *models.ProductAnalyticsResponse) *con return &contract.ProductAnalyticsResponse{ OrganizationID: resp.OrganizationID, OutletID: resp.OutletID, + OutletName: resp.OutletName, DateFrom: resp.DateFrom, DateTo: resp.DateTo, Data: data, @@ -337,6 +340,7 @@ func ProductAnalyticsPerCategoryModelToContract(resp *models.ProductAnalyticsPer return &contract.ProductAnalyticsPerCategoryResponse{ OrganizationID: resp.OrganizationID, OutletID: resp.OutletID, + OutletName: resp.OutletName, DateFrom: resp.DateFrom, DateTo: resp.DateTo, Data: data, @@ -421,15 +425,19 @@ func DashboardAnalyticsModelToContract(resp *models.DashboardAnalyticsResponse) return &contract.DashboardAnalyticsResponse{ OrganizationID: resp.OrganizationID, OutletID: resp.OutletID, + OutletName: resp.OutletName, DateFrom: resp.DateFrom, DateTo: resp.DateTo, Overview: contract.DashboardOverview{ - TotalSales: resp.Overview.TotalSales, - TotalOrders: resp.Overview.TotalOrders, - AverageOrderValue: resp.Overview.AverageOrderValue, - TotalCustomers: resp.Overview.TotalCustomers, - VoidedOrders: resp.Overview.VoidedOrders, - RefundedOrders: resp.Overview.RefundedOrders, + TotalSales: resp.Overview.TotalSales, + TotalOrders: resp.Overview.TotalOrders, + AverageOrderValue: resp.Overview.AverageOrderValue, + TotalCustomers: resp.Overview.TotalCustomers, + VoidedOrders: resp.Overview.VoidedOrders, + RefundedOrders: resp.Overview.RefundedOrders, + TotalItemSold: resp.Overview.TotalItemSold, + TotalLowStock: resp.Overview.TotalLowStock, + TotalProductActive: resp.Overview.TotalProductActive, }, TopProducts: topProducts, PaymentMethods: paymentMethods, @@ -519,6 +527,7 @@ func ProfitLossAnalyticsModelToContract(resp *models.ProfitLossAnalyticsResponse return &contract.ProfitLossAnalyticsResponse{ OrganizationID: resp.OrganizationID, OutletID: resp.OutletID, + OutletName: resp.OutletName, DateFrom: resp.DateFrom, DateTo: resp.DateTo, GroupBy: resp.GroupBy, @@ -664,6 +673,7 @@ func ExclusiveSummaryPeriodModelToContract(resp *models.ExclusiveSummaryPeriodRe return &contract.ExclusiveSummaryPeriodResponse{ OrganizationID: resp.OrganizationID, OutletID: resp.OutletID, + OutletName: resp.OutletName, Period: contract.ExclusiveSummaryPeriodRange{ DateFrom: resp.Period.DateFrom, DateTo: resp.Period.DateTo, @@ -726,6 +736,7 @@ func ExclusiveSummaryMonthlyModelToContract(resp *models.ExclusiveSummaryMonthly return &contract.ExclusiveSummaryMonthlyResponse{ OrganizationID: resp.OrganizationID, OutletID: resp.OutletID, + OutletName: resp.OutletName, Month: resp.Month, Summary: contract.ExclusiveSummaryMonthlySummary{ TotalSales: resp.Summary.TotalSales,