diff --git a/internal/capabilities/capabilities.go b/internal/capabilities/capabilities.go deleted file mode 100644 index 47f774ba..00000000 --- a/internal/capabilities/capabilities.go +++ /dev/null @@ -1,44 +0,0 @@ -package capabilities - -import ( - "strings" - - permission "gitlab.com/mbugroup/lti-api.git/internal/middleware" -) - -// FromPermissions returns a filtered map of capabilities that the frontend can use -// to toggle features. Only permissions recognized by the application are exposed. -func FromPermissions(perms []string) map[string]bool { - if len(perms) == 0 { - return nil - } - - out := make(map[string]bool) - for _, perm := range perms { - if key, ok := normalizeAndAllow(perm); ok { - out[key] = true - } - } - if len(out) == 0 { - return nil - } - return out -} - -func normalizeAndAllow(perm string) (string, bool) { - perm = strings.ToLower(strings.TrimSpace(perm)) - if perm == "" { - return "", false - } - if _, ok := allowed[perm]; !ok { - return "", false - } - return perm, true -} - -var allowed = map[string]struct{}{ - permission.PermissionRecordingRead: {}, - permission.PermissionRecordingCreate: {}, - permission.PermissionRecordingUpdate: {}, - permission.PermissionRecordingDelete: {}, -} diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 8da408ca..bb4090bb 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -588,6 +588,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Ekor", Category: "Day Old Chick", Price: 1, + Flags: []utils.FlagType{utils.FlagAyamAfkir}, }, { Name: "Ayam Mati", @@ -596,6 +597,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Ekor", Category: "Day Old Chick", Price: 1, + Flags: []utils.FlagType{utils.FlagAyamMati}, }, { Name: "Ayam Culling", @@ -604,6 +606,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Ekor", Category: "Day Old Chick", Price: 1, + Flags: []utils.FlagType{utils.FlagAyamCulling}, }, { Name: "Telur Konsumsi Baik", @@ -612,6 +615,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Unit", Category: "Telur", Price: 1, + Flags: []utils.FlagType{utils.FlagTelurUtuh}, }, { Name: "Telur Pecah", @@ -620,6 +624,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Uom: "Unit", Category: "Telur", Price: 1, + Flags: []utils.FlagType{utils.FlagTelurPecah}, }, { Name: "281 SPECIAL STARTER", @@ -632,6 +637,16 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter}, }, + { + Name: "Ayam Layer", + Brand: "-", + Sku: "LYR0001", + Uom: "Ekor", + Category: "Pullet", + Price: 20000, + Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, + Flags: []utils.FlagType{utils.FlagLayer}, + }, } for _, seed := range seeds { diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index a831c25b..a08d431b 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -104,11 +104,12 @@ func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) { } func ActorIDFromContext(c *fiber.Ctx) (uint, error) { - user, ok := AuthenticatedUser(c) - if !ok || user == nil || user.Id == 0 { - return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") - } - return user.Id, nil + // user, ok := AuthenticatedUser(c) + // if !ok || user == nil || user.Id == 0 { + // return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + // } + // return user.Id, nil + return 1, nil } // AuthDetails returns the full authentication context (token, claims, user). diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index 928242a0..f46c25a9 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -1,14 +1,198 @@ package middleware -//project-flock +// project-flock const ( - PermissionProjectFlockClosing = "lti:project-flock:closing" + P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing" + P_ProjectFlockKandangsCheckClosing = "lti.production.project_flock_kandangs.closing.detail" + P_ProjectFlockKandangsGetAll = "lti.production.project_flock_kandangs.list" + P_ProjectFlockKandangsGetOne = "lti.production.project_flock_kandangs.detail" + + P_ProjectFlockGetAll = "lti.production.project_flocks.list" + P_ProjectFlockCreate = "lti.production.project_flocks.create" + P_ProjectFlockGetOne = "lti.production.project_flocks.detail" + P_ProjectFlockUpdate = "lti.production.project_flocks.update" + P_ProjectFlockDelete = "lti.production.project_flocks.delete" + P_ProjectFlockApprove = "lti.production.project_flocks.approve" + P_ProjectFlockLookup = "lti.production.project_flocks.lookup" + P_ProjectFlockNextPeriod = "lti.production.project_flocks.next_period" + P_ProjectFlockResubmit = "lti.production.project_flocks.resubmit" ) -//recording const ( - PermissionRecordingRead = "recording.index" - PermissionRecordingCreate = "recording.create" - PermissionRecordingUpdate = "recording.update" - PermissionRecordingDelete = "recording.delete" -) \ No newline at end of file + P_ExpenseGetAll = "lti.expense.list" + P_ExpenseCreateOne = "lti.expense.create" + P_ExpenseUpdateOne = "lti.expense.update" + P_ExpenseGetOne = "lti.expense.detail" + P_ExpenseDeleteOne = "lti.expense.delete" + P_ExpenseApprovalManager = "lti.expense.approve.manager" + P_ExpenseApprovalFinance = "lti.expense.approve.finance" + P_ExpenseCreateRealizations = "lti.expense.create.realization" + P_ExpenseUpdateRealizations = "lti.expense.update.realization" + P_ExpenseCompleteExpense = "lti.expense.complete.expense" + P_ExpenseDocument = "lti.expense.document" + P_ExpenseDocumentRealizations = "lti.expense.document.realization" +) +const ( + P_AdjustmentGetAll = "lti.inventory.list" + P_AdjustmentCreate = "lti.inventory.create" + P_AdjustmentGetOne = "lti.inventory.detail" +) +const ( + P_ApprovalGetAll = "lti.approval.list" +) +const ( + P_ReportExpenseGetAll = "lti.repport.expense.list" + P_ReportDeliveryGetAll = "lti.repport.delivery.list" + P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list" +) + +const ( + P_ProductStockGetAll = "lti.inventory.product_stock.list" + P_ProductStockGetOne = "lti.inventory.product_stock.detail" + P_ProductWarehousekGetAll = "lti.inventory.product_warehouses.list" + P_ProductWarehouseGetOne = "lti.inventory.product_warehouses.detail" +) +const ( + P_ClosingGetAll = "lti.closing.list" + P_ClosingDetail = "lti.closing.detail" +) + +const ( + P_TransferGetAll = "lti.inventory.transfer.list" + P_TransferGetOne = "lti.inventory.transfer.detail" + P_TransferCreateOne = "lti.inventory.transfer.create" +) + +const ( + P_TransferToLaying_GetAll = "lti.production.transfer_to_laying.list" + P_TransferToLaying_GetOne = "lti.production.transfer_to_laying.detail" + P_TransferToLaying_CreateOne = "lti.production.transfer_to_laying.create" + P_TransferToLaying_UpdateOne = "lti.production.transfer_to_laying.update" + P_TransferToLaying_DeleteOne = "lti.production.transfer_to_laying.delete" + P_TransferToLaying_Approval = "lti.production.transfer_to_laying.approve" + P_TransferToLaying_GetAvailableQty = "lti.production.transfer_to_laying.getavailableqty" +) + +const ( + P_DeliveryGetAll = "lti.marketing.delivery_order.list" + P_DeliveryGetOne = "lti.marketing.delivery_order.detail" + P_DeliveryUpdateOne = "lti.marketing.delivery_order.update" + P_SalesOrderDelete = "lti.marketing.sales_order.delete" + P_SalesOrderApproval = "lti.marketing.sales_order.approve" + P_SalesOrderCreateOne = "lti.marketing.sales_order.create" + P_SalesOrderUpdateOne = "lti.marketing.sales_order.update" +) + +const ( + P_AreaGetAll = "lti.master.area.list" + P_AreaGetOne = "lti.master.area.detail" + P_AreaCreateOne = "lti.master.area.create" + P_AreaUpdateOne = "lti.master.area.update" + P_AreaDeleteOne = "lti.master.area.delete" + + P_BanksGetAll = "lti.master.banks.list" + P_BanksGetOne = "lti.master.banks.detail" + P_BanksCreateOne = "lti.master.banks.create" + P_BanksUpdateOne = "lti.master.banks.update" + P_BanksDeleteOne = "lti.master.banks.delete" + + P_CustomerGetAll = "lti.master.customer.list" + P_CustomerGetOne = "lti.master.customer.detail" + P_CustomerCreateOne = "lti.master.customer.create" + P_CustomerUpdateOne = "lti.master.customer.update" + P_CustomerDeleteOne = "lti.master.customer.delete" + + P_FcrGetAll = "lti.master.fcr.list" + P_FcrGetOne = "lti.master.fcr.detail" + P_FcrCreateOne = "lti.master.fcr.create" + P_FcrUpdateOne = "lti.master.fcr.update" + P_FcrDeleteOne = "lti.master.fcr.delete" + + P_FlocksGetAll = "lti.master.flocks.list" + P_FlocksGetOne = "lti.master.flocks.detail" + P_FlocksCreateOne = "lti.master.flocks.create" + P_FlocksUpdateOne = "lti.master.flocks.update" + P_FlocksDeleteOne = "lti.master.flocks.delete" + + P_KandangsGetAll = "lti.master.kandangs.list" + P_KandangsGetOne = "lti.master.kandangs.detail" + P_KandangsCreateOne = "lti.master.kandangs.create" + P_KandangsUpdateOne = "lti.master.kandangs.update" + P_KandangsDeleteOne = "lti.master.kandangs.delete" + + P_LocationsGetAll = "lti.master.locations.list" + P_LocationsGetOne = "lti.master.locations.detail" + P_LocationsCreateOne = "lti.master.locations.create" + P_LocationsUpdateOne = "lti.master.locations.update" + P_LocationsDeleteOne = "lti.master.locations.delete" + + P_NonstocksGetAll = "lti.master.nonstocks.list" + P_NonstocksGetOne = "lti.master.nonstocks.detail" + P_NonstocksCreateOne = "lti.master.nonstocks.create" + P_NonstocksUpdateOne = "lti.master.nonstocks.update" + P_NonstocksDeleteOne = "lti.master.nonstocks.delete" + + P_ProductCategoriesGetAll = "lti.master.Product_categories.list" + P_ProductCategoriesGetOne = "lti.master.Product_categories.detail" + P_ProductCategoriesCreateOne = "lti.master.Product_categories.create" + P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update" + P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete" + + P_ProductsGetAll = "lti.master.Products.list" + P_ProductsGetOne = "lti.master.Products.detail" + P_ProductsCreateOne = "lti.master.Products.create" + P_ProductsUpdateOne = "lti.master.Products.update" + P_ProductsDeleteOne = "lti.master.Products.delete" + + P_SuppliersGetAll = "lti.master.suppliers.list" + P_SuppliersGetOne = "lti.master.suppliers.detail" + P_SuppliersCreateOne = "lti.master.suppliers.create" + P_SuppliersUpdateOne = "lti.master.suppliers.update" + P_SuppliersDeleteOne = "lti.master.suppliers.delete" + + P_UomsGetAll = "lti.master.uoms.list" + P_UomsGetOne = "lti.master.uoms.detail" + P_UomsCreateOne = "lti.master.uoms.create" + P_UomsUpdateOne = "lti.master.uoms.update" + P_UomsDeleteOne = "lti.master.uoms.delete" + + P_WarehousesGetAll = "lti.master.warehouses.list" + P_WarehousesGetOne = "lti.master.warehouses.detail" + P_WarehousesCreateOne = "lti.master.warehouses.create" + P_WarehousesUpdateOne = "lti.master.warehouses.update" + P_WarehousesDeleteOne = "lti.master.warehouses.delete" +) + +const ( + P_ChickinsCreateOne = "lti.production.chickins.create" + P_ChickinsGetOne = "lti.production.chickins.detail" + P_ChickinsApproval = "lti.production.chickins.approve" +) + +// recording +const ( + P_RecordingGetAll = "lti.production.recording.list" + P_RecordingGetOne = "lti.production.recording.detail" + P_RecordingCreateOne = "lti.production.recording.create" + P_RecordingUpdateOne = "lti.production.recording.update" + P_RecordingDeleteOne = "lti.production.recording.delete" + P_RecordingNextDay = "lti.production.recording.next_day" + P_RecordingApproval = "lti.production.recording.approve" +) + +const ( + P_PurchaseGetAll = "lti.Purchase.list" + P_PurchaseGetOne = "lti.Purchase.detail" + P_PurchaseCreateOne = "lti.Purchase.create" + P_PurchaseUpdateOne = "lti.Purchase.update" + P_PurchaseDeleteOne = "lti.Purchase.delete" + P_PurchaseItemDeleteOne = "lti.Purchase.delete.item" + P_PurchaseReceive = "lti.Purchase.receive" + P_PurchaseApprovalStaff = "lti.Purchase.approve.staff" + P_PurchaseApprovalManager = "lti.Purchase.approve.manager" +) + +const ( + P_UserGetAll = "lti.users.list" + P_UserGetOne = "lti.users.detail" +) diff --git a/internal/modules/approvals/route.go b/internal/modules/approvals/route.go index 5dd39616..cd479c03 100644 --- a/internal/modules/approvals/route.go +++ b/internal/modules/approvals/route.go @@ -15,5 +15,5 @@ func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalServic route := v1.Group("/approvals") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) + route.Get("/", ctrl.GetAll,m.RequirePermissions(m.P_ApprovalGetAll)) } diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index a04fc5f9..c4580efb 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -245,3 +245,109 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error { Data: payload, }) } + +func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error { + param := c.Params("project_flock_id") + + projectFlockID, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id") + } + + result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get closing keuangan successfully", + Data: result, + }) +} + +func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error { + param := c.Params("project_flock_id") + + projectFlockID, err := strconv.Atoi(param) + if err != nil || projectFlockID <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") + } + + var projectFlockKandangID *uint + if raw := c.Query("project_flock_kandang_id"); raw != "" { + idInt, convErr := strconv.Atoi(raw) + if convErr != nil || idInt <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id") + } + idUint := uint(idInt) + projectFlockKandangID = &idUint + } + + result, err := u.ClosingService.GetExpeditionHPP(c, uint(projectFlockID), projectFlockKandangID) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get expedition HPP successfully", + Data: result, + }) +} + +func (u *ClosingController) GetExpeditionHPPByKandang(c *fiber.Ctx) error { + projectParam := c.Params("project_flock_id") + kandangParam := c.Params("project_flock_kandang_id") + + projectFlockID, err := strconv.Atoi(projectParam) + if err != nil || projectFlockID <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") + } + + pfkID, err := strconv.Atoi(kandangParam) + if err != nil || pfkID <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id") + } + + kandangID := uint(pfkID) + + result, err := u.ClosingService.GetExpeditionHPP(c, uint(projectFlockID), &kandangID) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get expedition HPP successfully", + Data: result, + }) +} + +func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error { + param := c.Params("projectFlockId") + + id, err := strconv.Atoi(param) + if err != nil || id <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId") + } + + result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Retrieved production data successfully", + Data: result, + }) +} diff --git a/internal/modules/closings/dto/closing.dto.go b/internal/modules/closings/dto/closing.dto.go index 1f1cb492..c05bd741 100644 --- a/internal/modules/closings/dto/closing.dto.go +++ b/internal/modules/closings/dto/closing.dto.go @@ -58,6 +58,52 @@ type ClosingSummaryDTO struct { StatusClosing string `json:"closing_status"` } +type ClosingPurchaseDTO struct { + InitialPopulation int `json:"initial_population"` + ClaimCulling int `json:"claim_culling"` + FinalPopulation int `json:"final_population"` + FeedIn float64 `json:"feed_in"` + FeedUsed float64 `json:"feed_used"` + FeedUsedPerHead float64 `json:"feed_used_per_head"` +} + +type ClosingSalesDTO struct { + SalesPopulation int `json:"sales_population"` + SalesWeight float64 `json:"sales_weight"` + AverageWeight float64 `json:"average_weight"` + AverageSellingPrice float64 `json:"chicken_average_selling_price"` +} + +type ClosingEggSalesDTO struct { + EggPieces int `json:"egg_pieces"` + EggMassKg float64 `json:"egg_mass_kg"` + AverageEggWeightKg float64 `json:"average_egg_weight_kg"` + AverageSellingPrice float64 `json:"egg_average_selling_price"` +} + +type ClosingPerformanceDTO struct { + Depletion float64 `json:"depletion"` + Age float64 `json:"age_day"` + MortalityStd float64 `json:"mortality_std"` + MortalityAct float64 `json:"mortality_act"` + DeffMortality float64 `json:"deff_mortality"` + FcrStd float64 `json:"fcr_std"` + FcrAct float64 `json:"fcr_act"` + DeffFcr float64 `json:"deff_fcr"` + Awg float64 `json:"awg"` +} + +type ClosingSalesGroupDTO struct { + Chicken ClosingSalesDTO `json:"chicken"` + Egg *ClosingEggSalesDTO `json:"egg,omitempty"` +} + +type ClosingProductionReportDTO struct { + Purchase ClosingPurchaseDTO `json:"purchase"` + Sales ClosingSalesGroupDTO `json:"sales"` + Performance ClosingPerformanceDTO `json:"performance"` +} + func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO { history := project.KandangHistory @@ -158,3 +204,20 @@ func ToClosingDetailDTO(e entity.ProjectFlock) ClosingDetailDTO { ClosingListDTO: ToClosingListDTO(e), } } + +func CalculateAgeFromChickinDataProduksi(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int { + if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 { + return 0 + } + + earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate + for _, chickin := range projectFlockKandang.Chickins { + if chickin.ChickInDate.Before(earliestChickinDate) { + earliestChickinDate = chickin.ChickInDate + } + } + + ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24) + ageInWeeks := ageInDays / 7 + return ageInWeeks +} diff --git a/internal/modules/closings/dto/closingExpedition.dto.go b/internal/modules/closings/dto/closingExpedition.dto.go new file mode 100644 index 00000000..5f8a09d4 --- /dev/null +++ b/internal/modules/closings/dto/closingExpedition.dto.go @@ -0,0 +1,14 @@ +package dto + +// ExpeditionCostItemDTO merepresentasikan biaya ekspedisi per vendor. +type ExpeditionCostItemDTO struct { + Id uint64 `json:"id"` + ExpeditionVendorName string `json:"expedition_vendor_name"` + HPPAmount float64 `json:"hpp_amount"` +} + +// ExpeditionHPPDTO adalah struktur response utama untuk HPP Ekspedisi. +type ExpeditionHPPDTO struct { + ExpeditionCosts []ExpeditionCostItemDTO `json:"expedition_costs"` + TotalHPPAmount float64 `json:"total_hpp_amount"` +} diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go new file mode 100644 index 00000000..90dda2a9 --- /dev/null +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -0,0 +1,568 @@ +package dto + +import ( + "slices" + "strings" + + "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/utils" +) + +// === CONSTANTS === +const ( + HPPGroupPengeluaran = "HPP dan Pengeluaran" + HPPGroupBahanBaku = "HPP dan Bahan Baku" + HPPLabelOverhead = "Pengeluaran Overhead" + HPPLabelEkspedisi = "Beban Ekspedisi" + HPPSummaryLabel = "HPP" + + PLSalesTypeChicken = "Penjualan Ayam Besar" + PLSalesTypeEgg = "Penjualan Telur" + + PLItemTypeSapronak = "Pembelian Sapronak" + PLItemTypeOverhead = "Pengeluaran Overhead" + PLItemTypeEkspedisi = "Beban Ekspedisi" + + PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO" + PLSummaryLabelSubTotal = "SUB TOTAL" + PLSummaryLabelNetProfit = "LABA RUGI NETTO" + + PurchaseLabelPrefix = "Pembelian " +) + +// === CONTEXT STRUCTS === + +type CalculationContext struct { + TotalPopulation float64 + TotalWeightProduced float64 + TotalDepletion float64 + TotalWeightSold float64 + ActualPopulation float64 +} + +type ClosingKeuanganInput struct { + ProjectFlockCategory string + PurchaseItems []entities.PurchaseItem + Budgets []entities.ProjectBudget + Realizations []entities.ExpenseRealization + DeliveryProducts []entities.MarketingDeliveryProduct + Chickins []entities.ProjectChickin + TotalWeightProduced float64 + TotalDepletion float64 +} + +// === BASE METRICS === + +type FinancialMetrics struct { + RpPerBird float64 `json:"rp_per_bird"` + RpPerKg float64 `json:"rp_per_kg"` + Amount float64 `json:"amount"` +} + +type Comparison struct { + Budgeting FinancialMetrics `json:"budgeting"` + Realization FinancialMetrics `json:"realization"` +} + +// === HPP PURCHASES PACKAGE === + +type HppItem struct { + Type string `json:"type"` + Comparison +} + +type HppGroup struct { + GroupName string `json:"group_name"` + Data []HppItem `json:"data"` +} + +type SummaryHpp struct { + Label string `json:"label"` + Comparison +} + +type HppPurchasesSection struct { + Hpp []HppGroup `json:"hpp"` + SummaryHpp SummaryHpp `json:"summary_hpp"` +} + +// === PROFIT LOSS PACKAGE === + +type PLItem struct { + Type string `json:"type"` + FinancialMetrics +} + +type PLSummaryItem struct { + Label string `json:"label"` + FinancialMetrics +} + +type PLSummaryGroup struct { + GrossProfit PLSummaryItem `json:"gross_profit"` + SubTotal PLSummaryItem `json:"sub_total"` + NetProfit PLSummaryItem `json:"net_profit"` +} + +type ProfitLossData struct { + Penjualan []PLItem `json:"penjualan"` + Pembelian []PLItem `json:"pembelian"` + Overhead PLItem `json:"overhead"` + Ekspedisi PLItem `json:"ekspedisi"` + Summary PLSummaryGroup `json:"summary"` +} + +type ProfitLossSection struct { + Data ProfitLossData `json:"data"` +} + +// === RESPONSE DTO (ROOT) === + +type ReportResponse struct { + HppPurchases HppPurchasesSection `json:"hpp_purchases"` + ProfitLoss ProfitLossSection `json:"profit_loss"` +} + +// === MAPPER FUNCTIONS === + +func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { + return FinancialMetrics{ + RpPerBird: rpPerBird, + RpPerKg: rpPerKg, + Amount: amount, + } +} + +func ToComparison(budgeting, realization FinancialMetrics) Comparison { + return Comparison{ + Budgeting: budgeting, + Realization: realization, + } +} + +// === HPP PENGELUARAN (from Purchase Items) === + +func getFlagLabel(flagType utils.FlagType) string { + return PurchaseLabelPrefix + string(flagType) +} + +func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem { + flags := []utils.FlagType{ + utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan, + utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher, + utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia, + } + + items := []HppItem{} + seenFlags := make(map[utils.FlagType]bool) + + for _, item := range purchaseItems { + if item.Product == nil || len(item.Product.Flags) == 0 { + continue + } + + for _, flag := range item.Product.Flags { + flagType := utils.FlagType(flag.Name) + + if slices.Contains(flags, flagType) && !seenFlags[flagType] { + amount := sumPurchasesByFlag(purchaseItems, flagType) + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced) + + items = append(items, HppItem{ + Type: getFlagLabel(flagType), + Comparison: ToComparison( + ToFinancialMetrics(rpPerBird, rpPerKg, amount), + ToFinancialMetrics(rpPerBird, rpPerKg, amount), + ), + }) + seenFlags[flagType] = true + } + } + } + + return items +} + +// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) === + +func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem { + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced) + + return HppItem{ + Type: HPPLabelOverhead, + Comparison: ToComparison( + ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount), + ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount), + ), + } +} + +func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem { + ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced) + + return HppItem{ + Type: HPPLabelEkspedisi, + Comparison: ToComparison( + ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), + ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount), + ), + } +} + +func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup { + items := []HppItem{} + + budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) + realizationAmount := getOperationalExpenses(realizations) + + if budgetAmount > 0 || realizationAmount > 0 { + items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx)) + } + + ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) + items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx)) + + return HppGroup{ + GroupName: HPPGroupBahanBaku, + Data: items, + } +} + +// === HPP SUMMARY === + +func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) SummaryHpp { + purchaseTotal := sumPurchaseTotal(purchaseItems) + budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true }) + totalBudget := purchaseTotal + budgetTotal + + totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true }) + + budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced) + realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced) + + return SummaryHpp{ + Label: label, + Comparison: ToComparison( + ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget), + ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization), + ), + } +} + +func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppPurchasesSection { + hppGroups := []HppGroup{ + { + GroupName: HPPGroupPengeluaran, + Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx), + }, + ToHppBahanBakuGroup(budgets, realizations, ctx), + } + + summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, ctx) + + return HppPurchasesSection{ + Hpp: hppGroups, + SummaryHpp: summaryHpp, + } +} + +// === PROFIT & LOSS === + +func ToPLItem(itemType string, metrics FinancialMetrics) PLItem { + return PLItem{ + Type: itemType, + FinancialMetrics: metrics, + } +} + +func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem { + return PLSummaryItem{ + Label: label, + FinancialMetrics: metrics, + } +} + +func createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem { + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced) + return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount)) +} + +func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) { + for _, item := range items { + totalAmount += item.Amount + totalPerBird += item.RpPerBird + } + return +} + +func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem { + rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold) + return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount)) +} + +func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem { + items := []PLItem{} + + categorized := categorizeDeliveriesBySalesType(deliveryProducts) + + if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) { + ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken]) + telurAmount := sumDeliveriesByCategory(categorized[PLSalesTypeEgg]) + + items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx)) + items = append(items, createPenjualanItem(PLSalesTypeEgg, telurAmount, ctx)) + } else { + ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken]) + items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx)) + } + + return items +} + +func ToPembelianItems(purchases []entities.PurchaseItem, realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem { + purchaseAmount := sumPurchaseTotal(purchases) + bopAmount := getOperationalExpenses(realizations) + totalCost := purchaseAmount + bopAmount + + return []PLItem{ + createPLItemWithMetrics(PLItemTypeSapronak, totalCost, ctx), + } +} + +func ToOverheadItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem { + realizationAmount := getOperationalExpenses(realizations) + return []PLItem{ + createPLItemWithMetrics(PLItemTypeOverhead, realizationAmount, ctx), + } +} + +func ToEkspedisiItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem { + amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi)) + return []PLItem{ + createPLItemWithMetrics(PLItemTypeEkspedisi, amount, ctx), + } +} + +func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup { + totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems) + totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems) + totalOverhead, totalOverheadPerBird := sumPLItems(overheadItems) + totalEkspedisi, totalEkspedisiPerBird := sumPLItems(ekspedisiItems) + + grossProfit := totalPenjualan - totalPembelian + grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird + + totalOtherExpenses := totalOverhead + totalEkspedisi + totalOtherExpensesPerBird := totalOverheadPerBird + totalEkspedisiPerBird + + netProfit := grossProfit - totalOtherExpenses + netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird + + return PLSummaryGroup{ + GrossProfit: ToPLSummaryItem(PLSummaryLabelGrossProfit, ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), + SubTotal: ToPLSummaryItem(PLSummaryLabelSubTotal, ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)), + NetProfit: ToPLSummaryItem(PLSummaryLabelNetProfit, ToFinancialMetrics(netProfitPerBird, 0, netProfit)), + } +} + +func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData { + summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) + + totalOverhead := aggregatePLItems(overheadItems, PLItemTypeOverhead) + totalEkspedisi := aggregatePLItems(ekspedisiItems, PLItemTypeEkspedisi) + + return ProfitLossData{ + Penjualan: penjualanItems, + Pembelian: pembelianItems, + Overhead: totalOverhead, + Ekspedisi: totalEkspedisi, + Summary: summary, + } +} + +func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection { + return ProfitLossSection{ + Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems), + } +} + +func aggregatePLItems(items []PLItem, label string) PLItem { + totalAmount, totalPerBird := sumPLItems(items) + return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount)) +} + +func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse { + return ReportResponse{ + HppPurchases: hppPurchases, + ProfitLoss: profitLoss, + } +} + +func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse { + var totalPopulation float64 + var totalWeightSold float64 + + for _, chickin := range input.Chickins { + totalPopulation += chickin.UsageQty + } + + for _, delivery := range input.DeliveryProducts { + totalWeightSold += delivery.TotalWeight + } + + ctx := CalculationContext{ + TotalPopulation: totalPopulation, + TotalWeightProduced: input.TotalWeightProduced, + TotalDepletion: input.TotalDepletion, + TotalWeightSold: totalWeightSold, + ActualPopulation: totalPopulation - input.TotalDepletion, + } + + hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, ctx) + penjualanItems := ToPenjualanItems(input.ProjectFlockCategory, input.DeliveryProducts, ctx) + pembelianItems := ToPembelianItems(input.PurchaseItems, input.Realizations, ctx) + overheadItems := ToOverheadItems(input.Realizations, ctx) + ekspedisiItems := ToEkspedisiItems(input.Realizations, ctx) + plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems) + + return ToReportResponse(hppSection, plSection) +} + +// === HELPER FUNCTIONS === + +func calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold float64) (rpPerBird, rpPerKg float64) { + if totalPopulation > 0 { + rpPerBird = amount / totalPopulation + } + if totalWeightSold > 0 { + rpPerKg = amount / totalWeightSold + } + return rpPerBird, rpPerKg +} + +func hasProductFlag(flags []entities.Flag, flagType utils.FlagType) bool { + for _, flag := range flags { + if strings.ToUpper(flag.Name) == string(flagType) { + return true + } + } + return false +} + +func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool { + return func(item *entities.PurchaseItem) bool { + if item.Product == nil || len(item.Product.Flags) == 0 { + return false + } + return hasProductFlag(item.Product.Flags, flagType) + } +} + +func filterRealizationByNonstockFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool { + return func(realization *entities.ExpenseRealization) bool { + if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Nonstock == nil { + return false + } + return hasProductFlag(realization.ExpenseNonstock.Nonstock.Flags, flagType) + } +} + +func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool { + hasFlag := filterRealizationByNonstockFlag(flagType) + return func(realization *entities.ExpenseRealization) bool { + return !hasFlag(realization) + } +} + +func sumByFilter[T any](items []T, extractor func(*T) float64, filter func(*T) bool) float64 { + amount := 0.0 + for i := range items { + if filter(&items[i]) { + amount += extractor(&items[i]) + } + } + return amount +} + +func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 { + return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, filter) +} + +func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 { + return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType)) +} + +func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 { + return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, func(*entities.PurchaseItem) bool { return true }) +} + +func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 { + return sumByFilter(budgets, func(b *entities.ProjectBudget) float64 { return b.Price * b.Qty }, filter) +} + +func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 { + return sumByFilter(realizations, func(r *entities.ExpenseRealization) float64 { return r.Price * r.Qty }, filter) +} + +func getOperationalExpenses(realizations []entities.ExpenseRealization) float64 { + return sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi)) +} + +func isChickenProductFlag(flagType utils.FlagType) bool { + switch flagType { + case utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, + utils.FlagAyamAfkir, utils.FlagAyamCulling, utils.FlagAyamMati: + return true + } + return false +} + +func isEggProductFlag(flagType utils.FlagType) bool { + switch flagType { + case utils.FlagTelur, utils.FlagTelurUtuh, utils.FlagTelurPecah, + utils.FlagTelurPutih, utils.FlagTelurRetak: + return true + } + return false +} + +func getSalesTypeFromProductFlags(product *entities.Product) string { + if product == nil || len(product.Flags) == 0 { + return PLSalesTypeChicken + } + + for _, flag := range product.Flags { + flagType := utils.FlagType(strings.ToUpper(flag.Name)) + + if isEggProductFlag(flagType) { + return PLSalesTypeEgg + } + if isChickenProductFlag(flagType) { + return PLSalesTypeChicken + } + } + + return PLSalesTypeChicken +} + +func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct { + categorized := make(map[string][]entities.MarketingDeliveryProduct) + + for _, delivery := range deliveries { + product := delivery.MarketingProduct.ProductWarehouse.Product + salesType := getSalesTypeFromProductFlags(&product) + + categorized[salesType] = append(categorized[salesType], delivery) + } + + return categorized +} + +func sumDeliveriesByCategory(deliveries []entities.MarketingDeliveryProduct) float64 { + amount := 0.0 + for _, delivery := range deliveries { + amount += delivery.TotalPrice + } + return amount +} diff --git a/internal/modules/closings/dto/closingMarketing.dto.go b/internal/modules/closings/dto/closingMarketing.dto.go index ea0ddb81..8c904561 100644 --- a/internal/modules/closings/dto/closingMarketing.dto.go +++ b/internal/modules/closings/dto/closingMarketing.dto.go @@ -35,8 +35,7 @@ type PenjualanRealisasiResponseDTO struct { func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO { - // todo: usia ayam masih dummy - age := 0 + age := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate) var product *productDTO.ProductRelationDTO if e.MarketingProduct.ProductWarehouse.Product.Id != 0 { @@ -101,3 +100,20 @@ func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int } return 0 } + +func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int { + if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 { + return 0 + } + + earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate + for _, chickin := range projectFlockKandang.Chickins { + if chickin.ChickInDate.Before(earliestChickinDate) { + earliestChickinDate = chickin.ChickInDate + } + } + + ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24) + ageInWeeks := ageInDays / 7 + return ageInWeeks +} diff --git a/internal/modules/closings/dto/closingOverhead.dto.go b/internal/modules/closings/dto/closingOverhead.dto.go index 95f3e10b..71975da1 100644 --- a/internal/modules/closings/dto/closingOverhead.dto.go +++ b/internal/modules/closings/dto/closingOverhead.dto.go @@ -69,7 +69,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal return dto } -func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty float64) OverheadListDTO { +func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO { overheadsByNonstockID := make(map[uint]*OverheadDTO) latestDateByNonstockID := make(map[uint]string) @@ -119,7 +119,8 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex for nonstockID, overhead := range overheadsByNonstockID { overhead.ActualDate = latestDateByNonstockID[nonstockID] - overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalChickinQty) + + overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalActualPopulation) if overhead.ActualQuantity > 0 { overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity @@ -139,7 +140,7 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex BudgetTotalAmount: totalBudgetAmount, ActualQuantity: totalActualQuantity, ActualTotalAmount: totalActualAmount, - CostPerBird: calculateCostPerBird(totalActualAmount, totalChickinQty), + CostPerBird: calculateCostPerBird(totalActualAmount, totalActualPopulation), }, Overheads: overheadItems, } @@ -158,9 +159,9 @@ func calculateTotal(qty, price float64) float64 { return qty * price } -func calculateCostPerBird(totalPrice, totalChickinQty float64) float64 { - if totalChickinQty > 0 { - return totalPrice / totalChickinQty +func calculateCostPerBird(totalPrice, totalActualPopulation float64) float64 { + if totalActualPopulation > 0 { + return totalPrice / totalActualPopulation } return 0 } diff --git a/internal/modules/closings/dto/sapronak.dto.go b/internal/modules/closings/dto/closingSapronak.dto.go similarity index 100% rename from internal/modules/closings/dto/sapronak.dto.go rename to internal/modules/closings/dto/closingSapronak.dto.go diff --git a/internal/modules/closings/module.go b/internal/modules/closings/module.go index c3de4a86..c89e6125 100644 --- a/internal/modules/closings/module.go +++ b/internal/modules/closings/module.go @@ -13,6 +13,8 @@ import ( rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" + rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -30,10 +32,12 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db) expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db) chickinRepo := rChickin.NewChickinRepository(db) + recordingRepo := rRecording.NewRecordingRepository(db) + purchaseRepo := rPurchase.NewPurchaseRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) - closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate) + closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index 7b568801..6a59c5f9 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -16,6 +16,13 @@ import ( type ClosingRepository interface { repository.BaseRepository[entity.ProjectFlock] GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) + SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) + SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) + SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) + SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) + SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) + GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) + GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) @@ -53,6 +60,11 @@ type SapronakRow struct { Notes string `gorm:"column:notes"` } +type ExpeditionHPPRow struct { + SupplierName string `gorm:"column:supplier_name"` + TotalAmount float64 `gorm:"column:total_amount"` +} + type SapronakQueryParams struct { Type string WarehouseIDs []uint @@ -111,6 +123,202 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak return rows, totalResults, nil } +func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, 0, nil + } + + var purchaseAgg struct { + TotalIn float64 `gorm:"column:total_in"` + } + + err := r.DB().WithContext(ctx). + Table("purchase_items pi"). + Joins("JOIN flags f ON f.flagable_id = pi.product_id AND f.flagable_type = 'products'"). + Where("f.name = ?", "PAKAN"). + Where("pi.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Select("COALESCE(SUM(pi.total_qty), 0) AS total_in"). + Scan(&purchaseAgg).Error + if err != nil { + return 0, 0, err + } + + var usageAgg struct { + TotalUsed float64 `gorm:"column:total_used"` + } + + err = r.DB().WithContext(ctx). + Table("recording_stocks rs"). + Joins("JOIN product_warehouses pw ON pw.id = rs.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name = ?", "PAKAN"). + Select("COALESCE(SUM(COALESCE(rs.usage_qty, 0) + COALESCE(rs.pending_qty, 0)), 0) AS total_used"). + Scan(&usageAgg).Error + if err != nil { + return 0, 0, err + } + + return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil +} + +func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, nil + } + + var agg struct { + Total float64 `gorm:"column:total_culling"` + } + + err := r.DB().WithContext(ctx). + Table("recording_depletions rd"). + Joins("JOIN product_warehouses pw ON pw.id = rd.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name = ?", utils.FlagAyamCulling). + Select("COALESCE(SUM(rd.qty), 0) AS total_culling"). + Scan(&agg).Error + if err != nil { + return 0, err + } + + return agg.Total, nil +} + +func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, 0, 0, nil + } + + var agg struct { + TotalWeight float64 `gorm:"column:total_weight"` + TotalQty float64 `gorm:"column:total_qty"` + TotalPrice float64 `gorm:"column:total_price"` + } + + err := r.DB().WithContext(ctx). + Table("marketing_products mp"). + Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price"). + Scan(&agg).Error + if err != nil { + return 0, 0, 0, err + } + + return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil +} + +func (r *ClosingRepositoryImpl) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) { + if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { + return 0, 0, 0, nil + } + + var agg struct { + TotalWeight float64 `gorm:"column:total_weight"` + TotalQty float64 `gorm:"column:total_qty"` + TotalPrice float64 `gorm:"column:total_price"` + } + + err := r.DB().WithContext(ctx). + Table("marketing_products mp"). + Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name IN ?", flagNames). + Select("COALESCE(SUM(mp.total_weight), 0) AS total_weight, COALESCE(SUM(mp.qty), 0) AS total_qty, COALESCE(SUM(mp.total_price), 0) AS total_price"). + Scan(&agg).Error + if err != nil { + return 0, 0, 0, err + } + + return agg.TotalWeight, agg.TotalQty, agg.TotalPrice, nil +} + +func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error) { + if len(projectFlockKandangIDs) == 0 || len(flagNames) == 0 { + return 0, nil + } + + var agg struct { + TotalQty float64 `gorm:"column:total_qty"` + } + + err := r.DB().WithContext(ctx). + Table("recording_eggs re"). + Joins("JOIN product_warehouses pw ON pw.id = re.product_warehouse_id"). + Joins("JOIN products prod ON prod.id = pw.product_id"). + Joins("JOIN flags f ON f.flagable_id = prod.id AND f.flagable_type = ?", "products"). + Where("pw.project_flock_kandang_id IN ?", projectFlockKandangIDs). + Where("f.name IN ?", flagNames). + Select("COALESCE(SUM(re.qty), 0) AS total_qty"). + Scan(&agg).Error + if err != nil { + return 0, err + } + + return agg.TotalQty, nil +} + +func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) { + if fcrID == 0 { + return []entity.FcrStandard{}, nil + } + + var standards []entity.FcrStandard + if err := r.DB().WithContext(ctx). + Where("fcr_id = ?", fcrID). + Order("weight ASC"). + Find(&standards).Error; err != nil { + return nil, err + } + + return standards, nil +} + +func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) { + db := r.DB().WithContext(ctx) + + if projectFlockID == 0 { + return nil, fmt.Errorf("invalid project flock id") + } + + query := db. + Table("expense_realizations AS er"). + Joins("JOIN expense_nonstocks ens ON ens.id = er.expense_nonstock_id"). + Joins("JOIN expenses e ON e.id = ens.expense_id"). + Joins("JOIN project_flock_kandangs pfk ON pfk.id = ens.project_flock_kandang_id"). + Joins("JOIN nonstocks n ON n.id = ens.nonstock_id"). + Joins("JOIN flags f ON f.flagable_id = n.id AND f.flagable_type = ?", entity.FlagableTypeNonstock). + Joins("JOIN suppliers s ON s.id = e.supplier_id"). + Where("pfk.project_flock_id = ?", projectFlockID). + Where("e.category = ?", "BOP"). + Where("UPPER(f.name) = ?", strings.ToUpper(string(utils.FlagEkspedisi))) + + if projectFlockKandangID != nil && *projectFlockKandangID != 0 { + query = query.Where("pfk.id = ?", *projectFlockKandangID) + } + + var rows []ExpeditionHPPRow + err := query. + Select( + "e.supplier_id AS supplier_id, " + + "s.name AS supplier_name, " + + "SUM(er.qty * er.price) AS total_amount", + ). + Group("e.supplier_id, s.name"). + Scan(&rows).Error + if err != nil { + return nil, err + } + + return rows, nil +} + const ( sapronakIncomingPurchasesSQL = ` SELECT @@ -260,7 +468,6 @@ type SapronakDetailRow struct { Price float64 } - func (r *ClosingRepositoryImpl) withCtx(ctx context.Context) *gorm.DB { return r.DB().WithContext(ctx) } func applyJoins(db *gorm.DB, joins ...string) *gorm.DB { @@ -323,7 +530,7 @@ func (r *ClosingRepositoryImpl) usageQuery( `) db = applyJoins(db, joins...) return db. - Joins("JOIN product_warehouses pw ON " + pwJoinCond). + Joins("JOIN product_warehouses pw ON "+pwJoinCond). Joins("JOIN products p ON p.id = pw.product_id"). Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where(where, args...) @@ -356,7 +563,7 @@ func (r *ClosingRepositoryImpl) detailQuery( ) *gorm.DB { db := r.withCtx(ctx). Table(table). - Joins("JOIN product_warehouses pw ON " + pwJoinCond). + Joins("JOIN product_warehouses pw ON "+pwJoinCond). Joins("JOIN products p ON p.id = pw.product_id"). Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct) @@ -450,7 +657,6 @@ func (r *ClosingRepositoryImpl) FetchSapronakChickinUsageDetails(ctx context.Con ) } - func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB { return r.withCtx(ctx). Table("purchase_items AS pi"). @@ -527,7 +733,7 @@ func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID ui COALESCE(sl.increase,0) AS increase, COALESCE(sl.decrease,0) AS decrease, COALESCE(p.product_price,0) AS price, - ` + movementSelect + ` + `+movementSelect+` `). Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id"). Joins("JOIN products p ON p.id = pw.product_id"). @@ -597,4 +803,4 @@ func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kand return fmt.Sprintf("TRF-%d", row.ID) }) return in, out, nil -} \ No newline at end of file +} diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index a76e8b79..52333b67 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -1,7 +1,7 @@ package closings import ( - // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/controllers" closing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -13,6 +13,7 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService ctrl := controller.NewClosingController(s, sapronakSvc) route := v1.Group("/closings") + route.Use(m.Auth(u)) // route.Get("/", m.Auth(u), ctrl.GetAll) // route.Post("/", m.Auth(u), ctrl.CreateOne) @@ -20,11 +21,15 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Get("/", ctrl.GetAll) - route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan) - route.Get("/:project_flock_id/overhead", ctrl.GetOverhead) - route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", ctrl.GetSapronakByKandang) - route.Get("/:project_flock_id/perhitungan_sapronak", ctrl.GetSapronakByProject) - route.Get("/:projectFlockId", ctrl.GetClosingSummary) - route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak) + route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll) + route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan) + route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary) + route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead) + route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang) + route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject) + route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak) + route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP) + route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang) + route.Get("/:projectFlockId/data-produksi", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi) + route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan) } diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index b1780359..48728195 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -3,7 +3,9 @@ package service import ( "context" "errors" + "math" "strconv" + "strings" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -15,6 +17,8 @@ import ( marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" + purchaseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" @@ -30,7 +34,10 @@ type ClosingService interface { GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) + GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) + GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) + GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) } type closingService struct { @@ -44,9 +51,11 @@ type closingService struct { ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository ChickinRepo chickinRepository.ProjectChickinRepository + PurchaseRepo purchaseRepository.PurchaseRepository + RecordingRepo recordingRepository.RecordingRepository } -func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, validate *validator.Validate) ClosingService { +func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService { return &closingService{ Log: utils.Log, Validate: validate, @@ -58,6 +67,8 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje ExpenseRealizationRepo: expenseRealizationRepo, ProjectBudgetRepo: projectBudgetRepo, ChickinRepo: chickinRepo, + PurchaseRepo: purchaseRepo, + RecordingRepo: recordingRepo, } } @@ -130,6 +141,7 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entit Preload("MarketingProduct.ProductWarehouse.Warehouse"). Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang"). Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang"). + Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins"). Preload("MarketingProduct.Marketing"). Preload("MarketingProduct.Marketing.Customer"). Order("marketing_delivery_products.delivery_date DESC") @@ -375,7 +387,396 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove totalChickinQty += chickin.UsageQty } - result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty) + totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) + } + + totalActualPopulation := totalChickinQty - totalDepletion + + result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation) return &result, nil } + +func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) { + if projectFlockID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + } + + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: func(ctx context.Context, id uint) (bool, error) { + _, err := s.ProjectFlockRepo.GetByID(ctx, id, nil) + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return err == nil, err + }}, + ); err != nil { + return nil, err + } + + projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), projectFlockID, nil) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") + } + + budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") + } + + purchaseItems, err := s.PurchaseRepo.GetItemsByProjectFlockID(c.Context(), projectFlockID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch purchase items") + } + + realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations") + } + + deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { + return db.Preload("MarketingProduct"). + Preload("MarketingProduct.ProductWarehouse"). + Preload("MarketingProduct.ProductWarehouse.Product") + }) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products") + } + + chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") + } + + totalWeightProduced, _, err := s.RecordingRepo.GetProductionWeightAndQtyByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Warnf("GetProductionWeightAndQtyByProjectFlockID error: %v", err) + } + + totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) + } + + input := dto.ClosingKeuanganInput{ + ProjectFlockCategory: projectFlock.Category, + PurchaseItems: purchaseItems, + Budgets: budgets, + Realizations: realizations, + DeliveryProducts: deliveryProducts, + Chickins: chickins, + TotalWeightProduced: totalWeightProduced, + TotalDepletion: totalDepletion, + } + + report := dto.ToClosingKeuanganReport(input) + + return &report, nil +} + +// GetExpeditionHPP menghitung HPP ekspedisi per vendor untuk sebuah project flock. +// Jika projectFlockKandangID tidak nil, maka hanya data untuk kandang tersebut yang dihitung. +func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) { + if projectFlockID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + } + + rows, err := s.Repository.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID) + if err != nil { + s.Log.Errorf("Failed to get expedition HPP for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch expedition HPP") + } + + expeditionCosts := make([]dto.ExpeditionCostItemDTO, 0, len(rows)) + var totalHPP float64 + + for idx, row := range rows { + expeditionCosts = append(expeditionCosts, dto.ExpeditionCostItemDTO{ + Id: uint64(idx + 1), + ExpeditionVendorName: row.SupplierName, + HPPAmount: row.TotalAmount, + }) + + totalHPP += row.TotalAmount + } + + result := &dto.ExpeditionHPPDTO{ + ExpeditionCosts: expeditionCosts, + TotalHPPAmount: totalHPP, + } + + return result, nil +} + +func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) { + if projectFlockID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + } + + project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") + } + if err != nil { + s.Log.Errorf("Failed get project flock %d for closing data produksi: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") + } + + var population float64 + for _, history := range project.KandangHistory { + for _, chickin := range history.Chickins { + population += chickin.UsageQty + chickin.PendingUsageQty + } + } + + isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing)) + + projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID) + if err != nil { + s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs") + } + + feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) + if err != nil { + s.Log.Errorf("Failed to sum feed purchase/used qty for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data") + } + + claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) + if err != nil { + s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch claim culling data") + } + + finalPopulation := population - claimCulling + + var standards []entity.FcrStandard + if project.FcrId > 0 { + standards, err = s.Repository.GetFcrStandardsByFcrID(c.Context(), project.FcrId) + if err != nil { + s.Log.Errorf("Failed to fetch FCR standards for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data") + } + } + age, err := s.calculateAverageSalesAge(c.Context(), projectFlockID) + if err != nil { + s.Log.Errorf("Failed to calculate sales age for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data") + } + + feedUsedPerHead := 0.0 + if population > 0 { + feedUsedPerHead = feedUsed / population + } + + purchase := dto.ClosingPurchaseDTO{ + InitialPopulation: int(population), + ClaimCulling: int(claimCulling), + FinalPopulation: int(finalPopulation), + FeedIn: feedIn, + FeedUsed: feedUsed, + FeedUsedPerHead: feedUsedPerHead, + } + + chickenFlagNames := []string{string(utils.FlagPullet)} + chickenSalesWeight, chickenSalesQty, chickenSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, chickenFlagNames) + if err != nil { + s.Log.Errorf("Failed to fetch chicken sales data for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chicken sales data") + } + + var chickenAverageWeight float64 + if chickenSalesQty > 0 { + chickenAverageWeight = chickenSalesWeight / chickenSalesQty + } + + var chickenAverageSellingPrice float64 + if chickenSalesWeight > 0 { + chickenAverageSellingPrice = chickenSalesPrice / chickenSalesWeight + } + + chickenSales := dto.ClosingSalesDTO{ + SalesPopulation: int(chickenSalesQty), + SalesWeight: chickenSalesWeight, + AverageWeight: chickenAverageWeight, + AverageSellingPrice: chickenAverageSellingPrice, + } + + chickenDepletion := population - chickenSalesQty + if chickenDepletion < 0 { + chickenDepletion = 0 + } + + chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards) + + var eggSales *dto.ClosingEggSalesDTO + var eggPerformance *dto.ClosingPerformanceDTO + if !isGrowing { + eggFlagNames := []string{ + string(utils.FlagTelur), + string(utils.FlagTelurUtuh), + string(utils.FlagTelurPecah), + string(utils.FlagTelurPutih), + string(utils.FlagTelurRetak), + } + + eggSalesWeight, eggSalesQty, eggSalesPrice, err := s.Repository.SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) + if err != nil { + s.Log.Errorf("Failed to fetch egg sales data for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg sales data") + } + + var averageEggWeight float64 + if eggSalesQty > 0 { + averageEggWeight = eggSalesWeight / eggSalesQty + } + + var averageEggSellingPrice float64 + if eggSalesWeight > 0 { + averageEggSellingPrice = eggSalesPrice / eggSalesWeight + } + + eggSales = &dto.ClosingEggSalesDTO{ + EggPieces: int(eggSalesQty), + EggMassKg: eggSalesWeight, + AverageEggWeightKg: averageEggWeight, + AverageSellingPrice: averageEggSellingPrice, + } + + harvestEggQty, err := s.Repository.SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(c.Context(), projectFlockKandangIDs, eggFlagNames) + if err != nil { + s.Log.Errorf("Failed to fetch recording egg qty for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch egg harvest data") + } + + eggDepletion := harvestEggQty - eggSalesQty + if eggDepletion < 0 { + eggDepletion = 0 + } + + eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards) + eggPerformance = &eggPerf + } + + sales := dto.ClosingSalesGroupDTO{ + Chicken: chickenSales, + Egg: eggSales, + } + + performance := dto.ClosingPerformanceDTO{ + Depletion: chickenPerformance.Depletion, + Age: age, + MortalityStd: chickenPerformance.MortalityStd, + MortalityAct: chickenPerformance.MortalityAct, + DeffMortality: chickenPerformance.DeffMortality, + } + if eggPerformance != nil { + performance.FcrStd = eggPerformance.FcrStd + performance.FcrAct = eggPerformance.FcrAct + performance.DeffFcr = eggPerformance.DeffFcr + performance.Awg = eggPerformance.Awg + } else { + performance.FcrStd = chickenPerformance.FcrStd + performance.FcrAct = chickenPerformance.FcrAct + performance.DeffFcr = chickenPerformance.DeffFcr + performance.Awg = chickenPerformance.Awg + } + + result := dto.ClosingProductionReportDTO{ + Purchase: purchase, + Sales: sales, + Performance: performance, + } + + return &result, nil +} + +func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlockID uint) (float64, error) { + deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(ctx, projectFlockID, func(db *gorm.DB) *gorm.DB { + return db. + Preload("MarketingProduct"). + Preload("MarketingProduct.ProductWarehouse"). + Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang"). + Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins") + }) + if err != nil { + return 0, err + } + + var ( + totalQty float64 + totalAgeWeeks float64 + ) + + for _, product := range deliveryProducts { + if product.Qty == 0 { + continue + } + projectFlockKandang := product.MarketingProduct.ProductWarehouse.ProjectFlockKandang + ageWeeks := dto.CalculateAgeFromChickinDataProduksi(projectFlockKandang, product.DeliveryDate) + totalAgeWeeks += float64(ageWeeks) * product.Qty + totalQty += product.Qty + } + + if totalQty == 0 { + return 0, nil + } + + return totalAgeWeeks / totalQty, nil +} + +func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO { + mortalityStd, fcrStd := closestFcrValues(standards, averageWeight) + + fcrAct := 0.0 + if totalWeight > 0 { + fcrAct = feedUsed / totalWeight + } + + mortalityAct := 0.0 + if basePopulation > 0 { + mortalityAct = (depletion / basePopulation) * 100 + } + + deffMortality := mortalityAct - mortalityStd + deffFcr := fcrAct - fcrStd + + awg := 0.0 + if age > 0 { + awg = averageWeight / age + } + + return dto.ClosingPerformanceDTO{ + Depletion: depletion, + Age: age, + MortalityStd: mortalityStd, + MortalityAct: mortalityAct, + DeffMortality: deffMortality, + FcrStd: fcrStd, + FcrAct: fcrAct, + DeffFcr: deffFcr, + Awg: awg, + } +} + +func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (float64, float64) { + if len(standards) == 0 || averageWeight <= 0 { + return 0, 0 + } + + closest := standards[0] + minDiff := math.Abs(closest.Weight - averageWeight) + for _, std := range standards[1:] { + diff := math.Abs(std.Weight - averageWeight) + if diff < minDiff { + minDiff = diff + closest = std + } + } + + return closest.Mortality, closest.FcrNumber + +} diff --git a/internal/modules/constants/route.go b/internal/modules/constants/route.go index 1da14371..46def610 100644 --- a/internal/modules/constants/route.go +++ b/internal/modules/constants/route.go @@ -12,6 +12,5 @@ func ConstantRoutes(v1 fiber.Router, s constant.ConstantService) { ctrl := controller.NewConstantController(s) route := v1.Group("/constants") - route.Get("/", ctrl.GetAll) } diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index e4d57b79..474b2962 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -44,12 +44,13 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte Preload("ExpenseNonstock"). Preload("ExpenseNonstock.Nonstock"). Preload("ExpenseNonstock.Nonstock.Uom"). + Preload("ExpenseNonstock.Nonstock.Flags"). Preload("ExpenseNonstock.Expense"). Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). - Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id"). Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). - Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). - Where("expenses.category = ?", "BOP"). + Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id"). + Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id"). + Where("project_flock_kandangs.project_flock_id = ? OR kandangs.id IN (SELECT kandang_id FROM project_flock_kandangs WHERE project_flock_id = ?)", projectFlockID, projectFlockID). Find(&realizations).Error return realizations, err } @@ -66,7 +67,8 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context Preload("Expense.Supplier"). Preload("Kandang"). Preload("Kandang.Location"). - Preload("Nonstock") + Preload("Nonstock"). + Preload("Nonstock.Flags") }). Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). diff --git a/internal/modules/expenses/route.go b/internal/modules/expenses/route.go index 1fc5c07a..fa3191fa 100644 --- a/internal/modules/expenses/route.go +++ b/internal/modules/expenses/route.go @@ -22,16 +22,16 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) - route.Post("/approvals/manager", ctrl.Approval) - route.Post("/approvals/finance", ctrl.Approval) - route.Post("/:id/realizations", ctrl.CreateRealization) - route.Patch("/:id/realizations", ctrl.UpdateRealization) - route.Post("/:id/complete", ctrl.CompleteExpense) - route.Delete("/:id/documents/:documentId", ctrl.DeleteDocument) - route.Delete("/:id/realization-documents/:documentId", ctrl.DeleteRealizationDocument) + route.Get("/",m.RequirePermissions(m.P_ExpenseGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_ExpenseCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_ExpenseGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_ExpenseUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_ExpenseDeleteOne), ctrl.DeleteOne) + route.Post("/approvals/manager",m.RequirePermissions(m.P_ExpenseApprovalManager), ctrl.Approval) + route.Post("/approvals/finance",m.RequirePermissions(m.P_ExpenseApprovalFinance), ctrl.Approval) + route.Post("/:id/realizations",m.RequirePermissions(m.P_ExpenseCreateRealizations), ctrl.CreateRealization) + route.Patch("/:id/realizations",m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization) + route.Post("/:id/complete",m.RequirePermissions(m.P_ExpenseCompleteExpense), ctrl.CompleteExpense) + route.Delete("/:id/documents/:documentId",m.RequirePermissions(m.P_ExpenseDocument), ctrl.DeleteDocument) + route.Delete("/:id/realization-documents/:documentId",m.RequirePermissions(m.P_ExpenseDocumentRealizations), ctrl.DeleteRealizationDocument) } diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index dbfb00c2..24ba4f2e 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -213,7 +213,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen var projectFlockKandangId *uint64 - if req.Category == "BOP" { + if req.Category == string(utils.ExpenseCategoryBOP) { projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID)) if err != nil { @@ -230,10 +230,10 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen nonstockId := costItem.NonstockID var kandangId *uint64 - if req.Category == "NON-BOP" { + if req.Category == string(utils.ExpenseCategoryNonBOP) { id := uint64(expenseNonstock.KandangID) kandangId = &id - } else if req.Category == "BOP" { + } else if req.Category == string(utils.ExpenseCategoryBOP) { if projectFlockKandangId != nil { kandangId = &expenseNonstock.KandangID } @@ -385,7 +385,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } if categoryChanged { - if currentExpense.Category == "BOP" && newCategory == "NON-BOP" { + if currentExpense.Category == string(utils.ExpenseCategoryBOP) && newCategory == string(utils.ExpenseCategoryNonBOP) { var existingExpenseNonstocks []entity.ExpenseNonstock if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { @@ -400,7 +400,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id to null") } } - } else if currentExpense.Category == "NON-BOP" && newCategory == "BOP" { + } else if currentExpense.Category == string(utils.ExpenseCategoryNonBOP) && newCategory == string(utils.ExpenseCategoryBOP) { var existingExpenseNonstocks []entity.ExpenseNonstock if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { @@ -457,7 +457,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) for _, expenseNonstock := range *req.ExpenseNonstocks { var projectFlockKandangId *uint64 - if updatedExpense.Category == "BOP" { + if updatedExpense.Category == string(utils.ExpenseCategoryBOP) { projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx) projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID)) if err != nil { @@ -480,10 +480,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } var kandangId *uint64 - if updatedExpense.Category == "NON-BOP" { + if updatedExpense.Category == string(utils.ExpenseCategoryNonBOP) { id := uint64(expenseNonstock.KandangID) kandangId = &id - } else if updatedExpense.Category == "BOP" { + } else if updatedExpense.Category == string(utils.ExpenseCategoryBOP) { if projectFlockKandangId != nil { kandangId = &expenseNonstock.KandangID } diff --git a/internal/modules/inventory/adjustments/route.go b/internal/modules/inventory/adjustments/route.go index 8c7e5f9f..f99fe01e 100644 --- a/internal/modules/inventory/adjustments/route.go +++ b/internal/modules/inventory/adjustments/route.go @@ -14,9 +14,9 @@ func AdjustmentRoutes(v1 fiber.Router, u user.UserService, s adjustment.Adjustme route := v1.Group("/adjustments") route.Use(m.Auth(u)) - - route.Get("/", ctrl.AdjustmentHistory) - route.Post("/", ctrl.Adjustment) - route.Get("/:id", ctrl.GetOne) + // Standard CRUD routes following master data pattern + route.Get("/",m.RequirePermissions(m.P_AdjustmentGetAll), ctrl.AdjustmentHistory) // Get all with pagination and filters + route.Post("/",m.RequirePermissions(m.P_AdjustmentCreate), ctrl.Adjustment) // Create adjustment + route.Get("/:id",m.RequirePermissions(m.P_AdjustmentGetOne), ctrl.GetOne) } diff --git a/internal/modules/inventory/product-stocks/route.go b/internal/modules/inventory/product-stocks/route.go index c7bb37f8..41714edc 100644 --- a/internal/modules/inventory/product-stocks/route.go +++ b/internal/modules/inventory/product-stocks/route.go @@ -1,7 +1,7 @@ package productStocks import ( - // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/controllers" productStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -13,13 +13,13 @@ func ProductStockRoutes(v1 fiber.Router, u user.UserService, s productStock.Prod ctrl := controller.NewProductStockController(s) route := v1.Group("/product-stocks") - +route.Use(m.Auth(u)) // route.Get("/", m.Auth(u), ctrl.GetAll) // route.Post("/", m.Auth(u), ctrl.CreateOne) // route.Get("/:id", m.Auth(u), ctrl.GetOne) // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Get("/", ctrl.GetAll) - route.Get("/:id", ctrl.GetOne) + route.Get("/",m.RequirePermissions(m.P_ProductStockGetAll), ctrl.GetAll) + route.Get("/:id",m.RequirePermissions(m.P_ProductStockGetOne), ctrl.GetOne) } diff --git a/internal/modules/inventory/product-stocks/services/product-stock.service.go b/internal/modules/inventory/product-stocks/services/product-stock.service.go index a0765d84..11475109 100644 --- a/internal/modules/inventory/product-stocks/services/product-stock.service.go +++ b/internal/modules/inventory/product-stocks/services/product-stock.service.go @@ -64,11 +64,18 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e offset := (params.Page - 1) * params.Limit productStocks, total, err := s.ProductRepository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = db.Where(`EXISTS ( + SELECT 1 + FROM product_warehouses pw + WHERE pw.product_id = products.id + AND pw.qty > 0 + )`) + db = s.withRelations(db) if params.Search != "" { - return db.Where("name ILIKE ?", "%"+params.Search+"%") + db = db.Where("products.name ILIKE ?", "%"+params.Search+"%") } - return db.Order("created_at DESC").Order("updated_at DESC") + return db.Order("products.created_at DESC").Order("products.updated_at DESC") }) if err != nil { diff --git a/internal/modules/inventory/product-warehouses/route.go b/internal/modules/inventory/product-warehouses/route.go index 9c6c8e2b..81c06a08 100644 --- a/internal/modules/inventory/product-warehouses/route.go +++ b/internal/modules/inventory/product-warehouses/route.go @@ -15,7 +15,7 @@ func ProductWarehouseRoutes(v1 fiber.Router, u user.UserService, s productWareho route := v1.Group("/product-warehouses") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Get("/:id", ctrl.GetOne) + route.Get("/",m.RequirePermissions(m.P_ProductWarehousekGetAll), ctrl.GetAll) + route.Get("/:id",m.RequirePermissions(m.P_ProductWarehouseGetOne), ctrl.GetOne) } diff --git a/internal/modules/inventory/transfers/route.go b/internal/modules/inventory/transfers/route.go index f608af42..d24dbcb4 100644 --- a/internal/modules/inventory/transfers/route.go +++ b/internal/modules/inventory/transfers/route.go @@ -15,8 +15,8 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ route := v1.Group("/transfers") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) + route.Get("/",m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne) } diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index 85d850a6..ba2c1133 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -31,8 +32,6 @@ func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProduct func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) { var deliveryProducts []entity.MarketingDeliveryProduct - // JOIN digunakan untuk filter WHERE clause ke ProjectFlockID yang berada 3 level relasi atas - // Entity relations digunakan di Preload (callback) untuk load data, bukan untuk filter db := r.DB().WithContext(ctx). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). @@ -91,16 +90,19 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C Preload("Marketing.SalesPerson"). Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). - Preload("ProductWarehouse.Warehouse") + Preload("ProductWarehouse.Product.Flags"). + Preload("ProductWarehouse.Warehouse"). + Preload("ProductWarehouse.ProjectFlockKandang"). + Preload("ProductWarehouse.ProjectFlockKandang.ProjectFlock") }). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id") - if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.ProjectFlockKandangId > 0 { + if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" { db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id") } - if filters.ProductId > 0 { + if filters.ProductId > 0 || filters.Search != "" { db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id") } @@ -109,8 +111,13 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C } if filters.Search != "" { - db = db.Where("marketing_delivery_products.vehicle_number ILIKE ?", - "%"+filters.Search+"%") + db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id") + } + + if filters.Search != "" { + searchPattern := "%" + filters.Search + "%" + db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?", + searchPattern, searchPattern, searchPattern, searchPattern) } if filters.CustomerId > 0 { @@ -121,10 +128,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C db = db.Where("marketings.sales_person_id = ?", filters.SalesPersonId) } - if filters.MarketingId > 0 { - db = db.Where("marketings.id = ?", filters.MarketingId) - } - if filters.ProductId > 0 { db = db.Where("product_warehouses.product_id = ?", filters.ProductId) } @@ -133,17 +136,92 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId) } - if filters.ProjectFlockKandangId > 0 { - db = db.Where("product_warehouses.project_flock_kandang_id = ?", filters.ProjectFlockKandangId) - } - - if filters.DeliveryDate != "" { - if deliveryDate, err := utils.ParseDateString(filters.DeliveryDate); err == nil { - nextDate := deliveryDate.AddDate(0, 0, 1) - db = db.Where("marketing_delivery_products.delivery_date >= ? AND marketing_delivery_products.delivery_date < ?", deliveryDate, nextDate) + if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") { + if filters.FilterBy == "so_date" { + if filters.StartDate != "" { + if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where("marketings.so_date >= ?", startDate) + } + } + if filters.EndDate != "" { + if endDate, err := utils.ParseDateString(filters.EndDate); err == nil { + nextDate := endDate.AddDate(0, 0, 1) + db = db.Where("marketings.so_date < ?", nextDate) + } + } + } else if filters.FilterBy == "realization_date" { + if filters.StartDate != "" { + if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where("marketing_delivery_products.delivery_date >= ?", startDate) + } + } + if filters.EndDate != "" { + if endDate, err := utils.ParseDateString(filters.EndDate); err == nil { + nextDate := endDate.AddDate(0, 0, 1) + db = db.Where("marketing_delivery_products.delivery_date < ?", nextDate) + } + } } } + sortColumn := "marketing_delivery_products.id" + sortOrder := "DESC" + + if filters.SortBy != "" { + switch filters.SortBy { + case "so_date": + sortColumn = "marketings.so_date" + case "realization_date": + sortColumn = "marketing_delivery_products.delivery_date" + case "customer": + sortColumn = "customers.name" + if !containsJoin(db, "customers") { + db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id") + } + case "warehouse": + sortColumn = "warehouses.name" + if !containsJoin(db, "warehouses") { + db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). + Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id") + } + case "product": + sortColumn = "products.name" + if !containsJoin(db, "products") { + db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). + Joins("LEFT JOIN products ON products.id = product_warehouses.product_id") + } + case "sales_person": + sortColumn = "sales_users.name" + if !containsJoin(db, "sales_users") { + db = db.Joins("LEFT JOIN users AS sales_users ON sales_users.id = marketings.sales_person_id") + } + case "vehicle_number": + sortColumn = "marketing_delivery_products.vehicle_number" + case "sales_amount": + sortColumn = "marketing_delivery_products.total_price" + case "hpp_amount": + sortColumn = "marketing_delivery_products.total_price" + case "qty": + sortColumn = "marketing_delivery_products.qty" + case "average_weight": + sortColumn = "marketing_delivery_products.avg_weight" + case "total_weight": + sortColumn = "marketing_delivery_products.total_weight" + case "sales_price": + sortColumn = "marketing_delivery_products.unit_price" + case "hpp_price": + sortColumn = "marketing_delivery_products.unit_price" + case "aging_days": + sortColumn = "marketing_delivery_products.delivery_date" + } + } + + if filters.SortOrder != "" && (filters.SortOrder == "asc" || filters.SortOrder == "desc") { + sortOrder = strings.ToUpper(filters.SortOrder) + } + + db = db.Order(sortColumn + " " + sortOrder) + if err := db.Count(&total).Error; err != nil { return nil, 0, err } @@ -151,10 +229,15 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C if err := db. Offset(offset). Limit(limit). - Order("marketing_delivery_products.id DESC"). Find(&deliveryProducts).Error; err != nil { return nil, 0, err } return deliveryProducts, total, nil } + +func containsJoin(db *gorm.DB, tableName string) bool { + statement := db.Statement + joinSQL := statement.SQL.String() + return strings.Contains(joinSQL, "JOIN "+tableName) +} diff --git a/internal/modules/marketing/route.go b/internal/modules/marketing/route.go index 75ecc0f6..139d1ee9 100644 --- a/internal/modules/marketing/route.go +++ b/internal/modules/marketing/route.go @@ -16,16 +16,12 @@ func RegisterRoutes(router fiber.Router, userService user.UserService, salesOrde route := router.Group("/marketing") route.Use(m.Auth(userService)) - route.Get("/", deliveryOrdersCtrl.GetAll) - route.Get("/:id", deliveryOrdersCtrl.GetOne) - route.Delete("/:id", salesOrdersCtrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll) + route.Get("/:id",m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne) + route.Delete("/:id",m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne) - route.Post("/sales-orders", salesOrdersCtrl.CreateOne) - route.Patch("/sales-orders/:id", salesOrdersCtrl.UpdateOne) - route.Post("/sales-orders/approvals", salesOrdersCtrl.Approval) + route.Post("/sales-orders",m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne) + route.Patch("/sales-orders/:id",m.RequirePermissions(m.P_SalesOrderUpdateOne), salesOrdersCtrl.UpdateOne) + route.Post("/sales-orders/approvals",m.RequirePermissions(m.P_SalesOrderApproval), salesOrdersCtrl.Approval) - route.Get("/delivery-orders", deliveryOrdersCtrl.GetAll) - route.Get("/delivery-orders/:id", deliveryOrdersCtrl.GetOne) - route.Post("/delivery-orders", deliveryOrdersCtrl.CreateOne) - route.Patch("/delivery-orders/:id", deliveryOrdersCtrl.UpdateOne) } diff --git a/internal/modules/master/areas/route.go b/internal/modules/master/areas/route.go index 755a542e..0d715fb7 100644 --- a/internal/modules/master/areas/route.go +++ b/internal/modules/master/areas/route.go @@ -15,9 +15,9 @@ func AreaRoutes(v1 fiber.Router, u user.UserService, s area.AreaService) { route := v1.Group("/areas") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_AreaGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_AreaCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_AreaGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_AreaUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_AreaDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/banks/route.go b/internal/modules/master/banks/route.go index 2e5bed3b..678a834c 100644 --- a/internal/modules/master/banks/route.go +++ b/internal/modules/master/banks/route.go @@ -14,10 +14,9 @@ func BankRoutes(v1 fiber.Router, u user.UserService, s bank.BankService) { route := v1.Group("/banks") route.Use(m.Auth(u)) - - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_BanksGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_BanksCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_BanksGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_BanksUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_BanksDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/customers/route.go b/internal/modules/master/customers/route.go index d361e167..92f8139e 100644 --- a/internal/modules/master/customers/route.go +++ b/internal/modules/master/customers/route.go @@ -15,9 +15,9 @@ func CustomerRoutes(v1 fiber.Router, u user.UserService, s customer.CustomerServ route := v1.Group("/customers") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_CustomerGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_CustomerCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_CustomerGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_CustomerUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_CustomerDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/fcrs/route.go b/internal/modules/master/fcrs/route.go index 60633f16..06291ce4 100644 --- a/internal/modules/master/fcrs/route.go +++ b/internal/modules/master/fcrs/route.go @@ -15,9 +15,9 @@ func FcrRoutes(v1 fiber.Router, u user.UserService, s fcr.FcrService) { route := v1.Group("/fcrs") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_FcrGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_FcrCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_FcrGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_FcrUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_FcrDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/flocks/route.go b/internal/modules/master/flocks/route.go index 429d8dcd..046e014a 100644 --- a/internal/modules/master/flocks/route.go +++ b/internal/modules/master/flocks/route.go @@ -15,9 +15,9 @@ func FlockRoutes(v1 fiber.Router, u user.UserService, s flock.FlockService) { route := v1.Group("/flocks") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_FlocksGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_FlocksCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_FlocksGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_FlocksUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_FlocksDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/kandangs/route.go b/internal/modules/master/kandangs/route.go index 6a425b64..4cbf2793 100644 --- a/internal/modules/master/kandangs/route.go +++ b/internal/modules/master/kandangs/route.go @@ -15,9 +15,9 @@ func KandangRoutes(v1 fiber.Router, u user.UserService, s kandang.KandangService route := v1.Group("/kandangs") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_KandangsGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_KandangsCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_KandangsGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_KandangsUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_KandangsDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/locations/route.go b/internal/modules/master/locations/route.go index 68bce594..771e2d0d 100644 --- a/internal/modules/master/locations/route.go +++ b/internal/modules/master/locations/route.go @@ -15,9 +15,9 @@ func LocationRoutes(v1 fiber.Router, u user.UserService, s location.LocationServ route := v1.Group("/locations") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_LocationsGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_LocationsCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_LocationsGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_LocationsUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_LocationsDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/nonstocks/route.go b/internal/modules/master/nonstocks/route.go index 2aa7b838..6f2a2016 100644 --- a/internal/modules/master/nonstocks/route.go +++ b/internal/modules/master/nonstocks/route.go @@ -15,9 +15,9 @@ func NonstockRoutes(v1 fiber.Router, u user.UserService, s nonstock.NonstockServ route := v1.Group("/nonstocks") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_NonstocksGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_NonstocksCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_NonstocksGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_NonstocksUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_NonstocksDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/product-categories/route.go b/internal/modules/master/product-categories/route.go index 4a2262f9..1fa0532f 100644 --- a/internal/modules/master/product-categories/route.go +++ b/internal/modules/master/product-categories/route.go @@ -15,9 +15,9 @@ func ProductCategoryRoutes(v1 fiber.Router, u user.UserService, s productCategor route := v1.Group("/product-categories") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_ProductCategoriesGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_ProductCategoriesCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_ProductCategoriesGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_ProductCategoriesUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_ProductCategoriesDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/products/route.go b/internal/modules/master/products/route.go index 369d6ea8..04431bd4 100644 --- a/internal/modules/master/products/route.go +++ b/internal/modules/master/products/route.go @@ -15,9 +15,9 @@ func ProductRoutes(v1 fiber.Router, u user.UserService, s product.ProductService route := v1.Group("/products") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_ProductsGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_ProductsCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_ProductsGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_ProductsUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_ProductsDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/suppliers/route.go b/internal/modules/master/suppliers/route.go index 17271d4a..564ac725 100644 --- a/internal/modules/master/suppliers/route.go +++ b/internal/modules/master/suppliers/route.go @@ -15,9 +15,9 @@ func SupplierRoutes(v1 fiber.Router, u user.UserService, s supplier.SupplierServ route := v1.Group("/suppliers") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_SuppliersGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_SuppliersCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_SuppliersGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_SuppliersUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_SuppliersDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/uoms/route.go b/internal/modules/master/uoms/route.go index 53faa239..8ffbcb62 100644 --- a/internal/modules/master/uoms/route.go +++ b/internal/modules/master/uoms/route.go @@ -20,4 +20,10 @@ func UomRoutes(v1 fiber.Router, u user.UserService, s uom.UomService) { route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) + + route.Get("/",m.RequirePermissions(m.P_AreaGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_AreaCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_AreaGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_AreaUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_AreaDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/master/warehouses/route.go b/internal/modules/master/warehouses/route.go index 8acf4452..a08b04a5 100644 --- a/internal/modules/master/warehouses/route.go +++ b/internal/modules/master/warehouses/route.go @@ -15,9 +15,9 @@ func WarehouseRoutes(v1 fiber.Router, u user.UserService, s warehouse.WarehouseS route := v1.Group("/warehouses") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_WarehousesGetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_WarehousesCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_WarehousesGetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_WarehousesUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_WarehousesDeleteOne), ctrl.DeleteOne) } diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go index bef062f5..43cafaac 100644 --- a/internal/modules/production/chickins/repositories/project_chickin.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -15,6 +15,7 @@ type ProjectChickinRepository interface { GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) + GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) } type ChickinRepositoryImpl struct { @@ -90,3 +91,14 @@ func (r *ChickinRepositoryImpl) GetTotalPendingUsageQtyByProjectFlockKandangID(c } return total, nil } + +func (r *ChickinRepositoryImpl) GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + var result float64 + err := r.db.WithContext(ctx). + Table("project_chickins"). + Select("COALESCE(SUM(project_chickins.usage_qty), 0)"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Scan(&result).Error + return result, err +} diff --git a/internal/modules/production/chickins/route.go b/internal/modules/production/chickins/route.go index a558dd29..103a3655 100644 --- a/internal/modules/production/chickins/route.go +++ b/internal/modules/production/chickins/route.go @@ -16,9 +16,9 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService route.Use(m.Auth(u)) // route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) + route.Post("/",m.RequirePermissions(m.P_ChickinsCreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_ChickinsGetOne), ctrl.GetOne) // route.Patch("/:id", ctrl.UpdateOne) // route.Delete("/:id", ctrl.DeleteOne) - route.Post("/approvals", ctrl.Approval) + route.Post("/approvals",m.RequirePermissions(m.P_ChickinsApproval), ctrl.Approval) } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index cb816431..b8eefa49 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -143,6 +143,10 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not bound to kandang's warehouse", chickinReq.ProductWarehouseId)) } + if productWarehouse.ProjectFlockKandangId == nil || *productWarehouse.ProjectFlockKandangId != req.ProjectFlockKandangId { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not attached to project_flock_kandang %d. Only product warehouses with matching project_flock_kandang_id can be chickin-ed", chickinReq.ProductWarehouseId, req.ProjectFlockKandangId)) + } + chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId)) @@ -450,7 +454,8 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") } - targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) + pfkID := approvableID + targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse") } @@ -466,7 +471,8 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") } - targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID) + pfkID := approvableID + targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse") } @@ -538,11 +544,19 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return updated, nil } -func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) { +func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) { products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId) if err == nil && len(products) > 0 { - return &products[0], nil + existingPW := &products[0] + // Update project_flock_kandang_id if not already set + if existingPW.ProjectFlockKandangId == nil && projectFlockKandangId != nil { + existingPW.ProjectFlockKandangId = projectFlockKandangId + if err := s.ProductWarehouseRepo.WithTx(dbTransaction).UpdateOne(ctx.Context(), existingPW.Id, existingPW, nil); err != nil { + return nil, fmt.Errorf("failed to update %s product warehouse with project_flock_kandang_id: %w", categoryCode, err) + } + } + return existingPW, nil } product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode) @@ -554,9 +568,10 @@ func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId } newPW := &entity.ProductWarehouse{ - ProductId: product.Id, - WarehouseId: warehouseId, - Quantity: 0, + ProductId: product.Id, + WarehouseId: warehouseId, + ProjectFlockKandangId: projectFlockKandangId, + Quantity: 0, // CreatedBy: actorID, } diff --git a/internal/modules/production/project-flock-kandangs/route.go b/internal/modules/production/project-flock-kandangs/route.go index 3998a324..d48d9990 100644 --- a/internal/modules/production/project-flock-kandangs/route.go +++ b/internal/modules/production/project-flock-kandangs/route.go @@ -14,16 +14,10 @@ func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo route := v1.Group("/project-flock-kandangs") route.Use(m.Auth(u)) - // route.Get("/", m.Auth(u), ctrl.GetAll) - // route.Post("/", m.Auth(u), ctrl.CreateOne) - // route.Get("/:id", m.Auth(u), ctrl.GetOne) - // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) - // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - - route.Get("/", ctrl.GetAll) - route.Get("/:id", ctrl.GetOne) + route.Get("/",m.RequirePermissions(m.P_ProjectFlockKandangsGetAll), ctrl.GetAll) + route.Get("/:id",m.RequirePermissions(m.P_ProjectFlockKandangsGetOne), ctrl.GetOne) // route.Post("/:id/closing", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.Closing) // route.Get("/:id/closing/check", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.CheckClosing) - route.Post("/:id/closing", ctrl.Closing) - route.Get("/:id/closing/check", ctrl.CheckClosing) + route.Post("/:id/closing",m.RequirePermissions(m.P_ProjectFlockKandangsClosing), ctrl.Closing) + route.Get("/:id/closing/check", m.RequirePermissions(m.P_ProjectFlockKandangsCheckClosing), ctrl.CheckClosing) } diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 7effdc35..cf2d87ee 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -190,13 +190,16 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project result := make(map[uint]float64) for _, pw := range products { - availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw) - if err != nil { - s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err) - } - if availableQty > 0 { - result[pw.Id] = availableQty + if pw.ProjectFlockKandangId != nil && *pw.ProjectFlockKandangId == projectFlockKandang.Id { + availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw) + if err != nil { + s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err) + } + + if availableQty > 0 { + result[pw.Id] = availableQty + } } } diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index 1fdefcdf..c0eb8657 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -15,13 +15,13 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj route := v1.Group("/project-flocks") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Delete("/:id", ctrl.DeleteOne) - route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) - route.Post("/approvals", ctrl.Approval) - route.Get("/locations/:location_id/periods", ctrl.GetPeriodSummary) - route.Put("/:id/resubmit", ctrl.Resubmit) + route.Get("/",m.RequirePermissions(m.P_ProjectFlockGetAll),ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_ProjectFlockCreate), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_ProjectFlockGetOne), ctrl.GetOne) + route.Delete("/:id",m.RequirePermissions(m.P_ProjectFlockGetAll), ctrl.DeleteOne) + route.Get("/kandangs/lookup",m.RequirePermissions(m.P_ProjectFlockLookup), ctrl.LookupProjectFlockKandang) + route.Post("/approvals",m.RequirePermissions(m.P_ProjectFlockApprove), ctrl.Approval) + route.Get("/locations/:location_id/periods",m.RequirePermissions(m.P_ProjectFlockNextPeriod), ctrl.GetPeriodSummary) + route.Put("/:id/resubmit",m.RequirePermissions(m.P_ProjectFlockResubmit), ctrl.Resubmit) } diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 60457074..6e362ba7 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -45,6 +45,10 @@ type RecordingRepository interface { GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) + GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) + GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error) + GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error) + GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error) } type RecordingRepositoryImpl struct { @@ -363,6 +367,85 @@ func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint return weight, true, nil } +func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) { + if projectFlockID == 0 { + return 0, 0, nil + } + + totalChickinQty, err := r.getTotalChickinQtyByProjectFlockID(ctx, projectFlockID) + if err != nil { + return 0, 0, err + } + + totalDepletion, err := r.GetTotalDepletionByProjectFlockID(ctx, projectFlockID) + if err != nil { + return 0, 0, err + } + + actualQty := totalChickinQty - totalDepletion + + avgWeight, err := r.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID) + if err != nil { + return 0, 0, err + } + + totalWeight = actualQty * avgWeight + + return totalWeight, actualQty, nil +} + +func (r *RecordingRepositoryImpl) getTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + var result float64 + err := r.DB().WithContext(ctx). + Table("project_chickins"). + Select("COALESCE(SUM(project_chickins.usage_qty), 0)"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Scan(&result).Error + return result, err +} + +func (r *RecordingRepositoryImpl) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + var result float64 + err := r.DB().WithContext(ctx). + Table("recording_depletions"). + Select("COALESCE(SUM(recording_depletions.qty), 0)"). + Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Scan(&result).Error + return result, err +} + +func (r *RecordingRepositoryImpl) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + var result float64 + err := r.DB().WithContext(ctx). + Table("recording_bws"). + Select("COALESCE(AVG(recording_bws.avg_weight), 0)"). + Joins("JOIN recordings ON recordings.id = recording_bws.recording_id"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Where("recordings.record_datetime = (SELECT MAX(record_datetime) FROM recordings r2 WHERE r2.project_flock_kandangs_id IN (SELECT id FROM project_flock_kandangs WHERE project_flock_id = ?))", projectFlockID). + Scan(&result).Error + return result, err +} + +func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) { + if projectFlockID == 0 { + return 0, nil + } + + var result float64 + err := r.DB().WithContext(ctx). + Table("recording_eggs"). + Select("COALESCE(SUM(recording_eggs.qty * recording_eggs.weight), 0) / 1000"). + Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id"). + Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id"). + Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). + Scan(&result).Error + return result, err +} + func nextRecordingDay(days []int) int { if len(days) == 0 { return 1 diff --git a/internal/modules/production/recordings/route.go b/internal/modules/production/recordings/route.go index 83b426db..f05d054d 100644 --- a/internal/modules/production/recordings/route.go +++ b/internal/modules/production/recordings/route.go @@ -15,11 +15,11 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS route := v1.Group("/recordings") route.Use(m.Auth(u)) - route.Get("/", ctrl.GetAll) - route.Get("/next-day", ctrl.GetNextDay) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Post("/approvals", ctrl.Approve) - route.Delete("/:id", ctrl.DeleteOne) + route.Get("/",m.RequirePermissions(m.P_RecordingGetAll), ctrl.GetAll) + route.Get("/:id",m.RequirePermissions(m.P_RecordingGetOne), ctrl.GetOne) + route.Post("/",m.RequirePermissions(m.P_RecordingCreateOne), ctrl.CreateOne) + route.Patch("/:id",m.RequirePermissions(m.P_RecordingUpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_RecordingDeleteOne), ctrl.DeleteOne) + route.Get("/next-day",m.RequirePermissions(m.P_RecordingNextDay), ctrl.GetNextDay) + route.Post("/approvals",m.RequirePermissions(m.P_RecordingApproval), ctrl.Approve) } diff --git a/internal/modules/production/transfer_layings/route.go b/internal/modules/production/transfer_layings/route.go index 868454c5..8f7a62c0 100644 --- a/internal/modules/production/transfer_layings/route.go +++ b/internal/modules/production/transfer_layings/route.go @@ -21,11 +21,11 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying. // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) // route.Post("/approval", m.Auth(u), ctrl.Approval) - route.Get("/", ctrl.GetAll) - route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) - route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) - route.Post("/approvals", ctrl.Approval) - route.Get("/project-flocks/:project_flock_id/available-qty", ctrl.GetAvailableQtyPerKandang) + route.Get("/",m.RequirePermissions(m.P_TransferToLaying_GetAll), ctrl.GetAll) + route.Post("/",m.RequirePermissions(m.P_TransferToLaying_CreateOne), ctrl.CreateOne) + route.Get("/:id",m.RequirePermissions(m.P_TransferToLaying_GetOne), ctrl.GetOne) + route.Patch("/:id",m.RequirePermissions(m.P_TransferToLaying_UpdateOne), ctrl.UpdateOne) + route.Delete("/:id",m.RequirePermissions(m.P_TransferToLaying_DeleteOne), ctrl.DeleteOne) + route.Post("/approvals",m.RequirePermissions(m.P_TransferToLaying_Approval), ctrl.Approval) + route.Get("/project-flocks/:project_flock_id/available-qty",m.RequirePermissions(m.P_TransferToLaying_GetAvailableQty), ctrl.GetAvailableQtyPerKandang) } diff --git a/internal/modules/purchases/repositories/purchase.repository.go b/internal/modules/purchases/repositories/purchase.repository.go index 9f008b0d..fc599877 100644 --- a/internal/modules/purchases/repositories/purchase.repository.go +++ b/internal/modules/purchases/repositories/purchase.repository.go @@ -25,6 +25,8 @@ type PurchaseRepository interface { NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error + GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) + GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) } type PurchaseRepositoryImpl struct { @@ -289,6 +291,38 @@ func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, return count > 0, nil } +func (r *PurchaseRepositoryImpl) GetItemsByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) { + + return r.GetItemsByWarehouseKandang(ctx, projectFlockID) +} + +func (r *PurchaseRepositoryImpl) GetItemsByWarehouseKandang(ctx context.Context, projectFlockID uint) ([]entity.PurchaseItem, error) { + var items []entity.PurchaseItem + + var kandangIDs []uint + err := r.DB().WithContext(ctx). + Table("project_flock_kandangs"). + Where("project_flock_id = ?", projectFlockID). + Pluck("kandang_id", &kandangIDs).Error + + if err != nil { + return nil, err + } + + if len(kandangIDs) == 0 { + return []entity.PurchaseItem{}, nil + } + + err = r.DB().WithContext(ctx). + Preload("Product"). + Preload("Product.Flags"). + Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). + Where("warehouses.kandang_id IN ?", kandangIDs). + Find(&items).Error + + return items, err +} + func parseNumericSuffix(value, prefix string) (int, bool) { if !strings.HasPrefix(value, prefix) { return 0, false diff --git a/internal/modules/purchases/route.go b/internal/modules/purchases/route.go index 5145bc94..0fe038c3 100644 --- a/internal/modules/purchases/route.go +++ b/internal/modules/purchases/route.go @@ -15,12 +15,12 @@ func Routes(router fiber.Router, purchaseService service.PurchaseService, userSe route := router.Group("/purchases") route.Use(m.Auth(userService)) - route.Get("/", ctrl.GetAll) - route.Get("/:id", ctrl.GetOne) + route.Get("/",m.RequirePermissions(m.P_PurchaseGetAll), ctrl.GetAll) + route.Get("/:id",m.RequirePermissions(m.P_PurchaseGetOne), ctrl.GetOne) route.Post("/", ctrl.CreateOne) route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase) route.Post("/:id/approvals/manager", ctrl.ApproveManagerPurchase) - route.Post("/:id/receipts", ctrl.ReceiveProducts) + route.Post("/:id/receipts",ctrl.ReceiveProducts) route.Delete("/:id", ctrl.DeletePurchase) route.Delete("/:id/items", ctrl.DeleteItems) } diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 64a91e9d..366a8c0e 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -247,7 +247,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase return nil, nil, utils.Internal("Failed to get warehouse") } if warehouse.KandangId == nil || *warehouse.KandangId == 0 { - return nil, nil, utils.BadRequest(fmt.Sprintf("Warehouse %d is not linked to a kandang", id)) + return nil, nil, utils.BadRequest(fmt.Sprintf("%s is not linked to a kandang", warehouse.Name)) } var pfkID *uint if s.ProjectFlockKandangRepo != nil { @@ -258,7 +258,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase idCopy := uint(pfk.Id) pfkID = &idCopy } else if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil, utils.BadRequest(fmt.Sprintf("Warehouse %d has no active project flock", id)) + return nil, nil, utils.BadRequest(fmt.Sprintf("%s has no active project flock", warehouse.Name)) } else if err != nil { s.Log.Errorf("Failed to validate project flock for warehouse %d: %+v", id, err) return nil, nil, utils.Internal("Failed to validate project flock") @@ -794,6 +794,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation deltas := make(map[uint]float64) affected := make(map[uint]struct{}) updates := make([]rPurchase.PurchaseReceivingUpdate, 0, len(prepared)) + priceUpdates := make([]rPurchase.PurchasePricingUpdate, 0, len(prepared)) fifoAdds := make([]struct { itemID uint pwID uint @@ -862,6 +863,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation } updates = append(updates, update) + + if item.Price > 0 && prep.receivedQty >= 0 { + priceUpdates = append(priceUpdates, rPurchase.PurchasePricingUpdate{ + ItemID: item.Id, + Price: item.Price, + TotalPrice: item.Price * prep.receivedQty, + }) + } } if err := repoTx.UpdateReceivingDetails(c.Context(), purchase.Id, updates); err != nil { @@ -876,6 +885,12 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation return err } + if len(priceUpdates) > 0 { + if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil { + return err + } + } + // Update due_date based on earliest received date when receiving approved. if earliestReceived != nil { due := earliestReceived.AddDate(0, 0, purchase.CreditTerm) diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 21d3c49a..0ab2ccbd 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -11,6 +11,17 @@ import ( "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 } @@ -62,16 +73,18 @@ func (c *RepportController) GetExpense(ctx *fiber.Ctx) error { 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)), - ProjectFlockKandangId: int64(ctx.QueryInt("project_flock_kandang_id", 0)), - DeliveryDate: ctx.Query("delivery_date", ""), - ProductId: int64(ctx.QueryInt("product_id", 0)), - WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)), - SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)), - MarketingId: int64(ctx.QueryInt("marketing_id", 0)), + 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)), + 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", ""), } if query.Page < 1 || query.Limit < 1 { @@ -83,8 +96,11 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { return err } + + total := dto.ToSummaryFromDTOItems(result) + return ctx.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.RepportMarketingListDTO]{ + JSON(MarketingReportResponse{ Code: fiber.StatusOK, Status: "success", Message: "Get marketing report successfully", @@ -94,6 +110,57 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, + Data: result, + Total: total, + }) +} + +func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { + query := &validation.PurchaseSupplierQuery{ + Page: ctx.QueryInt("page", 1), + Limit: ctx.QueryInt("limit", 10), + AreaId: int64(ctx.QueryInt("area_id", 0)), + SupplierId: int64(ctx.QueryInt("supplier_id", 0)), + ProductId: int64(ctx.QueryInt("product_id", 0)), + ProductCategoryId: int64(ctx.QueryInt("product_category_id", 0)), + StartDate: ctx.Query("start_date", ""), + EndDate: ctx.Query("end_date", ""), + SortBy: ctx.Query("sort_by", ""), + FilterBy: ctx.Query("filter_by", ""), + } + + 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.AreaId, + "supplier_id": query.SupplierId, + "product_id": query.ProductId, + "product_category_id": query.ProductCategoryId, + "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, }) } diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index 9cbd57ba..9c026590 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -4,216 +4,258 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) -// === DTO Structs === - -type RepportMarketingBaseDTO struct { - Id uint `json:"id"` - SoNumber string `json:"so_number"` - SoDate time.Time `json:"so_date"` - Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` - SalesPerson *userDTO.UserRelationDTO `json:"sales_person,omitempty"` - Notes string `json:"notes"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` +type RepportMarketingItemDTO struct { + ID int `json:"id"` + SoDate time.Time `json:"so_date"` + RealizationDate time.Time `json:"realization_date"` + AgingDays int `json:"aging_days"` + Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` + Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` + DoNumber string `json:"do_number"` + Sales *userDTO.UserRelationDTO `json:"sales,omitempty"` + VehicleNumber string `json:"vehicle_number"` + Product *productDTO.ProductRelationDTO `json:"product,omitempty"` + MarketingType string `json:"marketing_type"` + Qty float64 `json:"qty"` + AverageWeightKg float64 `json:"average_weight_kg"` + TotalWeightKg float64 `json:"total_weight_kg"` + SalesPricePerKg float64 `json:"sales_price_per_kg"` + HppPricePerKg float64 `json:"hpp_price_per_kg"` + SalesAmount float64 `json:"sales_amount"` + HppAmount float64 `json:"hpp_amount"` } -type RepportMarketingProductDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - Product *productDTO.ProductRelationDTO `json:"product,omitempty"` - CreatedAt time.Time `json:"created_at"` +type Summary struct { + TotalQty int `json:"total_qty"` + TotalWeightKg float64 `json:"total_weight_kg"` + TotalSalesAmount int64 `json:"total_sales_amount"` + TotalHppAmount int64 `json:"total_hpp_amount"` + TotalHppPricePerKg float64 `json:"total_hpp_price_per_kg"` } -type RepportMarketingDeliveryDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalWeight float64 `json:"total_weight"` - AvgWeight float64 `json:"avg_weight"` - TotalPrice float64 `json:"total_price"` - DeliveryDate *time.Time `json:"delivery_date,omitempty"` - VehicleNumber string `json:"vehicle_number"` - DoNumber string `json:"do_number"` - Product *productDTO.ProductRelationDTO `json:"product,omitempty"` - CreatedAt time.Time `json:"created_at"` +type RepportMarketingResponseDTO struct { + Items []RepportMarketingItemDTO `json:"items"` + Total *Summary `json:"total,omitempty"` } -type RepportMarketingListDTO struct { - RepportMarketingBaseDTO - MarketingProduct RepportMarketingProductDTO `json:"marketing_product"` - MarketingDelivery RepportMarketingDeliveryDTO `json:"marketing_delivery"` - TotalMarketingProduct float64 `json:"total_marketing_product"` - TotalMarketingDelivery float64 `json:"total_marketing_delivery"` - LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"` -} - -// === MAPPERS === - -func ToRepportMarketingBaseDTO(m *entity.Marketing) RepportMarketingBaseDTO { - if m == nil { - return RepportMarketingBaseDTO{} +func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingItemDTO { + soDate := time.Time{} + agingDays := 0 + if mdp.MarketingProduct.Marketing.SoDate.Year() > 1 { + soDate = mdp.MarketingProduct.Marketing.SoDate + agingDays = int(time.Since(soDate).Hours() / 24) } - var customer *customerDTO.CustomerRelationDTO - if m.Customer.Id != 0 { - mapped := customerDTO.ToCustomerRelationDTO(m.Customer) - customer = &mapped + realizationDate := time.Time{} + if mdp.DeliveryDate != nil { + realizationDate = *mdp.DeliveryDate } - var salesPerson *userDTO.UserRelationDTO - if m.SalesPerson.Id != 0 { - mapped := userDTO.ToUserRelationDTO(m.SalesPerson) - salesPerson = &mapped + doNumber := marketingDTO.GenerateDeliveryOrderNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId) + + totalWeightKg := mdp.Qty * mdp.AvgWeight + salesAmount := totalWeightKg * mdp.UnitPrice + + var hpp float64 + var hppAmount float64 + if isProductEligibleForHpp(mdp, category) { + hpp = hppPricePerKg + hppAmount = totalWeightKg * hppPricePerKg } - return RepportMarketingBaseDTO{ - Id: m.Id, - SoNumber: m.SoNumber, - SoDate: m.SoDate, - Customer: customer, - SalesPerson: salesPerson, - Notes: m.Notes, - CreatedAt: m.CreatedAt, - UpdatedAt: m.UpdatedAt, - } -} - -func ToRepportMarketingProductDTO(mp *entity.MarketingProduct) RepportMarketingProductDTO { - if mp == nil { - return RepportMarketingProductDTO{} + item := RepportMarketingItemDTO{ + ID: int(mdp.Id), + SoDate: soDate, + RealizationDate: realizationDate, + AgingDays: agingDays, + DoNumber: doNumber, + MarketingType: getMarketingType(mdp), + Qty: mdp.Qty, + AverageWeightKg: mdp.AvgWeight, + TotalWeightKg: totalWeightKg, + SalesPricePerKg: mdp.UnitPrice, + HppPricePerKg: hpp, + SalesAmount: salesAmount, + HppAmount: hppAmount, } - var product *productDTO.ProductRelationDTO - if mp.ProductWarehouse.Product.Id != 0 { - mapped := productDTO.ToProductRelationDTO(mp.ProductWarehouse.Product) - product = &mapped + if mdp.MarketingProduct.ProductWarehouse.WarehouseId != 0 { + mapped := warehouseDTO.ToWarehouseRelationDTO(mdp.MarketingProduct.ProductWarehouse.Warehouse) + item.Warehouse = &mapped } - return RepportMarketingProductDTO{ - Id: mp.Id, - MarketingProductId: mp.Id, - Qty: mp.Qty, - UnitPrice: mp.UnitPrice, - AvgWeight: mp.AvgWeight, - TotalWeight: mp.TotalWeight, - TotalPrice: mp.TotalPrice, - Product: product, - CreatedAt: time.Now(), - } -} - -func ToRepportMarketingDeliveryDTO(mdp *entity.MarketingDeliveryProduct, soNumber string) RepportMarketingDeliveryDTO { - if mdp == nil { - return RepportMarketingDeliveryDTO{} + if mdp.MarketingProduct.Marketing.CustomerId != 0 { + mapped := customerDTO.ToCustomerRelationDTO(mdp.MarketingProduct.Marketing.Customer) + item.Customer = &mapped } - var product *productDTO.ProductRelationDTO - if mdp.MarketingProduct.ProductWarehouse.Product.Id != 0 { + if mdp.MarketingProduct.Marketing.SalesPersonId != 0 { + mapped := userDTO.ToUserRelationDTO(mdp.MarketingProduct.Marketing.SalesPerson) + item.Sales = &mapped + } + + item.VehicleNumber = mdp.VehicleNumber + + if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 { mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product) - product = &mapped + item.Product = &mapped } - warehouseId := uint(0) - if mdp.MarketingProduct.ProductWarehouse.Id != 0 { - warehouseId = mdp.MarketingProduct.ProductWarehouse.WarehouseId + return item +} + +func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) []RepportMarketingItemDTO { + items := make([]RepportMarketingItemDTO, 0, len(mdps)) + for _, mdp := range mdps { + items = append(items, ToRepportMarketingItemDTO(mdp, hppPricePerKg, category)) + } + return items +} + +func ToRepportMarketingItemDTOsWithHppMap(mdps []entity.MarketingDeliveryProduct, hppMap map[uint]float64) []RepportMarketingItemDTO { + items := make([]RepportMarketingItemDTO, 0, len(mdps)) + for _, mdp := range mdps { + hppPerKg := float64(0) + category := "" + if projectFlockKandang := mdp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { + if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists { + hppPerKg = hpp + } + category = projectFlockKandang.ProjectFlock.Category + } + + item := ToRepportMarketingItemDTO(mdp, hppPerKg, category) + items = append(items, item) + } + return items +} + +func getMarketingType(mdp entity.MarketingDeliveryProduct) string { + hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags) + + if hasAyam { + return "ayam" + } + if hasTelur { + return "telur" + } + return "trading" +} + +func checkProductFlags(flags []entity.Flag) (hasAyam, hasTelur bool) { + if len(flags) == 0 { + return false, false } - doNumber := marketingDTO.GenerateDeliveryOrderNumber(soNumber, mdp.DeliveryDate, warehouseId) + for _, flag := range flags { + ft := utils.FlagType(flag.Name) - return RepportMarketingDeliveryDTO{ - Id: mdp.Id, - MarketingProductId: mdp.MarketingProductId, - Qty: mdp.Qty, - UnitPrice: mdp.UnitPrice, - TotalWeight: mdp.TotalWeight, - AvgWeight: mdp.AvgWeight, - TotalPrice: mdp.TotalPrice, - DeliveryDate: mdp.DeliveryDate, - VehicleNumber: mdp.VehicleNumber, - DoNumber: doNumber, - Product: product, - CreatedAt: time.Now(), + if ft == utils.FlagAyamAfkir || ft == utils.FlagAyamCulling || ft == utils.FlagAyamMati || + ft == utils.FlagDOC || ft == utils.FlagPullet || ft == utils.FlagLayer { + hasAyam = true + } + + if ft == utils.FlagTelur || ft == utils.FlagTelurUtuh || ft == utils.FlagTelurPecah || + ft == utils.FlagTelurPutih || ft == utils.FlagTelurRetak { + hasTelur = true + } + } + + return hasAyam, hasTelur +} + +func isProductEligibleForHpp(mdp entity.MarketingDeliveryProduct, category string) bool { + hasAyam, hasTelur := checkProductFlags(mdp.MarketingProduct.ProductWarehouse.Product.Flags) + + if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing { + return hasAyam + } + + return hasAyam || hasTelur +} + +func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) *Summary { + if len(mdps) == 0 { + return nil + } + + totalQty := 0 + totalWeightKg := 0.0 + totalEligibleWeightKg := 0.0 + totalSalesAmount := int64(0) + totalHppAmount := int64(0) + + for _, mdp := range mdps { + calculatedTotalWeight := mdp.Qty * mdp.AvgWeight + totalQty += int(mdp.Qty) + totalWeightKg += calculatedTotalWeight + totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice) + + if isProductEligibleForHpp(mdp, category) { + totalEligibleWeightKg += calculatedTotalWeight + totalHppAmount += int64(calculatedTotalWeight * hppPricePerKg) + } + } + + totalHppPricePerKg := float64(0) + if totalEligibleWeightKg > 0 { + totalHppPricePerKg = float64(totalHppAmount) / totalEligibleWeightKg + } + + return &Summary{ + TotalQty: totalQty, + TotalWeightKg: totalWeightKg, + TotalSalesAmount: totalSalesAmount, + TotalHppAmount: totalHppAmount, + TotalHppPricePerKg: totalHppPricePerKg, } } -func ToRepportMarketingListDTO(baseDTO RepportMarketingBaseDTO, mp *entity.MarketingProduct, mdp *entity.MarketingDeliveryProduct, latestApproval *approvalDTO.ApprovalRelationDTO) RepportMarketingListDTO { - var marketingProduct RepportMarketingProductDTO - var marketingDelivery RepportMarketingDeliveryDTO - - if mp != nil { - marketingProduct = ToRepportMarketingProductDTO(mp) +func ToSummaryFromDTOItems(items []RepportMarketingItemDTO) *Summary { + if len(items) == 0 { + return nil } - if mdp != nil { - marketingDelivery = ToRepportMarketingDeliveryDTO(mdp, baseDTO.SoNumber) + totalQty := 0 + totalWeightKg := 0.0 + totalSalesAmount := int64(0) + totalHppAmount := int64(0) + + for _, item := range items { + totalQty += int(item.Qty) + totalWeightKg += item.TotalWeightKg + totalSalesAmount += int64(item.SalesAmount) + totalHppAmount += int64(item.HppAmount) } - totalMarketingProduct := float64(0) - totalMarketingDelivery := float64(0) - - if mp != nil { - totalMarketingProduct = mp.Qty * mp.UnitPrice + totalHppPricePerKg := float64(0) + if totalWeightKg > 0 { + totalHppPricePerKg = float64(totalHppAmount) / totalWeightKg } - if mdp != nil { - totalMarketingDelivery = mdp.Qty * mdp.UnitPrice - } - - return RepportMarketingListDTO{ - RepportMarketingBaseDTO: baseDTO, - MarketingProduct: marketingProduct, - MarketingDelivery: marketingDelivery, - TotalMarketingProduct: totalMarketingProduct, - TotalMarketingDelivery: totalMarketingDelivery, - LatestApproval: latestApproval, + return &Summary{ + TotalQty: totalQty, + TotalWeightKg: totalWeightKg, + TotalSalesAmount: totalSalesAmount, + TotalHppAmount: totalHppAmount, + TotalHppPricePerKg: totalHppPricePerKg, } } -func ToRepportMarketingListDTOs(deliveryProducts []entity.MarketingDeliveryProduct) []RepportMarketingListDTO { - result := make([]RepportMarketingListDTO, 0, len(deliveryProducts)) +func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, category string) RepportMarketingResponseDTO { + items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg, category) + total := ToSummary(mdps, hppPricePerKg, category) - marketingMap := make(map[uint]entity.MarketingDeliveryProduct) - for _, dp := range deliveryProducts { - if dp.MarketingProduct.Marketing.Id == 0 { - continue - } - marketingID := dp.MarketingProduct.Marketing.Id - if _, exists := marketingMap[marketingID]; !exists { - marketingMap[marketingID] = dp - } + return RepportMarketingResponseDTO{ + Items: items, + Total: total, } - - for _, deliveryProduct := range marketingMap { - if deliveryProduct.MarketingProduct.Marketing.Id == 0 { - continue - } - - marketing := &deliveryProduct.MarketingProduct.Marketing - baseDTO := ToRepportMarketingBaseDTO(marketing) - - var latestApproval *approvalDTO.ApprovalRelationDTO - if marketing.LatestApproval != nil { - mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) - latestApproval = &mapped - } - - mdp := &deliveryProduct - dto := ToRepportMarketingListDTO(baseDTO, &deliveryProduct.MarketingProduct, mdp, latestApproval) - result = append(result, dto) - } - - return result } diff --git a/internal/modules/repports/dto/repportPurchase.dto.go b/internal/modules/repports/dto/repportPurchase.dto.go new file mode 100644 index 00000000..830a076f --- /dev/null +++ b/internal/modules/repports/dto/repportPurchase.dto.go @@ -0,0 +1,159 @@ +package dto + +import ( + "math" + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" +) + +type PurchaseSupplierRowDTO struct { + ReceiveDate string `json:"receive_date"` + PoDate string `json:"po_date"` + PoNumber string `json:"po_number"` + Product *productDTO.ProductRelationDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + PurchaseValue float64 `json:"purchase_value"` + TransportUnitPrice float64 `json:"transport_unit_price"` + TransportValue float64 `json:"transport_value"` + TotalAmount float64 `json:"total_amount"` + Expedition string `json:"expedition"` + DeliveryNumber string `json:"delivery_number"` +} + +type PurchaseSupplierSummaryDTO struct { + TotalQty float64 `json:"total_qty"` + TotalPurchaseValue float64 `json:"total_purchase_value"` + TotalTransportValue float64 `json:"total_transport_value"` + TotalAmount float64 `json:"total_amount"` + TotalUnitPrice float64 `json:"total_unit_price"` + TotalTransportUnitPrice float64 `json:"total_transport_unit_price"` +} + +type PurchaseSupplierDTO struct { + Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` + Rows []PurchaseSupplierRowDTO `json:"rows"` + Summary PurchaseSupplierSummaryDTO `json:"summary"` +} + +func formatDatePtr(t *time.Time) string { + if t == nil || t.IsZero() { + return "" + } + return t.Format("02-Jan-2006") +} + +func ToPurchaseSupplierRowDTO(item *entity.PurchaseItem) PurchaseSupplierRowDTO { + row := PurchaseSupplierRowDTO{ + ReceiveDate: formatDatePtr(item.ReceivedDate), + Qty: item.TotalQty, + UnitPrice: item.Price, + } + + if item.Purchase != nil { + row.PoDate = formatDatePtr(item.Purchase.PoDate) + if item.Purchase.PoNumber != nil { + row.PoNumber = *item.Purchase.PoNumber + } + } + + if item.Product != nil && item.Product.Id != 0 { + product := productDTO.ToProductRelationDTO(*item.Product) + row.Product = &product + } + + if item.Warehouse != nil && item.Warehouse.Id != 0 { + warehouse := warehouseDTO.ToWarehouseRelationDTO(*item.Warehouse) + row.Warehouse = &warehouse + } + + qty := row.Qty + if qty < 0 { + qty = 0 + } + + row.PurchaseValue = row.UnitPrice * qty + + var transportUnit float64 + var expeditionName string + + if item.ExpenseNonstock != nil { + transportUnit = item.ExpenseNonstock.Price + + if item.ExpenseNonstock.Expense != nil && + item.ExpenseNonstock.Expense.Supplier != nil && + item.ExpenseNonstock.Expense.Supplier.Id != 0 { + expSupplier := item.ExpenseNonstock.Expense.Supplier + expeditionName = expSupplier.Name + } + } + + row.TransportUnitPrice = transportUnit + row.TransportValue = transportUnit * qty + row.TotalAmount = row.PurchaseValue + row.TransportValue + + if expeditionName == "" { + row.Expedition = "-" + } else { + row.Expedition = expeditionName + } + + if item.TravelNumber != nil && *item.TravelNumber != "" { + row.DeliveryNumber = *item.TravelNumber + } else { + row.DeliveryNumber = "-" + } + + return row +} + +func ToPurchaseSupplierDTO(supplier entity.Supplier, items []entity.PurchaseItem) PurchaseSupplierDTO { + var supplierDTORef *supplierDTO.SupplierRelationDTO + if supplier.Id != 0 { + mapped := supplierDTO.ToSupplierRelationDTO(supplier) + supplierDTORef = &mapped + } + + rows := make([]PurchaseSupplierRowDTO, 0, len(items)) + summary := PurchaseSupplierSummaryDTO{} + + var unitPriceSum float64 + var unitPriceCount int + var transportUnitPriceSum float64 + var transportUnitPriceCount int + + for i := range items { + row := ToPurchaseSupplierRowDTO(&items[i]) + rows = append(rows, row) + + summary.TotalQty += row.Qty + summary.TotalPurchaseValue += row.PurchaseValue + summary.TotalTransportValue += row.TransportValue + summary.TotalAmount += row.TotalAmount + + unitPriceSum += row.UnitPrice + unitPriceCount++ + + transportUnitPriceSum += row.TransportUnitPrice + transportUnitPriceCount++ + } + + if unitPriceCount > 0 { + summary.TotalUnitPrice = math.Round(unitPriceSum / float64(unitPriceCount)) + } + + if transportUnitPriceCount > 0 { + summary.TotalTransportUnitPrice = math.Round(transportUnitPriceSum / float64(transportUnitPriceCount)) + } + + return PurchaseSupplierDTO{ + Supplier: supplierDTORef, + Rows: rows, + Summary: summary, + } +} diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index 4479b733..1e019c90 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -7,10 +7,17 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service" + repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" sRepport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" + recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" + purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" ) type RepportModule struct{} @@ -19,10 +26,16 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * expenseRealizationRepository := expenseRepo.NewExpenseRealizationRepository(db) marketingDeliveryProductRepository := marketingRepo.NewMarketingDeliveryProductRepository(db) + purchaseRepository := purchaseRepo.NewPurchaseRepository(db) + chickinRepository := chickinRepo.NewChickinRepository(db) + recordingRepository := recordingRepo.NewRecordingRepository(db) approvalRepository := commonRepo.NewApprovalRepository(db) + purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db) + userRepository := rUser.NewUserRepository(db) approvalSvc := approvalService.NewApprovalService(approvalRepository) - repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, approvalSvc) + repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository) + userService := sUser.NewUserService(userRepository, validate) - RepportRoutes(router, repportService) + RepportRoutes(router, userService, repportService) } diff --git a/internal/modules/repports/repositories/purchase_supplier.repository.go b/internal/modules/repports/repositories/purchase_supplier.repository.go new file mode 100644 index 00000000..979623fc --- /dev/null +++ b/internal/modules/repports/repositories/purchase_supplier.repository.go @@ -0,0 +1,195 @@ +package repositories + +import ( + "context" + "fmt" + "strings" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "gorm.io/gorm" +) + +type PurchaseSupplierRepository interface { + GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.PurchaseSupplierQuery) ([]entity.Supplier, int64, error) + GetItemsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.PurchaseSupplierQuery) ([]entity.PurchaseItem, error) +} + +type purchaseSupplierRepositoryImpl struct { + db *gorm.DB +} + +func NewPurchaseSupplierRepository(db *gorm.DB) PurchaseSupplierRepository { + return &purchaseSupplierRepositoryImpl{db: db} +} + +func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.PurchaseSupplierQuery) *gorm.DB { + dateColumn := "purchase_items.received_date" + switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) { + case "po_date": + dateColumn = "purchases.po_date" + case "receive_date", "": + dateColumn = "purchase_items.received_date" + } + + db := r.db.WithContext(ctx). + Model(&entity.Supplier{}). + Joins("JOIN purchases ON purchases.supplier_id = suppliers.id"). + Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id") + + if filters.SupplierId > 0 { + db = db.Where("suppliers.id = ?", filters.SupplierId) + } + + if filters.ProductId > 0 { + db = db.Where("purchase_items.product_id = ?", filters.ProductId) + } + + if filters.ProductCategoryId > 0 { + db = db. + Joins("JOIN products ON products.id = purchase_items.product_id"). + Where("products.product_category_id = ?", filters.ProductCategoryId) + } + + if filters.AreaId > 0 { + db = db. + Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). + Where("warehouses.area_id = ?", filters.AreaId) + } + + if filters.StartDate != "" { + if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) + } + } + + if filters.EndDate != "" { + if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo) + } + } + + return db +} + +func (r *purchaseSupplierRepositoryImpl) GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.PurchaseSupplierQuery) ([]entity.Supplier, int64, error) { + query := r.baseSupplierQuery(ctx, filters) + + var totalSuppliers int64 + if err := query. + Distinct("suppliers.id"). + Count(&totalSuppliers).Error; err != nil { + return nil, 0, err + } + + if totalSuppliers == 0 { + return []entity.Supplier{}, 0, nil + } + + if offset < 0 { + offset = 0 + } + + var supplierIDs []uint + if err := query. + Select("suppliers.id"). + Order("suppliers.id ASC"). + Offset(offset). + Limit(limit). + Pluck("suppliers.id", &supplierIDs).Error; err != nil { + return nil, 0, err + } + + if len(supplierIDs) == 0 { + return []entity.Supplier{}, totalSuppliers, nil + } + + var suppliers []entity.Supplier + if err := r.db.WithContext(ctx). + Where("id IN ?", supplierIDs). + Find(&suppliers).Error; err != nil { + return nil, 0, err + } + + return suppliers, totalSuppliers, nil +} + +func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.PurchaseSupplierQuery) ([]entity.PurchaseItem, error) { + if len(supplierIDs) == 0 { + return []entity.PurchaseItem{}, nil + } + + // Tentukan kolom tanggal yang akan dipakai untuk filter & sort + dateColumn := "purchase_items.received_date" + switch strings.ToLower(strings.TrimSpace(filters.FilterBy)) { + case "po_date": + dateColumn = "purchases.po_date" + case "receive_date", "": + dateColumn = "purchase_items.received_date" + } + + orderDirection := "ASC" + switch strings.ToUpper(strings.TrimSpace(filters.SortBy)) { + case "DESC": + orderDirection = "DESC" + case "ASC", "": + orderDirection = "ASC" + } + + db := r.db.WithContext(ctx). + Model(&entity.PurchaseItem{}). + Preload("Purchase"). + Preload("Purchase.Supplier"). + Preload("Product"). + Preload("Product.ProductCategory"). + Preload("Warehouse"). + Preload("Warehouse.Area"). + Preload("Warehouse.Location"). + Preload("Warehouse.Kandang"). + Preload("ExpenseNonstock"). + Preload("ExpenseNonstock.Expense"). + Preload("ExpenseNonstock.Expense.Supplier"). + Joins("JOIN purchases ON purchases.id = purchase_items.purchase_id"). + Where("purchases.supplier_id IN ?", supplierIDs) + + if filters.ProductId > 0 { + db = db.Where("purchase_items.product_id = ?", filters.ProductId) + } + + if filters.ProductCategoryId > 0 { + db = db. + Joins("JOIN products ON products.id = purchase_items.product_id"). + Where("products.product_category_id = ?", filters.ProductCategoryId) + } + + if filters.AreaId > 0 { + db = db. + Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). + Where("warehouses.area_id = ?", filters.AreaId) + } + + if filters.StartDate != "" { + if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) + } + } + + if filters.EndDate != "" { + if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil { + db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo) + } + } + + // Urutkan berdasarkan kolom tanggal yang dipilih dan arah sort + db = db.Order(fmt.Sprintf("%s %s", dateColumn, orderDirection)). + Order("purchase_items.id ASC") + + var items []entity.PurchaseItem + if err := db.Find(&items).Error; err != nil { + return nil, err + } + + return items, nil +} diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 4aea831c..45dc32b7 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -1,17 +1,21 @@ package repports import ( + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/controllers" repport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" "github.com/gofiber/fiber/v2" ) -func RepportRoutes(v1 fiber.Router, s repport.RepportService) { +func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService) { ctrl := controller.NewRepportController(s) route := v1.Group("/reports") + route.Use(m.Auth(u)) - route.Get("/expense", ctrl.GetExpense) - route.Get("/marketing", ctrl.GetMarketing) + route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense) + route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing) + route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier) } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 3adc5c0a..fbca69b7 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -1,7 +1,10 @@ package service import ( + "context" + "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" + repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -9,6 +12,11 @@ import ( approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" + recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" + purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -18,7 +26,8 @@ import ( type RepportService interface { GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) - GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) + GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) + GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) } type repportService struct { @@ -26,16 +35,33 @@ type repportService struct { Validate *validator.Validate ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository + PurchaseRepo purchaseRepo.PurchaseRepository + ChickinRepo chickinRepo.ProjectChickinRepository + RecordingRepo recordingRepo.RecordingRepository ApprovalSvc approvalService.ApprovalService + PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository } -func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, approvalSvc approvalService.ApprovalService) RepportService { +func NewRepportService( + validate *validator.Validate, + expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, + marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, + purchaseRepo purchaseRepo.PurchaseRepository, + chickinRepo chickinRepo.ProjectChickinRepository, + recordingRepo recordingRepo.RecordingRepository, + approvalSvc approvalService.ApprovalService, + purchaseSupplierRepo repportRepo.PurchaseSupplierRepository, +) RepportService { return &repportService{ Log: utils.Log, Validate: validate, ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, + PurchaseRepo: purchaseRepo, + ChickinRepo: chickinRepo, + RecordingRepo: recordingRepo, ApprovalSvc: approvalSvc, + PurchaseSupplierRepo: purchaseSupplierRepo, } } @@ -77,7 +103,7 @@ func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuer return result, total, nil } -func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) { +func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } @@ -89,27 +115,153 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing return nil, 0, err } - marketingIDMap := make(map[uint]bool) - marketingIDs := make([]uint, 0) + projectFlockIDMap := make(map[uint]bool) + hppMap := make(map[uint]float64) + for _, dp := range deliveryProducts { - if marketingID := dp.MarketingProduct.Marketing.Id; marketingID > 0 && !marketingIDMap[marketingID] { - marketingIDs = append(marketingIDs, marketingID) - marketingIDMap[marketingID] = true + if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { + projectFlockID := projectFlockKandang.ProjectFlockId + if projectFlockID > 0 && !projectFlockIDMap[projectFlockID] { + projectFlockIDMap[projectFlockID] = true + + category := projectFlockKandang.ProjectFlock.Category + hppPerKg := s.calculateHppPricePerKg(c.Context(), projectFlockID, category) + hppMap[projectFlockID] = hppPerKg + } } } - approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, marketingIDs, func(db *gorm.DB) *gorm.DB { - return db.Preload("ActionUser") - }) - if err != nil { - s.Log.Warnf("LatestByTargets error: %v", err) - } - - for i := range deliveryProducts { - if approval, exists := approvals[deliveryProducts[i].MarketingProduct.Marketing.Id]; exists && approval != nil { - deliveryProducts[i].MarketingProduct.Marketing.LatestApproval = approval - } - } - - return dto.ToRepportMarketingListDTOs(deliveryProducts), total, nil + items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap) + return items, total, nil +} + +func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, category string) float64 { + totalCost := s.getTotalProjectCost(ctx, projectFlockID) + if totalCost == 0 { + s.Log.Warnf("HPP calculation: No cost found for project flock ID %d. Check if purchase items are linked to project_flock_kandang_id", projectFlockID) + return 0 + } + + chickinQty, err := s.ChickinRepo.GetTotalChickinQtyByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("HPP calculation: Failed to get chickin qty for project flock ID %d: %v", projectFlockID, err) + } + + depletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("HPP calculation: Failed to get depletion for project flock ID %d: %v", projectFlockID, err) + } + + avgWeight, err := s.RecordingRepo.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("HPP calculation: Failed to get avg weight for project flock ID %d: %v", projectFlockID, err) + } + + var totalWeight float64 + if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing { + totalWeight = (chickinQty - depletion) * avgWeight + } else { + eggWeight, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("HPP calculation: Failed to get egg weight for project flock ID %d: %v", projectFlockID, err) + } + totalWeight = (chickinQty-depletion)*avgWeight + eggWeight + } + + if totalWeight == 0 { + return 0 + } + + hppPricePerKg := totalCost / totalWeight + return hppPricePerKg +} + +func (s *repportService) getTotalProjectCost(ctx context.Context, projectFlockID uint) float64 { + if projectFlockID == 0 { + return 0 + } + + purchases, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Errorf("getTotalProjectCost: GetItemsByProjectFlockID error for project flock ID %d: %v", projectFlockID, err) + return 0 + } + + cost := float64(0) + purchaseCost := float64(0) + for _, p := range purchases { + purchaseCost += p.TotalPrice + } + cost += purchaseCost + + realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) + if err != nil { + s.Log.Warnf("getTotalProjectCost: GetByProjectFlockID error for project flock ID %d: %v", projectFlockID, err) + } + + bopCost := float64(0) + for _, r := range realizations { + if r.ExpenseNonstock != nil && r.ExpenseNonstock.Expense != nil && + r.ExpenseNonstock.Expense.Category == string(utils.ExpenseCategoryBOP) { + bopCost += r.Price * r.Qty + } + } + cost += bopCost + + return cost +} + +func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + if offset < 0 { + offset = 0 + } + + suppliers, totalSuppliers, err := s.PurchaseSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params) + if err != nil { + return nil, 0, err + } + + if totalSuppliers == 0 || len(suppliers) == 0 { + return []dto.PurchaseSupplierDTO{}, totalSuppliers, nil + } + + supplierMap := make(map[uint]entity.Supplier, len(suppliers)) + supplierIDs := make([]uint, 0, len(suppliers)) + for _, supplier := range suppliers { + supplierMap[supplier.Id] = supplier + supplierIDs = append(supplierIDs, supplier.Id) + } + + items, err := s.PurchaseSupplierRepo.GetItemsBySuppliers(c.Context(), supplierIDs, params) + if err != nil { + return nil, 0, err + } + + itemsBySupplier := make(map[uint][]entity.PurchaseItem) + for _, item := range items { + if item.Purchase == nil { + continue + } + supplierID := item.Purchase.SupplierId + itemsBySupplier[supplierID] = append(itemsBySupplier[supplierID], item) + } + + result := make([]dto.PurchaseSupplierDTO, 0, len(supplierIDs)) + for _, supplierID := range supplierIDs { + supplier, exists := supplierMap[supplierID] + if !exists { + continue + } + + supplierItems := itemsBySupplier[supplierID] + dtoItem := dto.ToPurchaseSupplierDTO(supplier, supplierItems) + result = append(result, dtoItem) + } + + return result, totalSuppliers, nil } diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 7efc51f9..f1f46c6d 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -16,14 +16,29 @@ type ExpenseQuery struct { } type MarketingQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=100"` - CustomerId int64 `query:"customer_id" validate:"omitempty"` - ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"` - DeliveryDate string `query:"delivery_date" validate:"omitempty"` - ProductId int64 `query:"product_id" validate:"omitempty"` - WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` - SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` - MarketingId int64 `query:"marketing_id" validate:"omitempty"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=100"` + CustomerId int64 `query:"customer_id" validate:"omitempty"` + ProductId int64 `query:"product_id" validate:"omitempty"` + WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` + SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` + FilterBy string `query:"filter_by" validate:"omitempty,oneof=so_date realization_date"` + StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` + EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=so_date realization_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` +} + +type PurchaseSupplierQuery struct { + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` + AreaId int64 `query:"area_id" validate:"omitempty"` + SupplierId int64 `query:"supplier_id" validate:"omitempty"` + ProductId int64 `query:"product_id" validate:"omitempty"` + ProductCategoryId int64 `query:"product_category_id" validate:"omitempty"` + StartDate string `query:"start_date" validate:"omitempty"` + EndDate string `query:"end_date" validate:"omitempty"` + SortBy string `query:"sort_by" validate:"omitempty"` + FilterBy string `query:"filter_by" validate:"omitempty"` } diff --git a/internal/modules/sso/controllers/refresh_token_response.go b/internal/modules/sso/controllers/refresh_token_response.go new file mode 100644 index 00000000..1825342a --- /dev/null +++ b/internal/modules/sso/controllers/refresh_token_response.go @@ -0,0 +1,13 @@ +package controllers + +type refreshTokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` + IDToken string `json:"id_token"` + Error string `json:"error"` + Description string `json:"error_description"` +} + diff --git a/internal/modules/sso/controllers/sso.controller.go b/internal/modules/sso/controllers/sso.controller.go index f11a31c8..99bd67d6 100644 --- a/internal/modules/sso/controllers/sso.controller.go +++ b/internal/modules/sso/controllers/sso.controller.go @@ -138,6 +138,86 @@ func (h *Controller) Start(c *fiber.Ctx) error { return c.Redirect(authorizeURL.String(), fiber.StatusFound) } +// Refresh exchanges the current SSO refresh token for a new access/refresh pair +// without redirecting the browser to the SSO login page. +func (h *Controller) Refresh(c *fiber.Ctx) error { + refreshName := resolveSSOCookieName(config.SSORefreshCookieName, "refresh") + refreshToken := strings.TrimSpace(c.Cookies(refreshName)) + if refreshToken == "" { + return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated") + } + + tokenEndpoint := strings.TrimSpace(config.SSOTokenURL) + if tokenEndpoint == "" { + return fiber.NewError(fiber.StatusInternalServerError, "token endpoint not configured") + } + + form := url.Values{} + form.Set("grant_type", "refresh_token") + form.Set("refresh_token", refreshToken) + + req, err := http.NewRequestWithContext(c.Context(), http.MethodPost, tokenEndpoint, strings.NewReader(form.Encode())) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to create refresh request") + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := h.httpClient.Do(req) + if err != nil { + utils.Log.Errorf("token refresh request failed: %v", err) + return fiber.NewError(fiber.StatusBadGateway, "failed to refresh access token") + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + utils.Log.Warnf("token refresh response status %d", resp.StatusCode) + return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated") + } + + var tokenResp refreshTokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + return fiber.NewError(fiber.StatusBadGateway, "invalid token response") + } + if tokenResp.Error != "" { + return fiber.NewError(fiber.StatusBadGateway, tokenResp.Description) + } + if tokenResp.AccessToken == "" { + return fiber.NewError(fiber.StatusBadGateway, "missing access token") + } + + verification, err := sso.VerifyAccessToken(tokenResp.AccessToken) + if err != nil { + utils.Log.Errorf("access token verification failed: %v", err) + return fiber.NewError(fiber.StatusUnauthorized, "invalid access token") + } + + issueCookies(c, struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` + IDToken string `json:"id_token"` + Error string `json:"error"` + Description string `json:"error_description"` + }{ + AccessToken: tokenResp.AccessToken, + RefreshToken: tokenResp.RefreshToken, + TokenType: tokenResp.TokenType, + ExpiresIn: tokenResp.ExpiresIn, + Scope: tokenResp.Scope, + IDToken: tokenResp.IDToken, + Error: tokenResp.Error, + Description: tokenResp.Description, + }, verification) + + utils.Log.WithFields(logrus.Fields{ + "user_id": verification.UserID, + }).Info("sso refresh successful") + + return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"}) +} + // Callback handles the redirect from SSO containing the authorization code. func (h *Controller) Callback(c *fiber.Ctx) error { state := strings.TrimSpace(c.Query("state")) diff --git a/internal/modules/sso/route.go b/internal/modules/sso/route.go index a7288ef9..3f2a699e 100644 --- a/internal/modules/sso/route.go +++ b/internal/modules/sso/route.go @@ -31,6 +31,7 @@ func Routes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { group.Get("/start", middleware.NewLimiter(30, time.Minute), ctrl.Start) group.Get("/callback", ctrl.Callback) group.Get("/userinfo", middleware.NewLimiter(60, time.Minute), ctrl.UserInfo) + group.Post("/refresh", middleware.NewLimiter(60, time.Minute), ctrl.Refresh) group.Post("/logout", middleware.NewLimiter(60, time.Minute), ctrl.Logout) group.Post("/users/sync", middleware.NewLimiter(30, time.Minute), syncCtrl.Sync) } diff --git a/internal/modules/users/route.go b/internal/modules/users/route.go index 9ba6bfb3..d6aa03fe 100644 --- a/internal/modules/users/route.go +++ b/internal/modules/users/route.go @@ -3,7 +3,7 @@ package users import ( "github.com/gofiber/fiber/v2" - "gitlab.com/mbugroup/lti-api.git/internal/middleware" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/users/controllers" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" ) @@ -12,11 +12,11 @@ func UserRoutes(v1 fiber.Router, s user.UserService) { ctrl := controller.NewUserController(s) route := v1.Group("/users") - route.Use(middleware.Auth(s)) + route.Use(m.Auth(s)) - route.Get("/", ctrl.GetAll) + route.Get("/", m.RequirePermissions(m.P_UserGetAll), ctrl.GetAll) // route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) + route.Get("/:id", m.RequirePermissions(m.P_UserGetOne), ctrl.GetOne) // route.Patch("/:id", ctrl.UpdateOne) // route.Delete("/:id", ctrl.DeleteOne) } diff --git a/internal/response/response.go b/internal/response/response.go index c4ecca0f..710d320e 100644 --- a/internal/response/response.go +++ b/internal/response/response.go @@ -14,10 +14,11 @@ type Success struct { } type Meta struct { - Page int `json:"page"` - Limit int `json:"limit"` - TotalPages int64 `json:"total_pages"` - TotalResults int64 `json:"total_results"` + Page int `json:"page"` + Limit int `json:"limit"` + TotalPages int64 `json:"total_pages"` + TotalResults int64 `json:"total_results"` + Filters interface{} `json:"filters,omitempty"` } type SuccessWithPaginate[T any] struct { diff --git a/internal/utils/constant.go b/internal/utils/constant.go index b09bc187..85b33f9b 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -29,6 +29,18 @@ const ( FlagVitamin FlagType = "VITAMIN" FlagKimia FlagType = "KIMIA" FlagEkspedisi FlagType = "EKSPEDISI" + + // flag ayam + FlagAyamAfkir FlagType = "AYAM-AFKIR" + FlagAyamCulling FlagType = "AYAM-CULLING" + FlagAyamMati FlagType = "AYAM-MATI" + + //flag telur + FlagTelur FlagType = "TELUR" + FlagTelurUtuh FlagType = "TELUR-UTUH" + FlagTelurPecah FlagType = "TELUR-PECAH" + FlagTelurPutih FlagType = "TELUR-PUTIH" + FlagTelurRetak FlagType = "TELUR-RETAK" ) const ( @@ -123,6 +135,17 @@ const ( SupplierCategorySapronak SupplierCategory = "SAPRONAK" ) +// ------------------------------------------------------------------- +// ExpenseCategory +// ------------------------------------------------------------------- + +type ExpenseCategory string + +const ( + ExpenseCategoryBOP ExpenseCategory = "BOP" + ExpenseCategoryNonBOP ExpenseCategory = "NON-BOP" +) + // ------------------------------------------------------------------- // Kandang Status // ------------------------------------------------------------------- @@ -221,8 +244,8 @@ const ( ) var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{ - RecordingStepPengajuan: "Pengajuan", - RecordingStepDisetujui: "Disetujui", + RecordingStepPengajuan: "Pengajuan", + RecordingStepDisetujui: "Disetujui", } // ------------------------------------------------------------------- @@ -417,6 +440,14 @@ func IsValidSupplierCategory(v string) bool { return false } +func IsValidExpenseCategory(v string) bool { + switch ExpenseCategory(v) { + case ExpenseCategoryBOP, ExpenseCategoryNonBOP: + return true + } + return false +} + // example use // Recording helper