package controller import ( "encoding/json" "fmt" "math" "strconv" "strings" "time" "gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations" "gitlab.com/mbugroup/lti-api.git/internal/response" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" "github.com/gofiber/fiber/v2" ) type ExpenseController struct { ExpenseService service.ExpenseService } const expenseExcelExportFetchLimit = 100 func NewExpenseController(expenseService service.ExpenseService) *ExpenseController { return &ExpenseController{ ExpenseService: expenseService, } } func (u *ExpenseController) GetAll(c *fiber.Ctx) error { if exportprogress.IsProgressExportRequest(c) { query, err := exportprogress.ParseQuery(c) if err != nil { return err } rows, err := u.ExpenseService.GetProgressRows(c, query) if err != nil { return err } content, err := exportprogress.BuildWorkbook("Expenses", query, rows) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to generate progress excel file") } filename := fmt.Sprintf("expenses_progress_%s.xlsx", time.Now().Format("20060102_150405")) c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) return c.Status(fiber.StatusOK).Send(content) } query := &validation.Query{ Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 10), Search: strings.TrimSpace(c.Query("search", "")), TransactionDate: strings.TrimSpace(c.Query("transaction_date", "")), RealizationDate: strings.TrimSpace(c.Query("realization_date", "")), LocationID: uint64(c.QueryInt("location_id", 0)), VendorID: uint64(c.QueryInt("vendor_id", 0)), Category: strings.TrimSpace(c.Query("category", "")), ApprovalStatus: strings.TrimSpace(c.Query("approval_status", "")), RealizationStatus: strings.TrimSpace(c.Query("realization_status", "")), ProjectFlockID: uint64(c.QueryInt("project_flock_id", 0)), ProjectFlockKandangID: uint64(c.QueryInt("project_flock_kandang_id", 0)), SortBy: strings.TrimSpace(c.Query("sort_by", "")), SortOrder: strings.TrimSpace(c.Query("sort_order", "")), } if isAllExpenseExcelExportRequest(c) { allResults, err := u.getAllExpensesForExcel(c, query) if err != nil { return err } return exportExpenseListExcel(c, allResults) } if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } result, totalResults, err := u.ExpenseService.GetAll(c, query) if err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.ExpenseListDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get all expenses successfully", Meta: response.Meta{ Page: query.Page, Limit: query.Limit, TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, Data: result, }) } func (u *ExpenseController) getAllExpensesForExcel(c *fiber.Ctx, baseQuery *validation.Query) ([]dto.ExpenseListDTO, error) { query := *baseQuery query.Page = 1 query.Limit = expenseExcelExportFetchLimit results := make([]dto.ExpenseListDTO, 0) for { pageResults, total, err := u.ExpenseService.GetAll(c, &query) if err != nil { return nil, err } if len(pageResults) == 0 || total == 0 { break } results = append(results, pageResults...) if int64(len(results)) >= total { break } query.Page++ } return results, nil } func (u *ExpenseController) GetOne(c *fiber.Ctx) error { param := c.Params("id") id, err := strconv.Atoi(param) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } result, err := u.ExpenseService.GetOne(c, uint(id)) if err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", Message: "Get expense successfully", Data: result, }) } func (u *ExpenseController) CreateOne(c *fiber.Ctx) error { req := new(validation.Create) req.TransactionDate = c.FormValue("transaction_date") req.Category = c.FormValue("category") supplierID, err := strconv.ParseUint(c.FormValue("supplier_id"), 10, 64) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_id format") } req.SupplierID = supplierID locationID, err := strconv.ParseUint(c.FormValue("location_id"), 10, 64) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id format") } req.LocationID = locationID form, err := c.MultipartForm() if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form") } req.Documents = form.File["documents"] expenseNonstocksJSON := c.FormValue("expense_nonstocks") if expenseNonstocksJSON != "" { if err := json.Unmarshal([]byte(expenseNonstocksJSON), &req.ExpenseNonstocks); err != nil { var singleExpenseNonstock validation.ExpenseNonstock if err := json.Unmarshal([]byte(expenseNonstocksJSON), &singleExpenseNonstock); err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid expense_nonstocks JSON: %v", err)) } req.ExpenseNonstocks = []validation.ExpenseNonstock{singleExpenseNonstock} } } else { return fiber.NewError(fiber.StatusBadRequest, "Field expense_nonstocks is required") } result, err := u.ExpenseService.CreateOne(c, req) if err != nil { return err } return c.Status(fiber.StatusCreated). JSON(response.Success{ Code: fiber.StatusCreated, Status: "success", Message: "Create expense successfully", Data: result, }) } func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error { req := new(validation.Update) param := c.Params("id") id, err := strconv.Atoi(param) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } form, err := c.MultipartForm() if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form") } req.Documents = form.File["documents"] transactionDate := c.FormValue("transaction_date") if transactionDate != "" { req.TransactionDate = &transactionDate } categoryVal := c.FormValue("category") if categoryVal != "" { req.Category = &categoryVal } supplierIDVal := c.FormValue("supplier_id") if supplierIDVal != "" { supplierID, err := strconv.ParseUint(supplierIDVal, 10, 64) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_id format") } req.SupplierID = &supplierID } locationIDVal := c.FormValue("location_id") if locationIDVal != "" { locationID, err := strconv.ParseUint(locationIDVal, 10, 64) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id format") } req.LocationID = &locationID } expenseNonstocksJSON := c.FormValue("expense_nonstocks") if expenseNonstocksJSON != "" { var expenseNonstocks []validation.ExpenseNonstock if err := json.Unmarshal([]byte(expenseNonstocksJSON), &expenseNonstocks); err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid expense_nonstocks JSON: %v", err)) } req.ExpenseNonstocks = &expenseNonstocks } result, err := u.ExpenseService.UpdateOne(c, req, uint(id)) if err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", Message: "Update expense successfully", Data: result, }) } func (u *ExpenseController) DeleteOne(c *fiber.Ctx) error { param := c.Params("id") id64, err := strconv.ParseUint(param, 10, 64) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } if err := u.ExpenseService.DeleteOne(c, id64); err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.Common{ Code: fiber.StatusOK, Status: "success", Message: "Delete expense successfully", }) } func (u *ExpenseController) Approval(c *fiber.Ctx) error { req := new(validation.ApprovalRequest) if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } path := c.Path() approvalType := "" if strings.Contains(path, "/approvals/head-area") { approvalType = "head-area" } else if strings.Contains(path, "/approvals/finance") { approvalType = "finance" } else if strings.Contains(path, "/approvals/unit-vice-president") { approvalType = "unit-vice-president" } else { return fiber.NewError(fiber.StatusBadRequest, "Invalid approval path") } results, err := u.ExpenseService.Approval(c, req, approvalType) if err != nil { return err } var ( data interface{} message = "Submit expense approval successfully" ) if len(results) == 1 { data = results[0] } else { message = "Submit expense approvals successfully" data = results } return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", Message: message, Data: data, }) } func (u *ExpenseController) BulkApproveToStatus(c *fiber.Ctx) error { req := new(validation.BulkApprovalRequest) if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } targetStep, err := req.ResolveTarget() if err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } if req.RequiresDate(targetStep) && strings.TrimSpace(req.Date) == "" { return fiber.NewError(fiber.StatusBadRequest, "date is required for REALISASI bulk approval") } if err := ensureExpenseBulkApprovalPermission(c, targetStep); err != nil { return err } results, err := u.ExpenseService.BulkApproveToStatus(c, req, targetStep) if err != nil { return err } var ( data interface{} message = "Bulk approve expense successfully" ) if len(results) == 1 { data = results[0] } else { message = "Bulk approve expenses successfully" data = results } return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", Message: message, Data: data, }) } func (u *ExpenseController) CreateRealization(c *fiber.Ctx) error { expenseID := c.Params("id") id, err := strconv.Atoi(expenseID) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID") } req := new(validation.CreateRealization) form, err := c.MultipartForm() if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form") } req.Documents = form.File["documents"] req.RealizationDate = c.FormValue("realization_date") realizationsJSON := c.FormValue("realizations") if realizationsJSON != "" { if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err)) } } expense, err := u.ExpenseService.CreateRealization(c, uint(id), req) if err != nil { return err } return c.Status(fiber.StatusCreated). JSON(response.Success{ Code: fiber.StatusCreated, Status: "success", Message: "Create realization successfully", Data: expense, }) } func (u *ExpenseController) UpdateRealization(c *fiber.Ctx) error { expenseID := c.Params("id") id, err := strconv.Atoi(expenseID) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID") } var req validation.UpdateRealization form, err := c.MultipartForm() if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form") } req.Documents = form.File["documents"] realizationDate := c.FormValue("realization_date") if realizationDate != "" { req.RealizationDate = &realizationDate } realizationsJSON := c.FormValue("realizations") if realizationsJSON != "" { var realizations []validation.RealizationItem if err := json.Unmarshal([]byte(realizationsJSON), &realizations); err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err)) } req.Realizations = &realizations } expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req) if err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", Message: "Update realization successfully", Data: expense, }) } func (u *ExpenseController) CompleteExpense(c *fiber.Ctx) error { expenseID := c.Params("id") id, err := strconv.Atoi(expenseID) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID") } expense, err := u.ExpenseService.CompleteExpense(c, uint(id), nil) if err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", Message: "Complete expense successfully", Data: expense, }) } func (u *ExpenseController) Pay(c *fiber.Ctx) error { expenseID := c.Params("id") id, err := strconv.Atoi(expenseID) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID") } expense, err := u.ExpenseService.Pay(c, uint(id)) if err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", Message: "Pay expense successfully", Data: expense, }) } func ensureExpenseBulkApprovalPermission(c *fiber.Ctx, targetStep approvalutils.ApprovalStep) error { requiredPerms := []string{} switch targetStep { case utils.ExpenseStepHeadArea: requiredPerms = []string{m.P_ExpenseApprovalHeadArea} case utils.ExpenseStepUnitVicePresident: requiredPerms = []string{m.P_ExpenseApprovalUnitVicePresident} case utils.ExpenseStepFinance: requiredPerms = []string{m.P_ExpenseApprovalFinance} case utils.ExpenseStepRealisasi: requiredPerms = []string{m.P_ExpenseApprovalFinance, m.P_ExpenseCreateRealizations} default: return fiber.NewError(fiber.StatusBadRequest, "Invalid approval target") } for _, perm := range requiredPerms { if !m.HasPermission(c, perm) { return fiber.NewError(fiber.StatusForbidden, "Insufficient permission") } } return nil } func (u *ExpenseController) DeleteDocument(c *fiber.Ctx) error { expenseID, err := strconv.Atoi(c.Params("id")) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID") } documentID, err := strconv.ParseUint(c.Params("documentId"), 10, 64) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid document ID") } if err := u.ExpenseService.DeleteDocument(c, uint(expenseID), documentID, false); err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.Common{ Code: fiber.StatusOK, Status: "success", Message: "Delete document successfully", }) } func (u *ExpenseController) DeleteRealizationDocument(c *fiber.Ctx) error { expenseID, err := strconv.Atoi(c.Params("id")) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID") } documentID, err := strconv.ParseUint(c.Params("documentId"), 10, 64) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid document ID") } if err := u.ExpenseService.DeleteDocument(c, uint(expenseID), documentID, true); err != nil { return err } return c.Status(fiber.StatusOK). JSON(response.Common{ Code: fiber.StatusOK, Status: "success", Message: "Delete realization document successfully", }) }