add api upload documents daily checklist

This commit is contained in:
MacBook Air M1
2026-01-11 22:02:21 +07:00
parent 4ee5bf3628
commit ae41422776
9 changed files with 209 additions and 35 deletions
+7 -6
View File
@@ -7,12 +7,13 @@ import (
)
type Phases struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
IsActive bool `gorm:"not null;default:true"`
Category string `gorm:"type:category_code;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
IsActive bool `gorm:"not null;default:true"`
Category string `gorm:"type:category_code;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ActivityCount int `gorm:"-" json:"-"`
Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"`
}
@@ -74,6 +74,7 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error {
Name: name,
Status: status,
Category: item.Category,
RejectReason: item.RejectReason,
Date: item.Date,
Kandang: kandang,
CreatedUser: nil,
@@ -303,12 +304,22 @@ func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error {
return err
}
documentDTOs := make([]dto.DailyChecklistDocumentDTO, len(detail.DocumentURLs))
for i, doc := range detail.DocumentURLs {
documentDTOs[i] = dto.DailyChecklistDocumentDTO{
Id: doc.ID,
Name: doc.Name,
Size: doc.Size,
URL: doc.URL,
}
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get dailyChecklist successfully",
Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress),
Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress, documentDTOs),
})
}
@@ -342,6 +353,12 @@ func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
@@ -31,6 +31,7 @@ type DailyChecklistListDTO struct {
TotalPhase int `json:"total_phase"`
TotalActivity int `json:"total_activity"`
Progress int `json:"progress"`
RejectReason *string `json:"reject_reason"`
}
type DailyChecklistDetailDTO struct {
@@ -40,6 +41,14 @@ type DailyChecklistDetailDTO struct {
AssignedEmployees []employeeDTO.EmployeesRelationDTO `json:"assigned_employees"`
TotalActivity int `json:"total_activity"`
Progress float64 `json:"progress"`
DocumentURLs []DailyChecklistDocumentDTO `json:"document_urls"`
}
type DailyChecklistDocumentDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Size float64 `json:"size"`
URL string `json:"url"`
}
type DailyChecklistSummaryDTO struct {
@@ -165,10 +174,11 @@ func ToDailyChecklistListDTO(e entity.DailyChecklist) DailyChecklistListDTO {
TotalPhase: 0,
TotalActivity: 0,
Progress: 0,
RejectReason: e.RejectReason,
}
}
func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64) DailyChecklistDetailDTO {
func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64, documentURLs []DailyChecklistDocumentDTO) DailyChecklistDetailDTO {
phaseDTOs := make([]DailyChecklistPhaseDTO, 0, len(phases))
for _, phase := range phases {
phaseDTOs = append(phaseDTOs, DailyChecklistPhaseDTO{
@@ -228,5 +238,6 @@ func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.
AssignedEmployees: assignedDTOs,
TotalActivity: totalActivities,
Progress: progress,
DocumentURLs: documentURLs,
}
}
+11 -1
View File
@@ -1,10 +1,15 @@
package dailyChecklists
import (
"context"
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
sDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
@@ -19,8 +24,13 @@ func (DailyChecklistModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
dailyChecklistRepo := rDailyChecklist.NewDailyChecklistRepository(db)
phasesRepo := rPhases.NewPhasesRepository(db)
userRepo := rUser.NewUserRepository(db)
documentRepo := commonRepo.NewDocumentRepository(db)
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
if err != nil {
panic(fmt.Sprintf("failed to create document service: %v", err))
}
dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate)
dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate, documentSvc)
userService := sUser.NewUserService(userRepo, validate)
DailyChecklistRoutes(router, userService, dailyChecklistService)
@@ -9,6 +9,7 @@ import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations"
phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
@@ -17,6 +18,7 @@ import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
@@ -39,10 +41,18 @@ type DailyChecklistService interface {
}
type dailyChecklistService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.DailyChecklistRepository
PhaseRepo phaseRepo.PhasesRepository
Log *logrus.Logger
Validate *validator.Validate
Repository repository.DailyChecklistRepository
PhaseRepo phaseRepo.PhasesRepository
DocumentSvc commonSvc.DocumentService
}
type DailyChecklistDocument struct {
ID uint
Name string
Size float64
URL string
}
type DailyChecklistDetail struct {
@@ -52,6 +62,7 @@ type DailyChecklistDetail struct {
AssignedEmployees []entity.Employee
TotalActivities int
Progress float64
DocumentURLs []DailyChecklistDocument
}
type DailyChecklistListItem struct {
@@ -60,6 +71,7 @@ type DailyChecklistListItem struct {
Date time.Time
Category string
Status *string
RejectReason *string
CreatedAt time.Time
UpdatedAt time.Time
Kandang entity.Kandang
@@ -108,12 +120,13 @@ type DailyChecklistReportCategory struct {
Baik int
}
func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService {
func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate, documentSvc commonSvc.DocumentService) DailyChecklistService {
return &dailyChecklistService{
Log: utils.Log,
Validate: validate,
Repository: repo,
PhaseRepo: phaseRepo,
Log: utils.Log,
Validate: validate,
Repository: repo,
PhaseRepo: phaseRepo,
DocumentSvc: documentSvc,
}
}
@@ -158,7 +171,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
if params.Search != "" {
like := "%" + params.Search + "%"
db = db.Where("(k.name ILIKE ? OR dc.category ILIKE ?)", like, like)
db = db.Where("(k.name ILIKE ? OR dc.category::text ILIKE ?)", like, like)
}
countDB := db.Session(&gorm.Session{})
@@ -174,6 +187,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
Date time.Time
Category string
Status *string
RejectReason *string
CreatedAt time.Time
UpdatedAt time.Time
KandangID uint
@@ -192,6 +206,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
dc.date,
dc.category,
dc.status,
dc.reject_reason,
dc.created_at,
dc.updated_at,
dc.kandang_id,
@@ -265,6 +280,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([
Date: row.Date,
Category: row.Category,
Status: row.Status,
RejectReason: row.RejectReason,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
Kandang: kandangMap[row.KandangID],
@@ -345,6 +361,29 @@ func (s dailyChecklistService) GetDetail(c *fiber.Ctx, id uint) (*DailyChecklist
progress = math.Round((float64(completedAssignments) / float64(totalAssignments)) * 100)
}
documentURLs := make([]DailyChecklistDocument, 0)
if s.DocumentSvc != nil {
documents, err := s.DocumentSvc.ListByTarget(c.Context(), string(utils.DocumentTypeDailyChecklist), uint64(id))
if err != nil {
s.Log.Errorf("Failed to list documents for daily checklist %d: %+v", id, err)
return nil, err
}
for _, doc := range documents {
url, err := s.DocumentSvc.PresignURL(c.Context(), doc, 0)
if err != nil {
s.Log.Errorf("Failed to presign document %d for daily checklist %d: %+v", doc.Id, id, err)
continue
}
documentURLs = append(documentURLs, DailyChecklistDocument{
ID: doc.Id,
Name: doc.Name,
Size: doc.Size,
URL: url,
})
}
}
return &DailyChecklistDetail{
Checklist: *checklist,
Phases: phases,
@@ -352,6 +391,7 @@ func (s dailyChecklistService) GetDetail(c *fiber.Ctx, id uint) (*DailyChecklist
AssignedEmployees: assignedEmployees,
TotalActivities: totalActivities,
Progress: progress,
DocumentURLs: documentURLs,
}, nil
}
@@ -377,7 +417,7 @@ func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create)
err = s.Repository.DB().WithContext(c.Context()).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "date"}, {Name: "kandang_id"}, {Name: "category"}},
DoUpdates: clause.Assignments(map[string]any{"status": status, "updated_at": time.Now()}),
DoUpdates: clause.Assignments(map[string]any{"updated_at": time.Now()}),
}).Create(createBody).Error
if err != nil {
s.Log.Errorf("Failed to upsert dailyChecklist: %+v", err)
@@ -392,6 +432,22 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
return nil, err
}
deletedIDs := make([]uint, 0)
if req.DeletedDocumentIDs != nil {
parts := strings.Split(*req.DeletedDocumentIDs, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
parsedID, err := strconv.ParseUint(part, 10, 64)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid deleted_document_ids")
}
deletedIDs = append(deletedIDs, uint(parsedID))
}
}
updateBody := map[string]any{
"status": req.Status,
}
@@ -400,6 +456,40 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
updateBody["reject_reason"] = *req.RejectReason
}
actorID, err := middleware.ActorIDFromContext(c)
if err != nil {
return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
}
if len(deletedIDs) > 0 && s.DocumentSvc != nil {
if err := s.DocumentSvc.DeleteDocuments(c.Context(), deletedIDs, true); err != nil {
s.Log.Errorf("Failed to delete daily checklist documents: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete daily checklist documents")
}
}
if len(req.Documents) > 0 {
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
for idx, file := range req.Documents {
documentFiles = append(documentFiles, commonSvc.DocumentFile{
File: file,
Type: string(utils.DocumentTypeDailyChecklist),
Index: &idx,
})
}
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentTypeDailyChecklist),
DocumentableID: uint64(id),
CreatedBy: &actorID,
Files: documentFiles,
})
if err != nil {
s.Log.Errorf("Failed to upload daily checklist documents: %+v", err)
return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload daily checklist documents")
}
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found")
@@ -869,7 +959,8 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report
Joins("JOIN areas a ON a.id = loc.area_id").
Joins("JOIN phases p ON p.id = dcat.phase_id").
Where("EXTRACT(MONTH FROM dc.date) = ?", params.Month).
Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year)
Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year).
Where("dc.status = ?", "APPROVED")
if params.AreaID != nil {
db = db.Where("a.id = ?", *params.AreaID)
@@ -1,5 +1,9 @@
package validation
import (
"mime/multipart"
)
type Create struct {
Date string `json:"date" validate:"required"`
KandangId uint `json:"kandang_id" validate:"required"`
@@ -8,8 +12,10 @@ type Create struct {
}
type Update struct {
Status string `json:"status" validate:"required"`
RejectReason *string `json:"reject_reason"`
Status string `form:"status" json:"status" validate:"required"`
RejectReason *string `form:"reject_reason" json:"reject_reason"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
DeletedDocumentIDs *string `form:"deleted_document_ids" json:"deleted_document_ids"`
}
type Query struct {
@@ -15,12 +15,13 @@ type PhasesRelationDTO struct {
}
type PhasesListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
IsActive bool `json:"is_active"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
Id uint `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
IsActive bool `json:"is_active"`
ActivityCount int `json:"activity_count"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
}
type PhasesDetailDTO struct {
@@ -44,12 +45,13 @@ func ToPhasesListDTO(e entity.Phases) PhasesListDTO {
// }
return PhasesListDTO{
Id: e.Id,
Name: e.Name,
Category: e.Category,
IsActive: e.IsActive,
CreatedAt: e.CreatedAt,
CreatedUser: createdUser,
Id: e.Id,
Name: e.Name,
Category: e.Category,
IsActive: e.IsActive,
ActivityCount: e.ActivityCount,
CreatedAt: e.CreatedAt,
CreatedUser: createdUser,
}
}
@@ -63,6 +63,40 @@ func (s phasesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.
s.Log.Errorf("Failed to get phasess: %+v", err)
return nil, 0, err
}
if len(phasess) > 0 {
ids := make([]uint, 0, len(phasess))
for _, phase := range phasess {
ids = append(ids, phase.Id)
}
type activityCountRow struct {
PhaseID uint
Count int64
}
var rows []activityCountRow
if err := s.Repository.DB().WithContext(c.Context()).
Table("phase_activities").
Select("phase_id, COUNT(*) AS count").
Where("phase_id IN ? AND deleted_at IS NULL", ids).
Group("phase_id").
Scan(&rows).Error; err != nil {
s.Log.Errorf("Failed to count phase activities: %+v", err)
return nil, 0, err
}
countMap := make(map[uint]int64, len(rows))
for _, row := range rows {
countMap[row.PhaseID] = row.Count
}
for i := range phasess {
if count, ok := countMap[phasess[i].Id]; ok {
phasess[i].ActivityCount = int(count)
}
}
}
return phasess, total, nil
}
+2
View File
@@ -432,6 +432,8 @@ const (
DocumentableTypeExpense DocumentableType = "EXPENSE"
DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION"
DocumentableTypePurchaseItem DocumentableType = "PURCHASE_ITEM"
DocumentTypeDailyChecklist DocumentType = "DAILY_CHECKLIST_DOCUMENT"
)
// -------------------------------------------------------------------