mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'development' into 'codex/dashboard-without-uniformity'
# Conflicts: # internal/modules/dashboards/repositories/dashboard_stats.repository.go # internal/modules/dashboards/services/dashboard.service.go # internal/modules/production/uniformities/services/uniformity.service.go
This commit is contained in:
+38
-10
@@ -24,22 +24,50 @@ func NewProjectFlockKandangController(projectFlockKandangService service.Project
|
||||
|
||||
func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
ProjectFlockId: uint(c.QueryInt("project_flock_id", 0)),
|
||||
KandangId: uint(c.QueryInt("kandang_id", 0)),
|
||||
Category: c.Query("category", ""),
|
||||
AreaId: uint(c.QueryInt("area_id", 0)),
|
||||
SortBy: c.Query("sort_by", ""),
|
||||
SortOrder: c.Query("sort_order", ""),
|
||||
StepName: c.Query("step_name", ""),
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
NameWithPeriode: c.QueryBool("name_with_periode", false),
|
||||
ProjectFlockId: uint(c.QueryInt("project_flock_id", 0)),
|
||||
KandangId: uint(c.QueryInt("kandang_id", 0)),
|
||||
Category: c.Query("category", ""),
|
||||
AreaId: uint(c.QueryInt("area_id", 0)),
|
||||
LocationId: uint(c.QueryInt("location_id", 0)),
|
||||
SortBy: c.Query("sort_by", ""),
|
||||
SortOrder: c.Query("sort_order", ""),
|
||||
StepName: c.Query("step_name", ""),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
if query.NameWithPeriode {
|
||||
results, totalResults, err := u.ProjectFlockKandangService.GetAllNameWithPeriode(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := make([]dto.ProjectFlockKandangNameWithPeriodDTO, 0, len(results))
|
||||
for _, result := range results {
|
||||
data = append(data, dto.ToProjectFlockKandangNameWithPeriodDTOValues(result.Id, result.KandangName, result.Period))
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.ProjectFlockKandangNameWithPeriodDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all projectFlockKandangs successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
results, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+26
-3
@@ -60,6 +60,11 @@ type ProjectFlockKandangListDTO struct {
|
||||
ChickinApproval *approvalDTO.ApprovalRelationDTO `json:"chickin_approval,omitempty"`
|
||||
}
|
||||
|
||||
type ProjectFlockKandangNameWithPeriodDTO struct {
|
||||
Id uint `json:"id"`
|
||||
NameWithPeriod string `json:"name_with_period"`
|
||||
}
|
||||
|
||||
type ProjectFlockKandangDetailDTO struct {
|
||||
ProjectFlockKandangListDTO
|
||||
Chickins []chickinDTO.ChickinRelationDTO `json:"chickins,omitempty"`
|
||||
@@ -129,13 +134,17 @@ func toKandangRelation(kandang entity.Kandang) *kandangDTO.KandangRelationDTO {
|
||||
}
|
||||
|
||||
func toNameWithPeriod(kandang entity.Kandang, period int) string {
|
||||
if kandang.Name == "" {
|
||||
return toNameWithPeriodValue(kandang.Name, period)
|
||||
}
|
||||
|
||||
func toNameWithPeriodValue(kandangName string, period int) string {
|
||||
if kandangName == "" {
|
||||
return ""
|
||||
}
|
||||
if period == 0 {
|
||||
return kandang.Name
|
||||
return kandangName
|
||||
}
|
||||
return kandang.Name + " Period " + strconv.Itoa(period)
|
||||
return kandangName + " Period " + strconv.Itoa(period)
|
||||
}
|
||||
|
||||
func toApprovalDTOSelector(
|
||||
@@ -167,6 +176,20 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand
|
||||
}
|
||||
}
|
||||
|
||||
func ToProjectFlockKandangNameWithPeriodDTO(e entity.ProjectFlockKandang) ProjectFlockKandangNameWithPeriodDTO {
|
||||
return ProjectFlockKandangNameWithPeriodDTO{
|
||||
Id: e.Id,
|
||||
NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period),
|
||||
}
|
||||
}
|
||||
|
||||
func ToProjectFlockKandangNameWithPeriodDTOValues(id uint, kandangName string, period int) ProjectFlockKandangNameWithPeriodDTO {
|
||||
return ProjectFlockKandangNameWithPeriodDTO{
|
||||
Id: id,
|
||||
NameWithPeriod: toNameWithPeriodValue(kandangName, period),
|
||||
}
|
||||
}
|
||||
|
||||
func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserRelationDTO {
|
||||
if pf.CreatedUser.Id != 0 {
|
||||
mapped := userDTO.ToUserRelationDTO(pf.CreatedUser)
|
||||
|
||||
+37
@@ -26,6 +26,7 @@ import (
|
||||
|
||||
type ProjectFlockKandangService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error)
|
||||
GetAllNameWithPeriode(ctx *fiber.Ctx, params *validation.Query) ([]ProjectFlockKandangNameWithPeriode, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error)
|
||||
CheckClosing(ctx *fiber.Ctx, id uint) (*ClosingCheckResult, error)
|
||||
Closing(ctx *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error)
|
||||
@@ -51,6 +52,12 @@ type ClosingCheckResult struct {
|
||||
Expenses []ExpenseSummary `json:"expenses"`
|
||||
}
|
||||
|
||||
type ProjectFlockKandangNameWithPeriode struct {
|
||||
Id uint
|
||||
KandangName string
|
||||
Period int
|
||||
}
|
||||
|
||||
type StockRemainingDetail struct {
|
||||
FlagName string `json:"flag_name"`
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
@@ -133,6 +140,36 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer
|
||||
return projectFlockKandangs, total, nil
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) GetAllNameWithPeriode(c *fiber.Ctx, params *validation.Query) ([]ProjectFlockKandangNameWithPeriode, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
scope, err := m.ResolveLocationScope(c, s.Repository.DB())
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
rows, total, err := s.Repository.GetAllNameWithPeriodeScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get projectFlockKandangs name_with_periode: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
results := make([]ProjectFlockKandangNameWithPeriode, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
results = append(results, ProjectFlockKandangNameWithPeriode{
|
||||
Id: row.Id,
|
||||
KandangName: row.KandangName,
|
||||
Period: row.Period,
|
||||
})
|
||||
}
|
||||
|
||||
return results, total, nil
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) {
|
||||
scope, err := m.ResolveLocationScope(c, s.Repository.DB())
|
||||
if err != nil {
|
||||
|
||||
+13
-11
@@ -11,19 +11,21 @@ type Update struct {
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
ProjectFlockId uint `query:"project_flock_id" validate:"omitempty"`
|
||||
KandangId uint `query:"kandang_id" validate:"omitempty"`
|
||||
Category string `query:"category" validate:"omitempty,oneof=Growing Laying"`
|
||||
AreaId uint `query:"area_id" validate:"omitempty"`
|
||||
SortBy string `query:"sort_by" validate:"omitempty,oneof=created_at period"`
|
||||
SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"`
|
||||
StepName string `query:"step_name" validate:"omitempty,max=50"`
|
||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
NameWithPeriode bool `query:"name_with_periode"`
|
||||
ProjectFlockId uint `query:"project_flock_id" validate:"omitempty"`
|
||||
KandangId uint `query:"kandang_id" validate:"omitempty"`
|
||||
Category string `query:"category" validate:"omitempty,oneof=Growing Laying"`
|
||||
AreaId uint `query:"area_id" validate:"omitempty"`
|
||||
LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"`
|
||||
SortBy string `query:"sort_by" validate:"omitempty,oneof=created_at period"`
|
||||
SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"`
|
||||
StepName string `query:"step_name" validate:"omitempty,max=50"`
|
||||
}
|
||||
|
||||
type Closing struct {
|
||||
Action string `json:"action" validate:"required,oneof=close unclose"`
|
||||
ClosedDate *string `json:"closed_date,omitempty"`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,8 +322,8 @@ func (r *ProjectflockRepositoryImpl) buildOrderExpressions(sortBy, sortOrder str
|
||||
}
|
||||
default:
|
||||
return []string{
|
||||
"project_flocks.created_at DESC",
|
||||
"project_flocks.updated_at DESC",
|
||||
"project_flocks.flock_name ASC",
|
||||
"project_flocks.id ASC",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+112
-2
@@ -22,6 +22,7 @@ type ProjectFlockKandangRepository interface {
|
||||
GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error)
|
||||
GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error)
|
||||
GetAllWithFiltersScoped(ctx context.Context, offset int, limit int, params interface{}, locationIDs []uint, restrict bool) ([]entity.ProjectFlockKandang, int64, error)
|
||||
GetAllNameWithPeriodeScoped(ctx context.Context, offset int, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]ProjectFlockKandangNameWithPeriode, int64, error)
|
||||
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error)
|
||||
ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
|
||||
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
|
||||
@@ -40,6 +41,12 @@ type projectFlockKandangRepositoryImpl struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type ProjectFlockKandangNameWithPeriode struct {
|
||||
Id uint `gorm:"column:id"`
|
||||
KandangName string `gorm:"column:kandang_name"`
|
||||
Period int `gorm:"column:period"`
|
||||
}
|
||||
|
||||
const flockBaseNameExpression = "LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')))"
|
||||
|
||||
func NewProjectFlockKandangRepository(db *gorm.DB) ProjectFlockKandangRepository {
|
||||
@@ -171,13 +178,17 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex
|
||||
if query.AreaId > 0 {
|
||||
q = q.Where("\"project_flocks\".\"area_id\" = ?", query.AreaId)
|
||||
}
|
||||
|
||||
if query.LocationId > 0 {
|
||||
q = q.Where("\"kandangs\".\"location_id\" = ?", query.LocationId)
|
||||
}
|
||||
}
|
||||
|
||||
if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
sortBy := "\"project_flock_kandangs\".\"created_at\" DESC"
|
||||
sortBy := "\"project_flock_kandangs\".\"id\" ASC"
|
||||
if ok && query != nil && query.SortBy != "" {
|
||||
sortOrder := "DESC"
|
||||
if query.SortOrder == "ASC" {
|
||||
@@ -269,13 +280,17 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFiltersScoped(ctx context.
|
||||
if query.AreaId > 0 {
|
||||
q = q.Where("\"project_flocks\".\"area_id\" = ?", query.AreaId)
|
||||
}
|
||||
|
||||
if query.LocationId > 0 {
|
||||
q = q.Where("\"kandangs\".\"location_id\" = ?", query.LocationId)
|
||||
}
|
||||
}
|
||||
|
||||
if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
sortBy := "\"project_flock_kandangs\".\"created_at\" DESC"
|
||||
sortBy := "\"project_flock_kandangs\".\"id\" ASC"
|
||||
if ok && query != nil && query.SortBy != "" {
|
||||
sortOrder := "DESC"
|
||||
if query.SortOrder == "ASC" {
|
||||
@@ -297,6 +312,101 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFiltersScoped(ctx context.
|
||||
return records, total, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) GetAllNameWithPeriodeScoped(ctx context.Context, offset int, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]ProjectFlockKandangNameWithPeriode, int64, error) {
|
||||
var records []ProjectFlockKandangNameWithPeriode
|
||||
var total int64
|
||||
|
||||
q := r.db.WithContext(ctx).
|
||||
Table("project_flock_kandangs").
|
||||
Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\"").
|
||||
Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\"")
|
||||
|
||||
if restrict {
|
||||
if len(locationIDs) == 0 {
|
||||
return []ProjectFlockKandangNameWithPeriode{}, 0, nil
|
||||
}
|
||||
q = q.Where("\"project_flocks\".\"location_id\" IN ?", locationIDs)
|
||||
}
|
||||
|
||||
if params != nil && params.StepName != "" {
|
||||
q = q.Where(`
|
||||
EXISTS (
|
||||
SELECT 1 FROM "approvals"
|
||||
WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id"
|
||||
AND "approvals"."approvable_type" = ?
|
||||
AND LOWER("approvals"."step_name") = LOWER(?)
|
||||
AND "approvals"."id" IN (
|
||||
SELECT "approvals"."id" FROM "approvals"
|
||||
WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id"
|
||||
AND "approvals"."approvable_type" = ?
|
||||
ORDER BY "approvals"."id" DESC
|
||||
LIMIT 1
|
||||
)
|
||||
)
|
||||
`, "PROJECT_FLOCK_KANDANGS", params.StepName, "PROJECT_FLOCK_KANDANGS")
|
||||
}
|
||||
|
||||
if params != nil {
|
||||
if params.Search != "" {
|
||||
escapedSearch := strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_").Replace(params.Search)
|
||||
q = q.Where(
|
||||
r.db.Where("LOWER(\"kandangs\".\"name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%").
|
||||
Or("LOWER(\"project_flocks\".\"flock_name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"),
|
||||
)
|
||||
}
|
||||
|
||||
if params.ProjectFlockId > 0 {
|
||||
q = q.Where("\"project_flock_kandangs\".\"project_flock_id\" = ?", params.ProjectFlockId)
|
||||
}
|
||||
|
||||
if params.KandangId > 0 {
|
||||
q = q.Where("\"project_flock_kandangs\".\"kandang_id\" = ?", params.KandangId)
|
||||
}
|
||||
|
||||
if params.Category != "" {
|
||||
q = q.Where("\"project_flocks\".\"category\" = ?", params.Category)
|
||||
}
|
||||
|
||||
if params.AreaId > 0 {
|
||||
q = q.Where("\"project_flocks\".\"area_id\" = ?", params.AreaId)
|
||||
}
|
||||
|
||||
if params.LocationId > 0 {
|
||||
q = q.Where("\"kandangs\".\"location_id\" = ?", params.LocationId)
|
||||
}
|
||||
}
|
||||
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
sortBy := "\"project_flock_kandangs\".\"created_at\" DESC"
|
||||
if params != nil && params.SortBy != "" {
|
||||
sortOrder := "DESC"
|
||||
if params.SortOrder == "ASC" {
|
||||
sortOrder = "ASC"
|
||||
}
|
||||
|
||||
switch params.SortBy {
|
||||
case "created_at":
|
||||
sortBy = "\"project_flock_kandangs\".\"created_at\" " + sortOrder
|
||||
case "period":
|
||||
sortBy = "\"project_flocks\".\"period\" " + sortOrder
|
||||
}
|
||||
}
|
||||
|
||||
if err := q.
|
||||
Select("\"project_flock_kandangs\".\"id\", \"project_flock_kandangs\".\"period\", \"kandangs\".\"name\" AS kandang_name").
|
||||
Order(sortBy).
|
||||
Offset(offset).
|
||||
Limit(limit).
|
||||
Scan(&records).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return records, total, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository {
|
||||
return &projectFlockKandangRepositoryImpl{db: tx}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ func NewRecordingController(recordingService service.RecordingService) *Recordin
|
||||
|
||||
func (u *RecordingController) GetAll(c *fiber.Ctx) error {
|
||||
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
|
||||
exportType := strings.TrimSpace(c.Query("export"))
|
||||
|
||||
page := c.QueryInt("page", 1)
|
||||
limit := c.QueryInt("limit", 10)
|
||||
@@ -46,6 +47,11 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
listDTO := dto.ToRecordingListDTOs(result)
|
||||
if strings.EqualFold(exportType, "excel") {
|
||||
return exportRecordingListExcel(c, listDTO)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.RecordingListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
@@ -57,7 +63,7 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error {
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: dto.ToRecordingListDTOs(result),
|
||||
Data: listDTO,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/dto"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
func exportRecordingListExcel(c *fiber.Ctx, items []dto.RecordingListDTO) error {
|
||||
file := excelize.NewFile()
|
||||
defer file.Close()
|
||||
|
||||
const sheetName = "Recordings"
|
||||
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
|
||||
if defaultSheet != sheetName {
|
||||
if err := file.SetSheetName(defaultSheet, sheetName); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare excel sheet")
|
||||
}
|
||||
}
|
||||
|
||||
if err := setRecordingExportColumns(file, sheetName); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare excel columns")
|
||||
}
|
||||
if err := setRecordingExportHeaders(file, sheetName); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare excel headers")
|
||||
}
|
||||
if err := setRecordingExportRows(file, sheetName, items); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare excel rows")
|
||||
}
|
||||
|
||||
buffer, err := file.WriteToBuffer()
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("recordings_%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(buffer.Bytes())
|
||||
}
|
||||
|
||||
func setRecordingExportColumns(file *excelize.File, sheet string) error {
|
||||
columnWidths := map[string]float64{
|
||||
"A": 6,
|
||||
"B": 18,
|
||||
"C": 24,
|
||||
"D": 18,
|
||||
"E": 10,
|
||||
"F": 12,
|
||||
"G": 20,
|
||||
"H": 18,
|
||||
"I": 16,
|
||||
"J": 12,
|
||||
"K": 12,
|
||||
"L": 16,
|
||||
"M": 16,
|
||||
"N": 18,
|
||||
"O": 18,
|
||||
"P": 16,
|
||||
"Q": 16,
|
||||
"R": 16,
|
||||
"S": 16,
|
||||
"T": 16,
|
||||
"U": 16,
|
||||
"V": 16,
|
||||
"W": 18,
|
||||
"X": 18,
|
||||
"Y": 18,
|
||||
"Z": 22,
|
||||
"AA": 16,
|
||||
"AB": 18,
|
||||
}
|
||||
|
||||
for col, width := range columnWidths {
|
||||
if err := file.SetColWidth(sheet, col, col, width); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := file.SetRowHeight(sheet, 1, 30); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetRowHeight(sheet, 2, 30); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setRecordingExportHeaders(file *excelize.File, sheet string) error {
|
||||
verticalHeaderCols := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "Y", "Z", "AA", "AB"}
|
||||
for _, col := range verticalHeaderCols {
|
||||
if err := file.MergeCell(sheet, col+"1", col+"2"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
headerValues := map[string]string{
|
||||
"A1": "No",
|
||||
"B1": "Lokasi",
|
||||
"C1": "Flock",
|
||||
"D1": "Kandang",
|
||||
"E1": "Periode",
|
||||
"F1": "Kategori",
|
||||
"G1": "Umur (hari)",
|
||||
"H1": "Waktu Recording",
|
||||
"I1": "Populasi Akhir",
|
||||
"Y1": "Status Approval",
|
||||
"Z1": "Catatan Approval",
|
||||
"AA1": "Dibuat Oleh",
|
||||
"AB1": "Tanggal Submit",
|
||||
}
|
||||
for cell, value := range headerValues {
|
||||
if err := file.SetCellValue(sheet, cell, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := file.MergeCell(sheet, "J1", "K1"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "J1", "FCR"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "J2", "Actual"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "K2", "Standard"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := file.MergeCell(sheet, "L1", "M1"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "L1", "Feed Intake (KG)"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "L2", "Actual"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "M2", "Standard"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := file.MergeCell(sheet, "N1", "P1"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "N1", "Mortality"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "N2", "Cum Depletion Rate"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "O2", "Max Depletion Std"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "P2", "Total Depletion"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := file.MergeCell(sheet, "Q1", "T1"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "Q1", "Egg Production"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "Q2", "Egg Mass Actual"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "R2", "Egg Mass Standar"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "S2", "Egg Weight Actual"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "T2", "Egg Weight Standar"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := file.MergeCell(sheet, "U1", "X1"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "U1", "Hen Performance"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "U2", "Hen Day Actual"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "V2", "Hen Day Standar"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "W2", "Hen House Actual"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "X2", "Hen House Standar"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headerStyle, err := file.NewStyle(&excelize.Style{
|
||||
Font: &excelize.Font{
|
||||
Bold: true,
|
||||
Color: "7A7A7A",
|
||||
},
|
||||
Fill: excelize.Fill{
|
||||
Type: "pattern",
|
||||
Pattern: 1,
|
||||
Color: []string{"F5F5F5"},
|
||||
},
|
||||
Alignment: &excelize.Alignment{
|
||||
Horizontal: "center",
|
||||
Vertical: "center",
|
||||
WrapText: true,
|
||||
},
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: "DDDDDD", Style: 1},
|
||||
{Type: "top", Color: "DDDDDD", Style: 1},
|
||||
{Type: "bottom", Color: "DDDDDD", Style: 1},
|
||||
{Type: "right", Color: "DDDDDD", Style: 1},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return file.SetCellStyle(sheet, "A1", "AB2", headerStyle)
|
||||
}
|
||||
|
||||
func setRecordingExportRows(file *excelize.File, sheet string, items []dto.RecordingListDTO) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
for i, item := range items {
|
||||
rowNumber := i + 3
|
||||
|
||||
fcrStd := 0.0
|
||||
if item.ProjectFlock.Fcr != nil {
|
||||
fcrStd = item.ProjectFlock.Fcr.FcrStd
|
||||
}
|
||||
|
||||
maxDepletionStd := 0.0
|
||||
eggMassStd := 0.0
|
||||
eggWeightStd := 0.0
|
||||
henDayStd := 0.0
|
||||
henHouseStd := 0.0
|
||||
feedIntakeStd := 0.0
|
||||
if item.ProjectFlock.ProductionStandart != nil {
|
||||
maxDepletionStd = item.ProjectFlock.ProductionStandart.MaxDepletionStd
|
||||
eggMassStd = item.ProjectFlock.ProductionStandart.EggMassStd
|
||||
eggWeightStd = item.ProjectFlock.ProductionStandart.EggWeightStd
|
||||
henDayStd = item.ProjectFlock.ProductionStandart.HenDayStd
|
||||
henHouseStd = item.ProjectFlock.ProductionStandart.HenHouseStd
|
||||
feedIntakeStd = item.ProjectFlock.ProductionStandart.FeedIntakeStd
|
||||
}
|
||||
|
||||
locationName := "-"
|
||||
if item.Location != nil {
|
||||
locationName = safeExportText(item.Location.Name)
|
||||
}
|
||||
|
||||
kandangName := "-"
|
||||
if item.Kandang != nil {
|
||||
kandangName = safeExportText(item.Kandang.Name)
|
||||
}
|
||||
|
||||
createdBy := "-"
|
||||
if item.CreatedUser != nil {
|
||||
createdBy = safeExportText(item.CreatedUser.Name)
|
||||
} else if strings.TrimSpace(item.Approval.ActionBy.Name) != "" {
|
||||
createdBy = safeExportText(item.Approval.ActionBy.Name)
|
||||
}
|
||||
|
||||
rowValues := []interface{}{
|
||||
i + 1,
|
||||
locationName,
|
||||
safeExportText(item.ProjectFlock.FlockName),
|
||||
kandangName,
|
||||
item.ProjectFlock.Period,
|
||||
formatCategoryLabel(item.ProjectFlock.ProjectFlockCategory),
|
||||
formatAgeLabel(item),
|
||||
formatDateIndonesian(item.RecordDatetime),
|
||||
formatNumberID(item.ProjectFlock.TotalChickQty, 0, false),
|
||||
formatNumberID(item.FcrValue, 2, true),
|
||||
formatNumberID(fcrStd, 2, true),
|
||||
formatNumberID(item.FeedIntake, 2, true),
|
||||
formatNumberID(feedIntakeStd, 2, true),
|
||||
formatPercentID(item.CumDepletionRate, 2),
|
||||
formatPercentID(maxDepletionStd, 2),
|
||||
formatNumberID(item.TotalDepletionQty, 2, true),
|
||||
formatNumberID(item.EggMass, 2, true),
|
||||
formatNumberID(eggMassStd, 2, true),
|
||||
formatNumberID(item.EggWeight, 2, true),
|
||||
formatNumberID(eggWeightStd, 2, true),
|
||||
formatPercentID(item.HenDay, 2),
|
||||
formatPercentID(henDayStd, 2),
|
||||
formatPercentID(item.HenHouse, 2),
|
||||
formatPercentID(henHouseStd, 2),
|
||||
formatApprovalStatus(item),
|
||||
safeExportText(pointerString(item.Approval.Notes)),
|
||||
createdBy,
|
||||
formatDateIndonesian(item.CreatedAt),
|
||||
}
|
||||
|
||||
for idx, col := range columns {
|
||||
cell := fmt.Sprintf("%s%d", col, rowNumber)
|
||||
if err := file.SetCellValue(sheet, cell, rowValues[idx]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastRow := len(items) + 2
|
||||
dataCenterStyle, err := file.NewStyle(&excelize.Style{
|
||||
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
|
||||
}
|
||||
if err := file.SetCellStyle(sheet, "A3", fmt.Sprintf("AB%d", lastRow), dataCenterStyle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dataLeftStyle, err := file.NewStyle(&excelize.Style{
|
||||
Alignment: &excelize.Alignment{
|
||||
Horizontal: "left",
|
||||
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
|
||||
}
|
||||
|
||||
leftColumns := []string{"B", "C", "D", "F", "G", "H", "Y", "Z", "AA", "AB"}
|
||||
for _, col := range leftColumns {
|
||||
if err := file.SetCellStyle(sheet, col+"3", fmt.Sprintf("%s%d", col, lastRow), dataLeftStyle); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatAgeLabel(item dto.RecordingListDTO) string {
|
||||
if item.Day <= 0 {
|
||||
return "-"
|
||||
}
|
||||
|
||||
week := 0
|
||||
if item.ProjectFlock.ProductionStandart != nil && item.ProjectFlock.ProductionStandart.Week > 0 {
|
||||
week = item.ProjectFlock.ProductionStandart.Week
|
||||
} else {
|
||||
week = ((item.Day - 1) / 7) + 1
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d (Minggu ke-%d)", item.Day, week)
|
||||
}
|
||||
|
||||
func formatDateIndonesian(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "-"
|
||||
}
|
||||
|
||||
loc, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err == nil {
|
||||
t = t.In(loc)
|
||||
}
|
||||
|
||||
monthNames := []string{
|
||||
"",
|
||||
"Januari",
|
||||
"Februari",
|
||||
"Maret",
|
||||
"April",
|
||||
"Mei",
|
||||
"Juni",
|
||||
"Juli",
|
||||
"Agustus",
|
||||
"September",
|
||||
"Oktober",
|
||||
"November",
|
||||
"Desember",
|
||||
}
|
||||
|
||||
month := int(t.Month())
|
||||
monthLabel := strconv.Itoa(month)
|
||||
if month > 0 && month < len(monthNames) {
|
||||
monthLabel = monthNames[month]
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%02d %s %d", t.Day(), monthLabel, t.Year())
|
||||
}
|
||||
|
||||
func formatCategoryLabel(value string) string {
|
||||
normalized := strings.TrimSpace(strings.ReplaceAll(value, "_", " "))
|
||||
if normalized == "" {
|
||||
return "-"
|
||||
}
|
||||
|
||||
parts := strings.Fields(strings.ToLower(normalized))
|
||||
for i, part := range parts {
|
||||
if len(part) == 0 {
|
||||
continue
|
||||
}
|
||||
parts[i] = strings.ToUpper(part[:1]) + part[1:]
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func formatPercentID(value float64, decimals int) string {
|
||||
return fmt.Sprintf("%s%%", formatNumberID(value, decimals, false))
|
||||
}
|
||||
|
||||
func formatNumberID(value float64, decimals int, trim bool) string {
|
||||
if math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return "0"
|
||||
}
|
||||
if decimals < 0 {
|
||||
decimals = 0
|
||||
}
|
||||
|
||||
raw := strconv.FormatFloat(value, 'f', decimals, 64)
|
||||
if trim && strings.Contains(raw, ".") {
|
||||
raw = strings.TrimRight(raw, "0")
|
||||
raw = strings.TrimRight(raw, ".")
|
||||
}
|
||||
|
||||
parts := strings.SplitN(raw, ".", 2)
|
||||
intPart := parts[0]
|
||||
sign := ""
|
||||
if strings.HasPrefix(intPart, "-") {
|
||||
sign = "-"
|
||||
intPart = strings.TrimPrefix(intPart, "-")
|
||||
}
|
||||
if intPart == "" {
|
||||
intPart = "0"
|
||||
}
|
||||
|
||||
var grouped strings.Builder
|
||||
rem := len(intPart) % 3
|
||||
if rem > 0 {
|
||||
grouped.WriteString(intPart[:rem])
|
||||
if len(intPart) > rem {
|
||||
grouped.WriteString(".")
|
||||
}
|
||||
}
|
||||
for i := rem; i < len(intPart); i += 3 {
|
||||
grouped.WriteString(intPart[i : i+3])
|
||||
if i+3 < len(intPart) {
|
||||
grouped.WriteString(".")
|
||||
}
|
||||
}
|
||||
|
||||
result := sign + grouped.String()
|
||||
if len(parts) == 2 && parts[1] != "" {
|
||||
result += "," + parts[1]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func safeExportText(value string) string {
|
||||
normalized := strings.TrimSpace(value)
|
||||
if normalized == "" {
|
||||
return "-"
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func pointerString(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func formatApprovalStatus(item dto.RecordingListDTO) string {
|
||||
action := strings.ToUpper(strings.TrimSpace(pointerString(item.Approval.Action)))
|
||||
switch action {
|
||||
case "UPDATED":
|
||||
return "Diperbarui"
|
||||
case "CREATED":
|
||||
return safeExportText(item.Approval.StepName)
|
||||
default:
|
||||
return safeExportText(item.Approval.StepName)
|
||||
}
|
||||
}
|
||||
@@ -758,15 +758,39 @@ func (r *RecordingRepositoryImpl) GetCumulativeEggQtyByProjectFlockKandang(
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var result float64
|
||||
var cumulativeEggQty float64
|
||||
err := tx.
|
||||
Table("recording_eggs").
|
||||
Select("COALESCE(SUM(recording_eggs.qty), 0)").
|
||||
Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id").
|
||||
Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangId).
|
||||
Where("recordings.record_datetime <= ?", recordTime).
|
||||
Scan(&result).Error
|
||||
return result, err
|
||||
Scan(&cumulativeEggQty).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
productWarehouseSubQuery := tx.
|
||||
Table("recording_eggs").
|
||||
Select("DISTINCT recording_eggs.product_warehouse_id").
|
||||
Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id").
|
||||
Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangId).
|
||||
Where("recordings.record_datetime <= ?", recordTime)
|
||||
|
||||
var adjustmentEggQty float64
|
||||
err = tx.
|
||||
Table("adjustment_stocks").
|
||||
Select("COALESCE(SUM(adjustment_stocks.total_qty), 0)").
|
||||
Where("adjustment_stocks.product_warehouse_id IN (?)", productWarehouseSubQuery).
|
||||
Where("adjustment_stocks.function_code = ?", "RECORDING_EGG_IN").
|
||||
Where("adjustment_stocks.transaction_type = ?", "RECORDING").
|
||||
Where("adjustment_stocks.created_at <= ?", recordTime).
|
||||
Scan(&adjustmentEggQty).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return cumulativeEggQty + adjustmentEggQty, nil
|
||||
}
|
||||
func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) {
|
||||
// Body-weight tracking is removed; keep stub for report compatibility.
|
||||
|
||||
@@ -1989,9 +1989,9 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
|
||||
}
|
||||
|
||||
var eggMass float64
|
||||
if remainingChick > 0 && totalEggWeightGrams > 0 {
|
||||
// totalEggWeightGrams is in grams; egg mass is grams per hen.
|
||||
eggMass = totalEggWeightGrams / remainingChick
|
||||
if initialChickin > 0 && totalEggWeightGrams > 0 {
|
||||
// totalEggWeightGrams is in grams; egg mass uses initial chick population.
|
||||
eggMass = totalEggWeightGrams / initialChickin
|
||||
updates["egg_mass"] = eggMass
|
||||
recording.EggMass = &eggMass
|
||||
} else {
|
||||
|
||||
@@ -416,6 +416,9 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
||||
if latestWeek > 0 && computedWeek > latestWeek+1 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "week must be sequential without skipping")
|
||||
}
|
||||
// if latestWeek > 0 && req.Week > latestWeek+1 {
|
||||
// return nil, fiber.NewError(fiber.StatusBadRequest, "week must be sequential without skipping")
|
||||
// }
|
||||
|
||||
if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, computedWeek); err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user