@@ -0,0 +1,585 @@
package controller
import (
"fmt"
"math"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/xuri/excelize/v2"
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
)
func isCustomerPaymentExcelExportRequest ( c * fiber . Ctx ) bool {
return strings . EqualFold ( strings . TrimSpace ( c . Query ( "export" ) ) , "excel" )
}
func isCustomerPaymentExcelAllExportRequest ( c * fiber . Ctx ) bool {
return strings . EqualFold ( strings . TrimSpace ( c . Query ( "export" ) ) , "excel-all" )
}
func exportCustomerPaymentExcel ( c * fiber . Ctx , items [ ] dto . CustomerPaymentReportItem ) error {
content , err := buildCustomerPaymentWorkbook ( items )
if err != nil {
return fiber . NewError ( fiber . StatusInternalServerError , "failed to generate excel file" )
}
filename := fmt . Sprintf ( "laporan-kontrol-pembayaran-customer-%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 exportCustomerPaymentExcelAll ( c * fiber . Ctx , items [ ] dto . CustomerPaymentReportItem ) error {
content , err := buildCustomerPaymentAllWorkbook ( items )
if err != nil {
return fiber . NewError ( fiber . StatusInternalServerError , "failed to generate excel file" )
}
filename := fmt . Sprintf ( "laporan-kontrol-pembayaran-customer-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 )
}
func buildCustomerPaymentWorkbook ( items [ ] dto . CustomerPaymentReportItem ) ( [ ] byte , error ) {
file := excelize . NewFile ( )
defer file . Close ( )
defaultSheet := file . GetSheetName ( file . GetActiveSheetIndex ( ) )
if len ( items ) == 0 {
if err := writeCustomerPaymentSheet ( file , defaultSheet , dto . CustomerPaymentReportItem { } ) ; 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 := sanitizeCustomerPaymentSheetName ( customerPaymentName ( item ) )
if sheetName == "" {
sheetName = fmt . Sprintf ( "Customer %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 := writeCustomerPaymentSheet ( file , sheetName , item ) ; err != nil {
return nil , err
}
}
buf , err := file . WriteToBuffer ( )
if err != nil {
return nil , err
}
return buf . Bytes ( ) , nil
}
func buildCustomerPaymentAllWorkbook ( items [ ] dto . CustomerPaymentReportItem ) ( [ ] byte , error ) {
file := excelize . NewFile ( )
defer file . Close ( )
const sheet = "Kontrol Pembayaran Customer"
defaultSheet := file . GetSheetName ( file . GetActiveSheetIndex ( ) )
if defaultSheet != sheet {
if err := file . SetSheetName ( defaultSheet , sheet ) ; err != nil {
return nil , err
}
}
if err := setCustomerPaymentAllColumns ( file , sheet ) ; err != nil {
return nil , err
}
if err := setCustomerPaymentAllHeaders ( file , sheet ) ; err != nil {
return nil , err
}
if err := writeCustomerPaymentAllRows ( 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 cpSheetHeaders = [ ] string {
"No" ,
"Tanggal DO/Bayar" ,
"Tanggal Realisasi" ,
"Aging" ,
"Referensi" ,
"Nomor Polisi" ,
"Ekor/Qty" ,
"Berat (Kg)" ,
"AVG" ,
"Harga/Unit (Rp)" ,
"Harga Akhir (Rp)" ,
"Total (Rp)" ,
"Pembayaran (Rp)" ,
"Saldo Piutang (Rp)" ,
"Keterangan" ,
"Pengambilan" ,
"Sales/Marketing" ,
}
var cpAllSheetHeaders = append ( [ ] string { "Customer" } , cpSheetHeaders ... )
var cpSheetColumnWidths = map [ string ] float64 {
"A" : 5 ,
"B" : 15 ,
"C" : 12 ,
"D" : 8 ,
"E" : 12 ,
"F" : 15 ,
"G" : 10 ,
"H" : 12 ,
"I" : 10 ,
"J" : 15 ,
"K" : 15 ,
"L" : 15 ,
"M" : 15 ,
"N" : 15 ,
"O" : 20 ,
"P" : 15 ,
"Q" : 20 ,
}
var cpAllSheetColumnWidths = map [ string ] float64 {
"A" : 22 ,
"B" : 6 ,
"C" : 15 ,
"D" : 15 ,
"E" : 8 ,
"F" : 12 ,
"G" : 15 ,
"H" : 10 ,
"I" : 12 ,
"J" : 10 ,
"K" : 15 ,
"L" : 15 ,
"M" : 15 ,
"N" : 15 ,
"O" : 15 ,
"P" : 20 ,
"Q" : 15 ,
"R" : 20 ,
}
func writeCustomerPaymentSheet ( file * excelize . File , sheet string , item dto . CustomerPaymentReportItem ) error {
for col , width := range cpSheetColumnWidths {
if err := file . SetColWidth ( sheet , col , col , width ) ; err != nil {
return err
}
}
// Row 1: headers
for i , h := range cpSheetHeaders {
col , _ := excelize . ColumnNumberToName ( i + 1 )
if err := file . SetCellValue ( sheet , col + "1" , h ) ; err != nil {
return err
}
}
redStyle , err := file . NewStyle ( & excelize . Style {
Font : & excelize . Font { Color : "FF0000" } ,
} )
if err != nil {
return err
}
// Row 2: saldo awal
initialFormatted := formatCPRupiah ( item . InitialBalance )
if err := file . SetCellValue ( sheet , "N2" , initialFormatted ) ; err != nil {
return err
}
if item . InitialBalance < 0 {
if err := file . SetCellStyle ( sheet , "N2" , "N2" , redStyle ) ; err != nil {
return err
}
}
// Rows 3+: data rows
for i , row := range item . Rows {
rowNum := i + 3
rowStr := fmt . Sprintf ( "%d" , rowNum )
cells := customerPaymentRowCells ( row , i + 1 )
for colIdx , val := range cells {
col , _ := excelize . ColumnNumberToName ( colIdx + 1 )
if err := file . SetCellValue ( sheet , col + rowStr , val ) ; err != nil {
return err
}
}
if row . AccountsReceivable < 0 {
if err := file . SetCellStyle ( sheet , "N" + rowStr , "N" + rowStr , redStyle ) ; err != nil {
return err
}
}
}
// Total row
totalRowNum := len ( item . Rows ) + 3
totalRowStr := fmt . Sprintf ( "%d" , totalRowNum )
totalCells := map [ string ] string {
"A" : "Total" ,
"G" : formatCPIDInteger ( item . Summary . TotalQty ) ,
"H" : formatCPIDInteger ( item . Summary . TotalWeight ) ,
"K" : formatCPRupiah ( item . Summary . TotalFinalAmount ) ,
"L" : formatCPRupiah ( item . Summary . TotalGrandAmount ) ,
"M" : formatCPRupiah ( item . Summary . TotalPayment ) ,
"N" : formatCPRupiah ( item . Summary . TotalAccountsReceivable ) ,
}
for col , val := range totalCells {
if err := file . SetCellValue ( sheet , col + totalRowStr , val ) ; err != nil {
return err
}
}
if item . Summary . TotalAccountsReceivable < 0 {
if err := file . SetCellStyle ( sheet , "N" + totalRowStr , "N" + totalRowStr , redStyle ) ; err != nil {
return err
}
}
return nil
}
func setCustomerPaymentAllColumns ( file * excelize . File , sheet string ) error {
for col , width := range cpAllSheetColumnWidths {
if err := file . SetColWidth ( sheet , col , col , width ) ; err != nil {
return err
}
}
return file . SetRowHeight ( sheet , 1 , 24 )
}
func setCustomerPaymentAllHeaders ( file * excelize . File , sheet string ) 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 } ,
}
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 : borderStyle ,
} )
if err != nil {
return err
}
for i , h := range cpAllSheetHeaders {
col , _ := excelize . ColumnNumberToName ( i + 1 )
if err := file . SetCellValue ( sheet , col + "1" , h ) ; err != nil {
return err
}
}
lastCol , _ := excelize . ColumnNumberToName ( len ( cpAllSheetHeaders ) )
return file . SetCellStyle ( sheet , "A1" , lastCol + "1" , headerStyle )
}
func writeCustomerPaymentAllRows ( file * excelize . File , sheet string , items [ ] dto . CustomerPaymentReportItem ) 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
}
redDataStyle , err := file . NewStyle ( & excelize . Style {
Font : & excelize . Font { Color : "FF0000" , Family : "Arial" , Size : 10 } ,
Alignment : & excelize . Alignment { Vertical : "center" , WrapText : true } ,
Border : borderStyle ,
} )
if err != nil {
return err
}
redTotalStyle , err := file . NewStyle ( & excelize . Style {
Font : & excelize . Font { Bold : true , Color : "FF0000" , 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 ( cpAllSheetHeaders ) )
currentRow := 2
for _ , item := range items {
name := customerPaymentName ( item )
// Saldo awal row
saldoStr := fmt . Sprintf ( "%d" , currentRow )
if err := file . SetCellValue ( sheet , "A" + saldoStr , name ) ; err != nil {
return err
}
initialFormatted := formatCPRupiah ( item . InitialBalance )
if err := file . SetCellValue ( sheet , "O" + saldoStr , initialFormatted ) ; err != nil {
return err
}
if err := file . SetCellStyle ( sheet , "A" + saldoStr , lastHeaderCol + saldoStr , dataStyle ) ; err != nil {
return err
}
if item . InitialBalance < 0 {
if err := file . SetCellStyle ( sheet , "O" + saldoStr , "O" + saldoStr , redDataStyle ) ; 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 , name ) ; err != nil {
return err
}
cells := customerPaymentRowCells ( row , seq + 1 )
for colIdx , val := range cells {
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
}
if row . AccountsReceivable < 0 {
if err := file . SetCellStyle ( sheet , "O" + rowStr , "O" + rowStr , redDataStyle ) ; err != nil {
return err
}
}
currentRow ++
}
// Total row
totalStr := fmt . Sprintf ( "%d" , currentRow )
totalCells := map [ string ] string {
"A" : name ,
"B" : "Total" ,
"H" : formatCPIDInteger ( item . Summary . TotalQty ) ,
"I" : formatCPIDInteger ( item . Summary . TotalWeight ) ,
"L" : formatCPRupiah ( item . Summary . TotalFinalAmount ) ,
"M" : formatCPRupiah ( item . Summary . TotalGrandAmount ) ,
"N" : formatCPRupiah ( item . Summary . TotalPayment ) ,
"O" : formatCPRupiah ( item . Summary . TotalAccountsReceivable ) ,
}
for col , val := range totalCells {
if err := file . SetCellValue ( sheet , col + totalStr , val ) ; err != nil {
return err
}
}
if err := file . SetCellStyle ( sheet , "A" + totalStr , lastHeaderCol + totalStr , totalStyle ) ; err != nil {
return err
}
if item . Summary . TotalAccountsReceivable < 0 {
if err := file . SetCellStyle ( sheet , "O" + totalStr , "O" + totalStr , redTotalStyle ) ; err != nil {
return err
}
}
currentRow ++
// Empty separator row
currentRow ++
}
return nil
}
// customerPaymentRowCells returns 17 cell values for cols A..Q.
func customerPaymentRowCells ( row dto . CustomerPaymentReportRow , seq int ) [ ] interface { } {
return [ ] interface { } {
seq ,
formatCPDate ( row . TransDate ) ,
formatCPOptionalDate ( row . DeliveryDate ) ,
formatCPAging ( row . AgingDay ) ,
safeCPText ( row . Reference ) ,
joinCPStrings ( row . VehicleNumbers ) ,
formatCPIDInteger ( row . Qty ) ,
formatCPIDInteger ( row . Weight ) ,
formatCPAvg ( row . AverageWeight ) ,
formatCPRupiah ( row . UnitPrice ) ,
formatCPRupiah ( row . FinalPrice ) ,
formatCPRupiah ( row . TotalPrice ) ,
formatCPRupiah ( row . PaymentAmount ) ,
formatCPRupiah ( row . AccountsReceivable ) ,
safeCPText ( row . Status ) ,
joinCPStrings ( row . PickupInfo ) ,
safeCPText ( row . SalesPerson ) ,
}
}
func customerPaymentName ( item dto . CustomerPaymentReportItem ) string {
name := strings . TrimSpace ( item . Customer . Name )
if name == "" {
return "Customer"
}
return name
}
func sanitizeCustomerPaymentSheetName ( 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
}
var cpIndonesianMonths = [ 12 ] string {
"Jan" , "Feb" , "Mar" , "Apr" , "Mei" , "Jun" ,
"Jul" , "Agu" , "Sep" , "Okt" , "Nov" , "Des" ,
}
func formatCPDate ( t time . Time ) string {
if t . IsZero ( ) {
return "-"
}
loc , err := time . LoadLocation ( "Asia/Jakarta" )
if err == nil {
t = t . In ( loc )
}
return fmt . Sprintf ( "%02d %s %d" , t . Day ( ) , cpIndonesianMonths [ t . Month ( ) - 1 ] , t . Year ( ) )
}
func formatCPOptionalDate ( t * time . Time ) string {
if t == nil || t . IsZero ( ) {
return "-"
}
return formatCPDate ( * t )
}
func formatCPAging ( v * int ) string {
if v == nil {
return "-"
}
return strconv . Itoa ( * v )
}
func formatCPIDInteger ( v float64 ) string {
n := int64 ( math . Round ( v ) )
if n == 0 {
return "0"
}
negative := n < 0
abs := n
if negative {
abs = - n
}
s := strconv . FormatInt ( abs , 10 )
// insert dots as thousand separators
var b strings . Builder
start := len ( s ) % 3
if start == 0 {
start = 3
}
b . WriteString ( s [ : start ] )
for i := start ; i < len ( s ) ; i += 3 {
b . WriteByte ( '.' )
b . WriteString ( s [ i : i + 3 ] )
}
if negative {
return "-" + b . String ( )
}
return b . String ( )
}
func formatCPRupiah ( v float64 ) string {
const nbsp = " "
if v < 0 {
return "-Rp" + nbsp + formatCPIDInteger ( - v )
}
return "Rp" + nbsp + formatCPIDInteger ( v )
}
func formatCPAvg ( v float64 ) string {
if v == 0 {
return "0"
}
s := strconv . FormatFloat ( v , 'f' , 2 , 64 )
return strings . ReplaceAll ( s , "." , "," )
}
func safeCPText ( s string ) string {
t := strings . TrimSpace ( s )
if t == "" {
return "-"
}
return t
}
func joinCPStrings ( ss [ ] string ) string {
var parts [ ] string
for _ , s := range ss {
s = strings . TrimSpace ( s )
if s != "" {
parts = append ( parts , s )
}
}
if len ( parts ) == 0 {
return "-"
}
return strings . Join ( parts , "\n" )
}