Files
fm_be/internal/transport/http/handlers/duty_roster_presenter.go
2026-07-16 22:16:45 +07:00

314 lines
12 KiB
Go

package handlers
import (
"context"
"strings"
"time"
dutyroster "wucher/internal/domain/duty_roster"
"wucher/internal/shared/pkg/userctx"
"wucher/internal/shared/pkg/uuidv7"
responsedto "wucher/internal/transport/http/dto/response"
)
func loadRosterGroups(ctx context.Context, svc dutyroster.Service, baseID []byte, from, to time.Time, baseName, baseType string) ([]responsedto.DutyRosterBaseGroupResource, error) {
bt := normalizeRosterBaseType(baseType)
var (
headers []dutyroster.HeaderView
bases []dutyroster.BaseView
err error
)
if bt == "base" {
headers, err = svc.ListHeadersByRangeByBaseType(ctx, baseID, from, to, "base")
if err != nil {
return nil, err
}
bases, err = svc.ListBasesByType(ctx, baseID, "base")
} else {
headers, err = svc.ListHeadersByRange(ctx, baseID, from, to)
if err != nil {
return nil, err
}
bases, err = svc.ListHEMSBases(ctx, baseID)
}
if err != nil {
return nil, err
}
rosterIDs := make([][]byte, 0, len(headers))
for i := range headers {
rosterIDs = append(rosterIDs, headers[i].ID)
}
crews, err := svc.GetCrewsByRosterIDs(ctx, rosterIDs)
if err != nil {
return nil, err
}
byRoster := make(map[string][]dutyroster.CrewView)
for i := range crews {
k, _ := uuidv7.BytesToString(crews[i].RosterID)
byRoster[k] = append(byRoster[k], crews[i])
}
out := make([]responsedto.DutyRosterResource, 0, len(headers))
for i := range headers {
k, _ := uuidv7.BytesToString(headers[i].ID)
out = append(out, dutyRosterResource(ctx, headers[i], byRoster[k], bt))
}
grouped := mergeEmptyRosterGroups(groupRostersByBase(out, bt), bases, bt)
needle := strings.ToLower(strings.TrimSpace(baseName))
if needle == "" {
return grouped, nil
}
filtered := make([]responsedto.DutyRosterBaseGroupResource, 0, len(grouped))
for i := range grouped {
name := strings.ToLower(strings.TrimSpace(grouped[i].Attributes.Bases.BaseName))
if strings.Contains(name, needle) {
filtered = append(filtered, grouped[i])
}
}
return filtered, nil
}
func paginateRosterGroups(groups []responsedto.DutyRosterBaseGroupResource, start, length int) ([]responsedto.DutyRosterBaseGroupResource, int) {
total := len(groups)
if start > total {
start = total
}
end := start + length
if end > total {
end = total
}
return groups[start:end], total
}
func paginateUnifiedRosterGroups(
hems []responsedto.DutyRosterBaseGroupResource,
base []responsedto.DutyRosterBaseGroupResource,
start, length int,
) ([]responsedto.DutyRosterBaseGroupResource, []responsedto.DutyRosterBaseGroupResource, int) {
total := len(hems) + len(base)
if start > total {
start = total
}
end := start + length
if end > total {
end = total
}
if start >= end {
return []responsedto.DutyRosterBaseGroupResource{}, []responsedto.DutyRosterBaseGroupResource{}, total
}
outHEMS := []responsedto.DutyRosterBaseGroupResource{}
outBase := []responsedto.DutyRosterBaseGroupResource{}
hemsTotal := len(hems)
if start < hemsTotal {
hemsEnd := end
if hemsEnd > hemsTotal {
hemsEnd = hemsTotal
}
outHEMS = hems[start:hemsEnd]
}
baseStart := start - hemsTotal
if baseStart < 0 {
baseStart = 0
}
if end > hemsTotal && baseStart < len(base) {
baseEnd := end - hemsTotal
if baseEnd > len(base) {
baseEnd = len(base)
}
if baseStart < baseEnd {
outBase = base[baseStart:baseEnd]
}
}
return outHEMS, outBase, total
}
func dutyRosterResource(ctx context.Context, header dutyroster.HeaderView, crews []dutyroster.CrewView, baseType string) responsedto.DutyRosterResource {
bt := normalizeRosterBaseType(baseType)
details := responsedto.DutyRosterDetailsResponse{Pilot: []responsedto.DutyRosterPilotCrew{}, Doctor: []responsedto.DutyRosterCrew{}, Rescuer: []responsedto.DutyRosterCrew{}, Helper: []responsedto.DutyRosterCrew{}, OtherPerson: []responsedto.DutyRosterCrew{}}
for i := range crews {
baseItem := responsedto.DutyRosterCrew{ID: firstNonEmptyString(idString(crews[i].UserID), idString(crews[i].ID)), Name: strings.TrimSpace(crews[i].Name), Role: strings.TrimSpace(crews[i].Role), MobilePhone: strings.TrimSpace(crews[i].MobilePhone), Email: strings.TrimSpace(crews[i].Email)}
if roleNeedsShiftDateForDelete(crews[i].RoleCode) {
baseItem.ShiftDate = &responsedto.DutyRosterShiftDate{DateStart: dateToString(crews[i].DateStart), DateEnd: dateToString(crews[i].DateEnd)}
}
effectiveShiftStart := strings.TrimSpace(crews[i].ShiftStart)
effectiveShiftEnd := strings.TrimSpace(crews[i].ShiftEnd)
if effectiveShiftStart == "" {
effectiveShiftStart = strings.TrimSpace(header.ShiftStart)
}
if effectiveShiftEnd == "" {
effectiveShiftEnd = strings.TrimSpace(header.ShiftEnd)
}
switch strings.ToLower(strings.TrimSpace(crews[i].RoleCode)) {
case "pilot":
pilotItem := responsedto.DutyRosterPilotCrew{ID: baseItem.ID, Name: baseItem.Name, Role: firstNonEmptyString(baseItem.Role, "pilot"), MobilePhone: baseItem.MobilePhone, Email: baseItem.Email, ShiftDate: baseItem.ShiftDate, CrewType: strings.ToLower(strings.TrimSpace(crews[i].CrewType)), FlightInstructor: boolPtrDR(crews[i].FlightInstructor), LineChecker: boolPtrDR(crews[i].LineChecker), Supervisor: boolPtrDR(crews[i].Supervisor), Examiner: boolPtrDR(crews[i].Examiner), CoPilot: boolPtrDR(crews[i].CoPilot)}
details.Pilot = append(details.Pilot, pilotItem)
case "doctor":
if effectiveShiftStart != "" || effectiveShiftEnd != "" {
baseItem.ShiftTime = &responsedto.DutyRosterShiftTime{ShiftStart: effectiveShiftStart, ShiftEnd: effectiveShiftEnd}
}
baseItem.Role = firstNonEmptyString(baseItem.Role, "doctor")
details.Doctor = append(details.Doctor, baseItem)
case "rescuer", "air_rescuer":
if effectiveShiftStart != "" || effectiveShiftEnd != "" {
baseItem.ShiftTime = &responsedto.DutyRosterShiftTime{ShiftStart: effectiveShiftStart, ShiftEnd: effectiveShiftEnd}
}
baseItem.Role = firstNonEmptyString(baseItem.Role, "rescuer")
details.Rescuer = append(details.Rescuer, baseItem)
case "helper", "flight_assistant":
if effectiveShiftStart != "" || effectiveShiftEnd != "" {
baseItem.ShiftTime = &responsedto.DutyRosterShiftTime{ShiftStart: effectiveShiftStart, ShiftEnd: effectiveShiftEnd}
}
baseItem.Role = firstNonEmptyString(baseItem.Role, "helper")
details.Helper = append(details.Helper, baseItem)
default:
baseItem.Role = firstNonEmptyString(baseItem.Role, "other")
details.OtherPerson = append(details.OtherPerson, baseItem)
}
}
return responsedto.DutyRosterResource{Type: rosterResourceTypeFor(bt), ID: idString(header.ID), Attributes: responsedto.DutyRosterAttributes{Bases: responsedto.DutyRosterBaseInfo{ID: idString(header.BaseID), Type: bt, BaseName: strings.TrimSpace(header.BaseName), BaseAbbreviation: strings.TrimSpace(header.BaseAbbreviation)}, Helicopter: buildRosterHelicopterFromHeader(header), DutyDate: header.DutyDate.UTC().Format("2006-01-02"), ShiftTime: responsedto.DutyRosterShiftTime{ShiftStart: header.ShiftStart, ShiftEnd: header.ShiftEnd}, Detail: details, CreatedBy: uuidStringOrEmptyDR(ctx, header.CreatedBy), CreatedAt: header.CreatedAt.UTC().Format(time.RFC3339), UpdatedAt: header.UpdatedAt.UTC().Format(time.RFC3339)}}
}
func groupRostersByBase(items []responsedto.DutyRosterResource, baseType string) []responsedto.DutyRosterBaseGroupResource {
bt := normalizeRosterBaseType(baseType)
groupType := rosterGroupTypeFor(bt)
out := make([]responsedto.DutyRosterBaseGroupResource, 0)
indexByBase := map[string]int{}
for i := range items {
baseID := strings.TrimSpace(items[i].Attributes.Bases.ID)
if baseID == "" {
baseID = "unknown"
}
idx, ok := indexByBase[baseID]
if !ok {
idx = len(out)
indexByBase[baseID] = idx
out = append(out, responsedto.DutyRosterBaseGroupResource{Type: groupType, Attributes: responsedto.DutyRosterBaseGroupAttributes{Bases: items[i].Attributes.Bases, Rosters: []responsedto.DutyRosterGroupRoster{}}})
}
out[idx].Attributes.Rosters = append(out[idx].Attributes.Rosters, responsedto.DutyRosterGroupRoster{ID: items[i].ID, FlightID: items[i].Attributes.FlightID, Helicopter: items[i].Attributes.Helicopter, DutyDate: items[i].Attributes.DutyDate, ShiftTime: items[i].Attributes.ShiftTime, Detail: items[i].Attributes.Detail, CreatedBy: items[i].Attributes.CreatedBy, CreatedAt: items[i].Attributes.CreatedAt, UpdatedAt: items[i].Attributes.UpdatedAt})
}
return out
}
func mergeEmptyRosterGroups(groups []responsedto.DutyRosterBaseGroupResource, bases []dutyroster.BaseView, baseType string) []responsedto.DutyRosterBaseGroupResource {
bt := normalizeRosterBaseType(baseType)
groupType := rosterGroupTypeFor(bt)
indexByBase := make(map[string]int, len(groups))
for i := range groups {
baseID := strings.TrimSpace(groups[i].Attributes.Bases.ID)
if baseID == "" || baseID == "unknown" {
continue
}
indexByBase[baseID] = i
}
for i := range bases {
baseID := idString(bases[i].ID)
if baseID == "" {
continue
}
if _, exists := indexByBase[baseID]; exists {
continue
}
groups = append(groups, responsedto.DutyRosterBaseGroupResource{Type: groupType, Attributes: responsedto.DutyRosterBaseGroupAttributes{Bases: responsedto.DutyRosterBaseInfo{ID: baseID, Type: bt, BaseName: strings.TrimSpace(bases[i].BaseName), BaseAbbreviation: strings.TrimSpace(bases[i].BaseAbbreviation)}, Rosters: []responsedto.DutyRosterGroupRoster{}}})
}
return groups
}
func dutyRosterDeletedCrewItems(rows []dutyroster.DeletedCrewResult) []map[string]any {
out := make([]map[string]any, 0, len(rows))
for i := range rows {
out = append(out, map[string]any{"id": idString(rows[i].ID), "roster_id": idString(rows[i].RosterID), "user_id": idString(rows[i].UserID), "name": strings.TrimSpace(rows[i].Name), "role_code": strings.TrimSpace(rows[i].RoleCode), "crew_type": strings.TrimSpace(rows[i].CrewType), "date_start": dateToString(rows[i].DateStart), "date_end": dateToString(rows[i].DateEnd)})
}
return out
}
func dutyRosterCrewAuditMetadata(rosterID []byte, baseType string, crews []dutyroster.DutyRosterCrew) map[string]any {
roles := make([]string, 0, len(crews))
for i := range crews {
roles = append(roles, roleBucket(crews[i].RoleCode))
}
return map[string]any{"roster_id": idString(rosterID), "base_type": strings.ToLower(strings.TrimSpace(baseType)), "roles": roles, "crew_count": len(crews)}
}
func buildRosterHelicopterFromHeader(header dutyroster.HeaderView) responsedto.DutyRosterHelicopterSummary {
return responsedto.DutyRosterHelicopterSummary{ID: idString(header.HelicopterID), Designation: strings.TrimSpace(header.HelicopterDesignation), Identifier: strings.TrimSpace(header.HelicopterIdentifier), Type: strings.TrimSpace(header.HelicopterType)}
}
func uuidStringOrEmptyDR(ctx context.Context, id []byte) string {
if len(id) == 0 {
return ""
}
if name := getLastNameFromPresenterGetter(ctx, id); name != "" {
return name
}
return idString(id)
}
func getLastNameFromPresenterGetter(ctx context.Context, id []byte) string {
if name := userctx.GetDisplayName(ctx, id); name != "" {
return name
}
return ""
}
func firstNonEmptyString(a, b string) string {
if strings.TrimSpace(a) != "" {
return strings.TrimSpace(a)
}
return strings.TrimSpace(b)
}
func roleBucket(code string) string {
switch strings.ToLower(strings.TrimSpace(code)) {
case "pilot":
return "pilot"
case "doctor":
return "doctor"
case "rescuer", "air_rescuer":
return "rescuer"
case "helper", "flight_assistant":
return "helper"
default:
return "other_person"
}
}
func roleNeedsShiftDateForDelete(roleCode string) bool {
switch strings.ToLower(strings.TrimSpace(roleCode)) {
case "other_person", "other":
return false
default:
return true
}
}
func boolPtrDR(v bool) *bool {
return &v
}
func normalizeRosterBaseType(baseType string) string {
if strings.EqualFold(strings.TrimSpace(baseType), "base") {
return "base"
}
return "hems"
}
func rosterResourceTypeFor(baseType string) string {
if normalizeRosterBaseType(baseType) == "base" {
return "base_duty_roster"
}
return "duty_roster"
}
func rosterGroupTypeFor(baseType string) string {
if normalizeRosterBaseType(baseType) == "base" {
return "base_roster_group"
}
return "hems_base_roster_group"
}