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:
Adnan Zahir
2026-04-14 14:31:18 +07:00
25 changed files with 1528 additions and 52 deletions
@@ -36,6 +36,7 @@ type UniformityWeeklyMetric struct {
Week int
Uniformity float64
AverageWeight float64
UniformDate time.Time
}
type StandardWeeklyMetric struct {
@@ -265,6 +265,7 @@ func (s dashboardService) buildPerformanceCharts(ctx context.Context, params *va
}
bodyWeightDataset := make([]map[string]interface{}, 0, len(weeks))
bodyWeightDatasetIndexByWeek := make(map[int]int, len(weeks))
performanceDataset := make([]map[string]interface{}, 0, len(weeks))
fcrDataset := make([]map[string]interface{}, 0, len(weeks))
deplesiDataset := make([]map[string]interface{}, 0, len(weeks))
@@ -358,6 +359,15 @@ func (s dashboardService) buildPerformanceCharts(ctx context.Context, params *va
}
}
bodyWeightDataset = extendBodyWeightDatasetUntilEndDate(
bodyWeightDataset,
bodyWeightDatasetIndexByWeek,
uniformities,
uniformityMap,
standardMap,
params.PeriodEnd,
)
qualityRows, err := s.Repository.GetEggQualityWeeklyMetrics(ctx, startDate, endExclusive, filter)
if err != nil {
return nil, err
@@ -1081,6 +1091,69 @@ func (s dashboardService) avgSellingPrice(ctx context.Context, filter *validatio
return result.TotalPrice / result.TotalWeight, nil
}
func extendBodyWeightDatasetUntilEndDate(
dataset []map[string]interface{},
indexByWeek map[int]int,
uniformities []repository.UniformityWeeklyMetric,
uniformityMap map[int]repository.UniformityWeeklyMetric,
standardMap map[int]repository.StandardWeeklyMetric,
periodEnd time.Time,
) []map[string]interface{} {
latestUniformityWeek := 0
var latestUniformityDate time.Time
for _, row := range uniformities {
if row.Week <= 0 || row.UniformDate.IsZero() {
continue
}
if latestUniformityDate.IsZero() || row.UniformDate.After(latestUniformityDate) || (row.UniformDate.Equal(latestUniformityDate) && row.Week > latestUniformityWeek) {
latestUniformityDate = row.UniformDate
latestUniformityWeek = row.Week
}
}
if latestUniformityWeek <= 0 || latestUniformityDate.IsZero() || periodEnd.IsZero() || !periodEnd.After(latestUniformityDate) {
return dataset
}
additionalWeeks := int(math.Ceil(periodEnd.Sub(latestUniformityDate).Hours() / (24 * 7)))
if additionalWeeks <= 0 {
return dataset
}
lastUniformity := uniformityMap[latestUniformityWeek]
lastStandard := standardMap[latestUniformityWeek]
latestBodyWeight := roundTo(lastUniformity.AverageWeight, 2)
latestStdBodyWeight := roundTo(lastStandard.StdBodyWeight, 2)
targetWeek := latestUniformityWeek + additionalWeeks
for week := latestUniformityWeek + 1; week <= targetWeek; week++ {
row := map[string]interface{}{
"week": week,
"body_weight": latestBodyWeight,
"std_body_weight": latestStdBodyWeight,
}
if idx, ok := indexByWeek[week]; ok {
dataset[idx] = row
continue
}
dataset = append(dataset, row)
indexByWeek[week] = len(dataset) - 1
}
sort.Slice(dataset, func(i, j int) bool {
return datasetWeek(dataset[i]) < datasetWeek(dataset[j])
})
return dataset
}
func datasetWeek(row map[string]interface{}) int {
week, _ := row["week"].(int)
return week
}
func feedUsageToGrams(rows []repository.FeedUsageByUom) float64 {
total := 0.0
for _, row := range rows {
@@ -31,6 +31,8 @@ type MarketingDeliveryProductRepositoryImpl struct {
*commonRepo.BaseRepositoryImpl[entity.MarketingDeliveryProduct]
}
const marketingDeliveryProductSelectWithNullAttributed = "marketing_delivery_products.*, NULL AS attributed_project_flock_kandang_id"
func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository {
return &MarketingDeliveryProductRepositoryImpl{
BaseRepositoryImpl: commonRepo.NewBaseRepository[entity.MarketingDeliveryProduct](db),
@@ -43,9 +45,9 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlo
attributionQuery := commonRepo.MarketingDeliveryAttributionRowsQuery(r.DB().WithContext(ctx))
db := r.DB().WithContext(ctx).
Select("DISTINCT "+marketingDeliveryProductSelectWithNullAttributed).
Joins("JOIN (?) AS mda ON mda.marketing_delivery_product_id = marketing_delivery_products.id", attributionQuery).
Where("mda.project_flock_id = ?", projectFlockID).
Distinct("marketing_delivery_products.*")
Where("mda.project_flock_id = ?", projectFlockID)
if callback != nil {
db = callback(db)
@@ -110,6 +112,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Co
// JOIN untuk filter by marketing_id yang ada di related table
db := r.DB().WithContext(ctx).
Select(marketingDeliveryProductSelectWithNullAttributed).
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
Where("marketing_products.marketing_id = ?", marketingId)
@@ -124,6 +127,8 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx con
var deliveryProduct entity.MarketingDeliveryProduct
if err := r.DB().WithContext(ctx).
Model(&entity.MarketingDeliveryProduct{}).
Select(marketingDeliveryProductSelectWithNullAttributed).
Where("marketing_product_id = ?", marketingProductID).
First(&deliveryProduct).Error; err != nil {
return nil, err
@@ -132,6 +137,27 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx con
return &deliveryProduct, nil
}
func (r *MarketingDeliveryProductRepositoryImpl) GetByID(
ctx context.Context,
id uint,
modifier func(*gorm.DB) *gorm.DB,
) (*entity.MarketingDeliveryProduct, error) {
var deliveryProduct entity.MarketingDeliveryProduct
q := r.DB().WithContext(ctx).
Model(&entity.MarketingDeliveryProduct{}).
Select(marketingDeliveryProductSelectWithNullAttributed)
if modifier != nil {
q = modifier(q)
}
if err := q.First(&deliveryProduct, id).Error; err != nil {
return nil, err
}
return &deliveryProduct, nil
}
func (r *MarketingDeliveryProductRepositoryImpl) GetAttributionRowsByDeliveryProductIDs(ctx context.Context, deliveryProductIDs []uint) ([]commonRepo.MarketingDeliveryAttributionRow, error) {
if len(deliveryProductIDs) == 0 {
return []commonRepo.MarketingDeliveryAttributionRow{}, nil
@@ -211,6 +237,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) fetchClosingDeliveryProducts(
}
query := r.closingDeliveryProductsQuery(ctx).
Select(marketingDeliveryProductSelectWithNullAttributed).
Where("marketing_delivery_products.id IN ?", deliveryIDs).
Order("marketing_delivery_products.delivery_date DESC")
@@ -55,9 +55,9 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar
db = s.withRelations(db)
db, scopeErr = m.ApplyAreaScope(c, db, "id")
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
db = db.Where("name ILIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
return db.Order("name ASC").Order("id ASC")
})
if scopeErr != nil {
@@ -33,6 +33,14 @@ func (u *CustomerController) GetAll(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
if hasMarketingParam := c.Query("has_marketing", ""); hasMarketingParam != "" {
value, err := strconv.ParseBool(hasMarketingParam)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid has_marketing value")
}
query.HasMarketing = &value
}
result, totalResults, err := u.CustomerService.GetAll(c, query)
if err != nil {
return err
@@ -53,7 +53,28 @@ func (s customerService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
customers, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name ILIKE ?", "%"+params.Search+"%")
db = db.Where("name ILIKE ?", "%"+params.Search+"%")
if params.HasMarketing != nil && *params.HasMarketing {
db = db.Where(`
EXISTS (
SELECT 1
FROM marketings
WHERE marketings.customer_id = customers.id
AND marketings.deleted_at IS NULL
)
`)
}
return db
}
if params.HasMarketing != nil && *params.HasMarketing {
db = db.Where(`
EXISTS (
SELECT 1
FROM marketings
WHERE marketings.customer_id = customers.id
AND marketings.deleted_at IS NULL
)
`)
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
@@ -21,7 +21,8 @@ type Update struct {
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
HasMarketing *bool `query:"has_marketing" validate:"omitempty"`
}
@@ -71,7 +71,7 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
)
`, utils.ProjectFlockCategoryLaying)
}
return db.Order("created_at DESC").Order("updated_at DESC")
return db.Order("locations.name ASC").Order("locations.id ASC")
})
if scopeErr != nil {
@@ -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
@@ -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)
@@ -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 {
@@ -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",
}
}
}
@@ -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
@@ -169,6 +169,7 @@ type PurchaseReceivingUpdate struct {
TravelNumber *string
TravelDocumentPath *string
VehicleNumber *string
ClearVehicleNumber bool
ReceivedQty *float64
WarehouseID *uint
ProductWarehouseID *uint
@@ -246,6 +247,8 @@ func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
}
if upd.VehicleNumber != nil {
data["vehicle_number"] = upd.VehicleNumber
} else if upd.ClearVehicleNumber {
data["vehicle_number"] = gorm.Expr("NULL")
}
if upd.WarehouseID != nil && *upd.WarehouseID != 0 {
data["warehouse_id"] = upd.WarehouseID
@@ -183,15 +183,102 @@ func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[
return nil
}
func (b *expenseBridge) clearExpenseLinksForItems(ctx context.Context, itemIDs []uint) error {
if len(itemIDs) == 0 {
return nil
}
unique := make(map[uint]struct{}, len(itemIDs))
normalized := make([]uint, 0, len(itemIDs))
for _, id := range itemIDs {
if id == 0 {
continue
}
if _, exists := unique[id]; exists {
continue
}
unique[id] = struct{}{}
normalized = append(normalized, id)
}
if len(normalized) == 0 {
return nil
}
rows := make([]struct {
ItemID uint
ExpenseNonstockID *uint64
ExpenseID *uint64
}, 0, len(normalized))
if err := b.db.WithContext(ctx).
Table("purchase_items pi").
Select("pi.id as item_id, pi.expense_nonstock_id, en.expense_id").
Joins("LEFT JOIN expense_nonstocks en ON en.id = pi.expense_nonstock_id").
Where("pi.id IN ?", normalized).
Scan(&rows).Error; err != nil {
return err
}
expenseNonstockIDs := make([]uint64, 0, len(rows))
expenseIDs := make(map[uint64]struct{})
for _, row := range rows {
if row.ExpenseNonstockID != nil && *row.ExpenseNonstockID != 0 {
expenseNonstockIDs = append(expenseNonstockIDs, *row.ExpenseNonstockID)
}
if row.ExpenseID != nil && *row.ExpenseID != 0 {
expenseIDs[*row.ExpenseID] = struct{}{}
}
}
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&entity.PurchaseItem{}).
Where("id IN ?", normalized).
Update("expense_nonstock_id", gorm.Expr("NULL")).Error; err != nil {
return err
}
if len(expenseNonstockIDs) > 0 {
if err := tx.Where("id IN ?", expenseNonstockIDs).Delete(&entity.ExpenseNonstock{}).Error; err != nil {
return err
}
}
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
for expenseID := range expenseIDs {
var count int64
if err := tx.Model(&entity.ExpenseNonstock{}).
Where("expense_id = ?", expenseID).
Count(&count).Error; err != nil {
return err
}
if count > 0 {
continue
}
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
return err
}
if err := tx.Delete(&entity.Expense{}, expenseID).Error; err != nil {
return err
}
}
return nil
})
}
func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates []ExpenseReceivingPayload) error {
if purchaseID == 0 || len(updates) == 0 {
return nil
}
ctx := c.Context()
clearExpenseLinks := make([]uint, 0, len(updates))
filtered := make([]ExpenseReceivingPayload, 0, len(updates))
for _, upd := range updates {
if upd.PurchaseItemID == 0 {
continue
}
if upd.SupplierID == 0 {
clearExpenseLinks = append(clearExpenseLinks, upd.PurchaseItemID)
continue
}
if upd.TransportPerItem == nil || *upd.TransportPerItem <= 0 {
@@ -202,6 +289,11 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
}
filtered = append(filtered, upd)
}
if len(clearExpenseLinks) > 0 {
if err := b.clearExpenseLinksForItems(ctx, clearExpenseLinks); err != nil {
return err
}
}
if len(filtered) == 0 {
return nil
}
@@ -195,9 +195,14 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
db = db.Where("purchases.po_date >= ?", *poDateStart)
}
if poDateStart != nil {
db = db.Where("purchases.po_date >= ?", *poDateStart)
}
if poDateEnd != nil {
db = db.Where("purchases.po_date < ?", *poDateEnd)
}
if scope.Restrict {
if len(scope.IDs) == 0 {
return db.Where("1 = 0")
@@ -311,12 +316,119 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
WHERE pi.purchase_id = purchases.id
AND LOWER(COALESCE(p.name, '')) LIKE ?
)
OR EXISTS (
SELECT 1
FROM purchase_items pi
JOIN warehouses w ON w.id = pi.warehouse_id
JOIN locations l ON l.id = w.location_id
WHERE pi.purchase_id = purchases.id
AND LOWER(COALESCE(l.name, '')) LIKE ?
)
OR EXISTS (
SELECT 1
FROM purchase_items pi
JOIN expense_nonstocks en ON en.id = pi.expense_nonstock_id
JOIN expenses e ON e.id = en.expense_id
WHERE pi.purchase_id = purchases.id
AND LOWER(COALESCE(e.reference_number, '')) LIKE ?
)
)`,
like,
like,
like,
like,
like,
like,
like,
)
}
if len(approvalStatuses) > 0 {
approvalConditions := make([]string, 0, len(approvalStatuses))
approvalArgs := make([]any, 0, 2+(len(approvalStatuses)*3))
approvalArgs = append(approvalArgs, utils.ApprovalWorkflowPurchase.String(), utils.ApprovalWorkflowPurchase.String())
for _, status := range approvalStatuses {
if status == "" {
continue
}
like := "%" + status + "%"
approvalConditions = append(approvalConditions, `(LOWER(COALESCE(a.step_name, '')) LIKE ? OR LOWER(COALESCE(CAST(a.action AS TEXT), '')) LIKE ? OR CAST(a.step_number AS TEXT) = ?)`)
approvalArgs = append(approvalArgs, like, like, status)
}
if len(approvalConditions) > 0 {
approvalClause := strings.Join(approvalConditions, " OR ")
approvalQuery := fmt.Sprintf(
`EXISTS (
SELECT 1
FROM approvals a
WHERE a.approvable_type = ?
AND a.approvable_id = purchases.id
AND a.id = (
SELECT a2.id
FROM approvals a2
WHERE a2.approvable_type = ?
AND a2.approvable_id = purchases.id
ORDER BY a2.action_at DESC, a2.id DESC
LIMIT 1
)
AND (%s)
)`,
approvalClause,
)
db = db.Where(approvalQuery, approvalArgs...)
}
}
if search != "" {
like := "%" + search + "%"
db = db.Where(
`(
LOWER(COALESCE(purchases.pr_number, '')) LIKE ?
OR LOWER(COALESCE(purchases.po_number, '')) LIKE ?
OR EXISTS (
SELECT 1
FROM suppliers s
WHERE s.id = purchases.supplier_id
AND LOWER(COALESCE(s.name, '')) LIKE ?
)
OR EXISTS (
SELECT 1
FROM users u
WHERE u.id = purchases.created_by
AND LOWER(COALESCE(u.name, '')) LIKE ?
)
OR EXISTS (
SELECT 1
FROM purchase_items pi
JOIN products p ON p.id = pi.product_id
WHERE pi.purchase_id = purchases.id
AND LOWER(COALESCE(p.name, '')) LIKE ?
)
OR EXISTS (
SELECT 1
FROM purchase_items pi
JOIN warehouses w ON w.id = pi.warehouse_id
JOIN locations l ON l.id = w.location_id
WHERE pi.purchase_id = purchases.id
AND LOWER(COALESCE(l.name, '')) LIKE ?
)
OR EXISTS (
SELECT 1
FROM purchase_items pi
JOIN expense_nonstocks en ON en.id = pi.expense_nonstock_id
JOIN expenses e ON e.id = en.expense_id
WHERE pi.purchase_id = purchases.id
AND LOWER(COALESCE(e.reference_number, '')) LIKE ?
)
)`,
like,
like,
like,
like,
like,
like,
like,
)
}
@@ -949,6 +1061,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
supplierID uint
transportPerItem *float64
vehicleNumber *string
clearVehicle bool
overrideWarehouse bool
receivedQty float64
}
@@ -1041,12 +1154,16 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
}
var vehicleNumber *string
if payload.VehicleNumber != nil && strings.TrimSpace(*payload.VehicleNumber) != "" {
clearVehicle := false
if payload.VehicleNumber != nil {
val := strings.TrimSpace(*payload.VehicleNumber)
vehicleNumber = &val
} else if item.VehicleNumber != nil && strings.TrimSpace(*item.VehicleNumber) != "" {
val := strings.TrimSpace(*item.VehicleNumber)
vehicleNumber = &val
if val != "" {
vehicleNumber = &val
} else {
clearVehicle = true
}
} else {
clearVehicle = true
}
prepared = append(prepared, preparedReceiving{
@@ -1057,6 +1174,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
supplierID: supplierID,
transportPerItem: transportPerItem,
vehicleNumber: vehicleNumber,
clearVehicle: clearVehicle,
overrideWarehouse: overrideWarehouse,
receivedQty: receivedQty,
})
@@ -1170,7 +1288,8 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
ReceivedDate: &dateCopy,
TravelNumber: prep.payload.TravelNumber,
TravelDocumentPath: prep.payload.TravelDocumentPath,
VehicleNumber: prep.payload.VehicleNumber,
VehicleNumber: prep.vehicleNumber,
ClearVehicleNumber: prep.clearVehicle,
ReceivedQty: &qtyCopy,
ProductWarehouseID: newPWID,
ClearProductWarehouse: false,
@@ -432,6 +432,7 @@ func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error {
func parseCommaSeparatedInt64s(raw string) ([]int64, error) {
return parseCommaSeparatedInt64sWithField(raw, "supplier_ids")
}
func parseCommaSeparatedInt64sWithField(raw, field string) ([]int64, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
@@ -71,7 +71,7 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context,
if len(filters.ProductCategoryIDs) > 0 {
db = db.
Joins("JOIN products ON products.id = purchase_items.product_id").
Where("products.product_category_id IN ?", filters.ProductCategoryIDs)
Where("products.product_category_id IN ?", filters.ProductCategoryIDs)
}
if len(filters.AreaIDs) > 0 || filters.AllowedAreaIDs != nil {
@@ -194,7 +194,7 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context
if len(filters.ProductCategoryIDs) > 0 {
db = db.
Joins("JOIN products ON products.id = purchase_items.product_id").
Where("products.product_category_id IN ?", filters.ProductCategoryIDs)
Where("products.product_category_id IN ?", filters.ProductCategoryIDs)
}
if len(filters.AreaIDs) > 0 || filters.AllowedAreaIDs != nil {
db = db.Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id")