Merge branch 'feat/export-marketing' into 'rc/01'

Feat/export marketing and recording

See merge request mbugroup/lti-api!581
This commit is contained in:
Giovanni Gabriel Septriadi
2026-06-01 14:45:41 +00:00
10 changed files with 487 additions and 98 deletions
@@ -72,6 +72,7 @@ func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
MarketingId: uint(c.QueryInt("marketing_id", 0)),
ProjectFlockID: uint(c.QueryInt("project_flock_id", 0)),
ProjectFlockKandangID: uint(c.QueryInt("project_flock_kandang_id", 0)),
WarehouseID: uint(c.QueryInt("warehouse_id", 0)),
SortBy: sortBy,
SortOrder: sortOrder,
}
@@ -70,23 +70,26 @@ func buildMarketingExportWorkbook(items []dto.MarketingListDTO) ([]byte, error)
}
func setMarketingExportColumns(file *excelize.File, sheet string) error {
// AQ = 17 columns
// E = Sales (new), H = Gudang (new), Satuan (old I) removed
columnWidths := map[string]float64{
"A": 16,
"B": 14,
"C": 18,
"D": 20,
"E": 14,
"F": 40,
"G": 10,
"H": 12,
"I": 12,
"J": 12,
"K": 16,
"L": 16,
"M": 18,
"N": 18,
"O": 18,
"P": 24,
"A": 16, // No. Order
"B": 14, // Tanggal
"C": 18, // Status
"D": 20, // Customer
"E": 20, // Sales (new)
"F": 14, // Tipe
"G": 40, // Nama Produk
"H": 20, // Gudang (new)
"I": 10, // Week
"J": 12, // Jumlah
"K": 12, // Qty Peti
"L": 16, // Berat Rata-rata (kg)
"M": 16, // Total Berat (kg)
"N": 18, // Harga Satuan
"O": 18, // Total Harga
"P": 18, // Grand Total
"Q": 24, // Catatan
}
for col, width := range columnWidths {
@@ -104,22 +107,23 @@ func setMarketingExportColumns(file *excelize.File, sheet string) error {
func setMarketingExportHeaders(file *excelize.File, sheet string) error {
headers := []string{
"No. Order", // A
"Tanggal", // B
"Status", // C
"Customer", // D
"Tipe", // E
"Nama Produk", // F
"Week", // G
"Jumlah", // H
"Satuan", // I
"Qty Peti", // J
"Berat Rata-rata (kg)", // K
"Total Berat (kg)", // L
"Harga Satuan", // M
"Total Harga", // N
"Grand Total", // O
"Catatan", // P
"No. Order", // A
"Tanggal", // B
"Status", // C
"Customer", // D
"Sales", // E (new)
"Tipe", // F
"Nama Produk", // G
"Gudang", // H (new)
"Week", // I
"Jumlah Butir", // J
"Qty Peti", // K
"Berat Rata-rata (kg)", // L
"Total Berat (kg)", // M
"Harga Satuan", // N
"Total Harga", // O
"Grand Total", // P
"Catatan", // Q
}
for i, header := range headers {
@@ -148,7 +152,7 @@ func setMarketingExportHeaders(file *excelize.File, sheet string) error {
return err
}
return file.SetCellStyle(sheet, "A1", "P1", headerStyle)
return file.SetCellStyle(sheet, "A1", "Q1", headerStyle)
}
func setMarketingExportRows(file *excelize.File, sheet string, items []dto.MarketingListDTO) error {
@@ -162,17 +166,161 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke
soDate := formatMarketingExportDate(item.SoDate)
status := formatMarketingExportStatus(item)
customer := safeMarketingExportText(item.Customer.Name)
grandTotal := sumMarketingGrandTotal(item.SalesOrder)
notes := safeMarketingExportText(item.Notes)
salesPerson := safeMarketingExportText(item.SalesPerson.Name)
isDeliveryOrder := strings.EqualFold(strings.TrimSpace(status), "delivery order")
// ── Delivery Order branch ──────────────────────────────────────────────
if isDeliveryOrder {
grandTotal := sumDeliveryGrandTotal(item.DeliveryOrder)
if len(item.DeliveryOrder) == 0 {
row++
r := strconv.Itoa(row)
vals := map[string]interface{}{
"A": soNumber, "B": soDate, "C": status, "D": customer, "E": salesPerson,
"F": "-", "G": "-", "H": "-", "I": "-", "J": "-", "K": "-",
"L": "-", "M": "-", "N": "-", "O": "-",
"P": grandTotal, "Q": notes,
}
for col, val := range vals {
if err := file.SetCellValue(sheet, col+r, val); err != nil {
return err
}
}
continue
}
// Build lookup map: MarketingProductId → SO product (for Week & MarketingType)
soProductMap := make(map[uint]*dto.DeliveryMarketingProductDTO, len(item.SalesOrder))
for i := range item.SalesOrder {
soProductMap[item.SalesOrder[i].Id] = &item.SalesOrder[i]
}
for _, group := range item.DeliveryOrder {
doNumber := safeMarketingExportText(group.DoNumber)
doDate := "-"
if group.DeliveryDate != nil {
doDate = formatMarketingExportDate(*group.DeliveryDate)
}
gudang := "-"
if group.Warehouse != nil {
gudang = safeMarketingExportText(group.Warehouse.Name)
}
if len(group.Deliveries) == 0 {
row++
r := strconv.Itoa(row)
vals := map[string]interface{}{
"A": doNumber, "B": doDate, "C": status, "D": customer, "E": salesPerson,
"F": "-", "G": "-", "H": gudang, "I": "-", "J": "-", "K": "-",
"L": "-", "M": "-", "N": "-", "O": "-",
"P": grandTotal, "Q": notes,
}
for col, val := range vals {
if err := file.SetCellValue(sheet, col+r, val); err != nil {
return err
}
}
continue
}
for _, delivery := range group.Deliveries {
row++
r := strconv.Itoa(row)
productName := "-"
if delivery.ProductWarehouse != nil && delivery.ProductWarehouse.Product != nil {
if n := strings.TrimSpace(delivery.ProductWarehouse.Product.Name); n != "" {
productName = n
}
}
week := "-"
marketingType := "-"
if soProduct, ok := soProductMap[delivery.MarketingProductId]; ok {
if soProduct.Week != nil {
week = strconv.Itoa(*soProduct.Week)
}
marketingType = safeMarketingExportText(soProduct.MarketingType)
}
if err := file.SetCellValue(sheet, "A"+r, doNumber); err != nil {
return err
}
if err := file.SetCellValue(sheet, "B"+r, doDate); err != nil {
return err
}
if err := file.SetCellValue(sheet, "C"+r, status); err != nil {
return err
}
if err := file.SetCellValue(sheet, "D"+r, customer); err != nil {
return err
}
if err := file.SetCellValue(sheet, "E"+r, salesPerson); err != nil {
return err
}
if err := file.SetCellValue(sheet, "F"+r, marketingType); err != nil {
return err
}
if err := file.SetCellValue(sheet, "G"+r, productName); err != nil {
return err
}
if err := file.SetCellValue(sheet, "H"+r, gudang); err != nil {
return err
}
if err := file.SetCellValue(sheet, "I"+r, week); err != nil {
return err
}
if err := file.SetCellValue(sheet, "J"+r, delivery.Qty); err != nil {
return err
}
if delivery.TotalPeti != nil {
if err := file.SetCellValue(sheet, "K"+r, *delivery.TotalPeti); err != nil {
return err
}
} else {
if err := file.SetCellValue(sheet, "K"+r, "-"); err != nil {
return err
}
}
if err := file.SetCellValue(sheet, "L"+r, delivery.AvgWeight); err != nil {
return err
}
if err := file.SetCellValue(sheet, "M"+r, delivery.TotalWeight); err != nil {
return err
}
if err := file.SetCellValue(sheet, "N"+r, delivery.UnitPrice); err != nil {
return err
}
if err := file.SetCellValue(sheet, "O"+r, delivery.TotalPrice); err != nil {
return err
}
if err := file.SetCellValue(sheet, "P"+r, grandTotal); err != nil {
return err
}
if err := file.SetCellValue(sheet, "Q"+r, notes); err != nil {
return err
}
}
}
continue
}
// ── Sales Order branch (all other statuses) ───────────────────────────
grandTotal := sumMarketingGrandTotal(item.SalesOrder)
if len(item.SalesOrder) == 0 {
row++
r := strconv.Itoa(row)
vals := map[string]interface{}{
"A": soNumber, "B": soDate, "C": status, "D": customer,
"E": "-", "F": "-", "G": "-", "H": "-", "I": "-", "J": "-",
"K": "-", "L": "-", "M": "-", "N": "-",
"O": grandTotal, "P": notes,
"A": soNumber, "B": soDate, "C": status, "D": customer, "E": salesPerson,
"F": "-", "G": "-", "H": "-", "I": "-", "J": "-", "K": "-",
"L": "-", "M": "-", "N": "-", "O": "-",
"P": grandTotal, "Q": notes,
}
for col, val := range vals {
if err := file.SetCellValue(sheet, col+r, val); err != nil {
@@ -198,9 +346,9 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke
week = strconv.Itoa(*prod.Week)
}
satuan := "-"
if prod.ConvertionUnit != nil && strings.TrimSpace(*prod.ConvertionUnit) != "" {
satuan = *prod.ConvertionUnit
gudang := "-"
if prod.ProductWarehouse != nil {
gudang = safeMarketingExportText(prod.ProductWarehouse.Warehouse.Name)
}
if err := file.SetCellValue(sheet, "A"+r, soNumber); err != nil {
@@ -215,46 +363,49 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke
if err := file.SetCellValue(sheet, "D"+r, customer); err != nil {
return err
}
if err := file.SetCellValue(sheet, "E"+r, safeMarketingExportText(prod.MarketingType)); err != nil {
if err := file.SetCellValue(sheet, "E"+r, salesPerson); err != nil {
return err
}
if err := file.SetCellValue(sheet, "F"+r, productName); err != nil {
if err := file.SetCellValue(sheet, "F"+r, safeMarketingExportText(prod.MarketingType)); err != nil {
return err
}
if err := file.SetCellValue(sheet, "G"+r, week); err != nil {
if err := file.SetCellValue(sheet, "G"+r, productName); err != nil {
return err
}
if err := file.SetCellValue(sheet, "H"+r, prod.Qty); err != nil {
if err := file.SetCellValue(sheet, "H"+r, gudang); err != nil {
return err
}
if err := file.SetCellValue(sheet, "I"+r, satuan); err != nil {
if err := file.SetCellValue(sheet, "I"+r, week); err != nil {
return err
}
if err := file.SetCellValue(sheet, "J"+r, prod.Qty); err != nil {
return err
}
if prod.TotalPeti != nil {
if err := file.SetCellValue(sheet, "J"+r, *prod.TotalPeti); err != nil {
if err := file.SetCellValue(sheet, "K"+r, *prod.TotalPeti); err != nil {
return err
}
} else {
if err := file.SetCellValue(sheet, "J"+r, "-"); err != nil {
if err := file.SetCellValue(sheet, "K"+r, "-"); err != nil {
return err
}
}
if err := file.SetCellValue(sheet, "K"+r, prod.AvgWeight); err != nil {
if err := file.SetCellValue(sheet, "L"+r, prod.AvgWeight); err != nil {
return err
}
if err := file.SetCellValue(sheet, "L"+r, prod.TotalWeight); err != nil {
if err := file.SetCellValue(sheet, "M"+r, prod.TotalWeight); err != nil {
return err
}
if err := file.SetCellValue(sheet, "M"+r, prod.UnitPrice); err != nil {
if err := file.SetCellValue(sheet, "N"+r, prod.UnitPrice); err != nil {
return err
}
if err := file.SetCellValue(sheet, "N"+r, prod.TotalPrice); err != nil {
if err := file.SetCellValue(sheet, "O"+r, prod.TotalPrice); err != nil {
return err
}
if err := file.SetCellValue(sheet, "O"+r, grandTotal); err != nil {
if err := file.SetCellValue(sheet, "P"+r, grandTotal); err != nil {
return err
}
if err := file.SetCellValue(sheet, "P"+r, notes); err != nil {
if err := file.SetCellValue(sheet, "Q"+r, notes); err != nil {
return err
}
}
@@ -276,7 +427,7 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke
if err != nil {
return err
}
if err := file.SetCellStyle(sheet, "A2", "P"+lastRowStr, dataStyle); err != nil {
if err := file.SetCellStyle(sheet, "A2", "Q"+lastRowStr, dataStyle); err != nil {
return err
}
@@ -287,7 +438,7 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke
if err != nil {
return err
}
if err := file.SetCellStyle(sheet, "K2", "O"+lastRowStr, numberStyle); err != nil {
if err := file.SetCellStyle(sheet, "L2", "P"+lastRowStr, numberStyle); err != nil {
return err
}
@@ -298,7 +449,7 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke
if err != nil {
return err
}
for _, col := range []string{"G", "H", "J"} {
for _, col := range []string{"I", "J", "K"} {
if err := file.SetCellStyle(sheet, col+"2", col+lastRowStr, centerStyle); err != nil {
return err
}
@@ -327,16 +478,23 @@ func formatMarketingExportStatus(item dto.MarketingListDTO) string {
return safeMarketingExportText(item.LatestApproval.StepName)
}
func sumMarketingGrandTotal(items []dto.DeliveryMarketingProductDTO) float64 {
total := 0.0
for _, item := range items {
total += item.TotalPrice
}
return total
}
func sumDeliveryGrandTotal(groups []dto.DeliveryGroupDTO) float64 {
total := 0.0
for _, g := range groups {
for _, d := range g.Deliveries {
total += d.TotalPrice
}
}
return total
}
func safeMarketingExportText(value string) string {
trimmed := strings.TrimSpace(value)
@@ -28,6 +28,8 @@ type MarketingListDTO struct {
Customer customerDTO.CustomerRelationDTO `json:"customer"`
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
SoDocs string `json:"so_docs"`
GrandTotalSO float64 `json:"grand_total_so"`
GrandTotalDO float64 `json:"grand_total_do"`
SalesOrder []DeliveryMarketingProductDTO `json:"sales_order"`
DeliveryOrder []DeliveryGroupDTO `json:"delivery_order"`
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
@@ -198,11 +200,18 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType)
}
}
var grandTotalSO float64
for _, p := range marketing.Products {
grandTotalSO += p.TotalPrice
}
return MarketingListDTO{
MarketingRelationDTO: ToMarketingRelationDTO(marketing),
Customer: customer,
SalesPerson: salesPerson,
SoDocs: marketing.SoDocs,
GrandTotalSO: grandTotalSO,
GrandTotalDO: marketing.GrandTotal,
SalesOrder: salesOrderProducts,
DeliveryOrder: extractDeliveryGroupsFromProducts(marketing),
CreatedUser: createdUser,
@@ -287,6 +287,16 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
db = db.Where("marketings.customer_id = ?", params.CustomerId)
}
if params.WarehouseID != 0 {
db = db.Where(`EXISTS (
SELECT 1
FROM marketing_products mp
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
WHERE mp.marketing_id = marketings.id
AND pw.warehouse_id = ?
)`, params.WarehouseID)
}
db = s.applyMarketingProjectFlockFilter(c.Context(), db, params.ProjectFlockID, params.ProjectFlockKandangID)
db = s.applyMarketingSearchFilter(c.Context(), db, params.Search)
@@ -31,6 +31,7 @@ type DeliveryOrderQuery struct {
MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"`
ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"`
ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"`
WarehouseID uint `query:"warehouse_id" validate:"omitempty,gt=0"`
SortBy string `query:"sort_by" validate:"omitempty,oneof=so_number so_date status customer grand_total created_at"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
}
@@ -8,10 +8,12 @@ import (
"time"
"gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2"
)
@@ -75,6 +77,43 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
}
listDTO := dto.ToRecordingListDTOs(result)
recordingIDs := make([]uint, 0, len(result))
for i := range result {
if result[i].Id != 0 {
recordingIDs = append(recordingIDs, result[i].Id)
}
}
if len(recordingIDs) > 0 {
eggs, err := u.RecordingService.GetEggsWithFlagsByRecordingIDs(c.Context(), recordingIDs)
if err != nil {
return err
}
eggByRecording := make(map[uint][]entity.RecordingEgg, len(recordingIDs))
for _, egg := range eggs {
eggByRecording[egg.RecordingId] = append(eggByRecording[egg.RecordingId], egg)
}
for i := range listDTO {
id := listDTO[i].Id
if eggList, ok := eggByRecording[id]; ok {
breakdown := make(map[string]dto.EggExportBreakdownDTO)
for _, egg := range eggList {
flagName := eggTypeFromProductName(egg.ProductWarehouse.Product.Name)
if flagName == "" {
continue
}
entry := breakdown[flagName]
entry.Qty += egg.Qty
if egg.Weight != nil {
entry.Kg += *egg.Weight
}
breakdown[flagName] = entry
}
listDTO[i].EggExportBreakdown = breakdown
}
}
}
if strings.EqualFold(exportType, "excel") {
return exportRecordingListExcel(c, listDTO)
}
@@ -94,6 +133,33 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
})
}
// eggTypeFromProductName maps product name to egg type flag name by keyword matching.
// Falls back to empty string if no keyword matches.
func eggTypeFromProductName(name string) string {
normalized := strings.ToLower(strings.TrimSpace(name))
if normalized == "" {
return ""
}
// Ordered longest-first to prefer "papacal" over partial match of "pacal", etc.
keywords := []struct {
keyword string
flag string
}{
{"papacal", string(utils.FlagTelurPapacal)},
{"jumbo", string(utils.FlagTelurJumbo)},
{"retak", string(utils.FlagTelurRetak)},
{"putih", string(utils.FlagTelurPutih)},
{"pecah", string(utils.FlagTelurPecah)},
{"utuh", string(utils.FlagTelurUtuh)},
}
for _, k := range keywords {
if strings.Contains(normalized, k.keyword) {
return k.flag
}
}
return ""
}
func (u *RecordingController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
@@ -8,6 +8,7 @@ import (
"time"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2"
"github.com/xuri/excelize/v2"
@@ -79,6 +80,18 @@ func setRecordingExportColumns(file *excelize.File, sheet string) error {
"AB": 18,
"AC": 24,
"AD": 18,
"AE": 12,
"AF": 10,
"AG": 12,
"AH": 10,
"AI": 12,
"AJ": 10,
"AK": 12,
"AL": 10,
"AM": 12,
"AN": 10,
"AO": 12,
"AP": 10,
}
for col, width := range columnWidths {
@@ -208,6 +221,31 @@ func setRecordingExportHeaders(file *excelize.File, sheet string) error {
return err
}
eggTypes := []struct {
col1, col2, label string
}{
{"AE", "AF", "Telur Utuh"},
{"AG", "AH", "Telur Pecah"},
{"AI", "AJ", "Telur Putih"},
{"AK", "AL", "Telur Retak"},
{"AM", "AN", "Telur Papacal"},
{"AO", "AP", "Telur Jumbo"},
}
for _, et := range eggTypes {
if err := file.MergeCell(sheet, et.col1+"1", et.col2+"1"); err != nil {
return err
}
if err := file.SetCellValue(sheet, et.col1+"1", et.label); err != nil {
return err
}
if err := file.SetCellValue(sheet, et.col1+"2", "Butir"); err != nil {
return err
}
if err := file.SetCellValue(sheet, et.col2+"2", "Kg"); err != nil {
return err
}
}
headerStyle, err := file.NewStyle(&excelize.Style{
Font: &excelize.Font{
Bold: true,
@@ -234,7 +272,7 @@ func setRecordingExportHeaders(file *excelize.File, sheet string) error {
return err
}
return file.SetCellStyle(sheet, "A1", "AD2", headerStyle)
return file.SetCellStyle(sheet, "A1", "AP2", headerStyle)
}
func setRecordingExportRows(file *excelize.File, sheet string, items []dto.RecordingListDTO) error {
@@ -245,7 +283,8 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
columns := []string{
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N",
"O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "AA", "AB",
"AC", "AD",
"AC", "AD", "AE", "AF", "AG", "AH", "AI", "AJ", "AK", "AL", "AM", "AN",
"AO", "AP",
}
currentRow := 3
@@ -293,14 +332,14 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
// Expand recordings into one row per sapronak
type sapronakRow struct {
name string
input string
input interface{} // float64 for numeric, string "-" for placeholder
}
sapronaks := make([]sapronakRow, 0)
if len(item.FeedUsage) > 0 {
for _, fu := range item.FeedUsage {
sapronaks = append(sapronaks, sapronakRow{
name: safeExportText(fu.ProductName),
input: formatNumberID(fu.UsageAmount+fu.PendingQty, 2, true),
input: fu.UsageAmount + fu.PendingQty,
})
}
} else {
@@ -311,37 +350,66 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
for sIdx, s := range sapronaks {
if sIdx == 0 {
eggQty := func(flagName string) int {
if item.EggExportBreakdown != nil {
if bd, ok := item.EggExportBreakdown[flagName]; ok {
return bd.Qty
}
}
return 0
}
eggKg := func(flagName string) float64 {
if item.EggExportBreakdown != nil {
if bd, ok := item.EggExportBreakdown[flagName]; ok {
return bd.Kg
}
}
return 0
}
rowValues := []interface{}{
i + 1, // A
locationName, // B
safeExportText(item.ProjectFlock.FlockName), // C
kandangName, // D
item.ProjectFlock.Period, // E
i + 1, // A
locationName, // B
safeExportText(item.ProjectFlock.FlockName), // C
kandangName, // D
item.ProjectFlock.Period, // E
formatCategoryLabel(item.ProjectFlock.ProjectFlockCategory), // F
formatAgeLabel(item), // G
formatDateIndonesian(item.RecordDatetime), // H
formatNumberID(item.ProjectFlock.TotalChickQty, 0, false), // I
formatNumberID(item.FcrValue, 2, true), // J
formatNumberID(fcrStd, 2, true), // K
formatNumberID(item.FeedIntake, 2, true), // L
formatNumberID(feedIntakeStd, 2, true), // M
formatPercentID(item.CumDepletionRate, 2), // N
formatPercentID(maxDepletionStd, 2), // O
formatNumberID(item.TotalDepletionQty, 2, true), // P
formatNumberID(item.EggMass, 2, true), // Q
formatNumberID(eggMassStd, 2, true), // R
formatNumberID(item.EggWeight, 2, true), // S
formatNumberID(eggWeightStd, 2, true), // T
formatPercentID(item.HenDay, 2), // U
formatPercentID(henDayStd, 2), // V
formatPercentID(item.HenHouse, 2), // W
formatPercentID(henHouseStd, 2), // X
formatApprovalStatus(item), // Y
formatAgeLabel(item), // G
formatDateIndonesian(item.RecordDatetime), // H
item.ProjectFlock.TotalChickQty, // I
item.FcrValue, // J
fcrStd, // K
item.FeedIntake, // L
feedIntakeStd, // M
item.CumDepletionRate, // N
maxDepletionStd, // O
item.TotalDepletionQty, // P
item.EggMass, // Q
eggMassStd, // R
item.EggWeight, // S
eggWeightStd, // T
item.HenDay, // U
henDayStd, // V
item.HenHouse, // W
henHouseStd, // X
formatApprovalStatus(item), // Y
safeExportText(pointerString(item.Approval.Notes)), // Z
createdBy, // AA
formatDateIndonesian(item.CreatedAt), // AB
s.name, // AC
s.input, // AD
createdBy, // AA
formatDateIndonesian(item.CreatedAt), // AB
s.name, // AC
s.input, // AD
eggQty(string(utils.FlagTelurUtuh)), // AE
eggKg(string(utils.FlagTelurUtuh)), // AF
eggQty(string(utils.FlagTelurPecah)), // AG
eggKg(string(utils.FlagTelurPecah)), // AH
eggQty(string(utils.FlagTelurPutih)), // AI
eggKg(string(utils.FlagTelurPutih)), // AJ
eggQty(string(utils.FlagTelurRetak)), // AK
eggKg(string(utils.FlagTelurRetak)), // AL
eggQty(string(utils.FlagTelurPapacal)), // AM
eggKg(string(utils.FlagTelurPapacal)), // AN
eggQty(string(utils.FlagTelurJumbo)), // AO
eggKg(string(utils.FlagTelurJumbo)), // AP
}
for idx, col := range columns {
@@ -379,7 +447,7 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
if err != nil {
return err
}
if err := file.SetCellStyle(sheet, "A3", fmt.Sprintf("AD%d", lastRow), dataCenterStyle); err != nil {
if err := file.SetCellStyle(sheet, "A3", fmt.Sprintf("AP%d", lastRow), dataCenterStyle); err != nil {
return err
}
@@ -445,6 +513,7 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
mergeCols := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N",
"O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "AA", "AB",
"AE", "AF", "AG", "AH", "AI", "AJ", "AK", "AL", "AM", "AN", "AO", "AP",
}
for _, rng := range itemRanges {
if rng.end > rng.start {
@@ -454,6 +523,53 @@ func setRecordingExportRows(file *excelize.File, sheet string, items []dto.Recor
}
file.SetCellStyle(sheet, fmt.Sprintf("AC%d", rng.end), fmt.Sprintf("AC%d", rng.end), borderBottomLeftStyle)
file.SetCellStyle(sheet, fmt.Sprintf("AD%d", rng.end), fmt.Sprintf("AD%d", rng.end), borderBottomCenterStyle)
// Egg columns use center + thick bottom border
for _, col := range []string{"AE", "AF", "AG", "AH", "AI", "AJ", "AK", "AL", "AM", "AN", "AO", "AP"} {
file.SetCellStyle(sheet, fmt.Sprintf("%s%d", col, rng.end), fmt.Sprintf("%s%d", col, rng.end), borderBottomCenterStyle)
}
}
numFmtInt := "0"
numberIntStyle, err := file.NewStyle(&excelize.Style{
CustomNumFmt: &numFmtInt,
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
Border: []excelize.Border{
{Type: "left", Color: "E6E6E6", Style: 1},
{Type: "top", Color: "E6E6E6", Style: 1},
{Type: "bottom", Color: "E6E6E6", Style: 1},
{Type: "right", Color: "E6E6E6", Style: 1},
},
})
if err != nil {
return err
}
numFmtFloat := "0.00"
numberFloatStyle, err := file.NewStyle(&excelize.Style{
CustomNumFmt: &numFmtFloat,
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
Border: []excelize.Border{
{Type: "left", Color: "E6E6E6", Style: 1},
{Type: "top", Color: "E6E6E6", Style: 1},
{Type: "bottom", Color: "E6E6E6", Style: 1},
{Type: "right", Color: "E6E6E6", Style: 1},
},
})
if err != nil {
return err
}
intCols := []string{"E", "I", "AE", "AG", "AI", "AK", "AM", "AO"}
for _, col := range intCols {
if err := file.SetCellStyle(sheet, col+"3", fmt.Sprintf("%s%d", col, lastRow), numberIntStyle); err != nil {
return err
}
}
floatCols := []string{"J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "AD", "AF", "AH", "AJ", "AL", "AN", "AP"}
for _, col := range floatCols {
if err := file.SetCellStyle(sheet, col+"3", fmt.Sprintf("%s%d", col, lastRow), numberFloatStyle); err != nil {
return err
}
}
return nil
@@ -100,14 +100,20 @@ type RecordingFeedUsageDTO struct {
PendingQty float64 `json:"pending_qty"`
}
type EggExportBreakdownDTO struct {
Qty int `json:"qty"`
Kg float64 `json:"kg"`
}
type RecordingListDTO struct {
RecordingRelationDTO
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Kandang *RecordingKandangDTO `json:"kandang,omitempty"`
Location *RecordingLocationDTO `json:"location,omitempty"`
FeedUsage []RecordingFeedUsageDTO `json:"feed_usage,omitempty"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Kandang *RecordingKandangDTO `json:"kandang,omitempty"`
Location *RecordingLocationDTO `json:"location,omitempty"`
FeedUsage []RecordingFeedUsageDTO `json:"feed_usage,omitempty"`
EggExportBreakdown map[string]EggExportBreakdownDTO `json:"egg_breakdown,omitempty"`
}
type RecordingDetailDTO struct {
@@ -51,6 +51,7 @@ type RecordingRepository interface {
UpdateEggTotalQty(tx *gorm.DB, eggID uint, totalQty float64) error
UpdateEggWeight(tx *gorm.DB, eggID uint, weight *float64) error
GetRecordingEggByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.RecordingEgg, error)
GetEggsWithFlagsByRecordingIDs(ctx context.Context, recordingIDs []uint) ([]entity.RecordingEgg, error)
ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error)
@@ -581,6 +582,22 @@ func (r *RecordingRepositoryImpl) GetRecordingEggByID(
return &egg, nil
}
func (r *RecordingRepositoryImpl) GetEggsWithFlagsByRecordingIDs(ctx context.Context, recordingIDs []uint) ([]entity.RecordingEgg, error) {
if len(recordingIDs) == 0 {
return nil, nil
}
var eggs []entity.RecordingEgg
err := r.DB().WithContext(ctx).
Preload("ProductWarehouse.Product").
Where("recording_eggs.recording_id IN ?", recordingIDs).
Find(&eggs).Error
if err != nil {
return nil, err
}
return eggs, nil
}
func (r *RecordingRepositoryImpl) ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error) {
if projectFlockKandangId == 0 {
return false, nil
@@ -46,6 +46,7 @@ type RecordingService interface {
DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error)
GetProgressRows(ctx *fiber.Ctx, query *exportprogress.Query) ([]exportprogress.Row, error)
GetEggsWithFlagsByRecordingIDs(ctx context.Context, recordingIDs []uint) ([]entity.RecordingEgg, error)
}
type recordingService struct {
@@ -259,6 +260,10 @@ func (s recordingService) GetProgressRows(c *fiber.Ctx, query *exportprogress.Qu
return s.Repository.GetProgressRows(c.Context(), query.StartDate, query.EndDate, scope.IDs, scope.Restrict)
}
func (s recordingService) GetEggsWithFlagsByRecordingIDs(ctx context.Context, recordingIDs []uint) ([]entity.RecordingEgg, error) {
return s.Repository.GetEggsWithFlagsByRecordingIDs(ctx, recordingIDs)
}
func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, error) {
if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil {
return nil, err