add export laporang keuangan hutang ke supplier

This commit is contained in:
giovanni
2026-05-19 18:38:58 +07:00
parent 02b86be4c5
commit 907b695526
2 changed files with 536 additions and 0 deletions
@@ -392,6 +392,13 @@ func (c *RepportController) GetDebtSupplier(ctx *fiber.Ctx) error {
return err
}
if isDebtSupplierExcelExportRequest(ctx) {
return exportDebtSupplierExcel(ctx, result)
}
if isDebtSupplierExcelAllExportRequest(ctx) {
return exportDebtSupplierExcelAll(ctx, result)
}
supplierIDs = query.SupplierIDs
if supplierIDs == nil {
supplierIDs = []int64{}
@@ -505,6 +512,83 @@ func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error {
})
}
type BalanceMonitoringResponse struct {
Code int `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
Meta response.Meta `json:"meta"`
Data []dto.BalanceMonitoringRowDTO `json:"data"`
Totals dto.BalanceMonitoringTotalsDTO `json:"totals"`
}
func (c *RepportController) GetBalanceMonitoring(ctx *fiber.Ctx) error {
customerIDs, err := parseUintCSV(ctx.Query("customer_ids"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "customer_ids must be comma separated positive integers")
}
salesIDs, err := parseUintCSV(ctx.Query("sales_ids"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "sales_ids must be comma separated positive integers")
}
query := &validation.BalanceMonitoringQuery{
Page: ctx.QueryInt("page", 1),
Limit: ctx.QueryInt("limit", 10),
CustomerIDs: customerIDs,
SalesIDs: salesIDs,
FilterBy: strings.ToLower(ctx.Query("filter_by", "")),
SortBy: ctx.Query("sort_by", ""),
SortOrder: ctx.Query("sort_order", ""),
StartDate: ctx.Query("start_date", ""),
EndDate: ctx.Query("end_date", ""),
}
result, totals, totalResults, err := c.RepportService.GetBalanceMonitoring(ctx, query)
if err != nil {
return err
}
limit := query.Limit
if limit < 1 {
limit = 10
}
return ctx.Status(fiber.StatusOK).JSON(BalanceMonitoringResponse{
Code: fiber.StatusOK,
Status: "success",
Message: "Get balance monitoring report successfully",
Meta: response.Meta{
Page: query.Page,
Limit: limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(limit))),
TotalResults: totalResults,
},
Data: result,
Totals: totals,
})
}
func parseUintCSV(raw string) ([]uint, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, nil
}
parts := strings.Split(raw, ",")
result := make([]uint, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
id, err := strconv.ParseUint(part, 10, 32)
if err != nil || id == 0 {
return nil, fmt.Errorf("invalid id: %s", part)
}
result = append(result, uint(id))
}
return result, nil
}
func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error {
idParam := ctx.Params("idProjectFlockKandang")
if idParam == "" {
@@ -0,0 +1,452 @@
package controller
import (
"fmt"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/xuri/excelize/v2"
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
)
func isDebtSupplierExcelExportRequest(c *fiber.Ctx) bool {
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel")
}
func isDebtSupplierExcelAllExportRequest(c *fiber.Ctx) bool {
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel-all")
}
func exportDebtSupplierExcel(c *fiber.Ctx, items []dto.DebtSupplierDTO) error {
content, err := buildDebtSupplierWorkbook(items)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
}
filename := fmt.Sprintf("laporan-hutang-supplier-%s.xlsx", time.Now().Format("2006-01-02-1504"))
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)
}
func exportDebtSupplierExcelAll(c *fiber.Ctx, items []dto.DebtSupplierDTO) error {
content, err := buildDebtSupplierAllWorkbook(items)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
}
filename := fmt.Sprintf("laporan-hutang-supplier-all-%s.xlsx", time.Now().Format("2006-01-02-1504"))
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)
}
// buildDebtSupplierWorkbook creates a workbook with one sheet per supplier.
func buildDebtSupplierWorkbook(items []dto.DebtSupplierDTO) ([]byte, error) {
file := excelize.NewFile()
defer file.Close()
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
if len(items) == 0 {
if err := writeDebtSupplierSheet(file, defaultSheet, dto.DebtSupplierDTO{}); err != nil {
return nil, err
}
buf, err := file.WriteToBuffer()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
for idx, item := range items {
sheetName := sanitizeDebtSupplierSheetName(debtSupplierName(item))
if sheetName == "" {
sheetName = fmt.Sprintf("Supplier %d", idx+1)
}
if idx == 0 {
if defaultSheet != sheetName {
if err := file.SetSheetName(defaultSheet, sheetName); err != nil {
return nil, err
}
}
} else {
if _, err := file.NewSheet(sheetName); err != nil {
return nil, err
}
}
if err := writeDebtSupplierSheet(file, sheetName, item); err != nil {
return nil, err
}
}
buf, err := file.WriteToBuffer()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// buildDebtSupplierAllWorkbook creates a single-sheet workbook with purchase-supplier styling.
func buildDebtSupplierAllWorkbook(items []dto.DebtSupplierDTO) ([]byte, error) {
file := excelize.NewFile()
defer file.Close()
const sheet = "Rekap Hutang Supplier"
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
if defaultSheet != sheet {
if err := file.SetSheetName(defaultSheet, sheet); err != nil {
return nil, err
}
}
if err := setDebtSupplierAllColumns(file, sheet); err != nil {
return nil, err
}
if err := setDebtSupplierAllHeaders(file, sheet); err != nil {
return nil, err
}
if err := writeDebtSupplierAllRows(file, sheet, items); err != nil {
return nil, err
}
if err := file.SetPanes(sheet, &excelize.Panes{
Freeze: true,
YSplit: 1,
TopLeftCell: "A2",
ActivePane: "bottomLeft",
}); err != nil {
return nil, err
}
buf, err := file.WriteToBuffer()
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
var debtSupplierSheetHeaders = []string{
"No",
"Nomor PR",
"Nomor PO",
"Tanggal Terima/Bayar",
"Tanggal PO",
"Aging (Hari)",
"Area",
"Gudang",
"Jatuh Tempo",
"Status Jatuh Tempo",
"Nominal Pembelian (Rp)",
"Pembayaran (Rp)",
"Sisa Saldo Hutang (Rp)",
"Status",
"Nomor Perjalanan",
}
var debtSupplierAllSheetHeaders = append([]string{"Supplier"}, debtSupplierSheetHeaders...)
var debtSupplierSheetColumnWidths = map[string]float64{
"A": 5,
"B": 14,
"C": 12,
"D": 20,
"E": 10,
"F": 12,
"G": 15,
"H": 20,
"I": 12,
"J": 20,
"K": 20,
"L": 15,
"M": 20,
"N": 12,
"O": 15,
}
var debtSupplierAllSheetColumnWidths = map[string]float64{
"A": 24,
"B": 6,
"C": 14,
"D": 14,
"E": 20,
"F": 12,
"G": 10,
"H": 16,
"I": 22,
"J": 12,
"K": 22,
"L": 20,
"M": 18,
"N": 22,
"O": 14,
"P": 18,
}
func writeDebtSupplierSheet(file *excelize.File, sheet string, item dto.DebtSupplierDTO) error {
for col, width := range debtSupplierSheetColumnWidths {
if err := file.SetColWidth(sheet, col, col, width); err != nil {
return err
}
}
// Row 1: headers
for i, h := range debtSupplierSheetHeaders {
col, _ := excelize.ColumnNumberToName(i + 1)
if err := file.SetCellValue(sheet, col+"1", h); err != nil {
return err
}
}
// Row 2: saldo awal
if err := file.SetCellValue(sheet, "M2", item.InitialBalance); err != nil {
return err
}
// Rows 3+: data
redStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Color: "FF0000"},
})
if err != nil {
return err
}
for i, row := range item.Rows {
rowNum := i + 3
rowStr := fmt.Sprintf("%d", rowNum)
values := debtSupplierRowCells(row, i+1)
for colIdx, val := range values {
col, _ := excelize.ColumnNumberToName(colIdx + 1)
if err := file.SetCellValue(sheet, col+rowStr, val); err != nil {
return err
}
}
if row.DebtPrice < 0 {
if err := file.SetCellStyle(sheet, "M"+rowStr, "M"+rowStr, redStyle); err != nil {
return err
}
}
}
// Total row
totalRowNum := len(item.Rows) + 3
totalRowStr := fmt.Sprintf("%d", totalRowNum)
totalCells := map[string]interface{}{
"A": "Total",
"F": item.Total.Aging,
"K": item.Total.TotalPrice,
"L": item.Total.PaymentPrice,
"M": item.Total.DebtPrice,
}
for col, val := range totalCells {
if err := file.SetCellValue(sheet, col+totalRowStr, val); err != nil {
return err
}
}
if item.Total.DebtPrice < 0 {
if err := file.SetCellStyle(sheet, "M"+totalRowStr, "M"+totalRowStr, redStyle); err != nil {
return err
}
}
return nil
}
func setDebtSupplierAllColumns(file *excelize.File, sheet string) error {
for col, width := range debtSupplierAllSheetColumnWidths {
if err := file.SetColWidth(sheet, col, col, width); err != nil {
return err
}
}
if err := file.SetRowHeight(sheet, 1, 24); err != nil {
return err
}
return nil
}
func setDebtSupplierAllHeaders(file *excelize.File, sheet string) error {
headerStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "FFFFFF", Family: "Arial", Size: 10},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
Alignment: &excelize.Alignment{
Horizontal: "center",
Vertical: "center",
WrapText: true,
},
Border: []excelize.Border{
{Type: "left", Color: "000000", Style: 1},
{Type: "top", Color: "000000", Style: 1},
{Type: "bottom", Color: "000000", Style: 1},
{Type: "right", Color: "000000", Style: 1},
},
})
if err != nil {
return err
}
for i, h := range debtSupplierAllSheetHeaders {
col, _ := excelize.ColumnNumberToName(i + 1)
if err := file.SetCellValue(sheet, col+"1", h); err != nil {
return err
}
}
lastCol, _ := excelize.ColumnNumberToName(len(debtSupplierAllSheetHeaders))
return file.SetCellStyle(sheet, "A1", lastCol+"1", headerStyle)
}
func writeDebtSupplierAllRows(file *excelize.File, sheet string, items []dto.DebtSupplierDTO) error {
borderStyle := []excelize.Border{
{Type: "left", Color: "000000", Style: 1},
{Type: "top", Color: "000000", Style: 1},
{Type: "bottom", Color: "000000", Style: 1},
{Type: "right", Color: "000000", Style: 1},
}
dataStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Color: "000000", Family: "Arial", Size: 10},
Alignment: &excelize.Alignment{Vertical: "center", WrapText: true},
Border: borderStyle,
})
if err != nil {
return err
}
totalStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "000000", Family: "Arial", Size: 10},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"E2EFDA"}},
Alignment: &excelize.Alignment{Vertical: "center", WrapText: true},
Border: borderStyle,
})
if err != nil {
return err
}
lastHeaderCol, _ := excelize.ColumnNumberToName(len(debtSupplierAllSheetHeaders))
currentRow := 2
for _, item := range items {
supplierName := debtSupplierName(item)
// Saldo awal row
saldoRowStr := fmt.Sprintf("%d", currentRow)
if err := file.SetCellValue(sheet, "A"+saldoRowStr, supplierName); err != nil {
return err
}
if err := file.SetCellValue(sheet, "N"+saldoRowStr, item.InitialBalance); err != nil {
return err
}
if err := file.SetCellStyle(sheet, "A"+saldoRowStr, lastHeaderCol+saldoRowStr, dataStyle); err != nil {
return err
}
currentRow++
// Data rows
for seq, row := range item.Rows {
rowStr := fmt.Sprintf("%d", currentRow)
if err := file.SetCellValue(sheet, "A"+rowStr, supplierName); err != nil {
return err
}
values := debtSupplierRowCells(row, seq+1)
for colIdx, val := range values {
col, _ := excelize.ColumnNumberToName(colIdx + 2)
if err := file.SetCellValue(sheet, col+rowStr, val); err != nil {
return err
}
}
if err := file.SetCellStyle(sheet, "A"+rowStr, lastHeaderCol+rowStr, dataStyle); err != nil {
return err
}
currentRow++
}
// Total row
totalRowStr := fmt.Sprintf("%d", currentRow)
totalCells := map[string]interface{}{
"A": supplierName,
"B": "Total",
"L": item.Total.TotalPrice,
"M": item.Total.PaymentPrice,
"N": item.Total.DebtPrice,
}
for col, val := range totalCells {
if err := file.SetCellValue(sheet, col+totalRowStr, val); err != nil {
return err
}
}
if err := file.SetCellStyle(sheet, "A"+totalRowStr, lastHeaderCol+totalRowStr, totalStyle); err != nil {
return err
}
currentRow++
// Empty separator row
currentRow++
}
return nil
}
// debtSupplierRowCells returns cell values for one data row (columns: No, PR, PO, ReceivedDate, PoDate, Aging, Area, Warehouse, DueDate, DueStatus, TotalPrice, PaymentPrice, DebtPrice, Status, TravelNumber).
func debtSupplierRowCells(row dto.DebtSupplierRowDTO, seq int) []interface{} {
areaName := "-"
if row.Area != nil && strings.TrimSpace(row.Area.Name) != "" {
areaName = row.Area.Name
}
warehouseName := "-"
if row.Warehouse != nil && strings.TrimSpace(row.Warehouse.Name) != "" {
warehouseName = row.Warehouse.Name
}
return []interface{}{
seq,
safeDebtSupplierText(row.PrNumber),
safeDebtSupplierText(row.PoNumber),
safeDebtSupplierText(row.ReceivedDate),
safeDebtSupplierText(row.PoDate),
row.Aging,
areaName,
warehouseName,
safeDebtSupplierText(row.DueDate),
safeDebtSupplierText(row.DueStatus),
row.TotalPrice,
row.PaymentPrice,
row.DebtPrice,
safeDebtSupplierText(row.Status),
safeDebtSupplierText(row.TravelNumber),
}
}
func debtSupplierName(item dto.DebtSupplierDTO) string {
if item.Supplier != nil && strings.TrimSpace(item.Supplier.Name) != "" {
return item.Supplier.Name
}
return "Supplier"
}
func sanitizeDebtSupplierSheetName(name string) string {
replacer := strings.NewReplacer(
":", " ", "\\", " ", "/", " ",
"?", " ", "*", " ", "[", " ", "]", " ",
)
sanitized := strings.TrimSpace(replacer.Replace(name))
if sanitized == "" {
return "Sheet"
}
runes := []rune(sanitized)
if len(runes) > 31 {
return string(runes[:31])
}
return sanitized
}
func safeDebtSupplierText(s string) string {
t := strings.TrimSpace(s)
if t == "" {
return "-"
}
return t
}