654 lines
21 KiB
Go
654 lines
21 KiB
Go
package processor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"apskel-pos-be/internal/models"
|
|
"apskel-pos-be/internal/repository"
|
|
)
|
|
|
|
type AnalyticsProcessor interface {
|
|
GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error)
|
|
GetSalesAnalytics(ctx context.Context, req *models.SalesAnalyticsRequest) (*models.SalesAnalyticsResponse, error)
|
|
GetPurchasingAnalytics(ctx context.Context, req *models.PurchasingAnalyticsRequest) (*models.PurchasingAnalyticsResponse, error)
|
|
GetProductAnalytics(ctx context.Context, req *models.ProductAnalyticsRequest) (*models.ProductAnalyticsResponse, error)
|
|
GetProductAnalyticsPerCategory(ctx context.Context, req *models.ProductAnalyticsPerCategoryRequest) (*models.ProductAnalyticsPerCategoryResponse, error)
|
|
GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error)
|
|
GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error)
|
|
}
|
|
|
|
type AnalyticsProcessorImpl struct {
|
|
analyticsRepo repository.AnalyticsRepository
|
|
expenseRepo ExpenseRepository
|
|
}
|
|
|
|
func NewAnalyticsProcessorImpl(analyticsRepo repository.AnalyticsRepository, expenseRepo ExpenseRepository) *AnalyticsProcessorImpl {
|
|
return &AnalyticsProcessorImpl{
|
|
analyticsRepo: analyticsRepo,
|
|
expenseRepo: expenseRepo,
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
analyticsData, err := p.analyticsRepo.GetPaymentMethodAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get payment method analytics: %w", err)
|
|
}
|
|
|
|
var totalAmount float64
|
|
var totalOrders int64
|
|
var totalPayments int64
|
|
|
|
for _, data := range analyticsData {
|
|
totalAmount += data.TotalAmount
|
|
totalOrders += data.OrderCount
|
|
totalPayments += data.PaymentCount
|
|
}
|
|
|
|
var averageOrderValue float64
|
|
if totalOrders > 0 {
|
|
averageOrderValue = totalAmount / float64(totalOrders)
|
|
}
|
|
|
|
// Calculate percentages
|
|
var resultData []models.PaymentMethodAnalyticsData
|
|
for _, data := range analyticsData {
|
|
var percentage float64
|
|
if totalAmount > 0 {
|
|
percentage = (data.TotalAmount / totalAmount) * 100
|
|
}
|
|
|
|
resultData = append(resultData, models.PaymentMethodAnalyticsData{
|
|
PaymentMethodID: data.PaymentMethodID,
|
|
PaymentMethodName: data.PaymentMethodName,
|
|
PaymentMethodType: data.PaymentMethodType,
|
|
TotalAmount: data.TotalAmount,
|
|
OrderCount: data.OrderCount,
|
|
PaymentCount: data.PaymentCount,
|
|
Percentage: percentage,
|
|
})
|
|
}
|
|
|
|
summary := models.PaymentMethodSummary{
|
|
TotalAmount: totalAmount,
|
|
TotalOrders: totalOrders,
|
|
TotalPayments: totalPayments,
|
|
AverageOrderValue: averageOrderValue,
|
|
}
|
|
|
|
return &models.PaymentMethodAnalyticsResponse{
|
|
OrganizationID: req.OrganizationID,
|
|
OutletID: req.OutletID,
|
|
DateFrom: req.DateFrom,
|
|
DateTo: req.DateTo,
|
|
GroupBy: req.GroupBy,
|
|
Summary: summary,
|
|
Data: resultData,
|
|
}, nil
|
|
}
|
|
|
|
func (p *AnalyticsProcessorImpl) GetSalesAnalytics(ctx context.Context, req *models.SalesAnalyticsRequest) (*models.SalesAnalyticsResponse, error) {
|
|
// Validate date range
|
|
if req.DateFrom.After(req.DateTo) {
|
|
return nil, fmt.Errorf("date_from cannot be after date_to")
|
|
}
|
|
|
|
// Validate groupBy
|
|
if req.GroupBy == "" {
|
|
req.GroupBy = "day"
|
|
}
|
|
|
|
// Get analytics data from repository
|
|
analyticsData, err := p.analyticsRepo.GetSalesAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get sales analytics: %w", err)
|
|
}
|
|
|
|
// Calculate summary
|
|
var totalSales float64
|
|
var totalOrders int64
|
|
var totalItems int64
|
|
var totalTax float64
|
|
var totalDiscount float64
|
|
var netSales float64
|
|
|
|
for _, data := range analyticsData {
|
|
totalSales += data.Sales
|
|
totalOrders += data.Orders
|
|
totalItems += data.Items
|
|
totalTax += data.Tax
|
|
totalDiscount += data.Discount
|
|
netSales += data.NetSales
|
|
}
|
|
|
|
var averageOrderValue float64
|
|
if totalOrders > 0 {
|
|
averageOrderValue = totalSales / float64(totalOrders)
|
|
}
|
|
|
|
// Transform data
|
|
var resultData []models.SalesAnalyticsData
|
|
for _, data := range analyticsData {
|
|
resultData = append(resultData, models.SalesAnalyticsData{
|
|
Date: data.Date,
|
|
Sales: data.Sales,
|
|
Orders: data.Orders,
|
|
Items: data.Items,
|
|
Tax: data.Tax,
|
|
Discount: data.Discount,
|
|
NetSales: data.NetSales,
|
|
})
|
|
}
|
|
|
|
summary := models.SalesSummary{
|
|
TotalSales: totalSales,
|
|
TotalOrders: totalOrders,
|
|
TotalItems: totalItems,
|
|
AverageOrderValue: averageOrderValue,
|
|
TotalTax: totalTax,
|
|
TotalDiscount: totalDiscount,
|
|
NetSales: netSales,
|
|
}
|
|
|
|
return &models.SalesAnalyticsResponse{
|
|
OrganizationID: req.OrganizationID,
|
|
OutletID: req.OutletID,
|
|
DateFrom: req.DateFrom,
|
|
DateTo: req.DateTo,
|
|
GroupBy: req.GroupBy,
|
|
Summary: summary,
|
|
Data: resultData,
|
|
}, nil
|
|
}
|
|
|
|
func (p *AnalyticsProcessorImpl) GetPurchasingAnalytics(ctx context.Context, req *models.PurchasingAnalyticsRequest) (*models.PurchasingAnalyticsResponse, error) {
|
|
if req.DateFrom.After(req.DateTo) {
|
|
return nil, fmt.Errorf("date_from cannot be after date_to")
|
|
}
|
|
|
|
if req.GroupBy == "" {
|
|
req.GroupBy = "day"
|
|
}
|
|
|
|
result, err := p.analyticsRepo.GetPurchasingAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get purchasing analytics: %w", err)
|
|
}
|
|
|
|
data := make([]models.PurchasingAnalyticsData, len(result.Data))
|
|
for i, item := range result.Data {
|
|
data[i] = models.PurchasingAnalyticsData{
|
|
Date: item.Date,
|
|
Purchases: item.Purchases,
|
|
RawMaterialPurchases: item.RawMaterialPurchases,
|
|
NonInventoryPurchases: item.NonInventoryPurchases,
|
|
PurchaseOrders: item.PurchaseOrders,
|
|
RawMaterialPurchaseOrders: item.RawMaterialPurchaseOrders,
|
|
NonInventoryExpenseCount: item.NonInventoryExpenseCount,
|
|
Quantity: item.Quantity,
|
|
Ingredients: item.Ingredients,
|
|
Vendors: item.Vendors,
|
|
}
|
|
}
|
|
|
|
ingredientData := make([]models.PurchasingIngredientData, len(result.IngredientData))
|
|
for i, item := range result.IngredientData {
|
|
ingredientData[i] = models.PurchasingIngredientData{
|
|
IngredientID: item.IngredientID,
|
|
IngredientName: item.IngredientName,
|
|
Quantity: item.Quantity,
|
|
TotalCost: item.TotalCost,
|
|
AverageUnitCost: item.AverageUnitCost,
|
|
PurchaseOrderCount: item.PurchaseOrderCount,
|
|
}
|
|
}
|
|
|
|
vendorData := make([]models.PurchasingVendorData, len(result.VendorData))
|
|
for i, item := range result.VendorData {
|
|
vendorData[i] = models.PurchasingVendorData{
|
|
VendorID: item.VendorID,
|
|
VendorName: item.VendorName,
|
|
TotalCost: item.TotalCost,
|
|
PurchaseOrderCount: item.PurchaseOrderCount,
|
|
IngredientCount: item.IngredientCount,
|
|
Quantity: item.Quantity,
|
|
}
|
|
}
|
|
|
|
return &models.PurchasingAnalyticsResponse{
|
|
OrganizationID: req.OrganizationID,
|
|
OutletID: req.OutletID,
|
|
OutletName: result.OutletName,
|
|
DateFrom: req.DateFrom,
|
|
DateTo: req.DateTo,
|
|
GroupBy: req.GroupBy,
|
|
Summary: models.PurchasingSummary{
|
|
TotalPurchases: result.Summary.TotalPurchases,
|
|
RawMaterialPurchases: result.Summary.RawMaterialPurchases,
|
|
NonInventoryPurchases: result.Summary.NonInventoryPurchases,
|
|
TotalPurchaseOrders: result.Summary.TotalPurchaseOrders,
|
|
RawMaterialPurchaseOrders: result.Summary.RawMaterialPurchaseOrders,
|
|
NonInventoryExpenseCount: result.Summary.NonInventoryExpenseCount,
|
|
TotalQuantity: result.Summary.TotalQuantity,
|
|
AveragePurchaseOrderValue: result.Summary.AveragePurchaseOrderValue,
|
|
TotalIngredients: result.Summary.TotalIngredients,
|
|
TotalVendors: result.Summary.TotalVendors,
|
|
},
|
|
Data: data,
|
|
IngredientData: ingredientData,
|
|
VendorData: vendorData,
|
|
}, nil
|
|
}
|
|
|
|
func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *models.ProductAnalyticsRequest) (*models.ProductAnalyticsResponse, error) {
|
|
// Validate date range
|
|
if req.DateFrom.After(req.DateTo) {
|
|
return nil, fmt.Errorf("date_from cannot be after date_to")
|
|
}
|
|
|
|
// Set default limit
|
|
if req.Limit <= 0 {
|
|
req.Limit = 1000
|
|
}
|
|
|
|
// Get analytics data from repository
|
|
analyticsData, err := p.analyticsRepo.GetProductAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.Limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get product analytics: %w", err)
|
|
}
|
|
|
|
// Transform data
|
|
var resultData []models.ProductAnalyticsData
|
|
for _, data := range analyticsData {
|
|
resultData = append(resultData, models.ProductAnalyticsData{
|
|
ProductID: data.ProductID,
|
|
ProductName: data.ProductName,
|
|
ProductSku: data.ProductSku,
|
|
ProductPrice: data.ProductPrice,
|
|
CategoryID: data.CategoryID,
|
|
CategoryName: data.CategoryName,
|
|
CategoryOrder: data.CategoryOrder,
|
|
QuantitySold: data.QuantitySold,
|
|
Revenue: data.Revenue,
|
|
AveragePrice: data.AveragePrice,
|
|
OrderCount: data.OrderCount,
|
|
StandardHppPerUnit: data.StandardHppPerUnit,
|
|
StandardHppTotal: data.StandardHppTotal,
|
|
FifoHppPerUnit: data.FifoHppPerUnit,
|
|
FifoHppTotal: data.FifoHppTotal,
|
|
MovingAverageHppPerUnit: data.MovingAverageHppPerUnit,
|
|
MovingAverageHppTotal: data.MovingAverageHppTotal,
|
|
})
|
|
}
|
|
|
|
return &models.ProductAnalyticsResponse{
|
|
OrganizationID: req.OrganizationID,
|
|
OutletID: req.OutletID,
|
|
DateFrom: req.DateFrom,
|
|
DateTo: req.DateTo,
|
|
Data: resultData,
|
|
}, nil
|
|
}
|
|
|
|
func (p *AnalyticsProcessorImpl) GetProductAnalyticsPerCategory(ctx context.Context, req *models.ProductAnalyticsPerCategoryRequest) (*models.ProductAnalyticsPerCategoryResponse, error) {
|
|
// Validate date range
|
|
if req.DateFrom.After(req.DateTo) {
|
|
return nil, fmt.Errorf("date_from cannot be after date_to")
|
|
}
|
|
|
|
// Get analytics data from repository
|
|
analyticsData, err := p.analyticsRepo.GetProductAnalyticsPerCategory(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get product analytics per category: %w", err)
|
|
}
|
|
|
|
// Transform data
|
|
var resultData []models.ProductAnalyticsPerCategoryData
|
|
for _, data := range analyticsData {
|
|
resultData = append(resultData, models.ProductAnalyticsPerCategoryData{
|
|
CategoryID: data.CategoryID,
|
|
CategoryName: data.CategoryName,
|
|
TotalRevenue: data.TotalRevenue,
|
|
TotalQuantity: data.TotalQuantity,
|
|
ProductCount: data.ProductCount,
|
|
OrderCount: data.OrderCount,
|
|
TotalStandardHpp: data.TotalStandardHpp,
|
|
TotalFifoHpp: data.TotalFifoHpp,
|
|
TotalMovingAverageHpp: data.TotalMovingAverageHpp,
|
|
})
|
|
}
|
|
|
|
return &models.ProductAnalyticsPerCategoryResponse{
|
|
OrganizationID: req.OrganizationID,
|
|
OutletID: req.OutletID,
|
|
DateFrom: req.DateFrom,
|
|
DateTo: req.DateTo,
|
|
Data: resultData,
|
|
}, nil
|
|
}
|
|
|
|
func (p *AnalyticsProcessorImpl) GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error) {
|
|
// Validate date range
|
|
if req.DateFrom.After(req.DateTo) {
|
|
return nil, fmt.Errorf("date_from cannot be after date_to")
|
|
}
|
|
|
|
// Get dashboard overview
|
|
overview, err := p.analyticsRepo.GetDashboardOverview(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get dashboard overview: %w", err)
|
|
}
|
|
|
|
// Get top products (limit to 5 for dashboard)
|
|
productReq := &models.ProductAnalyticsRequest{
|
|
OrganizationID: req.OrganizationID,
|
|
OutletID: req.OutletID,
|
|
DateFrom: req.DateFrom,
|
|
DateTo: req.DateTo,
|
|
Limit: 5,
|
|
}
|
|
topProducts, err := p.GetProductAnalytics(ctx, productReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get top products: %w", err)
|
|
}
|
|
|
|
// Get payment methods
|
|
paymentReq := &models.PaymentMethodAnalyticsRequest{
|
|
OrganizationID: req.OrganizationID,
|
|
OutletID: req.OutletID,
|
|
DateFrom: req.DateFrom,
|
|
DateTo: req.DateTo,
|
|
GroupBy: "day",
|
|
}
|
|
paymentMethods, err := p.GetPaymentMethodAnalytics(ctx, paymentReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get payment methods: %w", err)
|
|
}
|
|
|
|
// Get recent sales (last 7 days)
|
|
recentDateFrom := time.Now().AddDate(0, 0, -7)
|
|
salesReq := &models.SalesAnalyticsRequest{
|
|
OrganizationID: req.OrganizationID,
|
|
OutletID: req.OutletID,
|
|
DateFrom: recentDateFrom,
|
|
DateTo: req.DateTo,
|
|
GroupBy: "day",
|
|
}
|
|
recentSales, err := p.GetSalesAnalytics(ctx, salesReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get recent sales: %w", err)
|
|
}
|
|
|
|
return &models.DashboardAnalyticsResponse{
|
|
OrganizationID: req.OrganizationID,
|
|
OutletID: 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,
|
|
},
|
|
TopProducts: topProducts.Data,
|
|
PaymentMethods: paymentMethods.Data,
|
|
RecentSales: recentSales.Data,
|
|
}, nil
|
|
}
|
|
|
|
func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error) {
|
|
if req.DateFrom.IsZero() {
|
|
return nil, fmt.Errorf("date_from is required")
|
|
}
|
|
|
|
if req.DateTo.IsZero() {
|
|
return nil, fmt.Errorf("date_to is required")
|
|
}
|
|
|
|
if req.DateFrom.After(req.DateTo) {
|
|
return nil, fmt.Errorf("date_from cannot be after date_to")
|
|
}
|
|
|
|
if req.GroupBy == "" {
|
|
req.GroupBy = "day"
|
|
}
|
|
|
|
result, err := p.analyticsRepo.GetProfitLossAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get profit/loss analytics: %w", err)
|
|
}
|
|
|
|
data := make([]models.ProfitLossData, len(result.Data))
|
|
for i, item := range result.Data {
|
|
data[i] = models.ProfitLossData{
|
|
Date: item.Date,
|
|
Revenue: item.Revenue,
|
|
Cost: item.Cost,
|
|
GrossProfit: item.GrossProfit,
|
|
GrossProfitMargin: item.GrossProfitMargin,
|
|
Tax: item.Tax,
|
|
Discount: item.Discount,
|
|
NetProfit: item.NetProfit,
|
|
NetProfitMargin: item.NetProfitMargin,
|
|
Orders: item.Orders,
|
|
}
|
|
}
|
|
|
|
productData := make([]models.ProductProfitData, len(result.ProductData))
|
|
for i, item := range result.ProductData {
|
|
productData[i] = models.ProductProfitData{
|
|
ProductID: item.ProductID,
|
|
ProductName: item.ProductName,
|
|
CategoryID: item.CategoryID,
|
|
CategoryName: item.CategoryName,
|
|
QuantitySold: item.QuantitySold,
|
|
Revenue: item.Revenue,
|
|
Cost: item.Cost,
|
|
GrossProfit: item.GrossProfit,
|
|
GrossProfitMargin: item.GrossProfitMargin,
|
|
AveragePrice: item.AveragePrice,
|
|
AverageCost: item.AverageCost,
|
|
ProfitPerUnit: item.ProfitPerUnit,
|
|
}
|
|
}
|
|
|
|
type categoryAmount struct {
|
|
Name string
|
|
TodayAmt float64
|
|
MtdAmt float64
|
|
}
|
|
|
|
categoryMap := make(map[string]*categoryAmount)
|
|
var categoryOrder []string
|
|
|
|
for _, cat := range result.TodayExpenseByCategory {
|
|
name := cat.CategoryName
|
|
if _, exists := categoryMap[name]; !exists {
|
|
categoryMap[name] = &categoryAmount{Name: name}
|
|
categoryOrder = append(categoryOrder, name)
|
|
}
|
|
categoryMap[name].TodayAmt = cat.Amount
|
|
}
|
|
|
|
for _, cat := range result.MtdExpenseByCategory {
|
|
name := cat.CategoryName
|
|
if _, exists := categoryMap[name]; !exists {
|
|
categoryMap[name] = &categoryAmount{Name: name}
|
|
categoryOrder = append(categoryOrder, name)
|
|
}
|
|
categoryMap[name].MtdAmt = cat.Amount
|
|
}
|
|
|
|
var todayTotalOps float64
|
|
var mtdTotalOps float64
|
|
var todayGaji float64
|
|
var mtdGaji float64
|
|
for _, cat := range categoryMap {
|
|
if isSalaryExpenseCategory(cat.Name) {
|
|
todayGaji += cat.TodayAmt
|
|
mtdGaji += cat.MtdAmt
|
|
continue
|
|
}
|
|
todayTotalOps += cat.TodayAmt
|
|
mtdTotalOps += cat.MtdAmt
|
|
}
|
|
|
|
todayGrossProfit := result.TodayRevenue - result.TodayCost
|
|
mtdGrossProfit := result.MtdRevenue - result.MtdCost
|
|
|
|
todayProfitBeforeGaji := todayGrossProfit - todayTotalOps
|
|
mtdProfitBeforeGaji := mtdGrossProfit - mtdTotalOps
|
|
|
|
todayNetProfit := todayProfitBeforeGaji - todayGaji
|
|
mtdNetProfit := mtdProfitBeforeGaji - mtdGaji
|
|
|
|
todayPct := func(nominal float64) float64 {
|
|
if result.TodayRevenue == 0 {
|
|
return 0
|
|
}
|
|
return (nominal / result.TodayRevenue) * 100
|
|
}
|
|
mtdPct := func(nominal float64) float64 {
|
|
if result.MtdRevenue == 0 {
|
|
return 0
|
|
}
|
|
return (nominal / result.MtdRevenue) * 100
|
|
}
|
|
|
|
opsSubItems := make([]models.ProfitLossSummaryRow, 0, len(categoryOrder)+1)
|
|
opsCategoryCount := 0
|
|
for _, name := range categoryOrder {
|
|
cat := categoryMap[name]
|
|
if isSalaryExpenseCategory(cat.Name) {
|
|
continue
|
|
}
|
|
opsCategoryCount++
|
|
opsSubItems = append(opsSubItems, models.ProfitLossSummaryRow{
|
|
ID: fmt.Sprintf("by_%s", slugify(name)),
|
|
Label: fmt.Sprintf("%d. %s", opsCategoryCount, cat.Name),
|
|
TodayNominal: cat.TodayAmt,
|
|
TodayPct: todayPct(cat.TodayAmt),
|
|
MtdNominal: cat.MtdAmt,
|
|
MtdPct: mtdPct(cat.MtdAmt),
|
|
})
|
|
}
|
|
opsSubItems = append(opsSubItems, models.ProfitLossSummaryRow{
|
|
ID: "total_biaya_ops",
|
|
Label: fmt.Sprintf("Total Biaya OPS (%d kategori)", opsCategoryCount),
|
|
IsBold: true,
|
|
TodayNominal: todayTotalOps,
|
|
TodayPct: todayPct(todayTotalOps),
|
|
MtdNominal: mtdTotalOps,
|
|
MtdPct: mtdPct(mtdTotalOps),
|
|
})
|
|
|
|
mainSummary := []models.ProfitLossSummaryRow{
|
|
{
|
|
ID: "total_omset", Label: "TOTAL OMSET",
|
|
TodayNominal: result.TodayRevenue, TodayPct: todayPct(result.TodayRevenue),
|
|
MtdNominal: result.MtdRevenue, MtdPct: mtdPct(result.MtdRevenue),
|
|
},
|
|
{
|
|
ID: "hpp", Label: "HPP",
|
|
TodayNominal: result.TodayCost, TodayPct: todayPct(result.TodayCost),
|
|
MtdNominal: result.MtdCost, MtdPct: mtdPct(result.MtdCost),
|
|
},
|
|
{
|
|
ID: "laba_kotor", Label: "Laba Kotor (1-2)",
|
|
TodayNominal: todayGrossProfit, TodayPct: todayPct(todayGrossProfit),
|
|
MtdNominal: mtdGrossProfit, MtdPct: mtdPct(mtdGrossProfit),
|
|
},
|
|
{
|
|
ID: "biaya_ops", Label: "BIAYA OPS",
|
|
TodayNominal: todayTotalOps, TodayPct: todayPct(todayTotalOps),
|
|
MtdNominal: mtdTotalOps, MtdPct: mtdPct(mtdTotalOps),
|
|
SubItems: opsSubItems,
|
|
},
|
|
{
|
|
ID: "laba_rugi_sblm_gaji", Label: "Laba/Rugi sblm Gaji (3-4)",
|
|
TodayNominal: todayProfitBeforeGaji, TodayPct: todayPct(todayProfitBeforeGaji),
|
|
MtdNominal: mtdProfitBeforeGaji, MtdPct: mtdPct(mtdProfitBeforeGaji),
|
|
},
|
|
{
|
|
ID: "biaya_gaji", Label: "BIAYA GAJI",
|
|
TodayNominal: todayGaji, TodayPct: todayPct(todayGaji),
|
|
MtdNominal: mtdGaji, MtdPct: mtdPct(mtdGaji),
|
|
},
|
|
{
|
|
ID: "laba_rugi", Label: "Laba/Rugi (5-6)", IsBold: true,
|
|
TodayNominal: todayNetProfit, TodayPct: todayPct(todayNetProfit),
|
|
MtdNominal: mtdNetProfit, MtdPct: mtdPct(mtdNetProfit),
|
|
},
|
|
}
|
|
|
|
opsItems := make([]models.OperationalExpenseItem, len(result.OperationalExpenseItems))
|
|
var opsTotal float64
|
|
for i, item := range result.OperationalExpenseItems {
|
|
opsItems[i] = models.OperationalExpenseItem{
|
|
Item: item.Item,
|
|
Nominal: item.Amount,
|
|
}
|
|
opsTotal += item.Amount
|
|
}
|
|
|
|
return &models.ProfitLossAnalyticsResponse{
|
|
OrganizationID: req.OrganizationID,
|
|
OutletID: req.OutletID,
|
|
DateFrom: req.DateFrom,
|
|
DateTo: req.DateTo,
|
|
GroupBy: req.GroupBy,
|
|
Summary: models.ProfitLossSummary{
|
|
TotalRevenue: result.Summary.TotalRevenue,
|
|
TotalCost: result.Summary.TotalCost,
|
|
GrossProfit: result.Summary.GrossProfit,
|
|
GrossProfitMargin: result.Summary.GrossProfitMargin,
|
|
TotalTax: result.Summary.TotalTax,
|
|
TotalDiscount: result.Summary.TotalDiscount,
|
|
NetProfit: result.Summary.NetProfit,
|
|
NetProfitMargin: result.Summary.NetProfitMargin,
|
|
TotalOrders: result.Summary.TotalOrders,
|
|
AverageProfit: result.Summary.AverageProfit,
|
|
ProfitabilityRatio: result.Summary.ProfitabilityRatio,
|
|
},
|
|
Data: data,
|
|
ProductData: productData,
|
|
MainSummary: mainSummary,
|
|
OperationalExpenses: opsItems,
|
|
OperationalExpensesTotal: opsTotal,
|
|
}, nil
|
|
}
|
|
|
|
func isSalaryExpenseCategory(name string) bool {
|
|
name = strings.ToLower(name)
|
|
return strings.Contains(name, "gaji") || strings.Contains(name, "salary")
|
|
}
|
|
|
|
func slugify(s string) string {
|
|
result := make([]byte, 0, len(s))
|
|
for i := 0; i < len(s); i++ {
|
|
c := s[i]
|
|
switch {
|
|
case c >= 'a' && c <= 'z':
|
|
result = append(result, c)
|
|
case c >= 'A' && c <= 'Z':
|
|
result = append(result, c+32)
|
|
case c >= '0' && c <= '9':
|
|
result = append(result, c)
|
|
default:
|
|
if len(result) == 0 || result[len(result)-1] != '_' {
|
|
result = append(result, '_')
|
|
}
|
|
}
|
|
}
|
|
return string(result)
|
|
}
|