314 lines
12 KiB
Go
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"
|
|
}
|