mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
feat(BE): recording
This commit is contained in:
@@ -71,7 +71,7 @@ func (u *RecordingController) GetOne(c *fiber.Ctx) error {
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get recording successfully",
|
||||
Data: dto.ToRecordingListDTO(*result),
|
||||
Data: dto.ToRecordingDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func (u *RecordingController) CreateOne(c *fiber.Ctx) error {
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Create recording successfully",
|
||||
Data: dto.ToRecordingListDTO(*result),
|
||||
Data: dto.ToRecordingDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func (u *RecordingController) UpdateOne(c *fiber.Ctx) error {
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Update recording successfully",
|
||||
Data: dto.ToRecordingListDTO(*result),
|
||||
Data: dto.ToRecordingDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ type RecordingBaseDTO struct {
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||
RecordDatetime time.Time `json:"record_datetime"`
|
||||
RecordDate *time.Time `json:"record_date,omitempty"`
|
||||
Status int `json:"status"`
|
||||
Ontime bool `json:"ontime"`
|
||||
Day *int `json:"day,omitempty"`
|
||||
TotalDepletion *int `json:"total_depletion,omitempty"`
|
||||
@@ -37,18 +36,51 @@ type RecordingListDTO struct {
|
||||
|
||||
type RecordingDetailDTO struct {
|
||||
RecordingListDTO
|
||||
BodyWeights []RecordingBodyWeightDTO `json:"body_weights"`
|
||||
Depletions []RecordingDepletionDTO `json:"depletions"`
|
||||
Stocks []RecordingStockDTO `json:"stocks"`
|
||||
}
|
||||
|
||||
type RecordingBodyWeightDTO struct {
|
||||
Weight float64 `json:"weight"`
|
||||
Qty int `json:"qty"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
type RecordingDepletionDTO struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
Total int64 `json:"total"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
type RecordingStockDTO struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
Increase *float64 `json:"increase,omitempty"`
|
||||
Decrease *float64 `json:"decrease,omitempty"`
|
||||
UsageAmount *int64 `json:"usage_amount,omitempty"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO {
|
||||
recordDate := e.RecordDate
|
||||
if recordDate == nil {
|
||||
rd := time.Date(
|
||||
e.RecordDatetime.Year(),
|
||||
e.RecordDatetime.Month(),
|
||||
e.RecordDatetime.Day(),
|
||||
0, 0, 0, 0,
|
||||
e.RecordDatetime.Location(),
|
||||
)
|
||||
recordDate = &rd
|
||||
}
|
||||
return RecordingBaseDTO{
|
||||
Id: e.Id,
|
||||
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
||||
RecordDatetime: e.RecordDatetime,
|
||||
RecordDate: e.RecordDate,
|
||||
Status: e.Status,
|
||||
Ontime: e.Ontime,
|
||||
RecordDate: recordDate,
|
||||
Ontime: e.Ontime == 1,
|
||||
Day: e.Day,
|
||||
TotalDepletion: e.TotalDepletion,
|
||||
CumDepletionRate: e.CumDepletionRate,
|
||||
@@ -88,5 +120,46 @@ func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
|
||||
func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
||||
return RecordingDetailDTO{
|
||||
RecordingListDTO: ToRecordingListDTO(e),
|
||||
BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights),
|
||||
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
||||
Stocks: ToRecordingStockDTOs(e.Stocks),
|
||||
}
|
||||
}
|
||||
|
||||
func ToRecordingBodyWeightDTOs(bodyWeights []entity.RecordingBW) []RecordingBodyWeightDTO {
|
||||
result := make([]RecordingBodyWeightDTO, len(bodyWeights))
|
||||
for i, bw := range bodyWeights {
|
||||
result[i] = RecordingBodyWeightDTO{
|
||||
Weight: bw.Weight,
|
||||
Qty: bw.Qty,
|
||||
Notes: bw.Notes,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []RecordingDepletionDTO {
|
||||
result := make([]RecordingDepletionDTO, len(depletions))
|
||||
for i, d := range depletions {
|
||||
result[i] = RecordingDepletionDTO{
|
||||
ProductWarehouseId: d.ProductWarehouseId,
|
||||
Total: d.Total,
|
||||
Notes: d.Notes,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
|
||||
result := make([]RecordingStockDTO, len(stocks))
|
||||
for i, s := range stocks {
|
||||
result[i] = RecordingStockDTO{
|
||||
ProductWarehouseId: s.ProductWarehouseId,
|
||||
Increase: s.Increase,
|
||||
Decrease: s.Decrease,
|
||||
UsageAmount: s.UsageAmount,
|
||||
Notes: s.Notes,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
||||
|
||||
@@ -16,11 +18,12 @@ type RecordingModule struct{}
|
||||
|
||||
func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
recordingRepo := rRecording.NewRecordingRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
recordingService := sRecording.NewRecordingService(recordingRepo, validate)
|
||||
recordingService := sRecording.NewRecordingService(recordingRepo, projectFlockKandangRepo, productWarehouseRepo, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
RecordingRoutes(router, userService, recordingService)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
@@ -28,16 +30,25 @@ type RecordingService interface {
|
||||
}
|
||||
|
||||
type recordingService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.RecordingRepository
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.RecordingRepository
|
||||
ProjectFlockKandangRepo rProjectFlockKandang.ProjectFlockKandangRepository
|
||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||
}
|
||||
|
||||
func NewRecordingService(repo repository.RecordingRepository, validate *validator.Validate) RecordingService {
|
||||
func NewRecordingService(
|
||||
repo repository.RecordingRepository,
|
||||
projectFlockKandangRepo rProjectFlockKandang.ProjectFlockKandangRepository,
|
||||
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
|
||||
validate *validator.Validate,
|
||||
) RecordingService {
|
||||
return &recordingService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +109,16 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recordTime, err := time.Parse(time.RFC3339, req.RecordDatetime)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "record_datetime must be in RFC3339 format")
|
||||
if _, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get project flock kandang: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx := s.Repository.DB().WithContext(c.Context()).Begin()
|
||||
@@ -122,20 +140,22 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
||||
return nil, err
|
||||
}
|
||||
|
||||
status := 0
|
||||
if req.Status != nil {
|
||||
status = *req.Status
|
||||
}
|
||||
ontime := false
|
||||
if req.Ontime != nil {
|
||||
ontime = *req.Ontime
|
||||
}
|
||||
currentTime := time.Now().UTC()
|
||||
recordTime := currentTime
|
||||
recordDate := time.Date(
|
||||
recordTime.Year(),
|
||||
recordTime.Month(),
|
||||
recordTime.Day(),
|
||||
0, 0, 0, 0,
|
||||
recordTime.Location(),
|
||||
)
|
||||
ontimeFlag := computeOntime(recordTime, currentTime)
|
||||
|
||||
recording := &entity.Recording{
|
||||
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||
RecordDatetime: recordTime,
|
||||
Status: status,
|
||||
Ontime: ontime,
|
||||
RecordDate: &recordDate,
|
||||
Ontime: boolToInt(ontimeFlag),
|
||||
Day: &nextDay,
|
||||
CreatedBy: 1, // TODO: replace with authenticated user
|
||||
}
|
||||
@@ -203,33 +223,13 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateBody := make(map[string]any)
|
||||
|
||||
if req.RecordDatetime != nil {
|
||||
parsed, err := time.Parse(time.RFC3339, *req.RecordDatetime)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "record_datetime must be in RFC3339 format")
|
||||
}
|
||||
updateBody["record_datetime"] = parsed
|
||||
recording.RecordDatetime = parsed
|
||||
}
|
||||
if req.Status != nil {
|
||||
updateBody["status"] = *req.Status
|
||||
recording.Status = *req.Status
|
||||
}
|
||||
if req.Ontime != nil {
|
||||
updateBody["ontime"] = *req.Ontime
|
||||
recording.Ontime = *req.Ontime
|
||||
}
|
||||
|
||||
if len(updateBody) > 0 {
|
||||
if err := tx.Model(&entity.Recording{}).Where("id = ?", id).Updates(updateBody).Error; err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to update recording: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
ontimeValue := boolToInt(computeOntime(recording.RecordDatetime, time.Now().UTC()))
|
||||
if err := tx.Model(&entity.Recording{}).Where("id = ?", id).Update("ontime", ontimeValue).Error; err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to refresh ontime flag: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
recording.Ontime = ontimeValue
|
||||
|
||||
if req.BodyWeights != nil {
|
||||
if err := s.replaceBodyWeights(tx, recording.Id, req.BodyWeights); err != nil {
|
||||
@@ -239,6 +239,10 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
}
|
||||
if req.Stocks != nil {
|
||||
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
if err := s.replaceStocks(tx, recording.Id, req.Stocks); err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to update stocks: %+v", err)
|
||||
@@ -246,6 +250,10 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
}
|
||||
if req.Depletions != nil {
|
||||
if err := s.ensureProductWarehousesExist(c, nil, req.Depletions); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
if err := s.replaceDepletions(tx, recording.Id, req.Depletions); err != nil {
|
||||
_ = tx.Rollback()
|
||||
s.Log.Errorf("Failed to update depletions: %+v", err)
|
||||
@@ -280,6 +288,38 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
|
||||
// === Persistence Helpers ===
|
||||
|
||||
func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []validation.Stock, depletions []validation.Depletion) error {
|
||||
idSet := make(map[uint]struct{})
|
||||
|
||||
for _, stock := range stocks {
|
||||
if stock.ProductWarehouseId != 0 {
|
||||
idSet[stock.ProductWarehouseId] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, dep := range depletions {
|
||||
if dep.ProductWarehouseId != 0 {
|
||||
idSet[dep.ProductWarehouseId] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if len(idSet) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for id := range idSet {
|
||||
ok, err := s.ProductWarehouseRepo.ExistsByID(c.Context(), id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to validate product warehouse %d: %+v", id, err)
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d not found", id))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *recordingService) generateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
||||
var days []int
|
||||
if err := tx.Model(&entity.Recording{}).
|
||||
@@ -319,6 +359,17 @@ func nextRecordingDay(days []int) int {
|
||||
return len(normalized) + 1
|
||||
}
|
||||
|
||||
func computeOntime(recordDatetime, reference time.Time) bool {
|
||||
return !recordDatetime.Before(reference)
|
||||
}
|
||||
|
||||
func boolToInt(v bool) int {
|
||||
if v {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *recordingService) persistBodyWeights(tx *gorm.DB, recordingID uint, payload []validation.BodyWeight) error {
|
||||
if len(payload) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -24,21 +24,15 @@ type (
|
||||
|
||||
type Create struct {
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||
RecordDatetime string `json:"record_datetime" validate:"required,datetime=2006-01-02T15:04:05Z07:00"`
|
||||
Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1 2 3"`
|
||||
Ontime *bool `json:"ontime,omitempty" validate:"omitempty"`
|
||||
BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"`
|
||||
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
||||
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
RecordDatetime *string `json:"record_datetime,omitempty" validate:"omitempty,datetime=2006-01-02T15:04:05Z07:00"`
|
||||
Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1 2 3"`
|
||||
Ontime *bool `json:"ontime,omitempty" validate:"omitempty"`
|
||||
BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"`
|
||||
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
||||
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||
BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"`
|
||||
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
||||
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
|
||||
Reference in New Issue
Block a user