mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 23:05:44 +00:00
add api daily checklist report
This commit is contained in:
@@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
type DailyChecklistService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.DailyChecklist, int64, error)
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]DailyChecklistListItem, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.DailyChecklist, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.DailyChecklist, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.DailyChecklist, error)
|
||||
@@ -35,6 +35,7 @@ type DailyChecklistService interface {
|
||||
GetChecklistPhaseIDs(ctx *fiber.Ctx, checklistID uint) ([]uint, error)
|
||||
GetDetail(ctx *fiber.Ctx, id uint) (*DailyChecklistDetail, error)
|
||||
GetSummary(ctx *fiber.Ctx, params *validation.SummaryQuery) ([]DailyChecklistSummary, error)
|
||||
GetReport(ctx *fiber.Ctx, params *validation.ReportQuery) ([]DailyChecklistReportItem, int64, error)
|
||||
}
|
||||
|
||||
type dailyChecklistService struct {
|
||||
@@ -53,6 +54,20 @@ type DailyChecklistDetail struct {
|
||||
Progress float64
|
||||
}
|
||||
|
||||
type DailyChecklistListItem struct {
|
||||
ID uint
|
||||
Name *string
|
||||
Date time.Time
|
||||
Category string
|
||||
Status *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Kandang entity.Kandang
|
||||
TotalPhase int
|
||||
TotalActivity int
|
||||
Progress int
|
||||
}
|
||||
|
||||
type DailyChecklistSummary struct {
|
||||
EmployeeID uint
|
||||
EmployeeName string
|
||||
@@ -65,6 +80,34 @@ type DailyChecklistSummary struct {
|
||||
LastActivity *time.Time
|
||||
}
|
||||
|
||||
type DailyChecklistReportItem struct {
|
||||
AreaID uint
|
||||
AreaName string
|
||||
LocationID uint
|
||||
LocationName string
|
||||
KandangID uint
|
||||
KandangName string
|
||||
EmployeeID uint
|
||||
EmployeeName string
|
||||
PhaseName string
|
||||
DailyActivities map[string]int
|
||||
Summary DailyChecklistReportSummary
|
||||
}
|
||||
|
||||
type DailyChecklistReportSummary struct {
|
||||
TotalChecklist int
|
||||
JumlahHariEfektif int
|
||||
AbkPercentage int
|
||||
KandangPercentage int
|
||||
Category DailyChecklistReportCategory
|
||||
}
|
||||
|
||||
type DailyChecklistReportCategory struct {
|
||||
Kurang int
|
||||
Cukup int
|
||||
Baik int
|
||||
}
|
||||
|
||||
func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService {
|
||||
return &dailyChecklistService{
|
||||
Log: utils.Log,
|
||||
@@ -78,26 +121,160 @@ func (s dailyChecklistService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Kandang")
|
||||
}
|
||||
|
||||
func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.DailyChecklist, int64, error) {
|
||||
func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]DailyChecklistListItem, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
dailyChecklists, 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 LIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
db := s.Repository.DB().WithContext(c.Context()).
|
||||
Table("daily_checklists dc").
|
||||
Joins("JOIN kandangs k ON k.id = dc.kandang_id")
|
||||
|
||||
if err != nil {
|
||||
if params.DateFrom != "" {
|
||||
dateFrom, err := time.Parse("2006-01-02", params.DateFrom)
|
||||
if err != nil {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "invalid date_from format, use YYYY-MM-DD")
|
||||
}
|
||||
db = db.Where("dc.date >= ?", dateFrom)
|
||||
}
|
||||
|
||||
if params.DateTo != "" {
|
||||
dateTo, err := time.Parse("2006-01-02", params.DateTo)
|
||||
if err != nil {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "invalid date_to format, use YYYY-MM-DD")
|
||||
}
|
||||
db = db.Where("dc.date <= ?", dateTo)
|
||||
}
|
||||
|
||||
if params.KandangID != nil {
|
||||
db = db.Where("dc.kandang_id = ?", *params.KandangID)
|
||||
}
|
||||
|
||||
if params.Status != "" {
|
||||
db = db.Where("dc.status = ?", params.Status)
|
||||
}
|
||||
|
||||
if params.Search != "" {
|
||||
like := "%" + params.Search + "%"
|
||||
db = db.Where("(k.name ILIKE ? OR dc.category ILIKE ?)", like, like)
|
||||
}
|
||||
|
||||
countDB := db.Session(&gorm.Session{})
|
||||
var total int64
|
||||
if err := countDB.Count(&total).Error; err != nil {
|
||||
s.Log.Errorf("Failed to count dailyChecklists: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
type dailyChecklistListRow struct {
|
||||
ID uint
|
||||
Name *string
|
||||
Date time.Time
|
||||
Category string
|
||||
Status *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
KandangID uint
|
||||
TotalPhase int64
|
||||
TotalActivity int64
|
||||
TotalAssignments int64
|
||||
CompletedAssignments int64
|
||||
}
|
||||
|
||||
rows := make([]dailyChecklistListRow, 0)
|
||||
selectDB := db.Session(&gorm.Session{})
|
||||
if err := selectDB.
|
||||
Select(`
|
||||
dc.id,
|
||||
dc.name,
|
||||
dc.date,
|
||||
dc.category,
|
||||
dc.status,
|
||||
dc.created_at,
|
||||
dc.updated_at,
|
||||
dc.kandang_id,
|
||||
COALESCE((
|
||||
SELECT COUNT(*)
|
||||
FROM daily_checklist_phases dcp
|
||||
WHERE dcp.checklist_id = dc.id
|
||||
), 0) AS total_phase,
|
||||
COALESCE((
|
||||
SELECT COUNT(pa.id)
|
||||
FROM daily_checklist_phases dcp
|
||||
JOIN phase_activities pa ON pa.phase_id = dcp.phase_id
|
||||
WHERE dcp.checklist_id = dc.id AND pa.deleted_at IS NULL
|
||||
), 0) AS total_activity,
|
||||
COALESCE((
|
||||
SELECT COUNT(*)
|
||||
FROM daily_checklist_activity_task_assignments dca
|
||||
JOIN daily_checklist_activity_tasks dcat ON dcat.id = dca.task_id
|
||||
WHERE dcat.checklist_id = dc.id
|
||||
), 0) AS total_assignments,
|
||||
COALESCE((
|
||||
SELECT COUNT(*)
|
||||
FROM daily_checklist_activity_task_assignments dca
|
||||
JOIN daily_checklist_activity_tasks dcat ON dcat.id = dca.task_id
|
||||
WHERE dcat.checklist_id = dc.id AND dca.checked
|
||||
), 0) AS completed_assignments`).
|
||||
Order("dc.date DESC, dc.created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(params.Limit).
|
||||
Scan(&rows).Error; err != nil {
|
||||
s.Log.Errorf("Failed to get dailyChecklists: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
return dailyChecklists, total, nil
|
||||
|
||||
kandangIDs := make([]uint, 0, len(rows))
|
||||
seen := make(map[uint]struct{})
|
||||
for _, row := range rows {
|
||||
if _, ok := seen[row.KandangID]; !ok {
|
||||
seen[row.KandangID] = struct{}{}
|
||||
kandangIDs = append(kandangIDs, row.KandangID)
|
||||
}
|
||||
}
|
||||
|
||||
kandangMap := make(map[uint]entity.Kandang)
|
||||
if len(kandangIDs) > 0 {
|
||||
var kandangs []entity.Kandang
|
||||
if err := s.Repository.DB().WithContext(c.Context()).
|
||||
Where("id IN ?", kandangIDs).
|
||||
Preload("Location").
|
||||
Preload("Pic").
|
||||
Preload("CreatedUser").
|
||||
Find(&kandangs).Error; err != nil {
|
||||
s.Log.Errorf("Failed to get kandangs for daily checklist list: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
for _, kandang := range kandangs {
|
||||
kandangMap[kandang.Id] = kandang
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]DailyChecklistListItem, len(rows))
|
||||
for i, row := range rows {
|
||||
progress := 0
|
||||
if row.TotalAssignments > 0 {
|
||||
progress = int(math.Round(float64(row.CompletedAssignments) / float64(row.TotalAssignments) * 100))
|
||||
}
|
||||
|
||||
items[i] = DailyChecklistListItem{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
Date: row.Date,
|
||||
Category: row.Category,
|
||||
Status: row.Status,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
Kandang: kandangMap[row.KandangID],
|
||||
TotalPhase: int(row.TotalPhase),
|
||||
TotalActivity: int(row.TotalActivity),
|
||||
Progress: progress,
|
||||
}
|
||||
}
|
||||
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s dailyChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.DailyChecklist, error) {
|
||||
@@ -633,7 +810,7 @@ func (s dailyChecklistService) GetSummary(c *fiber.Ctx, params *validation.Summa
|
||||
Joins("JOIN daily_checklists d ON d.id = t.checklist_id").
|
||||
Joins("JOIN kandangs k ON k.id = d.kandang_id").
|
||||
Joins("JOIN employees e ON e.id = a.employee_id").
|
||||
Where("d.date BETWEEN ? AND ?", dateFrom, dateTo)
|
||||
Where("d.date BETWEEN ? AND ? AND d.status = ?", dateFrom, dateTo, "APPROVED")
|
||||
|
||||
if params.Category != "" {
|
||||
db = db.Where("d.category = ?", params.Category)
|
||||
@@ -673,3 +850,274 @@ func (s dailyChecklistService) GetSummary(c *fiber.Ctx, params *validation.Summa
|
||||
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.ReportQuery) ([]DailyChecklistReportItem, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
buildBase := func() *gorm.DB {
|
||||
db := s.Repository.DB().WithContext(c.Context()).
|
||||
Table("daily_checklist_activity_task_assignments AS dca").
|
||||
Joins("JOIN daily_checklist_activity_tasks dcat ON dcat.id = dca.task_id").
|
||||
Joins("JOIN daily_checklists dc ON dc.id = dcat.checklist_id").
|
||||
Joins("JOIN employees e ON e.id = dca.employee_id").
|
||||
Joins("JOIN kandangs k ON k.id = dc.kandang_id").
|
||||
Joins("JOIN locations loc ON loc.id = k.location_id").
|
||||
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)
|
||||
|
||||
if params.AreaID != nil {
|
||||
db = db.Where("a.id = ?", *params.AreaID)
|
||||
}
|
||||
if params.LocationID != nil {
|
||||
db = db.Where("loc.id = ?", *params.LocationID)
|
||||
}
|
||||
if params.KandangID != nil {
|
||||
db = db.Where("k.id = ?", *params.KandangID)
|
||||
}
|
||||
if params.EmployeeID != nil {
|
||||
db = db.Where("dca.employee_id = ?", *params.EmployeeID)
|
||||
}
|
||||
if params.PhaseID != nil {
|
||||
db = db.Where("p.id = ?", *params.PhaseID)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
buildGroupedQuery := func() *gorm.DB {
|
||||
return buildBase().
|
||||
Select(`
|
||||
a.id AS area_id,
|
||||
a.name AS area_name,
|
||||
loc.id AS location_id,
|
||||
loc.name AS location_name,
|
||||
k.id AS kandang_id,
|
||||
k.name AS kandang_name,
|
||||
e.id AS employee_id,
|
||||
e.name AS employee_name,
|
||||
p.id AS phase_id,
|
||||
p.name AS phase_name,
|
||||
SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed_assignments,
|
||||
COUNT(*) AS total_assignments`).
|
||||
Group("a.id, a.name, loc.id, loc.name, k.id, k.name, e.id, e.name, p.id, p.name")
|
||||
}
|
||||
|
||||
var total int64
|
||||
groupedForCount := buildGroupedQuery()
|
||||
if err := s.Repository.DB().WithContext(c.Context()).
|
||||
Table("(?) AS grouped", groupedForCount).
|
||||
Count(&total).Error; err != nil {
|
||||
s.Log.Errorf("Failed to count report data: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
type reportRow struct {
|
||||
AreaID uint
|
||||
AreaName string
|
||||
LocationID uint
|
||||
LocationName string
|
||||
KandangID uint
|
||||
KandangName string
|
||||
EmployeeID uint
|
||||
EmployeeName string
|
||||
PhaseID uint
|
||||
PhaseName string
|
||||
CompletedAssignments int64
|
||||
TotalAssignments int64
|
||||
}
|
||||
|
||||
rows := make([]reportRow, 0)
|
||||
if err := buildGroupedQuery().
|
||||
Order("a.name, loc.name, k.name, e.name").
|
||||
Offset(offset).
|
||||
Limit(params.Limit).
|
||||
Scan(&rows).Error; err != nil {
|
||||
s.Log.Errorf("Failed to fetch report data: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return []DailyChecklistReportItem{}, total, nil
|
||||
}
|
||||
|
||||
type comboKey struct {
|
||||
EmployeeID uint
|
||||
KandangID uint
|
||||
PhaseID uint
|
||||
}
|
||||
|
||||
employeeIDs := make([]uint, 0)
|
||||
kandangIDs := make([]uint, 0)
|
||||
phaseIDs := make([]uint, 0)
|
||||
comboSet := make(map[comboKey]struct{})
|
||||
employeeSet := make(map[uint]struct{})
|
||||
kandangSet := make(map[uint]struct{})
|
||||
phaseSet := make(map[uint]struct{})
|
||||
|
||||
for _, row := range rows {
|
||||
key := comboKey{EmployeeID: row.EmployeeID, KandangID: row.KandangID, PhaseID: row.PhaseID}
|
||||
comboSet[key] = struct{}{}
|
||||
if _, ok := employeeSet[row.EmployeeID]; !ok {
|
||||
employeeSet[row.EmployeeID] = struct{}{}
|
||||
employeeIDs = append(employeeIDs, row.EmployeeID)
|
||||
}
|
||||
if _, ok := kandangSet[row.KandangID]; !ok {
|
||||
kandangSet[row.KandangID] = struct{}{}
|
||||
kandangIDs = append(kandangIDs, row.KandangID)
|
||||
}
|
||||
if _, ok := phaseSet[row.PhaseID]; !ok {
|
||||
phaseSet[row.PhaseID] = struct{}{}
|
||||
phaseIDs = append(phaseIDs, row.PhaseID)
|
||||
}
|
||||
}
|
||||
|
||||
dailyActivityMap := make(map[comboKey]map[string]int)
|
||||
if len(employeeIDs) > 0 {
|
||||
var dailyRows []struct {
|
||||
EmployeeID uint
|
||||
KandangID uint
|
||||
PhaseID uint
|
||||
Date time.Time
|
||||
Completed int64
|
||||
}
|
||||
|
||||
dailyQuery := buildBase().
|
||||
Where("dca.employee_id IN ?", employeeIDs).
|
||||
Where("dc.kandang_id IN ?", kandangIDs).
|
||||
Where("dcat.phase_id IN ?", phaseIDs).
|
||||
Select(`
|
||||
dca.employee_id,
|
||||
dc.kandang_id,
|
||||
dcat.phase_id,
|
||||
dc.date,
|
||||
SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed`).
|
||||
Group("dca.employee_id, dc.kandang_id, dcat.phase_id, dc.date")
|
||||
|
||||
if err := dailyQuery.Scan(&dailyRows).Error; err != nil {
|
||||
s.Log.Errorf("Failed to fetch daily activities for report: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, row := range dailyRows {
|
||||
key := comboKey{EmployeeID: row.EmployeeID, KandangID: row.KandangID, PhaseID: row.PhaseID}
|
||||
if _, ok := comboSet[key]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := dailyActivityMap[key]; !ok {
|
||||
dailyActivityMap[key] = make(map[string]int)
|
||||
}
|
||||
day := strconv.Itoa(row.Date.Day())
|
||||
dailyActivityMap[key][day] = int(row.Completed)
|
||||
}
|
||||
}
|
||||
|
||||
employeeStats := make(map[uint]struct {
|
||||
Completed int64
|
||||
Total int64
|
||||
})
|
||||
var employeeRows []struct {
|
||||
EmployeeID uint
|
||||
Completed int64
|
||||
Total int64
|
||||
}
|
||||
if err := buildBase().
|
||||
Select(`
|
||||
dca.employee_id,
|
||||
SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed,
|
||||
COUNT(*) AS total`).
|
||||
Group("dca.employee_id").
|
||||
Scan(&employeeRows).Error; err != nil {
|
||||
s.Log.Errorf("Failed to fetch employee stats for report: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
for _, row := range employeeRows {
|
||||
employeeStats[row.EmployeeID] = struct {
|
||||
Completed int64
|
||||
Total int64
|
||||
}{Completed: row.Completed, Total: row.Total}
|
||||
}
|
||||
|
||||
kandangStats := make(map[uint]struct {
|
||||
Completed int64
|
||||
Total int64
|
||||
})
|
||||
var kandangRows []struct {
|
||||
KandangID uint
|
||||
Completed int64
|
||||
Total int64
|
||||
}
|
||||
if err := buildBase().
|
||||
Select(`
|
||||
dc.kandang_id,
|
||||
SUM(CASE WHEN dca.checked THEN 1 ELSE 0 END) AS completed,
|
||||
COUNT(*) AS total`).
|
||||
Group("dc.kandang_id").
|
||||
Scan(&kandangRows).Error; err != nil {
|
||||
s.Log.Errorf("Failed to fetch kandang stats for report: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
for _, row := range kandangRows {
|
||||
kandangStats[row.KandangID] = struct {
|
||||
Completed int64
|
||||
Total int64
|
||||
}{Completed: row.Completed, Total: row.Total}
|
||||
}
|
||||
|
||||
items := make([]DailyChecklistReportItem, len(rows))
|
||||
for i, row := range rows {
|
||||
key := comboKey{EmployeeID: row.EmployeeID, KandangID: row.KandangID, PhaseID: row.PhaseID}
|
||||
|
||||
activities := dailyActivityMap[key]
|
||||
if activities == nil {
|
||||
activities = map[string]int{}
|
||||
}
|
||||
|
||||
totalChecklist := 0
|
||||
for _, count := range activities {
|
||||
totalChecklist += count
|
||||
}
|
||||
|
||||
employeeStat := employeeStats[row.EmployeeID]
|
||||
abkPercentage := 0
|
||||
if employeeStat.Total > 0 {
|
||||
abkPercentage = int(math.Round(float64(employeeStat.Completed) / float64(employeeStat.Total) * 100))
|
||||
}
|
||||
|
||||
kandangStat := kandangStats[row.KandangID]
|
||||
kandangPercentage := 0
|
||||
if kandangStat.Total > 0 {
|
||||
kandangPercentage = int(math.Round(float64(kandangStat.Completed) / float64(kandangStat.Total) * 100))
|
||||
}
|
||||
|
||||
items[i] = DailyChecklistReportItem{
|
||||
AreaID: row.AreaID,
|
||||
AreaName: row.AreaName,
|
||||
LocationID: row.LocationID,
|
||||
LocationName: row.LocationName,
|
||||
KandangID: row.KandangID,
|
||||
KandangName: row.KandangName,
|
||||
EmployeeID: row.EmployeeID,
|
||||
EmployeeName: row.EmployeeName,
|
||||
PhaseName: row.PhaseName,
|
||||
DailyActivities: activities,
|
||||
Summary: DailyChecklistReportSummary{
|
||||
TotalChecklist: totalChecklist,
|
||||
JumlahHariEfektif: len(activities),
|
||||
AbkPercentage: abkPercentage,
|
||||
KandangPercentage: kandangPercentage,
|
||||
Category: DailyChecklistReportCategory{
|
||||
Kurang: 0,
|
||||
Cukup: 0,
|
||||
Baik: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user