package controller import ( "fmt" "math" "strconv" "strings" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" "gitlab.com/mbugroup/lti-api.git/internal/response" "github.com/gofiber/fiber/v2" ) // === Marketing Report Response === type MarketingReportResponse struct { Code int `json:"code"` Status string `json:"status"` Message string `json:"message"` Meta response.Meta `json:"meta"` Data []dto.RepportMarketingItemDTO `json:"data"` Total *dto.Summary `json:"total,omitempty"` } type RepportController struct { RepportService service.RepportService } const expenseReportExcelExportFetchLimit = 100 func NewRepportController(repportService service.RepportService) *RepportController { return &RepportController{ RepportService: repportService, } } func (c *RepportController) GetExpense(ctx *fiber.Ctx) error { query := &validation.ExpenseQuery{ Page: ctx.QueryInt("page", 1), Limit: ctx.QueryInt("limit", 10), Search: ctx.Query("search", ""), Category: ctx.Query("category", ""), SupplierId: int64(ctx.QueryInt("supplier_id", 0)), KandangId: int64(ctx.QueryInt("kandang_id", 0)), ProjectFlockKandangId: int64(ctx.QueryInt("project_flock_kandang_id", 0)), NonstockId: int64(ctx.QueryInt("nonstock_id", 0)), AreaId: int64(ctx.QueryInt("area_id", 0)), LocationId: int64(ctx.QueryInt("location_id", 0)), RealizationDate: ctx.Query("realization_date", ""), } locationScope, err := m.ResolveLocationScope(ctx, c.RepportService.DB()) if err != nil { return err } areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) if err != nil { return err } if locationScope.Restrict { query.AllowedLocationIDs = toInt64Slice(locationScope.IDs) } if areaScope.Restrict { query.AllowedAreaIDs = toInt64Slice(areaScope.IDs) } if isAllExpenseExcelExportRequest(ctx) { allResults, err := c.getAllExpenseRowsForExcel(ctx, query) if err != nil { return err } return exportExpenseReportListExcel(ctx, allResults) } if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } result, totalResults, err := c.RepportService.GetExpense(ctx, query) if err != nil { return err } return ctx.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.RepportExpenseListDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get expense report successfully", Meta: response.Meta{ Page: query.Page, Limit: query.Limit, TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, Data: result, }) } func (c *RepportController) getAllExpenseRowsForExcel(ctx *fiber.Ctx, baseQuery *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, error) { query := *baseQuery query.Page = 1 query.Limit = expenseReportExcelExportFetchLimit results := make([]dto.RepportExpenseListDTO, 0) for { pageResults, total, err := c.RepportService.GetExpense(ctx, &query) if err != nil { return nil, err } if len(pageResults) == 0 || total == 0 { break } results = append(results, pageResults...) if int64(len(results)) >= total { break } query.Page++ } return results, nil } func (c *RepportController) GetExpenseDepreciation(ctx *fiber.Ctx) error { rows, meta, err := c.RepportService.GetExpenseDepreciation(ctx) if err != nil { return err } resp := struct { Code int `json:"code"` Status string `json:"status"` Message string `json:"message"` Meta dto.ExpenseDepreciationMetaDTO `json:"meta"` Data []dto.ExpenseDepreciationRowDTO `json:"data"` }{ Code: fiber.StatusOK, Status: "success", Message: "Get expense depreciation report successfully", Meta: *meta, Data: rows, } return ctx.Status(fiber.StatusOK).JSON(resp) } func (c *RepportController) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) error { rows, meta, err := c.RepportService.GetExpenseDepreciationManualInputs(ctx) if err != nil { return err } resp := struct { Code int `json:"code"` Status string `json:"status"` Message string `json:"message"` Meta dto.ExpenseDepreciationMetaDTO `json:"meta"` Data []dto.ExpenseDepreciationManualInputRowDTO `json:"data"` }{ Code: fiber.StatusOK, Status: "success", Message: "Get expense depreciation manual inputs successfully", Meta: *meta, Data: rows, } return ctx.Status(fiber.StatusOK).JSON(resp) } func (c *RepportController) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx) error { req := new(validation.ExpenseDepreciationManualInputUpsert) if err := ctx.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } if err := m.EnsureProjectFlockAccess(ctx, c.RepportService.DB(), req.ProjectFlockID); err != nil { return err } result, err := c.RepportService.UpsertExpenseDepreciationManualInput(ctx, req) if err != nil { return err } return ctx.Status(fiber.StatusOK).JSON(response.Success{ Code: fiber.StatusOK, Status: "success", Message: "Upsert expense depreciation manual input successfully", Data: result, }) } func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { query := &validation.MarketingQuery{ Page: ctx.QueryInt("page", 1), Limit: ctx.QueryInt("limit", 10), Search: ctx.Query("search", ""), CustomerId: int64(ctx.QueryInt("customer_id", 0)), ProductId: int64(ctx.QueryInt("product_id", 0)), WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)), SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)), AreaId: int64(ctx.QueryInt("area_id", 0)), LocationId: int64(ctx.QueryInt("location_id", 0)), MarketingType: ctx.Query("marketing_type", ""), FilterBy: ctx.Query("filter_by", ""), StartDate: ctx.Query("start_date", ""), EndDate: ctx.Query("end_date", ""), SortBy: ctx.Query("sort_by", ""), SortOrder: ctx.Query("sort_order", ""), } locationScope, err := m.ResolveLocationScope(ctx, c.RepportService.DB()) if err != nil { return err } areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) if err != nil { return err } if locationScope.Restrict { allowed := toInt64Slice(locationScope.IDs) if len(allowed) == 0 { allowed = []int64{-1} } query.AllowedLocationIDs = allowed } if areaScope.Restrict { allowed := toInt64Slice(areaScope.IDs) if len(allowed) == 0 { allowed = []int64{-1} } query.AllowedAreaIDs = allowed } if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } result, totalResults, err := c.RepportService.GetMarketing(ctx, query) if err != nil { return err } if isMarketingExcelExportRequest(ctx) { return exportMarketingReportExcel(ctx, result) } if isMarketingPdfExportRequest(ctx) { meta := buildMarketingPdfMeta(query.StartDate, query.EndDate) return exportMarketingReportPdf(ctx, result, meta) } total := dto.ToSummaryFromDTOItems(result) return ctx.Status(fiber.StatusOK). JSON(MarketingReportResponse{ Code: fiber.StatusOK, Status: "success", Message: "Get marketing report successfully", Meta: response.Meta{ Page: query.Page, Limit: query.Limit, TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, Data: result, Total: total, }) } func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { areaIDs, err := parseCommaSeparatedInt64sWithField(ctx.Query("area_id", ""), "area_id") if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } supplierIDs, err := parseCommaSeparatedInt64sWithField(ctx.Query("supplier_id", ""), "supplier_id") if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } productIDs, err := parseCommaSeparatedInt64sWithField(ctx.Query("product_id", ""), "product_id") if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } productCategoryIDs, err := parseCommaSeparatedInt64sWithField(ctx.Query("product_category_id", ""), "product_category_id") if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } query := &validation.PurchaseSupplierQuery{ Page: ctx.QueryInt("page", 1), Limit: ctx.QueryInt("limit", 10), AreaIDs: areaIDs, SupplierIDs: supplierIDs, ProductIDs: productIDs, ProductCategoryIDs: productCategoryIDs, StartDate: ctx.Query("start_date", ""), EndDate: ctx.Query("end_date", ""), SortBy: ctx.Query("sort_by", ""), FilterBy: ctx.Query("filter_by", ""), } areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) if err != nil { return err } if areaScope.Restrict { query.AllowedAreaIDs = toInt64Slice(areaScope.IDs) } if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } result, totalResults, err := c.RepportService.GetPurchaseSupplier(ctx, query) if err != nil { return err } filters := map[string]interface{}{ "area_id": query.AreaIDs, "supplier_id": query.SupplierIDs, "product_id": query.ProductIDs, "product_category_id": query.ProductCategoryIDs, "start_date": query.StartDate, "end_date": query.EndDate, "sort_by": query.SortBy, "filter_by": query.FilterBy, } return ctx.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.PurchaseSupplierDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get supplier purchase recap successfully", Meta: response.Meta{ Page: query.Page, Limit: query.Limit, TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, Filters: filters, }, Data: result, }) } func (c *RepportController) GetDebtSupplier(ctx *fiber.Ctx) error { supplierIDs, err := parseCommaSeparatedInt64s(ctx.Query("supplier_ids", "")) if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } query := &validation.DebtSupplierQuery{ Page: ctx.QueryInt("page", 1), Limit: ctx.QueryInt("limit", 10), SupplierIDs: supplierIDs, StartDate: ctx.Query("start_date", ""), EndDate: ctx.Query("end_date", ""), FilterBy: ctx.Query("filter_by", ""), SortOrder: ctx.Query("sort_order", ""), } locationScope, err := m.ResolveLocationScope(ctx, c.RepportService.DB()) if err != nil { return err } areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) if err != nil { return err } if locationScope.Restrict { query.AllowedLocationIDs = toInt64Slice(locationScope.IDs) } if areaScope.Restrict { query.AllowedAreaIDs = toInt64Slice(areaScope.IDs) } if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } result, totalResults, err := c.RepportService.GetDebtSupplier(ctx, query) if err != nil { return err } supplierIDs = query.SupplierIDs if supplierIDs == nil { supplierIDs = []int64{} } filters := map[string]interface{}{ "start_date": query.StartDate, "end_date": query.EndDate, "supplier_ids": supplierIDs, "filter_by": query.FilterBy, } return ctx.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.DebtSupplierDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get supplier debt recap successfully", Meta: response.Meta{ Page: query.Page, Limit: query.Limit, TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, Filters: filters, }, Data: result, }) } func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error { data, meta, err := c.RepportService.GetHppPerKandang(ctx) if err != nil { return err } resp := struct { Code int `json:"code"` Status string `json:"status"` Message string `json:"message"` Meta dto.HppPerKandangMetaDTO `json:"meta"` Data dto.HppPerKandangResponseData `json:"data"` }{ Code: fiber.StatusOK, Status: "success", Message: "Get HPP harian kandang layer successfully", Meta: *meta, Data: *data, } return ctx.Status(fiber.StatusOK).JSON(resp) } func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error { var customerIDs []uint if customerIDsStr := ctx.Query("customer_ids"); customerIDsStr != "" { ids := strings.Split(customerIDsStr, ",") for _, idStr := range ids { idStr = strings.TrimSpace(idStr) if idStr != "" { if id, err := strconv.ParseUint(idStr, 10, 32); err == nil { customerIDs = append(customerIDs, uint(id)) } } } } query := &validation.CustomerPaymentQuery{ Page: ctx.QueryInt("page", 1), Limit: ctx.QueryInt("limit", 10), CustomerIDs: customerIDs, FilterBy: strings.ToUpper(ctx.Query("filter_by", "")), StartDate: ctx.Query("start_date", ""), EndDate: ctx.Query("end_date", ""), } // Validate pagination if len(customerIDs) == 0 && (query.Page < 1 || query.Limit < 1) { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0 when customer_ids is not provided") } result, totalResults, err := c.RepportService.GetCustomerPayment(ctx, query) if err != nil { return err } // If single customer mode (only 1 customer ID), return without pagination if len(customerIDs) == 1 { return ctx.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", Message: "Get customer payment report successfully", Data: result, }) } // Multiple customers mode with pagination return ctx.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.CustomerPaymentReportItem]{ Code: fiber.StatusOK, Status: "success", Message: "Get customer payment report successfully", Meta: response.Meta{ Page: query.Page, Limit: query.Limit, TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, Data: result, }) } func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error { idParam := ctx.Params("idProjectFlockKandang") if idParam == "" { return fiber.NewError(fiber.StatusBadRequest, "idProjectFlockKandang is required") } projectFlockKandangID, err := strconv.ParseUint(idParam, 10, 64) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "invalid idProjectFlockKandang") } query := &validation.ProductionResultQuery{ Page: ctx.QueryInt("page", 1), Limit: ctx.QueryInt("limit", 10), ProjectFlockKandangID: uint(projectFlockKandangID), } if err := m.EnsureProjectFlockKandangAccess(ctx, c.RepportService.DB(), 0, query.ProjectFlockKandangID); err != nil { return err } if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } data, totalResults, err := c.RepportService.GetProductionResult(ctx, query) if err != nil { return err } return ctx.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.ProductionResultDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get Laporan Hasil Produksi successfully", Meta: response.Meta{ Page: query.Page, Limit: query.Limit, TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, Data: data, }) } func (c *RepportController) GetHppV2Breakdown(ctx *fiber.Ctx) error { query := &validation.HppV2BreakdownQuery{ ProjectFlockKandangID: uint(ctx.QueryInt("project_flock_kandang_id", 0)), Period: ctx.Query("period", ""), } if err := m.EnsureProjectFlockKandangAccess(ctx, c.RepportService.DB(), 0, query.ProjectFlockKandangID); err != nil { return err } data, err := c.RepportService.GetHppV2Breakdown(ctx, query) if err != nil { return err } return ctx.Status(fiber.StatusOK).JSON(response.Success{ Code: fiber.StatusOK, Status: "success", Message: "Get HPP v2 breakdown successfully", Data: data, }) } func parseCommaSeparatedInt64s(raw string) ([]int64, error) { return parseCommaSeparatedInt64sWithField(raw, "supplier_ids") } func parseCommaSeparatedInt64sWithField(raw, field string) ([]int64, error) { raw = strings.TrimSpace(raw) if raw == "" { return []int64{}, nil } parts := strings.Split(raw, ",") result := make([]int64, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } id, err := strconv.ParseInt(part, 10, 64) if err != nil { return nil, fmt.Errorf("%s must be comma separated integers", field) } result = append(result, id) } return result, nil } func toInt64Slice(ids []uint) []int64 { if len(ids) == 0 { return nil } out := make([]int64, 0, len(ids)) for _, id := range ids { out = append(out, int64(id)) } return out }